Stand-in for a live showcase example (out of scope for this PR): a real
screenshot of <CopilotThreadsDrawer> beside <CopilotChat>, rendered from the
v2 react demo against the Intelligence platform. Embedded as a <Frame> preview
right under the intro.
Match the final component name. Renames the guide and reference pages
(copilot-drawer.mdx -> copilot-threads-drawer.mdx, CopilotDrawer.mdx ->
CopilotThreadsDrawer.mdx), their slugs/URLs, the nav meta entry, the
data-testid default, the <copilotkit-threads-drawer> element mention, and
all prose/import references. The --cpk-drawer-* CSS tokens and ::part names
are unchanged.
Per review: remove the onUnlicensed prop, the unlicensed slot, and the
unlicensed/unlicensed-cta parts from the guide + reference so neither
humans nor agents surface them. Replace with a single neutral line:
threads require Intelligence; a locked view shows without a license key.
- Guide: lead with the user benefit (less reference-y opening), reframe the
headless useThreads alternative to stand alone, add the OpsPlatformCTA
sign-up callout (per review).
- Add threads.mdx (shared-snippet include) for a2a, adk, agent-spec,
deepagents + register each under Intelligence Platform in meta.json.
Per review: defer the Angular drawer docs. Removes the Angular reference
page, the guide's Angular section, and Angular cross-links; keeps the
React guide + reference + the Threads how-to callout.
- Add full StateGraph + Annotation setup with CopilotKitStateAnnotation.spec
- Show complete tool implementation with proper ToolMessage handling
- Include graph compilation with nodes, edges, and routing logic
- Pattern examples after working shared-state-streaming.ts reference
- Fix both Deep Agents and LangGraph docs versions
- Include formatter fixes for JSON files
Fixes FAC-101
- Replace incorrect StateSchema API with Annotation.Root
- Fix undefined modelWithTools variable (use model directly)
- Add missing imports (ChatOpenAI, SystemMessage, RunnableConfig)
- Correct StateGraph constructor to use (annotation, { input, output })
- Handle response.content type conversion properly
- Update comments to reflect actual TypeScript API
The example now matches the actual @langchain/langgraph TypeScript API
as used in the showcase integrations, making it copy-pasteable and
functional.
Resolves FAC-113
Two bugs fixed:
1. Tool result event path: the `a2ui_operations` container was emitted
inside a `TextMessageContentEvent` block. The A2UI middleware only scans
`TOOL_CALL_RESULT` events for the container, so the card never mounted
and the raw JSON appeared as plain text in the chat. Fixed by emitting a
`ToolCallResultEvent` (matching the claude-sdk-python peer).
2. Operation shape: the ops used the legacy flat form
(`{"type": "create_surface", ...}`) which the renderer silently ignores.
Updated to the v0.9 nested form (`{"version": "v0.9", "createSurface":
{...}}`) used by every other working peer (claude-sdk-python, strands,
google-adk).
Also adds the missing langroid aimock D6 fixture for `gen-ui-a2ui-fixed`
(`display_flight` → tool result → confirmation text) so the D6 probe has
a mock response to drive the full surface-render assertion.
D6 cell: d6:langroid/gen-ui-a2ui-fixed red → green
## Summary
Spring-ai's `DisplayFlightTool` was emitting legacy flat A2UI operations
(`{"type":"create_surface",...}`) but the A2UI middleware expects v0.9
nested operations (`{"version":"v0.9","createSurface":{...}}`). This is
the same flat→nested migration done for Python/TS in #5832 and langroid
in #5839. The flat shape was silently ignored by the middleware, so the
flight card never mounted and the `a2ui-fixed-schema` D6 cell was
permanently red.
**Root cause** (confirmed by prior local red-green on the disproven TS
fix):
`DisplayFlightTool.apply()` emitted `a2ui_operations` in the legacy flat
format. The middleware's `tryParseA2UIOperations` parses the container
correctly, but the op dispatchers inside require the v0.9 shape. No
surface
was ever created → card never mounted.
## Fix
Updated the three operations in `DisplayFlightTool.java` to v0.9 nested
format:
| Before (flat, ignored) | After (v0.9 nested, works) |
|---|---|
| `{"type":"create_surface","surfaceId":...,"catalogId":...}` |
`{"version":"v0.9","createSurface":{"surfaceId":...,"catalogId":...}}` |
| `{"type":"update_components","surfaceId":...,"components":...}` |
`{"version":"v0.9","updateComponents":{"surfaceId":...,"components":...}}`
|
| `{"type":"update_data_model","surfaceId":...,"data":{...}}` |
`{"version":"v0.9","updateDataModel":{"surfaceId":...,"path":"/","value":{...}}}`
|
Shape matches `sdk-python/copilotkit/a2ui.py` and the shared Python
tools.
## Local Red-Green Proof (real control-plane probe, `--rebuild` both
runs)
**RED — original flat ops (`{"type":"create_surface",...}`):**
```
$ showcase test spring-ai:a2ui-fixed-schema --d6 --rebuild --keep
✗ d6:spring-ai/gen-ui-a2ui-fixed red (0.0s)
state=red
⚠ Tests failed for spring-ai:a2ui-fixed-schema (exit 1)
```
**GREEN — v0.9 nested ops
(`{"version":"v0.9","createSurface":{...}}`):**
```
$ showcase test spring-ai:a2ui-fixed-schema --d6 --rebuild --keep
✓ d6:spring-ai/gen-ui-a2ui-fixed green (0.0s)
1 passed
✓ Tests passed for spring-ai:a2ui-fixed-schema
```
## Java Build & Tests
All 64 existing spring-ai JUnit tests pass after the change (`mvn test`:
64 run, 0 failures, 0 errors). Code compiles cleanly (`mvn compile -q`).
## Files Changed
-
`showcase/integrations/spring-ai/src/main/java/com/copilotkit/showcase/springai/tools/DisplayFlightTool.java`
— v0.9 nested op format
The `route.ts` file is unchanged from main (the prior no-op `a2ui: {
injectA2UITool: true }` addition has been reverted — it was disproven as
a fix by a real local red-green).
PR #5426 (ag2 multimodal unquarantine) added showcase/aimock/d6/ag2/multimodal.json
with two fixtures keyed on:
- userMessage: "can you tell me what is in this demo image I just attached", turnIndex: 0, context: ag2
- userMessage: "can you tell me what is in this demo pdf I just attached", turnIndex: 0, context: ag2
Those same keys already existed in showcase/aimock/d6/ag2/agentic-chat.json,
creating 2 exact duplicates within the ag2 context scope and pushing the
collision count from 297 → 299 (ceiling = 297), breaking the validate CI job.
Fix: remove the two multimodal-probe entries from agentic-chat.json since
the dedicated multimodal.json is now the authoritative home. The agentic-chat
probe does not send image/PDF turns; the multimodal probe matches via context
"ag2" against multimodal.json directly.
RED: AssertionError: Exact duplicate count (299) exceeds ceiling (297)
→ confirmed in CI run #28840506865 (Showcase: Validate main)
GREEN: all 824 tests pass after removing the duplicate entries
## Summary
- **Restores ag2's `multimodal` D6 pill** from `skipped-incapable` (NSF)
to a working feature by adding a showcase-local ASGI middleware that
normalises AG-UI image/document/binary content parts to OpenAI Chat
Completions `image_url` parts before they hit AG2's `ConversableAgent`.
- **Surgical scope**: middleware mounted only on the multimodal sub-app
— other ag2 routes never see image parts and pay no body-buffer cost.
- **No upstream wait**: option (A) showcase shim, not an autogen PR.
autogen still lacks AG-UI image-part support; the moment they add it the
normalizer is a no-op and the RED-half regression pin flips to alert us.
- **Reverses commit d8a0a25db** for the multimodal half: removes
`multimodal` from `not_supported_features`, adds it back to `features`,
and restores the D6 aimock fixture pair.
`tool-rendering-reasoning-chain` stays quarantined (a different upstream
gap — no `REASONING_MESSAGE_*` events emitted by AGUIStream).
## What was failing
AG2's `autogen.code_utils.content_str` only accepts content-part types
`{"text", "input_text", "image_url", "input_image", "function",
"tool_call", "tool_calls"}`. The harness sends user messages whose
`content` carries:
- modern AG-UI: `{"type": "image" \| "document", "source": {"type":
"data" \| "url", "value": ..., "mime_type": ...}}`
- legacy mirror (appended by `legacy-converter-shim.tsx` for LangChain
integrations): `{"type": "binary", mimeType, data \| url}`
Both trip the gate with `ValueError("Wrong content format: unknown type
image within the content")` BEFORE the request reaches the vision model
— observed live on staging in the D6 multimodal probe. That's why the
feature was quarantined NSF in d8a0a25db.
## How the fix works
`agents/_multimodal_normalize.py` adds a raw-ASGI middleware (mirrors
the existing `RequestUserMessageMiddleware` pattern) that:
1. Buffers each inbound POST body.
2. Walks `messages[*].content` on user-role messages only.
3. Rewrites each AG-UI image/document/binary part to `{"type":
"image_url", "image_url": {"url": ...}}` — data sources become
`data:<mime>;base64,<value>` URLs; URL sources pass through unchanged.
4. Updates the request's `content-length` header.
5. Replays the rewritten body to the downstream AGUIStream endpoint.
Non-user messages, plain-text content, already-normalised parts, and
unknown shapes pass through untouched (identity-preserved on no-op
turns). Any body-parse failure logs at WARNING and replays the ORIGINAL
body so autogen's verbatim error surface stays intact — visibility, not
silent rewrite.
## RED → GREEN evidence
`tests/python/test_multimodal_normalize.py` — 14 unit tests, all pass:
| # | Test | What it pins |
|---|------|------|
| 1 | `test_autogen_rejects_raw_agui_image_part` | RED: `content_str`
raises the verbatim `ValueError` text the D6 probe surfaced |
| 2 | `test_normalized_content_is_accepted_by_autogen` | GREEN: after
normalize, `content_str` returns the rendered string with `<image>`
placeholder |
| 3-7 | shape coverage | image data/url, document data, binary data/url,
mimeType camelCase alias |
| 8-10 | passthrough | text-only, plain-string content, assistant/tool
messages |
| 11 | idempotency | re-running on already-normalised content is a no-op
|
| 12 | error path | unrecognised source → text placeholder (not a hard
fail) |
| 13 | tripwire | middleware class exposes `__init__(app)` + `__call__`
|
RED was independently verified by monkey-patching
`_normalize_content_part` to passthrough — that reproduces the exact
`ValueError("Wrong content format: unknown type image within the
content")` from the staging probe. Restoring the normalizer flips it
back to GREEN.
End-to-end ASGI smoke (run inline during development): a synthetic AGUI
POST body with a modern image part is sent through
`MultimodalContentNormalizerMiddleware` → inner ASGI app sees rewritten
body with correct `content-length`. PASS.
## Out of scope / follow-ups
- **PDF rendering**: PDFs ride through as
`data:application/pdf;base64,...` inside an `image_url` part — they
survive autogen's gate but the vision model can't read them natively.
Flattening PDFs to inline text (the pattern langgraph-python uses via
pypdf) is a separate enhancement; this PR's scope is unblocking the
image path that the D6 `multimodal` pill assertion checks.
- **Upstream**: autogen could fix this in `content_str` by accepting
AG-UI's `image`/`document`/`binary` content types directly. When/if that
lands, the normalizer becomes a no-op and the RED-half test will start
failing (which is the signal to delete the shim).
## Test plan
- [x] `cd showcase/integrations/ag2 && python -m pytest tests/python/` —
16 passed (2 pre-existing gen_ui guard tests + 14 new
multimodal_normalize tests)
- [x] `ruff format --check` on touched python files — clean
- [x] `ruff check` on touched python files — clean
- [x] `cd showcase/scripts && pnpm validate-manifests` — ag2 manifest
validates
- [x] `oxfmt --check showcase/aimock/d6/ag2/multimodal.json` — clean
- [x] Verified `multimodal_app.user_middleware` includes
`MultimodalContentNormalizerMiddleware` after import
- [x] End-to-end ASGI smoke: middleware rewrites body + updates
content-length, downstream app sees normalised payload
- [ ] Staging deploy: D6 `multimodal` pill flips from
`skipped-incapable` to GREEN with image fixture (1×1 PNG → "image
attachment shows a small abstract test pattern..."). Validated
post-merge via the staging deploy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Remove unused imports: Iterable, ConversableAgent (from autogen),
AGStreamInput (from autogen.ag_ui.adapter) — none appear in executable
code, only in docstring prose.
- Fix raw_msgs possibly-unbound at dispatch guard: initialize to None
before the try block so the identity check at line 302 is always
safe even if model_dump raises before raw_msgs is assigned. Also
tighten the guard to `raw_msgs is not None` to make the no-normalization
fallback explicit.
autogen.ag_ui import unresolved and LLMConfig(dict) "Expected 0 positional
arguments" are ENVIRONMENT findings — autogen.ag_ui ships only in the
ag2[ag-ui] extra (present in the container, not in local Pyright's venv),
and LLMConfig({...}) is the codebase-wide pattern that works at runtime
with ag2>=0.9 as installed in the container.
AG2's ConversableAgent runs every user message through
``autogen.code_utils.content_str``, which only accepts content-part
types in {"text", "input_text", "image_url", "input_image", "function",
"tool_call", "tool_calls"}. CopilotChat / the AG-UI runtime emits image
and document attachments as the modern shape
{"type": "image" | "document", "source": {...}}
and the demo page's legacy-converter-shim.tsx ALSO appends a legacy
{"type": "binary", mimeType, data | url}
mirror alongside it (to keep the @ag-ui/langgraph converter happy on
LangChain-based integrations — it rides through on the ag2 path too).
Both shapes trip autogen's allowed-types gate with
ValueError("Wrong content format: unknown type image within the
content")
…BEFORE the request reaches the vision model — observed live in the
D6 multimodal probe (commit d8a0a25db, which originally quarantined
the feature as NSF).
Fix
---
Add ``agents/_multimodal_normalize.py``: a ``NormalizingAGUIStream``
subclass of ``AGUIStream`` that overrides ``dispatch()`` to normalize
AG-UI image/document/binary content parts to OpenAI Chat Completions
``image_url`` parts AFTER ``RunAgentInput`` Pydantic parsing and BEFORE
``AgentService`` serialises the messages for autogen.
This is the only correct interception point:
- Too early (ASGI body rewrite before Pydantic): ``RunAgentInput``
rejects ``image_url`` because it is not an AG-UI standard type —
the discriminated union only accepts image/document/binary/text.
- Too late (inside ConversableAgent): requires patching autogen
internals.
The override works by calling ``normalize_messages_for_autogen()`` on
the dict-serialised messages (same form as ``run_stream`` produces via
``model_dump()``) and re-injecting them via a ``_PatchedRunAgentInput``
wrapper that overrides only ``.messages``, delegating all other
attribute access to the original ``RunAgentInput``.
Conversions:
- {"type": "image", "source": {"type": "data", value, mime_type}} →
{"type": "image_url", "image_url": {"url": "data:<mime>;base64,<value>"}}
- {"type": "image", "source": {"type": "url", value}} →
{"type": "image_url", "image_url": {"url": value}}
- {"type": "document", "source": ...} → image_url with the document's
mime preserved (data:application/pdf;base64,...). The vision model
still can't natively read PDFs, but the request reaches the model
instead of being rejected upstream, which is the failure mode this
fix targets.
- {"type": "binary", mimeType, data | url} → image_url (the
legacy-shim parts ride through cleanly).
- {"type": "text", ...} and already-normalised image_url parts pass
through unchanged (identity-preserved on no-op turns).
Failure path: any normalization error is logged at WARNING and the
original messages are forwarded unchanged — autogen's own ValueError
fires verbatim with its error surface intact.
Manifest + fixture
------------------
- showcase/integrations/ag2/manifest.yaml: remove multimodal from
not_supported_features (with its now-stale comment) and add it back
to the features list next to voice.
- showcase/aimock/d6/ag2/multimodal.json: add the D6 fixture pair
using the actual autoPrompt strings from sample-attachment-buttons.tsx
("can you tell me what is in this demo image I just attached" /
"can you tell me what is in this demo pdf I just attached").
TDD evidence (red-green)
------------------------
showcase/integrations/ag2/tests/python/test_multimodal_normalize.py
contains 14 unit tests, pinned at three layers:
1. RED/GREEN against autogen's actual content gate:
* test_autogen_rejects_raw_agui_image_part — confirms
content_str([{type: image, source: ...}]) raises the verbatim
ValueError the D6 probe surfaced. This is the regression pin: if
autogen ever relaxes the gate, this test fails and we know to
revisit the normalizer.
* test_normalized_content_is_accepted_by_autogen — after
normalize_messages_for_autogen(...), content_str accepts every
part and renders "<image>" for the image_url part.
2. Shape coverage: modern image data/url, modern document, legacy
binary data/url, mimeType camelCase alias, plain-text passthrough,
plain-string content, assistant/tool messages untouched,
unrecognised source → text placeholder, idempotency.
3. NormalizingAGUIStream class surface tripwire.
Control-plane D6 RED→GREEN:
RED (no normalizer, pre-fix container): d6:ag2/multimodal → red
(HTTP 500 agent_run_error_event from content_str ValueError)
GREEN (NormalizingAGUIStream applied): d6:ag2/multimodal → green
## What
Adds a **Session-stack discipline / Cleanup after isolated runs**
subsection to `showcase/TESTING.md`, governing how `--isolate`/`--keep`
is used across a debugging/testing session.
## Why
`--keep` correctly lets an `--isolate <name>` stack survive a run so it
can be reused for a session-long test set. The leak was **agent
discipline**, not the flag:
1. Agents minted a **new** named kept stack per individual cell instead
of reusing ONE stack for the whole session — which is how Docker
accumulated `cvtest2`, `greenproof`, `conformred`, `conformgreen`,
`gp1`..`gp10`, `showcase-iso2/4`, etc.
2. When the session's work was done, the stacks it created were never
torn down — each one holds a slot and offset ports until the host fills
up.
## The discipline encoded
1. **One stack per session, reused** — choose ONE stable `--isolate
<session-name> --keep` and reuse it for ALL tests in the session (derive
the name from the primary slug, e.g. `--isolate <slug>-session`). Never
mint a new named stack per cell/feature/pill.
2. **`--keep` is for intra-session reuse only, never a license to leak**
— if you pass `--keep`, you OWN teardown at session end.
3. **Tear down at session end** — use the survival-notice command
`docker compose -p <name> down --remove-orphans --volumes && rm -rf
<run-dir> <slot-dir>`. `bin/showcase down` does NOT tear down isolated
stacks (it only stops the default `showcase-*` project). Bare
`--isolate` (no `--keep`) auto-cleans and is preferred for one-off
tests.
Teardown mechanics live once in `DEBUGGING.md → Cleanup` (cross-linked);
this section owns the discipline.
## Verification
This is a docs-only behavior change — no probe/Playwright/code red-green
surface applies. Doc quality gates run instead:
- `oxfmt --check showcase/TESTING.md` → passes (the file was correctly
formatted on `main`; the only formatter touch was `*new*` → `_new_`).
- Diff is purely additive (+49 lines, one file).
- Cross-link anchor `DEBUGGING.md#cleanup` verified against the `###
Cleanup` heading.
The three harness facts the guidance relies on were verified by reading
`scripts/cli/_common.sh`, `cmd-test.sh`, and `bin/showcase`: each
`--keep` run claims a fresh slot + idempotent pre-down + brings the
stack up (no attach); a same-name re-run against a still-live kept stack
fails loudly on the duplicate-name guard; the exact teardown command
matches the survival notice at `_common.sh:~1074`.
## Follow-up (not in this PR)
The harness could make this self-enforcing — e.g. warn when a session
uses >1 distinct kept `--isolate` name, or add a `bin/showcase slots
--reap-mine` convenience to tear down all stacks this user created.
Noted for later; no harness changes here.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The DisplayFlightTool was emitting legacy flat A2UI operations
({"type":"create_surface",...}) but the A2UI middleware expects v0.9
nested operations ({"version":"v0.9","createSurface":{...}}).
This is the same flat→nested migration done for Python/TS in #5832 and
langroid in #5839. The flat shape was silently ignored by the middleware,
so the flight card never mounted and the a2ui-fixed-schema D6 cell was
permanently red.
Fix: update the three ops to the v0.9 nested format:
- createSurface (was type:create_surface)
- updateComponents (was type:update_components)
- updateDataModel with value: (was type:update_data_model with data:)
Local red-green proof (real control-plane probe, --rebuild both times):
RED: d6:spring-ai/gen-ui-a2ui-fixed red (flat ops, card never mounts)
GREEN: d6:spring-ai/gen-ui-a2ui-fixed green (v0.9 nested ops, card mounts)
## Summary
The `version_drift` probe fails on the fleet control-plane with
`probe.discovery-enumerate-failed` / `discoveryFailed:true` and writes 0
PB rows.
**Root cause:** the fleet `pnpm-workspace.yaml` carries a multi-segment
glob `examples/v2/*/apps/*`. The `pnpm-packages` discovery source's
`matchPattern` only supports literals and trailing `/*`, and throws
`DiscoverySourceSchemaError("unsupported pnpm-workspace glob pattern")`
for any pattern whose segment after the first `*` is not `""`/`"/"`.
That throw happens inside `expandPatterns` during enumeration —
**before** the probe's `pathPrefix: "packages/"` filter is applied — so
it aborts the entire enumeration. `version_drift` never gets to filter
to `packages/`; it just fails.
**Fix (option a):** thread `pathPrefix` into `expandPatterns` and skip
any pattern whose static (wildcard-free) leading prefix cannot intersect
the requested `pathPrefix`, **before** validating its glob shape.
`examples/v2/*/apps/*` (static prefix `examples/v2/`) is skipped when
the probe only wants `packages/`, so enumeration completes and returns
the `packages/` set.
Preserved behavior:
- An unsupported deep-glob that DOES overlap the requested prefix (e.g.
`packages/*/apps/*` with `pathPrefix: packages/`) still throws the
strict-shape SchemaError.
- With no `pathPrefix`, every pattern is in scope and the existing
strict-throw behavior is unchanged.
## Changes
- `showcase/harness/src/probes/discovery/pnpm-packages.ts` —
`staticPrefix` + `patternIntersectsPrefix` helpers; `expandPatterns` now
takes `pathPrefix` and skips out-of-prefix include/exclude patterns
before shape validation.
- `showcase/harness/src/probes/discovery/pnpm-packages.test.ts` —
red→green regression tests.
## Test plan
- [x] Red: new "skips out-of-prefix deep-glob" test fails against
unfixed source (throws SchemaError in `expandPatterns`)
- [x] Green: same test passes after the fix; out-of-prefix deep-glob
skipped, `packages/` packages still enumerated
- [x] Preserved: unsupported deep-glob overlapping the prefix still
throws; existing "non-trailing `*` throws" test (no pathPrefix)
unchanged
- [x] Full harness suite: 2137 passed (120 files)
- [x] `tsc -p tsconfig.build.json` clean
Wire the starter-smoke probe's keyed errorClass into the dashboard cell-state
flip logic so transient SOFT failures (transport-error / aborted) get two-miss
tolerance: a single soft miss renders amber ~ ("transient, not yet actionable")
instead of flapping the cell red, and only flips red on a second consecutive
miss. HARD failures (smoke-failed) and untagged reds flip immediately.
The flip gate reuses the producer-maintained fail_count (the persisted
consecutive-red counter: 1 on green->red, incremented on sustained red, 0 on
red->green) so the dashboard stays a pure function of the current row — no
dashboard-side counter to thread or reset. Tolerance is applied as a
state->degraded downgrade in buildStarterBadge (same pattern as the existing
stale-green fold), keeping the change additive and self-contained.
Adds STARTER_FAILURE_CLASSES as a dashboard-side mirror of the harness
StarterFailureClass union (the dashboard imports only @/*), guarded by a new
starter-error-class-drift.test.ts set-equality lint against the harness source.
The fleet pnpm-workspace.yaml carries a multi-segment glob
(`examples/v2/*/apps/*`) that the strict matcher rejects with a
SchemaError. Because that throw happens during enumeration — before the
probe's `pathPrefix` filter applies — it aborted the entire version_drift
discovery, surfacing as probe.discovery-enumerate-failed / discoveryFailed
with 0 PB rows.
Skip patterns whose static (wildcard-free) prefix cannot intersect the
requested `pathPrefix` BEFORE validating their glob shape, so a deep-glob
for an unrelated subtree no longer aborts a probe that only wants
`packages/`. An unsupported pattern that DOES overlap the requested prefix
still surfaces the strict-shape SchemaError, and behavior with no
pathPrefix is unchanged.
Fixes TS4111 error in strict TypeScript configurations by using bracket
notation (process.env['PORT']) instead of dot notation (process.env.PORT)
for environment variable access.
Fixes: FAC-86
## Summary
- Productizes the Claude SDK Python and TypeScript showcase demos with
LangGraph-parity frontends.
- Wires the Claude demo backends through the official Claude Agent
SDK/AG-UI adapter paths using `claude-sonnet-4.6`.
- Keeps Claude integration docs hidden for this PR and excludes
generated/authored docs artifacts from scope.
## Why
The goal is to bring the productized LangGraph demo surface to Claude
Agents SDKs without publishing integration docs in this pass. This keeps
the PR focused on local showcase demos, runtime behavior, fixtures, and
validation support.
## How
- Ported the demo frontend surfaces and local shell-dojo support for
Claude SDK Python/TypeScript.
- Added official Claude SDK adapter/backend wiring plus real-Claude
local compose support.
- Updated Claude aimock fixtures and validation ratchets for the
expanded demo set.
- Set both Claude manifests to `docs_mode: hidden` and removed docs
setup/snippet artifacts from the PR scope.
## Problem
\`@ag-ui/a2ui-middleware\` v0.0.10's \`getOperationSurfaceId()\` reads
only the A2UI v0.9 NESTED op format:
\`\`\`json
{"version": "v0.9", "createSurface": {"surfaceId": "...", "catalogId":
"..."}}
\`\`\`
The integrations were emitting the legacy FLAT format:
\`\`\`json
{"type": "create_surface", "surfaceId": "...", "catalogId": "..."}
\`\`\`
Result: all ops fell back to the \"default\" surface key → frontend
never mounted the named surface → \`surface-missing\` failure on
\`declarative-gen-ui\` across ~11 integrations.
## Fix
Convert all a2ui op builders and inline ops to the nested v0.9 format
in:
- \`tools/generate_a2ui.py\` — 9 integrations (agno, claude-sdk-python,
crewai-crews, langgraph-fastapi, langgraph-python, langroid, llamaindex,
pydantic-ai, strands)
- \`tools/search_flights.py\` — 11 integrations (ag2, agno,
claude-sdk-python, crewai-crews, langgraph-fastapi, langgraph-python,
langroid, llamaindex, ms-agent-python, pydantic-ai, strands)
- \`src/agents/a2ui_fixed_agent.py\` / \`a2ui_fixed.py\` /
\`beautiful_chat.py\` — agno, crewai-crews, langroid, pydantic-ai
Already-correct integrations skipped: google-adk,
ms-agent-python/generate\_a2ui.py, ag2/generate\_a2ui.py.
**Total: 25 files changed.**
## Verification
Zero flat-format ops remain in non-comment/non-test code. 96 occurrences
of \`"version": "v0.9"\` present in changed integrations (excluding
google-adk which was already correct).
## Red→Green
\`bin/showcase test\` runs against Docker containers — requires
infrastructure startup. The structural change is a mechanical
search-and-replace: \`getOperationSurfaceId()\` in \`a2ui-middleware\`
v0.0.10 reads \`op.createSurface?.surfaceId\` (nested), which is exactly
what these changes now emit. The old flat \`op.surfaceId\` path is not
read at all by the middleware, explaining the surface-missing fallback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Commit 7c3edca changed sample-attachment-buttons.tsx across all integrations
to auto-send via agent.addMessage with autoPrompt strings:
- "can you tell me what is in this demo image I just attached"
- "can you tell me what is in this demo pdf I just attached"
But the d5 harness fixture and all 19 d6 per-integration multimodal.json
fixtures still matched on the old strings:
- "describe the sample image"
- "summarize the sample document"
Aimock received requests with the new prompts, found no match, returned
a STRICT 404, and the agent emitted a streaming error back to the UI
(exact symptom: "An internal error has occurred while streaming events").
Also update agentic-chat.json across all 20 integrations (those files had
duplicate fallback entries for the old prompts) and fix split-fixtures.ts
to route the new strings to the "multimodal" feature bucket.
Local RED: ms-agent-python and crewai-crews both fail with fixture-miss
status=miss before this change.
Local GREEN: langgraph-typescript passes after this change (both turns
settle with "image" / "document" keywords confirmed in transcript).
Remaining failures after this fix are pre-existing Python backend issues
(ChatClientException on binary content parts in ms-agent-python; CrewAI
flow failure on binary content in crewai-crews) — unrelated to fixture
keys and tracked separately in the pydantic-ai multimodal work.
## Root Cause
`agno 2.6.20` removed `agno.os.interfaces.agui.utils`. The floating
`agno>=2.5.17` pin in `requirements.txt` caused staging to pull the
breaking version on the next build, causing a startup failure.
## Red-Green Proof
**RED** — with `agno>=2.6.20` installed:
```
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'agno.os.interfaces.agui.utils'
```
**GREEN** — with `agno==2.6.19` installed:
```
GREEN: all 3 symbols OK
```
(Symbols confirmed: `async_stream_agno_response_as_agui_events`,
`extract_agui_user_input`, `validate_agui_state`)
## Changes
- `showcase/integrations/agno/requirements.txt`: pinned `agno>=2.5.17` →
`agno==2.6.19` (exact pin, last version with `agui.utils`)
- `showcase/integrations/agno/src/agent_server.py`: added TODO comment
at line 73 import site noting migration to agno 2.6.20+ API is a
follow-up; no structural changes to imports
## Follow-up
Migration of `agent_server.py` imports to the agno 2.6.20+ API (once the
replacement for `agui.utils` is identified) is tracked in the TODO
comment at line 73.
## Note on CI
The `validate-pins` CI check will likely flag pre-existing non-exact
pins across ~15 other integrations (`openai ^5.9.0`, `crewai` ranges,
etc.). This is pre-existing debt not introduced by this PR.
## What & why
Resolves [OSS-132](https://linear.app/copilotkit/issue/OSS-132).
Investigated with systematic-debugging; every conclusion verified
against the **real** OpenAI Responses API.
**Net change: a TanStack version bump only.** No showcase schema change.
- `@tanstack/ai` `0.18.0` → `0.35.0`
- `@tanstack/ai-openai` `0.9.1` → `0.15.6`
- `package-lock.json` regenerated (Dockerfile uses `npm ci
--legacy-peer-deps`)
## The bug
The built-in-agent showcase 400s on every prompt against real OpenAI.
The state tools (`AGUISendStateSnapshot` / `AGUISendStateDelta` /
`set_steps`) declare arbitrary payloads as `z.any()`, which serializes
to a **typeless** JSON-Schema property (`{ "description": ... }`, no
`"type"`).
The old `@tanstack/openai-base`'s `isStrictModeCompatible()` only
screened for `oneOf/allOf/not/$ref/$defs`, so it missed the missing
`type`, sent the tool with `strict: true`, and OpenAI rejected it:
```
400 Invalid schema for function 'AGUISendStateSnapshot':
In context=('properties','snapshot'), schema must have a 'type' key.
```
This was **masked in production** because the deployed showcase runs
against aimock, which replays fixtures without validating the request
schema — a raw `curl` to prod returns a clean `RUN_FINISHED`, green for
the wrong reason.
The ticket's original framing (zod3/zod4 drift → typeless *root*, `got
"None"`) was already fixed by the zod-4 migration; this is the same
symptom one layer down (typeless *property*).
## The fix is upstream
`@tanstack/ai-openai@0.15.6` (via `@tanstack/openai-base@0.9.2`) fixes
`isStrictModeCompatible`: it now detects typeless / `z.any()` properties
and sends `strict: false`. OpenAI accepts typeless properties under
`strict: false` — so `z.any()` works again with no schema change on our
side.
(`@tanstack/ai-openai@0.15.5` also dropped `@tanstack/ai-client` from
its peerDependencies, so no `ai-client` dep is added.)
## Verification (real OpenAI, gpt-4o)
| Probe | Result |
|---|---|
| Typeless property, `strict: true` (raw OpenAI) | **400** — `schema
must have a 'type' key` |
| Typeless property, `strict: false` (raw OpenAI) | **ACCEPTED** —
confirms it was the strict flag, not the schema |
| `z.any()` tool on old adapter (0.9.1/0.15.4) | adapter sends `strict:
true` → **400** |
| `z.any()` tool on new adapter (0.15.6) | adapter sends **`strict:
false`** → **ACCEPTED**, model calls the tool |
| All 3 `z.any()` state tools attached, new adapter | **ACCEPTED**, no
400 |
## Not covered here
The showcase's aimock + Playwright e2e suite was **not** run locally
(this worktree has no installed toolchain). CI runs it on this PR;
please confirm the gen-ui / shared-state demos still pass before merge.
---
_Branch history shows an interim `z.string()` workaround that was
reverted once the upstream fix shipped; the net diff is the version bump
only. Squash-merge recommended._
agno 2.6.20 removed agno.os.interfaces.agui.utils; the floating
agno>=2.5.17 pin in requirements.txt caused staging to pull the
breaking version. Pinned to 2.6.19 (last version with the module).
Added TODO comment at the import site for future migration.
Without chdir, npx resolves tsx from the repo root where it is not installed.
tsx is a dev dependency of showcase/scripts; chdir ensures npx resolves it correctly.
Gates per-request POST + 2xx Response-status + GET health-probe logs behind SHOWCASE_ROUTE_DEBUG across 19 integrations to stay under Railway's 500-logs/sec cap, while logging non-2xx responses unconditionally so production errors stay visible.
Boot-purge of stale .langgraph_api state plus a size-gated restart (du > threshold -> kill agent -> container restart -> purge), replacing an in-flight-wiping periodic truncate loop. Adds mutation-sensitive subprocess tests for the watchdog.