Skip to content

.NET: A2UI (Agent-to-UI) toolkit, adapter, and AG-UI streaming#6494

Open
ranst91 wants to merge 24 commits into
microsoft:mainfrom
ranst91:feat/a2ui-toolkit-dotnet
Open

.NET: A2UI (Agent-to-UI) toolkit, adapter, and AG-UI streaming#6494
ranst91 wants to merge 24 commits into
microsoft:mainfrom
ranst91:feat/a2ui-toolkit-dotnet

Conversation

@ranst91

@ranst91 ranst91 commented Jun 12, 2026

Copy link
Copy Markdown

Ports the A2UI generation toolkit to .NET, matching the existing TypeScript
(@ag-ui/a2ui-toolkit) and Python (ag-ui-a2ui-toolkit) implementations, so a .NET agent
can produce A2UI v0.9 surfaces (agent-generated UI) over the AG-UI protocol. The
cross-language wire contract (envelope shape, validation codes, recovery semantics, prompt
defaults) stays aligned.

What's added

  • Microsoft.Agents.AI.AGUI.A2UI (new, IsPackable=false, internal building block):
    component validator, validate-and-retry recovery loop (returns a structured
    a2ui_recovery_exhausted envelope on exhaustion rather than throwing), prior-surface walk,
    and envelope assembly with untrusted-output narrowing. Ported behavior-for-behavior from the
    siblings.
  • A2UIAgent / AGUIContextAgent, the Agent Framework adapter (the .NET getA2UITools
    equivalent). A2UIAgent injects a per-run generate_a2ui tool backed by a render subagent.
    AGUIContextAgent replays forwarded ag_ui_context into the prompt, the piece that makes
    the zero-config path work.
  • Incremental tool-call argument streaming in the ASP.NET Core hosting conversion, so A2UI
    surfaces paint component-by-component instead of in one bulk update. On the subagent path the
    generation loop runs at the agent level so the inner render_a2ui args reach the wire
    incrementally, the same event shape the LangGraph adapters produce.
  • AGUIDojoServer sample (plus README): /a2ui_fixed_schema, /a2ui_dynamic_schema,
    /a2ui_recovery, /a2ui_chat.

Design notes (flagged for direction)

  1. New OpenAI dependency on Hosting.AGUI.AspNetCore. Microsoft.Extensions.AI
    coalesces tool-call arguments; the per-fragment deltas a progressive UI needs survive only
    on the provider's RawRepresentation, so the tap reaches into
    OpenAI.Chat.StreamingChatCompletionUpdate. It is #if ASPNETCORE-gated and fails soft
    (non-OpenAI providers keep atomic args, so surfaces still render). This couples hosting to
    one SDK, which we would rather avoid. The preferred fix is a provider-neutral
    incremental-tool-call type in MEAI, after which the tap deletes itself. Guidance welcome.
  2. Should hosting replay forwarded context to the prompt by default? AGUIContextAgent
    exists only because MapAGUI forwards ag_ui_context but nothing replays it into the model
    prompt. This could arguably be a hosting default. Flagging it.

Testing

Microsoft.Agents.AI.AGUI.A2UI.UnitTests (103) plus hosting streaming-tap cases; no
regressions in AGUI.UnitTests (216) or hosting integration tests (32). End-to-end verified
against the AG-UI Dojo: fixed/dynamic/recovery/zero-config surfaces stream progressively,
recovery recovers, multi-turn stays valid. (Local net10.0; full TFM matrix in CI.)

ranst91 added 19 commits June 12, 2026 10:01
Adds the Microsoft.Agents.AI.AGUI.A2UI project (framework-agnostic A2UI
generation helpers ported from @ag-ui/a2ui-toolkit and ag-ui-a2ui-toolkit)
and its unit test project. Cross-language wire-contract constants are
implemented and pinned by tests; operation builders are stubbed and their
ported tests fail by design until the implementation lands.

Signed-off-by: ran <ran@copilotkit.ai>
Ports all test classes from the sibling toolkits (Python tests/test_toolkit.py,
tests/test_validate.py, tests/test_recovery.py and the TypeScript mirror suite)
to xunit: validator semantics, recovery-loop semantics, prompt assembly,
prior-surface reconstruction (11 ordering/delete cases), envelope assembly and
untrusted-output narrowing, and tool-parameter resolution. The public API
surface is stubbed (NotImplementedException), so 74 of 78 tests fail by design;
constants pins are green. Duck-typing cases covered by the .NET type system
(dict-shaped messages, non-list components) are documented instead of ported.

Signed-off-by: ran <ran@copilotkit.ai>
Implements the full toolkit ported from @ag-ui/a2ui-toolkit (TypeScript) and
ag-ui-a2ui-toolkit (Python): semantic component validation, the validate-and-
retry generation recovery loop, operation builders, prompt assembly with
verbatim default generation/design guidelines, prior-surface reconstruction,
request preparation, envelope assembly with untrusted-output narrowing, and
tool-parameter resolution. All 78 ported unit tests pass on net10.0; net472
test execution is a Windows-CI concern (zero tests run locally on macOS for
sibling AGUI test projects as well).

Signed-off-by: ran <ran@copilotkit.ai>
Adds the Agent Framework adapter over the framework-agnostic A2UI toolkit:
A2UIAgent wraps any AIAgent and injects a per-run generate_a2ui AIFunction
that reads the run's conversation history (prior-surface lookup) and the
AG-UI context forwarded by MapAGUI (component catalog), delegates surface
generation to a subagent chat client with a forced render_a2ui structured
tool call, and returns a validated A2UI operations envelope through the
shared recovery loop. Mirrors the LangGraph adapters' getA2UITools factory
shape. Includes unit tests over a scripted subagent chat client.

Signed-off-by: ran <ran@copilotkit.ai>
Adds three AG-UI dojo demos exercising the A2UI toolkit and adapter:
- /a2ui_fixed_schema — author-owned card layouts (flights/hotels) where plain
  backend tools wrap agent-supplied data in A2UI operations envelopes.
- /a2ui_dynamic_schema — A2UIAgent adapter path: a subagent designs the UI via
  the generate_a2ui tool against the dojo's dynamic catalog.
- /a2ui_recovery — same path with the validate-and-retry recovery loop capped
  at three attempts for the failure showcase.

Also makes the sample's model configuration dual-mode: the existing
AZURE_OPENAI_ENDPOINT path, or OPENAI_API_KEY with an optional OPENAI_BASE_URL
override (enables deterministic end-to-end tests against a local mock server).

Signed-off-by: ran <ran@copilotkit.ai>
A string function result is JSON-serialized a second time on its way into the
AG-UI tool-result event, double-encoding the A2UI envelope and breaking the
middleware's hard-failure detection. Returning the parsed JsonElement keeps
the envelope single-encoded on the wire.

Signed-off-by: ran <ran@copilotkit.ai>
… events

MEAI chat clients attach the typed FunctionCallContent only once a tool call's
arguments are complete, so the AG-UI wire carried a single atomic
TOOL_CALL_ARGS event per call — starving streaming consumers such as
generative-UI middlewares that paint arguments progressively. The OpenAI
argument fragments are still observable on each update's RawRepresentation:
the hosting-side event conversion now surfaces them as incremental
TOOL_CALL_ARGS events (one ToolCallStart per call, args deltas as they
arrive) and suppresses the duplicate atomic emission when the coalesced
FunctionCallContent follows, emitting only the closing event. Providers
without raw OpenAI fragments keep the existing behavior.

Verified live: a 36-fragment streamed call now yields 35 incremental
TOOL_CALL_ARGS events (previously 1). Adds five unit tests; no regressions
across the AGUI, hosting, and integration suites.

Signed-off-by: ran <ran@copilotkit.ai>
AGUIContextAgent surfaces forwarded AG-UI context entries (the A2UI
middleware's injected render-tool usage guide and component schema) to the
model as a system message — the piece that makes the zero-configuration A2UI
path work end to end: the client middleware injects the render_a2ui tool, the
hosting layer binds it automatically, and with this wrapper a plain chat agent
renders A2UI surfaces with progressively streamed tool arguments and no
A2UI-specific agent code. Adds the /a2ui_chat endpoint to the AGUIDojoServer
sample.

Signed-off-by: ran <ran@copilotkit.ai>
…ls at end of stream

Fixes for the raw argument-streaming tap:

- Per-call state is released when the coalesced FunctionCallContent closes a
  raw-streamed call. OpenAI restarts tool-call indexes at 0 for each assistant
  turn, so a stale index entry silently absorbed a later round's fragments
  into the finished call and the new call's coalesced content re-emitted a
  duplicate atomic Start/Args/End.
- An end-of-stream sweep closes any raw-streamed call whose coalesced content
  never arrived, so the wire never carries Start/Args without End.
- The raw-path ToolCallStartEvent's possibly-null ParentMessageId is
  documented (schema-legal; ignored by the inbound builder).

Call-site enumeration:
- rawToolCallIdsByIndex: TryGetValue start-gate — holds, now sees a fresh
  index after a close; assignment site unchanged; new removal scoped to the
  just-closed call only.
- rawStreamedToolCallIds: Add unchanged; dedupe-branch Contains became Remove
  (single consumer, End-only semantics preserved); new end-of-stream sweep
  reads then clears; Clear is idempotent.

Tests: +3 (index reuse across rounds, end-of-stream sweep, parallel calls now
assert one End per started call).

Signed-off-by: ran <ran@copilotkit.ai>
Fixes:

- FindPriorSurface deep-clones the captured component/data nodes: the public
  A2UIPriorSurface record exposes them, and a parent-attached JsonNode throws
  when a caller re-attaches it elsewhere.
- ReadAgentState is now internal and shared by AGUIContextAgent, so both
  wrappers route the catalog schema entry into the canonical
  '## Available Components' section instead of diverging on the same input.
- ToHistoryMessage walks every FunctionResultContent on a tool message
  (parallel calls) and accepts JsonNode-shaped results.

Call-site enumeration:
- ReadAgentState: A2UIAgent.PrepareRun (unchanged semantics) and the new
  AGUIContextAgent.WithContextPrompt caller — identical input contract.
- ToHistoryMessage: single caller CreateGenerateA2UIFunction — only broader
  content acceptance.
- FindPriorSurface captures: all existing consumers were read-only; clones
  preserve their behavior.

Tests: +7 (JsonElement argument marshalling, schema/context routing,
AGUIContextAgent behaviors, update-with-data envelope, prior-node
detachability).

Signed-off-by: ran <ran@copilotkit.ai>
Fixes:

- Agent descriptions no longer claim Azure OpenAI: the dual-mode
  initialization made those claims false on the OpenAI-compatible path.
- Endpoint configuration values are validated with Uri.TryCreate and fail
  with the offending key named instead of a bare UriFormatException.
- UserSecretsId is a valid GUID (the previous value contained non-hex
  characters, breaking the dotnet user-secrets workflow).
- The fixed-schema A2UI tools pass the source-generated serializer options
  like every other tool in the sample (JsonArray/JsonObject registered in the
  context); verified live against the mock server.

Signed-off-by: ran <ran@copilotkit.ai>
…streaming sweep

A2UIAgent.PrepareRun rebuilt the run options from scratch, silently dropping
the base AgentRunOptions members a caller may have set (ContinuationToken,
AllowBackgroundResponses, AdditionalProperties, ResponseFormat). The options
are now cloned — via ChatClientAgentRunOptions.Clone() when the caller passed
that type, or by copying the base members otherwise — and only the ChatOptions
tool list is augmented. Callers' options objects are no longer mutated.

In the AG-UI streaming conversion, the end-of-stream sweep that closes
raw-streamed tool calls now walks calls in tool-call index order so the
emitted ToolCallEnd events are deterministic, and the coalesced-call branch
closes any open reasoning block before emitting ToolCallEnd, matching the
other tool-event paths.

Also: derive RenderA2UIToolDeclaration.Description from the canonical tool
definition instead of a duplicated literal, pin the remaining wire-contract
constants in tests, document the deliberate pricePerNight/price cross-naming
in the fixed-schema sample, clarify the recovery sample's validation scope,
and add tests for run-option preservation, caller-tool retention, the
streaming entry point, and update-intent prior-surface lookup through
conversation history.

Signed-off-by: ran <ran@copilotkit.ai>
Tighten the parallel tool-call streaming test to assert the deterministic
index-ordered close sweep and the per-call start events on the wire instead
of comparing a re-sorted list, document the a2ui_recovery activity-type
constant's role in the cross-language contract, reference the default attempt cap by its constant
in the recovery sample, and drop an unused FluentAssertions package
reference from the toolkit test project.

Signed-off-by: ran <ran@copilotkit.ai>
The recovery status channel rides the a2ui-surface lifecycle activity today;
this constant is reserved wire vocabulary mirroring the TypeScript toolkit's
A2UI_RECOVERY_ACTIVITY_TYPE, not something the middleware currently emits as
an activity type. Say so instead of implying active emission.

Signed-off-by: ran <ran@copilotkit.ai>
The streaming entry point ran generate_a2ui through automatic function
invocation, so the planner's tool call and the render subagent's output were
both swallowed inside one inner-agent turn: the wire carried a single atomic
tool result and the surface painted in one bulk update with no loading state.

A2UIAgent.RunCoreStreamingAsync now runs the invocation loop at the agent
level. generate_a2ui is advertised as a schema-only declaration so the
planner's call surfaces on the update stream; the adapter then runs the render
subagent with a streaming chat call and forwards its updates, so the hosting
layer emits the inner render_a2ui argument fragments incrementally and the
surface paints one component at a time. A streaming twin of the recovery loop
makes each retry a fresh, visible subagent call. The validated envelope is
fed back to the planner as a tool result and the conversation continues — the
same event shape the LangGraph adapters produce. The non-streaming
RunCoreAsync path is unchanged.

Tests: +2 (subagent updates forwarded with the result fed back to the
planner; an invalid first attempt retries with a second visible subagent
call), and the existing injection test now asserts a declaration rather than
an invocable function.

Signed-off-by: ran <ran@copilotkit.ai>
Streaming the subagent's render_a2ui call onto the wire makes it part of the
persisted conversation, but its output was consumed internally to build the
generate_a2ui envelope, so the call had no matching tool result. A later turn
replaying that history failed (e.g. OpenAI rejects an assistant tool call with
no responding tool message), so the next user message produced no response.

Each forwarded render_a2ui call now emits a bare acknowledgement tool result,
keeping the conversation valid across turns. The painted surface still comes
from the streamed arguments, so the result carries no operations.

Tests: the two streaming tests now assert every forwarded render call is
balanced with its own result.

Signed-off-by: ran <ran@copilotkit.ai>
…e the planner-round cap, share attempt validation

Refinements to the streaming generate_a2ui path:

- The render subagent's arguments were read off each streamed update and
  overwritten, which keeps only the last fragment if a chat client streams a
  tool call's arguments across updates. The updates are now accumulated and
  coalesced via ToChatResponse() before the complete render_a2ui call is read,
  matching the non-streaming path.
- When the planner kept requesting generations through the round cap, the loop
  ended on an unanswered tool result with no closing assistant turn. It now
  runs one final planner turn with the generate tool withheld, so the planner
  narrates and cannot request another surface.
- The per-attempt validate-and-record step is extracted into
  A2UIGenerationRecovery.ValidateAttempt and shared by the streaming loop and
  the non-streaming recovery loop, so the two cannot drift on attempt semantics.

Tests: +3 streaming cases (recovery exhaustion with the OnAttempt callback,
update intent finding a prior surface through the streamed history, and the
planner-round cap closing with the generate tool withheld).

Signed-off-by: ran <ran@copilotkit.ai>
…, isolate the closing-turn options, guard a non-positive attempt cap

Refinements to the streaming generate_a2ui path:

- The per-round assistant message fed back to the planner was reconstructed
  from only its generate_a2ui calls, dropping any narration the planner
  streamed in the same turn. The planner's text content is now preserved
  alongside the calls so the resent history is not lossy.
- The round-cap closing turn mutated the loop's shared chat options to
  withhold the generate tool; it now runs against a fresh options instance.
- A non-positive A2UIRecoveryConfig.MaxAttempts skipped the loop entirely and
  emitted a confusing 'after 0 attempt(s)' envelope. A new shared
  A2UIGenerationRecovery.ResolveMaxAttempts treats a non-positive cap as unset
  and falls back to the default, used by both generation paths.

Tests: +1 (planner narration survives into the next round's history).
Signed-off-by: ran <ran@copilotkit.ai>
Add a README for AGUIDojoServer listing its per-feature AG-UI endpoints
(including the four A2UI demos), the dual Azure-OpenAI / OpenAI-compatible
configuration, and how to run it against the AG-UI Dojo viewer, and reference
it from the parent AGUIClientServer README.

Signed-off-by: ran <ran@copilotkit.ai>
Copilot AI review requested due to automatic review settings June 12, 2026 08:09
@moonbox3 moonbox3 added documentation Improvements or additions to documentation .NET labels Jun 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds progressive tool-call argument streaming support (via OpenAI raw updates) and introduces a new A2UI toolkit library with comprehensive unit tests and sample endpoints.

Changes:

  • Emit incremental ToolCallArgsEvent frames from OpenAI RawRepresentation fragments and suppress duplicate atomic args emission.
  • Add Microsoft.Agents.AI.AGUI.A2UI toolkit (prompting, envelope building, validation, recovery loop) plus extensive unit coverage.
  • Expand the Dojo sample server with multiple A2UI endpoints and OpenAI/Azure OpenAI configuration support.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/StreamingToolCallArgsTests.cs Adds unit tests validating incremental tool-call arg streaming behavior.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests.csproj Introduces a new unit test project for the A2UI toolkit.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/AGUIContextAgentTests.cs Tests that forwarded AG-UI context is surfaced to the model.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIPromptBuildingTests.cs Tests prompt assembly for context + subagent prompts.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIOperationBuilderTests.cs Tests v0.9 operation builder output shapes.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIGenerationRecoveryTests.cs Tests validate-and-retry recovery loop semantics and envelopes.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIFindPriorSurfaceTests.cs Tests conversation-history surface reconstruction rules.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIEnvelopeTests.cs Tests envelope assembly, tool defs, and param defaulting.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIConstantsTests.cs Pins cross-language constants to prevent protocol drift.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIComponentValidatorTests.cs Tests structural/semantic validation behavior.
dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/A2UIAgentTests.cs Tests A2UIAgent tool injection and streaming/non-streaming behavior.
dotnet/src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj Adds OpenAI dependency needed for raw-update tap on ASP.NET Core build.
dotnet/src/Microsoft.Agents.AI.AGUI/Shared/ChatResponseUpdateAGUIExtensions.cs Implements incremental tool-call arg streaming from OpenAI raw updates.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/Microsoft.Agents.AI.AGUI.A2UI.csproj Adds new internal A2UI toolkit project and dependencies.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/AGUIContextAgent.cs Adds agent wrapper to prepend forwarded AG-UI context as a system message.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UIValidation.cs Adds A2UI validator, error codes, and catalog model.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UITypes.cs Adds shared A2UI types (state, history, prompts, prior surface, etc.).
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UIToolkit.cs Adds prompt building, surface-walk, request prep, and envelope helpers.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UIToolParams.cs Adds shared tool params and canonical tool definitions/defaults.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UIOperationBuilder.cs Adds builders for create/update operations (v0.9).
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UIGenerationRecovery.cs Adds async recovery loop and exhaustion envelope logic.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UIConstants.cs Adds protocol constants shared cross-language.
dotnet/src/Microsoft.Agents.AI.AGUI.A2UI/A2UIAgent.cs Adds Agent Framework adapter that injects generate_a2ui tool per run.
dotnet/samples/05-end-to-end/AGUIClientServer/README.md Documents new Dojo server project within the sample.
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/README.md Adds endpoint list and configuration docs for Dojo server.
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/Program.cs Maps additional A2UI endpoints in Dojo server.
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/ChatClientAgentFactory.cs Adds A2UI demo agent factories and OpenAI-compatible configuration.
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServerSerializerContext.cs Adds JsonNode types to source-gen serialization context.
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/AGUIDojoServer.csproj Fixes BOM/metadata; references new A2UI toolkit project.
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/A2UI/A2UIFixedSchemaTools.cs Adds fixed-schema A2UI tools emitting operations envelopes.
dotnet/samples/05-end-to-end/AGUIClientServer/AGUIDojoServer/A2UI/A2UICompositionGuides.cs Adds Dojo-specific composition guides for A2UI subagent prompting.
dotnet/agent-framework-dotnet.slnx Adds new A2UI project + test project to the solution.
Comments suppressed due to low confidence (1)

dotnet/tests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests/Microsoft.Agents.AI.AGUI.A2UI.UnitTests.csproj:1

  • This test project file is missing the usual test-project configuration (e.g., TargetFramework(s), IsTestProject, and test dependencies like xUnit + test SDK), unless those are injected via shared repo-wide props/targets. If they are not centrally provided, the project won’t compile or run tests; consider aligning it with the structure of existing *.UnitTests.csproj files in the repo.

Comment on lines +176 to +183
var closingChatOptions = chatOptions.Clone();
closingChatOptions.Tools = (chatOptions.Tools ?? Enumerable.Empty<AITool>())
.Where(t => !string.Equals(t.Name, generateTool.Name, StringComparison.Ordinal))
.ToList();
var closingOptions = new ChatClientAgentRunOptions { ChatOptions = closingChatOptions, ChatClientFactory = runOptions.ChatClientFactory };
await foreach (AgentResponseUpdate update in this.InnerAgent
.RunStreamingAsync(history, pendingSession, closingOptions, cancellationToken)
.ConfigureAwait(false))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in this commit

Comment on lines +129 to +140
var ids = new HashSet<string>(StringComparer.Ordinal);
foreach (JsonNode? node in components)
{
if (node is JsonObject component && component["id"] is JsonValue idValue &&
idValue.TryGetValue(out string? id) && !ids.Add(id))
{
errors.Add(new A2UIValidationError(
A2UIValidationErrorCodes.DuplicateId,
$"components[id={id}]",
$"Duplicate component id '{id}'"));
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in this commit

Comment on lines +189 to +200
if (component is not null)
{
foreach (string reference in CollectChildReferences(component["children"]))
{
if (!ids.Contains(reference))
{
errors.Add(new A2UIValidationError(
A2UIValidationErrorCodes.UnresolvedChild,
$"components[{i}].children",
$"Child reference '{reference}' does not match any component id"));
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in this+this

Comment on lines +621 to +636
if (rawStreamedToolCallIds.Remove(functionCallContent.CallId))
{
int? closedIndex = null;
foreach (KeyValuePair<int, string> entry in rawToolCallIdsByIndex)
{
if (string.Equals(entry.Value, functionCallContent.CallId, StringComparison.Ordinal))
{
closedIndex = entry.Key;
break;
}
}

if (closedIndex is int index)
{
rawToolCallIdsByIndex.Remove(index);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed here

Comment on lines +29 to +32
<ItemGroup>
<!-- Raw-representation tap for progressive tool-call argument streaming. -->
<PackageReference Include="OpenAI" />
</ItemGroup>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repo uses Central Package Management (ManagePackageVersionsCentrally=true in Directory.Packages.props), so versions are pinned centrally, not on the PackageReference. OpenAI is pinned there at 2.10.0 (Directory.Packages.props:120). An unversioned PackageReference is the required pattern under CPM adding a version here would actually error

Comment on lines +129 to +140
var ids = new HashSet<string>(StringComparer.Ordinal);
foreach (JsonNode? node in components)
{
if (node is JsonObject component && component["id"] is JsonValue idValue &&
idValue.TryGetValue(out string? id) && !ids.Add(id))
{
errors.Add(new A2UIValidationError(
A2UIValidationErrorCodes.DuplicateId,
$"components[id={id}]",
$"Duplicate component id '{id}'"));
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Find the fixed here and here

ranst91 added 5 commits June 12, 2026 10:27
The round-cap closing turn rebuilt the run options as a partial
ChatClientAgentRunOptions carrying only ChatOptions and ChatClientFactory,
dropping the base AgentRunOptions members (continuation token,
background-response opt-in, additional properties, response format) for that
final planner turn. Clone the run options instead, then strip only the
generate tool from the cloned chat options, so all members survive.

Tests: the planner-round-cap test now passes base run-option members and
asserts they reach the closing turn.

Signed-off-by: ran <ran@copilotkit.ai>
The duplicate-id pass added every string id to its set, so two components
with an empty-string id flagged each other as duplicate_id on top of (or
instead of) the intended missing_id. Skip empty ids in the duplicate pass so
they are reported once as a missing id.

Tests: empty-string ids report missing_id and not duplicate_id.
Signed-off-by: ran <ran@copilotkit.ai>
The validator collected child references from the plural `children` field
only, so a dangling singular `child` reference (the one-child container shape
the default prompt uses for Card and Button) passed validation and surfaced
as a render-time failure the recovery loop could not catch. It now validates
both `child` and `children`, and CollectChildReferences handles a bare string
id (the singular shape) in addition to arrays and templates.

Scope: dangling-reference resolution only; cycle/self-reference detection
remains a separate gap.

Tests: a dangling singular `child` reports unresolved_child; a resolved one
is valid.

Signed-off-by: ran <ran@copilotkit.ai>
The OpenAI streaming tap recovered a finished tool call's fragment index with
a linear scan over the index map. Track a callId-to-index map alongside the
index-to-callId map so the close is a direct lookup, and keep the two in
lockstep on open and close. The end-of-stream sweep still emits closes in
tool-call index order. No change to the emitted event sequence.

Signed-off-by: ran <ran@copilotkit.ai>
A component whose child/children references form a cycle (a self-reference
like id 'avatar' with child 'avatar', or a longer loop) passed validation and
never terminated at render time, even though the generation prompt tells the
model the child tree must be a DAG. The validator now walks the child graph
and reports a child_cycle error, feeding the failure back into the recovery
loop instead of letting it reach the renderer.

The walk is an iterative (explicit-stack) depth-first search so a pathologically
deep child chain in untrusted model output cannot overflow the call stack. Each
cycle is canonicalised (rotated so the lexicographically smallest id leads) and
reported once; the error code and message format match the TypeScript and Python
toolkits for cross-language parity.

Tests: self-reference and multi-component cycles report child_cycle (once), an
acyclic graph does not, and a 20k-deep linear chain validates without overflow.

Signed-off-by: ran <ran@copilotkit.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation .NET

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants