Skip to content

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
claude/a2ui-dotnet-review-783bd9from
mark/a2ui-dotnet-inject-parity
Draft

fix(dotnet): align A2UI injectA2UITool with the sibling adapter contract + wire AGUI.A2UI into CI/release#2289
contextablemark wants to merge 2 commits into
claude/a2ui-dotnet-review-783bd9from
mark/a2ui-dotnet-inject-parity

Conversation

@contextablemark

Copy link
Copy Markdown
Contributor

What

Review follow-ups for #2262, targeting its branch so they land with it. Two independent problems: the injectA2UITool contract, and the fact that AGUI.A2UI is 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. injectA2UITool diverged from the sibling adapters

injectA2UITool is not just a boolean gate — it's half of a handshake. The middleware's injectToolAndFlag sets forwardedProps.injectA2UITool and appends a render_a2ui proxy to input.tools in the same inseparable step, and the type is boolean | string where a string names that proxy. ADK, AWS Strands and Mastra therefore all implement the same six steps.

Step ADK / Strands / Mastra A2UIChatClient before Now
forwarded ?? config precedence ✅ already correct
USER PREVAILS on dev-wired tool ✅ already correct
Default when nothing is set off on ✅ off
String form names the proxy ignored ✅ honored
Drop the injected render proxy absent ✅ dropped

The proxy drop is the substantive fix. RunAgentInputExtensions maps input.Tools onto ChatOptions.Tools, so whenever the flag arrives the proxy arrives with it. The planner then saw both render_a2ui and generate_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 injectsA2UITool for langgraph* / mastra-agent-local only — ag-ui-dotnet is deliberately excluded. So the flag is never forwarded, no proxy is ever injected, and input.tools is empty. #2262 cites exactly that as proof of pillar 1 ("generate_a2ui appears with request tools: []"), 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 into ag-ui state and the graph decides.

The dojo demos now opt in server-side via that same backend mechanism, so behavior is unchanged. a2ui_fixed_schema needed nothing (never wrapped).

API shape — one call worth your review

InjectA2UITool stays bool? and gains a sibling InjectedRenderToolName (string?) rather than emulating TS's boolean | string union 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 from bool/string would do it — say the word.

2. AGUI.A2UI was invisible to CI and release

Neither 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 declaring PackageId/Description/PackageTags.

  • CI — build src/AGUI.A2UI, run tests/AGUI.A2UI.UnitTests, in both unit-dotnet-sdk.yml and the publish-release.yml pre-publish gate.
  • Release — enrolled in its own sdk-dotnet-a2ui scope, not the shared sdk-dotnet train, mirroring how its own siblings sdk-ts-a2ui-toolkit / sdk-py-a2ui-toolkit are already sequenced.

autoPublish, and why a scope alone isn't enough

For 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 autoPublish flag. The scope stays enrolled — discoverable, canary-selectable, covered by the scope guards — but is skipped by the automatic sweep. Flipping it to true (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.

⚠️ The predicate is != false, not (.autoPublish // true). jq's // treats false as absent, so false // true evaluates to true — my first attempt was a silent no-op that still published A2UI. Caught only by testing the predicate rather than trusting it; there's a comment warning off the // form.

Version pinning

AGUI.A2UI 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 train's shared default.

Package Version Train
@ag-ui/core 0.0.57 sdk-ts
ag-ui-protocol 0.1.19 sdk-py
@ag-ui/a2ui-toolkit 0.0.4 independent
ag-ui-a2ui-toolkit 0.0.4 independent

Without 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.4 is a coincidence that masks the drift.

0.0.4 is a judgment call and the one line to change if you disagree — 0.0.1 is 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 a sdk-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 drives ChatOptions through the real ToChatRequestContext path so forwardedProps and 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.

Suite net8.0 net9.0 net10.0
AGUI.A2UI.UnitTests 111 ✅ 111 ✅ 111 ✅
AGUI.Abstractions.UnitTests 185 ✅ 185 ✅ 185 ✅
AGUI.Client.UnitTests 114 ✅ 114 ✅ 114 ✅
AGUI.Server.UnitTests 119 ✅ 119 ✅ 119 ✅
AGUI.Protobuf.UnitTests 53 ✅ 53 ✅ 53 ✅
AGUI.Formatting.UnitTests 8 ✅ 8 ✅ 8 ✅
Hosting.AspNetCore.IntegrationTests 117 ✅

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.sh and verify-nx-release-allowlist.sh both pass.

Verification gaps

  • dotnet pack never ran — killed by the sandbox this was developed in. AGUI.A2UI.0.0.4.nupkg is derived from verified MSBuild properties (PackageId=AGUI.A2UI, PackageVersion=0.0.4) plus the config's name/versionSource, not confirmed against a built artifact. Worth one real pack before the first release.
  • Version decoupling was verified empirically: bumping the train to 0.9.9 leaves AGUI.A2UI at 0.0.4 while AGUI.Server/Abstractions follow to 0.9.9.
  • e2e/Playwright and live-model runs were not executed.

Not addressed (for #2262)

  • Its description says three dojo demos; there are four (a2ui_fixed_schema was added in the last commit), and the e2e note says "three features" against four spec files.
  • CreateA2UIAdvanced's doc claims the per-run injectA2UITool flag "arrive[s] on the forwarded RunAgentInput" — for ag-ui-dotnet it never does; injection comes from the backend opt-in.
  • Planner-history lossiness: only TextContent and generate_a2ui calls are accumulated into history, so a developer's own tool calls/results (resolved by the inner UseFunctionInvocation) 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

contextablemark and others added 2 commits July 30, 2026 21:37
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>
@github-actions

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1785472836 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then 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 testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1785472836

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: f76ba3d

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2289

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2289

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2289

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2289

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2289

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2289

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2289

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2289

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2289

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2289

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2289

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2289

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2289

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2289

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2289

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2289

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2289

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2289

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2289

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2289

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2289

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2289

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2289

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2289

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2289

commit: 7427603

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant