fix(builtin): disable tools during continue/retry to force frontmatter output

Agent was using all continue turns to keep calling tools instead of
outputting the required frontmatter. Now continue runs with noTools=true,
forcing LLM to emit text-only response.

Also supports null tools in chatCompletionWithTools to omit tools from
the API request entirely.
This commit is contained in:
2026-05-23 21:36:22 +08:00
parent 3d1850ddbe
commit edb979baa9
4 changed files with 24 additions and 10 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"check": "bunx tsc --build && biome check . && bash scripts/lint-log-tags.sh", "check": "bunx tsc --build && biome check . && bash scripts/lint-log-tags.sh",
"typecheck": "bunx tsc --build", "typecheck": "bunx tsc --build",
"format": "biome format --write .", "format": "biome format --write .",
"test": "bun run --filter '*' test", "test": "bun run --filter './packages/*' test",
"changeset": "bunx changeset", "changeset": "bunx changeset",
"version": "bunx changeset version", "version": "bunx changeset version",
"release": "bun run build && bun test && node scripts/publish-all.mjs" "release": "bun run build && bun test && node scripts/publish-all.mjs"
@@ -66,6 +66,7 @@ async function runBuiltinWithMessages(
session: SessionRecord, session: SessionRecord,
store: Store, store: Store,
maxTurns: number, maxTurns: number,
noTools: boolean,
): Promise<AgentRunResult> { ): Promise<AgentRunResult> {
const loopResult = await runBuiltinLoop({ const loopResult = await runBuiltinLoop({
provider, provider,
@@ -74,6 +75,7 @@ async function runBuiltinWithMessages(
maxTurns, maxTurns,
storageRoot, storageRoot,
sessionId: session.sessionId, sessionId: session.sessionId,
noTools,
}); });
session.messages = loopResult.messages; session.messages = loopResult.messages;
@@ -119,6 +121,7 @@ async function runBuiltin(ctx: AgentContext): Promise<AgentRunResult> {
session, session,
ctx.store, ctx.store,
BUILTIN_MAX_TURNS, BUILTIN_MAX_TURNS,
false,
); );
} }
@@ -141,6 +144,7 @@ async function continueBuiltin(
session, session,
store, store,
BUILTIN_CONTINUE_MAX_TURNS, BUILTIN_CONTINUE_MAX_TURNS,
true,
); );
} }
+11 -7
View File
@@ -96,8 +96,17 @@ function serializeMessage(message: ChatMessage): Record<string, unknown> {
export async function chatCompletionWithTools( export async function chatCompletionWithTools(
provider: ResolvedLlmProvider, provider: ResolvedLlmProvider,
messages: ChatMessage[], messages: ChatMessage[],
tools: OpenAiToolDefinition[], tools: OpenAiToolDefinition[] | null,
): Promise<LlmAssistantResponse> { ): Promise<LlmAssistantResponse> {
const body: Record<string, unknown> = {
model: provider.model,
messages: messages.map(serializeMessage),
};
if (tools !== null && tools.length > 0) {
body.tools = tools;
body.tool_choice = "auto";
}
let response: Response; let response: Response;
try { try {
response = await fetch(chatUrl(provider.baseUrl), { response = await fetch(chatUrl(provider.baseUrl), {
@@ -106,12 +115,7 @@ export async function chatCompletionWithTools(
Authorization: `Bearer ${provider.apiKey}`, Authorization: `Bearer ${provider.apiKey}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify(body),
model: provider.model,
messages: messages.map(serializeMessage),
tools,
tool_choice: "auto",
}),
}); });
} catch (cause) { } catch (cause) {
const message = cause instanceof Error ? cause.message : String(cause); const message = cause instanceof Error ? cause.message : String(cause);
+8 -2
View File
@@ -23,6 +23,8 @@ export type RunBuiltinLoopOptions = {
maxTurns: number; maxTurns: number;
storageRoot: string; storageRoot: string;
sessionId: string; sessionId: string;
/** When true, do not provide tools — force LLM to emit text only. */
noTools: boolean;
}; };
export type RunBuiltinLoopResult = { export type RunBuiltinLoopResult = {
@@ -73,13 +75,17 @@ export async function runBuiltinLoop(
options: RunBuiltinLoopOptions, options: RunBuiltinLoopOptions,
): Promise<RunBuiltinLoopResult> { ): Promise<RunBuiltinLoopResult> {
const messages = [...options.messages]; const messages = [...options.messages];
const openAiTools = builtinToolsToOpenAi(getBuiltinTools()); const openAiTools = options.noTools ? [] : builtinToolsToOpenAi(getBuiltinTools());
let finalText = ""; let finalText = "";
let turnCount = 0; let turnCount = 0;
for (let turn = 0; turn < options.maxTurns; turn++) { for (let turn = 0; turn < options.maxTurns; turn++) {
log("8K2M4N7P", `builtin loop turn ${turn + 1}/${options.maxTurns}`); log("8K2M4N7P", `builtin loop turn ${turn + 1}/${options.maxTurns}`);
const response = await chatCompletionWithTools(options.provider, messages, openAiTools); const response = await chatCompletionWithTools(
options.provider,
messages,
openAiTools.length > 0 ? openAiTools : null,
);
const assistantMessage: ChatMessage = { const assistantMessage: ChatMessage = {
role: "assistant", role: "assistant",