Human-in-the-Loop Tool Approval#
Human-in-the-Loop (HITL) tool approval is the mechanism by which the agent chat pauses a run when a tool requires browser-side fulfillment or explicit user consent, collects the required response, and then resumes the run. It consolidates two distinct interaction paths β approval-gated server tools (approve/deny before the runner executes) and client tools (browser fulfills the tool directly, e.g., OAuth, form input) β through a shared resume predicate.
The full system spans:
- Resume predicate β decides when a paused run should continue
- Queue gating β prevents user messages from injecting into the transcript while a tool is pending
- Render hint infrastructure β routes tool calls to the correct browser widget
- Approval metadata guard β distinguishes client-tool results from server-tool approvals
Introduced in PR #4934 and extended with schema-driven elicitation in PR #5155.
Resume Predicate and Approval Metadata Guard#
agentApprovalResume.ts is the single module that decides when agentShouldResumeAfterApproval fires. It recognizes three distinct settled states :
| Settled state | Resume trigger |
|---|---|
approval-responded + approval metadata | Approval/deny response received |
output-available / output-error + providerExecuted falsy + no approval metadata | Client-tool result settled |
output-available + providerExecuted falsy + approval metadata present | Approved server tool β excluded |
The approval metadata guard (checking approval == null) is load-bearing: an approval-gated tool that was approved and subsequently ran server-side also lands in output-available with providerExecuted falsy. Without the guard, the predicate would fire a second spurious resume.
Two classification helpers support the predicate :
isClientTool(part, renderMap)β returns true whenrenderKindFor()resolves a knownrender.kindor the tool name is inCLIENT_TOOL_NAMES(v1 backward-compatibility)isClientToolResult(part, renderMap)β returns true for a settled client-tool part; triggers auto-resume
Queue Gating#
agentMessageQueue.ts holds the outbound user-message queue via canReleaseQueuedMessage() for as long as any pending client-tool interaction exists. Without this gate, a queued user message could inject itself into the conversation transcript before the widget has settled, producing an incoherent run.
The gate is released automatically when the resume predicate fires β that is, when the widget calls settle(...), which chains through:
- Widget β
settle(...)via theonClientToolOutputcallback addToolOutputinAgentChatPanelmatches the parked part bytoolCallIdagentShouldResumeAfterApprovaldetects the settled result β queue released
Cross-turn isolation (runner side): A companion fix in responder.ts scopes the client-tool output store to the current turn only. currentTurnToolResultBlocks yields tool_result blocks at or after the latest user-role message, so a second identical request_input call in a new turn re-pauses and shows a fresh interaction rather than silently reusing the prior turn's answer. Approval grants remain full-history (idempotent).
Render Hint Infrastructure#
Because AI SDK v6 tool chunks are strict objects that drop inline fields, render.kind cannot be embedded directly in the tool-call part. Instead it travels as a sibling data-render stream part containing { toolCallId, render: { kind, ... } }.
renderMap.ts provides two utilities :
buildRenderMap(parts)β filtersdata-renderparts from a message and returns aMap<toolCallId, RenderHintLike>. This is the single source of truth for render hints within a turn.renderKindFor(part, renderMap)β checks the inlinerender.kindfield first (forward compatibility), then falls back to the map.
Both registry.tsx (widget dispatch) and agentApprovalResume.ts (resume predicate) consume the same render map, ensuring consistent tool classification.
AI SDK v6 stream
ββ tool-call part { toolCallId, toolName, args }
ββ data-render part { toolCallId, render: { kind: "elicitation" } }
β
βΌ
buildRenderMap()
ββββΆ registry.tsx β widget lookup
ββββΆ agentApprovalResume.ts β resume predicate
Client-Tool Registry and Dispatch#
registry.tsx resolves the widget for a tool part using three-tier precedence :
| Priority | Key | Handler |
|---|---|---|
| 1 | render.kind = "elicitation" | ElicitationWidget |
| 1 | render.kind = "connect" | ConnectToolWidget |
| 2 | toolName = "request_connection" | ConnectToolWidget (v1 compat) |
| 3 | (fallback) | UnhandledClientTool |
render.kind is the primary axis for all new interaction kinds. toolName-based dispatch is legacy β request_connection predates the render-hint guarantee.
meta.ts derives the renderKind and toolName properties a registry lookup needs, from a ToolUIPart and the message-scoped render map.
Graceful degradation β UnhandledClientTool auto-settles any unrecognized tool so the run continues. ElicitationWidget handles malformed payloads by auto-settling with an errorText prefix; a degradedEarlierInTurn flag prevents the settle β resume β re-emit loop from repeating more than once per turn.
Key Files Reference#
| File | Purpose |
|---|---|
execution/agentApprovalResume.ts | Resume predicate; isClientTool(), isClientToolResult(), approval metadata guard |
execution/agentMessageQueue.ts | Queue gating via canReleaseQueuedMessage() |
execution/renderMap.ts | buildRenderMap(), renderKindFor() β render-hint extraction |
clientTools/registry.tsx | BY_RENDER_KIND / BY_TOOL_NAME dispatch tables |
clientTools/meta.ts | clientToolMeta() β derives renderKind + toolName from a ToolUIPart |
clientTools/types.ts | ClientToolHandler, ClientToolMeta, ClientToolOutputHandler types |
clientTools/ElicitationWidget.tsx | Schema-driven inline form; degradation handling + retry cap |
clientTools/ConnectToolWidget.tsx | OAuth connection widget (v1) |
clientTools/UnhandledClientTool.tsx | Generic fallback; auto-settles unknown tools |
services/runner/src/responder.ts | Turn-scoped client-tool output store (cross-turn isolation) |
agenta-shared/src/utils/elicitation.ts | Elicitation wire contract: validator, envelopes, format normalization |
tests/unit/renderMap.test.ts | Unit suite for render map, resume predicate, queue gating |
tests/unit/agentApprovalResume.test.ts | Unit tests for approval resume predicate |