feat: industry-standard conformance — AG-UI transport + schema-verified A2UI (phase 1) - #79
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Owner
Author
|
Pre-merge review pass (multi-agent finders + verification) — findings and dispositions: Fixed in
Deferred, deliberately:
🤖 Generated with Claude Code |
This was referenced Aug 30, 2026
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 18:30
ba8137b to
872c436
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 18:38
872c436 to
42267eb
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 18:49
42267eb to
497fce3
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 18:55
497fce3 to
ce549b4
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 18:56
ce549b4 to
db57202
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 19:06
db57202 to
ff4e6ac
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 19:14
ff4e6ac to
9aa18cd
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 19:28
9aa18cd to
4e886aa
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 19:38
4e886aa to
9c6a033
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 19:41
9c6a033 to
24966e8
Compare
adamstankiewicz
force-pushed
the
feat/a2ui-agui-conformance
branch
from
August 30, 2026 19:45
24966e8 to
7a65b2f
Compare
Presentation is the renderer's half of the A2UI protocol — the surface carries semantics, the host draws them in its own idiom. Our demo renderer was being generic to the point of foreignness, which made the projection look broken rather than portable. Now it draws like the app: cards get the native card treatment, a tab switch turns the panel over with a flip (the whole panel is tappable to advance), and a horizontal List pages like a deck, one card in view. The flashcard mapping uses that List for its deck — the catalog had the word for it all along. None of this exceeds the surface's semantics: no invented state, no smuggled progress. What still separates the two columns is what the basic catalog cannot say — per-card progress, checked completion, markdown — which is the versioned a2learn catalog's job (#98). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Card whose sole child is a two-tab Tabs is a thing with two faces, and the renderer now draws it as one: a single container rotating in perspective, two backface-hidden faces sharing a grid cell so the taller face sets the height, tap anywhere to turn it over. The caption names the other face from the surface's own tab titles — no flashcard vocabulary baked into the renderer, and no semantics beyond what the surface already modeled. The pill-tab presentation remains for Tabs in any other composition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third mapped kind, and the first whose fixture exercises Tabs beyond two faces: each step is a tab (title, body, optional why as caption) inside a card, so the walk-through survives as navigation. The gating does not — tabs browse freely where the native widget enforces one-at-a-time disclosure — and the mapper says so instead of hiding it. The a2learn-catalog column needed zero changes: parity by construction, as claimed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renderer affordances for a horizontal List: paging buttons in the app's own idiom, dots for position, snap-scroll kept so swiping still works, scroll position read back so the two stay in sync. Buttons disable at the ends. Position is presentation of where the reader is in a list the surface already modeled — no new state invented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The native flashcard changes cards with a 320ms slide-and-fade keyed on the card, not a scroll — the deck now does the same: one item in view, remounted per page with the identical curve and offset, direction-aware. The snap-scroll container goes away, which also removes the deck's only horizontal scroll surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The native flashcard ends on its own CTA — the last card's Next becomes the completion action. The deck now composes the same way: a horizontal List immediately followed by an action Button folds that button into the paging controls as the final page's primary action, label taken from the surface's own button text. Same button, same action, presented where the reader finishes; nothing dropped, nothing added. A deck without a trailing button (or a button elsewhere, like step-reveal's) is left exactly as the surface laid it out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The catalog's Text is specified to support simple Markdown (without HTML, images, or links) — drawing literal ** marks was under-rendering the spec, not honesty. The renderer now uses the same react-markdown stack as the native widgets, scoped exactly to the catalog: GFM formatting renders, raw HTML is ignored, links and images unwrap to their text. The fidelity notes shrink accordingly — what a body actually loses in projection is links and images, and the docs now say that instead of overclaiming a plain-text ceiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three long tab titles crowding one row communicated nothing about stepping through — the wrong catalog word. A walk-through is a sequence, and the catalog's sequence primitive is the horizontal List: one Card per step (title, divider, body, why as caption), paged by the renderer with Back/Next, and the trailing done button folding in as the final step's action. No new primitive needed — new primitives are the a2learn catalog's business (#98), not the vendored basic catalog's. The gating note stands: a deck pages freely where the native widget disciplines one-at-a-time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the fly Two parameterized primitives join the draft a2learn catalog, replacing the idea of one primitive per widget kind: a2learn:Sequence (children plus a traversal policy — linear or free, gated or all, accumulate or replace — and a completeAction fired past the end) and a2learn:Reveal (one thing with N faces). step-reveal composes as Sequence(linear, gated, accumulate); a flashcard deck as Sequence(free, all, replace) of Reveal(front, back) — and any other policy mix is pure data, no new component code, which is what lets a planner compose novel activity shapes at generation time. Invariant 1 holds: generated output is a schema-shaped composition tree; every primitive's implementation is human-written renderer code. Content inside compositions reuses the basic catalog verbatim (draft catalog = basic ∪ primitives). The demo shows all three tiers per composed kind: full fidelity, behavior-as-data, universal fallback — gating visibly survives at the primitives tier and not below. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first content primitive beside the two behavior words: a block whose
intent ('why', 'tip', 'note', open set) is data the renderer maps to its
design language — amber for the elaborative why, primary tint for the
tip, neutral otherwise. step-reveal's why and markdown-card's tip now
survive projection at the primitives tier with the emphasis the authors
gave them; markdown-card gains a composition for exactly that reason.
The basic fallback keeps its honest caption — the tiers keep stating
what they keep.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three-tier comparison page did its exploration job: the mapper and conformance stand on their own gates, and the surface renderer becomes the composed kind's component in the next PR up the stack. The page, the comparison component, the whole-widget draft-catalog module, and the per-kind compositions all retire; primitives.ts stays as the catalog identity module (catalog URN + policy vocabulary) that both the renderer and the composed kind consume. Since this PR squash-merges, the demo never reaches main at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A2UISurfaceView and the catalog identity module served the retired demo here; their only consumer now is the composed kind, one PR up. This PR stays what its title says: transports, mapper, vendored schemas, and the conformance gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Google's schema files leave the tree. What the repo owns now is spec/a2ui/manifest.json — upstream repo, pinned commit, SHA-256 per file — and a fetch script that materializes the files into the gitignored spec/a2ui/v1_0/, refusing any bytes that miss the pin (verified: a tampered checksum aborts the fetch). pnpm conformance fetches automatically when files are missing, so CI needs no extra step; determinism is the same as vendoring, the copied bulk is gone. Along the way the old README's provenance was audited: the files WERE verbatim from a2ui-project/a2ui@abcff1515fad — its stated path just omitted the json/ segment for the envelope schemas. The manifest now records the exact upstream path per file, so provenance is machine-checked instead of prose-claimed. Updating the spec version is editing the manifest: a reviewable contract change, exactly as re-vendoring was. Our own fixtures and goldens under spec/a2learn/ stay committed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urce Adam's call, and the right one: interop is with shipped renderers, not with the spec repo's HEAD. The one incompatibility was ours — the emitted action event carried userMessage, a newer-spec addition the released schemas close out (additionalProperties: false). The event now carries the intersection every revision accepts: name + context. Prose for a model reader belongs to the MCP Apps channel, not this boundary. With that fixed, the official package replaces the fetch-and-pin machinery it briefly had: @a2ui/web_core pinned exact as a devDependency, conformance reading its schemas from node_modules, the lockfile integrity hash serving as the checksum. The manifest and fetch script retire after one commit of life; a spec bump is now an ordinary dependency bump the conformance gate passes judgment on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7 tasks
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 & why
Makes the standards story checkable instead of aspirational, and hardens the a2learn layer into code. Phase 1 of an incremental, verifiable migration — each phase lands only with fixtures proving it.
@a2ui/web_core(the A2UI project's own publication —google/A2UInow redirects toa2ui-project/a2ui), exact-pinned as a devDependency with the lockfile integrity hash as the checksum. Nothing of the upstream spec is committed; a spec bump is an ordinary dependency bump the conformance gate passes judgment on. Our surfaces target the released revision, because interop is with shipped renderers, not the spec repo's HEAD — concretely, the emitted action event carries the intersection every revision accepts (name+context).src/lib/a2learn/): a versionedActivityManifestbuilder and the A2UI boundary mapper. Three kinds map today (markdown-card, flashcard, step-reveal); fidelity limits are stated, not papered over — the flashcard reveal survives as Tabs and its deck as a horizontal List (renderer-local interactions the catalog does provide), while flip animation and per-card progress stay renderer-side./api/pathway/agui): the pathway build as a typed@ag-ui/corerun — protocol drift failstsc.pnpm conformance, folded into ci.yml's verify job: zod-validated fixtures → upstream-schema validation → structural invariants (including Tabs' nested child refs) → golden diffs — with negative controls at every layer, so a vacuously-green validator can't hide: the schema validator must reject an empty message and a malformed component, the structural checker must report duplicate ids, dangling refs (tab refs included), and a missing root, and the zod gate must refuse an unknown kind.A2UI_SUPPORTED_KINDSderives from the dispatch table and must be fixture-covered — support is proven, never asserted./api/widgetacceptsformat: "a2ui"and returns the built widget's surface alongside the spec, refusing an unmappable kind with the mapped-kinds list before spending a model call.Reviewer notes on history: a three-tier demo page (
/demo/a2ui) was built mid-review to exercise the mapper visually and retired within this PR once it had served its purpose — it nets out of the squash and never reaches main. Its surface renderer moved up the stack to #101, where its consumer (the composed widget kind) lives. An interim fetch-and-pin scheme for the schemas likewise lived and died inside this PR once the official package proved viable.Migration phases: 2 = manifest served by
find_activity/build_pathway, more kinds one fixture-proven PR each; 3 = interactive kinds via the a2learn catalog (#98/#100 — the composed kind in #101 is the first fruit); 4 = the app renders from its own surfaces (dogfood).Type of change
Breaking or contract changes
Additive: a new route, a new library layer, and an additive
formatoption on/api/widget; the app's existing wire format is untouched. The spec schemas update only by bumping the pinned@a2ui/web_coreversion.How it was verified
pnpm lintandrm -rf .next && npx tsc --noEmitpnpm conformance— all four layers green against the package's schemas; negative controls verified at every layerpnpm test(40 on this branch),pnpm build/api/widgeta2ui format returns a 41-component surface for a generated flashcard and refusesdrag-sorthonestly; the retired demo verified rendering end to end (Playwright) before retiringcorrectclaims, image sides mapped, protocol-legal 400s, quiet cancel teardownAuthorship
package.json/pnpm-lock.yamlchanges are the feature's own dependencies (@ag-ui/core, ajv, ajv-formats, @a2ui/web_core exact-pinned)🤖 Generated with Claude Code