fix(dotnet): align A2UI injectA2UITool with the sibling adapter contract + wire AGUI.A2UI into CI/release - #2289
Draft
contextablemark wants to merge 2 commits into
Conversation
The .NET adapter diverged from ADK / AWS Strands / Mastra on the
`injectA2UITool` gate in two ways that could silently disable A2UI's
subagent and recovery pillars.
1. Default was ON. The siblings are all OFF unless the run forwards
`injectA2UITool` or the backend opts in ("no injectA2UITool, no
injection"). Default-on also meant a client could only ever veto
A2UI, never enable it. `InjectA2UITool` is now the backend opt-in —
the `config` half of the siblings' `forwarded ?? config` rule (ADK
`a2ui["inject_a2ui_tool"]`, Strands/Mastra `a2ui.injectA2UITool`),
which exists precisely for hosts that do not forward the flag.
2. The middleware-injected render proxy was never dropped. The A2UI
middleware adds `render_a2ui` to `RunAgentInput.Tools` in the SAME
step that forwards the flag, and the hosting layer maps those onto
`ChatOptions.Tools` — so whenever the flag arrives, the proxy does
too. Left in place the planner saw both tools and could call the
proxy directly, painting a surface that skipped the subagent and the
validate-and-retry loop entirely. Now dropped, mirroring the
siblings' `dropToolNames` / `drop_tool_names` step.
The wire flag is `boolean | string` (a string names the injected proxy);
the string form is now honored, and an empty string reads as an opt-out
to match the siblings' falsy `if (!flag)` gate. A non-boolean,
non-string value is treated as absent so malformed input falls back to
the backend option rather than silently flipping behavior.
Precedence is unchanged and was already correct: a forwarded `false`
still beats a backend opt-in, and USER PREVAILS still leaves a
dev-wired `generate_a2ui` untouched.
The dojo demos opt in server-side, so they behave exactly as before.
Adds A2UIInjectionGateTest (13 cases) covering the gate matrix, the
string form, and the proxy drop. Verified against the pre-fix adapter:
6 of the 13 fail there, so they pin real behavior rather than passing
vacuously. A2UI suite: 98 -> 111, green on net8.0/net9.0/net10.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…release AGUI.A2UI was invisible to both CI and the release pipeline: neither workflow built the library or ran its test project, and the package was absent from release.config.json. So its unit tests never ran on any PR, and the package could never publish despite the csproj declaring PackageId/Description/PackageTags. CI: build src/AGUI.A2UI and run tests/AGUI.A2UI.UnitTests in unit-dotnet-sdk.yml and in the publish-release.yml pre-publish gate. Release: enroll AGUI.A2UI in its own `sdk-dotnet-a2ui` scope rather than the shared `sdk-dotnet` train, mirroring how its own cross-language siblings are already sequenced (`sdk-ts-a2ui-toolkit`, `sdk-py-a2ui-toolkit`). A scope alone is not sufficient: for stable releases the .NET detector is not scope-filtered — it sweeps every dotnet package in the config, and a package that is not yet on NuGet returns 404, is marked NEW, and would publish on the very next stable run. So this adds a declarative scope-level `autoPublish` flag: the scope stays enrolled (discoverable, canary-selectable, covered by the scope-dropdown guards) but is skipped by the automatic sweep. Flipping it to true is the reviewed, git-tracked graduation event. The flag is honored by all three detectors (dotnet/ts/py) so it generalizes to any future separately-sequenced library in any ecosystem. Omitted => true, so all 32 existing scopes are unaffected (verified: TS still resolves 25 packages, Python 10). The predicate is `!= false`, NOT `(.autoPublish // true)` — jq's `//` treats false as absent, so the latter evaluates to true and would silently publish the very scopes the flag exists to hold back. AGUI.A2UI also gets its own VersionPrefix, pinned to the cross-language A2UI family version (0.0.4, shared by @ag-ui/a2ui-toolkit and ag-ui-a2ui-toolkit) rather than inheriting the .NET SDK train's shared default. Without it the package would publish at the train's version and thereafter move only when the train moved — delayed, then permanently re-coupled. That the train currently also reads 0.0.4 is a coincidence that masks the drift; verified by bumping the train to 0.9.9, where AGUI.A2UI holds at 0.0.4 while AGUI.Server/Abstractions follow to 0.9.9. Adding a scope requires updating the four hand-maintained projections that verify-release-scope-dropdowns.sh enforces: the scope dropdowns in publish-release.yml, prepare-release.yml and canary.yml, plus the notify-job ecosystem case. All updated; that guard and verify-nx-release-allowlist.sh pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
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.dev1785472836' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1785472836' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1785472836' --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.dev1785472836' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1785472836' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1785472836' --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.dev1785472836
Commit: f76ba3d |
@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
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@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: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Review follow-ups for #2262, targeting its branch so they land with it. Two independent problems: the
injectA2UIToolcontract, and the fact thatAGUI.A2UIis invisible to CI and the release pipeline.Everything here was verified by building and running the suites locally (.NET SDK 10 + the 8/9 runtimes) — see Verification for what was not verified.
1.
injectA2UITooldiverged from the sibling adaptersinjectA2UIToolis not just a boolean gate — it's half of a handshake. The middleware'sinjectToolAndFlagsetsforwardedProps.injectA2UITooland appends arender_a2uiproxy toinput.toolsin the same inseparable step, and the type isboolean | stringwhere a string names that proxy. ADK, AWS Strands and Mastra therefore all implement the same six steps.A2UIChatClientbeforeforwarded ?? configprecedenceThe proxy drop is the substantive fix.
RunAgentInputExtensionsmapsinput.ToolsontoChatOptions.Tools, so whenever the flag arrives the proxy arrives with it. The planner then saw bothrender_a2uiandgenerate_a2ui; if the model picked the proxy, the surface still painted — via the middleware's direct-render path, with no subagent, no validation, no validate-and-retry, no prior-surface grounding. Two of the four A2UI pillars silently evaporate. A silent degradation is worse than a failure.Why this wasn't caught: the dojo route sets
injectsA2UIToolforlanggraph*/mastra-agent-localonly —ag-ui-dotnetis deliberately excluded. So the flag is never forwarded, no proxy is ever injected, andinput.toolsis empty. #2262 cites exactly that as proof of pillar 1 ("generate_a2uiappears with requesttools: []"), which confirms the one configuration in which the gap cannot manifest.On the default: flipping it to off is the smaller change of the two, and
.UseA2UI()being an explicit opt-in is a real distinction the siblings lack. But every sibling that does adapter-level injection defaults off and ships a backend opt-in documented for exactly this case ("opt in without the runtime flag… for non-CopilotKit hosts"). The concrete cost of default-on: a client can only ever veto A2UI, never enable it. LangGraph is not a counterexample — it has no adapter-level injection at all; it passes the flag intoag-uistate and the graph decides.The dojo demos now opt in server-side via that same backend mechanism, so behavior is unchanged.
a2ui_fixed_schemaneeded nothing (never wrapped).API shape — one call worth your review
InjectA2UIToolstaysbool?and gains a siblingInjectedRenderToolName(string?) rather than emulating TS'sboolean | stringunion in one property. Two typed properties are more idiomatic C#, need no new public type, and give the same capability. If you'd rather mirror the siblings exactly, a small struct with implicit conversions frombool/stringwould do it — say the word.2.
AGUI.A2UIwas invisible to CI and releaseNeither workflow built the library or ran its test project, and the package was absent from
release.config.json. Its unit tests never ran on any PR (#2262's CI is green partly because they don't run), and the package could never publish despite the csproj declaringPackageId/Description/PackageTags.src/AGUI.A2UI, runtests/AGUI.A2UI.UnitTests, in bothunit-dotnet-sdk.ymland thepublish-release.ymlpre-publish gate.sdk-dotnet-a2uiscope, not the sharedsdk-dotnettrain, mirroring how its own siblingssdk-ts-a2ui-toolkit/sdk-py-a2ui-toolkitare already sequenced.autoPublish, and why a scope alone isn't enoughFor stable releases the .NET detector is not scope-filtered: it sweeps every dotnet package in the config, and one not yet on NuGet returns 404, is marked
NEW (unpublished), and would publish on the very next stable run. Scope only gates prerelease/canary.So this adds a declarative scope-level
autoPublishflag. The scope stays enrolled — discoverable, canary-selectable, covered by the scope guards — but is skipped by the automatic sweep. Flipping it totrue(or deleting it) is the reviewed, git-tracked graduation event.It scales: honored by all three detectors (dotnet/ts/py), so it works for any future separately-sequenced library in any ecosystem.
Omitted => true, so all 32 existing scopes are untouched — verified: TS still resolves 25 packages, Python 10.Version pinning
AGUI.A2UIgets its ownVersionPrefix, pinned to the cross-language A2UI family version (0.0.4, shared by@ag-ui/a2ui-toolkitandag-ui-a2ui-toolkit) rather than inheriting the .NET train's shared default.@ag-ui/coreag-ui-protocol@ag-ui/a2ui-toolkitag-ui-a2ui-toolkitWithout its own prefix the package would publish at the train's version and thereafter move only when the train moved — delayed, then permanently re-coupled, which isn't separate sequencing. That the .NET train also currently reads
0.0.4is a coincidence that masks the drift.0.0.4is a judgment call and the one line to change if you disagree —0.0.1is the alternative if it should carry its own history.Adding a scope trips
verify-release-scope-dropdowns.sh, which enforces four hand-maintained projections (three scope dropdowns + the notify-job ecosystem case). All updated. I tried asdk-dotnet*)glob so future scopes would auto-map, but the guard does literal pattern comparison — explicit enumeration is the intended anti-drift design.Tests
A2UIInjectionGateTest— 13 cases covering the gate matrix, precedence, the string form (including empty-string-as-falsy and malformed values), the proxy drop, and USER PREVAILS. It drivesChatOptionsthrough the realToChatRequestContextpath soforwardedPropsand the client tool list travel production plumbing, not a test-only shortcut.These pin real behavior. Run against the pre-fix adapter, 6 of 13 fail — exactly the default-off, empty-string and four proxy-drop cases. The other 7 pass on both, guarding the precedence that was already correct.
A2UI suite: 98 → 111.
0 failures. All six libraries + the dojo sample build with zero warnings (
TreatWarningsAsErrors=true, so the PublicAPI and AOT/trim analyzers are enforcing). Release guards:verify-release-scope-dropdowns.shandverify-nx-release-allowlist.shboth pass.Verification gaps
dotnet packnever ran — killed by the sandbox this was developed in.AGUI.A2UI.0.0.4.nupkgis derived from verified MSBuild properties (PackageId=AGUI.A2UI,PackageVersion=0.0.4) plus the config'sname/versionSource, not confirmed against a built artifact. Worth one real pack before the first release.0.9.9leavesAGUI.A2UIat0.0.4whileAGUI.Server/Abstractionsfollow to0.9.9.Not addressed (for #2262)
a2ui_fixed_schemawas added in the last commit), and the e2e note says "three features" against four spec files.CreateA2UIAdvanced's doc claims the per-runinjectA2UIToolflag "arrive[s] on the forwardedRunAgentInput" — forag-ui-dotnetit never does; injection comes from the backend opt-in.TextContentandgenerate_a2uicalls are accumulated intohistory, so a developer's own tool calls/results (resolved by the innerUseFunctionInvocation) are dropped from planner round 2 on. Harmless for the dojo demos (no server tools on those endpoints), latent for anyone combining server tools with A2UI. The pairs are balanced and could be preserved.🤖 Generated with Claude Code