Tool Result Handling and Resume Flow#
When the agent runner emits a client-tool call (e.g., request_input for elicitation or request_connection for OAuth), the frontend parks the call, shows a widget, collects the user's response, then resumes the run by replaying the conversation with the tool result injected. This article covers the three distinct phases of that lifecycle: settlement timing, message array composition, and cross-turn scoping.
Introduced in PR #4934 and extended with schema-driven elicitation in PR #5155 .
Settlement Timing: settle() β addToolOutput β Resume#
The settlement chain runs entirely on the frontend :
- Widget calls
settle(...)βConnectToolWidget(line 186β188) orElicitationWidget(line 151) call settle on user action (confirm/accept/cancel). ClientToolPartdispatcher (lines 40β51) wraps the settlement into{ toolName, toolCallId, output | errorText }and calls theonClientToolOutputcallback.handleClientToolOutputinAgentChatPanel.tsx(lines 264β289) calls the AI SDK'saddToolOutputwith{ tool: toolName, toolCallId, output }(success) or{ state: "output-error", tool: toolName, toolCallId, errorText }(failure) .agentShouldResumeAfterApprovalinagentApprovalResume.tsfires when it detects a settled client-tool part (lines 113β151) β the AI SDK'ssendAutomaticallyWhenhook then re-sends the conversation as the resume HTTP request .
widget.settle()
βββΆ ClientToolPart dispatcher (40β51)
βββΆ handleClientToolOutput / addToolOutput (264β289)
βββΆ agentShouldResumeAfterApproval fires (113β151)
βββΆ HTTP resume POST β runner
Resume Predicate and Approval Metadata Guard#
agentApprovalResume.ts contains the three-state logic that triggers resume :
| Settled state | Resume triggered? |
|---|---|
approval-responded + approval metadata | β Approval/deny path |
output-available/output-error + providerExecuted falsy + no approval metadata | β Client-tool result |
output-available + providerExecuted falsy + approval metadata present | β Approved server tool β excluded |
The approval == null guard is load-bearing: an approval-gated server tool that was approved and ran also lands in output-available with providerExecuted falsy. Without this guard, resume fires twice .
Two helpers classify parts: isClientTool(part, renderMap) (checks render.kind or falls back to CLIENT_TOOL_NAMES for v1 compat) and isClientToolResult(part, renderMap) (triggers auto-resume on settled state) .
Message Array Composition for Resume#
The resume HTTP body is assembled by the AI SDK from the current message array. The message array contains :
tool-callparts:{ toolCallId, toolName, args }β the original AI requestdata-rendersibling parts:{ toolCallId, render: { kind: "elicitation" | "connect" } }β render hints that traveled as separate stream parts because AI SDK v6 strict objects drop inline fieldstool_resultblocks: injected byaddToolOutputβ the settled widget output (output: Record<string, unknown>for success;errorText: stringfor error)
buildRenderMap(parts) reconstructs a Map<toolCallId, RenderHintLike> from the data-render parts at resume time. Both the registry dispatcher and the resume predicate consume this same map .
Cross-Turn Scoping of Tool Result Blocks#
On the runner side, responder.ts gates which tool_result blocks satisfy a pending client-tool call :
currentTurnStartIndex(request)β finds the latestuser-role message boundary (returns 0 if none exists).currentTurnToolResultBlocks(request)β yields onlytool_resultblocks at or after that boundary.
extractClientToolOutputs() was updated to use currentTurnToolResultBlocks (was previously full-history) so a second identical request_input call in turn 2 cannot reuse turn 1's answer β it parks and shows a fresh form . Approval grants remain full-history; they are idempotent.
Queue Gating#
agentMessageQueue.ts holds queued user messages via canReleaseQueuedMessage() (lines 47β56) while any pending client-tool interaction exists . The gate releases automatically when the resume predicate fires β preventing a user's next message from injecting into the transcript before the widget settles .
Graceful Degradation#
- Unknown tools β
UnhandledClientTool.tsxauto-settles unrecognized tools so the run never hangs silently . - Invalid payloads β
ElicitationWidget.tsxauto-settles malformed payloads with"elicitation: unsupported payload β ...". AdegradedEarlierInTurnflag caps the settleβresumeβre-emit loop at one retry per turn .
Key Files#
| File | Purpose |
|---|---|
execution/agentApprovalResume.ts | Resume predicate; isClientTool(), isClientToolResult(), approval metadata guard |
execution/agentMessageQueue.ts | Queue gating via canReleaseQueuedMessage() |
execution/renderMap.ts | buildRenderMap(), renderKindFor() |
clientTools/registry.tsx | BY_RENDER_KIND / BY_TOOL_NAME dispatch tables |
clientTools/ElicitationWidget.tsx | Schema-driven form; degradation + retry cap |
clientTools/UnhandledClientTool.tsx | Generic fallback; auto-settles unknown tools |
services/runner/src/responder.ts | currentTurnToolResultBlocks() β cross-turn scoping |
agenta-shared/src/utils/elicitation.ts | Elicitation wire contract: validator, envelopes, format normalization |
tests/unit/agentApprovalResume.test.ts | Resume predicate unit tests |
tests/unit/renderMap.test.ts | Render map, resume predicate, queue gating (233 lines) |