Revert tool-choice forcing — the Codex regression was strict:null
The GPT-account (Codex) path supports tool calls and worked before; my forced tool_choice broke it and was the wrong fix. The real regression was strict:null in the rebuild's tool definitions, which made the endpoint drop the tool list so the model had nothing to call — a "Done" run that hired no one. Forcing then turned that silent failure into a red one. Restore the original "auto" loop (no forced tool_choice, no finish tool) and keep only the one-line strict:null omission. Codex now receives valid tools on the working "auto" choice, as in the version that worked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
74422da4f8
commit
97242ae684
13 files changed
+57
-160
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { ProviderService } from "./provider-service";
|
||||
import { AGENT_TOOLS, dispatchAgentTool, FINISH_TOOL, type AgentToolContext, type AgentToolDeps } from "./agent-tools";
|
||||
import { AGENT_TOOLS, dispatchAgentTool, type AgentToolContext, type AgentToolDeps } from "./agent-tools";
|
||||
import type { AgentTurnMessage } from "./provider-tool-turn";
|
||||
|
||||
const MAX_ITERATIONS = 12;
|
||||
@@ -46,37 +46,26 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
|
||||
instructions: input.instructions,
|
||||
messages,
|
||||
tools: AGENT_TOOLS,
|
||||
// Compel a tool call on every turn so a weak model cannot answer with
|
||||
// prose and stop after a token gesture. The agent ends deliberately by
|
||||
// calling `finish` — only after it has actually done the work.
|
||||
toolChoice: "required",
|
||||
});
|
||||
lastRequestId = turn.requestId;
|
||||
|
||||
if (turn.toolCalls.length === 0) {
|
||||
// The provider ignored the forced tool choice; treat it as no action.
|
||||
throw new Error("Agent returned no tool call on a forced turn.");
|
||||
}
|
||||
|
||||
const wantsFinish = turn.toolCalls.some((call) => call.name === FINISH_TOOL);
|
||||
const actionable = turn.toolCalls.filter((call) => call.name !== FINISH_TOOL);
|
||||
if (actionable.length > 0) {
|
||||
messages.push({ role: "assistant", text: turn.text, toolCalls: actionable });
|
||||
const results = actionable.map((call) => {
|
||||
toolCallCount += 1;
|
||||
return { toolCallId: call.id, toolName: call.name, output: dispatchAgentTool(deps.tools, input.toolContext, call) };
|
||||
});
|
||||
messages.push({ role: "tool", results });
|
||||
deps.onToolResults?.();
|
||||
}
|
||||
|
||||
if (wantsFinish) {
|
||||
if (toolCallCount === 0) {
|
||||
// Finishing before doing anything is the silent no-op we must reject.
|
||||
throw new Error("Agent finished without taking any action.");
|
||||
if (toolCallCount === 0 && !turn.text.trim()) {
|
||||
// A first turn with neither text nor tool calls means the provider
|
||||
// produced nothing actionable (e.g. output truncated mid-reasoning);
|
||||
// completing the run would silently mark a no-op as done.
|
||||
throw new Error("Provider returned an empty turn: no text and no tool calls.");
|
||||
}
|
||||
return { finalText: turn.text, iterations: iteration, toolCallCount, lastRequestId };
|
||||
}
|
||||
|
||||
messages.push({ role: "assistant", text: turn.text, toolCalls: turn.toolCalls });
|
||||
const results = turn.toolCalls.map((call) => {
|
||||
toolCallCount += 1;
|
||||
return { toolCallId: call.id, toolName: call.name, output: dispatchAgentTool(deps.tools, input.toolContext, call) };
|
||||
});
|
||||
messages.push({ role: "tool", results });
|
||||
deps.onToolResults?.();
|
||||
}
|
||||
|
||||
throw new Error(`Agent run did not converge within ${MAX_ITERATIONS} iterations.`);
|
||||
|
||||
Reference in new issue
Block a user