Repository navigation
Conversation
…otocol#2815) A resume entry whose payload carried no toolCall was rejected before the approval branch ran, so it became a generic InterruptResponseContent and the pending approval was never answered. The response schema the server advertises requires only `approved`, so a client following that contract could not resume an approval at all. TryDecodeToolApprovalResume now splits. A payload carrying toolCall decodes exactly as before, so deployed first-party clients are unaffected. A payload without one is resolved against the run's messages: a toolCallId on the payload matches the pending set, otherwise a single unanswered approval-gated call binds unambiguously, otherwise the run fails with an error naming the interrupt id and every candidate call id. Zero pending calls still fall through to a generic interrupt. Each entry removes the call it bound, so two entries cannot claim the same call, and a toolCallId naming an already-answered call returns null rather than approving it twice. AGUIResume.Status is now read where the response is built, so a cancelled entry produces a declined approval regardless of its payload. That applies to the echo path too, where a cancelled entry carrying approved:true previously executed the tool. The pending set is filtered by client tool name, which approximates the emit side at Microsoft.Extensions.AI 10.6.0. Once the package moves to 10.9.0 the filter should use RequiresConfirmation so both sides share one predicate.
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen install the packages you need: # Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1793106201' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1793106201' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1793106201' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1793106201' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1793106201' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1793106201' --index testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1793106201
Commit: bfa3ebf |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk-js
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/antigravity
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/claude-managed-agents
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
|
One pending call works well, and the tests fail without the change. Some gaps:
Smaller: collect the echo entries before you resolve the bare ones, so order does not matter. Also, #2815's repro sends |
|
Hey @BenTaylorDev, Pushed a follow-up. A bare |
A resume entry whose payload carried `approved` and `toolCallId` was matched against pending calls without checking that its interrupt was an approval interrupt. A custom interrupt answered with that shape was turned into a tool approval, so FunctionInvokingChatClient ran the tool and the custom handler never saw its answer; a `toolCallId` naming a call outside the history threw during request conversion. The interrupt id is now authoritative: an id without the FICC approval prefix never approves anything and stays a generic interrupt response, and a `toolCallId` that contradicts the approval interrupt's own call is rejected. Test summaries that still described pre-fix behaviour are rewritten.
Thanks @yousuf64 — filing this alongside the other .NET interrupt issues made the pattern much easier to see.
A resume entry whose payload carried no
toolCallwas rejected atRunAgentInputExtensions.cs:184-188before the approval branch ran, so it became a genericInterruptResponseContentand the pending approval was never answered. The response schema the server advertises atChatResponseUpdateAGUIExtensions.cs:17-26requires onlyapproved, so a client building its payload from that schema could not resume an approval at all.TryDecodeToolApprovalResumenow splits. A payload carryingtoolCalldecodes exactly as before, so deployed first-party .NET clients are unaffected. A payload without one is resolved against the run's messages: atoolCallIdon the payload matches against the pending set, otherwise a single unanswered approval-gated call binds unambiguously, otherwise the run fails with an error naming the interrupt id and every candidate call id. Zero pending calls still fall through to a generic interrupt, which is why the existing pin atInterruptContentTest.cs:129stays green. Each entry removes the call it bound, so two entries cannot claim the same call, and atoolCallIdnaming an already-answered call returns null rather than approving it twice.Recovering from the messages is the mechanism the spec already names: the uncovered-interrupt rule in
docs/spec/draft/basic/patterns/interrupt-resume.mdxsays a producer may notice an open interrupt because the messages carry an approval-gated tool call with neither a result nor an entry answering it. The same section requires a resuming run to carry the accumulated messages, so a resume sent with an empty message list still cannot be recovered, which is the one case in the original report that stays unsupported.AGUIResume.Statusis now read where the response is built, so acancelledentry produces a declined approval regardless of its payload. That applies to the echo path too, where a cancelled entry carryingapproved:truepreviously executed the tool.One behavior worth naming: the ambiguity error is thrown during request conversion, which happens before the event stream opens, so it reaches the operator as a 500 and in server logs rather than as a
RUN_ERROR. That is how every exception on that path already surfaces, including the existingNotSupportedExceptionandFormatExceptionfrom message conversion, so changing it belongs in its own PR.The pending set is filtered by client tool name, which approximates the emit-side rule at Microsoft.Extensions.AI 10.6.0. Once the package moves to 10.9.0 the filter should use
RequiresConfirmation, which is the predicate the emit side will use, and the two converge exactly.ToolApprovalResumeWithoutEchoTest.csadds 7 tests. Four cover the fix, including one that readsresponseSchemaoff a live interrupt, builds a payload from its ownrequiredlist and feeds it back. Three pin the boundaries: the echo path still resuming, a resume with no messages having nothing to recover from, and a resuming run carrying exactly one unresolved call.AGUI.Server.UnitTests goes 168 to 175. All seven .NET projects green on net10.0 locally; CI covers net8.0 and net9.0.
Closes #2815