Skip to content

feat: the composed widget kind — activities generated from pedagogical primitives - #101

Open
adamstankiewicz wants to merge 7 commits into
feat/mcp-sdk-transportfrom
feat/composed-widget-kind
Open

adamstankiewicz wants to merge 7 commits into
feat/mcp-sdk-transportfrom
feat/composed-widget-kind

Conversation

@adamstankiewicz

@adamstankiewicz adamstankiewicz commented Aug 30, 2026

Copy link
Copy Markdown
Owner

What & why

The primitives design (#98, #100) folds into the actual product. A new composed widget kind — one registry entry — whose spec is a flat tree of pedagogical primitives (Sequence with a traversal policy, Reveal, Callout, Text, Group: the verdict-free v1 alphabet from #100). The model authors the composition at generation time; human-written renderers draw it; the structural gate (compositionProblems) refuses unsound trees. New activity shapes now exist that no hand-built React kind describes — the generative-substrate direction made real.

The /demo/a2ui exploration page retires; its renderer graduated into the product as the composed kind's component, and /demo/composed demonstrates the alphabet as a product surface.

What a reviewer should scrutinize

  • Invariant 1: the model emits data only. Completion wiring is injected renderer-side (Composed.tsx), never model-authored; canonicalization maps field-name synonyms but never invents content or repairs references.
  • Invariant 3: assesses: false by construction — nothing in the alphabet measures. The registry entry documents the derivation plan for when verdict-carrying primitives arrive (a2learn primitives: the exhaustive pedagogical vocabulary, and the build plan #100 phase 3, gated on a2learn interaction contract: a standard event/function vocabulary for registry widgets #99).
  • The structural gate (src/lib/pathway/schema.ts): unique ids, root, resolving refs, no cycles, two faces per Reveal — every rule has a rejection test, because this validator stands between model output and a student's screen.
  • A2UISurfaceView and primitives.ts are introduced here, relocated from feat: industry-standard conformance — AG-UI transport + schema-verified A2UI (phase 1) #79 so they arrive with their only consumer.
  • generateStructured gains fenced-output salvage (shared infra, own commit): unfence + revalidate before the cross-vendor retry; never loosens validation. Negative tests included.
  • Three live-observed model behaviors are now design decisions with tests: the envelope belongs to the generator (models drop top-level fields), the authoring schema advertises the full field superset (models obey the serialized schema — a loose object serializes to nothing and the model dutifully emits nothing), and synonyms canonicalize deterministically (bodytext, flat policy, front/back shorthand).

Deliberately deferred

How it was verified

  • 79 tests green, including composition structural gates (each proves it can reject), canonicalization synonym cases, and salvage negative controls
  • rm -rf .next && npx tsc --noEmit, pnpm build, targeted lint clean, pnpm mcp:build (shell is prebuild-generated since feat: self-building widget shell with single-source theming #86; nothing to commit)
  • Live end to end: buildWidget and POST /api/widget {kind: "composed"} produce 13–14-component compositions for verified 3.NF.A.3; an unsound tree falls back honestly to another kind with a note
  • Live UI: /demo/composed gated walkthrough completes Next → Next → Done with the completion counted (Playwright)

Authorship

  • I understand every line of this diff
  • Agent-assisted: authored by Claude working interactively with @adamstankiewicz; the MCP route 405 fix that rode the same working tree was committed separately to the SDK-transport branch
  • No dependency changes

Closes nothing yet — implements phase "fold into product" of #100; the tracking issue stays open for the remaining phases.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
interactive-learning-experiences Ready Ready Preview Aug 31, 2026 1:32am

Request Review

adamstankiewicz and others added 3 commits August 30, 2026 20:39
Long structured outputs sometimes arrive wrapped in a markdown fence —
valid JSON the SDK refuses to parse, observed live on the composed
generator. generateStructured now unfences and revalidates against the
real schema before burning the cross-vendor retry; validation is never
loosened, only the location of the JSON. Both the primary and fallback
paths get the salvage, and the negative tests prove fenced-but-invalid
stays rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tives

One registry entry, unbounded activity shapes: the model authors a flat
tree of primitives (Sequence with a traversal policy, Reveal, Callout,
Text, Group — #100's verdict-free v1 alphabet) and human-written code
does everything else. The spec is the tree (hand-maintained union and
kind enum updated together per the drift gotcha); compositionProblems
gates structure — unique ids, a root, resolving refs, no cycles, two
faces per Reveal — with tests proving every rule can reject, because
this validator stands between model output and a student's screen.

The generator learned three things from live failures, each now a test:
models fixate on the components array and drop top-level fields (the
envelope is the generator's job; the plan step titles the fallback);
models obey the *serialized* schema, so the authoring shape advertises
the superset of node fields rather than pretending looseness; and field
names drift (body for text, flat policy, front/back shorthand) — one
deterministic canonicalization pass maps synonyms and never invents
content or repairs references. An unsound tree falls back honestly.

assesses is false by construction — nothing in the alphabet measures —
and completion wiring is injected renderer-side, never model-authored.
The A2UI surface renderer from the demo work graduates to product as
the composed kind's renderer. Verified live end to end: buildWidget and
/api/widget produce 13–14-component compositions for 3.NF.A.3, and the
demo's gated walkthrough completes through Next → Next → Done with the
completion counted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sumer

Relocated from the conformance PR below, where the retired demo was
their last consumer: A2UISurfaceView (the a2learn-catalog renderer the
composed kind draws with) and primitives.ts (the catalog URN and the
policy vocabulary). Content unchanged — only the PR that introduces
them moves, so each PR's diff is what its title says.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adam's verdict on the first compositions was right: a verdict-free
reading alphabet composes articles, and articles aren't novel. Check
fixes the interaction ceiling without touching the evidence contract —
options with instant feedback, wrong answers invite another try, and
*nothing leaves the component*: no verdict, no event, assesses stays
false. It exists because retrieval beats rereading, not because anyone
is measured; the answer that should count as evidence is the Response
primitive, still gated on #99, and the schema comment draws that line.

Structural rules gate it (2+ options, answer in range, tested); the
generator prompt now optimizes for interaction density — a Check every
few nodes, Reveal over exposition, Text under ~60 words. Live probe of
the full pipeline: a fractions composition came back with 3 Checks,
2 Reveals, 1 Callout, 4 Texts. UI verified via Playwright: miss shows
feedback and invites retry, hit settles with icon + word, never color
alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two game primitives with real mechanics, implemented entirely
renderer-side: Match pairs a fixed left column against a shuffled right
column (hits lock with a check, misses shake and clear, live n-of-N
progress) — paired-associate retrieval; Hunt asks for every target among
near-miss decoys with instant per-tap feedback — discrimination
practice, the example/non-example move. Like Check, nothing leaves the
component: no verdict, no event, assesses stays false, and the schema
comments keep drawing the line to Response (#99).

Structural rules gate both (2+ pairs; 3+ items with a target and a
decoy; tested), canonicalization covers them, and the generator prompt
teaches when each fits. Live probe: one build returned Sequence + 3
Text + 2 Hunt + 2 Check + 1 Match + 1 Reveal + 1 Callout — a genuinely
game-shaped activity no fixed kind describes. UI verified via
Playwright: match miss does not advance progress, hunt decoys explain
themselves, state is icon + word throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inputs split three ways by where the value goes (#100 round 4), and the
two local roles ship: Estimate is the commit input — slide, lock it in,
then the reveal shows your mark and the actual on the same track,
juxtaposed with the gap named, never a verdict badge (committing before
seeing is the hypercorrection pedagogy, and nothing is transmitted).
Model is the parameter input — an explanation with a knob: the student
sets a variable and the authored outcome for that value swaps in.
All-numeric options render as a stepped slider, labels as chips, and
the invariant-1 line is drawn in the schema: outcomes are authored data
per option, never formulas — the renderer selects, it never computes.
The third role, values that leave the widget, stays deliberately behind
the #99 Response contract.

The authoring superset moves from nullable to nullish — with eight more
per-type-irrelevant fields, demanding explicit nulls for keys a model
naturally omits was the parse failure the probe caught. Structural
rules gate both primitives (actual inside range; exactly one outcome
per option; tested). Live probe, unprompted: 'Earth's Spheres in
Action' came back with a Model knob — Event = Rain falls / Plant grows
/ Volcano erupts / River flows — plus Check, Hunt, Match, and Reveal.
UI verified via Playwright: the reveal juxtaposes without verdict
words, and moving the knob swaps outcomes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A composition may now declare finite state (each variable an enumerated
value list with an initial), flip it with Action nodes (set a declared
value, or cycle), gate any component's visibility on it (showWhen), and
display it inside Text ({varName}). Four data-mechanics, one
human-written interpreter — and the model can design widget mechanics
nobody named: counters, staged builds, toggle flows, choose-your-path
machines. Invariant 1 is what makes the tier auditable rather than
bent: because state is finite and declared, the structural gate checks
every reference — undeclared variables, out-of-set values, set+cycle
ops, bad initials all reject, each with a test.

The spelling is deliberate on two axes: it follows A2UI's
dataModel/actions semantics (state rides the surface's dataModel, so
free-form compositions project cleanly — #98 convergence), and state
stays flat and JSON-Patch-friendly because AG-UI's
STATE_SNAPSHOT/STATE_DELTA is the wire form for the day state is
allowed to leave the widget. Local stays the default; sharing state
through the AG-UI channel is #99-contract territory like every other
boundary crossing.

Generator guidance: named primitives first, free-form only for a
mechanic they cannot express. Demo: 'Build three quarters' — a counter
mechanic no primitive names, with a halfway callout that appears at 2/4
and yields to the completion callout at 3/4. Playwright-verified:
interpolation tracks state, visibility flips exactly on the declared
values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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