Skip to content

fix(a2ui-toolkit): validate singular child and detect child-reference cycles#1944

Merged
ranst91 merged 4 commits into
mainfrom
claude/competent-shannon-a881c9
Jun 12, 2026
Merged

fix(a2ui-toolkit): validate singular child and detect child-reference cycles#1944
ranst91 merged 4 commits into
mainfrom
claude/competent-shannon-a881c9

Conversation

@ranst91

@ranst91 ranst91 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

Closes two gaps in the A2UI component validators, matching the .NET sibling (Microsoft.Agents.AI.AGUI.A2UI) so the cross-language validation wire contract (error codes, messages) stays byte-aligned across TypeScript, Python, and .NET.

Gap 1: singular child not validated

The validators collected child references from the plural children field only. A dangling singular child (the one-child container shape Card/Button use, which the default generation prompt itself emits) passed validation and then failed at render time, defeating the recovery loop. Now both child and children are validated, and a bare-string child id resolves (TS/Python previously ignored a top-level string; .NET already handled it). Reported via the existing unresolved_child code with a per-field path (components[i].child / components[i].children).

Gap 2: no cycle / self-reference detection

None of the validators detected a component referencing itself (e.g. id avatar with child avatar) or a longer cycle, even though the default prompt requires the child tree to be a DAG. Added a new child_cycle error code and an iterative three-colour DFS over the child graph in all three toolkits:

  • Code: child_cycle
  • Path: components[id=<canonicalEntryId>]
  • Message: Child reference cycle detected: <n0> -> ... -> <n0> — nodes rotated so the smallest ordinal id leads, chain closed back to n0; each unique cycle reported once.

