Replies: 2 comments
-
|
This will land together with the upcoming persistence PR. |
Beta Was this translation helpful? Give feedback.
-
|
Native interrupt support should make the interrupt a durable thread-level object, not merely another message part. The approval UI is a projection of that object; it should not be the source of truth. I would preserve these invariants across adapters:
A custom connection adapter can bridge this temporarily, but only if the client state model exposes interrupt outcomes first-class. Otherwise every adapter has to maintain a shadow state machine and recovery becomes ambiguous. We use the same ownership split in Better Agent for native Claude, Codex, and Gemini approvals: the backend persists pending decisions and continuation state, while clients render and acknowledge them. I maintain it; the project is source-available and free for non-commercial use, while commercial use requires separate permission: https://github.com/ofekron/better-agent AI-assistance disclosure: this comment was drafted by Codex under the maintainer’s authorization and reviewed in Better Agent. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm using
@tanstack/ai/@tanstack/ai-clienton the frontend, with a backend built on LangGraph / LangChain that speaks the AG-UI protocol.First off — I can see that TanStack AI already supports tool approvals via the CUSTOM
approval-requestedevent (needsApproval→approval-requested→addToolApprovalResponse), and that flow works well. My question is specifically about interop with the native AG-UI interrupt contract, which appears to be a different mechanism.What I'm trying to do
Support the AG-UI human-in-the-loop interrupt flow from the spec: https://docs.ag-ui.com/concepts/interrupts
The spec models HITL as an interrupt-aware run lifecycle:
RUN_FINISHEDcarrying an interrupt outcome:{ "type": "RUN_FINISHED", "threadId": "thread-1", "runId": "run-1", "outcome": { "type": "interrupt", "interrupts": [ { "id": "int-abc", "reason": "tool_call", "toolCallId": "tc-001", "message": "Send email to a@b.com?", "responseSchema": { "type": "object", "properties": { "approved": { "type": "boolean" } }, "required": ["approved"] } } ] } }RunAgentInputon the same thread with a top-levelresumearray:{ "threadId": "thread-1", "runId": "run-2", "resume": [ { "interruptId": "int-abc", "status": "resolved", "payload": { "approved": true } } ] .... }TOOL_CALL_RESULTagainst the originaltoolCallId(it does not re-emitTOOL_CALL_START/TOOL_CALL_ARGS/TOOL_CALL_END), thenRUN_FINISHEDwithoutcome: { type: "success" }.LangGraph natively supports this (it accepts
RunAgentInput.resume[]and can emitRunFinished.outcome = { type: "interrupt" }).What I found looking at the client internals
approval-requested) and stores the decision on the message part (state: "approval-responded",approval.approved) — butuiMessagesToWiredoesn't serializeapproval/approved. So on the resume run, a non-TanStack backend never receives the approve/deny decision.RUN_FINISHED.outcomeis ignored.handleRunFinishedEventonly readschunk.finishReason, so the client never surfacesoutcome.interrupt.resumefield.buildRunAgentInputBodyproduces a fixed shape (threadId,runId,state,messages,tools,context,forwardedProps,data), andforwardedProps/body/dataall land nested underforwardedProps— not at the request root where the spec putsresume.My understanding is that TanStack's approval flow is designed around the TanStack engine (which recovers the decision from the parts model, e.g.
extractClientStateFromOriginalMessages) rather than the AG-UI interrupt wire contract. That works great TanStack-engine ↔ TanStack-client, but doesn't interop with an AG-UI agent that expectsoutcome.interrupt+resume[].Questions
RUN_FINISHED.outcome.interruptinbound, top-levelresume[]outbound) supported today, or on the roadmap? Is the intended approach to keep usingapproval-requestedand have the backend adapt, or to speak the interrupt contract directly?outcome.interruptfrom the stream and building the request body myself to inject a top-levelresume[]? Is there a cleaner/supported hook I'm missing?addToolResult/addToolApprovalResponseauto-continuation?RUN_FINISHED.outcome(success / interrupt) first-class in the client state, so UIs can render interrupt prompts frommessage/responseSchema/reason?Any guidance on the recommended pattern for AG-UI interrupt interop would be much appreciated.
Beta Was this translation helpful? Give feedback.
All reactions