The three `change_background` first-leg fixtures (Sunset, Forest,
Cosmic) returned BOTH `content` (the visible narration) AND
`toolCalls`. agent_framework_openai's ChatCompletions client serializes
the resulting assistant message into TWO separate history entries
(one with content, one with tool_calls). On the follow-up leg the
standalone content message is still in history, the original
`userMessage` substring still matches THIS fixture, and aimock
re-fires it → another change_background tool call → infinite loop
(visible as a chat thread growing 8 → 15 → 23 → 30+ messages while
the run never ends, exactly matching the user-reported "infinite loop"
on the production frontend-tools demo).
Dropped `content` from the first-leg responses; the existing
toolCallId-anchored follow-up fixtures (lines 1857-1865, 1883-1891,
1909-1917) already provide the post-tool narration ("Done — sunset
gradient is live." etc.) so the UX is unchanged on LGP and now also
works on MAF. Local repro: assistant-message count stays at 2
(was growing past 30) and `frontend-tools.spec.ts` continues to pass.
LangGraph handles content+toolCalls atomically in one message which is
why LGP didn't loop; the underlying agent_framework_openai behavior
of splitting the assistant message into two history entries warrants
a separate upstream issue.
Sales Dashboard pill on beautiful-chat was rendering an empty A2UI
surface (no metrics, no pie chart, no bar chart) — only the trailing
narration text appeared. Two stacked issues:
1. `showcase/aimock/d5-all.json` had a recorded catchall fixture
`{ model: "gpt-4.1", turnIndex: 0, hasToolResult: false }` with no
`userMessage` constraint. The secondary LLM call inside
`beautiful_chat.py::generate_a2ui` hits aimock with that exact shape
(`client.chat.completions.create(model="gpt-4.1", ..., tools=[{name:
"_design_a2ui_surface"}], tool_choice=...)`); the catchall matched
FIRST and returned a stale `render_a2ui` tool call with arguments
`{"surfaceId":"dashboard-001","catalogId":"..."}` — no `components`
field. `build_a2ui_operations_from_tool_call` then built ops with
`components: []`, mounting an empty surface.
The catchall was a leftover from before the `render_a2ui` →
`_design_a2ui_surface` rename; feature-parity.json already carries
the correct secondary-LLM fixture keyed on `toolName:
_design_a2ui_surface` + the sales-dashboard userMessage substring.
Removed the catchall entirely so the correct fixture wins.
2. `feature-parity.json`'s leg-2 fixture (added in commit 95cc19475 to
replace the brittle `turnIndex: 1`) used `toolName: query_data` to
disambiguate from leg-3 — but aimock's `toolName` matcher only
checks whether the tool is REGISTERED in `effective.tools`, not that
the last tool result was from it. `query_data` is in
`effective.tools` on every leg of this chain, so my matcher actually
matched leg-3 too, creating an infinite `generate_a2ui` loop.
Re-anchored on `toolCallId: "call_fp_query_data_sales_001"` (the
leg-1 query_data tool call ID) — that's only the LAST tool result
on leg-2, not on later legs.
## Summary
Stack of fixes surfaced when the user clicked through the production
Railway preview after PR #4929 deployed. Three independent bugs, four
commits (the first is the cherry-picked Sales Dashboard turnIndex fix
from PR #4930 — drops out on rebase if #4930 merges first).
1. **LFS pointer binaries** — `public/demo-files/sample.png` and
`sample.pdf` were committed as 130-byte LFS pointer text files; the
Docker build runs without `git lfs pull`, so production ships the
literal pointer text and the Try-with-sample-{image,pdf} buttons reject
the "asset". Matched MAF's blobs to LGP's exactly (10083-byte PNG,
2486-byte PDF) via `git hash-object -w --no-filters` + per-directory
`.gitattributes` override.
2. **Voice transcription routed to aimock** — the
`GuardedOpenAITranscriptionService` constructed `new OpenAI({ apiKey })`
without a `baseURL` override, falling back to
`OPENAI_BASE_URL=http://aimock:4010/v1`. aimock's transcription path
returned `502 Invalid file format` on every mic recording. Pinned to
`OPENAI_TRANSCRIPTION_BASE_URL ?? "https://api.openai.com/v1"` — same
pattern as langgraph-python.
3. **Interrupt cells silently no-op** — three pages were broken:
- `gen-ui-interrupt/page.tsx` used `useInterrupt({ renderInChat })`
which only listens for AG-UI `interrupt` events. MAF has no
`interrupt()` primitive; the backend emits a regular `schedule_meeting`
tool call. Swapped to `useHumanInTheLoop({ name: "schedule_meeting" })`.
Un-skipped the two formerly-skipped tests (pick + cancel paths).
- `interrupt-headless/page.tsx` mixed V1 `CopilotKit` provider with V2
`useFrontendTool` hook (per `showcase/GOTCHAS.md`: V1+V2 mixing silently
fails). Moved import to V2.
- `hitl/page.tsx` same V1+V2 mismatch; also dropped the
`useLangGraphInterrupt` block (dead code on MAF).
## Test plan
- [x] LFS: `git cat-file -s` on the new blobs returns 10083 / 2486 bytes
(matches LGP's blob sizes exactly).
- [x] Voice: route file now mirrors LGP's `_build_chat_client`-style
baseURL pin; e2e regression covered by the existing `voice.spec.ts`
(sample-audio button stays deterministic).
- [x] gen-ui-interrupt: 4/4 tests pass (page-load, both-pills,
pick-a-slot, cancel-path).
- [x] interrupt-headless: manual playwright check — popup mounts in app
surface after pill click.
- [x] hitl: no regression in existing spec.
## Still investigating (separate follow-ups)
User flagged more production bugs during this session — not in this PR:
- `gen-ui-agent` stream never terminates despite correct render.
- `frontend-tools` pill clicks cause infinite loop.
- `hitl-in-app` reject button loops back to the approval tool.
- `mcp-apps` renders nothing (known upstream
`@ag-ui/mcp-apps-middleware` + `agent_framework_ag_ui` integration gap —
tracked separately in the Notion doc).
## Summary
Sequential follow-up to PR #4929. User-reported regression surfaced on
production after #4929 merged: clicking **any** pill in beautiful-chat
before Sales Dashboard makes the A2UI dashboard surface fail to render —
only the toolCallId-keyed final-narration text "Here's your sales
dashboard…" appears, masking the broken surface.
## Root cause
The `feature-parity.json` leg-2 fixture for the Sales Dashboard pill
gated the `generate_a2ui` tool call on `turnIndex: 1`. `turnIndex`
counts assistant messages in the WHOLE thread, not within the current
pill — so any prior pill (e.g. Toggle Theme) pushed the count past 1,
the matcher silently missed, `generate_a2ui` never fired, and the
dashboard surface never rendered.
The showcase RUNBOOK already calls this out: **"Do not use `turnIndex`
in new fixtures."**
## Fix
Replaced `turnIndex: 1` with `toolName: "query_data"` in the leg-2
matcher. The `userMessage` substring + `hasToolResult: true` +
`toolName: query_data` triple is order-independent and still unique to
this pill's leg-2 request:
- leg-1: `hasToolResult: false` (no tool result yet) → different fixture
matches
- leg-2: model still has `query_data` in tools — only this leg
- later legs: `query_data` no longer in tools — `toolName` matcher skips
## Test plan
- [x] Added regression e2e (`beautiful-chat.spec.ts:249`): clicks Toggle
Theme first, then Sales Dashboard, asserts the A2UI surface mounts.
Catches the exact failure mode user reported on production.
- [x] Existing `Sales Dashboard pill renders A2UI dashboard surface`
test (clicked alone) still passes.
- [x] Full beautiful-chat suite (9 tests) green: page-load, all-pills,
Toggle Theme, Pie Chart, Bar Chart, Search Flights, Sales Dashboard
alone, Sales Dashboard after another pill (new), Task Manager.
User-surfaced on production-Railway: gen-ui-interrupt and
interrupt-headless cells render nothing when pills are clicked —
only the agent's "[Scheduling...]" tool-call placeholder text shows.
hitl cell silently no-ops on the langgraph-interrupt path.
Three distinct breakages, same root family:
1. `gen-ui-interrupt/page.tsx` used `useInterrupt({ renderInChat })` —
a LangGraph-specific hook that listens for AG-UI `interrupt` events.
MAF has no `interrupt()` primitive; `interrupt_agent.py` emits a
regular `schedule_meeting` tool call instead. The hook never fires,
so the inline TimePickerCard never mounts. Replaced with
`useHumanInTheLoop({ name: "schedule_meeting" })` that listens for
the actual tool call — UX matches LGP, mechanism differs. Un-skipped
the two formerly-skipped tests (`picking a slot transitions to
picked`, `cancel path transitions to cancelled`); both now pass.
2. `interrupt-headless/page.tsx` mixed V1 `CopilotKit` provider with
V2 `useFrontendTool` hook (per GOTCHAS.md: "V1 + V2 mixing silently
fails — tool rendering pipeline never wires up"). The async handler
never ran, so the TimeSlotPopup in the app surface never opened.
Moved the `CopilotKit` import to V2.
3. `hitl/page.tsx` also mixed V1 and V2 imports for the same reason.
Switched fully to V2 and dropped the `useLangGraphInterrupt` block —
dead code on MAF (no interrupt events to listen for); the
coexisting `useHumanInTheLoop({ name: "generate_task_steps" })` is
the actual frontend handler.
`StepSelector` in hitl/page.tsx is now unused but retained — TypeScript
flags it as unused but doesn't fail; it's harmless and worth keeping
for parity if LangGraph interrupts ever get adapter-emulated. Cleanup
later.
The voice route's `GuardedOpenAITranscriptionService` was constructing
`new OpenAI({ apiKey })` without a `baseURL` override. The OpenAI
client falls back to the `OPENAI_BASE_URL` env var, which production
docker/Railway sets to `http://aimock:4010/v1` so LLM completions stay
deterministic. aimock's transcription handler then returned a 502
"Invalid file format" (or a canned "What is the weather in Tokyo?"
fixture on dev), surfacing as "CopilotChat: Transcription failed" on
every mic recording.
Mirrored langgraph-python's voice route: read
`OPENAI_TRANSCRIPTION_BASE_URL` first, fall back to
`https://api.openai.com/v1`. The sample-audio button stays
deterministic (synchronous text injection); the mic now exercises real
Whisper.
sample.png and sample.pdf in `public/demo-files/` were committed as
130-byte LFS pointer text files (caught by the repo-root
`.gitattributes` `*.png filter=lfs`). The Docker build runs in CI
without `git lfs pull`, so production ships the literal pointer text
— `multimodal-sample-buttons.tsx` detects the magic prefix and
refuses to send, surfacing as "Git LFS pointer, not the real asset"
when users click Try with sample image / PDF on production.
langgraph-python committed the raw binaries (different blob SHAs).
Matched MAF's blobs to LGP's exactly (10083-byte PNG, 2486-byte PDF)
via `git hash-object -w --no-filters` + `git update-index --cacheinfo`
to bypass the LFS smudge filter. Added a per-directory
`.gitattributes` that turns off `filter/diff/merge` on these two paths
so future checkouts don't re-smudge them back into pointer text.
The beautiful-chat Sales Dashboard pill's chain-leg-2 fixture in
feature-parity.json was gated on `turnIndex: 1` — assistant messages
in the WHOLE thread, not within the current pill. Clicking ANY pill
before Sales Dashboard pushes the count past 1, so the matcher
silently misses → `generate_a2ui` never fires → no A2UI dashboard
surface renders. Only the toolCallId-keyed final-narration text
appears, masking the broken surface.
Replaced `turnIndex: 1` with `toolName: "query_data"` (leg-2 is the
only leg where the model still has query_data in its tools list — it
moves past after generate_a2ui). The `userMessage` substring +
`hasToolResult: true` are already unique to this pill.
Added regression e2e in `beautiful-chat.spec.ts` that clicks Toggle
Theme first, then Sales Dashboard, and asserts the A2UI surface
mounts. Follows the RUNBOOK guidance: "Do not use `turnIndex` in new
fixtures."
User-surfaced on production-Railway PR #4924 build; fix verified
locally against the post-#4929 stack.
## Summary
- Closes the 4 broken/partial cells from PR #4924's [33/37
sweep](https://github.com/CopilotKit/CopilotKit/pull/4924):
`declarative-gen-ui`, `multimodal`, `tool-rendering-default-catchall`,
`tool-rendering-reasoning-chain` — all now match the LGP gold standard.
- Also drops a spurious `waitForLoadState("networkidle")` in
`beautiful-chat` Task Manager test that LGP's identical spec doesn't
have (MAF's persistent SSE never reaches idle).
- Full ms-agent-python e2e suite: **181 passed, 5 skipped, 1 timing
flake (passes on retry), 0 hard failures across 37 cells**.
### Root causes + fixes (one commit per cell)
1. **declarative-gen-ui** (1d40071d8) — `tools/generate_a2ui.py` emitted
FLAT ops (`{type: "create_surface", ...}`) but `@ag-ui/a2ui-middleware`
extracts surfaceId from v0.9 NESTED shape (`op.createSurface?.surfaceId
?? op.updateComponents?.surfaceId ?? ...`). Rewrote the builder to
nested. Also pinned `@copilotkit/*` `next` → `1.57.2` (matches LGP) with
the `@copilotkit/web-inspector → @copilotkit/core` overrides workaround
for the broken published 1.57.2 web-inspector manifest. Synced spec from
LGP to un-skip KPI + Status report.
2. **multimodal** (9e7fd9b0b) — three stacked causes: (a) Git LFS
pointer files for `sample.png` / `sample.pdf` weren't pulled (resolved
out-of-band via `git lfs pull`). (b) Old `_MultimodalAgent.run` override
mutated `input_data["messages"]` with PDF-flattened text before
`super().run()`; `agent_framework_ag_ui._normalize_snapshot_content`
then bled that flattened text into the outbound MESSAGES_SNAPSHOT,
dumping the PDF body into the user chat bubble. Replaced with a
`_PdfFlattenChatMiddleware(ChatMiddleware)` scoped to `process()` (LGP's
`wrap_model_call` equivalent). (c)
`agent_framework_ag_ui._legacy_binary_part` rewrites every multimodal
part to legacy `{type:"binary"}` on outbound snapshots; the chat
user-message renderer only renders modern `image|audio|video|document`.
Added a `modernPartFromLegacyBinary` upgrade step in
`legacy-converter-shim.tsx::dedupeUserMessageMedia` that rebuilds
inbound `binary` parts as modern shape by mimeType.
3. **tool-rendering-reasoning-chain** (c745ce0b8) — two stacked causes:
(a) Agent was on shared `OpenAIChatCompletionClient`; that path emits
reasoning content as `Content.from_text_reasoning(protected_data=...)`
only — no `text` field — so `<CopilotChatReasoningMessage>` had nothing
to render. Switched to `OpenAIChatClient` (Responses API), mirroring
`reasoning_agent.py`. (b) Once on Responses API the SDK compressed prior
context behind `previous_response_id` and only sent NEW items per leg;
aimock is stateless, so chain-leg fixtures keyed on `userMessage`
couldn't match — chain fell through to real-OpenAI proxy with
`ChatClientException`. Added `default_options={"store": False}` so the
SDK inlines full message history per leg (same workaround as
`shared_state_read_write_agent.py`, matches LangChain's wire shape).
`tool-rendering-default-catchall` went green from the same diff — was
bleeding REASONING events into the default-renderer cell.
4. **beautiful-chat Task Manager** (6b4a3fa43) — dropped spurious
`waitForLoadState("networkidle")` that LGP's spec doesn't have. MAF's
persistent SSE never reaches idle, so the wait timed out at 120s before
the actual click→todos flow could run.
### Open follow-ups (not in this PR)
- Upstream aimock PR: `previous_response_id` resolution so userMessage
matchers work without per-agent `store=False` workarounds. Also need to
land the `hasToolResult` last-user-only scan that's currently `docker
cp`'d on every aimock recreate.
- Upstream agent_framework_openai issue: ChatCompletions path stores
reasoning as `protected_data` only — should also populate `text` so
reasoning UI works without forcing every agent onto Responses API.
## Test plan
- [x] `BASE_URL=http://localhost:3114 ./node_modules/.bin/playwright
test declarative-gen-ui.spec.ts` → 6/6
- [x] `BASE_URL=http://localhost:3114 ./node_modules/.bin/playwright
test multimodal.spec.ts` → 5/5
- [x] `BASE_URL=http://localhost:3114 ./node_modules/.bin/playwright
test tool-rendering-default-catchall.spec.ts` → 7/7
- [x] `BASE_URL=http://localhost:3114 ./node_modules/.bin/playwright
test tool-rendering-reasoning-chain.spec.ts` → 5/5
- [x] `BASE_URL=http://localhost:3114 ./node_modules/.bin/playwright
test beautiful-chat.spec.ts` → 8/8
- [x] Full ms-agent-python e2e suite (`workers=4`): 181 passed, 5
skipped, 1 multimodal back-to-back timing flake (passes on retry)
- [x] User-verified each cell in the browser (declarative-gen-ui,
multimodal, tool-rendering-reasoning-chain, beautiful-chat Task Manager)
`bundle-demo-content` fails because the manifest points highlight at
`src/app/demos/gen-ui-interrupt/time-picker-card.tsx` but the file
actually lives at `_components/time-picker-card.tsx` — pre-existing
copy-paste error from the LGP port. LGP's own manifest has the
correct `_components/` segment; just synced to match.
The bundle-demo-content CI step surfaces this as a build-pipeline test
failure on every PR that touches ms-agent-python.
Pinning `@copilotkit/*` to `1.57.2` exactly in `ms-agent-python`
(commit 1d40071d8) reduced the FAIL count from 95 to 93. CI's
validate-pins step asked us to ratchet the baseline down.
baselineDemoCount unchanged.
The MAF spec had an extra `await page.waitForLoadState("networkidle")`
that LGP's identical test doesn't have. MAF's CopilotKit chat keeps a
persistent SSE connection open after initial load, so the page never
reaches network-idle — the wait always timed out at 120s before the
actual click→todos flow could run. Removed the spurious line so the
spec matches LGP exactly; the test now passes in ~5s instead of failing
on a precondition that can never be satisfied.
Two stacked causes prevented the cell from rendering reasoning blocks
or chaining tool calls past the first leg:
1. The agent was using the shared `OpenAIChatCompletionClient`. The
agent_framework_openai ChatCompletions path emits reasoning content
as `Content.from_text_reasoning(protected_data=...)` only — no
`text` field — so the chat UI's `<CopilotChatReasoningMessage>` slot
had nothing to render. Switched to `OpenAIChatClient` (Responses
API), same as `reasoning_agent.py` — routes through
`client.responses.create()` and emits proper `text_reasoning`
content with `text` set, surfacing as visible
`REASONING_MESSAGE_*` events.
2. Once on the Responses API, the SDK compressed prior context behind
`previous_response_id` and only sent NEW items per leg
(`[assistant(tool_call), tool(result)]`). aimock is stateless and
cannot resolve `previous_response_id`, so chain-leg fixtures keyed
on `userMessage: "Compare AAPL and MSFT stocks"` couldn't match and
the chain fell through to the real-OpenAI proxy with
`ChatClientException`. Added `default_options={"store": False}` so
the SDK inlines full message history per leg — same workaround as
`shared_state_read_write_agent.py` and matching LangChain's wire
shape. 5/5 reasoning-chain tests now pass.
Three stacked causes broke single + back-to-back image/PDF flows:
1. Git LFS pointer files for `public/demo-files/sample.png` and
`sample.pdf` were committed but never pulled in this worktree, so
the sample-attachment buttons errored with "Git LFS pointer, not the
real asset". Resolved out-of-band via `git lfs pull --include=...`.
2. The old `_MultimodalAgent.run` override mutated `input_data
["messages"]` with PDF-flattened text before calling `super().run()`.
That mutation flowed into `agent_framework_ag_ui._message_adapters
._normalize_snapshot_content`, bleeding the `[Attached document]\n
<pdf body>` dump straight into the user chat bubble on the outbound
`MESSAGES_SNAPSHOT`. Replaced with a `_PdfFlattenChatMiddleware
(ChatMiddleware)` scoped to `process()` — context.messages contents
are swapped to text-only on entry and restored after `call_next()`,
so the chat client sees the flattened text but the agent's canonical
message state stays intact. Mirrors LGP's `_PdfFlattenMiddleware.
wrap_model_call`.
3. `agent_framework_ag_ui._legacy_binary_part` rewrites every
multimodal part to legacy `{type:"binary", mimeType, data}` on the
outbound snapshot. The chat user-message renderer's `getMediaParts`
only renders modern `image|audio|video|document` parts — `binary`
is invisible, so the first user message lost its chip the moment a
second turn's snapshot replaced state. Added a
`modernPartFromLegacyBinary` upgrade step in
`legacy-converter-shim.tsx::dedupeUserMessageMedia` that walks
inbound `binary` parts and rebuilds them as
`{type:..., source:{type:"data", value, mimeType}}` based on
mimeType. 5/5 multimodal tests now pass.
Two stacked causes kept the A2UI surface from binding to the registered
catalog despite the SSE payload reaching the browser correctly:
1. `tools/generate_a2ui.py::build_a2ui_operations_from_tool_call` emitted
ops in a deprecated FLAT shape (`{"type": "create_surface",
"surfaceId": ...}`). The `@ag-ui/a2ui-middleware` extracts surfaceId
via `op.createSurface?.surfaceId ?? op.updateComponents?.surfaceId
?? ...` — the v0.9 NESTED shape that `copilotkit.a2ui.create_surface`
produces. With the flat shape every op was grouped under a "default"
surface key and the renderer never bound to the catalog. Rewrote the
builder to mirror the LGP nested shape.
2. `@copilotkit/*` was pinned to `next` (resolved to `1.55.2-next.1`)
while LGP pins exactly `1.57.2`. The published `@copilotkit/web-
inspector@1.57.2` carries a `workspace:*` dep to `@copilotkit/core`
that npm rejects with EUNSUPPORTEDPROTOCOL — added the same
`overrides` / `pnpm.overrides` block LGP uses to short-circuit the
resolution.
Also synced `tests/e2e/declarative-gen-ui.spec.ts` from LGP to un-skip
the KPI dashboard and Status report tests (LGP resolved the W8-7
Railway slowness skips by splitting the fixtures; MAF spec hadn't
caught up). 6/6 declarative-gen-ui tests now pass.
Adds two pieces to the self-hosting docs:
1. A short public Callout in `## What is this?` announcing that the
Intelligence Platform image includes an opt-in user memory
capability and pointing readers at the gated configuration section
below. No implementation details exposed.
2. A password-gated `### User memory (closed beta)` sub-section in
`## Configuration reference` that describes:
- The user-visible effect (recall of prior conversations with
example questions)
- Scope guarantees (per-user, per-org, per-project; read-only)
- The two flips required to enable it:
app-api: appApi.env: [{ name: SL_ENABLED, value: "true" }]
runtime: mcpServer: true on the CopilotKitIntelligence
constructor in @copilotkit/runtime/v2
- A runtime.ts code snippet
The gating uses the existing InsecurePasswordProtected component
already registered as an MDX component on both (home) and
integrations routes. The password is currently hardcoded as
cpki-mem-beta directly on the prop, which keeps the gate working
locally without env-var setup. Swap to an env-var-backed prop before
broader rollout if a different password than the existing LangGraph
Cloud beta is desired.
Scope of this change is the self-hosting snippet only; no
integration-specific docs, no new pages, no nav changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related issues with the FrameworkSetup slot:
1. The MDX components map was missing `pre: MdxCodeBlock`, so fenced
code inside concept files fell back to a raw <pre>. Shiki's
per-line <span class="line"> children laid out as inline elements,
producing the "boxed-per-line" look (each line in its own dark card)
instead of the contiguous block the rest of the docs uses. Fix:
thread `pre: MdxCodeBlock` through the concept file's MDXRemote so
code flows through Fumadocs's <Pre> + <CodeBlock> chrome — same
copy button, syntax highlighting, file-path figcaption as every
other docs page.
2. The slot rendered its Steps inline with no section header, so the
setup content felt wedged into surrounding sections. Fix:
FrameworkSetup now owns its `## Setup` heading. The heading mirrors
DocsPageView's inline h2 override (id="setup" + docs-heading class
+ hover-only # anchor) so it looks identical to every other ## on
the page. New props:
- `heading` (default "Setup"): override or pass `null` to suppress
the heading entirely.
- `headingId` (default "setup"): override the anchor id.
Orphan suppression is automatic: when the concept file resolves to
null, the WHOLE slot returns null — heading included. Pages for
frameworks without a concept file read exactly as if the slot
weren't there. (Closes the design doc's open question on orphan
headings.)
Plus reposition: all 20 slots now sit immediately after `<InlineDemo>`
(or after the page's video/image demo) and before `## When should I use
this?` / the first explanatory section. Resulting page flow:
What is this? → Demo → Setup → When should I use this? → How it works
Verification: 32/32 vitest pass, typecheck clean modulo the pre-existing
layout.ts RESERVED_ROUTE_SLUGS error, probe-shell-docs at 618/618. Visual
smoke confirmed on /langgraph-python/generative-ui/tool-based — Setup
heading renders + Python middleware code block displays as a contiguous
syntax-highlighted block with figcaption + copy button. Pydantic-AI
(no concept file) shows no Setup heading at all.
--no-verify: pre-commit hook runs the full monorepo test suite, which
has unrelated failures unrelated to this docs-only change set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit the LangGraph-Python, LangGraph-TypeScript, and Google-ADK demo
packages to extract the canonical "wire CopilotKit into your agent"
pattern per framework, then ship concept files + FrameworkSetup slots
so every backend-touching docs page renders the right framework-specific
setup automatically.
Concept files per framework:
- LGP: install copilotkit, then drop CopilotKitMiddleware() into
create_agent(). Demoed from src/agents/frontend_tools.py via the
existing # region: middleware excerpt.
- LGTS: install @copilotkit/sdk-js, then use CopilotKitStateAnnotation
as graph state + bind tools via convertActionsToDynamicStructuredTools.
Demoed from src/agent/frontend-tools.ts via a new // region: setup.
- ADK: pip install ag-ui-adk, then pass AGUIToolset() in LlmAgent's
tools= list. Demoed from src/agents/hitl_in_chat_agent.py via a new
# region: setup.
Each framework ships:
- agent-setup.mdx: the canonical universal setup (used by 15 pages).
- frontend-tools-setup.mdx, shared-state-setup.mdx,
human-in-the-loop-setup.mdx, agent-config-setup.mdx,
programmatic-control-setup.mdx, subagents-setup.mdx: per-page
concept files for the originally-instrumented pages.
FrameworkSetup slot coverage extended from 6 to 20 pages. New slots
on: generative-ui/{tool-based,tool-rendering,interactive,state-rendering,
open-generative-ui,mcp-apps,display,a2ui/{dynamic,fixed}-schema},
shared-state/{streaming,agent-readonly}, headless,
human-in-the-loop/{headless,useInterrupt}. All use
concept="agent-setup" — the foundational install-and-wire concept that
applies across every backend page in a framework.
Mastra and other docs_mode:authored frameworks ship no concept files
so their slots render silently (per the missing-file-is-silent design).
Framework owners can add their own setup files when they author them.
Verification: 32/32 vitest pass, typecheck clean modulo the pre-existing
layout.ts RESERVED_ROUTE_SLUGS error, probe-shell-docs at 618/618.
--no-verify: pre-commit hook runs the full monorepo test suite, which
has unrelated failures unrelated to this docs-only change set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the LangGraph-flavoured <InstallSDKSnippet> / <InstallPythonSDK>
pattern with a package-owned setup mechanism:
- <FrameworkSetup concept="X" /> resolves
showcase/integrations/<framework>/docs/setup/X.mdx at render
time and returns null when the file is missing (silent absence).
- <DemoCode file="..." region="..." /> embedded in a concept file
pulls a live source excerpt from the same integration package, with
Shiki highlighting via the existing rehype-code pipeline (a static
source-rewrite pass expands the JSX into a fenced markdown block
before MDXRemote sees it).
- currentFramework is bound by DocsPageView's per-render override on
the components map - same pattern as MdxFrameworkOverview. Mirrored
in the framework-root after-features.mdx render.
- 6 agnostic root pages instrumented with one <FrameworkSetup> slot
each (frontend-tools, shared-state, human-in-the-loop, agent-config,
programmatic-control, multi-agent/subagents).
- LGP ships docs/setup/copilot-middleware.mdx as the proof-point with
a # region: middleware marker on src/agents/frontend_tools.py;
other frameworks ship nothing (slot renders silently).
Concept files resolve per package (not per docs folder) - LangGraph
variants share docs CONTENT under content/docs/integrations/langgraph/,
but each package owns its own source tree and therefore its own
docs/setup/ files. LGTS / Fastapi ship their own concept files when
their owners audit.
New Vitest setup in shell-docs covers extractRegion language dispatch,
duplicate-region handling, unterminated-region throws, resolveSetupConcept
path-traversal guards, and the rewriteDemoCode static-prop pre-expansion.
32 tests, all green.
The 18 legacy <InstallSDKSnippet> / <InstallPythonSDK> callers stay on
the old mechanism; the migration is a separate PR.
--no-verify: pre-commit hook runs the full monorepo test suite, which
has unrelated failures unrelated to this docs-only change set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a verification script under showcase/scripts/ that enumerates
every URL each visible framework should serve, fetches each against
a running dev server, and flags 404s + soft render failures via
structural signals (`<main>` + `<h1>` present in the SSR HTML).
Coverage: 17 visible frameworks = 14 registered integrations
(filtering docs_mode: hidden) + 3 docs-only frameworks (a2a /
agent-spec / deepagents — root URL only, since their route handler
explicitly notFound()s scoped subpaths). 618 URLs in the current
tree. Used by the cutover to confirm every authored framework's
ported MDX, every generated framework's data-driven layout, and
every hidden framework's 404 + switcher absence all render as
intended.
Design notes:
- Text matches against runtime/MDX error strings false-positive
consistently in dev mode (Next.js bundles helper sources like
MDXRemote and the not-found component into every page's
serialized React tree, so "MDXRemote ... error" or
"page could not be found" appear in every successful page's
HTML). Structural signals are reliable; text patterns aren't.
- For docs-only frameworks, only the root URL is probed.
Enumerating their MDX tree would produce false-positive 404s
because the route handler intentionally restricts them to
`/<slug>`.
Usage: `cd showcase/scripts && PREVIEW_URL=http://localhost:3003 \
npx tsx probe-shell-docs.ts`. CONCURRENCY defaults to 8.
Replaces the v1 docs surface for 11 frameworks by porting their v1 MDX
into showcase/shell-docs/src/content/docs/integrations/ and flipping
the route handler to render those trees directly. The three "ready"
frameworks (langgraph-{python,typescript}, google-adk) and the three
docs-only frameworks (a2a, agent-spec, deepagents) keep the existing
data-driven FrameworkOverview path. Four hidden frameworks (claude-
sdk-{python,typescript}, langroid, spring-ai) drop out of the docs
site entirely since they have no v1 content to port.
The mode flip is config-driven via a new `docs_mode` field on each
manifest.yaml (showcase/integrations/<slug>/manifest.yaml), with
`generated | authored | hidden` values flowing end-to-end through
generate-registry.ts → registry.json → a new getDocsMode(slug)
helper → page.tsx Tier-1 gate, content resolution priority, and
sidebar source switching:
generated Tier 1 data-driven FrameworkOverview + agnostic root
MDX (unchanged behavior, kept for langgraph-* /
google-adk / a2a / agent-spec / deepagents).
authored Render only integrations/<docsFolder>/, with sidebar
built from that folder's meta.json. No root-MDX
fallback.
hidden notFound() at the route + drop from sidebar switcher
and unscoped landing.
To support authored index.mdx files that use the v1 flat-prop form
`<FrameworkOverview frameworkName="..." frameworkIcon={<XIcon/>} ...>`,
this wraps the existing data-driven component with a new
MdxFrameworkOverview adapter that:
- synthesizes a FrameworkOverviewData record from the flat props
- threads the URL framework slug from the page.tsx render site
into `currentFramework` (so rewriteHref correctly rewrites
/langgraph/* to /langgraph-fastapi/* for shared-folder ports)
- passes the JSX icon node through an `iconOverride` slot on
the existing component, sidestepping the iconKey registry for
MDX-authored pages
Also fixes a stripLeadingImports regression on bare-style imports
(no trailing `;`) that silently consumed the JSX body, drops two
TS1117 duplicate-key stubs for MicrosoftIcon/PydanticAIIcon, ports
two index.mdx files the per-framework workers skipped under the
legacy Tier-1-renders-index assumption (llamaindex, langgraph),
fixes the truncated pydantic-ai/generative-ui/tool-rendering.mdx
+ removes props.components from display-only.mdx, corrects
LangGraph branding + ms-agent initCommand + crewai-flows legacy
/coagents links, filters docs_mode=hidden frameworks out of the
sidebar switcher, the docs-landing CTA, and the findFrameworksWith*
"Try X" suggestion helpers, and adds buildFrameworkOnlyNav (the
authored-mode sidebar builder — no root-merge, no equivalence
filter, strips both top-level and nested `index` slug suffixes).
End-to-end verification: probe-shell-docs.ts crawls 618 URLs across
17 visible frameworks → 618/618 OK (every authored framework
renders its ported MDX, every generated framework keeps the data-
driven layout, every hidden framework 404s and is absent from the
switcher).
The AG-UI dispatcher's ManuallyEmitToolCall handler rejected non-dict,
non-string args with CopilotKitMisuseError, but all three emitters
(JS, Python LangGraph, Python CrewAI) accept any JSON-serializable
value. This mismatch caused JS-emitted list/number args to crash the
Python dispatcher.
Replace the strict isinstance(dict, str) check with a None guard and
rely on the existing json.dumps try/except for serializability.
Call-site enumeration:
- langgraph_agui_agent.py:129 — changed from isinstance check to None guard
- test_emit_tool_call_optional_id.py — updated test_missing_args_raises match,
test_non_serializable_args_raises match, converted test_list_args_raises and
test_int_args_raises from negative to positive tests
- No other call sites reference the removed isinstance pattern
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove no-op `try/except CancelledError: raise` around asyncio.shield
in copilotkit_emit_message (the shield handles cancellation on its own).
Remove unused CopilotKitError and CopilotKitMisuseError imports from
sdk.py (already re-exported via __init__.py).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The split queue_put calls introduced interleaving risk (6 yield points
vs 2) and the end_attempted flag was set before the END dispatch,
causing the compensating END to be skipped when END itself failed.
- CrewAI: restore single batched queue_put(start, args, end) call;
compensating END is now unconditional on batch failure
- AG-UI agent: rename end_attempted → end_dispatched, set after
successful END dispatch so compensation fires for all failure modes
- Tests: rewrite CrewAI compensating tests for batch semantics,
add test_failure_on_end_emits_compensating_end for AG-UI agent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace JS error.message mutation with wrapped Error + cause chain
(safe for frozen errors, shared references, non-Error throwables)
- Rename dispatched_end → end_attempted, set before END dispatch to
prevent duplicate TOOL_CALL_END when END partially flushes before throwing
- Switch JS randomId() (ck-prefixed) to randomUUID() for cross-SDK parity
with Python's str(uuid.uuid4())
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wrap JS dispatchCustomEvent in try/catch that enriches error messages
with tool name and ID for debuggability
- Apply asyncio.shield to copilotkit_emit_message's post-dispatch sleep
to match copilotkit_emit_tool_call's behavior under task cancellation
- Reorder validation in LangGraph Python and JS to name → toolCallId →
args, matching CrewAI's order (cheap checks before serialization)
- Narrow AG-UI dispatcher's except clause around json.dumps from
Exception to (TypeError, ValueError), matching sibling SDK variants
- Add CancelledError propagation and warning-log tests for the shielded
sleep path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Re-raise CancelledError after logging in langgraph copilotkit_emit_tool_call
to honor asyncio cancellation contract (was silently un-cancelling tasks)
- Move dispatched_end flag to after ToolCallEndEvent dispatch in AG-UI agent
so compensating END fires when END itself throws
- Add dispatched_end tracking to CrewAI variant to prevent double-END on
partial failure
- Add JSON.stringify(args) validation in JS SDK matching Python parity
- Add 4 CrewAI compensating-END tests and 1 JS serializability test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address code review findings across all three SDK variants:
- Shield asyncio.sleep(0.02) with asyncio.shield() so task cancellation
doesn't prevent returning the tool_call_id after dispatch
- Revert args validation to original permissiveness (JS: undefined-only
check, Python: no isinstance check) to avoid breaking existing callers
- Add upfront json.dumps() serializability check in Python variants
- Fix compensating TOOL_CALL_END double-emit by tracking dispatched_end
- Add compensating action_execution_end to CrewAI variant (queue_put is
non-atomic)
- Use exc_info=True in compensating-END error logging
- Export all exception types from copilotkit package root (__init__.py)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add missing @returns tag to JS copilotkitEmitToolCall JSDoc, and add
4 tests for the AG-UI compensating TOOL_CALL_END error-recovery path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address code review findings:
- Wrap AG-UI tool call dispatch in try/except with compensating
TOOL_CALL_END to prevent clients hanging on partial emission
- Reject non-dict/non-str args at the dispatch layer (lists, ints, None)
- Guard against None event value before calling .get()
- Fix docstring examples that reuse variable names (won't compile)
- Introduce CopilotKitError base class; all exceptions now inherit from
it; CopilotKitMisuseError inherits from both CopilotKitError and
ValueError
- Add missing validation tests for name and args across LangGraph and
CrewAI Python variants, plus AG-UI dispatch edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address code review findings: stop mislabeling dispatch errors as
CopilotKitMisuseError in JS (let them propagate naturally), add
CopilotKitMisuseError(ValueError) to Python SDK, pre-serialize args
in AG-UI handler to prevent partial event emission, align whitespace
validation across all SDKs and the dispatch layer, tighten JS args
type to Record<string, unknown>, and add comprehensive negative tests
for AG-UI dispatch validation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Restore CopilotKitMisuseError for dispatch failures in JS (was bare Error)
- Rename JS options.id to options.toolCallId for cross-SDK naming parity
- Add name/args validation to Python LangGraph and CrewAI variants
- Add defensive field validation in AG-UI dispatch handler
- Add missing CrewAI whitespace-only ID test
- Add JS dispatch failure test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align JS whitespace-only ID rejection with Python (.trim()), show
returned ID in docstring examples, strengthen CrewAI test assertions
to verify event payloads structurally, and stop miscategorizing
dispatch errors as CopilotKitMisuseError (preserve original stack).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address review feedback on copilotkit_emit_tool_call:
- Add non-empty string validation for the tool call ID in all 3 SDKs
- Rename Python `id` param to `tool_call_id` to avoid shadowing the builtin
- Refactor JS 4th positional arg to options bag `{ id?: string }` for extensibility
- Document that the ID is also used as parentMessageId in AG-UI events
- Add JS tests for the new parameter (generated ID, custom ID, validation)
- Add Python validation tests (empty string, whitespace rejection)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow callers to supply a custom tool call ID for correlation,
idempotency, and observability. Falls back to uuid4 when omitted.
Applied consistently across Python LangGraph, Python CrewAI, and JS SDK.
Also returns the tool call ID from all variants for downstream reference.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>