The DFS is iterative (explicit frame stack), not recursive, so untrusted deep child chains from model output cannot overflow the stack (.NET's StackOverflowException is uncatchable). The gray/black colour distinction prevents diamond-DAG false positives.

Tests

  • TypeScript (vitest): 84/84 — dangling singular child, self-referential child, multi-component cycle (reported once), acyclic graph, 50k-deep linear chain (no overflow) + deep chain closing into a cycle.
  • Python (unittest): 83/83 — same coverage, 5k-deep chain past CPython's recursion limit.
  • TypeScript tsc --noEmit clean.

Cross-language parity

The .NET sibling change ships separately in microsoft/agent-framework#6494 (commit 089f9166c): same child_cycle contract, same iterative DFS, verified 110/110 xunit + 12/12 e2e. All three toolkits now validate singular child and detect child-reference cycles with identical error codes and messages.

… cycles

Close two gaps in the A2UI component validators, matching the .NET sibling
(Microsoft.Agents.AI.AGUI.A2UI) so the cross-language wire contract stays aligned.

Gap 1: the validators collected child references from the plural `children`
field only, so a dangling singular `child` (the one-child container shape
Card/Button use, which the default generation prompt emits) passed validation
and failed at render time. Both `child` and `children` are now validated, and a
bare-string `child` id resolves (TS/Python previously ignored a top-level string;
.NET already handled it).

Gap 2: none of the validators detected a component referencing itself or a
longer cycle, despite the prompt requiring the child tree to be a DAG. Add an
iterative three-colour DFS over the child graph with a new `child_cycle` error
code. Cycles are canonicalised (rotated so the smallest id leads) so each unique
cycle reports once with a byte-identical message across languages. The DFS is
iterative, not recursive, so untrusted deep child chains cannot overflow the
stack.

Tests: dangling singular child, self-referential child, multi-component cycle,
acyclic graph, and deep-chain (no overflow) cases in both vitest and unittest.
@ranst91 ranst91 requested a review from a team as a code owner June 12, 2026 12:01
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ag-ui-dojo Ready Ready Preview, Comment Jun 12, 2026 4:48pm

Request Review

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1781282624 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.dev1781282624' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1781282624' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1781282624' --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.dev1781282624' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1781282624' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1781282624' --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.dev1781282624

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


Commit: 656a196

@pkg-pr-new

pkg-pr-new Bot commented Jun 12, 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@1944

@ag-ui/a2ui-middleware

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

@ag-ui/event-throttle-middleware

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

@ag-ui/mcp-apps-middleware

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

@ag-ui/mcp-middleware

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

@ag-ui/a2a

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

@ag-ui/adk

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

@ag-ui/ag2

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

@ag-ui/agno

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

@ag-ui/aws-strands

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

@ag-ui/claude-agent-sdk

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

@ag-ui/crewai

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

@ag-ui/langchain

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

@ag-ui/langgraph

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

@ag-ui/llamaindex

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

@ag-ui/mastra

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

@ag-ui/pydantic-ai

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

@ag-ui/vercel-ai-sdk

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

@ag-ui/watsonx

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

@ag-ui/a2ui-toolkit

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

create-ag-ui-app

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

@ag-ui/client

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

@ag-ui/core

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

@ag-ui/encoder

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

@ag-ui/proto

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

commit: 717d30c

…ers (#1948)

The dangling-ref check and cycle graph cover the singular child + plural
children fields only; Modal trigger/content and Tabs tabs[].child are
tracked for catalog-derived ref-field extraction in #1948.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@contextablemark contextablemark 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.

Reviewed in depth — approving.

Verified beyond the PR's own test claims:

  • TS toolkit 84/84 + tsc --noEmit clean; Python 83/83 (reproduced locally).
  • a2ui-middleware suite (the consumer this PR doesn't touch): 94/94 against this branch. The middleware's paint-gate only validates once the components array has closed, so the new checks can't false-positive on mid-stream forward refs — and a dangling/cyclic tree now correctly folds into the retrying lifecycle instead of painting broken, which is exactly the failure mode this fixes.
  • Cross-toolkit parity, empirically: ran identical payloads (dangling singular child, self-ref, multi-node cycle, mixed child/children cycle) through the TS and Python validators and diffed the JSON — byte-identical, canonical rotation included.
  • Algorithm: the iterative three-colour DFS is sound (back-edge-to-gray ⇔ cycle; gray-path slice recovers exact membership; black-skip keeps shared subtrees / diamond DAGs from false-positiving), and the explicit-stack choice is the right call for untrusted model output — the 50k/5k depth tests prove the overflow rationale.

Known remainder (non-blocking, now tracked): other catalog ref-fields — Modal trigger/content, Tabs tabs[].child — still pass dangling refs silently and are excluded from the cycle graph (verified: a cycle routed through Modal goes undetected). Filed #1948 for catalog-derived ref-field extraction in tri-toolkit lockstep, and pushed a small docs commit (717d30c) noting the scope on both adjacency builders so the cycle graph isn't assumed complete.

One release-time note: this is a behavior change (payloads that previously validated now fail and trigger recovery), so worth running the dojo a2ui e2e once when the toolkits republish.

@ranst91 ranst91 merged commit 3a4c936 into main Jun 12, 2026
16 of 18 checks passed
@ranst91 ranst91 deleted the claude/competent-shannon-a881c9 branch June 12, 2026 16:43
ranst91 added a commit that referenced this pull request Jun 16, 2026
#1944 closed the singular-child + cycle gaps for the `child`/`children`
fields. This extends both the dangling-ref check and the cycle graph to every
child reference a component makes, derived from the catalog rather than a
hardcoded field list, matching the .NET sibling (microsoft/agent-framework#6494).

A property is treated as a child reference only when its catalog schema marks it
`"format": "componentRef"` (single) or `"componentRefList"` (list); an array
property whose `items` is an object schema has its marked sub-properties honoured
per element (this is how Tabs `tabItems[].child` is found, derived, never
hardcoded). `child`/`children` stay implicit references always, so the #1944 and
catalog-free structural behaviour is unchanged. An unmarked property is data,
never a reference: a bare data string ("$289") and a bare ref string ("card")
are otherwise indistinguishable, so shape-based detection is unsafe.

A new `collectComponentRefEdges` / `_collect_component_ref_edges` returns
(path, ref) pairs consumed by both the dangling-ref check and the cycle
adjacency, so a cycle routed through a marked field (e.g. Modal `content`) is
now detected. Paths are byte-aligned with the sibling toolkits: single-ref
`components[i].<field>`, list-ref `components[i].<field>[k]`, nested
`components[i].<arrayField>[k].<refField>`. Extended fields are gated on a marked
catalog; with no catalog only `child`/`children` are checked, exactly as before.

Note: hosts must enrich their catalogs to emit the `format` markers for this to
fire; the dojo demos pass no catalog, so there is no dojo behaviour change.
Catalog producers emitting markers is downstream follow-up.

Tests (vitest + unittest): Modal trigger/content dangling, unmarked data string
not flagged, nested tabItems[k].child per-index, cycle through a marked field,
list-ref array per-index, and marked fields ignored without a catalog. All #1944
child/children + deep-chain tests stay green.
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.

2 participants