Force agents to act so weak models can't complete a no-op

The whole premise is cheap models + skills + a team — so the control plane,
not the model, must guarantee action. Every adapter hardcoded
tool_choice:"auto", letting a weak model answer the kickoff with prose and
"succeed" having hired no one, delegated nothing, and said nothing.

Now the agent loop compels a tool call on the opening turn (tool_choice
plumbed through every backend: OpenAI/Responses/Chat, Anthropic, Codex,
Copilot, Azure, Custom, Gemini ANY mode, Bedrock any). If the forced turn
still yields no tool call, the run fails loudly instead of recording a
deceptive success — surfacing the problem (let it crash) rather than a
silent dead company. After it has acted, the model chooses freely.

Tested: a stub that rambles on "auto" but acts when forced now posts a
kickoff message; a prose-only first turn fails the run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.8 committed 2026-06-10 21:43:49 -04:00
1 parent fa01aa682f
commit ef9ee463c9
9 files changed
+107 -35

No files matched your search

@@ -46,15 +46,19 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn
instructions: input.instructions,
messages,
tools: AGENT_TOOLS,
// Compel a concrete action on the opening turn so even a weak model
// cannot answer with prose and quietly finish a no-op; once it has acted,
// let it choose freely (continue working, or stop when done).
toolChoice: iteration === 1 ? "required" : "auto",
});
lastRequestId = turn.requestId;
if (turn.toolCalls.length === 0) {
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.");
if (iteration === 1) {
// The opening turn was forced to call a tool. None means the agent took
// no action at all — fail loudly instead of recording a deceptive
// "success" that hired no one, delegated nothing, and said nothing.
throw new Error("Agent took no action on its first turn — the model returned no tool calls.");
}
return { finalText: turn.text, iterations: iteration, toolCallCount, lastRequestId };
}