Summary
An authored, executable tool with needsApproval: always() parks correctly for approval (input.requested → session.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.failed → session.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
- 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 } },
})
- Have the agent call it. The harness parks it for approval.
- Approve via the client (
agent.send({ inputResponses: [{ requestId, optionId: 'approve' }] })).
- 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)
- 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.
- 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).
- 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.
- The OpenAI provider converters confirm neither transport pairs an approval-only local call:
- Responses converter maps
tool-approval-response → mcp_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.
Summary
An authored, executable tool with
needsApproval: always()parks correctly for approval (input.requested→session.waiting), but on the resume turn after the user approves, the next model call is rejected by the OpenAI Responses API:The approved tool's
execute()never runs (the durable step stayswaiting_approval), andturn.failed→session.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 duplicatetool_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
eve0.13.1 (also repro'd on 0.11.5)ai7.0.0-beta.178,@ai-sdk/openai4.0.0-beta.74openai/gpt-5.5(defaults to the Responses API)withEve,useEveAgentover same-origin/eve/v1/*inputResponses(optionId: 'approve'), freeform disabled during the gateReproduction
executeandneedsApproval: always()(eve/tools/approval):agent.send({ inputResponses: [{ requestId, optionId: 'approve' }] })).Observed event trace
Root-cause analysis (evidence)
No tool output found for function callis not present in@ai-sdk/openai— it is OpenAI's server rejecting a request whose input contains afunction_callwith no matchingfunction_call_output.tool-approval-responsepart for the gated call, but the call'stool-resultis never produced before the model is re-invoked (the executor hasn't run — the step stayswaiting_approval).aipackage stripstool-approval-responseparts 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'stool-resultis expected instead.tool-approval-response→mcp_approval_responsekeyed byapproval_request_id(MCP tools only). A regular authored function tool has noapproval_request_id, so thefunction_callis left without afunction_call_output.tool-approval-responseentirely (if (toolResponse.type === 'tool-approval-response') continue;), leaving the assistanttool_callsunpaired too.Net: on the resume turn the reconstructed prompt carries the gated tool's
function_call/tool_callwith 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 itstool-resultfor that call id before the next model call (so thefunction_callis paired). Today, on the OpenAI Responses provider, the model is re-invoked with the call still unpaired.Workaround
Drop
needsApprovalon the side-effect tools and gate via the built-inask_question(Confirm/Cancel) instead — the answer is paired as a realtool-result, so resume succeeds. This trades the framework-enforced gate for an instruction-enforced one.