Skip to content

fix(agent): stop tool loop on unresolved manual tool calls instead of sending broken history#59

Merged
ayush-or merged 1 commit into
mainfrom
devin/1783526849-unresolved-manual-tool-calls
Jul 8, 2026
Merged

fix(agent): stop tool loop on unresolved manual tool calls instead of sending broken history#59
ayush-or merged 1 commit into
mainfrom
devin/1783526849-unresolved-manual-tool-calls

Conversation

@ayush-or

@ayush-or ayush-or commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

When a tool round mixes auto-executed tools with manual (no execute fn) client tools, executeToolRound produces no output for the manual calls, but the loop in ModelResult still called makeFollowupRequest — sending a history containing the manual function_call with no matching function_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:

const resolvedCallIds = new Set(toolResults.map((r) => r.callId));
const hasUnresolvedToolCalls = currentToolCalls.some((tc) => !resolvedCallIds.has(tc.id));
if (hasUnresolvedToolCalls) {
  break;
}

Break-and-surface (rather than stub outputs à la the stopWhen tail) mirrors the existing all-manual hasExecutableToolCalls guards: the caller receives the response, executes its manual calls, and continues the conversation. Also covers calls to tool names not present in options.tools at 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.5 via OpenRouter /api/v1/responses died 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

  1. Codex sends web_search + image_generation (OpenRouter server tools) plus its own client function tools (exec_command, ...). image_generation isn't native-eligible, so the router takes server-tools:native-fallback-to-sdk → SDK interception via @openrouter/agent's callModel loop.
  2. In that loop, Codex's client tools are registered without execute ("manual") — the SDK cannot run them.
  3. The inner model emits a web_search call and an exec_command call in the same round. executeToolRound executes web_search but returns nothing for exec_command (not HITL → not in pausedCalls either).
  4. The loop then unconditionally called makeFollowupRequest, whose input is [...history, ...model output, ...toolResults] — carrying exec_command's function_call with no matching function_call_output.
  5. OpenAI returns 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).
  6. The consumer's event mapping masks non-auth upstream errors, so the client only ever sees Server tool request failed via response.failed.

With the guard, step 4 never happens: the loop breaks and surfaces the response, the client executes exec_command itself 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)

# Failure time (PT) Parent generation ID
1 Jul 7 12:14:19 gen-1783440842-oCj0vbNDTO82qAeFaQUA
2 Jul 7 12:14:35 gen-1783440863-kiiI3dwzhddJ7enZtlkx
3 Jul 7 17:51:36 gen-1783461081-ET5BsGwoQavwKO33pNTA
4 Jul 7 17:53:45 gen-1783461211-IJNonTceUZYItOSyhBLn
5 Jul 7 17:55:02 gen-1783461289-WR6Ep2xF8So0Rwu77YFf
6 Jul 7 18:15:20 gen-1783462506-ul0WMJ1OoWBB33XotnuT → nested gen-1783462519-SyRqzI4Kl8OXr9OySeYp, missing output for call_icUiQZXCN0xpcRIW4UEDB9hM
7 Jul 7 18:41:24 gen-1783464070-Ja08WSZX0sBCRwkG0UjR
8 Jul 8 09:25:55 gen-1783517129-njmEZGWFW2nwn0EFJKpd
9 Jul 8 09:28:56 gen-1783517312-DuEXxgR9A0Lh6JTYzBCs

Ruled out as causes: Codex's /models decode error (missing field 'models') and a dead local qmd MCP server — both present in logs but uncorrelated with the failures.

Datadog signal to watch post-deploy: server-tools:upstream-error with raw_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

… 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.
@ayush-or ayush-or self-assigned this Jul 8, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor
Original prompt from ayush.patel

https://app.graphite.com/github/pr/OpenRouterTeam/openrouter-web/27541

explain why the diff here is a patch and what should it be context

❯ can you create a PR against main and create a PR in this format?: TL;DR Lift the OpenAI Responses request b…

  • Branch pushed: codex/server-tools-missing-tool-output → main, containing the @openrouter/agent@0.7.2 patch, the patchedDependencies wiring, and the DI-based regression test.
  • PR body follows your format: TL;DR → 📈 Live monitoring (the Datadog server-tools:upstream-error / "No tool output found for function call" signature to watch drop to zero) → What changed? → Why? → Reviewer focus → collapsed Additional details with the RCA trail, all 9 gen IDs, verification results, and the checklist → "Requested by: @ayush-or".
  • One deviation from the template: no Devin session link, since this fix came out of this session rather than a Devin run — happy to add one if you want to link the Datadog-RCA session there.
  • Heads-up: your gh shell wrapper reroutes gh pr create to Graphite's gt submit, so I used command gh pr create directly. If you track this branch in Graphite you may want to run gt track on it.

The main thing to flag for reviewers (also in the PR): this patches the published npm package as a stopgap — the same ~10-line guard should be ported into the @openrouter/agent source repo, and the patch file will need carrying forward if the SDK version bumps before that lands.

✻ Cogitated for 2m 11s

❯ patches/@openrouter%2Fagent@0.7.2.patch can you exchange why the change was made here, i guess i dont rlly get the diff[Image #8], what actually changed
[Image #8]

Thought for 18s

The confusion is because you're looking at a diff of a diff. The file patches/@openrouter%2Fagent@0.7.2.patch is itself a patch file (its content is a diff), and since the whole file is newly added to the repo, GitHub shows every line of it with a leading +. So you get two columns of markers:

  • The outer + (leftmost, on every line) just means... (979 chars truncated...)

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@perry-the-pr-reviewer perry-the-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)*

@ayush-or ayush-or merged commit 8edae63 into main Jul 8, 2026
5 checks passed
@ayush-or ayush-or mentioned this pull request Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant