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.
## Summary
`shared-state-read-write` pills showed **no chat responses** on staging.
### Cause
#6227 wired deterministic replies for the suggestion pills, but those
updates were emitted as:
```csharp
new AgentRunResponseUpdate { Contents = [new TextContent(...)] }
```
without `Role = ChatRole.Assistant`. AG-UI's .NET adapter only turns
assistant-role text into `TEXT_MESSAGE_*` events, so the frontend
dropped every pill reply. Notes snapshots could still land; chat looked
dead.
### Fix
- Set `Role = ChatRole.Assistant` on deterministic text updates
- Prefer `message.Text` when resolving the latest user message
- Broaden pill matching for greet / weekend / remember-something copy
## Test plan
- [x] `dotnet build` ms-agent-dotnet agent
- [ ] Staging after deploy: Greet / Remember something / Plan a weekend
all show assistant text; Remember something updates the notes panel
## Summary
`declarative-gen-ui` on staging painted surfaces but charts showed **No
data available** and tables were empty.
### Cause
With `injectA2UITool: false`, the secondary design LLM does **not**
receive frontend App Context (`useSalesAnalystContext` /
sales-context.ts). It only got a thin design prompt, so it omitted or
emptied `PieChart`/`BarChart` `data` arrays and `DataTable` rows.
### Fix
- Embed the Vantage Threads Q2 dataset + composition rules into
`DeclarativeGenUiDesignSystemPrompt`
- Add concrete non-empty PieChart / BarChart / DataTable examples
- Coerce string chart values to numbers
- Tighten outer agent: one short sentence, no prose dashboards
## Test plan
- [x] GenerateA2ui unit tests 12/12
- [ ] Staging after deploy: all four declarative-gen-ui pills show
populated charts/tables from the Q2 dataset
Mechanical repairs found while auditing the pydantic-ai docs. Each was
verified against the tree; nothing here is a content rewrite.
- Delete `quickstart/pydantic-ai.mdx` + its `meta.json`. `seo-redirects.ts`
already routes `/pydantic-ai/quickstart/pydantic-ai` ->
`/pydantic-ai/quickstart` (rule F6), and adk got the same treatment (F7).
pydantic-ai was the only framework still carrying a `quickstart/`
subdirectory alongside the canonical `quickstart.mdx`.
- `human-in-the-loop/agent.mdx`: link to the canonical quickstart directly
instead of the redirected legacy path, and point the starter link at
`examples/integrations/pydantic-ai` — `examples/coagents-starter-pydantic-ai`
does not exist.
- `docs-links.json`: `subagents.shell_docs_path` was `/multi-agent/subagents`,
which has no page. The real page is `/multi-agent-flows`, which the
entry's own `og_docs_url` already pointed at.
- `headless-simple/chat.tsx`: the console tag said `langgraph-python` inside
the pydantic-ai package. This sits in an `@region` block, so it is pulled
into docs as a snippet. 11 other integrations carry the same copy-paste;
they are left for the fleet sweep.
- `examples/showcases/pydantic-ai-todos/README.md`: `uv run src/main.py` ->
`uv run main.py` (there is no `src/main.py` in that tree), and the stated
Python floor now matches `agent/pyproject.toml` (`>=3.13`).
- `examples/canvas/pydantic-ai/README.md`: Python 3.8+ was unrunnable —
`agent/agent.py` uses PEP 604 unions. Aligned to the sibling tree that
pins the same `pydantic-ai-slim==2.22.0`.
PARITY_NOTES.md and qa/beautiful-chat.md described `agent.to_ag_ui()`,
which v2 removes. Replaced with the AG-UI adapter / `mount_agent()`
wording this branch introduces.
Also flags the PARITY_NOTES "Skipped demos" section as stale rather than
silently leaving it: mcp-apps, hitl-in-chat and hitl-in-chat-booking all
ship, and the reasoning/interrupt reasons no longer match manifest.yaml
(which is the authority). Full rewrite tracked in OSS-777.
The gen-ui-agent comments asserted a `src/agents/gen_ui_agent.py` and a
`set_steps` tool that exist nowhere in this package. The cell has no route
override, so it proxies to the root sales agent and its D6 probe is red on
main (GH #6381). The comments now describe that, instead of an
intended-but-unbuilt contract.
Adds the missing `shared-state-read` entry to manifest.yaml `demos:` — it
was declared under `features:` with no route or highlight. Mirrors
langgraph-python's entry, which likewise omits an agent file because the
cell runs on the neutral default agent.
## Finalize MAF Python: D6 green on official agent-framework 1.0 latest
Brings the `ms-agent-python` showcase integration to a clean,
reproducible D6 state on the officially published latest
`agent-framework` packages, with feature parity to `langgraph-python` on
everything buildable today.
### Dependencies (exact pins, official latest)
- `agent-framework-ag-ui==1.0.1`
- `agent-framework-openai==1.12.0`
- `agent-framework-core==1.13.0`
No beta/rc floors, no ranges. Removed two unused `langchain-*` deps. All
framework deps are exact pins; `validate-pins` ratchet baseline moves
down 31 to 27. The only remaining ms-agent-python pin FAIL is the
shared-frontend `openai ^5.9.0`, identical across every integration
(pre-existing baseline).
### D6 result: all green on the published mock
Verified with `showcase test ms-agent-python --d6 --direct --rebuild`
against the actual published `ghcr.io/copilotkit/aimock:latest`
(**v1.38.0**), freshly pulled: 37 distinct cells executed, 37
conversations completed, zero failures, aggregate `d6:ms-agent-python
green (104.2s)`.
`tool-rendering-reasoning-chain` (previously the only red on the
published mock) is now green: it needed `reasoning.encrypted_content`
echoed back on the second Responses request (upstream
microsoft/agent-framework#7233), which the published mock did not
synthesize until
[aimock#342](https://github.com/CopilotKit/aimock/pull/342), shipped in
aimock **v1.38.0**. Fixed upstream, not worked around.
`multimodal` is un-quarantined and now matches langgraph. It had been
wrongly marked unsupported based on a local-only failure: the
`sample.png`/`sample.pdf` demo assets are Git LFS pointers, and without
git-lfs on PATH the attachment send fails before the run starts
(`runStartCount=0`). langgraph-python multimodal fails locally for the
identical reason yet declares the feature supported. Verified the MAF
agent works (D6 cell green with the real assets, 2 turns, assertions
passed); both production deploys serve the real 10KB PNG.
`not_supported_features` now equals langgraph exactly:
`[gen-ui-interrupt, interrupt-headless]` (both a shared
`@copilotkit/react-core/v2` resume-path bug, quarantined in langgraph
too).
### Cells fixed on this branch
- `tool-rendering-custom-catchall` (18-entry fixture +
MESSAGES_SNAPSHOT-drop subclass so narration renders last)
- `shared-state-streaming` (seed `/document` after RUN_STARTED +
`chunkSize` fixtures so replay emits per-token deltas)
- `tool-rendering-reasoning-chain` (un-quarantined; green on aimock
v1.38.0)
- `frontend-tools-async` (removed a stray broad fixture that
shadowed/looped)
- `open-gen-ui` + `open-gen-ui-advanced` (removed six stray fixtures
colliding in the shared gen-ui fixture file)
- `multimodal` (un-quarantined; parity with langgraph)
### Deferred to upstream (not worked around)
- **a2ui-recovery**: langgraph ships a bespoke A2UI validate-and-retry
recovery demo. MAF Python's A2UI is going native via
[microsoft/agent-framework#7423](https://github.com/microsoft/agent-framework/pull/7423),
which delivers progressive streaming, error recovery, and the sub-agent
design built into `agent-framework-ag-ui`, and even includes the same
two bridge fixes hand-rolled here (unanswered-tool-call stripping + A2UI
MESSAGES_SNAPSHOT suppression). Building a bespoke recovery demo now
would be throwaway. When #7423 merges and releases, the showcase A2UI
migrates to the native path and the recovery demo lands with it.
### Validators
- `generate-registry`: OK
- `validate-pins`: 27 fails, hash matches ratcheted baseline
- `validate-parity`: PASS
- `validate-fixture-tool-surface`: clean
### Notes
- `useCoAgent` is deprecated; all demos use `useAgent` from
`@copilotkit/react-core/v2`.
- Kept in draft pending review. No blocking external gates: aimock#342
shipped in v1.38.0.
Switching models only moved the failure rate around, it never removed it, so
stop relying on the model getting hand-escaped JSON right on the first try.
`create_view` takes `elements` as a stringified JSON array. When the model
appends a stray `}` past the closing `]`, the MCP server rejects the call and
names the exact fault ("Invalid JSON in elements: Unexpected non-whitespace
character after JSON at position N"). That error already comes back as a tool
result, and the agent had no step cap, so a retry was mechanically possible
all along. What blocked it was our own prompt: "Call create_view ONCE" and
"do NOT iterate, do NOT make multiple calls. Ship on the first shot."
The prompt now tells the model to read the error and try again, capped at 2
corrections (3 calls total), with stopWhen: stepCountIs(6) bounding the loop
if it never converges. This mirrors the validate-then-retry recovery pattern
already used for A2UI on the other integrations.
Validated against the real Excalidraw MCP server, using the agent's prompt
extracted verbatim from this file and the real tool schema:
normal runs 12/12 succeeded, all on the first call
attempt 1 force-corrupted with
the real-world stray `}` 10/10 recovered on the second call
Also verified in the running app (local dev server, real key): valid JSON,
isError false, diagram rendered.
Not yet verified in-app: the recovery path itself. No natural failure occurred
during the in-app runs, so the retry is proven at the API level rather than
through the Mastra agent loop.
Owner preference for the 5.x line. Recorded honestly: this reduces the
empty-diagram failure but does not remove it.
Measured against the real Excalidraw MCP server (same system prompt, real
tool schema, via the Responses API the AI SDK actually uses):
gpt-4o-mini create_view OK 3, isError 5
gpt-5.4 create_view OK 7, isError 3
gpt-4.1 create_view OK 8, isError 0
gpt-5.5 create_view OK 10, isError 0
JSON validity of the `elements` argument:
gpt-4o-mini 7 invalid of 12
gpt-5.4 7 invalid of 28, plus two runs whose tool call came back
garbled with unrelated spam text
gpt-5.4 + a hardened prompt 2 invalid of 16 (prompting does not fix it)
gpt-4.1 0 invalid of 12
gpt-5.5 0 invalid of 16
So roughly 30% of diagrams still render as an empty iframe on gpt-5.4. Closing
that gap needs a follow-up, most likely validating or repairing the `elements`
string before the MCP call rather than relying on the model to hand-escape
nested JSON correctly.