Repository navigation
Conversation
contextablemark
left a comment
There was a problem hiding this comment.
Changes requested
#2456 is now merged, so the continuation-filtering dependency is resolved. Please rebase this PR onto current main and update the dependency section. I checked the combined tree; two correctness blockers remain.
1. Bind each approval response to its original request
AGUIChatClient.ValidateApprovalResponses only checks that every request ID appears in the responses. Resume construction then trusts approvalResponse.ToolCall.
With two requests, callers can cross the request IDs and tool calls. A “deny delete_file / approve get_weather” response pair is consequently serialized as “approve delete_file / deny get_weather.” Both calls exist in the original batch, so the server’s call validation accepts them.
Please map responses to requests by RequestId, verify call ID/name/arguments match, and construct the resume from the original request’s tool call. Add a regression test with crossed responses.
2. Preserve the existing cross-language continuation contract
RunAgentInputExtensions.ConfigureForMixedInvocation now rejects a mixed batch containing an AGUIToolMessage client result unless the request includes synthetic Resume entries for every peer call.
That conflicts directly with the existing TypeScript contract:
CrossLanguage.Vitest/tests/mixed-tool-invocation.test.tssends the assistant batch plus the client-owned tool result and expects the C# server to execute the unresolved server tool.- The new
LegacyResult_MixedCallBatchRequiresResumetest sends that same shape and expects it to throw.
The TypeScript client cannot manufacture approval IDs for server peers that were never exposed as interrupts. Please retain the message-only continuation path—while keeping the new exact-call validation—or land a coordinated protocol and TypeScript-client change. The cross-language mixed-tool test needs to pass before merge.
Architecture follow-up
AGUI.Server now references full Microsoft.Extensions.AI, while AGENTS.md and docs/architecture.md still define it as depending only on Microsoft.Extensions.AI.Abstractions. Please either preserve that boundary or explicitly update and justify the architecture contract.
If it would be easier, I’m happy to take over this PR and handle the rebase and fixes—just let me know.
Wrap client declarations as approval-required invocable proxies, use MEAI approval causality for collateral server calls, and rebuild continuation batches with exact client results. Co-authored-by: Copilot App <[email protected]>
Keep the existing approval flow unchanged and use RequiresConfirmation only to unwrap collateral server calls. Co-authored-by: Copilot App <[email protected]>
Coordinate client and server approvals through complete Resume batches, preserve genuine approval decisions, and prevent partial, stale, or replayed tool execution. Co-authored-by: Copilot App <[email protected]>
b66d3e1 to
ec558c0
Compare
Prove the canonical TS client receives an ordinary tool call, no interrupt, and resumes from message history. Co-authored-by: Copilot App <[email protected]>
Prove an ordinary server tool executes and returns its result within one successful run. Co-authored-by: Copilot App <[email protected]>
Prove protected server tools surface one interrupt and resume through explicit approve and reject paths. Co-authored-by: Copilot App <[email protected]>
Prove local client approval and rejection remain message-only and do not create AG-UI interrupts or Resume entries. Co-authored-by: Copilot App <[email protected]>
Accept C+S plus the client-owned result without Resume, while rebuilding the complete internal FICC approval batch for valid provider ordering. Co-authored-by: Copilot App <[email protected]>
Prove client-local approve and reject policies retain the ordinary C+S message-only continuation. Co-authored-by: Copilot App <[email protected]>
Prove C+P+S emits only P as an interrupt and resumes with result(C) plus Resume(P), while ordinary S executes internally. Co-authored-by: Copilot App <[email protected]>
Prove every protected server interrupt must be covered and complete approve/reject Resume batches continue successfully. Co-authored-by: Copilot App <[email protected]>
Keep client approvals inside local FICC, send Resume only for genuine server interrupts, preserve informational peers and original call order, and reject crossed responses. Co-authored-by: Copilot App <[email protected]>
Treat completed batches as continuations for replay suppression while allowing canonical message-only and interrupt-subset continuation shapes. Co-authored-by: Copilot App <[email protected]>
Record message-only mixed continuation and genuine server-only Resume shapes after aligning the .NET client with TypeScript. Co-authored-by: Copilot App <[email protected]>
Record the full Microsoft.Extensions.AI dependency used for exact call-ID correlation in mixed continuations. Co-authored-by: Copilot App <[email protected]>
|
Addressed the requested changes and pushed the updated scenario-by-scenario series:
Validation is green: Client 133/TFM, Server 150/TFM, Hosting 121, CrossLanguage.Vitest 133/133. Please re-review when convenient. |
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.dev1793292470' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1793292470' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1793292470' --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.dev1793292470' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1793292470' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1793292470' --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.dev1793292470
Commit: 2f45d24 |
@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: |
contextablemark
left a comment
There was a problem hiding this comment.
Changes requested
Thanks for the thorough follow-up. I re-reviewed the latest head (d9dd9ac) and confirmed that the issues from my first review are addressed: the branch is rebased after #2456, approval responses are bound to their original calls, the TypeScript message-only continuation works again, replay suppression is restored, and the full Microsoft.Extensions.AI dependency is now documented.
I also reran the advertised suites locally. The solution builds cleanly, Client passes 133/133, Server 150/150, Hosting 121/121, C#→TypeScript 20/20, and TypeScript→C# 133/133. However, the cross-language TypeScript typecheck currently fails as described below.
Two approval-flow correctness blockers remain, and the new cross-language test file does not typecheck.
1. Reject a partial Resume before rebuilding the approval batch
IsResumeForLatestCallBatch currently checks only that every resumed call belongs to the latest batch. It does not require every unresolved call in that batch to be covered by either a persisted result or a Resume entry.
ProcessContinuation then creates a RequiresConfirmation = false request and an approved response for each omitted call. Consequently, when a batch contains two protected server calls, a caller can send one fully formed Resume and omit the other; the omitted protected call is silently approved and executed.
I reproduced this with two ApprovalRequiredAIFunction server tools. Resuming only the first call resulted in the omitted second tool executing once; the expected count was zero.
The new “requires Resume for every pending interrupt” TypeScript test does not cover this server-side path: its partial payload omits toolCall, so HttpAgent rejects it from local pendingInterrupts before a request reaches the .NET server. Please add a server-level or raw-HTTP regression using two protected calls and one otherwise-valid, fully shaped Resume, and reject the request before either pending call is executed.
2. Do not require responses for approvals already resolved in history
The client correctly collects fresh approval responses only from the last message, but ValidateApprovalResponses collects approval requests from the entire message history and requires every one of them to appear in that fresh-response set.
This breaks the second approval round in the SDK's full-history model. I reproduced it with an already-resolved old request/response followed by a new request/response; the new turn throws:
Approval responses are missing for request(s): approval-old.
Please validate only the current unresolved approval batch, or account for historical responses when determining which requests remain unanswered, while retaining the new exact request/call binding checks.
3. Make the new cross-language tests pass typecheck
The current CI typecheck job is red, and I reproduced it locally with:
nx run @ag-ui/cross-language-tests:typecheck
All 13 diagnostics are TS2322 errors in tool-approval-scenarios.test.ts. Each expression-bodied callback has the form:
onEvent: ({ event }) => turn1.push(event)Array.push returns a number, so these callbacks do not satisfy the subscriber contract's void | AgentStateMutation return type. Using a block body or explicitly discarding the result fixes the target; after changing only these callbacks to ({ event }) => void turn1.push(event), the exact Nx target passes.
Additional cleanup
RawRepresentationFactoryis invoked once for the Resume preflight and again by the inner handler (first call, second call). A counting regression sees two invocations, and a factory that produces fresh input can make preflight routing inspect a differentRunAgentInputfrom the one sent. Please evaluate it once and carry that value through.dotnet format --verify-no-changesrejectsAGUIChatClient.cs, primarily the block at lines 97–130, and reports three unused imports.
My offer to take over the remaining fixes still stands if that would be easier.
contextablemark
left a comment
There was a problem hiding this comment.
The partial-Resume fix, historical-response handling, and TypeScript typecheck check out in the targeted runs. One approval gap remains; I’ve left it inline. This is an existing path left open, rather than a regression introduced by the latest merge.
| #pragma warning restore MEAI001 | ||
| }; | ||
| approvalRequests.Add(request); | ||
| approvalResponses.Add(request.CreateResponse(approved: true)); |
There was a problem hiding this comment.
The partial-Resume fix works, but omitting Resume entirely still skips approval.
I reproduced this on 1d9762ce0e03eca26377ec6461f3765178b025be with one completed client result and two server calls wrapped in ApprovalRequiredAIFunction:
- One valid Resume: rejected; neither protected tool executes.
- No Resume: accepted; both protected tools execute once.
With no approval Resume, the request takes the older message-only continuation path, and this line supplies an approved response for each protected call. That path was already present at the previously reviewed head, so this is a remaining gap rather than a regression from this merge.
Please keep message-only continuation working for ordinary server peers, while requiring a decision before protected tools execute. Could you add the same server-level regression with the client result present and Resume omitted, asserting zero protected executions?
I haven’t included a one-click patch because the actual server tools are registered after this adapter runs; a safe fix needs to distinguish their approval requirements without breaking ordinary mixed continuation.
contextablemark
left a comment
There was a problem hiding this comment.
Thanks, the direct protected-tool case is fixed now. I confirmed protected peers no longer run without Resume, whether they're passed in, added afterward, or only in AdditionalTools. There's one remaining gap, noted inline. The other inline note is non-blocking.
| // Wire history cannot establish whether a server tool requires approval. | ||
| // Only the application's trusted registrations can authorize the legacy path. | ||
| var tool = chatOptions.Tools?.SingleOrDefault(tool => tool.Name == call.Name); | ||
| if (tool is not AIFunction || tool is ApprovalRequiredAIFunction) |
There was a problem hiding this comment.
This catches ApprovalRequiredAIFunction itself, but not one that's been wrapped in another DelegatingAIFunction (a logging or telemetry wrapper, say). The wrapped tool passes this check as ordinary, gets a synthetic approval, and runs with no Resume. FICC still treats the same wrapper as needing approval on a normal call, so the two disagree.
I reproduced it on 54bb333 with new Wrap(new ApprovalRequiredAIFunction(f)), where Wrap : DelegatingAIFunction. With a client result and zero Resume entries, the protected function ran once.
Checking it the way FICC does should close it:
if (tool is not AIFunction || tool.GetService<ApprovalRequiredAIFunction>() is not null)Could you also add a wrapped case to LegacyResult_ProtectedServerPeersCannotExecuteWithoutResume?
|
|
||
| // Wire history cannot establish whether a server tool requires approval. | ||
| // Only the application's trusted registrations can authorize the legacy path. | ||
| var tool = chatOptions.Tools?.SingleOrDefault(tool => tool.Name == call.Name); |
There was a problem hiding this comment.
Non-blocking: if two registered tools share a name, this throws LINQ's "Sequence contains more than one matching element". It fails safe, so it's fine as is, but a message naming the ambiguous tool would be easier to debug.
|
This also fixes #3271 (a client tool that already has a result loses its schema and description on the next continuation, and its stand-in answers new calls with the old result). In case they're useful, here are regression tests for that issue, written against this PR's API:
Run with .NET SDK 10.0.100, net10.0: The only change needed for this PR is in
|
|
@javiercn thanks, e4f0beb covers both of my earlier comments. Wrapped protected tools are caught now, and the duplicate-name error is clear. I ran the Server unit tests on it: 222 passed. Two things before this can go in:
|
Summary
main, including call-ID-aware continuation filtering from fix(dotnet): stream new tool calls on continuations #2456C,S + result(C), no Resume)ToolApprovalRequestContent.RequiresConfirmationto distinguish genuine server approvals from collateral FICC wrappingRelated to microsoft/agent-framework#6967 and dotnet/extensions#7549.
Canonical TS scenarios
The TS→.NET suite now covers each scenario independently:
Each scenario was added as an independent commit.
Reviewer feedback addressed
Validation
The cross-language suite uses the repository-managed
@protobuf-ts/protoc; the conflicting external WinGet protobuf installation was removed.