fix(a2ui-toolkit): validate singular child and detect child-reference cycles#1944
Conversation
… 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.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 testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1781282624
Commit: 656a196 |
@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: |
contextablemark
left a comment
There was a problem hiding this comment.
Reviewed in depth — approving.
Verified beyond the PR's own test claims:
- TS toolkit 84/84 +
tsc --noEmitclean; 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
retryinglifecycle 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/childrencycle) 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.
#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.
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
childnot validatedThe validators collected child references from the plural
childrenfield only. A dangling singularchild(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 bothchildandchildrenare validated, and a bare-stringchildid resolves (TS/Python previously ignored a top-level string; .NET already handled it). Reported via the existingunresolved_childcode 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
avatarwith childavatar) or a longer cycle, even though the default prompt requires the child tree to be a DAG. Added a newchild_cycleerror code and an iterative three-colour DFS over the child graph in all three toolkits:child_cyclecomponents[id=<canonicalEntryId>]Child reference cycle detected: <n0> -> ... -> <n0>— nodes rotated so the smallest ordinal id leads, chain closed back ton0; 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
child, self-referentialchild, multi-component cycle (reported once), acyclic graph, 50k-deep linear chain (no overflow) + deep chain closing into a cycle.tsc --noEmitclean.Cross-language parity
The .NET sibling change ships separately in microsoft/agent-framework#6494 (commit
089f9166c): samechild_cyclecontract, same iterative DFS, verified 110/110 xunit + 12/12 e2e. All three toolkits now validate singularchildand detect child-reference cycles with identical error codes and messages.