You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'm using @tanstack/ai / @tanstack/ai-client on 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-requested event (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.
On resume, the agent emits TOOL_CALL_RESULT against the original toolCallId (it does not re-emit TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END), then RUN_FINISHED with outcome: { type: "success" }.
LangGraph natively supports this (it accepts RunAgentInput.resume[] and can emit RunFinished.outcome = { type: "interrupt" }).
What I found looking at the client internals
Approval decision isn't serialized to the wire. The approval path is CUSTOM-event based (approval-requested) and stores the decision on the message part (state: "approval-responded", approval.approved) — but uiMessagesToWire doesn't serialize approval / approved. So on the resume run, a non-TanStack backend never receives the approve/deny decision.
RUN_FINISHED.outcome is ignored.handleRunFinishedEvent only reads chunk.finishReason, so the client never surfaces outcome.interrupt.
No top-level resume field.buildRunAgentInputBody produces a fixed shape (threadId, runId, state, messages, tools, context, forwardedProps, data), and forwardedProps / body / data all land nested under forwardedProps — not at the request root where the spec puts resume.
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 expects outcome.interrupt + resume[].
Questions
Is native AG-UI interrupt support (RUN_FINISHED.outcome.interrupt inbound, top-level resume[] outbound) supported today, or on the roadmap? Is the intended approach to keep using approval-requested and have the backend adapt, or to speak the interrupt contract directly?
If native interrupts aren't supported yet, is a custom connection adapter the intended extension point — i.e. reading outcome.interrupt from the stream and building the request body myself to inject a top-level resume[]? Is there a cleaner/supported hook I'm missing?
Is there a supported way to trigger a resume run with no new user message (a "continue" that re-POSTs the thread), other than piggybacking on addToolResult / addToolApprovalResponse auto-continuation?
Any plan to make RUN_FINISHED.outcome (success / interrupt) first-class in the client state, so UIs can render interrupt prompts from message / responseSchema / reason?
Any guidance on the recommended pattern for AG-UI interrupt interop would be much appreciated.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
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