## Summary
- enable the LlamaIndex multimodal Showcase demo
- update the LlamaIndex AG-UI protocol pin and its compatible
core/OpenAI adapter pins
- register the multimodal agent and refresh the integration parity notes
## Verification
- Showcase manifest and route validation passed
- LlamaIndex integration production build passed
- container dependency check passed with core 0.14.24, llms-openai
0.7.10, and protocols-ag-ui 0.4.1
- live local OpenAI smoke passed for both image and PDF attachments
## Known harness issue
The shared D6 image assertion still expects the exact contiguous phrase
`copilotkit logo`; OpenAI returned the semantically equivalent `a logo
for CopilotKit`. The browser validation confirmed one intact image
attachment and a relevant response.
## What
Eleven demos in the `crewai-crews` showcase column (labelled **CrewAI
Flows** in the UI) were served by `add_crewai_crew_fastapi_endpoint`
through a root catch-all, not by the Flow helper. This moves them onto a
real CrewAI Flow and removes the catch-all.
Affected demos: agentic-chat, gen-ui-tool-based, prebuilt-sidebar,
prebuilt-popup, chat-slots, chat-customization-css, headless-simple,
readonly-state-agent-context, agent-config, auth, voice.
## Why
`add_crewai_crew_fastapi_endpoint` wraps the crew in `ChatWithCrewFlow`,
which composes its system message with CrewAI's `build_system_message`.
That boilerplate is unconditional: it instructs the model to introduce
itself and to steer every answer back to the crew's purpose, using a
research-report example. Because the catch-all served the scaffold
research crew, those demos answered the user's question and then offered
to research the latest AI developments.
Measured against real OpenAI on `main`, first turn:
> Hey! I'm here to help you with researching cutting-edge developments
and producing detailed, actionable reports.
> The capital of France is **Paris**. If you'd like, I can also help by
generating a **current research report** on a topic of your choice.
Second turn, arithmetic question:
> 12 × 12 = 144.
> I'm here to help with researching the latest AI developments and
producing actionable reports.
Pre-seeding a hand-written `crew_description` (the existing
`_chat_flow_helpers.preseed_system_prompt`) only retargets that tail, it
does not remove it — verified on `/mcp-apps`, which is pre-seeded and
still introduces itself and offers a diagram.
## How
- New `src/agents/chat_flow.py` holds `PromptedChatFlow`, a one-turn
Flow that owns its own prompt and forwards frontend tools.
`crewai-conversational-flows` already had this class inline; it now
imports the same file, so both columns share one prompt.
- `agent_server.py` registers it at `/chat` via
`add_crewai_flow_fastapi_endpoint`, and the root catch-all registration
is gone. An unrouted agent name now fails loudly instead of landing on
someone else's backend.
- The runtime route's default target becomes `/chat`; the
`agent-config`, `auth`, and `voice` routes point there too.
- The remaining crew endpoints (`/mcp-apps`, `/byoc-hashbrown`,
`/byoc-json-render`) are untouched — each already overrides the composed
system message explicitly.
- Comments that described the removed catch-all were corrected in both
CrewAI columns.
## Verification
Against real OpenAI on the patched backend:
- `/chat` answers `Paris.` and `12 × 12 = 144.` with no purpose-reminder
tail.
- A frontend tool still round-trips: `generate_haiku` emits
`TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END`.
- `POST /` returns 404.
Python suites: 162 passed (`crewai-crews`), 164 passed
(`crewai-conversational-flows`). New coverage in `test_chat_flow.py`
(prompt contract, no crew-chat boilerplate, tool forwarding) plus a
routing contract test asserting no cell can reach a crew endpoint by
fall-through.
D6 replay: see the checklist below.
### D6 replay (local, `--d6 --direct`, warm stack)
All fourteen green: the eleven affected cells (agentic-chat,
gen-ui-tool-based, prebuilt-sidebar, prebuilt-popup, chat-slots,
chat-customization-css, headless-simple, readonly-state-agent-context,
agent-config, auth, voice) plus tool-rendering, hitl-in-chat and
shared-state-read-write as untouched controls.
gen-ui-tool-based needed the second commit: the shared probe had both
CrewAI columns off its chart-integration list, so it sent the haiku
prompt and waited for a haiku card the page cannot draw. The probe's own
unit tests still pass (11).
The conversational column's D6 was not run — it is not deployed, and its
image was not built in this session. Its Python suite passes and its
wiring mirrors the crews column line for line.
Closes OSS-901.
## Problem
`/mastra/generative-ui/a2ui/fixed-schema` could not be followed. A
Mastra onboarding run on Codex stopped there rather than invent an API,
reporting that the guide "depends on unbundled showcase helpers."
That is true, and the mechanism is worse than the report. Region bodies
are assembled at bundle time, so what ships is invisible in a source
diff. The `backend-render-operations` marker sits on **line 1** of
`mastra/src/mastra/tools/index.ts` — put there by the marker-hoist sweep
in 34b6418 so snippets would carry their imports — and the closing
marker is at the bottom of the file. The page therefore published **all
432 lines** of the tools barrel: weather, stock price, dice, d20,
query-data, schedule-meeting, search-flights, the aimock
header-forwarding import, and
```ts
import { generateA2uiImpl, buildA2uiOperationsFromToolCall } from "@copilotkit/showcase-shared-tools";
```
`@copilotkit/showcase-shared-tools` is not a package. It is a tsconfig
`paths` entry (`mastra/tsconfig.json:23`) pointing at `./shared-tools`,
a symlink to `showcase/shared/typescript/tools`. There is nothing for a
reader to install.
Same defect on the strands page from the same sweep: 586 lines of a
1688-line `agents/agent.py`.
## What changed
**The two cells get a dedicated module for the A2UI tool**, so hoisting
the marker to the top of the file yields exactly the tool plus its own
imports. This is the shape of the reference cell
(`langgraph-typescript/src/agent/a2ui-fixed.ts`, which likewise builds
A2UI operations locally) and, on the Python side, of `gen_ui_agent.py` /
`a2ui_dynamic.py`.
| page | before | after |
| --- | --- | --- |
| mastra fixed-schema | 432 lines, 16.5 KB | 166 lines |
| strands fixed-schema | 586 lines | 171 lines |
Every line in the new snippets either installs from npm or is a visibly
local `./` / `@/` module carrying a comment about what a real app uses
instead. Mastra keeps a single operation builder — the beautiful-chat
flight tool now calls the same one. The strands cell also highlights
`tools/generate_a2ui.py` so the guide shows the helper the tool calls.
**A guard in the bundler**, because neither failure mode shows up in
review:
- any `@copilotkit/showcase-*` specifier in a published body fails the
build (corpus is at zero after this change, so no baseline);
- over 200 lines fails the build (median region is 28, p90 is 125; the
48 already over the line are baselined by `slug::region::file` and the
list only shrinks).
**The entrypoint half of the issue lands differently than I first read
it.** OSS-901 flagged `@copilotkit/runtime` +
`@copilotkit/react-core/v2` on the shared A2UI page as a v1/v2 trap. It
is not a broken pairing — v1's `CopilotRuntime` forwards `a2ui` (and
`mcpApps` / `openGenerativeUI`) straight to the v2 runtime
(`packages/runtime/src/lib/runtime/copilot-runtime.ts:414`). But #6618
landed while this branch was open and retired the v1 runtime adapter
across every showcase integration, so the page's v1 root import *was*
the stale half. The block also imported `ExperimentalEmptyAdapter` and
`copilotRuntimeNextJSAppRouterEndpoint` and used neither, so it was a
route a reader could not run. It now shows `createCopilotRuntimeHandler`
from `@copilotkit/runtime/v2` in the single-route form, matching the
rebased showcase route and `/runtime-server-adapter`, with a note that
the legacy form still works.
**On the systemic question.** A separate sweep counted ~50 regions whose
marker sits on line 1 with a matching close at end-of-file, and proposed
failing a region that spans >=90% of its file. That rule does not
survive contact with the published bodies: of 141 regions at >=90% span,
only **2** publish more than 200 lines, and 98 publish under 100 —
dedicated single-purpose files whose whole content *is* the intended
snippet. It would also flag this PR's own fix
(`strands/a2ui_generate.py` is 171/188 = 91%) and the langgraph
reference cells. Published size is the signal that separates the defect
from the pattern, which is what the guard here measures.
## Testing
**Guard catches the pre-fix tree** (restored HEAD sources, moved the new
modules aside, ran the bundler):
```
REAL EXIT=1
Region bodies importing repo-only modules:
mastra::agentic-chat: region "weather-tool-backend" (src/mastra/tools/index.ts) imports "@copilotkit/showcase-shared-tools", ...
mastra::agentic-chat: region "backend-render-operations" (src/mastra/tools/index.ts) imports "@copilotkit/showcase-shared-tools", ...
Region bodies over the published-snippet limit:
strands::a2ui-fixed-schema: region "backend-render-operations" (src/agents/agent.py) publishes 586 lines (limit 200) ...
```
and passes on this branch (`bundler exit=0`, 801 demos bundled).
**Guard unit tests** —
`showcase/scripts/lib/__tests__/demo-region-guard.test.ts`, 10 passed.
Mutation-checked: raising `MAX_REGION_LINES` and short-circuiting the
alias scan fails exactly 2 of them; restoring passes 10/10.
**Showcase script suites** — `demo-region-guard`, `bundle-demo-content`,
`validate-parity`, `verify-shell-docs`, `validate-shared-symlinks`:
**151 passed (5 files)**.
**Mastra vitest** — `tests/vitest/a2ui-context.test.ts`, 5 passed. The
prompt builder lives in the dependency-free `a2ui-context.ts` so this
regression test still runs without the Mastra SDK installed, as it did
before. Mutation-checked: breaking the join fails 1 of 5.
**Strands pytest** — `tests/python/test_generate_a2ui_errors.py` 11
passed (was 1 failed / 10 passed after the move, because the happy-path
test patched `agents.agent.build_a2ui_operations_from_tool_call`;
retargeted at the new module). Whole runnable suite: **40 passed**
across `test_generate_a2ui_errors`, `test_hook_injection`,
`test_sales_state_from_args`, `test_tool_call_cap`. Mutation-checked:
stubbing out the builder call fails the happy-path test.
`test_cvdiag_boundaries` / `test_instrumentor_patch` need `starlette` /
`opentelemetry-instrumentation-threading`, absent from this venv —
unrelated to this change.
**Published snippet, rendered** (`demo-content.json` after bundling):
```
mastra snippet lines: 166 | file: src/mastra/tools/a2ui-generate.ts
import { createTool } from "@mastra/core/tools";
import { z } from "zod";
import { generateText, tool as aiTool } from "ai";
// In your own app this is `import { openai } from "@ai-sdk/openai"`. ...
```
**Docs verification** — `verify-shell-docs.ts` produces a byte-identical
finding set with my two MDX edits toggled on and off (empty diff), so
the edits add no new findings. `component-imports`, `essential-content`
and the rest are unchanged; the suite's pre-existing failures are
untouched.
**Lint / format** — `oxfmt` on all changed TS, `oxlint` clean on the new
and edited files.
## Follow-ups (not in this PR)
- The 48 baselined regions are the same class of defect on other pages —
`strands::supervisor-delegation-tools` publishes 795 lines,
`strands::subagent-setup` 625, `ms-agent-dotnet::weather-tool-backend`
549. Each wants the same split.
- `claude-sdk-typescript/shared-tools/` and
`langgraph-typescript/shared-tools/` are real directories where symlinks
belong — the erosion `showcase/AGENTS.md` documents. Untouched here.
- Dropping the strands commit (`14c7351`) is safe on its own; it only
requires adding
`strands::backend-render-operations::src/agents/agent.py` to the guard
baseline.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reapplies the formatter fix the CI auto-format job pushed (13bed7d4),
which a rebase force-push dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same defect as the mastra cell, same page: the `backend-render-operations`
marker is hoisted to the top of `agents/agent.py`, so
`/aws-strands/generative-ui/a2ui/fixed-schema` published 586 lines of a
1688-line module — the messages-snapshot wrapper, the weather/dice/query
tools, everything — instead of the A2UI tool the step is about.
Move `generate_a2ui` and its `_A2uiError` shape into
`agents/a2ui_generate.py`, which is what `gen_ui_agent.py` /
`a2ui_dynamic.py` already do for their own surfaces ("this module lives in
its own file so the surface area is reviewable in isolation"). `agent.py`
imports the tool back for the shared agent's tool list, so the wiring and
the tool id are unchanged; the published snippet is now 171 lines with its
own imports. `tools/generate_a2ui.py` joins the cell's highlighted files so
the guide also shows the helper the tool calls.
The error-handling suite patched `agents.agent.build_a2ui_operations_from_tool_call`,
which now lives on the new module — retargeted, and it still imports the
tool via `agents.agent` so the re-export stays covered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`/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>
Retires the v1 runtime adapter from `showcase/integrations/`. After
this, **no code under `showcase/integrations/` calls
`copilotRuntimeNextJSAppRouterEndpoint`** — 239 routes across 20
integrations.
This is the cheap path we discussed: single-route mode, which is a
genuine drop-in. **No demo page changes, no route path changes, no `GET`
exports, no new dependencies.**
## The shape
```ts
const copilotHandler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit-x",
mode: "single-route",
});
...
return await copilotHandler(req);
```
**Why single-route:** these demos' frontends are `<CopilotKit
runtimeUrl="/api/copilotkit-x">` with no transport prop, and every
released provider pins the single-route transport. So single-route mode
is what the v1 adapter was already serving. Migrating to multi-route
instead would have meant editing every demo page in lockstep, for no
functional gain — nothing in the showcase probes `/info`.
**Why `createCopilotRuntimeHandler`** rather than
`createCopilotEndpointSingleRoute`: that helper is itself deprecated in
favour of the `mode` option (per the deprecated-aliases table in
`docs/backend/runtime-endpoints.mdx`), and 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 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's optional and none of these routes passed one.
13 `copilotkit-auth/[[...slug]]` routes already use the v2 fetch handler
and are left alone; they only name v1 in comments.
## Verified
The shape 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 in the browser.
It also typechecks against the exact version these integrations pin
(1.68.2).
`mastra` is the 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**:
| | baseline (`origin/main`) | after |
| --- | --- | --- |
| `tsc --noEmit` | errors in 10 files | errors in **9** |
| `vitest run` | 2 files / 13 tests failed, 21 passed | 2 files / 13
tests failed, 21 passed |
- **New type errors introduced: none.**
- **Fixed:** `src/app/api/copilotkit-mcp-apps/route.ts`, whose
`@ts-expect-error` was *already* unused on `main`.
- **Test-neutral:** those 13 failures are pre-existing on `main` (mostly
`extractXHeaders` dereferencing `req.headers` on a `{}` fake request).
Structural audit over all 239 routes, re-run after the pre-commit
formatter: none still imports the v1 root, calls the v1 adapter,
references `ExperimentalEmptyAdapter` or `handleRequest` in code, or is
missing `createCopilotRuntimeHandler` / `basePath` / `mode:
"single-route"`.
**CI has now built all 21 integrations green** — `showcase_build_check`
Docker-builds each changed integration and `next build` typechecks
inside it. That covers the ones I could not stand up locally, including
the non-JS backends (`spring-ai`, `ms-agent-dotnet`, `ms-agent-python`,
`ms-agent-harness-dotnet`, `langroid`, `strands`). Full run: 41 pass / 3
skip / 0 fail.
To be precise about what each gate proves: CI proves these 21 apps still
**build and typecheck**. It does not exercise a chat turn per
integration — that came from the pre-rollout live proof of the shape
itself, plus mastra's local test suite.
## Runtime verification on real cells
Docker is not running on my machine, and `bin/showcase test --d6
--direct` requires it
(`--direct` only swaps the in-process driver for the fleet
control-plane; the containers are
not optional). **So the sanctioned Iron Rule 4 probe has NOT been run**
— that gap is real and
a reviewer should weigh it. What I did instead was run a real
integration directly.
`showcase/integrations/mastra` on this branch, `npm run dev`, aimock as
the model backend.
Probing eight migrated routes with the exact envelope the released
provider sends
(`POST {basePath}` with `{"method":"info"}`) — all **200** with real
runtime payloads:
copilotkit-multimodal 200 agents: multimodal-demo
copilotkit-mcp-apps 200 agents: headless-complete
copilotkit-a2ui-fixed-schema 200 agents: a2ui-fixed-schema
copilotkit-beautiful-chat 200 agents: beautiful-chat
copilotkit-agent-config 200 agents: agent-config-demo
copilotkit-ogui 200 agents: open-gen-ui
copilotkit-declarative-gen-ui 200 agents: declarative-gen-ui
copilotkit-background-agents 200 agents: background-agents
Then three real demo cells driven in a browser, each rendering and
completing a chat turn
through its migrated route (two POSTs, both 200, assistant message
rendered):
/demos/beautiful-chat -> POST /api/copilotkit-beautiful-chat 200, 200
/demos/a2ui-fixed-schema -> POST /api/copilotkit-a2ui-fixed-schema 200,
200
/demos/multimodal -> POST /api/copilotkit-multimodal 200, 200
Deliberately spread across different route configs — plain, `a2ui`, and
a dedicated
vision-model route — rather than three variations of the same one.
### One route is 500, and it is pre-existing
`POST /api/copilotkit` (mastra's main route) returns 500 in dev:
`module-not-found` on
`./schema.js` from `src/cvdiag/cvdiag-emitter.ts`. `schema.ts` **is**
tracked and present —
Turbopack in dev just does not resolve the ESM-style `.js` specifier to
it. Proven
pre-existing by restoring **only** that file to `origin/main` and
re-probing: same 500 on the
v1 code. Its Docker build passes, which is why CI is green.
### A mistake in my own verification, recorded
My first pass at the above ran against the wrong branch — I was still on
the docs branch,
where these routes are v1, so the first six 200s I collected were the
**v1** routes. Caught it,
switched to this branch, and re-ran everything above against the
migrated code. The accidental
run was not wasted: it independently confirms the premise of this PR,
that the v1 adapter and
v2 single-route mode answer the same envelope the same way.
## Judgement call worth reviewing
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 compile error.
I demoted them to `@ts-ignore`, which compiles whether or not the
mismatch survives in a given integration. The honest reason at the time:
19 of these apps can't be built locally, so I couldn't prove per-file
which still need a suppression, and `@ts-ignore` is what 190 sibling
files already use.
Now that CI has built all 21 green, that constraint is gone — the ~220
now-stale suppressions (all of which cite a **v1** type hole that no
longer applies) can be removed and verified by the same 21 builds. I've
left them in place here to keep this PR mechanical and reviewable; say
the word and I'll do it as a second pass.
## 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`. The Dockerfile uses `npm ci
--legacy-peer-deps`, which *does* succeed, so the image still builds —
but a plain `npm ci` doesn't. No manifest or lockfile is in this diff.
- **`mastra`'s vitest suite is red on `main`** — 13 failures, as tabled
above.
## What's left of the v1 entrypoint
| Surface | Before | After |
| --- | --- | --- |
| `showcase/integrations/**` | 239 | **0** |
| `examples/v1/**` | 12 | 12 (deliberately v1, never in scope) |
| everything else in `examples/` | ~61 | ~61 (not in this PR) |
Stacks cleanly with #6617 (docs) — no file overlap. It also **unblocks
the two Claude SDK quickstart pages** I had to revert there:
single-route keeps the starter file at the plain `route.ts` path, so
those pages' prose claims and `verify-shell-docs.ts` starter-path checks
stay valid, and only the fence body needs updating.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Review found two gaps in the previous commit.
`RunAgentInput.context` never reached the model. The bridge puts it on state
under `context`, but `CopilotKitState` does not declare the field, so pydantic
drops it when the endpoint validates the request into the Flow's state and the
"Application context" block rendered without it. That hollowed out the two cells
whose whole point is reading application context: readonly-state-agent-context
(now on the default `/chat` route) and agent-config. `ChatState` declares the
field so it survives validation.
Neither existing test caught it: the readonly probe only asserts the browser
request body carries its sentinel, the agent-config probe encodes the expected
value in the user message, and Flow-level unit tests bypass endpoint state
initialization. The new endpoint test drives the real FastAPI route with two
context entries and asserts both appear in the model's system message; it fails
without the declared field.
The Channels setup fragment still told readers the shared crew sits at the
server root and set `AGENT_URL` accordingly, which the removed catch-all turned
into a dead endpoint. It now points at `/chat` and says Flow rather than crew.
Migrating the consumer was preferred over restoring `/`, which would put the
silent-fallback trap back in place.
Verified: 164 and 166 Python tests pass across the two columns, the docs
setup-content tests pass (16), and readonly-state-agent-context, agent-config
and agentic-chat are green on D6 replay.
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>
The `crewai-crews` column is presented as "CrewAI Flows", but eleven of its
demos were served by `add_crewai_crew_fastapi_endpoint` through a root
catch-all: agentic-chat, gen-ui-tool-based, both prebuilt-chrome cells,
chat-slots, chat-customization-css, headless-simple,
readonly-state-agent-context, agent-config, auth and voice.
That helper wraps the crew in `ChatWithCrewFlow`, whose system message comes
from CrewAI's `build_system_message`. Its boilerplate is unconditional: it tells
the model to introduce itself and to steer every answer back to the crew's
purpose, illustrated with a research-report example. Since the catch-all served
the scaffold research crew, those cells answered the user and then offered to
research the latest AI developments. Measured on real OpenAI, turn two of a
plain arithmetic question came back as "12 x 12 = 144." followed by "I'm here to
help with researching the latest AI developments and producing actionable
reports."
Pre-seeding a hand-written crew description (the existing
`preseed_system_prompt`) only retargets that tail; the boilerplate around it
stays. Verified against the pre-seeded `/mcp-apps` crew, which still introduces
itself and offers a diagram.
Changes:
- Add `chat_flow.PromptedChatFlow`, a neutral one-turn Flow that owns its prompt
and forwards frontend tools, and serve it at `/chat` via
`add_crewai_flow_fastapi_endpoint`. The conversational column already had this
class inline; it now imports the same module so both columns share one prompt.
- Point the runtime's default agent target plus the agent-config, auth and voice
routes at `/chat`, and drop the root catch-all registration. An unrouted agent
name now fails loudly instead of landing on someone else's backend.
- Give gen-ui-tool-based its own `gen_ui_tool_based` Flow, mirroring
langgraph-python's dedicated graph: the cell must force a `render_*` chart call
on the user's turn, which a neutral assistant does not do. Wired into the
conversational registry too.
- Leave the remaining crew endpoints (`/mcp-apps`, `/byoc-hashbrown`,
`/byoc-json-render`) alone; each already overrides the composed system message.
- Refresh the comments that described the removed catch-all.
Coverage: new `test_chat_flow.py` pins the prompt contract, the absence of
crew-chat boilerplate and frontend-tool forwarding; a routing contract test
asserts no cell can reach a crew endpoint by fall-through.
Fixes defects 3, 4, 6, 7, 8, 10, 11 and 12 from the OSS-856 phase 1
validation run. Every claim below was re-verified against installed
package source or a live run, not recalled.
LangGraph quickstart (`integrations/langgraph/quickstart.mdx`):
- Route shape: a caution at the route step. The POST-only route runs the
runtime in single-route mode, which is all chat needs; Threads and the
Inspector need the multi-route catch-all with GET/POST/PATCH/DELETE.
Links to the canonical runtime-endpoints section.
- Port: bare `langgraph dev` serves 2024, not 8123. Verified against both
CLIs (`@langchain/langgraph-cli` help output, and `default=2024` in
`langgraph_cli/cli.py`). The guide keeps `--port 8123` to stay
consistent with every sibling page, and now says so.
- Drop `@copilotkit/react-ui` from the install list. `CopilotSidebar`
lives in `@copilotkit/react-core/v2`; react-ui exports no `./v2` JS
entry point and the v2 react example does not depend on it.
- Checkpointer: state the reason each tab differs. `langgraph dev` fails
to load a graph compiled with a custom checkpointer (reproduced), while
the FastAPI tab needs one because `ag-ui-langgraph` calls
`graph.aget_state(...)`, which raises `ValueError: No checkpointer set`.
- Narrow the shared `uv add` line to what both tabs import, and warn that
a project with exact pins should add them by hand.
A2UI fixed schema (`generative-ui/a2ui/fixed-schema.mdx`):
- Add the missing install step for `@copilotkit/a2ui-renderer` + `zod`,
which the catalog/definitions/renderer snippets all import.
- Add a `StateGraph` + `ToolNode` form for developers who already have a
hand-built graph, gated to the Python LangGraph slugs by a new
`a2ui_agent_form` docs flag so the shared page does not show Python to
langgraph-typescript or LangGraph code to LlamaIndex/ADK/Mastra.
- Repoint the cross-tree `/integrations/langgraph/...` link, which 301'd
back to this same page, at the action-handler reference it promises.
Raw Markdown pipeline (`src/lib/llm-text.ts`):
- `renderPageToLlmText` never applied `filterFrameworkScopedBlocks`, so
`/<framework>/<page>.md` emitted every `<WhenFrameworkHas>` branch with
raw JSX tags, each carrying the one selected framework's snippet. On the
A2UI page that produced three mutually-exclusive "how the schema is
delivered" sections whose prose contradicted the identical code under
each. Gate on the same framework the snippets resolve to, with a
regression test.
Also corrects a factually wrong comment in the langgraph-python showcase
`.env.example` that claimed 8123 was the `langgraph dev` default — the
same mis-belief this ticket found in the docs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Problem
PR #6505 added an inline `NODE_OPTIONS="--max-old-space-size=1536"` to
the `langgraph-typescript` agent launch in
`showcase/integrations/langgraph-typescript/entrypoint.sh`, intending to
bound V8 old-space on the many-core Railway host.
In staging this cap is forcing crash-restarts, not delivering savings.
Staging `showcase-langgraph-typescript` hit a V8 heap-OOM `exit 134` at
`2026-08-18T09:52:31Z` (RSS dropped `2.289 GB -> 0.902 GB` on the
crash-reset).
The cap is also structurally un-overridable. It's appended as
`${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=1536`, so it is
always the *last* `--max-old-space-size` flag on the command line — and
V8 takes the last flag when the same one repeats. An operator-supplied
`NODE_OPTIONS` override therefore always loses to the inline `1536`, so
a Railway env var can't raise the ceiling for this process; only another
code change can.
## Change
Removes only the inline `--max-old-space-size=1536` addition (and its
now-stale explanatory comment) from `entrypoint.sh`, restoring the exact
pre-#6505 launch line:
```
cd /app/src/agent && PORT=8123 HOST=0.0.0.0 npm start &> >(awk '{print "[agent] " $0; fflush()}') &
```
`NODE_OPTIONS` now passes through untouched — an operator override wins
again, and with no `NODE_OPTIONS` set V8 falls back to its own default
sizing (pre-#6505 behavior).
Untouched, by design:
- Worker-recycle logic in the same entrypoint
- `langgraph-python` / `langgraph-fastapi` entrypoints and their
`MALLOC_ARENA_MAX` / `MALLOC_TRIM_THRESHOLD_` allocator tuning (also
from #6505)
`git diff --stat` confirms the diff is scoped to exactly one file:
```
showcase/integrations/langgraph-typescript/entrypoint.sh | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
```
## Local red-green proof
Reconstructed the NODE_OPTIONS composition with plain `node` (v25.8.0 —
absolute MiB numbers will vary by machine/Node version, but the
*ordering*, which is the defect, will not):
**RED — current (pre-fix) launch, operator override lost:**
```
NODE_OPTIONS="--max-old-space-size=3072 --max-old-space-size=1536" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 1728
```
The operator asked for a 3072 MiB ceiling and got capped to 1728 — well
below what was requested, and the source of the crash-restart loop.
**GREEN 1/2 — fix applied, operator override now wins:**
```
NODE_OPTIONS="--max-old-space-size=3072" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 3264
```
**GREEN 2/2 — fix applied, no NODE_OPTIONS set, V8 default restored
(pre-#6505 behavior):**
```
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 4288
```
## Post-merge validation gate
This PR does not attempt to prove the fix in production. Before this is
considered validated, it needs a **24h+ re-soak on a pinned image
digest** of the `showcase-langgraph-typescript` service to confirm the
OOM/exit-134 crash-restart loop is gone under real traffic.
Ref: #6505
PR #6505 added an inline NODE_OPTIONS="--max-old-space-size=1536" to the
langgraph-typescript agent launch to bound V8 old-space on the many-core
Railway host. In production the cap is forcing crash-restarts rather than
saving memory: staging showcase-langgraph-typescript hit a V8 heap-OOM
exit 134 at 2026-08-18T09:52:31Z (RSS dropped 2.289 GB -> 0.902 GB on the
crash-reset).
The cap is also structurally broken for override: because it's appended
after ${NODE_OPTIONS:+$NODE_OPTIONS }, an operator-supplied
--max-old-space-size loses to the inline 1536 (V8 takes the last flag of
a duplicate, but the inline one is always last). A Railway env var can
raise the ceiling for the frontend process but not for the agent process
this line targets, so there's no way to dial the cap up without another
code change.
This removes only the inline --max-old-space-size=1536 addition (and its
now-stale explanatory comment) from entrypoint.sh, restoring the exact
pre-#6505 launch line so NODE_OPTIONS passes through untouched — an
operator override wins again, and with no NODE_OPTIONS set V8 falls back
to its own default sizing. Worker-recycle and the langgraph-python/
langgraph-fastapi allocator tuning (MALLOC_ARENA_MAX, MALLOC_TRIM_THRESHOLD_)
added in the same PR are untouched.
Local red-green proof (node v25.8.0, numbers will vary by machine/node
version but the ordering is the defect):
RED - current launch's NODE_OPTIONS composition, operator override lost:
NODE_OPTIONS="--max-old-space-size=3072 --max-old-space-size=1536" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 1728 (capped well below the requested 3072)
GREEN 1/2 - fix applied, operator override now wins:
NODE_OPTIONS="--max-old-space-size=3072" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 3264
GREEN 2/2 - fix applied, no NODE_OPTIONS set, V8 default restored (pre-#6505):
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 4288
Post-merge validation gate: this needs a 24h+ re-soak on a pinned image
digest before it's considered proven in production; this PR does not
attempt that.
## Summary
- advance the canonical Showcase CopilotKit pin to `1.68.1`
- apply the release consistently across all 21 integrations and the
Showcase shell
- regenerate the affected npm lockfiles, including the LangGraph
TypeScript agent lockfile and the Shell's strict peer-dependency entries
This picks up
[#5837](https://github.com/CopilotKit/CopilotKit/pull/5837), which
bounds the in-memory agent runner to prevent unbounded thread retention
and OOMs.
## Verification
- Showcase pin ratchet passes at the existing 26-failure baseline/hash
- `@copilotkit/showcase-scripts`: 2,511 tests pass
- strict Shell `npm ci --ignore-scripts` succeeds, matching the Showcase
validation workflow
- Shell unit tests pass (241/241) and its production build succeeds
- clean installs and production builds pass for Ag2, Langroid, and
Mastra
- comparison with `origin/main` found no pin-induced TypeScript
diagnostics; the standalone TypeScript failures are pre-existing and
outside the current production build gate
Set MALLOC_ARENA_MAX=2 and MALLOC_TRIM_THRESHOLD_ on the langgraph-python and
langgraph-fastapi entrypoints to curb glibc arena fragmentation on the many-core
Railway host, and NODE_OPTIONS --max-old-space-size=1536 scoped to the
langgraph-typescript agent process (not the sibling Next.js server) to cap the
V8 heap. All values use ${VAR:-default} so explicit Railway overrides win.
Use AsyncOpenAI so cancellation reaches the in-flight GenerateA2UI request while retaining the thread fallback for synchronous backend tools.
Preserve cancelled versus resolved-null interrupts across pinned ag-ui-crewai 0.3.0 by encoding only resolved null as JSON null and failing loudly on version drift.
Reuse the canonical shared render_a2ui schema so the secondary request remains aligned with the shared tool contract.
Ports `showcase/integrations/pydantic-ai` — the last pydantic-ai surface
still on v1 — to Pydantic AI v2. Refs #6364.
Three commits plus a bot formatting fix, best reviewed separately.
## 1. `chore(showcase): port pydantic-ai integration to Pydantic AI v2`
- **`requirements.txt`** → `pydantic-ai-slim[ag-ui,openai]==2.22.0`,
`ag-ui-protocol==0.1.19`. Drops the `opentelemetry-api<1.44` ceiling
from #6374; v2 resolves cleanly against otel 1.44.0, so the workaround
is no longer needed. `starlette<1.0.0` is unchanged and satisfies v2's
`>=0.46.2`.
- **9 `StateDeps` imports** move from `pydantic_ai.ag_ui` (removed in
v2) to `pydantic_ai.ui`.
- **`agent_server.py`** — `Agent.to_ag_ui()` was removed in 2.0.0, so a
`mount_agent()` helper builds the equivalent Starlette sub-app and
mounts it. The shape is deliberately identical to what v1's `AGUIApp`
produced — a Starlette app whose only route is `POST /`, named
`run_agent` — so **all 19 mount paths behave exactly as before, trailing
slashes included, and no TypeScript route file changes**.
`deps` is constructed **per request**. v1's `run_ag_ui` did `deps =
replace(deps, state=state)`, handing each run its own object; v2's
adapter does `deps.state = state`, mutating what it is given. A single
shared instance under v2 therefore lets concurrent runs overwrite each
other's state mid-run.
## 2. `fix(showcase): apply the multimodal provider gate to v2 native
content`
v2's `AGUIAdapter.load_messages` converts AG-UI attachments to native
content types *before* the model boundary; v1 delivered the raw AG-UI
part dicts. `_NATIVE_CONTENT` listed `BinaryContent` as a flatten
fixpoint, so under v2 inline attachments were waved straight through and
the entire provider gate was skipped:
- inline PDFs were no longer text-extracted, so raw bytes went to OpenAI
- unsupported image subtypes (HEIC/SVG/TIFF) were no longer degraded and
reached the provider as images, which fails the turn
- missing-mime magic-byte sniffing never ran
- `AudioUrl`/`VideoUrl` were neither fixpoints nor classifiable, so they
hit the fail-loud raise
`BinaryContent` is no longer a fixpoint. `_classify_native_content` maps
native content onto the same `(kind, scheme, mime, value)` tuple the
AG-UI classifier already produces, so **every existing gate applies
unchanged** — no gate logic was rewritten. `audio/*` and `video/*` are
named explicitly because `_kind_for` routes them to `"other"`, and a
missing mime defaults to `"image"` so the sniffer runs.
Net behaviour matches v1: a supported inline image still flattens to an
`ImageUrl` data URI, which is why most of the suite went green without
touching assertions.
Five assertions did change. They checked that state-backing content was
still AG-UI `InputContent`, which encoded v1's bridging. They now assert
the flatten's output (`ImageUrl`) never appears in state — the leak they
were written to guard. The adjacent identity and snapshot checks that
prove non-mutation are untouched.
## 3. `fix(showcase): gate url-source content and correct the v1-parity
claim`
Adversarial review of the first two commits found the gate was only half
fixed. `_NATIVE_CONTENT` still short-circuited `ImageUrl` and
`DocumentUrl`, which v2 builds from unvetted client input, so url-source
attachments bypassed the gate where v1 routed them through it:
- an `image/heic` or `image/svg+xml` url reached the provider as
`input_image`, which the Responses API rejects — failing the turn
- an `audio/mpeg` document url reached it as `input_file`
- a blank-mime inline PDF went to the image sniffer instead of text
extraction, because `load_messages` collapses `ImageInputContent` and
`DocumentInputContent` to the same bare `BinaryContent` and erases the
modality v1 defaulted on
Native content is now gated **before** the fixpoint check rather than
instead of it. `_classify_native_content` returns a tuple only when the
gate must act; `None` means provider-safe and falls through to the
fixpoint, preserving object identity. `ImageUrl` is gated rather than
rerouted so a provider-safe one keeps its identity and any explicit
`_media_type`.
It also corrected a false claim. The `mount_agent` docstring said
routing *and* behaviour were unchanged. Routing is; model input is not.
v2 defaults `manage_system_prompt='server'`, so each agent's
`system_prompt=` now reaches the model. On v1 it never did —
`_agent_graph` emitted system parts only `if not messages` and the AG-UI
bridge always supplied history — so **18 of 19 agents had silently dead
system prompts on main**. A/B on both versions with the same agent and
request: v1 sends 0 system-prompt parts, v2 sends 1. The new behaviour
is correct and kept; the docstring now says so.
## Verification
Against pydantic-ai 2.22.0, in a venv built from this branch's
`requirements.txt`:
- **52/52 Python tests pass**, up from 42/52.
`test_multimodal_content_mapping.py`'s `importorskip` pointed at the
removed `pydantic_ai.ag_ui`, which would have skipped all 43 of its
tests **green** under v2; it now targets `pydantic_ai.ui.ag_ui` and uses
the public `AGUIAdapter.load_messages` in place of the v1 private
helper.
- **16/19 mounts** return `200 text/event-stream` with `RUN_STARTED …
RUN_FINISHED` and no `RUN_ERROR`, driven through the real app with
`TestClient` using trailing-slash URLs as the TS routes do. The other
three (`/a2ui_dynamic`, `/beautiful_chat`, `/`) reach tool execution and
then fail on a raw `OpenAI()` client constructed inside a tool, which
the harness cannot intercept and aimock handles in CI.
- **Per-request deps isolation** confirmed on
`/shared_state_read_write`: state sent by one request does not appear in
the next.
`build-check (pydantic-ai)` is green on this branch, and because
`requirements.txt` changed, the cached pip layer was invalidated — so
that was a **genuine fresh resolve of pydantic-ai 2.22.0 inside the real
Dockerfile**, not a cached pass. It also confirms dropping the
`opentelemetry-api<1.44` ceiling is safe.
### D6 harness probes — run, with a baseline
The behavioural gate is the shared harness D6 probes. No CI job runs
them for showcase paths, so they were run locally on both this branch
and `main`:
| | main (v1) | this branch (v2) |
|---|---|---|
| passed | **33** / 36 | **34** / 36 |
| `reasoning-display` | ✗ `no reasoning-role message rendered within
5000ms` | ✅ **passes** |
| `gen-ui-agent` | ✗ `waitForTurnComplete … runStartCount=2,
done-signal-missing` | ✗ identical error |
| `shared-state-read` | ✗ `Strict mode: 1 candidate fixture(s) skipped
by sequence/turn state` | ✗ identical error |
```bash
cd showcase
AIMOCK_URL_LOCAL=http://localhost:4010 bin/showcase test pydantic-ai --d6 --direct --rebuild --cycle --verbose
```
**The port takes D6 from 33/36 to 34/36.** The two remaining failures
are pre-existing on `main` with byte-identical error strings — this
branch neither causes nor fixes them, and both are tracked in #6381
rather than blocking here.
`gen-ui-agent` is root-caused and is not fixture drift: that demo was
never ported to pydantic-ai. `src/agents/gen_ui_agent.py` exists in
llamaindex with a real `set_steps` tool but has no counterpart here, the
route points at `/gen_ui_tool_based/` (the chart-viz agent), and
`set_steps` is declared nowhere in the package. The fixture fabricates
`set_steps` calls the backend cannot honour, so pydantic-ai rejects the
unknown tool and exhausts its single retry. Confirmed live against real
OpenAI: the cell returns plain text, which is correct for the code as
written.
`reasoning-display` going green is the notable behavioural gain, and it
retires a documented v1 limitation. `PARITY_NOTES.md:91-97` justifies
omitting the reasoning-message branch of `use-rendered-messages.tsx` on
the grounds that "PydanticAI's AG-UI adapter does not emit reasoning
content today" — true on v1, false on v2. (That block is stale on two
further counts: it cites `@ag-ui/core@0.0.43` where `package.json` pins
0.0.57, and claims `ReasoningMessage` is not exported where it is
imported at `reasoning-block.tsx:4`.) Correcting it is tracked on #6364.
To be precise about what that proves: **v2 forwards reasoning content
where v1 dropped it.** The probe supplies the reasoning channel via its
fixture, so what is verified is the forwarding path — adapter → AG-UI
stream → frontend renderer — end to end. Whether a given model actually
emits a reasoning summary live is a separate matter and outside this
port's control: it requires a native reasoning model
(`reasoning_agent.py` defaults to `gpt-5`, overridable via
`REASONING_MODEL`) and, for summary text, a verified OpenAI
organisation. A live run here returned prose with no reasoning block,
consistent with the org-verification gate rather than anything in the
port.
Also verified: the image builds from scratch on v2. Because
`requirements.txt` changed, the cached pip layer was invalidated, so
`build-check (pydantic-ai)` in CI was a genuine fresh resolve of
pydantic-ai 2.22.0 inside the real Dockerfile — which also confirms
dropping the `opentelemetry-api<1.44` ceiling is safe.
### CI gate coverage, for the record
No CI job exercises this package's runtime behaviour on a PR, on this
branch or on `main`:
- `test / e2e / dojo` runs from the upstream `ag-ui` checkout (`ref:
main`) against upstream example agents, and filters on `packages/**` /
`sdk-python/**`
- `test_showcase-frontend-matrix.yml` is dispatch-only and builds the
integration from `base/` — a frozen-backend React baseline
- `showcase_validate.yml` asserts `tests/e2e/` exists with a minimum
spec count; it does not run it
- the package's own `tests/e2e/` (37 files) is invoked by nothing — per
`AGENTS.md` rule 1 the measuring test is the shared harness probe, so
that layer is legacy
## Remaining for #6364
Two acceptance criteria are outstanding, which is why this says Refs
rather than Closes:
- the harness D6 value-test (`bin/showcase test pydantic-ai --d6
--rebuild`), which no CI gate runs for showcase paths
- `PARITY_NOTES.md` has 6 version-dependent blocks, 4 of which were
already inaccurate against the tree before this PR; left alone
deliberately to keep this diff scoped
## Possible follow-up
`multimodal_agent.py` still reaches into three private APIs
(`pydantic_ai._run_context`, `pydantic_ai.models.wrapper`,
`pydantic_ai.models.{ModelRequestParameters,StreamedResponse}`) and
subclasses `WrapperModel`, overriding
`request`/`count_tokens`/`request_stream`. v2 adds a supported
alternative: `AbstractCapability.before_model_request`, which receives a
`ModelRequestContext` carrying `messages` and `streaming`. Migrating
would delete those private imports and ~85 lines. Deliberately not in
this PR — it fixes nothing and would obscure the review.