fix(agent): stop tool loop on unresolved manual tool calls instead of sending broken history#59
Conversation
… sending broken history When a round mixes auto-executed tools with manual (execute-less) client tools, executeToolRound returns no output for the manual calls, yet the loop still made a follow-up request whose input carried the manual function_call with no matching function_call_output. Providers reject that history with 400 invalid_request_error "No tool output found for function call ...". Break the loop and surface the response when any tool call in the round has no output — mirroring the existing all-manual hasExecutableToolCalls guard — so the caller can execute its manual calls and continue. Ports the openrouter-web patchedDependencies stopgap (OpenRouterTeam/openrouter-web#27541) upstream.
Original prompt from ayush.patel
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
✅ LGTM — clean fix with good test coverage and clear comments.\n\n- Logic correctly prevents sending broken follow-up history with orphaned items.\n- Regression test covers the mixed auto+manual round and the all-auto happy path.\n- CI all green.\n\n*(Note: maintainer token not available on this repo, so posting as COMMENT instead of APPROVE.)*
Summary
When a tool round mixes auto-executed tools with manual (no
executefn) client tools,executeToolRoundproduces no output for the manual calls, but the loop inModelResultstill calledmakeFollowupRequest— sending a history containing the manualfunction_callwith no matchingfunction_call_output, which providers deterministically reject with a 400 "No tool output found for function call ...".New guard in the main execution loop, right after the HITL-pause check:
Break-and-surface (rather than stub outputs à la the
stopWhentail) mirrors the existing all-manualhasExecutableToolCallsguards: the caller receives the response, executes its manual calls, and continues the conversation. Also covers calls to tool names not present inoptions.toolsat all.Includes a regression test (
mixed-manual-tool-round.test.ts) that is red on the current loop (poisoned follow-up sent) and green with the guard, plus an all-auto case verifying the loop still continues normally. Changeset: patch bump.Production incident this fixes
Symptom: Codex Desktop turns on
openai/gpt-5.5via OpenRouter/api/v1/responsesdied mid-turn with the generic error "Server tool request failed" — 9 failed turns across Jul 7–8, each needing a manual "continue" to resume. Failures always landed seconds after web-search activity in long, multi-tool turns.E2E flow that triggers the bug
web_search+image_generation(OpenRouter server tools) plus its own client function tools (exec_command, ...).image_generationisn't native-eligible, so the router takesserver-tools:native-fallback-to-sdk→ SDK interception via@openrouter/agent'scallModelloop.execute("manual") — the SDK cannot run them.web_searchcall and anexec_commandcall in the same round.executeToolRoundexecutesweb_searchbut returns nothing forexec_command(not HITL → not inpausedCallseither).makeFollowupRequest, whose input is[...history, ...model output, ...toolResults]— carryingexec_command'sfunction_callwith no matchingfunction_call_output.400 invalid_request_error, param: "input" — "No tool output found for function call call_XXX."The Azure failover returns the identical 400 (deterministic, so retries can't help).Server tool request failedviaresponse.failed.With the guard, step 4 never happens: the loop breaks and surfaces the response, the client executes
exec_commanditself and sends the output back, and the conversation continues — the same contract the loop already honors when all calls in a round are manual.Failing generation IDs (all 9, same nested-400 signature)
gen-1783440842-oCj0vbNDTO82qAeFaQUAgen-1783440863-kiiI3dwzhddJ7enZtlkxgen-1783461081-ET5BsGwoQavwKO33pNTAgen-1783461211-IJNonTceUZYItOSyhBLngen-1783461289-WR6Ep2xF8So0Rwu77YFfgen-1783462506-ul0WMJ1OoWBB33XotnuT→ nestedgen-1783462519-SyRqzI4Kl8OXr9OySeYp, missing output forcall_icUiQZXCN0xpcRIW4UEDB9hMgen-1783464070-Ja08WSZX0sBCRwkG0UjRgen-1783517129-njmEZGWFW2nwn0EFJKpdgen-1783517312-DuEXxgR9A0Lh6JTYzBCsRuled out as causes: Codex's
/modelsdecode error (missing field 'models') and a dead localqmdMCP server — both present in logs but uncorrelated with the failures.Datadog signal to watch post-deploy:
server-tools:upstream-errorwithraw_message: "No tool output found for function call ..."should drop to zero.Link to Devin session: https://openrouter.devinenterprise.com/sessions/8f73f70aa85546efaf30d8c21472357f
Requested by: @ayush-or