Secondary A2UI design call (injectA2UITool:false path) never saw frontend
App Context, so PieChart/BarChart shipped with empty data arrays ("No data
available") and DataTables rendered blank rows while Metrics invented
placeholders.
Bake the Vantage Threads Q2 dataset + composition rules (from sales-context.ts)
and non-empty chart/table examples into DeclarativeGenUiDesignSystemPrompt.
Coerce string chart values to numbers. Tighten outer agent to not dump the
dashboard as prose.
The export test self-imported the package by name:
const mod = await import("@copilotkit/a2ui-renderer");
which resolves through node_modules to dist/. Nothing guarantees dist/ exists
when the test runs: nx.json sets test.dependsOn to ["^build"], and the caret
means *dependencies'* builds, not the project's own. Under `nx run-many` the
scheduler ran a2ui-renderer:test before a2ui-renderer:build, so the import
failed with "Failed to resolve entry for package @copilotkit/a2ui-renderer".
Standalone runs passed only because an earlier build had left dist/ populated.
Nx reported the task as flaky, which it was not — it was order-dependent. The
same root cause failed check-types, since tsc resolves the dynamic import to
dist/index.d.mts.
Importing the source entry instead keeps what the test is actually for (that
src/index.ts -> react-renderer/index.ts -> ./filter-catalog re-export chain is
intact, so callers can import filterCatalog) while making it hermetic. The
package.json "exports" mapping is already covered by this package's publint and
attw targets.
Verified with dist/ deleted: 16/16 pass and check-types is clean. Both
`nx run-many -t test` and `-t check-types` across core, react-core, runtime,
web-inspector and a2ui-renderer now pass with --skip-nx-cache.
Co-Authored-By: Claude <noreply@anthropic.com>
Two built-in-agent demos were broken against a real model while their D5/D6
rows stayed green, because aimock exercises neither failure. Both root causes
were confirmed against the live OpenAI API.
gen-ui-agent stopped mid-plan on every run. `@tanstack/ai`'s `chat()` applies
`maxIterations(5)` when no `agentLoopStrategy` is passed, and nothing errors
when the budget runs out — the run just ends. GEN_UI_AGENT_PROMPT scripts 7
`set_steps` calls (1 initial + in_progress/completed per step x 3) plus a
closing message, so the walk died two calls short with the last step pinned at
`pending` and no narration. Reproduced with the real model and the real prompt:
default budget -> 5 calls, "completed, completed, pending", no message
maxIterations(25) -> 7 calls, all completed, 333-char summary
Every demo factory now passes the shared DEMO_AGENT_LOOP_STRATEGY (25 —
several times the longest scripted walk, still bounded). The two non-streaming
tool-free `chat()` calls keep the default: they have no loop to exhaust.
declarative-json-render rendered nothing at all: RUN_STARTED -> RUN_FINISHED,
no events, no console error, no banner. `text.format: { type: "json_object" }`
has a server-side precondition that the word "json" appear in the request
`input`, but the adapter sends `systemPrompts` as `instructions` and only
`messages` as `input` — so with the JSON directive living solely in
SYSTEM_PROMPT the API rejected every run with
400 Response input messages must contain the word 'json' in some form to
use 'text.format' of type 'json_object'. (param: input)
The directive now rides in `messages` (as `user`, since TanStackChatMessage
admits no `system` role and the runtime hoists system messages into
systemPrompts — the half that is not input). json_object enforcement is kept;
verified live that the run then streams a complete, JSON.parse-able spec.
That 400 was invisible because the hand-rolled converters forward a whitelist
of chunk types and dropped RUN_ERROR — every one except reasoning-factory. The
`type: "tanstack"` factories were fine (the runtime's converter rethrows), so
the four `type: "custom"` converters now call the shared throwOnRunError.
Also: the gen-ui-agent progress card announced "All N steps complete" whenever
the RUN ended, ignoring the step data, so a truncated run read as a UI glitch
instead of an agent that stopped early. The wording is now derived from the
steps (`describeProgress`, extracted pure so it is testable without a DOM) and
a stalled run says so.
Both gotchas recorded in showcase/GOTCHAS.md.
## What does this PR do?
Fixes a fleet-wide false red on `mcp-apps` D5/D6: red on all 18
integrations that support the feature since `first_failure_at`
2026-07-28 23:03Z, while the demos rendered correctly by hand.
**Root cause.** The `completeOnMount` gate added in d70d48a561 named the
`mcp-app-iframe` testid, which only Angular's `copilot-mcp-apps-widget`
declares. `react-core` and `vue` build the sandbox iframe imperatively
(`document.createElement("iframe")`) with no testid, so the settle gate
could never be satisfied — every React/Vue integration timed the turn
out at 30s with `reason=surface-missing` and never reached
`assertIframePresent`, whose `iframe[sandbox]` fallback would have
passed. `crewai-crews` and `langroid` stayed green only because they
skip the feature (`errorClass: "skipped-incapable"`).
Verified against live staging (`showcase-built-in-agent-staging`, after
clicking the "Sketch a system diagram" pill):
```
oldGate_testIdOnly: 0 <- what the probe waited for
newGate_cascade: 1 <- the surface that was there all along
allIframes: [{ sandbox: "allow-scripts allow-same-origin allow-forms", testid: null, hasSrcdoc: true }]
```
**Fixed on both sides of the contract:**
1. **Product** — `react-core` and `vue` `MCPAppsActivityRenderer` now
set `data-testid="mcp-app-iframe"` and `title="Interactive MCP
application"` on the host-created iframe, matching Angular. Pinned by a
new test in each package, so dropping the attribute fails in the package
that owns it rather than silently reddening the fleet a day later.
2. **Harness** — `completeOnMount` accepts CSS `selectors` alongside
`testIds`, so the probe settles on the cascade its own module doc and
assertion already declare:
```ts
completeOnMount: { selectors: ['[data-testid="mcp-app-iframe"],
iframe[sandbox]'] }
```
`querySelectorAll` unions comma-separated branches, so one entry
expresses "any conforming form of this surface" while the
conjunctive-across-entries and `minNewMounts` delta semantics stay
exactly as before; `testIds: ["x"]` is now sugar for `selectors:
['[data-testid="x"]']`, so every other probe is untouched. This half
matters on its own: the integrations pin
`@copilotkit/react-core@1.61.2`, so a testid-only fix would leave 18
cells red until a release plus a fleet redeploy.
Also: a `completeOnMount` spec naming no surface now throws instead of
burning the whole turn budget and reporting a misleading
`surface-missing`. The trap is recorded in `showcase/GOTCHAS.md` next to
the sibling `copilot-assistant-message` testid gotcha.
## Verification
- harness `conversation-runner.test.ts` + `d5-mcp-apps.test.ts` —
101/101, including new red-green pairs: a `selectors` cascade greens, a
never-mounting cascade still reds `surface-missing`, a leftover-only
surface still reds, an empty spec fails loud
- `@copilotkit/vue` `MCPAppsActivityRenderer.test.ts` — 6/6
- `@copilotkit/react-core` `MCPAppsActivityRenderer.e2e.test.tsx` —
17/17
- `oxfmt` clean, `check-types` clean on both packages
- Pre-existing and unrelated (fail identically on an untouched worktree
on Windows): harness `typecheck` wants the gitignored generated
`showcase/shell/src/data/frontend-catalog.json`; 6 `src/probes` tests
assert POSIX path separators
## Related PRs and Issues
- Regressed by #6212-era commit d70d48a561 ("test(showcase): add
deterministic Angular parity audit")
## Checklist
- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation (`showcase/GOTCHAS.md`)
- [x] "Allow edits by maintainers" is checked
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The approval loop could never pass, independent of the demo's behavior:
for (let step = 0; step < 4; step++) {
const approve = page.getByRole("button", { name: /^approve$/i });
await approve.first().click({ timeout: 30_000 }).catch(...)
}
Two compounding problems. The recall path emits at most three HITL cards
(openPolicyException -> finalizePolicyException -> approveTransaction) but the
loop demanded four, so the last iteration always waited on a card that never
arrives. And the per-click timeout (30s) equalled the whole test budget
(playwright.config.ts sets no `timeout`, so Playwright's 30s default applied),
so the first miss consumed the entire test and the `.catch()` fallback to the
other APPROVE_LABELS was unreachable dead code.
Now: poll every label for the next visible approve control, treat "no card
left" as flow completion rather than a timeout, keep a runaway guard that is
explicitly not the expected card count, and give the test a budget that can
actually hold one agent turn plus the closing server poll.
Confirmed against a local run (aimock + the docker Intelligence stack): the
agent recalls the procedure, files the EXC-BOARD-APPROVED exception, and
approves the charge, and the "Record a workflow?" card never appears. The
header's "verify on first green run" notes are updated to record what that
run settled and what it did not.
The spec still fails at its closing assertion: PUT /api/v1/transactions/t-3
{status:"approved"} keeps returning 422, i.e. store.hasApprovedException() is
false, so the over-limit gate is never lifted even though the (fixture-scripted)
agent text claims it was. That is pre-existing and out of scope here — this PR
touches no part of the approval path (lib/store, the exceptions/transactions
routes, copilot-context, components/wow are all untouched). Left failing rather
than skipped so it stays visible.
Co-Authored-By: Claude <noreply@anthropic.com>
## Summary
Brings the **langgraph-typescript** showcase integration to D6 parity
with the north-star **langgraph-python** (OSS-583). The frontend + demo
set were already aligned; this PR fixes the behavior gaps so the D6 grid
is honest.
**Final local D6: 39/39 green** (all cells the north star wires),
verified serially from the harness worker logs. Two of the fixes were
genuine TypeScript-runtime bugs that aimock's scripted replay had masked
as "green".
## Fixes
| Cell | Root cause | Fix |
|---|---|---|
| gen-ui-open, gen-ui-open-advanced | stale `turnIndex:0` on leg-1
tool-call fixtures → turnIndex-relaxed re-serve loop
(`done-signal-missing`) | `turnIndex:0` → `hasToolResult:false` (mirror
LGP) |
| reasoning-display | `@langchain/openai@1.4.4` streaming Responses
converter collapses reasoning + answer to the same content-block index →
answer swallowed (`text-unstable`) | `disableStreaming:true` (forces the
non-streaming converter → separate blocks); documented streaming
trade-off in PARITY_NOTES |
| tool-rendering | langchain-js streamed-chunk reassembly leaves a
tool-call-with-content only under `additional_kwargs.tool_calls` →
`shouldContinue` routes to `__end__`, no card |
`normalizeAssistantMessage()` promotes it to a clean `AIMessage` |
| frontend-tools-async | stale bare `turnIndex:0` duplicate fixture →
re-emit loop | remove the stale fixture (same class is red on LGP too —
flagged) |
| a2ui-recovery | fixture prompt drift (still LGP's prompt) + inner
`render_a2ui` sub-agent invoked config-less so `x-test-id` isn't
forwarded → flaky heal | retarget prompt to the
langgraph-typescript-unique one + `wrapModelCall`/`wrapToolCall` + ALS
header forwarding (mirror mastra) |
| multimodal | `@ag-ui/langgraph` collapses every attachment to an
`image_url` data-URL; `rewritePart` had no `image_url` branch → a PDF
was forwarded as an image → OpenAI 400 | add an `image_url` branch
routing on the data-URL MIME (image → pass through; PDF → extract text
via `pdf-parse`), mirroring langgraph-python |
## Verification
- D6 run serially against aimock (real durations from
`showcase-harness-pool-worker-1`, not the CLI's "0.0s"); the two
previously-flaky cells (frontend-tools-async, a2ui-recovery) verified
with 4–6 consecutive green runs.
- multimodal PDF upload + the fixed cells confirmed live against real
OpenAI.
## Known items (documented in `showcase/GOTCHAS.md`, out of this PR's
scope)
- **mcp-apps Excalidraw** is a masked-green cell: `gpt-4o-mini` (kept,
D6-green) emits malformed Excalidraw `create_view` JSON live → black
canvas. `gpt-5.4` fixes it live but reds aimock D6, because the **TS
MCP-Apps middleware doesn't act on aimock's replayed responses-path
tool-call** (fixture is byte-identical to LGP; LGP's Python runtime
mounts the iframe). The real fix is a TS-runtime/`@copilotkit/runtime`
change + then gpt-5.4 — follow-up.
- **multimodal** needs the image built with `git lfs pull` (its sample
attachments are LFS-tracked and baked, not mounted); a build without LFS
ships pointers and reds it. Build-layer follow-up.
- **frontend-tools-async** stale fixture is fleet-wide — the same
deletion is needed on langgraph-python (its D6 is red for the same
reason).
## Scope
Only `integrations/langgraph-typescript/`, its aimock fixtures, and
`showcase/GOTCHAS.md`. langgraph-python (the north star) was not
touched.
Recently we made a few commits that let in some changesets. As a result,
agents now frequently try to add them to every change that gets made to
CopilotKit. Likely this was just an old branch and review didn't catch
them getting merged in.
Immediate remediation is to just remove the folder and make this commit
message so future agents can find the decision in commit history.
The x-pathname middleware added in this branch activates layout.tsx's
request-time readFileSync(cwd/manifest.yaml) in generateMetadata. Without
the manifest in the runner stage, every /demos page 500s with ENOENT once
middleware sets the header. Mirrors langgraph-python's Dockerfile and
satisfies scripts/__tests__/runtime-manifest-copy.test.ts.
Recently we made a few commits that let in some changesets.
As a result, agents now frequently try to add them to every
change that gets made to CopilotKit. Likely this was just an
old branch and review didn't catch them getting merged in.
Immediate remmediation is to just remove the folder and make
this commit message so future agents can find the decision
in commit history.
Signed-off-by: Tyler Slaton <tyler@copilotkit.ai>
The mcp-apps POST catch block returned the raw error message and stack
to the client and logged nothing server-side, leaking internal paths and
library versions to anyone who could hit /api/copilotkit-mcp-apps. Mirror
the sibling copilotkit/route.ts hardening: log full details server-side
under a randomUUID correlation id and return only the id plus a generic
message with status 500.
mcp-apps is aimock-green but live-degraded: gpt-4o-mini emits malformed Excalidraw
create_view elements JSON (stray trailing }) -> MCP JSON.parse fails -> black
canvas live. gpt-5.4 fixes it live but reds aimock D6 because the TS MCP-Apps
middleware doesn't act on aimock's REPLAYED responses-path create_view tool call
(never mounts the iframe; fixture is byte-identical to LGP, LGP's Python runtime
mounts it — so TS-runtime-specific, not a fixture issue). Kept gpt-4o-mini to hold
D6 green; the real fix is a TS MCP-Apps-middleware/@copilotkit runtime change +
then gpt-5.4. Flagged as follow-up.
The @ag-ui/langgraph converter collapses EVERY attachment (image and document)
into a LangChain image_url data-URL, so a PDF reached rewritePart as an
image_url part and hit neither the image nor document branch -> passed through
unchanged -> sent to gpt-4o as an image -> OpenAI 400 'Invalid MIME type. Only
image types are supported.' (The existing document-branch pdf-parse code was
dead — the part never arrives as 'document'.)
Add an image_url branch to rewritePart that routes on the data-URL MIME (mirrors
langgraph-python's multimodal_agent): image/* passes through unchanged; any
non-image (application/pdf) is flattened to text via the existing pdf-parse path
instead of being forwarded as an image. Source-only, no dep/model change.
multimodal D6 still green (PNG path unchanged); PDF upload verified live (text
extracted, no 400).
## Summary
Fixes more **ms-agent-dotnet** (Agent Framework) staging failures
against a real LLM.
### Root cause (load-bearing)
`ChatClientAgent` ctor is `(chatClient, instructions, name, description,
tools, …)`.
Almost every agent in this integration passed the system prompt as
**`description:`** (metadata only). The model received **null
instructions**, so:
| Demo | Symptom |
|------|---------|
| shared-state-read-write | Pills appear to do nothing / no set_notes |
| declarative-hashbrown | Empty — frontend JSON parser rejects prose |
| declarative-json-render | Wall of text instead of rendered UI |
| declarative-gen-ui | Wall of text + invented `DashboardCard` on later
pills |
### Fixes
1. **Global**: `description:` → `instructions:` for every
ChatClientAgent system prompt in `ms-agent-dotnet/agent`.
2. **Hashbrown + json-render**: also set `ChatResponseFormat.Json` (same
contract as langgraph-python's `response_format=json_object`).
3. **declarative-gen-ui**: catalog-specific secondary design prompt
(Card, not DashboardCard/FlightCard) + stricter outer agent (always call
`generate_a2ui`, one short sentence).
4. **shared-state-streaming**: add feature to `manifest.yaml`
`features:` so the shell no longer shows **Backend fixture unavailable**
for a demo that is already wired end-to-end.
### shared-state-streaming note
You *were* supposed to be able to open that link if the feature is
supported — the page existed under `demos/` but was missing from
`features:`, so the shell treated it as a non-runnable cell. That is
fixed. (D6 fixture content for this slug is still thin vs LGP; live LLM
path is the product path.)
## Test plan
- [x] `dotnet test` SharedStateAgent.Tests 79/79 (Docker SDK 9)
- [ ] Staging after deploy:
- [ ] `/react/ms-agent-dotnet/shared-state-read-write/preview` — all
three pills respond; Remember something updates notes
- [ ] `/react/ms-agent-dotnet/shared-state-streaming/preview` — cell
loads (not fixture-unavailable); document streams
- [ ] `/react/ms-agent-dotnet/declarative-hashbrown/preview` — pills
paint metrics/charts
- [ ] `/react/ms-agent-dotnet/declarative-json-render/preview` — pills
paint MetricCard/charts not raw text
- [ ] `/react/ms-agent-dotnet/declarative-gen-ui/preview` — all four
pills paint A2UI without DashboardCard error / wall of text
Root cause across nearly every ChatClientAgent: system prompts were passed as
`description:` (agent metadata) instead of `instructions:` (the actual system
message). ChatClientAgent(instructions, name, description, …) therefore ran
with null instructions, so models ignored tool guidance and BYOC JSON demos
emitted prose.
Fixes reported staging failures:
- shared-state-read-write pills: instructions now reach the model + stronger set_notes guidance
- declarative-hashbrown / declarative-json-render: instructions + ChatResponseFormat.Json (LGP parity)
- declarative-gen-ui: catalog-specific design prompt (no DashboardCard), stronger outer agent
- shared-state-streaming: feature was demo-only in the manifest → shell "Backend fixture unavailable"; added to features
Verified: unit suite 79/79 in Docker SDK 9.
The `completeOnMount` gate added in d70d48a561 named the `mcp-app-iframe`
testid, which only Angular's `copilot-mcp-apps-widget` declared. react-core
and vue build the sandbox iframe imperatively with no testid, so every
React/Vue integration timed the turn out at 30s with
`reason=surface-missing` and never reached `assertIframePresent` — whose
`iframe[sandbox]` fallback would have passed. D5 + D6 `mcp-apps` went red on
all 18 integrations that support the feature (first_failure_at 2026-07-28
23:03Z) while the demos rendered correctly by hand.
Fixed on both sides of the contract:
- react-core and vue now set `data-testid="mcp-app-iframe"` and a `title` on
the host-created iframe, matching Angular. Pinned by a test in each package.
- `completeOnMount` accepts CSS `selectors` alongside `testIds`, so the probe
settles on the same cascade its module doc and assertion already use
(`[data-testid="mcp-app-iframe"], iframe[sandbox]`). A comma-joined entry is
one conjunctive surface whose branches `querySelectorAll` unions, so the
delta/`minNewMounts` semantics are unchanged and `testIds` is now sugar for
the equivalent selector. This half greens the fleet on the next sweep
without waiting for a package release, since the integrations pin
@copilotkit/react-core 1.61.2.
A spec naming no surface now throws instead of burning the turn budget and
reporting a misleading `surface-missing`.
Verified against live staging: after clicking the pill, the old gate matched
0 elements and the cascade matched 1 (the sandboxed iframe was there all
along). Also recorded in showcase/GOTCHAS.md.
Staging click-through on ms-agent-dotnet / ms-agent-harness-dotnet hit
several GOTCHAS #8 defects: aimock D6 was green while live LLMs failed.
A2UI (beautiful-chat sales dashboard, declarative-gen-ui pills):
- Force the page-registered catalogId (models invent "sales_dashboard").
- Sanitize/normalize flat components; salvage type-as-key nests; drop
entries missing id/component (SummaryCard without id, charts without type).
- Strengthen secondary design prompts with the flat catalog contract.
Shared state + subagents side panels:
- Tool invocation drops AsyncLocal set by SetActiveThread, so writes landed
in the global slot while snapshots keyed by AgentSession/AgentThread.
Mirror writes and fall back on read (same pattern as D5ParityAgents).
- Wire the dead TryBuildDeterministicReply path for the "Remember something"
pill so notes update without relying on the model calling set_notes.
Open generative UI advanced + beautiful-chat calculator:
- Prompt for clickable keypad (not form/submit) and notifyHost ping wiring.
Verified: ms-agent-dotnet unit suite 79/79 green in Docker SDK 9; harness
agent builds clean and cvdiag tests 5/5.
The reference page's Parameters section covered only `agentId` and `updates`, so
both props this branch adds were undocumented.
Adds an entry for each, and notes on `agentId` that passing `runtimeAgentId`
makes it required and turns it into a name the hook registers an agent under
rather than one it retrieves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UseAgentProps left `agentId`, `runtimeAgentId`, and `threadId` independently
optional, so every unsafe or pointless combination compiled and only
`threadId`-without-`runtimeAgentId` was caught — at runtime, on render. Given
the bug this branch fixes was a silently-ignored `threadId` prop, the type is
where it should have been caught.
Split UseAgentProps into a base plus a two-branch union so exactly two shapes
are valid:
useAgent() // shared agent
useAgent({ agentId }) // shared agent
useAgent({ agentId, runtimeAgentId, threadId }) // private agent, pinned thread
Every partial combination is now a compile error, each for its own reason.
`threadId` alone would scope a thread onto a shared singleton (the original
review finding). `runtimeAgentId` without `threadId` registers a private agent
that behaves like the shared one, minus nothing but a registration and a local
id to keep unique. And without an explicit `agentId`, the proxy registers under
the chat configuration's agentId or DEFAULT_AGENT_ID — ids that already belong
to real agents — which either throws `already registered` or silently shadows
one, depending on whether runtime discovery has landed.
The runtime throws stay as backstops for callers TypeScript doesn't cover (plain
JS, `as any`, props widened to `string | undefined` at a call boundary), and now
cover all three cases, each failing before any registration happens.
Adds use-agent-types.test.tsx pinning the compile-time half of the contract
across the accept/reject matrix, plus contract tests for the two new throws. The
`@ts-expect-error` on each deliberately-bad call doubles as an assertion that the
type still rejects it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Record the durable findings from OSS-583:
- langgraph-typescript per-framework notes: langchain-js streaming-reassembly bugs
(reasoning index-collision -> disableStreaming; tool-call+content reassembly ->
normalizeAssistantMessage), a2ui-recovery inner-render header forwarding via
ALS/wrapToolCall (mirrors mastra), and multimodal's LFS-asset build requirement.
- 'What Was Green But Still Wrong' #8: single-pass sweeps hide flaky cells and
aimock-green can mask a real backend bug -> re-run for stability + verify the
right graph runs.
- Known fleet-wide follow-ups: the frontend-tools-async stale turnIndex fixture
reds the north star too (needs the same deletion in langgraph-python); image
builds must git lfs pull so multimodal ships real assets not LFS pointers.
## Release monorepo v1.64.1
**Scope:** `monorepo` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.64.1`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `monorepo` packages to npm at version `1.64.1`
- Creates git tag `monorepo/v1.64.1`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
Fixes FAC-61.
## Summary
- Documents the full Google ADK voice runtime route path:
`app/api/copilotkit-voice/[[...slug]]/route.ts`.
- Explains how browser calls to `/api/copilotkit-voice` map through the
Next.js runtime to the ADK backend `/voice` endpoint.
- Keeps the ADK-specific backend paragraph gated to integrations that
declare `voice_backend_pattern: adk-fastapi-agent-path`, so other
framework voice pages do not show Google ADK prose.
- Replaces the docs `voice-page` snippet with a minimal `CopilotKit` +
`CopilotChat` example so the docs no longer show the live demo-only
`VoiceChat` wrapper as copy-paste app code.
- Normalizes docs-only `*.snippet.*` caption display so
`page.snippet.tsx` renders as `page.tsx` while still remaining a
docs-only extraction file.
- Corrects the sample-audio button prose to match the actual
implementation: the button emits canned text through `onTranscribed`,
and the parent chat component performs the textarea insertion.
## Acceptance Evidence
- Full route path: `voice.mdx` names
`app/api/copilotkit-voice/[[...slug]]/route.ts`.
- Backend endpoint: `voice.mdx` explains that `voice-demo` is registered
with `HttpAgent` at `${AGENT_URL}/voice`, and that the Python ADK server
mounts agents at `path=f"/{agent_name}"`.
- Framework scoping: the ADK backend paragraph is wrapped in
`<WhenFrameworkHas flag="voice_backend_pattern"
equals="adk-fastapi-agent-path">`; only the Google ADK manifest declares
that value.
- No undefined docs component: the extracted `voice-page` region lives
in `page.snippet.tsx` and imports `CopilotChat` and `CopilotKit` from
`@copilotkit/react-core/v2`; the rendered caption is normalized to
`page.tsx`; the live route still uses `VoiceChat` outside the docs
region.
- Source inference removed: the page explicitly connects `runtimeUrl`,
`basePath`, catch-all runtime routes, and the backend `/voice` hop in
prose.
## Validation
- `git diff --check`
- `pnpm -C showcase/scripts validate-manifests`
- `pnpm -C showcase/scripts bundle-content`
- `npm run typecheck` in `showcase/shell-docs`
- `npm run lint` in `showcase/shell-docs`
- `npm run build` in `showcase/shell-docs`
## What this is now
Opt-in instrumentation for the channel egress path, plus a
replay-stability guard. **Measurement and safety only — no behaviour
change to how frames are sent.**
It started as a fix too. That fix is reverted here; see below.
## The problem it measures
A streaming reply pushes one render frame per AG-UI token and awaits
each durable acceptance receipt before sending the next, so reply
latency scales with token count. Nothing recorded that, so the cost was
invisible and got attributed to the Connector Outbox's 400ms poll
instead.
```bash
COPILOTKIT_CHANNELS_RENDER_METRICS=summary # one summary per turn
COPILOTKIT_CHANNELS_RENDER_METRICS=frames # also one line per frame
```
Unset means off and no collector is allocated. Output goes to the
transport `log` callback when configured, else the console.
## What it found, measured against production
Hosted api + realtime gateway, local runtime, fixture agent emitting
exactly 120 deltas 15ms apart:
```
pushMsMean: 478.43 pushMsP50: 463 pushMsP95: 589 pushBlockedPct: 99.67
```
**478ms per round trip against an ~85ms network RTT**, so roughly 390ms
of every push is server-side work — the `lookupClerkOrgId` query, the
off-cluster ops-api entitlement call, the read-model build, and the
Redis publish. Intelligence#627 removes those for `text_delta` frames.
At 478ms a push, a 120-token reply costs ~123 round trips ≈ **59s**.
That matches the original "sometimes many seconds" report.
## Why the fix was reverted
Coalescing adjacent text deltas worked — 123 frames to 4, a production
turn from ~59s to 3.4s — and broke replay.
A turn's id is a pure function of its delivery id (`turn_<deliveryId>`,
enforced by app-api) and the adapter resets the per-turn seq counter on
every dispatch, so a redelivery replays into the **same seq space**.
app-api treats that space as an idempotency key: same payload at a seq
is `duplicate_accepted`, a different payload is
`CHANNEL_RENDER_FRAME_CONFLICT`, which nacks the delivery. Merge
boundaries depend on wall-clock arrival, so a replay cuts them
differently:
| | attempt 1 (slow, merged) | attempt 2 (redelivery, faster) |
|---|---|---|
| seq 1–3 | *merged away, never pushed* | `"a"`, `"b"`, `"c"` → INSERT ✓
|
| seq 4 | `text_delta "abcd"` | `text_delta "d"` → **CONFLICT** ✗ |
Before coalescing, deltas were 1:1 with AG-UI events and a replay was
byte-identical. I verified both directions: the new test passes on the
pre-coalescing adapter and fails with coalescing. The regression was
mine.
`render-frame-replay.test.ts` stays as a standing guard — it passes with
frames 1:1 and fails for any change that makes a frame's payload at a
given seq depend on timing.
## The replacement
Batch the **transport**, don't merge frame **content**: send the
buffered frames as an array in one request. Same collapse in round
trips, `seq k` still carries `delta k` so replay stays byte-identical,
and seqs stay dense so the outbox can later require contiguity
(OSS-653).
That needs an app-api batch accept and a gateway envelope, so it ships
after launch with its own deploy ordering (app-api → gateway → SDK)
rather than two days before.
## Also found, filed separately
OSS-653 — the outbox advances an in-memory cursor to the highest seq it
read (`WHERE seq > cursor ORDER BY seq ASC`), so a frame accepted out of
order is skipped and never rendered. Pre-existing and independent of
this PR. Not durable corruption: thread history is rebuilt from all
acceptance rows ordered by seq, independent of that cursor, so threads
read back complete.
## Validation
```
nx run-many -t test check-types build --projects=@copilotkit/channels-intelligence --skip-nx-cache
```
216 tests pass (12 files), check-types and build clean, `oxlint` clean
on all changed files.
refs OSS-648
Coalescing adjacent text deltas cut a 120-token reply from 123 frames to 4 and
took a production turn from an extrapolated ~59s to 3.4s, but it broke replay.
A turn's id is a pure function of its delivery id (turn_<deliveryId>, which
app-api enforces) and the adapter resets the per-turn seq counter on every
dispatch, so a redelivery replays into the same seq space. app-api treats that
space as an idempotency key: the same payload at a given seq is
duplicate_accepted, a different payload is CHANNEL_RENDER_FRAME_CONFLICT, which
nacks the delivery. Merge boundaries depend on wall-clock arrival, so a replay
cuts them differently and collides — attempt 1 stores "abcd" at seq 4, attempt 2
pushes "d" there. Before coalescing, deltas were 1:1 with AG-UI events and a
replay was byte-identical, so this worked; the regression is mine.
Keep the instrumentation, which is what found the problem and is what will
measure the replacement. Add the replay-stability test as a standing guard: it
passes with frames 1:1 and fails for any change that makes a frame's payload at a
given seq depend on timing.
The cost problem is real and unfixed. The replacement is to batch the TRANSPORT
(many frames per request) rather than merge frame CONTENT: it collapses the same
number of round trips, keeps seq k carrying delta k so replay stays
byte-identical, and keeps seqs dense so the outbox can later require contiguity
(OSS-653). That needs an app-api batch accept and a gateway envelope, so it ships
after launch with its own deploy ordering rather than two days before.
Both blockers from the third review.
**1. `banner_viewed` lost its pre-handshake queue.** On `main`,
`fetchAnnouncement()` stored `pendingBannerViewed` and the flush waited for
`connected`, discarding the event when the runtime reported `telemetryDisabled`.
Removing the deferral in the previous commit made the impression post directly,
so it now went out on the `false` placeholder that `telemetryDisabled` reads
before /info answers. That is a regression this PR introduced for an existing
event, distinct from the pre-existing `banner_clicked` race. The queue is
restored — widened from a single slot to a bounded array, since opening the panel
reveals the second surface and can happen before the first impression flushes —
and per-surface dedup is unchanged.
**2. `opened` recorded segmentation it could not know.** `licenseStatus` is
undefined and `runtimeMode` reads its `sse` default before the handshake
(`agent-registry.ts:77`), so an early open against an Intelligence runtime was
permanently filed as SSE. Rather than queue the open — a disconnected runtime
should not suppress a real user's open — `license_status` and `runtime_mode` are
now omitted unless the handshake has landed. An absent dimension is honest; a
wrong one corrupts the segmentation OSS-566 exists to provide. `runtime_url_type`
is config-derived and stays, since it is accurate immediately.
Also fixes a test that passed for the wrong reason: `HeaderMockCore` had no
`runtimeMode` at all, so a pre-handshake assertion about it read `undefined`
either way and would have gone green against the very bug it was meant to catch.
The mock now mirrors the real pre-handshake state (`runtimeMode: "sse"`,
`licenseStatus` absent). With that corrected, 3 of the 4 new cases fail against
the previous commit; the fourth guards the connected path.
Coverage: impression held then dropped on disabled, held then released on
allowed, open before the handshake omits both fields while keeping
`runtime_url_type`, and an open against a connected Intelligence runtime carries
`runtime_mode: intelligence` + `license_status: valid`.
Verified: web-inspector 115 passed, tsc clean, tsdown clean, oxfmt clean, oxlint
9 warnings all pre-existing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Restores Angular Showcase parity with the current React feature
contracts and makes the paired browser audit distinguish Angular
regressions from failures shared by both frontends.
The Angular host is still one shared application. Agent frameworks can
differ behind the runtime boundary, but BuiltInAgent no longer needs
frontend-only route or default-agent fallbacks: its current demos expose
the same named runtime and agent contracts used by React.
## What changed
### Angular runtime parity
- align BuiltInAgent runtime routes and named agent IDs with the current
React demos
- remove stale BuiltIn-only route, agent, reasoning, and interrupt
branches
- configure dynamically created chats with their agent and thread before
component construction, preventing the transient `default` agent lookup
against named-only runtimes
- preserve the remaining integration-specific behavior only where the
backend interaction contract genuinely differs
- preserve currency values in assistant messages without breaking inline
KaTeX rendering
- align Angular A2UI row and column schemas with the shared layout
contract
- expose authoritative running state and prevent an unnecessary
follow-up run in the complete headless demo
- reset interrupt selection state between repeated turns
### Audit and probe hardening
- merge exact-cell shard artifacts and validate source, fixture, and
feature-contract identity
- classify paired results as passed, Angular regression, shared failure,
Angular improvement, React-only, identity mismatch, or missing
counterpart
- make Angular-only failures, identity drift, and missing counterparts
blocking while keeping shared failures visible
- use rendered-surface completion for tool-rendered probes while
retaining lifecycle gates
- avoid duplicate or empty submissions in the headless-simple probe
- require interrupt resolution and resumed-run completion before
advancing
- use turn-scoped assistant content where narration is an accepted
shared contract
- add focused regression tests for Angular bindings, dynamic component
creation, matrix lifecycle handling, parity classification, and
representative probes
No new CI workflow is added by this PR.
## Scope of the parity claim
The audit proves the narrower parity condition: Angular has no failure
in a cell where React passes at the same source revision. Shared
React/Angular failures remain reported but are non-blocking, so this
does not claim that every supported cell is green.
## Validation
Final rebased head: `aeb503c37` on `d80cc9244` (`main`).
Current-head local validation:
- `pnpm nx run @copilotkit/showcase-scripts:test --skip-nx-cache
--output-style=static` — 74 files, 2,366 tests passed
- `pnpm nx run @copilotkit/showcase-harness:test:ci --skip-nx-cache
--output-style=static` — 175 files passed, 2 skipped; 3,672 tests
passed, 18 skipped
- `pnpm nx run @copilotkit/showcase-harness:test:quarantine-ratchet
--skip-nx-cache --output-style=static` — passed
- `pnpm nx run @copilotkit/showcase-harness:typecheck --skip-nx-cache
--output-style=static` — passed
- `pnpm nx run @copilotkit/angular:check-types --skip-nx-cache
--output-style=static` — passed with dependencies
- `pnpm nx run-many --targets=typecheck,test,build
--projects=@copilotkit/showcase-angular-host --parallel=1
--skip-nx-cache --output-style=static` — passed; 20 Angular files and
168 tests passed
- Angular browser build passed its performance-budget and artifact
audits
Focused BuiltInAgent regression rerun at the equivalent pre-rebase
implementation head `b9d51551c`:
- all six previously deterministic Angular failures passed: auth,
multimodal, agent-config, voice, shared-state-read, and
a2ui-fixed-schema
- 68 exact React/Angular cells classified into 35 feature comparisons
- 25 paired passes
- 7 shared failures
- 1 Angular improvement
- 2 React-only cells
- zero Angular regressions
- zero identity mismatches
- zero missing counterparts
The earlier exhaustive browser audit also passed in [GitHub
Actions](https://github.com/CopilotKit/CopilotKit/actions/runs/30065131330):
- 1,296 exact cells: 660 React and 636 Angular
- 497 paired passes
- 46 Angular improvements
- 93 shared failures
- 24 React-only cells
- zero Angular regressions, identity mismatches, or missing counterparts