/langgraph-python/frontend-tools referenced regions
frontend-tool-registration and frontend-tool-handler which were missing
markers on the production demo. Added in-place @region markers on the
existing useFrontendTool block in
showcase/integrations/langgraph-python/src/app/demos/frontend-tools/page.tsx
since the demo is a clean teaching example.
/langgraph-python/custom-look-and-feel/slots referenced regions
register-welcome-slot, register-assistant-message-slot, and
register-disclaimer-slot. The chat-slots production demo registers ~12
slot overrides at once with `as unknown as typeof X` casts that obscure
the per-pattern teaching shape, so added a sibling
slot-overrides.snippet.tsx file (mirrors the llamaindex chat-slots
sibling pattern) with the three minimal teaching regions.
Verified by re-bundling demo-content.json, running shell-docs production
build, and curling both pages on a local server: zero Missing snippet
boxes, and the region code (change_background, CustomWelcomeScreen,
CustomDisclaimer, CustomAssistantMessage) renders on the pages.
Three classes of regression are now pinned:
1. Secondary-LLM tool name doesn't collide with the A2UI middleware's
default intercept list (`render_a2ui`). New
`src/agents/test_a2ui_internal_tools.py` parametrises over
`beautiful_chat._design_a2ui_surface`, `a2ui_dynamic._design_a2ui_surface`,
and `a2ui_fixed.display_flight` and asserts none match the
middleware's `a2uiToolNames` default. Catches accidental rename
reverts that would re-enable the bypass.
2. `generate_a2ui` force-pins the canonical `catalog_id` even when the
secondary LLM hallucinates a wrong one. The new test stubs
`ChatOpenAI` with a fake response carrying a bogus catalogId and
asserts the surface op carries the module's `CUSTOM_CATALOG_ID`.
3. `generate_a2ui` short-circuits with a clean error string when the
LLM emits a root component without a `component` field — never
feeds the renderer the partial tree that surfaced as the "Cannot
create component root without a type" infinite-loop.
7 unit tests, all green locally (`pytest src/agents/test_a2ui_internal_tools.py`).
E2E tests on the same fixes now also assert:
- No `A2UI render error: Catalog not found` banner on the page after
Beautiful Chat → Sales Dashboard, Declarative Gen UI → BarChart, and
A2UI Fixed Schema → Find SFO → JFK round-trips.
- No `Cannot create component … without a type` banner on the same
three pills.
- Exactly ONE flight card on A2UI Fixed Schema (was 6+ on deploy
pre-fix from the `display_flight` loop) — `Flight Details` count
pinned to 1, `Book flight` count pinned to 1.
- At most one ResponsiveContainer on the BarChart pill (loops would
stack multiple).
- At most two ResponsiveContainers on Beautiful Chat → Sales Dashboard
(one pie + one bar = single dashboard render).
Two follow-up fixes layered on the previous internal-tool rename:
(1) `a2ui_fixed.py` — fixed-schema demo infinite loop on deploy. The
`display_flight` tool returns the raw `a2ui.render(...)` JSON descriptor
as its tool result. gpt-4o-mini reads that opaque blob, can't tell the
flight was rendered, and re-calls `display_flight` indefinitely (visible
on the deployed showcase as 6+ duplicate flight cards stacked under
repeated assistant text). Local was just lucky.
Hardened the docstring + system prompt to spell out: the JSON return
value is the surface descriptor, the card is already rendered, do NOT
call again, reply with one short confirmation and stop.
(2) Rename `render_a2ui` → `_design_a2ui_surface` in shared and
langgraph-python parity copies of `tools/generate_a2ui.py` (+
`tools/__init__.py` re-export `RENDER_A2UI_TOOL_SCHEMA` →
`DESIGN_A2UI_SURFACE_TOOL_SCHEMA`), and in `showcase/shared/typescript/
tools/generate-a2ui.ts`. These shared helpers were the source-of-truth
for the secondary-LLM tool name across integrations; renaming here keeps
parity with the langgraph-python agents already renamed in
`beautiful_chat.py` / `a2ui_dynamic.py`. Other framework integrations
keep their own `render_a2ui` for now (separate parity sweep).
(3) `showcase/aimock/feature-parity.json` — added a sibling fixture
matching `toolName: "_design_a2ui_surface"` for the beautiful-chat Sales
Dashboard pill so the langgraph-python e2e suite still hits a
deterministic mock on Railway. The original `render_a2ui` fixture is
kept above it so other integrations whose secondary LLM still requests
`render_a2ui` continue to match.
(4) Comment update in `beautiful-chat.spec.ts` to name the new internal
tool.
The A2UI middleware (`@ag-ui/a2ui-middleware`) defaults `a2uiToolNames` to
`["render_a2ui"]` and synthesises ACTIVITY_SNAPSHOT events from the
streaming tool-call args of any matching call — using the LLM's RAW
catalogId and components verbatim, before the Python tool body has a
chance to validate or normalise.
Both `beautiful_chat.py` and `a2ui_dynamic.py` use a `generate_a2ui` tool
that internally invokes a secondary LLM bound to a structured-output
`render_a2ui` helper. Because the helper's name matched the middleware's
default intercept list, the secondary LLM's hallucinated catalogId
(`declarative-gen-ui-catalog` leaking into the beautiful-chat dashboard)
and malformed root components (no `component` field on KPI dashboard)
were emitted to the frontend bypass, surfacing as:
- "A2UI render error: Catalog not found: declarative-gen-ui-catalog" on
the beautiful-chat Sales Dashboard pill
- "A2UI render error: Cannot create component root without a type"
infinite-loop on the declarative-gen-ui KPI Dashboard pill
The earlier force-pin of `catalog_id` and the defensive component sweep
in `generate_a2ui` were correct but ran too late — they execute on the
tool-result, after the middleware has already fired surface events from
the streaming args.
Fix: rename the internal helper to `_design_a2ui_surface` (and update
`tool_choice`, prompt header, and module docstring) so it falls outside
the middleware's intercept list. The explicit `a2ui.render(...)` ops the
outer `generate_a2ui` returns are then the only path to the frontend,
and our Python validation layer is authoritative.
Verified locally: Beautiful Chat → Sales Dashboard pill renders Total
Revenue / New Customers / etc. with no errors.
## Summary
- Beautiful Chat **Search Flights** pill: restored
`_build_flight_components` so the agent emits a flat literal-children
FlightCard tree (the structural-children template form via
`flight_schema.json` + `update_data_model` doesn't expand correctly
through GenericBinder for our custom catalog — it was working until the
recent flagship-cell pass swapped it back). FlightCards render again
instead of falling through to the default tool card.
- Beautiful Chat **Sales Dashboard** pill: hardened `generate_a2ui` so
the secondary LLM gets explicit catalog-id + component-shape rules and
the resulting `catalog_id` is force-pinned to the registered catalog.
Kills the "Catalog not found: declarative-gen-ui-catalog" sibling-demo
hallucination.
- **Declarative Gen UI** (`a2ui_dynamic.py`): same `generate_a2ui`
hardening, plus a defensive sweep that drops malformed components and
bails clean when the LLM omits a typed root. Kills the "Cannot create
component root without a type" infinite-loop renderer error reported on
the KPI Dashboard pill.
Two files touched (`src/agents/beautiful_chat.py`,
`src/agents/a2ui_dynamic.py`); no frontend / runtime / package changes.
Existing aimock fixtures in `feature-parity.json` already use the
canonical catalogId and well-formed components, so e2e specs should pass
without fixture changes.
## Test plan
- [x] Verified locally: `langgraph_cli dev` + `next dev`, clicked
**Search Flights** on `/demos/beautiful-chat` — Delta DL 405 ($329) and
United UA 120 ($289) FlightCards rendered live with airline, route,
times, status, Select.
- [x] Verified locally: clicked **Show a KPI dashboard** on
`/demos/declarative-gen-ui` — REVENUE / SIGNUPS / CHURN metric tree
rendered cleanly, no "Cannot create component root without a type"
error.
- [ ] CI / `nx run @copilotkit/showcase-langgraph-python:e2e`
(beautiful-chat spec already authored against the literal-children
form).
Three regressions, two files. Without these the Search Flights pill falls
through to the default tool card instead of rendering FlightCards, the
Sales Dashboard pill errors with "Catalog not found:
declarative-gen-ui-catalog", and the Declarative Gen UI cell loops on
"Cannot create component root without a type."
beautiful_chat.py — search_flights:
- Restore _build_flight_components and have search_flights emit the flat
literal-children component tree it produces, instead of the
flight_schema.json structural-children template (Row.children =
{ componentId, path: "/flights" }) plus update_data_model. The
GenericBinder doesn't reliably expand the structural form for our
custom FlightCard catalog — sibling demos avoid the form for the same
reason. The literal form has been the working shape since it was
introduced; the recent flagship-cell pass swapped it back to the
schema/data-model form and broke fixed-schema rendering.
- Make Flight TypedDict permissive (total=False) and drop the required
id / statusIcon fields. langchain rejected calls when the LLM (or
aimock fixture) omitted those, surfacing the validation string as the
tool result and never producing a surface.
beautiful_chat.py + a2ui_dynamic.py — generate_a2ui:
- Prepend a hard-requirements header to the secondary LLM's prompt
pinning the canonical catalogId and the component-shape contract
(every entry — including root — must carry both `id` AND `component`).
With injectA2UITool: false the runtime context alone leaves the LLM
enough room to hallucinate sibling-demo catalog IDs (e.g.
declarative-gen-ui-catalog leaking into the beautiful-chat dashboard)
and to omit `component` on the root entry (the source of the
"Cannot create component root without a type" infinite loop).
- Force catalog_id to the module-level CUSTOM_CATALOG_ID after the tool
call so any residual hallucination still routes to the registered
frontend catalog.
- Drop malformed component entries before constructing the operations
list, and bail with a clean error string if no valid root survives —
fail-soft instead of looping the renderer.
The renderer-side definitions.ts/renderers.tsx changes from the same
flagship pass are kept as-is; their Row/Column children union
(string-array OR { componentId, path }) is forward-compatible and
doesn't hurt the literal-children path.
The voice demo's <CopilotKit> didn't pass enableInspector, so
shouldShowDevConsole(undefined) defaulted to isLocalhost(), which
auto-mounts <cpk-web-inspector> on any local Docker host
(localhost:3100 in showcase compose). The inspector overlay
intercepts pointer events on top of the voice sample-audio button,
so dev/D5 probe runs can't click it through Playwright.
Production isn't localhost, so the inspector never mounts there —
voice is D5 in prod and D4 locally for this reason alone. Set
enableInspector={false} explicitly so the demo behaves the same in
both environments.
Probe result: d5:langgraph-python/voice flips green.
The headless-complete refactor replaced the old 5-chip set
(Weather/AAPL/Highlight/Sketch/Largest continent) with a new 4-chip
set wired through `useConfigureSuggestions`/SuggestionBar:
Weather → "What's the weather in Tokyo?" → WeatherCard
Stock price → "What's the price of AAPL right now?" → StockCard
Highlight a note → "Highlight this note for me: 'ship the demo on Friday'."
→ HighlightNote
Revenue chart → "Show me a chart of revenue over the last six months."
→ ChartCard
Probe rewrite:
- Drop the wrapper-scoped chip selector (`[data-testid=
"headless-suggestions"] >> text=...`) — the new SuggestionBar
doesn't have that container testid. Click via plain `button >>
text="<chip title>"` instead.
- Per-turn assert the matching tool card testid mounts
(headless-weather-card, headless-stock-card, headless-highlight-card,
headless-revenue-chart) plus a distinguishing text token. Driven
off a TURN_EXPECTATIONS table so adding a chip is one entry.
- Drop the readMessagesText helper that scoped to a missing
`[data-testid="headless-complete-messages"]` container; collect
text by concatenating all `headless-message-assistant` bubbles.
Fixture rewrite:
- Drop the Excalidraw + Largest-continent entries (no longer in the
chip set). Weather is already covered by feature-parity.json.
- Add a Revenue chart fixture for `get_revenue_chart` with inline
data so ChartCard renders without backend.
- Stock + Highlight fixtures preserved (still match the new chip
messages via substring).
Also restore `data-message-role` attributes on the headless-complete
UserBubble + AssistantBubble so the runner's settle plateau cascade
can resolve them — matches the runner's documented headless-template
contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The headless-simple demo was refactored to a deliberately minimal
"two hooks, one shadcn shell" template — text-in/text-out only, no
gen-UI. The D5 type literal `gen-ui-headless` no longer described
what the probe tests, and the old probe (Profile-card useComponent
+ continent fallback) was asserting against UI that no longer exists.
Three coordinated changes:
1. Rename `gen-ui-headless` D5FeatureType to `headless-simple` so the
slug matches the demo. Updated d5-registry, REGISTRY_TO_D5,
CATALOG_TO_D5_KEY (dashboard), and dependent tests/comments.
`headless-complete` keeps its existing literal because that demo
still drives the full gen-UI surface.
2. Replace d5-gen-ui-headless.{ts,test.ts,fixture} with
d5-headless-simple.ts + headless-simple.json fixture. New probe
clicks the "Say hello in one short sentence." chip and asserts the
`[data-testid="headless-message-assistant"]` bubble mounts with
non-empty content.
3. Restore `data-message-role` attributes on the headless-simple
UserBubble + AssistantBubble. The runner's chat-input cascade
documents these as the headless-template contract; the refactor
dropped them, breaking the runner's settle plateau detection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chat-slots refactor replaced the explicit `data-testid="custom-assistant-message"`
attribute with a SlotMarker wrapper component. The probe still asserted
the old testid which no longer exists, so the cell stuck at D4.
Add `data-slot-label={label}` to SlotMarker's outer span — idiomatic
data attribute that mirrors the existing `label` prop and gives the
probe a stable contract without restoring the legacy testid pattern.
The marker was already passing label="MessageView.AssistantMessage";
this just surfaces it in the DOM.
Update d5-chat-slots probe + test to assert
`[data-slot-label="MessageView.AssistantMessage"]`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes targeting the fc=97 'Target page closed' D5 failure
on `beautiful-chat-toggle-theme`:
1. Probe (`_beautiful-chat-shared.ts:assertToggleTheme`) — replace the
manual 200ms `page.evaluate` poll over 30s with Playwright's
`waitForFunction`. The native polling is event-driven inside the
browser context, disconnects cleanly on page-close, and avoids
~150 round-trip evaluates per probe. The catch branch now checks
`page.isClosed()` first and surfaces a diagnostic message naming
the renderer-crash case explicitly, so operators don't have to
guess what 'Target page closed' meant.
2. Demo (`use-generative-ui-examples.tsx`) — drop `[theme, setTheme]`
from the `useFrontendTool` deps array. The handler reads `document`
directly and the setter is stable across renders, so the deps
array forced a re-registration after every theme flip. That race
could collide with an in-flight tool result and surface as a
renderer error during multi-turn sequences. Removing deps keeps
the tool registration stable for the duration of the conversation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_invoke_sub_agent's last-resort branch silently returned '' or a
Python repr like \"[{'type': 'text', ...}]\" for block-list content,
which the UI would render as a blank/garbled card. Return a stable
SUB_AGENT_EMPTY_SENTINEL ('<sub-agent produced no output>') instead
so the d5-subagents probe can match it against its boilerplate-marker
list and fail the genuine-pass test loudly when a sub-agent produces
no usable output.
The get_stock_price tool returned randint-based price/change every
call, so e2e specs asserting $338.37 / -2.96% would only pass when
aimock short-circuited the entire call. The Python tool body still
runs server-side under aimock — only the LLM call is mocked.
Mirror the deterministic-`value` pattern on roll_d20: accept optional
price_usd / change_pct arguments and echo them back when present.
Defaults to random mock data when the args are omitted, preserving
the legacy live-LLM behaviour.
The blitz removed the explicit useDefaultRenderTool() call expecting a
framework-level fallback to handle zero-hook registrations, but the
integration uses the published @copilotkit/react-core@1.56.5 which does
not yet ship that fallback. Without the call, useRenderToolCall has no
'*' renderer and tool calls render invisibly — the user only sees the
agent's final text summary instead of the OOTB tool card.
Restore the explicit useDefaultRenderTool() invocation. The framework
fallback (committed in this PR but inert until react-core publishes a
release that includes it) becomes a no-op once that ships.
Resolved d5-all.json conflict by appending B6's headless-simple/complete fixtures (Say hello, joke, fun fact, Highlight, chart) before the tool-rendering and open-gen-ui entries already on integration.
Adds production-code testids needed by the Phase-2B genuine D5 probes:
- frontend-tools/background.tsx: data-testid='frontend-tools-background'
+ data-background-value mirror so the probe can read the live gradient
off the DOM without computing styles.
- declarative-gen-ui/a2ui/renderers.tsx: data-testid for Card,
StatusBadge, Metric, PieChart, BarChart so per-pill probes can assert
the expected catalog component painted.
- a2ui-fixed-schema/a2ui/renderers.tsx: data-testid='a2ui-fixed-card'
on the Card override so the fixed-schema component-tree mount is
observable without keyword-matching the transcript.
Each cell's Layer 1 spec is already green; this is purely additive — no
spec rewrites.
Add stable testids and rendering surfaces for the three tool-rendering
cells so the e2e suite can distinguish each cell's strategy:
- tool-rendering: register useRenderTool for get_stock_price and
roll_d20; new StockCard / D20Card components with testids
stock-card / d20-card / stock-price / stock-change / d20-value.
Rename FlightListCard testid flight-list-card -> flights-card.
- tool-rendering-default-catchall: drop the custom shadcn
useDefaultRenderTool registration so the cell is truly zero
custom-render-hooks. The framework's built-in
DefaultToolCallRenderer now paints every tool call, with stable
data-testid='copilot-tool-render' wrapper plus data-tool-name,
data-args, and data-result attributes for inspection without
expanding the card.
- tool-rendering-custom-catchall: rename the wildcard renderer's
testids from custom-catchall-* to custom-wildcard-* so the cell
is distinguishable from the (now-OOTB) default-catchall demo.
- packages/react-core: when no per-tool / wildcard renderer is
registered, useRenderToolCall now falls back to the built-in
DefaultToolCallRenderer instead of returning null.
Replace the random roll_dice tool with roll_d20(value), which echoes
the LLM-supplied value back as the result. Aimock fixtures script the
five sequential calls returning [7, 14, 3, 19, 20] so the e2e suite can
assert exact values rather than rolling until 20 lands.
Update SYSTEM_PROMPT to allow multi-tool chaining when the user
explicitly asks for it (Chain tools pill emits 3 tool calls in one
turn).
Drop the 5 cross-origin contentFrame() / page.on('console', ...) skipped
assertions across the two specs — sandbox=allow-scripts only blocks host
introspection of the iframe DOM, and console-spying on the host page
catches no inner-iframe logs. Replace with iframe-presence assertions:
each pill click must produce iframe[sandbox*='allow-scripts'] with a
non-empty srcdoc (or src) attribute. That is the load-bearing signal
that the open-generative-ui pipeline mounted SOMETHING.
Rewrite each suggestion message string as a short verbatim label that
doubles as a deterministic aimock fixture key (paired with the new
fixtures in showcase/aimock/d5-all.json). Drop pill-title parentheticals
per the cosmetic note in lgp-test-genuine-pass.md so titles read as
natural human prompts; keep the message field aligned with the fixture
key.
Final test counts: 5 minimal (page-load + 4 pill-iframe), 4 advanced
(page-load + 3 pill-iframe). All .skip() removed. The sandbox-function
round-trip (evaluateExpression / notifyHost) is intentionally not
asserted here — that requires a same-origin sandbox option or a
host-side spy on the runtime's sandbox-function-call event, both
deferred to a follow-up.
- subagent-activity-card: emit data-testid="subagent-card-<role>" on
each card wrapper (researcher | writer | critic), data-testid=
"subagent-result" on the Result content div, and data-testid=
"subagent-status" on the status pill. The previous testids
(subagent-activity-card, subagent-activity-result) collapsed across
roles, so the e2e suite couldn't count or content-assert per role.
- delegation-log: render a fixed row of 3 always-visible role
indicators (data-testid="subagent-indicator-<role>") so the page
exposes a stable hook for the load-state assertion regardless of
whether the supervisor has delegated yet.
- Annotate AgentState.delegations with operator.add reducer so concurrent
sub-agent emissions in one supervisor step accumulate instead of
raising INVALID_CONCURRENT_GRAPH_UPDATE (HTTP 400 on the Summarize pill).
- Update _delegation_update to return only the new entry (the reducer
concatenates) instead of echoing the full prior list, which would
duplicate entries each step under operator.add.
- Cap supervisor -> critique_agent loop at _MAX_CRITIQUE_ITERATIONS
(default 1). Re-entrant critique calls short-circuit with a finish-now
ToolMessage and do not append a second delegation, so the UI shows
exactly one critic card per supervisor run.
- _invoke_sub_agent now walks messages newest-first and returns the
first non-empty AIMessage content (handles list-of-content-blocks
shape too). Prevents the previous failure mode where a final empty
AIMessage made the card Result blank or echoed the showcase-assistant
intro.
- Strengthen supervisor system prompt: each sub-agent must be called
exactly once, with no further calls after critique returns.
Adds stable data-testid hooks on the hand-rolled headless chat surface
shared by headless-simple and headless-complete so e2e specs can target
the headless surface (and not the default CopilotChat surface) by
selector.
Shared:
- 'headless-message-assistant' on the custom assistant bubble
- 'headless-message-user' on the custom user bubble
- 'headless-composer' on the composer container
headless-complete only:
- 'headless-weather-card' on the WeatherCard rendered via useRenderTool
- 'headless-stock-card' on the StockCard
- 'headless-highlight-card' on the HighlightNote rendered via useComponent
- 'headless-revenue-chart' on the ChartCard
If the headless surface ever silently regresses to the default
CopilotChat surface, the headless-specific testids are absent and the
spec fails. Each tool-card testid is scoped per-component so a regression
in a single render hook fails only that test.
Add four production-code testids on demo-layout.tsx:
- identity-name on the user-name display
- identity-timezone on the timezone display
- identity-avatar on the first-letter avatar circle
- activity-<slug> on each activity <label>, kebab-case of activity name
These let Playwright lock the identity card against the published
context defaults (Atai, America/Los_Angeles, A) and assert the
default-checked activity rows ("Viewed the pricing page", "Watched
the product demo video") without depending on assistant text.
Add two verbatim-prompt aimock fixtures to showcase/aimock/d5-all.json
for the demo's suggestion pills:
- "What do you know about me from my context?" leading phrase
"I see you're Atai, and you're in the America/Los_Angeles timezone.
Recently, you viewed the pricing page and watched the product demo
video."
- "Based on my recent activity, what should I try next?" leading phrase
"Since you recently viewed the pricing page and watched the product
demo video, ..."
Pinned to the verbatim message bodies so they don't contend with the
showcase-assistant catch-all fixture.
Cross-cutting changes that don't belong with any one demo: manifest +
landing-page tags, runtime route adjustments, e2e + QA notes that
follow the demo renames, and a few small cleanups.
Manifest (manifest.yaml + src/app/page.tsx tag labels):
- Naming convention: every demo uses `Thing: Subthing` (Generative UI:
Tool Rendering - Default / Custom Default / Specific; Open Generative
UI: Default / Advanced; Shared State: Streaming / Read + Write;
Reasoning: Default / Custom; Frontend Tools: In-App Actions / Async;
Human in the Loop: In-chat / In-App / Interrupt based; Chat
Customization: CSS / Slots; Headless UI: Simple / Complete).
- Retags: Auth → `platform`; HITL Step Selection + Interrupt-based →
`interactivity`; Reasoning Default + Custom → `chat-ui`; Generative
UI: Tools → `generative-ui`.
- Renames: Readonly State (Agent Context) → Frontend Context Sharing.
- HITL slot points at /demos/hitl-in-chat (working
useHumanInTheLoop+interrupt path) instead of the previous
/demos/hitl that had no backend `interrupt()` calls.
- Highlight paths corrected for the rebuilt headless demos (root-level
paths replaced with hooks/, chat/, tools/, attachments/ subdirs).
- Descriptions rewritten where they had drifted from the implementation
(gen-ui-agent: dropped useCoAgentStateRender claim;
headless-simple: shadcn primitives, not raw Tailwind;
headless-complete: enumerates the actual hooks wired).
Runtime / route:
- src/app/api/copilotkit/route.ts — 30 agents registered (incl. the
reasoning-custom rename from agentic-chat-reasoning).
- copilotkit-mcp-apps/route.ts replaced with [[...slug]]/route.ts so v2
subpath POSTs (/v2/agent/run) resolve.
- src/app/api/copilotkit-voice/[[...slug]]/route.ts — env var standardized
(was `AGENT_URL || LANGGRAPH_DEPLOYMENT_URL`, now matches the rest
of the showcase with just LANGGRAPH_DEPLOYMENT_URL); trailing `/`
removed from deploymentUrl.
Tests / QA:
- e2e specs renamed and paths updated for the demo renames.
- qa notes for a2ui-fixed-schema (booked-state checklist removed) and
byoc-json-render (Wave 4a residue removed).
- docs-links.json key renamed for reasoning-custom.
Cleanup:
- Removed remaining stub agent.py files in demo dirs (real graphs in
src/agents/); removed dead beautiful-chat/components/headless-chat.tsx
(zero importers); removed [A2UI-DEBUG] / [A2UI-RESPONSE] print
statements from beautiful_chat.py; gpt-5.4-mini → gpt-5-mini typo
fix in beautiful_chat.py:249 (would have 4xx'd every model call);
stripped iframe-restriction LLM-prompt copy bleed from
open-gen-ui-advanced suggestion titles.
The convention pass that ran across ~28 demos earlier in this branch is
already reflected in their per-demo commits — every page.tsx reads as
imports + provider + suggestions hook + JSX, with `useConfigureSuggestions`
extracted to a sibling suggestions.ts.
Two demos that showcase how a platform team integrates CopilotKit:
auth gating and runtime config injection.
Auth — defaults UNAUTHENTICATED. First paint is a centered shadcn Card
with the demo token visible (`demo-token-123`) and a "Sign in" button.
<CopilotKit> doesn't mount until the user signs in; clicking the button
stores the token in localStorage and triggers a re-render that mounts
the chat with `Authorization: Bearer <token>` header attached. Reload
preserves the token; sign-out clears it and returns to the card.
Earlier drafts of this demo defaulted to authenticated with a
ChatErrorBoundary + onError-driven banner to recover from <CopilotChat>
's 401-on-mount crash. Both have been removed — gating the chat behind
`isAuthenticated` means it never mounts with bad creds, so the boundary
has no purpose.
Backend route uses `createCopilotRuntimeHandler` from
@copilotkit/runtime/v2 directly because the Next.js adapter does not
forward `hooks`. The `onRequest` hook validates the bearer token and
throws a Response(401) on missing/wrong tokens.
Agent Config — typed knobs (tone / expertise / responseLength) that
change the agent's behavior per turn. Pivoted to `useAgentContext` from
the original `<CopilotKit properties={...}>` transport, which silently
dropped the values in @ag-ui/langgraph 0.0.31 — those payloads landed
at the top level of the LangGraph stream and weren't routed into
RunnableConfig["configurable"]. A prior workaround that repacked them
there triggered LangGraph 0.6's "cannot specify both configurable and
context" 400.
useAgentContext is the supported LangGraph 0.6+ path for "frontend →
agent runtime context." A small ConfigContextRelay component sits inside
the provider and publishes the live toggles. The Python graph collapses
to a single static system prompt with three rulebooks; CopilotKitMiddleware
injects the context entry into the model's prompt automatically.
Four state-flow demos plus the multi-agent demo, all sharing the
page-as-entry-point convention with extracted suggestions.
- shared-state-streaming (Shared State: Streaming) —
StateStreamingMiddleware(state_key="document", tool="write_document",
tool_argument="document"). The argument name MUST match the state_key
for the partial-JSON streamer to index correctly. Fixed in this pass
(previous version had a name mismatch). Frontend renders `LIVE` badge
+ char counter so per-token streaming is visible.
- shared-state-read-write (Shared State: Read + Write) — bidirectional.
UI writes preferences via `agent.setState`; agent writes notes via a
`set_notes` tool that returns Command(update={...}). PreferencesInjector
middleware reads the state on every turn and injects it as a system
message. CopilotPopup layout, 2-col card UI, "Agent Scratch pad"
copy.
- shared-state-read (deprecated route stub) — kept for back-compat with
any external links; the canonical demo is shared-state-read-write.
- readonly-state-agent-context (Frontend Context Sharing) — frontend
publishes read-only context via useAgentContext (the LangGraph 0.6+
idiom). Backend has tools=[] and only CopilotKitMiddleware; read-only
is enforced by the absence of any state-write tool, not a flag.
- subagents (Sub-Agents) — supervisor + research / writer / critic
sub-agents. Per-tool useRenderTool registrations surface delegation
events to the chat. State has a delegations[] log; the Python side
only writes status="completed" so the type was tightened (dropped
unused "running" / "failed" legs from both Python TypedDict and the
TypeScript shape). Frontend infers active sub-agent from in-flight
tool calls via a defensive structural probe over agent.messages.
A single commit for the "agent-authored UI" cluster — five distinct
strategies, all sharing a common shape (declare a catalog, let the
agent pick + populate components):
- declarative-gen-ui (Declarative UI: A2UI) — A2UI dynamic schema. The
agent calls `generate_a2ui` (not the runtime's auto-injected
`render_a2ui`) which secondary-binds an internal render tool with
forced tool_choice, then returns operations via `a2ui.render(...)`.
Custom catalog (Card / StatusBadge / Metric / InfoRow / PrimaryButton
/ PieChart / BarChart) wired via `a2ui.catalog` on the provider.
- a2ui-fixed-schema (Declarative UI: A2UI Fixed Schema) — fixed
server-side schema. The "Book flight" button is an inert label; the
earlier draft tried a schema swap to a booked-confirmation but the
SDK doesn't yet expose `action_handlers` from Python. Removed
BOOKED_SCHEMA + booked_schema.json since they were dead weight.
Cleaned 8 (props as Record<string, any>) casts down to a single
shared `s()` helper.
- mcp-apps — MCP server-driven UI via activity renderers. The runtime's
`mcpApps.servers` config wires Excalidraw; agent has tools=[] and
the middleware emits activity events that the built-in
MCPAppsActivityRenderer auto-mounts as a sandboxed iframe.
- byoc-hashbrown (Declarative UI: Hashbrown) — streaming structured
output via @hashbrownai/react. Agent prompt locks output to JSON via
`response_format: json_object` with a `{ ui: [{ tag: { props } }] }`
contract. Custom slot override on messageView.assistantMessage parses
the streaming JSON.
- byoc-json-render (Declarative UI: json-render) — streaming hierarchical
JSON UI spec via @json-render/react with a Zod-validated catalog.
Catalog (defineCatalog) + registry (defineRegistry) split keeps the
schema as the single source of truth.
- open-gen-ui (Open Generative UI: Default) — runtime's
`openGenerativeUI` config injects a sandboxed UI tool; design-skill
override steers the agent toward educational visualizations. Built-in
OpenGenerativeUIActivityRenderer auto-mounts.
- open-gen-ui-advanced (Open Generative UI: Advanced) — adds frontend
sandboxFunctions registered on the provider; each Zod-typed handler
is exposed to the iframe via the host bridge. Suggestion titles read
as normal user prompts (no iframe-restriction LLM-prompt copy bleed).
Backends sit at src/agents/{a2ui_fixed,byoc_hashbrown_agent,
byoc_json_render_agent}.py and the MCP runtime at
src/app/api/copilotkit-byoc-hashbrown/route.ts.
Five demos exercising the per-tool / catch-all / agent-state rendering
patterns. The three tool-rendering cells share the tool_rendering_agent
graph; they differ only in how the frontend renders the same tool
calls.
- tool-rendering (Tool Rendering - Specific) — per-tool useRenderTool
for get_weather + search_flights, plus a useDefaultRenderTool wildcard
for everything else.
- tool-rendering-default-catchall (Tool Rendering - Default) — single
shadcn-styled wildcard via useDefaultRenderTool. Without registering
*some* renderer the runtime has no `*` entry and tool calls render
invisibly; this demo shows the minimum-viable shape.
- tool-rendering-custom-catchall (Tool Rendering - Custom Default) —
same single-wildcard shape, branded with a custom card.
Backend system prompt (src/agents/tool_rendering_agent.py) defaults to
ONE tool per user question. Chaining is opt-in via a "Chain tools"
suggestion that triggers an explicit-ask exception in the prompt — the
previous default-on-chaining generated extra unsolicited tool-call
cards on every turn.
- gen-ui-tool-based (Generative UI: useComponent) — useComponent for
render_bar_chart + render_pie_chart with Zod schemas; backend has
tools=[] and the runtime injects the tools.
- gen-ui-agent (Generative UI: Agent State) — agent-state-driven step
list. The Python graph plans steps via a `set_steps` tool that
returns Command(update={"steps": …}); the frontend reads via
useAgent({updates: [OnStateChanged]}) + a custom MessageList that
renders steps inside CopilotChat's messageView.children slot.
Removed: src/app/demos/{tool-rendering,gen-ui-agent}/agent.py — TODO
stubs; real graphs live in src/agents/.
Frontend Tools (in-app + async) demonstrate the spectrum of agent →
client tool calls:
- frontend-tools — useFrontendTool with a synchronous handler that
mutates page state. The agent calls `change_background` with any CSS
background value and the canvas re-paints. <CopilotSidebar /> layout;
default background is solid indigo so the canvas reads as a clean
start.
- frontend-tools-async — useFrontendTool with an async handler. The
agent calls `query_notes`, the handler awaits a 500ms simulated DB
query, and the agent uses the returned notes in its reply. Pure
frontend tool — backend has tools=[].
HITL family — three patterns for human-in-the-loop, all keyed on the
useHumanInTheLoop / useInterrupt primitives:
- hitl — step-feedback variant rendering inside the chat via
useHumanInTheLoop + useInterrupt (the v2 replacement for
useLangGraphInterrupt, which is not exported in v2).
- hitl-in-chat — time-slot picker variant. Backend graph (hitl_in_chat)
calls `interrupt({slots, …})` and resumes when the user picks. This
is the canonical "in-chat HITL" demo; the manifest entry points here.
- hitl-in-app — async useFrontendTool with an app-LEVEL approval modal
(rendered via createPortal OUTSIDE the chat). The completion callback
resolves the pending tool Promise with the user's decision. This is
HITL where the human surface is your app's UI, not the chat.
- gen-ui-interrupt — the lower-level useInterrupt primitive. Backend
(src/agents/interrupt_agent.py) has a real `schedule_meeting` tool
that emits an interrupt with topic / attendee / slots, and the
frontend renders an inline TimePickerCard.
Three "production-feel" chat demos that exercise the same convention
(page.tsx as entry point + suggestions extracted) and add their own
specialized hooks:
- multimodal — image + PDF uploads via CopilotChat attachments. Includes
a LegacyConverterShim (until @ag-ui/langgraph ships an updated
converter), magic-byte + LFS-pointer guard for safe content sniffing,
and sample-attachment-buttons that inject test images via DataTransfer
+ dispatch `change` event so screenshots / Playwright reproduce.
- voice — speech-to-text via @copilotkit/voice. Uses the V2 runtime
directly with [[...slug]] catch-all because `transcriptionService` is
V2-only. A guarded sample-audio-button injects deterministic sample
text via the textarea's native value setter (CopilotChat has no
controlled-input prop today). useSingleEndpoint={false} opts into the
V2 multi-endpoint protocol.
- beautiful-chat — flagship polished starter chat with brand fonts,
theme tokens, suggestion pills, generative-UI charts, and
enableAppMode / enableChatMode tools. Backend
(src/agents/beautiful_chat.py) wires query_data, manage_todos,
search_flights, and an A2UI dynamic generator (generate_a2ui) that
hits a secondary LLM for schema design. Model: gpt-5-mini.
The convention pass extracted suggestions into separate files for each
demo and slimmed page.tsx down to imports + provider + render.
Three demos showing the prebuilt component formats. All three share the
neutral assistant graph and follow the page-as-entry-point convention:
each page.tsx slimmed to imports + provider + the suggestions hook
mount + JSX, with `useConfigureSuggestions` extracted to its own file.
- agentic-chat (Prebuilt: CopilotChat) — full-page CopilotChat. Plain
text suggestions (joke / fun fact / limerick) — earlier drafts had a
"Weather in Paris" prompt but the agent has no weather tool; trimmed
to non-tool prompts so suggestions actually work.
- prebuilt-sidebar (Pre-Built: Sidebar) — <CopilotSidebar /> docked to
the edge of the viewport. Page content centered in mx-auto max-w-2xl
column with icon + heading + paragraph. The sidebar genuinely PUSHES
the page now — see the body width fix in src/app/globals.css from the
scaffolding commit.
- prebuilt-popup (Pre-Built: Popup) — <CopilotPopup /> with a floating
launcher. Same centered content shape as the sidebar demo.
Removed: src/app/demos/agentic-chat/agent.py — TODO stub with a
misleading docstring; the real graph is at src/agents/agentic_chat.py.
A pair of demos that exercise the same backend reasoning graph but
differ only in whether the frontend overrides the
`messageView.reasoningMessage` slot.
Backend (src/agents/reasoning_agent.py): uses a reasoning-capable OpenAI
model (gpt-5-mini by default, override via OPENAI_REASONING_MODEL) routed
through the Responses API so the model's chain-of-thought streams as
AG-UI REASONING_MESSAGE_* events with `role: "reasoning"`. The prompt
asks for a concrete physics answer, which reliably triggers reasoning;
meta-prompts like "show your reasoning step by step" produce no
reasoning summary because the model recognizes those as a request to
reveal chain-of-thought (which it refuses).
Frontend:
- reasoning-default/ — no slot override; built-in
CopilotChatReasoningMessage renders the "Thinking… / Thought for X"
header with an expandable content region.
- reasoning-custom/ — overrides `messageView.reasoningMessage` with a
ReasoningBlock (amber banner with `data-testid="reasoning-block"`).
The label flips from "Thinking…" while streaming to "Agent reasoning"
once the stream settles.
Suggestions live in their own files (per the page-as-entry-point
convention). Both demos share `agent="reasoning-default"` /
`agent="reasoning-custom"` against the same `reasoning_agent` graph,
registered in api/copilotkit/route.ts.
Removed:
- src/app/demos/agentic-chat-reasoning/ — replaced by reasoning-custom/
for naming clarity.
- src/app/demos/reasoning-default-render/ — earlier draft of the Default
demo with a slightly different page name.
- tests/e2e/agentic-chat-reasoning.spec.ts — replaced by
reasoning-custom.spec.ts.
Two paired demos showing the spectrum of "change the look" without
rewriting components:
CSS theming (chat-customization-css/) — HALCYON, a warm-paper editorial
brand. Two layers do the work:
1. v2 token overrides on `[data-copilotkit]` recolor every Tailwind
utility (cpk:bg-muted, cpk:text-foreground, …) the runtime renders.
2. Class-targeted styling on .copilotKitChat, .copilotKitMessage*,
.copilotKitInput, suggestions, scrollbar, welcome screen for the
editorial details that CSS variables alone can't express.
Aesthetic: cream parchment surface, sharp 90° corners, copper-ember
accents, italic display serif (Instrument Serif) + Fraunces body +
JetBrains Mono dispatch, paper-grain noise via inline SVG, mono masthead
pinned under the top edge. All selectors namespaced under
`.chat-css-demo-scope` — no leakage.
Slot atlas (chat-slots/) — every overrideable slot wrapped in a
dashed-outline marker. Markers nest correctly: each shows ONLY its own
label on hover via `:has(.slot-marker:hover)` rather than lighting up
every nested label as the cursor enters the outermost one. Each label is
a click-to-copy button (✓ flash on success) that copies the slot's
PascalCase component path (`Input.TextArea`,
`MessageView.AssistantMessage`, …) so a developer can paste straight
into IDE search. SuggestionPill, ScrollToBottomButton, and Feather slots
are all wired. CustomFeather has its own `FeatherCopyLabel` because the
default Feather uses position:absolute and can't share SlotMarker.
Both demos use the neutral assistant graph (chat-customization-css and
chat-slots are entries in the route.ts neutralAssistantCells list).
Full headless surface — a hand-rolled CopilotChat replacement that wires
every render hook on top of shadcn/ui primitives. Visual chrome matches
Headless: Simple so the two read as a paired sibling demo.
Architecture is progressive-disclosure: the entry file is a 30-line
HeadlessCompleteRoot that enumerates capabilities, each registered via
a focused hook module:
page.tsx
hooks/
use-tool-renderers.tsx — useRenderTool x3, useDefaultRenderTool
use-frontend-components.ts — useComponent (highlight_note)
use-headless-suggestions.ts — useConfigureSuggestions
chat/
chat.tsx, header.tsx, empty-state.tsx, composer.tsx,
suggestion-bar.tsx, message-list.tsx, message-user.tsx,
message-assistant.tsx, message-activity.tsx, typing-indicator.tsx
attachments/use-attachments-config.ts + attachment-preview.tsx
tools/weather-card.tsx, stock-card.tsx, chart-card.tsx,
generic-tool-card.tsx, highlight-note.tsx
Backend (src/agents/headless_complete.py): get_weather, get_stock_price,
get_revenue_chart tools; the chart tool replaces the previous Excalidraw
"Sketch a diagram" suggestion (MCP capability stays wired).
Bug fixes folded in:
- Tool-call cards stuck "running" forever — message-list.tsx indexes
role:"tool" messages by toolCallId and passes the matching ToolMessage
to renderToolCall so cards advance to "complete".
- Empty state was hugging the top of the viewport — Radix ScrollArea
wraps content in a `display: table` div that breaks h-full propagation.
Empty state now renders OUTSIDE the ScrollArea.
- SuggestionBar duplicated the empty-state prompts on first paint.
Hidden until the conversation starts.
Minimum-viable headless chat that wires only `useAgent` + `useCopilotKit`,
dressed in shadcn/ui primitives. Five small single-purpose files so a
reader can grok the surface in under a minute:
page.tsx — provider + <Chat />, ~10 lines
chat.tsx — useAgent + useCopilotKit + send loop
composer.tsx — Textarea + send button
empty-state.tsx — sparkles + sample prompts
message-bubble.tsx + typing-indicator.tsx — render pieces
Wires runtimeUrl="/api/copilotkit" + agent="headless-simple" against
the neutral assistant graph registered in route.ts.
Also adds the v2 catch-all route at copilotkit-mcp-apps/[[...slug]]/route.ts
(used by Headless: Complete in the next commit). v2 hooks POST to subpaths
like /v2/agent/run; the previous flat route 404'd, leaving headless demos
stuck on "Thinking…".
Foundational layer that the per-demo work in subsequent commits builds on.
- Manifest-driven landing page (src/app/page.tsx) — auto-generated grid
of demo cards from manifest.yaml, grouped by tag with explicit ordering
for chat-ui / interactivity / generative-ui / agent-state / multi-agent
/ headless / platform.
- Per-demo titles (src/middleware.ts + src/app/demos/layout.tsx).
- Diagnostic console gated to NODE_ENV=production in app/layout.tsx so
deployed showcases surface uncaught errors and iframe context.
- src/app/globals.css — Tailwind v4 @theme inline block that maps
showcase CSS variables into Tailwind theme tokens (without it, shadcn
utilities like bg-muted / text-foreground compile to nothing). body
intentionally NOT given width:100% so <CopilotSidebar /> can shrink
the document via marginInlineEnd.
- shadcn primitives under src/components/ui/ + src/lib/utils.ts.
- tsconfig.json — @/* path alias rooted at src/.
Removed:
- src/app/copilotkit-overrides.css (global override layer, superseded
by per-demo theming)
- src/app/api/copilotkit-mcp-apps/route.ts (replaced with
[[...slug]]/route.ts so v2 subpath POSTs like /v2/agent/run resolve)
## Summary
- **Dashboard mapping fix.** `CATALOG_TO_D5_KEY` in
`showcase/shell-dashboard/src/lib/live-status.ts` was missing `voice →
["voice"]`, so `computeMaxPossible` capped the langgraph-python voice
cell at D4 even when the d5-voice probe row was green. The harness
`REGISTRY_TO_D5` already had the entry; only the dashboard mirror was
out of sync.
- **Sample-button decoupled from `/transcribe`.** The "Play sample"
button used to fetch `sample.wav` and POST it to the runtime's
transcription endpoint, which made the sample button and the mic
indistinguishable under aimock (both returned the canned transcription).
Reworked it into a synchronous static-text injector — sample button is
now a deterministic test/demo affordance, and the mic is the only path
that exercises real Whisper transcription. Synced across all 18
voice-enabled integrations. Phrase stays `"What is the weather in
Tokyo?"` so aimock's `weather in Tokyo` substring fixture still matches.
- **Probe-test parity.** Added the missing `d5-voice.test.ts` companion
(every other `d5-*.ts` script has one) — 9 tests covering registration,
`buildTurns`, `preFill` (sample-button click + textarea-poll path), and
the weather/Tokyo assertion.
- **QA + e2e cleanup** for langgraph-python: dropped the
no-longer-applicable "Transcribing…" mid-flight assertion and the `block
/demo-audio/sample.wav` error-state subsection. Other 16 integrations'
qa/e2e files follow in a parity sync PR.
## Test plan
- [x] `nx test @copilotkit/showcase-harness -- --run d5-voice` → 9/9
pass
- [x] `npm test` in `showcase/shell-dashboard` → 509/510 pass (1
skipped, 0 failed)
- [x] `nx build @copilotkit/showcase-harness` → clean
- [x] Local boot: `langgraph-cli dev` (port 8123) + `next dev` (port
3000) + dashboard (port 3002) — voice page at `/demos/voice` renders,
"Play sample" injects the canned phrase instantly, send → agent returns
weather, mic → real Whisper transcription with `OPENAI_API_KEY` set
- [ ] Reviewer: confirm the langgraph-python voice cell on the live
dashboard advances to D5 once the next d5-voice probe tick lands a green
row
The langgraph-python voice cell sat at D4 even when its d5-voice probe
row was green. Root cause: the dashboard's CATALOG_TO_D5_KEY mirror in
showcase/shell-dashboard/src/lib/live-status.ts was missing voice ->
["voice"], so computeMaxPossible capped voice at D4 regardless of probe
state. The harness REGISTRY_TO_D5 already had the entry; only the
dashboard mirror was out of sync.
Separately, the "Play sample" button used to fetch sample.wav and POST
it to /transcribe. With aimock that meant both the sample button AND
the mic returned the same canned response, which made it impossible to
demo the mic path locally without conflating the two affordances.
Reworked the button into a synchronous static-text injector
(onTranscribed(sampleText)) so:
- Sample button = deterministic test/demo affordance, no runtime calls.
- Mic = real Whisper transcription via /transcribe.
Synced across all 18 voice-enabled integrations. Phrase stays "What is
the weather in Tokyo?" so aimock's "weather in Tokyo" substring fixture
still matches.
Also adds the missing d5-voice.test.ts companion (every other d5-* probe
script has one) and trims the langgraph-python qa/voice.md + e2e steps
that depended on the now-removed async behavior.
Promotes /demos/headless-complete to its own D5 feature type so the
dashboard cell can reach D5 instead of riding on the headless-simple
probe (which was navigating to /demos/headless-simple regardless of
which catalog feature triggered it).
- New gen-ui-headless-complete D5 feature type + script that clicks
each suggestion chip via preFill and asserts the right surface
renders: WeatherCard (get_weather), StockCard (get_stock_price),
HighlightNote (frontend useComponent), Excalidraw best-effort, and
the canonical "Asia is the largest continent" text reply.
- Existing gen-ui-headless script now drives both turns by chip
click (Profile card + Largest continent) instead of typing.
- Fixtures pin narration legs with both userMessage AND toolCallId
and order them before the bare userMessage toolCall fixture —
aimock's toolCallId matcher reads the LAST tool message in the
request, but in a multi-turn probe that "last tool" stays on a
previous turn's id until a new tool runs, which would otherwise
hijack a later turn's prompt with a stale narration.
- headless-complete UserBubble + AssistantBubble now carry
data-message-role so the harness conversation runner can detect
message arrivals (mirrors the headless-simple convention).
- Mappings updated in lockstep:
- REGISTRY_TO_D5: headless-complete -> ["gen-ui-headless-complete"]
- CATALOG_TO_D5_KEY (dashboard): same.
## Summary
Adds hand-rolled persistent suggestion chips to the `headless-simple`
and `headless-complete` demos in the langgraph-python north-star,
propagates the same surface to the other 17 showcase integrations, and
adds a deterministic aimock fixture so a new chip-click e2e test
("Largest continent") rounds-trips against a stable `Asia is the largest
continent…` response across all 18 demos.
## What changed
**Phase 0 — north-star (commit `7cbc5ea8`)**
- `showcase/aimock/feature-parity.json` — new fixture: `What is the
largest continent?` → `Asia is the largest continent — about 30% of
Earth's land area, home to over 4.6 billion people.`
- `langgraph-python/src/app/demos/headless-{simple,complete}/page.tsx` —
refactor `send` / `handleSubmit` to accept `(override?: string)` so chip
clicks dispatch synchronously without a `setInput` round-trip; render a
persistent `<div data-testid="headless-suggestions">` chip row above the
composer with 5 canonical entries; remove the dead
`useConfigureSuggestions` call from headless-complete (it was
registering suggestions nothing rendered).
- `langgraph-python/tests/e2e/headless-{simple,complete}.spec.ts` —
append one new test in each spec asserting chip click → user message →
`Asia` reply.
**Phase 1 — parity propagation across 17 integrations (commit
`4882c61f`)**
- Spec files `headless-simple.spec.ts` and `headless-complete.spec.ts`
are now byte-identical to the north-star in every integration (10 tests
each = 5 simple + 5 complete; verified via `cmp` for all 34 spec files).
- The 5-entry `suggestions` const is byte-identical between every
integration's simple and complete demos.
- All 17 integrations now expose the same selector surface (canonical
headings, empty-state text, `data-testid="headless-complete-messages"`,
dynamic placeholder, `rounded-br-sm` user bubble, no CopilotChat-default
testids).
**Glue preserved per integration** (verified by post-blitz code review):
- `built-in-agent`: `<CopilotKitProvider runtimeUrl="/api/copilotkit"
useSingleEndpoint>` + `agentId: "default"`
- `google-adk` / `llamaindex`: `agentId: "headless_simple"` /
`"headless_complete"` (Python-style underscores)
- `claude-sdk-typescript`: headless-complete
`runtimeUrl="/api/copilotkit-headless-complete"`
- `spring-ai`: 70-line `deduplicateMessages` adapter workaround +
`useMemo` import preserved verbatim
- All `@region[...]` markers preserved in place
**Adapter-specific decisions worth flagging in review:**
- `google-adk` headless-complete: rewrote `message-list.tsx` from
`msg-user`/`msg-assistant`/`agent-thinking` testid scheme to the
canonical `headless-complete-messages` wrapper; rewrote `input-bar.tsx`
placeholder to canonical dynamic; added the missing subtitle and
empty-state hint
- `ms-agent-dotnet`: extracted inline composer to a new `input-bar.tsx`
to match north-star structure
- `llamaindex`, `ms-agent-python`: added the canonical empty-state hint
(was missing entirely)
- `agno`, `built-in-agent`, `crewai-crews`, `mastra`, `ms-agent-dotnet`,
`pydantic-ai`: replaced per-integration empty-state hint with the
canonical Excalidraw line — chosen for parity over per-integration
accuracy (some demos don't actually wire an Excalidraw tool; alignment
was the explicit goal)
## Verification
- `validate-parity.ts`: 18/18 packages pass, 0 MUST failures
- `aimock-fixtures` test suite: 18/18 pass
- aimock fixture probed directly: `What is the largest continent?`
returns the canonical Asia response
- Each propagation slot reported `tsc --noEmit` clean (0 new errors) +
`playwright --list` shows all 10 expected tests
- Code review (`pr-review-toolkit:code-reviewer`) on the full diff: 0
Critical / Important / Minor findings, 1 stylistic nit (north-star
`input-bar.tsx` `onSubmit` type contravariant-loose, harmless)
## What was NOT done
Live per-integration Playwright runs against rebuilt Docker images. The
17 containers would each need a no-cache rebuild (~5-15 min each = hours
total) and the canonical local-test path is `showcase test <slug>` per
the existing CLI / CI pipeline. Static + structural verification covers
the propagation pattern.
## Test plan
- [ ] Run `showcase test <slug>` (or equivalent CI job) for at least one
drift-heavy integration: `google-adk` (testid scheme rewrite),
`built-in-agent` (provider glue), `spring-ai` (dedup workaround),
`llamaindex` (added testid + empty-state)
- [ ] Run the existing per-integration Playwright suites for at least
the north-star (`langgraph-python`) to confirm the new chip test passes
against a real backend + aimock
- [ ] Confirm aimock fixture validation still passes after deploy
The validate-fixture-tool-surface check on PR #4669 flagged 18 drift
violations: every headless-simple demo carried 'Weather in Tokyo' /
'AAPL stock price' / 'Highlight a note' / 'Sketch a diagram' chips
that substring-match aimock fixtures returning tool calls
(get_weather / get_stock_price / highlight_note / etc.) — but
headless-simple demos only register 'show_card' via useComponent.
Tool-call dispatch had no matching renderer.
Trim the headless-simple chip list to two in-surface entries:
- 'Profile card' → 'Show me a profile card for Ada Lovelace' (existing
show_card fixture; show_card is already registered by useComponent).
- 'Largest continent' → 'What is the largest continent?' (text-only
fixture from Phase 0; no tool dependency).
The chip-click e2e test only asserts on the 'Largest continent' chip,
so the trim is test-compatible.
Headless-complete keeps the canonical 5-chip list (its tool surface
covers weather/stock/highlight/excalidraw via tool-renderers.tsx and
backend agents).
For google-adk/headless-complete: add a useDefaultRenderTool() wildcard
catch-all. The validator looks at page.tsx + hooks/* and a backend
agent file; google-adk's tool registrations live in tool-renderers.tsx
(unparsed) and there's no matching agents/headless_complete.py file,
so the validator saw an empty tool surface. The wildcard registers '*'
which matches every fixture tool — same pattern north-star already
uses in its own tool-renderers.tsx.
Search Flights and Sales Dashboard pills both produce visible surfaces
on the langgraph-python beautiful-chat demo. Three independent bugs were
masking each other:
- Flight TypedDict required `id` + `statusIcon`, which the aimock fixture
doesn't supply. langchain rejected the call with `flights.0.id: Field
required` and the agent surfaced the error string as the tool result.
Made the type permissive (only the fields `_build_flight_components`
reads need to be there).
- search_flights now expands flights into literal-children FlightCard
components server-side instead of relying on the structural-children
template form (the binder doesn't reliably expand it for our custom
catalog — sibling demos avoid the form for the same reason).
- Sales Dashboard pill went into a tool-call loop because the
userMessage+toolName fixtures matched both the initial call and the
post-tool turn. Hoisted the toolCallId fixture above them so the
follow-up turn returns content and breaks the loop.
Custom Row/Column reintroduced with `gap` support — the basic catalog's
versions ignore it, leaving cards squished. Children are array-of-strings
only (matches what the agent and fixture emit).
Two new e2e tests cover both pills end-to-end. 3s wait in beforeEach so
the v2 chat provider hydrates before the click dispatches. Full spec:
7/7 green.