Skip to content

Approved HITL tool's execution result missing from history — "tool_use ids were found without tool_result blocks" #460

Description

@edoardorinaldi

eve version

eve@0.17.1

Node.js version

v24.18.0

Where does the bug occur?

  • Production (eve start / deployed, on Vercel)

Describe the bug

An approval-gated (approval: once()) authored tool with a real execute sometimes has its side effect run successfully, but the resulting tool_result never makes it into session history before the harness sends the next model call — leaving that tool_use id dangling. The next request to Anthropic is rejected with:

messages.10: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01KM8aeG4pT8SNYub2Cbgj4Q. Each `tool_use` block must have a corresponding `tool_result` block in the next message.

This is not a one-off: it reproduced twice independently, in the same app, on two different turns of two different conversations, always with the same shape:

  1. The model calls an approval-gated tool (hubspot_create_object, approval: once(), with a real async execute that PATCHes/POSTs to an external API).
  2. The user approves the tool call (Responded: Yes in the UI).
  3. The tool's real side effect completes — confirmed both times by checking the external system directly (a HubSpot company record was created both times, with the exact properties from the approved call's input).
  4. A second call to the same tool name (a different object, e.g. a HubSpot deal or contact — auto-approved via once()'s "already approved this session" logic) completes and renders normally in the UI.
  5. The next turn's model call to Anthropic 400s with the error above, naming the first, human-approved call's tool_use id as the one missing a tool_result. From that point on, every subsequent message in the session fails identically, since eve keeps resending the same corrupted history.

I instrumented nothing in the built output for this one — this is from production usage of a Next.js app using @eve/react's chat hook against eve's session/stream API. I confirmed there's no custom code in the app that constructs or replays the Anthropic message array; the app only renders/forwards eve's own session/stream events, so the corrupted history is assembled entirely inside eve's harness.

I traced through node_modules/eve/dist/src/harness/{tool-loop,input-requests,input-extraction}.js and node_modules/ai/dist/index.js (ai@7.0.4) trying to find the exact defect:

  • Ruled out: the tool's execute throwing (it didn't — the HubSpot property-name validation the tool does locally didn't reject either payload).
  • Ruled out: an approvalKey collision between the two same-tool-name calls causing them to be matched to the wrong approval response — each tool_use gets an independent approval request keyed by toolCallId/approvalId in the AI SDK (resolveToolApproval, collectToolApprovals), and eve's approvalKey only affects what gets recorded into the session's approvedTools set for future once() checks, not response matching.
  • Still open: something in the approval-resume path (resolvePendingInput in input-requests.js, and/or handleStepResult in tool-loop.js) appears to let the harness proceed to build the next model request before the approved tool's actual (async, network-bound) execution result is durably appended to session history — i.e. the tool call resolves and its side effect fires, but the harness doesn't wait for/persist that specific tool_result before continuing the loop.

This looks related to the general fragility around HITL approval-resume + multi-round tool history reconstruction already visible in #203 (duplicate tool_result for client-resolved tools on resume) and #236 (approve-resume crash on OpenAI: "No tool output found for function call"), but distinct from both — here the tool is a normal execute-based approval-gated tool (not client-resolved), and the failure is a missing result rather than a duplicate one.

Steps to reproduce

  1. npx eve@latest init repro
  2. agent/agent.ts: export default defineAgent({ model: anthropic("claude-sonnet-4-6") }) (direct @ai-sdk/anthropic provider, not the AI Gateway model-reference string).
  3. Add an authored tool with approval: once() and a real async execute that does non-trivial I/O (e.g. an await fetch(...) to an external API — the delay seems relevant, since a fast/instant execute may not reproduce it).
  4. Give the agent a skill/instruction that calls this tool twice with different inputs in one user turn (e.g. "create object A, then create object B" using the same tool).
  5. Approve the first (human-gated) call when prompted.
  6. Watch the second, auto-approved (once()-satisfied) call complete, then send any further message in the same session.
  7. The next model call to Anthropic 400s with tool_use ids were found without tool_result blocks immediately after: <id>, where <id> is the first, human-approved call's id — even though its side effect actually ran.

Expected behavior

After a human approves a HITL-gated tool call, eve should durably persist that specific tool call's tool_result into session history before advancing the turn/loop, so every tool_use block always has a matching tool_result in the next Anthropic request — regardless of how long the approved tool's execute takes to resolve, and regardless of whether other tool calls in the same turn complete first.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingp1tools

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions