The strands-typescript SSOT entry is ciBuilt:true but staging-only (prod
instance not yet provisioned). redeploy-env.ts's default scope was the full
CI_BUILT_SERVICES set for BOTH envs, so a staging-only ciBuilt service would
wrongly enter the prod default scope and fail a manual `redeploy-env.ts prod`
(no prod instance). Filter the default scope by env declaration (explicit
--services stays unfiltered, preserving the contract-pin that an operator can
force a named service in an env it does not declare). imageOf expansion was
already env-aware; this extends the same invariant to the base scope.
Update the inventory-lock test counts for the new service (total 40->41,
CI_BUILT 38->39, staging default scope 39->40; prod default scope stays 38 as
the staging-only service is now correctly excluded).
## Summary
Greens two of the three failing D6 (e2e-full) cells for **both** the
`strands` (Python) and `strands-typescript` integrations: **32/35 →
34/35** each.
## Fixes
- **shared-state-read** — the turn-2 fixture leg wrongly pinned
`turnIndex: 0`, so the aimock matcher skipped it on turn 2 ("candidate
fixture skipped by sequence/turn state") → 404 → turn-2 `sse-missing`.
Dropped `turnIndex` to mirror the langgraph-python gold-standard fixture
(whose turn-2 leg omits it).
- **multimodal** — `sample.png/pdf/wav` were committed as **git-LFS
pointers**, so deploy/test environments without `git lfs pull` served
the ~130-byte pointer text as the uploaded file → the agent run never
started (`runsFinished=0`). Now shipped as regular binaries via a
per-integration `.gitattributes` lfs-unset + real bytes, mirroring
langgraph-python's existing convention ("must stay as regular binaries
... so deploy environments without `git lfs pull` serve the actual
files").
Both fixes verified locally: `showcase/bin/showcase test
{strands,strands-typescript} --d6 --direct --rebuild --isolate` → 34/35
each, shared-state-read + multimodal green.
## Known remaining red (out of scope here)
**gen-ui-declarative** (A2UI dynamic) stays red on both. The A2UI
surface **paints correctly** (real dashboard data), but `generate_a2ui`
never completes, so the run hangs "Running" (`runsFinished=0`). This
reproduces on **real-LLM staging** too, so it is not a fixture/aimock
artifact — it is a Strands A2UI-dynamic run-completion issue. The
fixtures and frontend are byte-identical to langgraph-python's (which
passes), pointing at the Strands adapter's auto-inject completion path.
Tracked separately for an adapter-level fix.
Wire the strands-typescript showcase integration for staging deployment,
mirroring how the Python strands integration is deployed.
- manifest: flip deployed: true so the shell lists it in the integration menu
- railway-envs.ts: add showcase-strands-typescript SSOT entry (staging-only
for now: prod instance not yet provisioned, so it omits the prod env and is
gateIgnore'd until promoted dual-env); regenerate railway-envs.generated.json
- showcase_build.yml + showcase_build_check.yml: add the strands-typescript
build matrix entry, change-detection filter, and dispatch option (railway_id
is the new Railway service id)
- golden fixture + image-ref-gate inventory tests updated for the new service
Railway staging service showcase-strands-typescript provisioned
(showcase-strands-typescript-staging.up.railway.app, health /api/health,
OpenAI-via-aimock env). Prod is added later via the promote pipeline.
## What
Fixes the 6 remaining red D6 cells for the **google-adk** showcase
integration, bringing it to **39/39 on the D6 matrix** (under aimock
replay, reproduced across two independent full-matrix runs, no in-matrix
regression). See the scope/caveats section before reading this as "fully
done."
## Root cause + changes
**1. `entrypoint.sh` — remove `ADK_DISABLE_PROGRESSIVE_SSE_STREAMING=1`
(the big one).**
A/B tested in-container: with the flag set, ADK's non-progressive
aggregation path ends the agentic loop after the first tool round — no
post-tool LLM re-invoke. That broke every demo needing a second turn
after a tool result:
- `subagents` (research → writing → critique never chained past
research)
- `tool-rendering-reasoning-chain` (AAPL → MSFT)
- `shared-state-read-write` (the post-`set_notes` confirmation text)
- `tool-rendering-custom-catchall` (the post-tool narration)
The flag was added to dodge an intermittent "last event is partial"
abort on tool-rendering; the in-callback `stop_on_terminal_text` guard
(shared by every agent) is intended to cover that. See caveat 1 — that
guard's sufficiency is verified under aimock only, not against real
Gemini.
**2. `manifest.yaml` — un-skip-list `tool-rendering-reasoning-chain`.**
It was marked `not_supported` (vacuous-green) only because of the loop
gap; it passes now.
**3. `headless_complete_agent.py` — add `AGUIToolset()`.** Removing the
flag unmasked a pre-existing gap: the frontend `highlight_note` tool was
never injected/routed, so turn 3 dispatched it server-side and the
backend registry rejected it (`Tool 'highlight_note' not found`).
langgraph-python auto-injects frontend tools; ADK needs `AGUIToolset()`
in the agent's `tools` list (every other frontend-tool google-adk agent
has it).
**4. `aimock/d6/google-adk/gen-ui-interrupt.json` — re-key.** The alice
pill 503'd: its emit leg was gated `hasToolResult:false`, but the
earlier sales pill leaves a tool result in the thread (thread-global).
Re-ordered each pill's narration (`toolCallId`) leg before its emit leg
and dropped the `hasToolResult` gate, mirroring the reasoning-chain
fixture pattern.
## Verification
`showcase test google-adk --d6 --isolate` (full per-pill matrix), run
twice on separate isolated stacks:
- Both: `passed=39, failed=0, total=39, state=green`.
- The 6 previously-red cells all pass: shared-state-write,
tool-rendering-custom-catchall, subagents,
tool-rendering-reasoning-chain, gen-ui-interrupt (sales + alice),
gen-ui-headless-complete.
- No regression: all previously-passing cells (incl. all
`tool-rendering*` and A2UI: gen-ui-declarative, gen-ui-a2ui-fixed,
beautiful-chat) stay green.
## Scope and caveats (read before merging)
1. **The flag removal is verified under aimock only, NOT against real
Gemini.** `ADK_DISABLE_PROGRESSIVE_SSE_STREAMING=1` was originally added
for a *real-Gemini intermittent* "last event is partial" abort. D6 is
deterministic aimock replay and cannot reproduce that intermittent
condition, so the "0 partial-aborts" result does not prove the
`stop_on_terminal_text` guard is sufficient against real Gemini. There
is a real (unmeasured) risk this re-introduces the partial-abort on
real-LLM tool-rendering runs. Recommend a real-Gemini smoke of the
tool-rendering demos before relying on this in a real-LLM context.
2. **"39/39" is the in-matrix set, not every feature.** Three features
remain `not_supported` and are excluded from the matrix (not fixed):
`interrupt-headless`, `reasoning-default-render`,
`agentic-chat-reasoning`.
3. **Local verification only.** D6 is not CI-gated for google-adk;
evidence is two isolate runs on one machine, not an independent CI
signal.
## Summary
The pydantic-ai showcase integration crashed with `assert_never` in
pydantic-ai's `_map_user_prompt` whenever AG-UI multimodal
`InputContent` (images / documents / binary, data: and url: sources)
reached the model — AG-UI content types were never normalized to the
native pydantic-ai types (`str` / `ImageUrl` / `BinaryContent` /
`DocumentUrl`) the mapper requires.
## What changed
- **`_MultimodalFlattenModel(WrapperModel)`** normalizes content at the
**model-call boundary** (overriding `request` / `request_stream` /
`count_tokens`) — deliberately NOT a `history_processor`, because that
hook persists its return into `message_history` and would leak flattened
content back to the UI.
- **Supported-type gating + degrade centralized at the single
native-type emission choke point** (instead of scattered
per-content-branch): unsupported image subtypes (HEIC/SVG/TIFF/BMP),
audio/video, and non-fetchable url attachments degrade to a text
placeholder rather than emitting a native type the OpenAI Responses
vision API rejects (which would fail the turn).
- **Mime normalized once** — strips RFC-2045 params/whitespace,
lowercases, and aliases the common non-canonical `image/jpg` →
`image/jpeg` before the allow-list (png/jpeg/gif/webp) test, so real
JPEGs are no longer silently dropped.
- Identity-based (`is`) no-op detection replaces fragile structural
`==`.
## Why it matters
Unblocks multimodal turns in the pydantic-ai showcase integration;
eliminates the `assert_never` crash and stops valid `image/jpg` JPEGs
and url-borne documents from being silently mishandled.
## Testing
- **43 unit tests** (clean pinned venv, pydantic-ai 1.0.18), red-green
proven per gap: `image/jpg` forwarded as a supported JPEG; url-media /
non-PDF-doc degrade instead of emitting an unconditional `DocumentUrl`;
parameterized mime forwarded; state-leak guard (`request_stream`
forwards flattened, not raw); `count_tokens` override; `assert_never`
provably unreachable (all flatten paths return a native type or raise).
- 9 rounds of code review (7 agents/round) to a clean confirmation round
(zero blocking findings).
## Test plan
- [ ] CI green
## Summary
- The pydantic-ai `generate_a2ui` declarative D6 turn was missing an
aimock fixture, producing HTTP 503 `no_fixture_match` on staging
(pydantic-ai 503 vs ms-agent-dotnet 200 for the same turn) — a source of
dashboard flapping.
- Adds the canonical mirror fixtures (outer `generate_a2ui` + matching
inner `_design_a2ui_surface`) to
`showcase/aimock/d6/pydantic-ai/gen-ui-declarative.json`. These are
deterministic canonical mirrors matching the langgraph-python convention
— **not** a non-deterministic real-LLM recording — preserving the
mandatory LGP 1:1 parity.
## Red-green proof
- **RED:** exact failing request (`POST /v1/responses`, gpt-4.1, "Show
me my sales dashboard for this quarter.", tools=[`generate_a2ui`],
header `x-aimock-context: pydantic-ai`, strict) against the pre-fix
fixture set → **HTTP 503 `no_fixture_match`** (reproduces staging
exactly; confirmed live on staging too).
- **GREEN:** same request against the new set → **HTTP 200** SSE
emitting the `generate_a2ui` tool call; the inner `_design_a2ui_surface`
turn also returns 200 with the dashboard surface.
- Independently re-verified. `validate-on-load` clean (no fixture
shadowing); existing pydantic-ai D6 turns (KPI/pie/bar/status) still
match identically — no regression.
## Notes
- No credentials in the committed fixture — the OpenAI key was never
even resolved (canonical mirror, not a recording). Credential scan of
the diff + full blob: zero matches.
## Test plan
- [ ] CI green
- [ ] After deploy, confirm the `generate_a2ui` declarative D6 cell
flips red→green on staging
## Summary
Makes the showcase harness probe's turn-done signal **reliable**,
killing the dominant class of dashboard false-red flaps without ever
hiding a real failure.
`waitForTurnComplete` previously relied on a fragile SSE fetch-counter
conjunct that false-reds healthy demos whenever the page-side fetch
wrapper missed the runtime URL/transport. This change makes the
**`data-copilot-running` DOM attribute** (driven directly by the agent
run lifecycle, `RUN_STARTED`→true / `RUN_FINISHED`→false,
transport-independent) the **PRIMARY** done-signal, with the SSE counter
demoted to a **headless-only fallback** (headless demos never render
`CopilotChatView`, so the attribute is absent).
Design (all three preserved — no false-green, no false-red, hangs still
red):
- **Primary signal** = the `data-copilot-running` true→false
**transition** with a **stayed-stopped quiescence window** (a stop must
persist on the same run-start count for `settleMs`; a new sub-run resets
it) — so it cannot complete on an intermediate stop in a multi-step
turn.
- **SSE counter** = headless fallback only; never an OR-trigger when the
DOM signal is present.
- **`done-signal-missing` backstop** (gated on `attrPresent===true` +
`runningNow!==true`) reds a genuine painted-but-never-finished DOM turn
before the hard timeout; headless turns use their full timeout for their
only signal.
## How it was reviewed
A full 4-round `cr-loop` (7 unbiased agents/round + confirmation rounds
+ a Procedure-3 promotion audit) caught and fixed **5 distinct
correctness defects** in the implementation before merge:
- **F1** — SSE OR-trigger could complete a multi-step turn early on an
intermediate stop (false-GREEN), in both the loop and the post-loop
classifier.
- **F2** — the run-start baseline was captured *after* the message send,
killing the primary signal on fast turns (false-RED).
- **F3** — non-atomic double `surfaceReady` read per poll (latent hazard
+ wasted round-trip).
- **F4** — the surface-mount (`completeOnMount`) path had no quiescence
window (false-GREEN on intermediate stop + false-RED on a still-running
gen-UI turn).
- **F5** — the early backstop false-redded slow-but-healthy **headless**
turns (now gated on the DOM signal).
Bidirectional red-green tests for F1–F5 plus a systematic `{DOM,
headless} × {completes, lagging-recovers, genuine-hang} × {text,
surface}` completion/backstop matrix. Full harness unit suite: **3173
passed / 18 skipped / 0 failed**; `tsc --noEmit` clean; lint 0 errors;
build clean.
## Known follow-ups (NOT in this PR — pre-existing / non-blocking)
- **Theoretical edge (not reachable on real or realistically-streamed
turns):** if a run completed within a single synchronous microtask
(zero-duration), the page-side MutationObserver could miss the true edge
while `attrPresent===true` → false-red. Real LLM turns and aimock
realistic-streaming hold the attribute true across many event-loop
ticks, so the observer reliably latches it. A naive "re-add SSE fallback
for DOM-present" fix would reintroduce F1's multi-step false-green, so
it's intentionally not done here.
- **Recommended quick follow-up (latency only, no wrong verdict):**
capture `baselineBannerText` pre-`sendTurnMessage` (mirroring the
run-start/count baselines) so a fast-erroring cold-start turn fast-fails
(#5142) instead of burning the full timeout.
- **Pre-existing sse-interceptor capture/counter internals** (none
load-bearing for the new done-signal; verified STAY_IN_C by the
Procedure-3 audit): page-side counter soft-nav/multi-capture reset,
`__hk_fetchWrapped` pattern reuse + hardcoded fallback, g/y-flag
stateful RegExp, TextDecoder end-of-stream flush, bare-catch
reader-error swallow, framenav payload discard/TOCTOU,
CDP-wallTime-vs-Date.now TTFT, addInitScript/close-listener
re-registration accumulation.
## Test plan
- [x] `pnpm test` (harness) — 3173 passed / 18 skipped / 0 failed
- [x] `tsc --noEmit` exit 0, lint 0 errors, build exit 0
- [ ] Verify on staging that auth / prebuilt-sidebar / claude-sdk-tools
(and other previously-flapping cells) stop false-redding while
genuinely-broken cells stay red
Please review the replay/primary-signal approach. Not auto-merging.
## Summary
Hardening + pre-existing-debt cleanup of the showcase promote-notify
Slack renderer (follow-up to #5657, scoped to deferred review items).
**Fail loud on a dropped page-the-humans alert.** The #oss-alerts
failure cross-post used to pipe the Slack API response to `/dev/null`,
so a `200`/`{"ok":false,"error":"channel_not_found"}` silently dropped
the alert that fires when a promote *failed* — on a green job nobody
would notice. Both posts now capture the response via a shared
`slack_alert_posted_ok` predicate; the #oss-alerts page fails the
renderer job loud on a dropped delivery, while the informational thread
reply stays warn-only. The predicate is mirrored byte-identically across
the live workflow and the dry-run helper.
**Debt cleanup.** Removed a dead `failed_count` var, corrected a
misleading `gha_url` comment, and added `^[0-9a-f]{6}$` validation on
the decoded blob `run_id` in the render step so a malformed run_id can't
reach Slack or the run name.
## Test plan
- [x] `promote-notify.bats` 11/11 — predicate edge cases (non-JSON,
malformed, missing/`null` ok), anti-drift parity guard (predicate
identical in both files), and call-site tests locking the #oss-alerts
fail-loud vs thread warn-only exit semantics
- [x] red→green proven: re-adding `|| true` to the #oss-alerts call-site
flips the call-site test red
- [x] shellcheck clean on the dry-run helper
Brings google-adk to 39/39 D6 (reproduced across two independent full-matrix
runs, zero regressions). Four changes:
- entrypoint.sh: remove ADK_DISABLE_PROGRESSIVE_SSE_STREAMING=1. That flag's
non-progressive aggregation path ended ADK's agentic loop after the first
tool round (no post-tool LLM re-invoke), which broke every demo needing a
second turn: the subagents chain (research -> writing -> critique),
tool-rendering-reasoning-chain (AAPL -> MSFT), shared-state-read-write's
confirmation, and the custom-catchall narration. The partial-event abort it
guarded against is already handled in-callback by stop_on_terminal_text.
- manifest.yaml: un-skip-list tool-rendering-reasoning-chain (now passes with
the loop restored).
- headless_complete_agent.py: add AGUIToolset() so the frontend highlight_note
tool is injected and routed to the browser. Removing the flag unmasked this
pre-existing gap — turn 3 dispatched highlight_note server-side and the
backend registry rejected it. langgraph-python auto-injects frontend tools;
ADK needs AGUIToolset() in the agent's tools list.
- aimock/d6/google-adk/gen-ui-interrupt.json: order each pill's narration leg
(toolCallId) before its emit leg and drop the thread-global hasToolResult
gate, so the alice pill no longer 503s after the sales pill leaves a tool
result in the thread.
Flatten AG-UI attachment content into native pydantic-ai content types in
the OUTGOING request only, via a WrapperModel-scoped flatten rather than a
history_processor (so the flatten never persists into ctx.state.message_history
and leak into UI state). Normalize mime types, gate on supported content types,
and degrade unsupported types at the single emission choke point. Fixes the
_map_user_prompt assert_never crash on raw AG-UI multimodal content.
Fail loud on a dropped #oss-alerts page: the failure-alert cross-post no longer
swallows a 200/ok:false Slack response, so a dropped page-the-humans alert reds
the renderer job instead of vanishing on a green run. The thread reply stays
warn-only. Both posts capture the response via a shared slack_alert_posted_ok
predicate, mirrored byte-identically across the live workflow and the dry-run
helper.
Debt cleanup: drop a dead failed_count var, correct a misleading gha_url comment,
and validate the decoded blob run_id against ^[0-9a-f]{6}$ in the render step so
a malformed run_id can't reach Slack or the run name.
Tests: predicate edge cases (non-JSON, malformed, missing/null ok), an anti-drift
parity guard asserting the predicate is identical in both files, and call-site
tests locking the #oss-alerts fail-loud vs thread warn-only exit semantics.
- @ag-ui/aws-strands 0.2.2 -> 0.2.3 (strands-typescript)
- ag_ui_strands 0.2.1 -> 0.2.2 (strands)
These releases carry the A2UI-dynamic (declarative-gen-ui) run-completion fix:
the auto-injected generate_a2ui now completes after the A2UI surface paints,
so the run emits RUN_FINISHED instead of hanging 'Running'. Should green the
gen-ui-declarative D6 cell on both integrations (-> 35/35) and resolve the
real-LLM staging hang. Pending local D6 re-verify.
Fixes#5635.
## What
Headers set directly on an `HttpAgent` registered via
`agents__unsafe_dev_only` were silently replaced by the provider
headers. Per-agent auth headers (like an `Authorization` for a
self-hosted backend) got dropped, causing 401s.
## Why
`AgentRegistry.applyHeadersToAgent` did `agent.headers = {
...core.headers }`, a full overwrite. The run handler and the react-core
`useAgent` hook did the same. So an agent built with its own headers
lost them on registration, on every `setHeaders`, and before each
request.
## Fix
Merge instead of replace. The registry captures each agent's own headers
once (in a WeakMap, before the first apply) and rebuilds `{
...ownHeaders, ...coreHeaders }`. Core wins on key conflicts, which
keeps the existing "provider headers are authoritative" and logout/clear
behavior. All header application now routes through one method,
`CopilotKitCore.applyHeadersToAgent`, so runs never clobber per-agent
headers.
Vue and Angular benefit too: they dispatch runs through `core.runAgent`
/ `connectAgent`, so the merge is re-applied before every request.
## Tests
- core: 3 new cases in `core-headers.test.ts` (preserve, merge,
retain-across-setHeaders); existing overwrite and clear tests still
pass.
- react-core: new `use-agent-provider-headers.e2e.test.tsx` with a real
provider and an HttpAgent that has its own headers.
Verified locally: format, lint, full core + react-core suites, and both
builds.
## Summary
Adds `unarchiveThread(id)` to the v2 thread store (`@copilotkit/core`)
and the `useThreads` hook (`@copilotkit/react-core/v2`), restoring an
archived thread via the existing generic `PATCH /threads/:id { archived:
false }` update path — no new runtime endpoint. Mirrors `archiveThread`
across the store and hook.
This is the durable, architecture-independent piece extracted from the
threads-drawer effort. The drawer UI itself is being restarted as a
framework-agnostic **CopilotDrawer** (Lit web component + React/Angular
wrappers) under a separate spec; this hook method stands on its own and
is needed regardless.
## Testing
TDD. New core store test (`PATCH … { archived: false }`) and
`useThreads` hook test; full suites green (core, react-core).
## What
Brings the google-adk showcase **A2UI** demos to D6 parity with the
langgraph-python / AWS Strands gold standard.
- **Auto-inject**: `declarative-gen-ui` switches from backend-owned
(`get_a2ui_tool`, `injectA2UITool: false`) to runtime auto-injection
(`injectA2UITool: true`, plain agent). The `ag-ui-adk` 0.7.0 adapter
auto-injects `generate_a2ui` via `plan_a2ui_injection`, matching how
langgraph-python and Strands wire the same demo. First time ADK's
adapter auto-injection is exercised in the showcase.
- **Recovery un-red**: removes the "known-failing on purpose" mark from
the `a2ui-recovery` heal e2e and the OSS-374 inner-render-disambiguation
notes (spec, fixture `_meta`, QA doc, agent docstring, route comment).
That premise was stale: on `ag-ui-adk` 0.7.0 the adapter forwards the
run conversation into the inner `render_a2ui` call, so each pill's last
user turn is its own prompt and aimock selects the right per-pill
fixture. Confirmed against the aimock journal.
`a2ui-recovery` stays backend-owned (the only path that surfaces the
recovery loop) and remains ADK-only (OSS-375 tracks langgraph-python
parity).
## Verification
Canonical D6 driver (`showcase test google-adk --d6 --isolate`), per
pill:
- `declarative-gen-ui`: all 4 pills pass under auto-inject
(sales-dashboard metric>=4 + pie + bar; team-performance data-table +
bar; at-risk status-badge>=3 + metric>=3; top-account info-row + pie)
- `a2ui-fixed-schema`: pass
- `beautiful-chat` (5 sub-pills): pass
- `a2ui-recovery` Playwright spec: 3/3 (page-load, heal, exhaust)
- aimock journal confirms `generate_a2ui` was adapter-injected (the
agent declares no tools) then drove the inner `render_a2ui`.
## Scope and honest status
This is **A2UI D6 parity**, not full-slug D6. The google-adk D6
aggregate is still red (33/39): six **non-A2UI** cells fail and are
untouched by this PR: `shared-state-write`,
`tool-rendering-custom-catchall`, `subagents`,
`tool-rendering-reasoning-chain`, `gen-ui-interrupt`,
`gen-ui-headless-complete`. They are pre-existing (D6 is not CI-gated
for google-adk) and out of scope here.
Takes d6:strands and d6:strands-typescript from 32/35 to 34/35.
- shared-state-read: the turn-2 fixture leg wrongly pinned turnIndex:0, so the
aimock matcher skipped it on turn 2 -> 404 -> turn-2 sse-missing. Drop
turnIndex to mirror the langgraph-python gold-standard fixture.
- multimodal: sample.png/pdf/wav shipped as git-LFS pointers, so deploy/test
environments without 'git lfs pull' served the ~130-byte pointer text as the
upload -> the run never started (runsFinished=0). Ship them as regular
binaries via a per-integration .gitattributes lfs-unset + real bytes,
mirroring langgraph-python's convention.
Remaining red (gen-ui-declarative) is a Strands A2UI-dynamic run-completion bug
(reproduces on real-LLM staging too): the surface paints but generate_a2ui
never completes, so the run hangs 'Running'. Tracked separately.
Restores an archived thread via the existing generic PATCH /threads/:id
update path with { archived: false } — the same mechanism example apps
already use for restore — so no new runtime route is required. Mirrors
archiveThread across the core thread store and the v2 useThreads hook.
## What
A catalog passed to the React provider is now enough to use A2UI end to
end:
```tsx
<CopilotKit a2ui={{ catalog }}>
```
Previously you also had to set `a2ui: { injectA2UITool: true }` on the
runtime. Now that second step is gone: ask the agent to render A2UI and
it works, with no other A2UI-related code anywhere.
## How
- **Provider**
([CopilotKitProvider.tsx](packages/react-core/src/v2/providers/CopilotKitProvider.tsx)):
A catalog flips A2UI active locally (renders surfaces + mounts the
catalog context regardless of the runtime flag), and forwards an
`a2uiCatalogAvailable` signal per run via `properties`.
- **Runtime**
([handle-run.ts](packages/runtime/src/v2/runtime/handlers/handle-run.ts)):
parses the run body before configuring middleware (the request is
single-read), derives `providerA2UIHasCatalog` from
`forwardedProps.a2uiCatalogAvailable`, and passes it on.
- **Decision spot**
([agent-utils.ts](packages/runtime/src/v2/runtime/handlers/shared/agent-utils.ts)):
A2UI is enabled when the runtime enables it OR a catalog was forwarded,
and `injectA2UITool` defaults to `true` when a catalog is present.
The downstream backend chain (inject flag -> agent tool awareness) is
unchanged.
## Opt-outs respected
This only fills a default, it never overrides a deeper decision:
- explicit `a2ui: { injectA2UITool: false }` stays `false` (via `??`)
- explicit `a2ui: { enabled: false }` stays disabled (short-circuit)
## Tests
TDD at the runtime layer plus a provider forwarding test:
- catalog forwarded, no `a2ui` config -> middleware applied with
`injectA2UITool: true`
- catalog forwarded + `injectA2UITool: false` -> respected
- catalog forwarded + `enabled: false` -> not applied
- catalog forwarded + `enabled: true` (no flag) -> defaults to `true`
- no catalog, no `a2ui` -> not applied (unchanged)
- provider forwards `a2uiCatalogAvailable` only when a catalog is
present, preserving user properties
Verified: runtime 1538/1538, react-core provider + a2ui suites green,
typecheck clean, build green, oxfmt clean.
Switch declarative-gen-ui to runtime auto-injection (injectA2UITool: true, plain
agent), matching the langgraph-python / AWS Strands gold standard. The ag-ui-adk
0.7.0 adapter auto-injects generate_a2ui via plan_a2ui_injection. Verified under
the canonical D6 driver: all four declarative pills pass (sales-dashboard,
team-performance, at-risk, top-account), plus a2ui-fixed-schema and
beautiful-chat.
Remove the stale known-failing mark from the a2ui-recovery heal e2e and the
OSS-374 inner-render-disambiguation notes (fixture _meta, QA doc, agent
docstring, route comment). Verified against the aimock journal: ag-ui-adk 0.7.0
forwards the run conversation into the inner render_a2ui call, so each pill
matches its own fixture by userMessage and both heal and exhaust pass. The
recovery demo stays backend-owned (the only path with the recovery loop) and
ADK-only (OSS-375 tracks langgraph-python parity).
addReaction/removeReaction resolved the channel from messageRef.channelId only,
unlike Slack and Telegram which fall back to the target channel. The shipped
example reacts with `{ id }` (no channelId) — the channel is meant to come from
the thread's reply target — so on Discord fetchSendable("") threw, the call
returned { ok: false }, and the 👀/✅/⚠️ acks silently never fired (contradicting
the example's "works on Slack, Discord, and Telegram alike" claim).
Fall back to the target channel when the reacted ref carries no channelId, for
parity with the other two adapters. Adds a regression test.
## What
Surfaces the OSS-158 A2UI work — now published as **`ag-ui-adk 0.7.0`**
— in the `google-adk` showcase, and adds an explicit **ADK-only
`a2ui-recovery` demo**.
Before this PR, the google-adk A2UI demos ran on a **hand-rolled
`google.genai` planner** (pinned `ag-ui-adk==0.6.3`, predating all the
A2UI work), so the middleware's recovery loop / healing / hard-fail were
never exercised. This re-wires them onto the published middleware
(`get_a2ui_tool`) and makes the recovery behavior visible.
## Key decisions
- **Backend-owned wiring (`injectA2UITool: false`)** for
`declarative-gen-ui` + `beautiful-chat`, replacing the hand-rolled
planner with the middleware's `get_a2ui_tool()` (forced `render_a2ui`
sub-agent + toolkit validate→retry recovery +
`render_as_llm_instructions`/`parse_and_fix` healing +
`a2ui_recovery_exhausted` hard-fail). This matches the **AWS Strands /
ag2 external-framework convention**, not langgraph-python's
runtime-driven `injectA2UITool: true`.
- The explicit `false` is **load-bearing**: the planner now lives in the
ADK middleware, so letting the runtime also inject would double-bind the
tool slot. It's also required post-**#5611** (a provider catalog
otherwise defaults `injectA2UITool` to `true`).
- **`a2ui-recovery` is ADK-only by design.** The validate→retry loop +
hard-fail envelope live in the ADK middleware; the runtime path
langgraph-python uses (`@ag-ui/a2ui-middleware`) is single-pass
parse-and-salvage with **no** recovery loop, so there's no LP parity
reference for this demo (exempt from LP e2e-parity). It reuses the
declarative-gen-ui catalog — no new components.
## Changes (commits)
1. `chore`: bump `ag-ui-adk==0.7.0`
2. `feat`: re-wire `declarative-gen-ui` + `beautiful-chat` onto the
backend-owned middleware path
3. `chore`: remove the dead hand-rolled planner + orphaned
`SalesPipelineAgent` from `main.py` (retire `test_generate_a2ui.py`; fix
the stale manifest highlight)
4. `feat`: add the ADK-only `a2ui-recovery` demo (heal + exhaust pills;
agent/route/page/manifest/qa/aimock-fixture +
`feature-registry`/`constraints` entries)
5. `test`: add `a2ui-recovery.spec.ts` (mirrors the AG-UI dojo recovery
spec)
Depends on `ag-ui-adk 0.7.0` (published to PyPI via ag-ui#2017).
## Verification (in-sandbox, all green)
- **Backend**: import-smoke (41 agents construct); OSS-158 assurance
gate through the exact wiring — sub-agent emit, recovery (invalid→valid,
2 attempts), hard-fail (`a2ui_recovery_exhausted`, capped at 3); a live
trial against real Gemini emitted a real `a2ui_operations` envelope.
- **Python suite**: 62 passed.
- **Static validators**: `generate-registry` ✅, `validate-constraints`
(google-adk OK) ✅, `validate-parity` 20/20 pass ✅, `aimock-fixtures` 818
passed ✅, `validate-fixture-tool-surface` no drift ✅.
## E2E coverage & parity
`a2ui-recovery` is **ADK-only** — the validate→retry recovery loop +
hard-fail envelope live in the `ag_ui_adk` middleware, and
langgraph-python's runtime A2UI path (`@ag-ui/a2ui-middleware`,
single-pass parse-and-salvage) has no equivalent. **There is therefore
no langgraph-python reference for this demo, and the d6 `e2e-parity`
harness skips it by design** (parity-exempt, like `voice` / the `byoc-*`
variants). Its automated coverage is the per-integration Playwright spec
`tests/e2e/a2ui-recovery.spec.ts`, run via `pnpm test:e2e` against the
local stack (aimock + `pnpm dev` with `GOOGLE_GEMINI_BASE_URL` →
aimock).
## ⚠️ Known-failing e2e test, kept on purpose (showcase-aimock
deficiency)
Running `pnpm test:e2e tests/e2e/a2ui-recovery.spec.ts` against the
local stack gives **2 pass / 1 fail**: page-load ✅, **exhaust/hard-fail
✅**, **heal ❌**. The heal failure is **left in deliberately** as a live
demonstration for the Showcase team — it does **not** red CI (these
per-integration specs aren't run for google-adk in CI; we only see it
via a manual run).
**Diagnosis (from the agent logs):** the showcase aimock **cannot
disambiguate the two pills' _inner_ `render_a2ui` sub-agent calls.** The
backend-owned middleware (`get_a2ui_tool`) issues the inner render with
a *generic* render prompt + shared suggestion context, so the "last user
turn" aimock keys on is **not** the pill prompt. Both pills therefore
match the **same** inner fixture (the EXHAUST one, by first-match order)
→ the heal pill **exhausts** (`a2ui_recovery_exhausted`, "Couldn't
generate the UI", 0 `declarative-metric`) instead of healing. The agent
log shows the heal pill running the full 3-attempt loop on the exhaust
payload (`unresolved_child 'never-defined'`).
This is an **aimock harness limitation, not a middleware/demo bug** —
the middleware heals free-form args correctly in the OSS-158 toolkit
gate (in-sandbox) and against real Gemini; the exhaust path proves the
recovery loop + hard-fail envelope + aimock context routing all work
end-to-end in the browser.
Two follow-ups (Linear):
- **OSS-374 — Showcase-aimock inner-subagent disambiguation** — the
harness needs a way to key an inner sub-agent call per pill (e.g. on
`surfaceId`/`intent`, or a per-pill agent/context). Likely **not**
ADK-specific: any backend-owned A2UI integration (Strands/ag2) that
wants per-pill inner renders hits the same wall.
- **OSS-375 — Recovery-demo langgraph-python parity** — `a2ui-recovery`
is ADK-only (LP's runtime A2UI path has no recovery loop); tracked so
the parity exemption is explicit and revisited as other frameworks gain
an equivalent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What
Upgrade every `@copilotkit/*` dependency across the showcase from
`1.60.2` (plus stray `latest` override pins) to an exact `1.61.1` pin,
and regenerate the standalone npm lockfiles to match.
## Changes
- **22 `package.json`**: 20 integrations +
`langgraph-typescript/src/agent` + `shell`. All `@copilotkit/*` deps
pinned to exact `1.61.1` (`core`, `react-core`, `react-ui`, `runtime`,
`sdk-js`, `shared`, `voice`, `a2ui-renderer`, plus the
`web-inspector>core` overrides). The two `latest` override pins are now
exact `1.61.1` to honor the repo's exact-pin discipline.
- **22 `package-lock.json`**: regenerated with `npm install
--package-lock-only --legacy-peer-deps` (matches the `npm ci
--legacy-peer-deps` the Dockerfiles use). Transitive
`@copilotkit/runtime-client-gql` -> `1.61.1` and
`@copilotkit/license-verifier` -> `~0.5.0` came along.
- **`showcase/scripts/showcase-canonical-pins.json`**:
`canonicalCopilotKitVersion` `1.60.2` -> `1.61.1` (the pin SSOT consumed
by `validate-pins.ts`).
## Out of scope (intentionally unchanged)
- `@copilotkit/aimock` stays on its own version line (`1.26.1`).
- Python `copilotkit` SDK was already `0.1.94` in every
`requirements.txt`; no pyproject refs. No-op.
- Root `pnpm-lock.yaml` untouched — the integrations and shells are not
pnpm-workspace members (they ship standalone npm lockfiles).
## Validation
- `validate-pins`: ratchet unchanged (`FAIL=38`, identical hash) — zero
`@copilotkit` drift; the 38 are pre-existing non-copilotkit framework
range-pins already in `fail-baseline.json`.
- `validate-parity`: 20/20 pass.
- `validate-fixture-tool-surface`: no drift.
- `showcase/scripts` vitest: 2102 passed, 5 skipped.
Bump every @copilotkit/* dependency across the showcase integrations and
the shell from 1.60.2 (and stray "latest" override pins) to an exact
1.61.1 pin, and move the canonical pin source of truth to match.
Regenerate each standalone npm package-lock.json with the same
--legacy-peer-deps flag the Dockerfiles use for "npm ci".
- showcase/integrations/*/package.json + package-lock.json
- showcase/integrations/langgraph-typescript/src/agent/*
- showcase/shell/package.json + package-lock.json
- showcase/scripts/showcase-canonical-pins.json: canonical 1.60.2 to 1.61.1
aimock stays on its own version line (1.26.1). The Python copilotkit SDK
was already 0.1.94 across every requirements.txt, so no change there.
validate-pins ratchet is unchanged (FAIL=38, identical hash);
validate-parity, validate-fixture-tool-surface, and the showcase/scripts
vitest suite (2102 tests) all pass.
Replace the "Tracked in Linear" placeholders with the filed issue IDs:
OSS-374 (showcase-aimock inner-subagent disambiguation) and OSS-375
(recovery-demo langgraph-python parity).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `heal` e2e test fails against the live showcase aimock because the harness
cannot disambiguate the two pills' inner render_a2ui sub-agent calls (the
backend-owned middleware issues the inner call with a generic render prompt +
shared suggestion context, so the "last user turn" aimock keys on is not the
pill prompt). Both pills match the same inner fixture (the EXHAUST one, by
first-match order), so the heal pill exhausts instead of healing.
This is an aimock harness limitation, not a middleware/demo bug: the middleware
heals free-form args correctly in the OSS-158 toolkit gate and against real
Gemini. Keep the test running (not skipped) as a deliberate demonstration for
the Showcase team; it does not red CI (these specs aren't run for google-adk in
CI). Update the spec header, the heal-test note, the fixture _note, the qa doc,
and the recovery_agent docstring to reflect this accurately (and drop the stale
sequenceIndex description). Tracked in Linear: aimock inner-subagent
disambiguation + recovery-demo LP parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The heal fixture relied on per-attempt aimock response switching (sequenceIndex
0=invalid -> 1=valid), but sequenceIndex doesn't advance across the recovery
loop's intra-turn render_a2ui calls in the per-integration Playwright context
(no X-Test-Id), so aimock served invalid on every attempt and the demo never
healed (0 metrics).
Switch the heal pill to a single deterministic response: the inner render_a2ui
returns FREE-FORM args (components/data as JSON strings) that the middleware
heals via parse_and_fix into a valid surface in one pass — no per-attempt
switching. Verified in-sandbox: free-form args -> a2ui_operations with 2 Metric
components. The exhaust pill is unchanged (structurally invalid every attempt ->
a2ui_recovery_exhausted hard-fail, already verified e2e).
Updates the agent docstring, qa checklist, and spec test name/comment to reflect
heal = parse_and_fix healing (not invalid->valid retry). aimock-fixtures schema
test: 818 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clickPill helper (copied from declarative-gen-ui.spec.ts) matched the user
message with the stale '[data-message-role="user"]' selector, which no longer
exists in @copilotkit/react-core/v2 >= 1.60 — the user bubble is now
'[data-testid="copilot-user-message"]' (CopilotChatUserMessage). The message
dispatched fine; the helper just never found the bubble, timing out before the
heal/exhaust assertions ran. The sibling google-adk specs carry the same stale
selector (they don't run in CI for this integration, so it drifted unnoticed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Adds first-class support for the **AG-UI standard interrupt protocol**
to `useInterrupt`, while keeping the legacy `on_interrupt` custom-event
path working (additive, no breaking changes).
The AG-UI spec
([concepts/interrupts](https://docs.ag-ui.com/concepts/interrupts))
pauses a run via `RUN_FINISHED` with `outcome: { type: "interrupt",
interrupts: Interrupt[] }` and resumes via a `resume: ResumeEntry[]`
array. CopilotKit previously only spoke the non-standard `on_interrupt`
+ `forwardedProps.command.resume` dialect; the standard resume path was
literally unreachable.
## What changed
- **core** — `CopilotKitCore.runAgent` forwards a standard `resume:
ResumeEntry[]` to `agent.runAgent` (no-op when omitted), making the spec
resume path reachable.
- **react-core `useInterrupt`** — dual-path detection (standard
`RUN_FINISHED outcome:interrupt` + `pendingInterrupts`, alongside legacy
`on_interrupt`); surfaces `interrupt` (primary) + `interrupts` (full
open set); `resolve(payload?, interruptId?)` / `cancel(interruptId?)`
that accumulate per-interrupt responses and auto-submit a spec `resume`
array once all open interrupts are addressed; expiry handling via
`isInterruptExpired`; re-exports `Interrupt` / `ResumeEntry` /
`ResumeStatus`.
- **vue** — full parity, incl. `CopilotChatMessageView.vue` slot
bindings.
- **react-native** — re-exports the standard interrupt types.
- **docs** — 4 shell-docs pages (`human-in-the-loop/useInterrupt`,
`headless`, and both reference pages).
`responseSchema` is surfaced on `interrupt` but not validated
client-side (the agent stays authoritative, per spec).
## Notable fix
vue legacy `resolve` now defers clearing the interrupt card to
`onRunStartedEvent` — matching react-core **and** vue's own standard
path. Previously it cleared synchronously, reintroducing a UX regression
react-core had a dedicated test/comment guarding against.
## Verification
- Tests green: react-core **1285**, vue **19**, core resume **2**
- `nx run-many -t build` green across core / react-core / react-native /
vue
- oxlint/oxfmt clean across all changed files
- Independent integration review **APPROVED**; multi-agent code review
converged (only doc + a vue-parity issue, all fixed; a
multi-interrupt-without-id safety warning was added)
## Deferred (separate follow-up PR — pre-existing, out-of-subject)
Surfaced incidentally during review, untouched by this PR:
- `run-handler.ts` parallel tool-call results spliced in reverse order
(`:623`/`:763`)
- wildcard tool handler not passed the abort `signal`
- `runTool` follow-up runs even after abort (missing the
`processAgentResult` abort guard)
- `isArgumentError` computed but never read (dead field)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adding a2ui-recovery to the shared feature-registry cross-joins it across all
20 integrations (wired for google-adk, unshipped elsewhere), so the catalog
generator's hardcoded counts move: cross-join 920->940, metadata.total_cells
900->920, LGP 46->47 (unshipped 7->8). docs_only stays 20; the relative
sum-checks are unchanged. Fixes the Validate Showcase failure on #5662.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Bug hunt on the **strands-typescript** showcase integration, comparing
every flagged demo against the reference **strands (Python)**
integration on real OpenAI (gpt-4o). Every showcase-side fix was applied
to **both** integrations for 1:1 parity. Each flagged issue was
classified as showcase-side (fixed here), upstream `@ag-ui/aws-strands`
adapter (chipped, not fixed here), or shared-with-Python.
## Fixed here (showcase) — verified working on real LLM, both
integrations
- **readonly-state-agent-context** — agent ignored `useAgentContext`
("Who am I?" hallucinated). Root cause: neither adapter surfaces
`RunAgentInput.context` to the model. Fix: lift `context` into the
prompt in `buildStatePrompt` / `build_state_prompt`. (Same context
channel also feeds open-gen-ui — see below.)
- **open-gen-ui** + **open-gen-ui-advanced** — model answered in plain
text instead of calling `generateSandboxedUi`. The
`openGenerativeUI.designSkill` is delivered via the same dropped
`context` channel; with context now lifted into the prompt, an added
imperative makes the model call the tool. Advanced cell also clarifies
sandbox functions are iframe→host bridges, not LLM tools.
- **hitl-in-chat** — "Schedule a 1:1 with Alice" sometimes produced no
time-picker (the shared backend `schedule_meeting` tool won over the
frontend `book_call` HITL). Fix: sharpen `book_call` to own scheduling
intents.
- **tool-rendering catch-all** ("Roll a d20" / "Chain tools") — no dice
tool existed. Fix: add a `roll_dice` tool (shared python tools + TS).
## Also
- **Register strands-typescript as a dashboard baseline partner**
(`BASELINE_PARTNERS`) so it gets its own coverage column like
langgraph-typescript. D5 (e2e-deep) is **green** for strands-typescript.
## Deferred / out of scope (by decision)
- **a2ui-fixed-schema** and **declarative-gen-ui** — both depend on A2UI
(`generate_a2ui`) which strands-typescript intentionally does not ship
yet. Pre-existing; left intact to handle later. (My exploratory route
opt-in was reverted.)
- **multimodal** attachments fail with a git-LFS pointer error — a
shared deploy-environment issue (same on Python), not a demo bug; base
chat works.
## Upstream (ag-ui `@ag-ui/aws-strands` TS adapter) — chipped, not fixed
here
These are genuine TS-vs-Python adapter divergences; Python's adapter
handles both. Tracked as task chips to land upstream first, then
incorporate:
- **U2 — empty tool-result content** (`agent.ts:178`): the adapter sends
`{text:""}` for empty render-tool results → OpenAI 400
(`STRANDS_ERROR`). Breaks beautiful-chat + chart demos (renders, then
the run dies and poisons the thread). Python coerces to a non-empty
placeholder.
- **U1 — INCOMPLETE_STREAM on parallel tool calls** (`agent.ts`
~1730-1756 / ~1864-1921): emits `RUN_FINISHED` without draining
`TOOL_CALL_END` for parallel calls; rejected by the AG-UI client verify
layer. Trips the catch-all "Chain tools" pill.
## Verification
- All fixes confirmed via real-LLM browser testing on both
strands-typescript (:3119) and strands-python (:3112) production
containers.
- `oxlint` 0 errors, `oxfmt` clean. D5 strands-typescript green. Both
containers rebuilt + booted clean.
- Note: shell-dashboard `vitest` could not run in the worktree
(`@vitejs/plugin-react` transitive dev-dep absent); the only
test-affecting change is the `BASELINE_PARTNERS` count 26→27, verified
by inspection — CI runs the suite authoritatively.
## Not yet green (separate follow-up)
D6 (e2e-full) for strands-typescript is 25/35; the remaining reds are
pre-existing (newly-merged) demo/fixture gaps and one harness-level
`__name` page.evaluate error — unrelated to this bug hunt.
Playwright spec for the ADK-only a2ui-recovery demo, mirroring the AG-UI dojo
reference (apps/dojo/e2e/tests/adkMiddlewareTests/a2uiRecovery.spec.ts):
assert the STABLE end-states and skip the transient 'Retrying...' label
(threshold-gated/timing-dependent).
- heal pill: recovered surface paints (>=2 declarative-metric tiles), no
hard-failure UI, no render-error banners
- exhaust pill: 'Couldn't generate the UI' shows (A2UIRecoveryStates on the
a2ui_recovery_exhausted envelope), no faulty surface ever paints, chat stays usable
Selectors verified against @copilotkit/react-core/v2 A2UIRecoveryStates (failure
text) and the showcase declarative catalog testids. Clears the validate-parity
'no e2e spec' warning. Runs in the browser-e2e stack (frontend + aimock + agent_server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the Declarative Generative UI (A2UI dynamic schema) demo for both
the strands (Python) and strands-typescript integrations, bringing them to 1:1
parity with the canonical langgraph-python demo.
Backend:
- Add a dedicated dynamic-A2UI agent (a2ui_dynamic.py / buildA2uiDynamicAgent)
that wires no generate_a2ui tool. The runtime route sets
injectA2UITool: true + defaultCatalogId "declarative-gen-ui-catalog"; the
Strands adapter auto-injects generate_a2ui and drives a secondary render_a2ui
planner. StrandsAgentConfig.a2ui supplies the catalog id and a
composition_guide (sales dataset + composition rules) so the planner is
self-contained.
- Mount the agent on the /declarative-gen-ui sub-path and point the route's
HttpAgent at it, mirroring the a2ui-fixed-schema pattern.
- Bump ag_ui_strands 0.1.9 -> 0.2.1 (Python): the A2UI auto-injection config
(default_catalog_id, guidelines, render planner) landed in 0.2.0. Pulls
ag-ui-a2ui-toolkit transitively. strands-typescript already on 0.2.2.
Frontend:
- Port the langgraph-python declarative-gen-ui frontend (suggestions,
sales-context, definitions incl DataTable, renderers, chat) so the demo
matches the shared D5/D6 probe's sales-analyst pills and catalog components.
Fixtures:
- Author gen-ui-declarative D6 fixtures for both slugs from the langgraph-python
template (3 legs per pill: outer generate_a2ui, inner render_a2ui, narration).
Verified on real OpenAI (gpt-4o): both integrations paint a real A2UI surface
(4 Metric tiles + Revenue-by-Region PieChart + Monthly Revenue BarChart +
narration), matching the probe's expected testids and counts.
Note: the gen-ui-declarative D6 cell currently cannot go green because the
shared harness hits "ReferenceError: __name is not defined" in the probe's
page.evaluate (turn 1 preFill), before any agent call. That harness-tooling bug
is owned separately; the fixtures and wiring here are correct and the real-LLM
paint confirms the expected surface.