`/mastra/generative-ui/a2ui/fixed-schema` published the entire 432-line
tools barrel, because the `backend-render-operations` marker sits at the
top of the file (the marker-hoist sweep in 34b6418 put it there so the
snippet would carry its imports). The published body therefore included
every unrelated tool plus
`import { ... } from "@copilotkit/showcase-shared-tools"` — a tsconfig
path alias to a symlink in this repo, not a package a reader can install.
A Mastra onboarding run stopped there rather than invent an API (OSS-901).
Move `generateA2uiTool` into its own module and mark the region there, so
hoisting to the top of the file yields exactly the tool plus its own
imports — the same shape as the reference cell,
`langgraph-typescript/src/agent/a2ui-fixed.ts`, which likewise builds the
A2UI operations locally instead of importing the showcase's shared tools.
The published snippet goes from 432 lines to 166, and everything in it
either installs from npm or is a visibly local `./` / `@/` module with a
comment saying what a real app would use instead.
`buildA2uiOperations` and `systemPromptFrom` replace the two shared-tools
helpers so mastra keeps a single operation builder: the beautiful-chat
flight tool now calls the same one. The prompt builder lands in the
dependency-free `a2ui-context.ts` so its regression test keeps running
without the Mastra SDK installed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves all 239 showcase integration routes off
`copilotRuntimeNextJSAppRouterEndpoint`, the deprecated v1 Next.js adapter, so
the v1 entrypoint has no remaining code-level users under
`showcase/integrations/`.
const copilotHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit-x",
mode: "single-route",
});
...
return await copilotHandler(req);
## Why single-route, and why this handler
**Single-route** because these demos' frontends are
`<CopilotKit runtimeUrl="/api/copilotkit-x">` with no transport prop, and every
released provider pins the single-route transport. Single-route mode is
therefore a drop-in for the v1 adapter: no frontend change, no path change, no
`GET` export, and nothing here probes `/info`. Migrating to multi-route instead
would have required editing every demo page in lockstep for no functional gain.
**`createCopilotRuntimeHandler`** rather than `createCopilotEndpointSingleRoute`
because that helper is itself deprecated in favour of the `mode` option (see the
deprecated-aliases table in `docs/backend/runtime-endpoints.mdx`), and because
the fetch handler needs no `hono` dependency and composes directly with the
wrappers these routes already have.
The statement is rewritten in place, inside whatever wrapper it already sat in,
so `withForwardedHeaders`, the try/catch envelopes, `wrapStreamingResponse` and
`withCvdiagBackend` are all untouched. 75 of these routes construct the runtime
inline in the call; rewriting in place preserves that per-request construction
exactly as v1 did. No `runner` is added — it is optional, and none of these
routes passed one before.
13 `copilotkit-auth/[[...slug]]` routes already use the v2 fetch handler and are
left alone; they only mention the v1 name in explanatory comments.
## Collateral
- `mastra`'s main route declared a module-level
`const serviceAdapter = new ExperimentalEmptyAdapter()` plus a startup log
about the adapter choice. V2 has no service adapters, so both are gone and the
comment now explains that there is nothing to configure.
- The three `mastra` vitest suites mocked `@copilotkit/runtime` and the v1
`{ handleRequest }` return shape; they now mock `@copilotkit/runtime/v2` and
`createCopilotRuntimeHandler`, which returns the handler directly.
- 27 `@ts-expect-error` directives guarded the **v1** `CopilotRuntime` agents
type ("wraps Record in MaybePromise<NonEmptyRecord<...>>"). Under `/v2` that
hole is gone, which makes the directive unused — a hard error. They are
demoted to `@ts-ignore`, which compiles whether or not the mismatch survives
in a given integration, because 19 of these apps cannot be built locally to
prove it either way. Removing all ~220 now-stale suppressions is left as
follow-up once CI has built every integration green.
## Verified
`mastra` is the one integration installed and exercised locally (19 routes, the
`withCvdiagBackend` main route, and the only vitest suites that touch routes).
Measured against `origin/main` in the same tree:
tsc --noEmit baseline: errors in 10 files
after: errors in 9 files
new errors introduced: NONE
fixed: src/app/api/copilotkit-mcp-apps/route.ts, whose
@ts-expect-error was ALREADY unused on main
vitest run baseline: 2 files failed, 13 tests failed, 21 passed
after: 2 files failed, 13 tests failed, 21 passed
→ test-neutral; those 13 failures are pre-existing on main
Structural audit over all 239 routes: none still imports the v1 root, uses the
v1 adapter, references `ExperimentalEmptyAdapter` or `handleRequest` in code, or
is missing `createCopilotRuntimeHandler` / `basePath` / `mode: "single-route"`.
The shape itself was proved end-to-end before the rollout, in a real running
app with an untouched provider (aimock as the model backend):
`POST /api/copilotkit` -> 200 twice, chat turn rendered.
## Two pre-existing problems found on the way
- `npm ci` fails in `showcase/integrations/mastra`: `Missing:
@types/http-errors@2.0.5 from lock file`. Its Dockerfile uses
`npm ci --legacy-peer-deps`, which does succeed, so the image still builds —
but a plain `npm ci` does not. Untouched here; no manifest or lockfile is in
this diff.
- `mastra`'s vitest suite is red on `main` (13 failures, mostly
`extractXHeaders` dereferencing `req.headers` on a `{}` fake request).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Switching models only moved the failure rate around, it never removed it, so
stop relying on the model getting hand-escaped JSON right on the first try.
`create_view` takes `elements` as a stringified JSON array. When the model
appends a stray `}` past the closing `]`, the MCP server rejects the call and
names the exact fault ("Invalid JSON in elements: Unexpected non-whitespace
character after JSON at position N"). That error already comes back as a tool
result, and the agent had no step cap, so a retry was mechanically possible
all along. What blocked it was our own prompt: "Call create_view ONCE" and
"do NOT iterate, do NOT make multiple calls. Ship on the first shot."
The prompt now tells the model to read the error and try again, capped at 2
corrections (3 calls total), with stopWhen: stepCountIs(6) bounding the loop
if it never converges. This mirrors the validate-then-retry recovery pattern
already used for A2UI on the other integrations.
Validated against the real Excalidraw MCP server, using the agent's prompt
extracted verbatim from this file and the real tool schema:
normal runs 12/12 succeeded, all on the first call
attempt 1 force-corrupted with
the real-world stray `}` 10/10 recovered on the second call
Also verified in the running app (local dev server, real key): valid JSON,
isError false, diagram rendered.
Not yet verified in-app: the recovery path itself. No natural failure occurred
during the in-app runs, so the retry is proven at the API level rather than
through the Mastra agent loop.
Owner preference for the 5.x line. Recorded honestly: this reduces the
empty-diagram failure but does not remove it.
Measured against the real Excalidraw MCP server (same system prompt, real
tool schema, via the Responses API the AI SDK actually uses):
gpt-4o-mini create_view OK 3, isError 5
gpt-5.4 create_view OK 7, isError 3
gpt-4.1 create_view OK 8, isError 0
gpt-5.5 create_view OK 10, isError 0
JSON validity of the `elements` argument:
gpt-4o-mini 7 invalid of 12
gpt-5.4 7 invalid of 28, plus two runs whose tool call came back
garbled with unrelated spam text
gpt-5.4 + a hardened prompt 2 invalid of 16 (prompting does not fix it)
gpt-4.1 0 invalid of 12
gpt-5.5 0 invalid of 16
So roughly 30% of diagrams still render as an empty iframe on gpt-5.4. Closing
that gap needs a follow-up, most likely validating or repairing the `elements`
string before the MCP call rather than relying on the model to hand-escape
nested JSON correctly.
Gold `beautiful_chat.py` uses `ChatOpenAI(model="gpt-5.4")`; align the mastra
beautifulChatAgent to the same model (was gpt-4o). Verified live: dashboard turn
stays [query_data, generate_a2ui] (no standalone chart over-call), full surface
renders (3 metrics + pie + bar).
PNI-121's reported symptom was "the weather appears after 'Find flights from
SFO to JFK'". Two separate things produced that screenshot; the blank flight
rows were the previous commit. This is the other one.
On gpt-4o this agent opened the flights turn by restating the PREVIOUS turn's
weather ("The weather in San Francisco is currently 20C with heavy rain.")
before narrating the flights, which reads exactly like a tool result arriving
a turn late. Nothing was actually late: only one weather card exists and it
stays in turn 1, and the flights turn's card holds the flights turn's data.
The system prompt is already byte-identical to gold tool_rendering_agent.py,
so the model was the remaining divergence - gold runs gpt-5.4.
Verified live (real LLM, ticket's exact click order - "Weather in SF" then
"Find flights"): the stale weather sentence is gone and the narration now
matches gold's shape ("SFO -> JFK options: United UA231 08:15-16:45 $348; ..."
against gold's "Flights SFO -> JFK: United UA231 08:15-16:45 $348; ...").
Weather, stock and d20 pills re-checked on the same rig.
No effect on CI: aimock fixtures never match on model, and the d20 pill's
5-roll chain is fixture-scripted (7/14/3/19/20), so the e2e sequence is
unchanged. Live, gold rolls the d20 once too - so this moves the demo toward
gold rather than away from it.
Issue: asking for the Sales Dashboard (esp. after a prior turn) made the model
call the standalone pieChart + barChart frontend tools AND generate_a2ui, so
loose charts painted next to the dashboard.
- beautifulChatAgent: mirror gold `beautiful_chat.py` `parallel_tool_calls=False`
(defaultOptions.providerOptions.openai.parallelToolCalls) + sharpen the
steering so a dashboard / "using A2UI" request calls generate_a2ui ONLY (it
draws the charts inside the surface), while a single-chart request still uses
the standalone pieChart/barChart tool. Verified live: dashboard turn now calls
[query_data, generate_a2ui] only, single + after-flights.
- aimock beautiful-chat flights fixture: model the fixed-schema `search_flights`
path (returns the A2UI FlightCard envelope directly) instead of the old
generate_a2ui -> render_a2ui chain, so the fixture matches the live behavior
and the e2e spec's stated intent (United $349 / Delta $289).
The MCP Apps cell intermittently rendered an empty iframe (an empty box or a
thin band) on a live endpoint while passing under aimock.
Root cause is the model, not the renderer. Excalidraw's `create_view` declares
`elements` as `type: "string"` holding a JSON array, so the model must emit
double-encoded, hand-escaped JSON. gpt-4o-mini frequently appends a stray `}`
just past the closing `]`. The MCP server then rejects the call with
"Invalid JSON in elements: Unexpected non-whitespace character after JSON",
returns isError, and there is no diagram to draw, so the iframe paints empty.
aimock never catches this because it replays a recorded, valid payload.
Measured against the real Excalidraw MCP server using this agent's exact
system prompt and the real tool schema:
gpt-4o-mini create_view OK 3, isError 5
gpt-4.1 create_view OK 8, isError 0
And on JSON validity alone (n=12 unless noted):
gpt-4o-mini 8 invalid
gpt-4o-mini + hardened prompt 7 invalid (prompting does not fix it)
gpt-4.1-mini 3 invalid, and it bloats output
gpt-4.1 1 invalid of 24, output stays compact
gpt-4.1 is already used by five other agents in this file, so this keeps the
integration consistent. The cell's aimock fixture does not key on the model, so
d6 replay is unaffected.
On a live endpoint the tool-rendering flight card rendered every row blank
("United ? -> ? --") while the model's narration below it carried the real
times and prices. The result was delivered in full; the card just never
matched it.
search_flights emitted Mastra-flavored keys (flightNumber / departureTime /
arrivalTime / price) but FlightListCard - in both this integration and gold
langgraph-python, byte-identical - reads { airline, flight, depart, arrive,
price_usd }, which is exactly what gold's tool_rendering_agent.py returns.
Only `airline` overlapped, so the rest fell back to placeholders.
Return the gold result shape directly. The legacy caller-supplied `flights`
passthrough is untouched, and the only consumers of this tool are the three
tool-rendering-style agents, all of which drive gold-shaped cards.
Verified on a live real-LLM endpoint (no aimock): reproduced the blank rows
before the change, then confirmed all three rows render
"United UA231 08:15 -> 16:45 $348" (plus Delta and JetBlue) after it, across
tool-rendering, tool-rendering-custom-catchall and
tool-rendering-reasoning-chain, and across a two-turn weather + flights
conversation.
The existing e2e only asserted origin/destination and a row count, so blank
rows passed. It now asserts the result's depart/arrive/price and rejects the
"? -> ?" placeholder.
Fixes PNI-121
The "Search Flights (A2UI Fixed Schema)" pill narrated flight results as plain
text instead of rendering FlightCards. Root cause: mastra's beautiful-chat
reused the shared `searchFlightsTool` (returns plain `{ flights }`, which the
tool-rendering cells render via their own frontend FlightListCard), so nothing
produced an A2UI surface and the model just described the data.
langgraph-python's beautiful_chat.py wires a DEDICATED fixed-schema
`search_flights` whose tool RESULT is a complete `a2ui_operations` envelope (a
flat Row of literal FlightCards on `app-dashboard-catalog`, surface
`flight-search-results`). Mirror it:
- Add `searchFlightsA2uiTool` returning that envelope (buildFlightCardComponents
mirrors `_build_flight_components` — inline values, no structural-template
children).
- Add a dedicated `beautifulChatAgent` (query_data, todos, generate_a2ui, the
fixed search_flights, + the flight/dashboard steering prompt) and point the
beautiful-chat route at it, so the fixed-schema flights and steering don't
leak into the shared weatherAgent / tool-rendering cells.
Verified live (real LLM, dedicated agent): the flights pill paints two
FlightCard surfaces (catalogId app-dashboard-catalog, surface
flight-search-results), and the Sales Dashboard dynamic surface still renders in
full (metrics + pie + bar) via the grounded generate_a2ui.
The dynamic `generate_a2ui` tool grounded its inner `render_a2ui` subagent from
the tool's `contextEntries` arg, which the outer model always sends empty
(captured live: `{"messages":[…],"contextEntries":[]}`). On a live LLM the inner
render then ran with an EMPTY system prompt: ungrounded, it emitted
invalid/misnamed components (or none), so the surface never resolved against the
catalog. Result: the Beautiful Chat A2UI dynamic surface (Sales Dashboard,
flights) rendered no UI, with a render error that varied run to run. aimock hid
it: the recorded fixture returns a valid envelope regardless of the empty
context.
Read the catalog schema + A2UI generation guidelines the `@ag-ui/mastra` bridge
already forwards onto Mastra's request context
(`requestContext.get("ag-ui").context`) and ground the render there instead of
trusting the model-supplied arg. Mirrors `readAgUiContext` in `@ag-ui/mastra`'s
`getA2UITools`. Preserves per-demo catalogId (the grounded model emits it, and
`buildA2uiOperationsFromToolCall` uses `args.catalogId`), so the same shared
weatherAgent serves both beautiful-chat and declarative-gen-ui. Falls back to
the arg when no request context is present.
Both open-gen-ui cells reused the shared weatherAgent
("You are a helpful assistant.", gpt-4o). On a live LLM that produced
static HTML with no JS wiring - buttons rendered but were dead. aimock
hid it: fixtures match on userMessage/hasToolResult and replay a
fully-wired UI regardless of the agent.
Gold langgraph-python uses dedicated agents whose system prompt mandates
a single interactive generateSandboxedUi call and reads the design-skill
+ sandbox-function descriptors from copilotkit context. On Mastra,
RunAgentInput.context is a read-channel (requestContext.get("ag-ui")),
not injected into the prompt, so that guidance never reached the model.
Add dedicated openGenUiAgent / openGenUiAdvancedAgent porting gold's
prompts, with dynamic instructions that fold the ag-ui context into the
system prompt, and point the ogui route at them.
Verified live (real OpenAI, aimock bypassed) - all three advanced pills
render interactive, host-wired UI with confirmed round-trips:
evaluateExpression 7*8=56, notifyHost, evaluateExpression 5+3*2=11.
aimock matcher keys are untouched, so replay is unaffected.
14 integrations' `src/app/demos/layout.tsx` hardcoded "LangChain - Python"
in `generateMetadata` — a copy-paste leftover from langgraph-python, which
the file was cloned from. Every `/demos/*` page in mastra, strands, ag2,
agno and 10 others rendered `<title>LangChain - Python</title>`.
Each now uses the display name from its own `manifest.yaml` `name:` field,
matching the convention the already-correct integrations use
(langgraph-typescript -> "LangGraph (TypeScript)", strands-typescript ->
"AWS Strands (TypeScript)").
langgraph-python itself is included: its manifest name and root layout both
say "LangGraph (Python)", so "LangChain - Python" (the legacy Notion
partner-column label) was stale there too.
## What
Fixes multi-turn chat on every mastra showcase demo, which 400s on the
2nd turn:
> `AI_APICallError: Invalid 'input[3].id': 'msg-92Y7BhMpWBhXt7dm'.
Expected an ID that contains letters, numbers, underscores, or dashes,
but this value contained additional characters.` (`INCOMPLETE_STREAM`)
## Root cause
OpenAI's Responses API actually **rejects dashes** in `input[].id`. Its
400 message misleadingly lists dashes as allowed, but empirically only
`[A-Za-z0-9_]` is accepted:
| `input[].id` | result |
|---|---|
| *(omitted)* | ✅ 200 |
| `msg_92Y7BhMpWBhXt7dm` (underscore) | ✅ 200 |
| `msg-92Y7BhMpWBhXt7dm` (**the failing client id**) | ❌ 400 |
CopilotKit mints message ids like `msg-…`, and `@ag-ui/mastra` + the AI
SDK forward them straight into `input[].id` when replaying prior-turn
history — so turn 1 works (no prior ids) and every turn after dies.
## Fix
The mastra provider already routes every outbound LLM call through
`forwardingFetch` (the header-forwarding shim). This rewrites dashes
(and any other non-`[A-Za-z0-9_]` char) in each outbound `input[].id` to
`_` there.
**Why here, not in the bridge's message conversion:** it touches only
the bytes sent to OpenAI. Mastra's in-memory `CoreMessage.id` — which
drives its upsert-by-id history **dedup** — is untouched, so dedup is
unaffected. OpenAI-issued ids (`msg_…`, `rs_…`, no dashes) pass through
unchanged, preserving server-side conversation-state references. Chat
Completions bodies (no `input[]` array) are untouched.
This **supersedes ag-ui-protocol/ag-ui#2227** — a bridge-layer charset
munge that *kept* dashes (`[^A-Za-z0-9_-] → -`), making it a no-op on
the real failing ids. That PR is being reverted.
## Tests
`tests/vitest/header-forwarding-id-sanitize.test.ts` — 8 cases: the real
failing id, valid-id no-op, full-charset mapping, in-body rewrite,
no-op/passthrough, and chat-completions-untouched. All green.
## Verification status
- ✅ Transform verified against the exact failing id; `msg_…` form
confirmed accepted by the real OpenAI Responses API.
- ✅ 8 unit tests pass in-module.
- ⚠️ Full end-to-end wasn't run locally (the showcase runtime OOMs a 16
GB box), but **every** showcase OpenAI call flows through this wrapper,
so **staging is the final check** — deploy and re-run a two-click
multi-turn on `/demos/agentic-chat`.
## Refs
- Linear **OSS-381** (mastra refresh umbrella).
- Supersedes ag-ui-protocol/ag-ui#2227 (revert incoming).
- Follow-up worth filing upstream: `@ag-ui/mastra` / AI SDK shouldn't
forward non-provider message ids into `input[].id` at all.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Multi-turn chat on every mastra demo fails on the 2nd turn:
AI_APICallError: Invalid 'input[3].id': 'msg-92Y7BhMpWBhXt7dm'.
Expected an ID that contains letters, numbers, underscores, or dashes,
but this value contained additional characters. (code: INCOMPLETE_STREAM)
Root cause: OpenAI's Responses API actually REJECTS dashes in `input[].id`
(its 400 message misleadingly lists dashes as allowed — empirically only
`[A-Za-z0-9_]` is accepted; `msg-92Y7…` 400s, `msg_92Y7…` succeeds).
CopilotKit mints message ids like `msg-…`, and @ag-ui/mastra + the AI SDK
forward them straight into `input[].id` when replaying prior-turn history, so
the whole request fails. Turn 1 works (no prior ids); every turn after dies.
Fix at the HTTP boundary: the mastra provider already routes every outbound
LLM call through `forwardingFetch` (header-forwarding shim). Rewrite dashes
(and any other non-`[A-Za-z0-9_]` char) in each outbound `input[].id` to `_`
there.
Why here and not in the @ag-ui/mastra message conversion: this touches ONLY
the bytes sent to OpenAI. Mastra's in-memory `CoreMessage.id` — which drives
its upsert-by-id history dedup — is left untouched, so dedup is unaffected.
OpenAI-issued ids (`msg_…`, `rs_…`) contain no dashes and pass through
unchanged, preserving server-side conversation-state references. Chat
Completions bodies (no `input[]` array) are untouched.
Supersedes the ineffective ag-ui-protocol/ag-ui#2227 (a bridge-layer charset
munge that kept dashes — a no-op on the real failing ids; being reverted).
Tests: tests/vitest/header-forwarding-id-sanitize.test.ts (8 cases — the real
failing id, valid-id no-op, full-charset mapping, body rewrite, no-op/passthrough,
chat-completions untouched). All green.
--no-verify: sparse showcase checkout has no monorepo lefthook/commitlint binaries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Generative UI: Agent State" cell (gen-ui-agent) was stuck at D4 — the
d5-single-pill e2e ("marks every step as completed") failed with only 2/3 steps
reaching "completed", which blocks D6.
Root cause: the planner scripts 3 steps × 2 set_steps transitions (in_progress →
completed) + 1 initial "all pending" call + 1 closing message (~8 model turns),
but genUiAgent set no stop condition, so the AI SDK's default halted the agentic
loop before the 3rd step completed. (LangGraph gold loops until the graph ends
and needs no equivalent; this is the AI-SDK step-cap analogue — cf.
toolRenderingAgent's d20 sequence.)
Add defaultOptions.stopWhen = stepCountIs(12) to genUiAgent so the full
progression runs to completion.
Verified on the faithful rig (Node 22 + next build/start + aimock 1.37.4 replay):
gen-ui-agent.spec 6/6; tool-rendering 6/6 and gen-ui-tool-based 4/4 unchanged
(no regression). Isolated to genUiAgent — no other agent/demo affected.
--no-verify: sparse showcase checkout has no monorepo lefthook/commitlint binaries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
headless-complete is marked supported but its WeatherCard / StockCard / ChartCard
stalled in the "running" state. headlessCompleteAgent registered tools via object
shorthand ({ weatherTool, stockPriceTool }), which exposes the JS variable names
instead of the snake_case names the aimock fixtures + useRenderTool renderers emit
(get_weather / get_stock_price / get_revenue_chart) — so the scripted tool calls
were never executable — and get_revenue_chart had no backend tool at all.
- Re-key headlessCompleteAgent to explicit { get_weather, get_stock_price,
get_revenue_chart } (mirrors gold langgraph-python headless_complete.py).
- Add revenueChartTool (id get-revenue-chart) returning gold's fixed payload
{ title: "Quarterly revenue", subtitle, data: [6x {label,value}] }.
- Make weatherTool accept optional scripted temperature/conditions/humidity/
wind_speed (echoed when provided, else the seeded getWeatherImpl) — mirrors
get_stock_price's scripted price_usd. Gold's headless get_weather is a fixed
68 degF / Sunny mock while mastra's is seeded, so the headless weather fixtures
script 68/Sunny to match gold's card; tool-rendering's SF pill keeps its seeded
value. Scripted the winning headless-complete + gen-ui-headless-complete
"What's the weather in Tokyo" legs and aligned the narration to gold. (No gold/
shared backend touched — mastra tool + mastra fixtures only.)
Verified (Node 22 + next build/start + aimock 1.37.4 replay): headless-complete
5/5; tool-rendering 6/6, tool-rendering-reasoning-chain 5/5, beautiful-chat 8/8,
agentic-chat and headless-simple weather unaffected — no regression.
--no-verify: sparse showcase checkout has no monorepo lefthook/commitlint binaries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tool-rendering cell (marked supported) had 5/6 e2e failing and
tool-rendering-reasoning-chain 2/5, all from mastra aimock fixtures diverging
from the langgraph-python gold standard. Root cause: several fixtures were
keyed on generic substrings where gold uses unique tails, so they
substring-collided with the longer chain pills and — loading earlier
(alphabetical file order) — hijacked them.
Verified on a faithful rig (Node 22 + next build/start + aimock 1.37.4 replay):
- Stock: the pill's scripted $338.37 fixture was shadowed by
headless-complete's ticker-only "price of AAPL" leg (tool's 189.42 default).
Restore gold's unique "price of AAPL right now" key; key the tool-rendering
emit leg on toolName (gold parity).
- d20: the first-roll leg gated on hasToolResult:false never matched once
prior-pill tool results lingered in thread history -> 0 cards. Match on
userMessage only (gold). Add stopWhen: stepCountIs(8) to toolRenderingAgent
so the 5-roll sequence + narration (and the 3-tool chain-tools turn) run to
completion instead of stopping at the default step cap.
- chain-tools: headless-complete's generic "weather in Tokyo" leg hijacked the
"...get the weather in Tokyo..." pill and emitted only get_weather. Restore
gold's "What's the weather in Tokyo" key.
- reasoning-chain flights+weather + sequential: beautiful-chat's generic
"Find flights from SFO to JFK" legs hijacked the "...JFK and show me the
weather there" pill. Restore gold's "for next Tuesday" key.
Result: tool-rendering 6/6, tool-rendering-reasoning-chain 5/5, beautiful-chat
8/8 (no regression) under aimock replay.
Note: headless-complete's own weather/stock/revenue cards remain red on a
separate pre-existing bug (headlessCompleteAgent tool-registration + a missing
get_revenue_chart tool) — addressed in a follow-up commit.
--no-verify: this sparse showcase checkout has no monorepo lefthook/commitlint
binaries (matches prior commits on this branch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the tool-rendering CHANGES_REQUESTED on #5798 (5/6 e2e failing),
mirroring gold langgraph-python tool_rendering_agent.py:
1. Dedicated toolRenderingAgent bound to all four demo tools (get_weather,
search_flights, get_stock_price, roll_d20) + route tool-rendering and its
default/custom-catchall variants to it. Previously routed to weatherAgent,
which lacks get_stock_price/roll_d20, so the Stock, d20, and Chain pills
emitted uncallable tool calls the AI SDK dropped (no card).
2. New deterministic roll_d20 tool (echoes a scripted value) and get_stock_price
now echoes optional price_usd/change_pct — lets the fixtures script exact
values, mirroring gold's roll_d20/get_stock_price.
3. search_flights now accepts gold {origin,destination} and GENERATES the
flights list (kept lenient — still accepts a legacy {flights} array so the
D5 harness probe keeps working). Fixes the reasoning-chain flights leg that
failed input validation.
4. Re-keyed the tool-rendering + reasoning-chain fixtures off the fragile
turnIndex onto hasToolResult:false (turn-scoped in aimock >=1.37.x) and
added the missing Find-flights first leg (was 'No fixture matched' 404).
5. Weather e2e assertion -> 77%/3 (mastra's seeded getWeatherImpl for SF; a
documented divergence from gold's fixed 55/10).
Route /api/copilotkit compiles + loads cleanly; full e2e to be confirmed via
/eval d5 mastra on the harness (local next-dev verification is blocked by this
machine's 7.7GiB Docker VM OOMing on the heavy route compile).
The A2UI Button renderer was inert - it rendered the label but never
wired the schema 'action' or a click handler, so 'Book flight' did
nothing despite the narration telling users to tap it to confirm.
Replace it with an ActionButton (mirrors built-in-agent's
a2ui-fixed-schema renderer): calls the resolved action on click and
flips to a disabled 'Booked' confirmation state.
Verified with Playwright (next-dev build of the fixed renderer).
declarative-gen-ui (render-a2ui.json): the outer generate_a2ui fixtures
used the stale {context} signature (the generate-a2ui tool now requires
'messages' -> input validation failed, so the inner secondary LLM never
ran) and the inner render_a2ui fixtures gated on context:mastra, which
the secondary-LLM request never carries. Rewrite generate_a2ui args to
carry 'messages' and match the inner render_a2ui on toolName only. All
four pills (KPI, pie, bar, status) now render their A2UI surface.
declarative-hashbrown / declarative-json-render (page.tsx): the demos
pointed runtimeUrl at non-existent routes (/api/copilotkit-declarative-*,
404 -> agent not found). Point them at the existing byoc runtime routes
(/api/copilotkit-byoc-hashbrown, /api/copilotkit-byoc-json-render) and
fix the hashbrown agent id to the registered 'byoc-hashbrown-demo'.
Verified with Playwright (gen-ui on the live container; hashbrown and
json-render on a next-dev build of the fixed pages).
weatherTool/stockPriceTool/searchFlightsTool/rollDiceTool/queryDataTool returned
JSON.stringify(...); the @ag-ui/mastra bridge encodes the tool result once more,
so the typed cards' single-parse (parseJsonResult) read back a string and every
field came out empty — e.g. the weather card showed "Humidity--%". Return the
object instead (single-encode), matching the browse_web fix and the Mastra
capability-map rule. Verified: the weather card now renders "Humidity77%" (real
value) instead of "--%"; the catch-all renderers (which JSON.parse once) also
render cleanly.
NOTE: some tool-rendering e2e still fail on a SEPARATE, pre-existing fixture/
expectation drift (e.g. the weather spec hardcodes "55%" but getWeatherImpl seeds
"San Francisco" to 77; the search_flights aimock fixture calls the tool with
{origin,destination} while the tool input schema requires {flights}). That drift
is independent of this encoding fix and predates OSS-452. (--no-verify: worktree
commitlint binary broken post-crash.)
The Task Manager (Shared State) pill added todos at the agent/tool level but the
app-mode canvas stayed on "No todos yet". Three compounding causes:
1. Wrong tool. weatherAgent shipped a sales-CRM `manage_sales_todos`/`get_sales_todos`
(shape {stage,value,completed}) that the shared beautiful-chat frontend — which
reads `agent.state.todos` of shape {id,title,description,emoji,status} — cannot
render, and that the recorded fixtures never call (they call `manage_todos`).
Replaced with `manage_todos`/`get_todos`, ported from the langgraph-python
north-star (src/agents/beautiful_chat.py): same tool names + Todo shape.
2. State never bound. The tool returned the todos as its result only; that never
reaches agent state. `manage_todos` now writes the list to working memory
(writeTodosToWorkingMemory), which the @ag-ui/mastra adapter surfaces as a
STATE_SNAPSHOT. Added `todos` to AgentState so the slice exists.
3. Silent no-op. `Agent.getMemory()` is async in current @mastra/core; the
working-memory helper called it without await, so `memory.updateWorkingMemory`
read off the pending Promise as undefined and every write silently failed
("memory has no updateWorkingMemory method"). Now awaited — also un-breaks the
set_notes / set_steps / delegations writers that share the helper.
Playwright-verified on :3104: the To Do column renders all three todos
(emoji + title + description); beautiful-chat e2e Task Manager test passes.
Search Flights remains the only failing pill (A2UI fixed-schema, no fixture —
out of scope). (--no-verify: worktree commitlint binary still broken post-crash.)
The mastra declarative-gen-ui D6 cell failed turn 1 with reason=surface-missing.
Three defects fixed at the layers the real failure surface showed:
1. Stale aimock fixture: aimock/d6/mastra/gen-ui-declarative.json carried the
old D5 pill prompts and the stale inner tool name _design_a2ui_surface, so
aimock matched 0 fixtures against the current 4 driver prompts (STRICT: No
fixture matched x2). Re-authored to the current prompts + the green two-stage
shape (outer generate_a2ui + inner forced render_a2ui, context mastra,
catalogId declarative-gen-ui-catalog, per-pill narration).
2. Mastra outer-tool arg schema: unlike the google-adk peer whose generate_a2ui
takes {}, mastra's generateA2uiTool requires a messages array. The outer
generate_a2ui fixture calls now carry a valid messages payload, so the tool
passes input validation and emits the a2ui_operations container.
3. Renderer testid parity: added the DataTable catalog definition + renderer
(data-testid declarative-data-table, turn 2) and added
data-testid declarative-info-row to the InfoRow renderer (turn 4), mirroring
the green google-adk peer.
Verified RED->GREEN on the control-plane surface (slot 13, --rebuild):
red state=red -> green 1 passed, all 4 turns complete, real-Playwright DOM
assertions passed. Live-browser screenshots confirm each turn paints its
surface (KPI dashboard, DataTable, StatusBadge cards, InfoRow facts).
Playwright-verified fixes for the Mastra demo validation round:
- aimock interrupt fixtures (gen-ui-interrupt, interrupt-headless): add
hasToolResult:false to the schedule_meeting suspend legs so the resume
request falls through to the toolCallId confirmation fixture instead of
re-matching the suspend leg (picker loop, duplicated intro). Mirrors
hitl-in-chat.json.
- aimock-fixtures test: ceiling 301 -> 303; the two suspend keys now
intentionally collide across the three mastra interrupt cells
(runtime-disambiguated by route/fixtureFile like existing aliases).
- browse-web tool: return the result OBJECT instead of JSON.stringify;
the bridge encodes once more so stringifying double-encoded the result
and BrowseResultsCard showed "0 results" despite a successful browse.
- reasoning-chain pill: "Roll a d20 ..." instead of "Roll a 20-sided die
..." — the d4 agentic-chat fixture shadowed the first leg under replay
(d4 loads before d6) and pushed reasoning a step late. Real-LLM order
verified correct.
- header-forwarding shim: default x-aimock-context to "mastra" when absent
so browser-driven demos replay against aimock instead of 404ing. Harness
header wins when present; real providers ignore it.
- docker-compose.local: make OPENAI_BASE_URL overridable via .env (default
aimock unchanged) so real-LLM cells like browser-use can be tested live.
(--no-verify: commitlint binary missing in this worktree after the session
crash — ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL, infra not message)
Three wired demos 404'd on load: their pages point <CopilotKit runtimeUrl>
at /api/copilotkit-<demo>, but those route handlers were never created in the
Mastra integration (the pages were mirrored from langgraph-python without
porting the routes). The runtime-info fetch 404'd, so the page never mounted
(runtime_info_fetch_failed).
Add the three dedicated routes, mirroring the proven copilotkit-beautiful-chat
pattern. The two A2UI demos set a2ui.injectA2UITool:false (weatherAgent already
owns generate_a2ui — avoid a double-bind) and pin defaultCatalogId to the
catalog the page registers. agent-config registers the agent id the page
requests (agent-config-demo).
Page-load fix only; full behavioral parity (dedicated Mastra agents) is OSS-381.
Verified: next build compiles all three into the route manifest; POST returns
400 (route resolves) identically to copilotkit-beautiful-chat, vs 404 for a
nonexistent route.
Refs OSS-451
The reasoning demos (reasoning-default, reasoning-custom,
tool-rendering-reasoning-chain) never rendered a reasoning message. Two
root causes, both showcase-side wiring (the @ag-ui/mastra bridge forwards
reasoning correctly):
1. Agent-not-found: the reasoning-default and reasoning-custom pages request
agent="reasoning-default"/"reasoning-custom", but the runtime registry
listed the stale feature keys "reasoning-default-render" and
"agentic-chat-reasoning" instead, so the chat never started. Register the
real agent names (the demoAgentNames parity test enforces this).
2. Non-reasoning model: all reasoning demos mapped to the default weatherAgent
(gpt-4o), which the OpenAI Responses API never emits reasoning-summary
items for, so the reasoning slot stayed dark. Add a dedicated reasoningAgent
(gpt-5-mini via OPENAI_REASONING_MODEL) with
providerOptions.openai.{reasoningEffort,reasoningSummary:"detailed"} on the
agent's default stream options, mirroring langgraph-python's reasoning_agent.
Map reasoning-default and reasoning-custom to it.
For tool-rendering-reasoning-chain, add a dedicated reasoningChainAgent that
registers the four chain tools (get_weather, search_flights, get_stock_price,
roll_dice, the latter new) under the exact tool-call names the aimock fixtures
emit, on the reasoning model, so Mastra executes each leg and the multi-turn
chain advances through its toolCallId-keyed fixtures to the closing narration.
Fixture parity fixes so the chain's gold toolCallId scheme wins on the
Responses path (matching langgraph-python):
- d4/mastra/chat.json: rename the broad "weather" and "flights from SFO to JFK"
probes to the non-colliding "_d4_unused_*" sentinels gold uses.
- d6/mastra/tool-rendering.json: the basic AAPL fixture used turnIndex:0, which
matched as a behind-count turn and stole the stock chain's later turns; switch
to hasToolResult:false (gold parity) so it only answers the no-tool-result turn.
Manifest features aligned to gold (reasoning-default, reasoning-custom). Removed
the stray agentic-chat-reasoning e2e spec (no page, no gold equivalent) and
renamed the reasoning QA docs to match the cell ids.
Verified via Playwright against the up --dev mastra container + aimock:
reasoning-default 2/2, reasoning-custom 7/7, tool-rendering-reasoning-chain 5/5.
Validated the OSS-426 background-agents cell against the running dev stack
(showcase up mastra --dev + Playwright e2e on :3104). Findings:
- Under getLocalAgents({untilIdle:true}) the activity card NEVER reaches
'Completed': a probe polling the card status for 45s showed it stuck on
'Working…' the whole time, and the e2e was flaky (first attempt timed out
at 60s before the card even painted).
- Root cause: instrumenting run_deep_research.execute proved it NEVER fires
within the run. Mastra dispatches the backgroundable tool but nothing
executes it — this single-process Next.js demo has no background worker to
pick the task up, so no background-task-completed chunk is ever produced.
untilIdle only holds the stream open for the full idle timeout (many wasted
re-entry LLM calls, slow/flaky render) with zero completion benefit.
- Reverted to plain getLocalAgent: the run closes right after
background-task-started, the 'working' card paints fast and deterministically,
completion is out of band exactly as the tool/renderer/e2e/qa already
document. Full spec now green: 4 passed in ~3.7s, stable across 3 runs.
The shared-state-streaming demo mapped to the generic weatherAgent (state
{proverbs}, no `document` field) and its aimock fixture returned plain text
gated on a stale "stream the counter to 5" match, so nothing ever streamed
into `state.document` and the e2e failed.
Add a dedicated `sharedStateStreamingAgent` with working memory enabled on a
`{ document: string }` schema. It writes drafts through Mastra's built-in
`updateWorkingMemory` tool; the @ag-ui/mastra bridge intercepts the streamed
tool-call args (OSS-414) and emits a leading STATE_SNAPSHOT followed by
incremental STATE_DELTA on /document, so the UI renders the document
token-by-token. This is the Mastra-native equivalent of langgraph-python's
StateStreamingMiddleware / predictive-state pattern.
- agents: add sharedStateStreamingAgent + SharedStateStreamingAgentState
- index: register the agent on the Mastra instance
- route: map shared-state-streaming -> sharedStateStreamingAgent with a
dedicated resourceId, build guard, and LocalMastraAgentName entry
- fixture: drive updateWorkingMemory with the streamed document for all three
pills (poem / email / quantum) plus a confirmation turn on hasToolResult
The native interrupt tool destructured `suspend`/`resumeData` off the top level
of the ToolExecutionContext, but in @mastra/core 1.48 they live under
`executionContext.agent` (the AgentToolExecutionContext sub-object). So
`suspend` was `undefined`, `return suspend(...)` threw `suspend is not a
function`, the model got a tool-error and re-called schedule_meeting, and the
agentic loop spun to the step cap (~30 LLM calls) with NO `tool-call-suspended`
chunk -> no `on_interrupt` -> the time-picker never rendered.
Reading off `executionContext.agent` (matching the proven @ag-ui/mastra dojo
tool) makes the loop pause at the interrupt. Verified e2e against aimock:
gen-ui-interrupt 4/4 and the interrupt-headless probe flow both green
(picker renders backend suspend-payload slots -> pick -> resume -> confirmation).
Two fixes surfaced by running the actual Playwright e2e (per showcase-demo-debugging):
- interrupt.ts: Mastra createTool execute is (inputData, executionContext) —
suspend/resumeData live on arg2, not arg1. Was destructured off arg1 →
suspend was undefined → suspend() threw → loop never paused. Fixed signature.
(NOTE: e2e still shows the loop not pausing — a deeper suspend-path issue
remains; tracked to the interrupt fix chip.)
- background-agents route: use getLocalAgents({ untilIdle: true }) (plural; the
singular getLocalAgent lacks the toggle) so the background-task lifecycle
(incl. background-task-completed + result) pipes in-band per Mastra's
resolution-order docs — card can complete in-turn instead of stuck 'working'.
Honest status: the advanced Mastra cells (interrupt, a2ui-recovery, reasoning,
shared-state-streaming) are NOT yet e2e-green under aimock; 7 focused follow-up
tasks spawned (one per item) with repro + the sanctioned up --dev validation rig.
Integrates 3 gap demos (authored in parallel worktrees, ported onto the
upgraded branch):
- background-agents (OSS-426): run_deep_research tool flagged
background:{enabled:true} + Mastra backgroundTasks:{enabled:true} → the bridge
maps background-task-started → a live 'working' activity card. Completion is
out-of-band by design (not asserted). Dedicated route + fixture + e2e + qa.
- observational-memory (OSS-427): OM enabled on the agent Memory
(scope:thread, observation 600/300) + surfaced via getLocalAgents({
observationalMemory:true }). SIZABLE pills trip the token-size trigger.
NOTE: OM data-om-* chunks come from the OM processor + observer LLM, not the
mocked completion — so it does NOT replay deterministically under aimock; the
e2e asserts the deterministic subset (page + pills + completing turn) and
full OM-card verification needs a real-LLM run (documented in qa).
- browser-use (OSS-91): Mastra-only, real-LLM. browse_web tool drives a LOCAL
headless Playwright Chromium (NO Browserbase) — top HN / page read, rendered
as in-chat cards. Non-deterministic → no aimock D6 fixture; smoke e2e only.
Needs 'npx playwright install chromium' at runtime (documented in Dockerfile+qa).
Shared: 3 agents + registrations + backgroundTasks toggle, 2 tool exports,
manifest features+demos (now 42 demos, not_supported_features still []),
playwright dep, demoAgentNames excludes for the 4 dedicated-route cells.
next build clean (all routes); validate-parity 0-fail; validate-pins baseline (38).
The @copilotkit/react-core v2 resume-path bug that quarantined gen-ui-interrupt
+ interrupt-headless is fixed as of 1.62.1, so migrate both cells to the native
interrupt path and move them out of not_supported_features.
- Bump @copilotkit/* 1.61.2 -> 1.62.1 (react-core/runtime/shared/voice/
a2ui-renderer + web-inspector core override). next build clean (40 routes).
- Backend: new src/mastra/tools/interrupt.ts — a real Mastra suspend tool
(schedule_meeting) with suspend/resume schemas; returns suspend() directly so
the agentic loop pauses. Wired into interruptAgent (was tools:{}). Instance
storage (src/mastra/index.ts) already satisfies the resume snapshot prereq;
emitInterruptOutcome defaults true in the v1 bridge so the standard
RUN_FINISHED outcome fires (resumable on client >=1.61.2).
- gen-ui-interrupt: useHumanInTheLoop workaround -> native useInterrupt
(renderInChat), reading the Mastra suspend wrapper's suspendPayload.
- interrupt-headless: hand-rolled on_interrupt subscription -> native
useInterrupt({renderInChat:false}) placed in the app surface (the hook handles
both the standard outcome and legacy on_interrupt + the correct resume array).
- manifest: not_supported_features now []; gen-ui-interrupt + interrupt-headless
in features; interrupt_pattern: native (parity with langgraph-python).
- fixture: gen-ui-interrupt.json userMessage keys (introductory sales team call
/ one-on-one with Alice) didn't substring-match the D6 probe + e2e prompts
(never caught while skipped) -> aligned to 'intro call with the sales team' /
'1:1 with Alice'. interrupt-headless.json already correct.
Local D6 e2e not run (Docker daemon container-create wedged in this env);
build-verified + relying on CI/harness. Native suspend/resume path is
dojo-proven on CopilotKit >=1.61.2.