Skip to content

needsApproval on an authored (local) executable tool crashes on approve-resume with the OpenAI Responses provider: No tool output found for function call #236

Description

@ya5huk

Summary

An authored, executable tool with needsApproval: always() parks correctly for approval (input.requestedsession.waiting), but on the resume turn after the user approves, the next model call is rejected by the OpenAI Responses API:

AI_APICallError: No tool output found for function call call_<id>

The approved tool's execute() never runs (the durable step stays waiting_approval), and turn.failedsession.failed. So an approval-gated executable tool cannot complete a single approve→execute cycle on the OpenAI Responses provider.

This is distinct from #203/#204 (defineClientTool): that is about client-resolved, executeless HITL tools producing a duplicate tool_result, verified on Anthropic via Gateway. This is an approval-gated executable tool producing a missing result on the OpenAI Responses provider. #203's discussion asserts the approval-gated-executable path works — but the cited eval uses Anthropic; on OpenAI Responses it fails.

Environment

  • eve 0.13.1 (also repro'd on 0.11.5)
  • ai 7.0.0-beta.178, @ai-sdk/openai 4.0.0-beta.74
  • Model: openai/gpt-5.5 (defaults to the Responses API)
  • Next.js 16 (App Router), withEve, useEveAgent over same-origin /eve/v1/*
  • Default harness; approval answered via structured inputResponses (optionId: 'approve'), freeform disabled during the gate

Reproduction

  1. Author a tool that has both execute and needsApproval: always() (eve/tools/approval):
    export default defineTool({
      description: 'Do a gated side effect.',
      inputSchema: z.object({ /* … */ }),
      needsApproval: always(),
      async execute(input, ctx) { /* real side effect */ return { success: true } },
    })
  2. Have the agent call it. The harness parks it for approval.
  3. Approve via the client (agent.send({ inputResponses: [{ requestId, optionId: 'approve' }] })).
  4. The resume turn fails on the first model call.

Observed event trace

input.requested            ← harness parks the gated tool call
turn.completed
session.waiting            ← parked ✅
[user approves]
turn.started               ← resume accepted ✅
step.started
step.failed                ← MODEL_CALL_FAILED: AI_APICallError
                             "No tool output found for function call call_<id>"
turn.failed
session.failed

Root-cause analysis (evidence)

  1. The error string No tool output found for function call is not present in @ai-sdk/openai — it is OpenAI's server rejecting a request whose input contains a function_call with no matching function_call_output.
  2. On approve, the harness records the approval and emits a tool-approval-response part for the gated call, but the call's tool-result is never produced before the model is re-invoked (the executor hasn't run — the step stays waiting_approval).
  3. The ai package strips tool-approval-response parts for non-providerExecuted (local) tools before sending to the provider — i.e. for a local tool the approval-response is not the provider-level resolution; the executor's tool-result is expected instead.
  4. The OpenAI provider converters confirm neither transport pairs an approval-only local call:
    • Responses converter maps tool-approval-responsemcp_approval_response keyed by approval_request_id (MCP tools only). A regular authored function tool has no approval_request_id, so the function_call is left without a function_call_output.
    • Chat converter skips tool-approval-response entirely (if (toolResponse.type === 'tool-approval-response') continue;), leaving the assistant tool_calls unpaired too.

Net: on the resume turn the reconstructed prompt carries the gated tool's function_call/tool_call with no result, because the executor hasn't run and the approval-response isn't a provider-level result for a local tool → the OpenAI Responses API 400s.

Expected

Per the contract described in #203 ("approval allows execution; the executor produces the result"), after the user approves a local executable tool the harness should run execute() and supply its tool-result for that call id before the next model call (so the function_call is paired). Today, on the OpenAI Responses provider, the model is re-invoked with the call still unpaired.

Workaround

Drop needsApproval on the side-effect tools and gate via the built-in ask_question (Confirm/Cancel) instead — the answer is paired as a real tool-result, so resume succeeds. This trades the framework-enforced gate for an instruction-enforced one.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
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