v2.3.0-sampling-params-ask-user: agent sampling params, ask_user_input in CoderPane, UX polish

Add top_p/top_k/min_p/presence_penalty to AGENTS.md frontmatter and thread
through inference (agents.ts parser → Agent type → stream-phase → sentinel
summaries). Null means omit from request body, preserving provider defaults.

Wire ask_user_input interactive card into both BooCoder frontends: the
CoderPane in BooChat's SPA (CoderMessageList now renders AskUserInputCard
instead of ToolCallLine for ask_user_input tool calls) and the standalone
coder SPA (MessageBubble + new AskUserInputCard + shadcn ui primitives).

Additional fixes: SessionLandingPage uses ChatInput with slash-command
support and lazy chat creation; Session.tsx hydrate-race fix for empty pane
promotion; AgentPicker wider dropdown with line-clamp; ModelPicker min-width;
Textarea converted to forwardRef; Recon agent added to AGENTS.md; codecontext
host port exposed in docker-compose.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 21:02:21 +00:00
parent 31e1b32be1
commit 792bbb9da3
21 changed files with 721 additions and 79 deletions

View File

@@ -83,6 +83,10 @@ export function slugify(name: string): string {
interface ParsedFrontmatter {
temperature?: number;
top_p?: number;
top_k?: number;
min_p?: number;
presence_penalty?: number;
tools?: string[];
description?: string;
model?: string;
@@ -132,6 +136,46 @@ function parseFrontmatter(yaml: string): { data: ParsedFrontmatter; errors: stri
const n = Number(valueRaw);
if (Number.isFinite(n)) data.temperature = n;
else errors.push(`temperature must be a number (got "${valueRaw}")`);
} else if (key === 'top_p') {
const n = Number(valueRaw);
if (Number.isFinite(n)) {
data.top_p = n;
if (n < 0 || n > 1) {
console.warn(`agents: top_p ${n} out of range 0-1, ignoring (falling back to default)`);
}
} else {
errors.push(`top_p must be a number (got "${valueRaw}")`);
}
} else if (key === 'top_k') {
const n = Number(valueRaw);
if (Number.isInteger(n)) {
data.top_k = n;
if (n < 0 || n > 200) {
console.warn(`agents: top_k ${n} out of range 0-200, ignoring (falling back to default)`);
}
} else {
errors.push(`top_k must be an integer (got "${valueRaw}")`);
}
} else if (key === 'min_p') {
const n = Number(valueRaw);
if (Number.isFinite(n)) {
data.min_p = n;
if (n < 0 || n > 1) {
console.warn(`agents: min_p ${n} out of range 0-1, ignoring (falling back to default)`);
}
} else {
errors.push(`min_p must be a number (got "${valueRaw}")`);
}
} else if (key === 'presence_penalty') {
const n = Number(valueRaw);
if (Number.isFinite(n)) {
data.presence_penalty = n;
if (n < -2 || n > 2) {
console.warn(`agents: presence_penalty ${n} out of range -2-2, ignoring (falling back to default)`);
}
} else {
errors.push(`presence_penalty must be a number (got "${valueRaw}")`);
}
} else if (key === 'tools') {
if (valueRaw === '') {
data.tools = [];
@@ -276,6 +320,10 @@ function parseAgentSection(section: RawSection): Omit<Agent, 'source'> {
description: fm.description ?? '',
system_prompt: systemPrompt,
temperature: typeof fm.temperature === 'number' ? fm.temperature : DEFAULT_TEMPERATURE,
top_p: typeof fm.top_p === 'number' ? fm.top_p : null,
top_k: typeof fm.top_k === 'number' ? fm.top_k : null,
min_p: typeof fm.min_p === 'number' ? fm.min_p : null,
presence_penalty: typeof fm.presence_penalty === 'number' ? fm.presence_penalty : null,
tools: filteredTools,
model: typeof fm.model === 'string' && fm.model.length > 0 ? fm.model : null,
max_tool_calls: typeof fm.max_tool_calls === 'number' ? fm.max_tool_calls : null,

View File

@@ -86,7 +86,7 @@ export async function runCapHitSummary(
ctx,
session.model,
messages,
{ tools: null, temperature: agent?.temperature },
{ tools: null, temperature: agent?.temperature, top_p: agent?.top_p ?? undefined, top_k: agent?.top_k ?? undefined, min_p: agent?.min_p ?? undefined, presence_penalty: agent?.presence_penalty ?? undefined },
(delta) => {
accumulated += delta;
ctx.publish(sessionId, {
@@ -346,7 +346,7 @@ export async function runDoomLoopSummary(
ctx,
session.model,
messages,
{ tools: null, temperature: agent?.temperature },
{ tools: null, temperature: agent?.temperature, top_p: agent?.top_p ?? undefined, top_k: agent?.top_k ?? undefined, min_p: agent?.min_p ?? undefined, presence_penalty: agent?.presence_penalty ?? undefined },
(delta) => {
accumulated += delta;
ctx.publish(sessionId, {
@@ -545,7 +545,7 @@ export async function runStepCapSummary(
ctx,
session.model,
messages,
{ tools: null, temperature: agent?.temperature },
{ tools: null, temperature: agent?.temperature, top_p: agent?.top_p ?? undefined, top_k: agent?.top_k ?? undefined, min_p: agent?.min_p ?? undefined, presence_penalty: agent?.presence_penalty ?? undefined },
(delta) => {
accumulated += delta;
ctx.publish(sessionId, {

View File

@@ -31,6 +31,10 @@ interface StreamOptions {
// (rare; we still omit from the request body to avoid OpenAI 400).
tools: ToolJsonSchema[] | null;
temperature?: number;
top_p?: number | null;
top_k?: number | null;
min_p?: number | null;
presence_penalty?: number | null;
}
// v1.13.1-A: convert BooCode's OpenAI-shaped history into AI SDK
@@ -199,6 +203,9 @@ export async function streamCompletion(
? { tools: aiTools, toolChoice: 'auto' as const, experimental_repairToolCall: repairToolCall }
: {}),
...(typeof opts.temperature === 'number' ? { temperature: opts.temperature } : {}),
...(typeof opts.top_p === 'number' ? { topP: opts.top_p } : {}),
...(typeof opts.top_k === 'number' ? { topK: opts.top_k } : {}),
...(typeof opts.presence_penalty === 'number' ? { presencePenalty: opts.presence_penalty } : {}),
abortSignal: signal,
});
@@ -388,6 +395,10 @@ export async function executeStreamPhase(
: toolJsonSchemas()
).filter((t) => webToolsEnabled || !WEB_TOOL_NAMES.has(t.function.name));
const effectiveTemperature = agent?.temperature;
const effectiveTopP = agent?.top_p ?? undefined;
const effectiveTopK = agent?.top_k ?? undefined;
const effectiveMinP = agent?.min_p ?? undefined;
const effectivePresencePenalty = agent?.presence_penalty ?? undefined;
// v1.12.2: ctx_max lookup is cached after the first hit per model, so this
// is a Map probe in steady state. We capture nCtx once at the top of the
@@ -425,7 +436,7 @@ export async function executeStreamPhase(
ctx,
session.model,
messages,
{ tools: effectiveTools, temperature: effectiveTemperature },
{ tools: effectiveTools, temperature: effectiveTemperature, top_p: effectiveTopP, top_k: effectiveTopK, min_p: effectiveMinP, presence_penalty: effectivePresencePenalty },
(delta) => {
state.accumulated += delta;
ctx.publish(sessionId, {