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:
- The model calls an approval-gated tool (
hubspot_create_object, approval: once(), with a real async execute that PATCHes/POSTs to an external API).
- The user approves the tool call (
Responded: Yes in the UI).
- 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).
- 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.
- 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
npx eve@latest init repro
agent/agent.ts: export default defineAgent({ model: anthropic("claude-sonnet-4-6") }) (direct @ai-sdk/anthropic provider, not the AI Gateway model-reference string).
- 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).
- 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).
- Approve the first (human-gated) call when prompted.
- Watch the second, auto-approved (
once()-satisfied) call complete, then send any further message in the same session.
- 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.
eve version
eve@0.17.1Node.js version
v24.18.0Where does the bug occur?
eve start/ deployed, on Vercel)Describe the bug
An approval-gated (
approval: once()) authored tool with a realexecutesometimes has its side effect run successfully, but the resultingtool_resultnever makes it into session history before the harness sends the next model call — leaving thattool_useid dangling. The next request to Anthropic is rejected with: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:
hubspot_create_object,approval: once(), with a real asyncexecutethat PATCHes/POSTs to an external API).Responded: Yesin the UI).once()'s "already approved this session" logic) completes and renders normally in the UI.tool_useid as the one missing atool_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}.jsandnode_modules/ai/dist/index.js(ai@7.0.4) trying to find the exact defect:executethrowing (it didn't — the HubSpot property-name validation the tool does locally didn't reject either payload).approvalKeycollision between the two same-tool-name calls causing them to be matched to the wrong approval response — eachtool_usegets an independent approval request keyed bytoolCallId/approvalIdin the AI SDK (resolveToolApproval,collectToolApprovals), and eve'sapprovalKeyonly affects what gets recorded into the session'sapprovedToolsset for futureonce()checks, not response matching.resolvePendingInputininput-requests.js, and/orhandleStepResultintool-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 specifictool_resultbefore 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_resultfor 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 normalexecute-based approval-gated tool (not client-resolved), and the failure is a missing result rather than a duplicate one.Steps to reproduce
npx eve@latest init reproagent/agent.ts:export default defineAgent({ model: anthropic("claude-sonnet-4-6") })(direct@ai-sdk/anthropicprovider, not the AI Gateway model-reference string).approval: once()and a real asyncexecutethat does non-trivial I/O (e.g. anawait fetch(...)to an external API — the delay seems relevant, since a fast/instantexecutemay not reproduce it).once()-satisfied) call complete, then send any further message in the same session.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_resultinto session history before advancing the turn/loop, so everytool_useblock always has a matchingtool_resultin the next Anthropic request — regardless of how long the approved tool'sexecutetakes to resolve, and regardless of whether other tool calls in the same turn complete first.