Pre-existing drift on main: count held at 134, hash shifted (one
FAIL healed, another regressed). Captured the new sorted-FAIL hash
locally with the same algorithm CI uses (sort -u | shasum -a 256)
and updated showcase/scripts/fail-baseline.json to match.
Two bugs introduced in 534cd1efa (D5 integration fixes) when the
langroid adapter switched backend tool results from TEXT_MESSAGE_*
triples to TOOL_CALL_RESULT events:
1. Two leftover `logger.warning("DEBUG ...")` calls in agui_adapter.py
that fired on every chat turn — exactly the noise pattern the
`test_plain_text_turn_does_not_warn` test was written to prevent.
2. Three stale tests in test_agui_adapter.py (only run on 3.11+, so
the failure was 3.12-only):
- test_backend_tool_execution_happy_path: still expected the old
TEXT_MESSAGE_START/CONTENT/END inner triple instead of a single
TOOL_CALL_RESULT event after TOOL_CALL_END.
- test_backend_tool_exception_returns_sanitized_error: still read
the sanitized error JSON from TEXT_MESSAGE_CONTENT.delta — it
now rides on TOOL_CALL_RESULT.content.
- test_plain_text_turn_does_not_warn: caught the DEBUG leak above.
Tests are skipped on Python 3.10 (langroid imports `typing.Self`),
which is why this only shows up on the 3.12 matrix entry.
Beautiful Chat suggestion clicks looped forever because feature-parity.json
tool-calling fixtures lacked an `id` and a paired `toolCallId` followup.
After the agent ran the tool and re-prompted aimock, the same userMessage
substring matched again and the same toolCall was returned indefinitely.
Added explicit ids to 10 broken fixtures (pieChart, barChart, render_*_chart,
scheduleTime, search_flights, toggleTheme) and 11 paired toolCallId
followups returning content summaries — same convention the file already
uses for show_card, weather, etc.
Beautiful Chat layout also showed a black/white split and a broken logo on
the 8 integrations using the full ExampleLayout pattern (crewai-crews,
langgraph-fastapi, langgraph-python, langgraph-typescript, mastra,
ms-agent-dotnet, ms-agent-python, pydantic-ai). Two issues:
1. globals.css hardcoded `body { background: #fafaf9 }` and never defined
the brand tokens (--background, --foreground, --card, --primary, …) that
the layout, mode-toggle, todo card/column, and chart components reference
via Tailwind 4 arbitrary values. ThemeProvider was also adding `dark` to
<html> from system preference, so CopilotKit's chat went dark while body
stayed cream.
2. example-layout/index.tsx renders <img src="/copilotkit-logo.svg" /> but
the file did not exist in any integration's public/.
Added the full token set (light + dark) under :root and :root.dark/.dark,
registered the Tailwind 4 dark variant, switched body to var(--background)
/var(--foreground), and copied copilotkit-logo.svg + copilotkit-logo-mark.svg
into each integration's public/ from examples/integrations/langgraph-python.
Two bugs introduced in 534cd1efa (D5 integration fixes) when the
langroid adapter switched backend tool results from TEXT_MESSAGE_*
triples to TOOL_CALL_RESULT events:
1. Two leftover `logger.warning("DEBUG ...")` calls in agui_adapter.py
that fired on every chat turn — exactly the noise pattern the
`test_plain_text_turn_does_not_warn` test was written to prevent.
2. Three stale tests in test_agui_adapter.py (only run on 3.11+, so
the failure was 3.12-only):
- test_backend_tool_execution_happy_path: still expected the old
TEXT_MESSAGE_START/CONTENT/END inner triple instead of a single
TOOL_CALL_RESULT event after TOOL_CALL_END.
- test_backend_tool_exception_returns_sanitized_error: still read
the sanitized error JSON from TEXT_MESSAGE_CONTENT.delta — it
now rides on TOOL_CALL_RESULT.content.
- test_plain_text_turn_does_not_warn: caught the DEBUG leak above.
Tests are skipped on Python 3.10 (langroid imports `typing.Self`),
which is why this only shows up on the 3.12 matrix entry.
Pre-existing drift on main: count held at 134, hash shifted (one
FAIL healed, another regressed). Captured the new sorted-FAIL hash
locally with the same algorithm CI uses (sort -u | shasum -a 256)
and updated showcase/scripts/fail-baseline.json to match.
Drop gen-ui-interrupt and interrupt-headless demo entries from
manifest.yaml for langroid and claude-sdk-typescript, and delete
the orphan stub page.tsx + README.md files for those routes. Both
features stay listed in not_supported_features so the dashboard
keeps showing the no-entry icon with hover tooltip.
Aligns these two integrations with the convention used by the other
12 integrations that mark these features unsupported (ag2, agno,
built-in-agent, claude-sdk-python, crewai-crews, google-adk,
llamaindex, mastra, pydantic-ai, spring-ai, strands): icon-only
with tooltip rather than a Demo / Code row that links to a stub
page explaining why there is nothing to demo.
Three D5 probe failures fixed:
tool-rendering: get_weather was a backend tool so the workflow emitted
TOOL_CALL_CHUNK but no TOOL_CALL_RESULT, leaving useRenderTool stuck in
loading state forever. Move get_weather to frontend_tools so CopilotKit
manages the tool call lifecycle. Add ToolCallResultWorkflowEvent (which
subclasses ToolCallEndWorkflowEvent to pass the AG_UI_EVENTS isinstance
filter) and override aggregate_tool_calls to emit TOOL_CALL_RESULT for
render-only tools. The WeatherCard component now always renders with
data-testid="weather-card" even during loading, instead of switching to
a separate div that lacks the testid.
gen-ui-headless: the shared agent was missing a show_card frontend tool
stub, so the workflow never emitted TOOL_CALL_CHUNK for it. Add the
stub and register it in frontend_tools.
hitl-steps: human_in_the_loop was routed to the hitl-in-chat specialized
agent (which only has book_call), not the shared agent (which has
generate_task_steps). Move human_in_the_loop to sharedAgentNames so it
routes through the default agent. Introduce render_only_tool_names set
to distinguish render-only tools (get_weather) from interactive tools
(generate_task_steps, book_call, show_card) — only render-only tools
get premature TOOL_CALL_RESULT; interactive tools let CopilotKit manage
the result lifecycle to keep buttons enabled.
The gen-ui-headless D5 probe had two bugs:
1. esbuild's keepNames transform injects __name() wrappers around const
arrow functions. When those functions run inside page.evaluate() in
the browser context, __name is not defined. Replace with string-based
new Function() construction using function declarations.
2. findFirstNonTrivial and readChildCountForSelector used querySelector
which only returns the first match. On headless chat pages, the first
assistant message div is an empty wrapper (children=0), causing the
cascade to skip the selector even when a later match contains the
rendered gen-UI component. Switch to querySelectorAll and iterate all
matches.
## Summary
Fixes a regression introduced by
[f9eee68](https://github.com/CopilotKit/CopilotKit/commit/f9eee688b)
(overlay chat input on scroll area) where late messages and "always"
suggestions slid underneath the absolute-positioned input pill once the
user submitted their first message.
**Root cause:** the `ResizeObserver` `useEffect` in `CopilotChatView`
had `[]` deps. On a fresh chat it mounted with the welcome-screen branch
active — `inputContainerRef.current` was null, the effect bailed, and it
never re-ran when the chat-view branch attached the overlay element.
`inputContainerHeight` stayed at 0, so the scroll content's reserved
bottom padding sat at 32px instead of ~input height.
**Fix:** hold the overlay element in state via a callback ref and key
the effect on the element. Same pattern already used by
`nonAutoScrollRefCallback` elsewhere in this file. The observer now
attaches and detaches reactively as the overlay mounts/unmounts.
## Test plan
- [x] New regression test in `CopilotChatView.inputOverlay.test.tsx`
mounts on the welcome screen, re-renders with messages, and asserts the
observer attaches to the new overlay element and feeds the correct
`paddingBottom` (88 + 32 = 120px). Reverting the fix makes it fail at
the post-transition padding assertion.
- [x] Existing 4 inputOverlay tests still pass.
- [ ] Verify in the demo: load a fresh chat, submit a message, confirm a
long assistant response leaves a gap above the input pill (no content
sliding under the pill).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Spring AI's OpenAiChatModel.internalStream() auto-executes tool calls
through the global ToolCallingManager, which only knows about backend
tools. When the model calls frontend tools (generate_task_steps,
show_card, etc.) injected by the CopilotKit runtime, the
StaticToolCallbackResolver returns null and DefaultToolCallingManager
throws IllegalStateException, crashing the stream.
Two fixes:
1. StreamingToolAgent Phase 1 (streaming): set
internalToolExecutionEnabled=false via OpenAiChatOptions so Spring AI
detects tool_calls in the stream without attempting execution. Phase 2
(.call()) keeps internal execution enabled so Spring AI's built-in
loop handles backend tools.
2. BoundedToolCallingManagerConfig: wrap StaticToolCallbackResolver with
LenientToolCallbackResolver that returns a FrontendToolPlaceholder
for unknown tools instead of letting the resolver return null. This
prevents the IllegalStateException during Phase 2 when the model
calls frontend tools.
Also reorder AG-UI event emission so tool call events are emitted
BEFORE textMessageEnd, which is required for the frontend's
useRenderTool to see them while the message is still open.
Recovers agentic-chat, gen-ui-custom, and gen-ui-headless D5 tests
from 0/11 to 3/11. Remaining failures are pre-existing issues in
other controllers (subagents, shared-state, hitl).
## What does this PR do?
Polish image attachments in chat. Previously they rendered at the full
message
width, taking up most of the screen. After this PR they render as
compact 80x80
thumbnails — closer to how Claude shows attached images.
**Layout & sizing**
- Image attachments render as 80x80 thumbnails with `object-cover`, 12px
rounded corners, and a muted background so transparent PNGs stay
readable.
- Multiple attachments lay out in a horizontal row (`flex-row` +
`flex-wrap` +
`justify-end`) instead of stacking vertically.
- Attachments render *above* the message text instead of below.
**Click-to-zoom**
- Clicking a thumbnail opens it in a fullscreen lightbox with a smooth
view-transition morph (same UX as the attachment queue preview).
- The `Lightbox` + `useLightbox` previously private to
`CopilotChatAttachmentQueue` got extracted into a shared module
(`packages/react-core/src/v2/components/chat/Lightbox.tsx`) so the queue
and
the rendered message attachment share one modal implementation.
**Both renderers updated**
- v2 (Tailwind): `CopilotChatAttachmentRenderer.tsx` +
`CopilotChatUserMessage.tsx`
- legacy (CSS): `packages/react-ui/src/css/messages.css`
## Related PRs and Issues
- N/A
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
Seven D5 features fail because built-in-agent demo pages use different
tool names and hook types than the LangGraph-Python reference agent
that the D5 fixtures were recorded against.
- tool-rendering: switch from useComponent("weather") to
useRenderTool("get_weather") with correct {parameters, result, status}
shape
- gen-ui-tool-based: rename useComponent("haiku") to
useComponent("generate_haiku"), fix HaikuCard to accept args as
direct props instead of {status, result, parameters}
- hitl-in-chat: create /demos/hitl-in-chat route (was missing); reuses
TimePickerCard from hitl-in-chat-booking with book_call tool
- shared-state: add set_notes server tool to baseServerTools so the
fixture's set_notes call has a backend handler
- subagents: rename delegate_to_planner/researcher to
research_agent/writing_agent/critique_agent matching LGP tool names
- subagents page: update useComponent registrations and DelegationCard
for the renamed tools
The hitl-in-app agent was registered with a trailing slash in the URL
(`/hitl-in-app/`), while the FastAPI backend mounts the endpoint at
`/hitl-in-app` (no trailing slash). FastAPI's default redirect_slashes
behavior returns a 307 redirect for POST requests to the trailing-slash
variant, and the AG-UI HttpAgent does not follow POST redirects. This
caused the agent to appear completely unresponsive — the D5
hitl-approve-deny probe timed out at 60s with zero assistant messages.
Use the shared `createAgent()` helper (which does not append a trailing
slash) for consistency with every other agent registration in the file.
Install the pkg.pr.new build of @copilotkit/runtime from PR #4482 to
validate the runFinished fix that stops duplicate TOOL_CALL_END events
in convertTanStackStream after TanStack's RUN_FINISHED.
## Summary
- When a dashboard cell has no probe data for a depth (D5, D6), the
badge now renders the dimension name with CSS `line-through` instead of
appending a `?` label
- Strikethrough communicates "this depth hasn't run" more clearly than
`D5 ?`
- Same treatment applied to the CellDrilldown panel (renders
strikethrough `n/a` instead of `?`)
## Test plan
- [x] cell-drilldown tests updated and passing
- [x] live-status tests passing (formatLabel still returns `?`
internally; rendering layer handles display)
- [x] depth-chip, depth-utils, chips-explainer tests all green
When a dashboard cell has no probe data for a depth (D5, D6), render
the dimension name with CSS line-through instead of appending a "?"
label. Strikethrough communicates "this depth doesn't exist / hasn't
run" more clearly than "D5 ?".
Changes:
- Badge component: when label is "?", render name with line-through
styling and suppress the question mark
- CellDrilldown BadgeRow: render "n/a" with line-through instead of "?"
- Update cell-drilldown test to verify strikethrough rendering
`CopilotChatView` mounts on the welcome-screen branch, where the absolute-
positioned input overlay (and its `inputContainerRef`) does not exist. The
ResizeObserver useEffect ran once with an empty `[]` dep array, found
`ref.current === null`, and bailed. Submitting the first message swapped
to the chat-view branch and attached the overlay element — but the effect
never re-ran, so `inputContainerHeight` stayed at 0 and the scroll
content's reserved bottom padding sat at 32px instead of ~input height.
Late messages and any "always" suggestion strip slid underneath the input
pill, invisible to the user.
Hold the overlay element in state via a callback ref and key the effect
on the element. Same pattern already used by `nonAutoScrollRefCallback`
in this file. Effect now attaches and detaches reactively as the overlay
mounts/unmounts (e.g. clearing messages and falling back to the welcome
screen also resets the measured height instead of holding stale data).
Add a test that mounts on the welcome screen, re-renders with messages,
and asserts the observer attaches to the new overlay element and feeds
the correct paddingBottom. Reverting the fix makes it fail on the
post-transition padding assertion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Apply `FixedAGUIChatWorkflow` to `hitl_in_app_agent.py` and the main
`agent.py` router
- PR #4464 only fixed `hitl_in_chat_agent.py`; the same three upstream
llamaindex bugs affect all agents using `AGUIChatWorkflow`
- Switches both files to `workflow_factory` pattern importing
`FixedAGUIChatWorkflow`
## Why
The D5 probe failures for llamaindex hitl-approve-deny and hitl-steps
features are caused by the same upstream bugs (duplicate tool-call
events, missing parent_message_id, wrong tool-result message role) that
were already fixed for hitl-in-chat.
## Test plan
- [ ] GHCR image builds successfully
- [ ] D5 probes pass for llamaindex hitl-approve-deny and hitl-steps
features
PR #4464 only applied the upstream bug fixes to hitl_in_chat_agent.py.
The same three bugs (duplicate tool-call rendering, missing
parent_message_id, incorrect tool-result message roles) affect the
hitl_in_app_agent and the main agent router.
Switch both to use FixedAGUIChatWorkflow via workflow_factory so all
llamaindex demo features get the corrected AG-UI event stream.
## Summary
Adds comprehensive `console.debug` logging to the D5 probe execution
pipeline so production probe failures are diagnosable from logs alone,
without requiring reproduction.
**conversation-runner.ts** (the core multi-turn conversation engine):
- Logs conversation start/end with turn count and settle configuration
- Logs chat input selector cascade resolution -- each selector miss and
the final hit
- Logs per-turn lifecycle: message send, assistant settle wait, message
count changes, assertion execution, and failures with elapsed times
- Logs the fillAndVerifySend retry loop with attempt counts and user
message baseline vs current counts
- Logs waitForAssistantSettled polling with count changes, periodic
status during long waits (~every 5s), and timeout details
**e2e-deep.ts** (the driver that orchestrates per-feature runs):
- Logs page navigation URL and result
- Logs React hydration wait start, success, and timeout
- Logs conversation turn count after buildTurns
- Logs conversation success/failure with diagnostic capture summary
**All 9 D5 scripts** (agentic-chat, gen-ui-custom, gen-ui-headless,
hitl-approve-deny, hitl-steps, hitl-text-input, mcp-subagents,
shared-state, tool-rendering):
- Logs expected tokens/fragments and actual assistant text at each
assertion checkpoint
- Logs DOM element discovery (matched selectors, child counts, card
structure)
- Logs polling progress for long-running assertions (tool card probe,
chained reply fragments)
All logging uses `console.debug(...)` which is suppressed at INFO level
in production but available when operators set `LOG_LEVEL=debug` for
investigation.
## Test plan
- [x] All 1372 existing tests pass (`cd showcase/harness && npx vitest
run` -- 70 test files, 0 failures)
- [x] Debug output is visible in test stdout (vitest captures it
per-test)
- [x] No functional changes -- all logging is additive `console.debug`
calls
Instrument the conversation runner, e2e-deep driver, and all D5 scripts
with console.debug logging at every decision point so production probe
failures are diagnosable without reproduction.
conversation-runner.ts:
- Log conversation start/end with turn count and settle config
- Log chat input selector cascade resolution (each miss and final hit)
- Log per-turn lifecycle: message send, settle wait, count changes,
assertions, and failures with elapsed times
- Log fillAndVerifySend retry loop with attempt counts and user message
baseline/current counts
- Log waitForAssistantSettled polling with count changes, periodic
status during long waits, and timeout details
e2e-deep.ts (runFeature):
- Log page navigation URL and result
- Log React hydration wait (start, success, timeout)
- Log conversation turn count after buildTurns
- Log conversation success/failure with diagnostics
All D5 scripts (d5-agentic-chat, d5-gen-ui-custom, d5-gen-ui-headless,
d5-hitl-approve-deny, d5-hitl-steps, d5-hitl-text-input,
d5-mcp-subagents, d5-shared-state, d5-tool-rendering):
- Log expected tokens and actual assistant text at each assertion
- Log DOM element discovery (selectors matched, child counts)
- Log polling progress for long-running assertions (tool card probe,
chained reply fragments)
## Summary
- **Root cause**: `readLastAssistantText` was reading `textContent` from
the entire `[data-testid="copilot-assistant-message"]` wrapper, which
includes rendered tool-component output (PieChart SVG labels like
`Electronics42,000Clothing28,000...`) alongside the actual assistant
chat text
- Scopes the text extraction to the prose/markdown child div
(`[class*="prose"]`) inside the canonical CopilotKit message DOM
structure, which contains only the assistant's text response
- Falls back gracefully for headless/custom-composer demos that don't
use the canonical DOM layout
- Adds debug logging (`[readLastAssistantText]`) so production traces
show which DOM element was selected and what text was extracted
## Test plan
- [x] All 1372 harness unit tests pass (`npx vitest run`)
- [x] TypeScript compiles cleanly (`npx tsc --noEmit`)
- [ ] Local D5 test against langgraph-python gen-ui-custom passes
- [ ] Production D5 probe for langgraph-python stops flapping
readLastAssistantText was reading textContent from the entire assistant
message wrapper, which includes rendered tool-component output (SVG
chart labels like "Electronics42,000Clothing28,000...") in addition to
the actual chat text. This caused gen-ui-custom probes to fail token
assertions because the extracted text was the rendered PieChart content
instead of the follow-up narration.
Scope the extraction to the prose/markdown child div inside the
canonical CopilotKit message structure, which contains only the
assistant's text response. Falls back gracefully when the expected
DOM structure isn't present (headless/custom composers).
Also adds debug logging so production traces show which element was
selected and what text was extracted.
## Summary
- Fixes built-in-agent D5 failures caused by duplicate `TOOL_CALL_END`
events from TanStack's internal tool execution loop
- TanStack's `chat()` engine runs a multi-turn agent loop that re-emits
`TOOL_CALL_END` for frontend-only tools (like `render_pie_chart`)
without a preceding `TOOL_CALL_START`, violating the ag-ui verify
middleware
- Adds `runFinished` flag to `convertTanStackStream` to discard all
events after the first `RUN_FINISHED`
- Adds built-in-agent to `docker-compose.local.yml` and
`local-ports.json`
## Test plan
- [x] Runtime test suite passes (1412/1412 tests, 101 files)
- [x] Container hotpatch verified: no more duplicate TOOL_CALL_END
errors in logs
- [x] D5 hitl-approve-deny now passes (was failing due to this bug)
- [x] aimock v1.16.3 released with complementary item_id fix
TanStack's chat() engine runs a multi-turn agent loop: after the model
returns tool calls, it tries to execute them via processToolCalls().
Frontend-only tools (like render_pie_chart) are unknown to TanStack, so
executeToolCalls() treats them as errors and buildToolResultChunks()
re-emits TOOL_CALL_END without a preceding TOOL_CALL_START. The ag-ui
verify middleware rejects this duplicate.
Fix: track a runFinished flag in convertTanStackStream and discard all
events after the first RUN_FINISHED, which marks the boundary between
the streaming pass and TanStack's internal tool execution loop.
Also adds built-in-agent to docker-compose.local.yml and local-ports.json.
Picks up ag-ui-protocol/ag-ui#1578 — `import * as jsonpatch from
"fast-json-patch"` produced an empty namespace under Node native ESM
because fast-json-patch@3.x populates exports via Object.assign, which
the CJS→ESM named-export detector cannot see. Result: every STATE_DELTA
and ACTIVITY_DELTA event threw "applyPatch is not a function", and
LangGraph generative UI streams floods the console with the failure on
each patch.
0.0.53 switches to a default import so the emitted bundle works under
both ESM and CJS consumers. Bumped @ag-ui/core and @ag-ui/encoder in
lockstep since they share the release.
## Summary
- Replace `SpringAIAgent` (AG-UI SDK) with custom `StreamingToolAgent`
that uses `.stream()` for real-time text delivery and `.call()` with
tool callbacks when tools are needed
- Spring AI's `ChatClient.stream()` fundamentally does NOT auto-execute
tool callbacks; only `.call()` has the built-in tool execution loop. The
stock `SpringAIAgent` used `.stream()` exclusively, so tools were never
invoked and the CopilotKit runtime re-invoked the agent infinitely
- Fix Jackson `InvalidTypeIdException` crashes from CopilotKit runtime
messages with unrecognized roles (`activity`, `reasoning`) by disabling
`FAIL_ON_INVALID_SUBTYPE`, with null-filtering at controller boundaries
## Root cause
The AG-UI Java SDK's `SpringAIAgent` calls
`chatClient.prompt().stream().chatResponse().subscribe()`. In Spring AI
1.0.1, the `.stream()` path does not execute tool callbacks — it only
streams text chunks. When the model returns `tool_calls` in the stream,
they're captured as AG-UI events but the actual Java tool functions are
never invoked. The CopilotKit runtime sees incomplete tool cycles and
re-invokes the agent infinitely.
## Approach
`StreamingToolAgent` implements a two-phase approach:
1. **Phase 1 (stream)**: Uses `.stream()` WITHOUT tool callbacks for
real-time text delivery. Detects whether the model wants to call tools.
2. **Phase 2 (call with tools)**: If tools are needed, re-invokes via
`.call()` WITH tool callbacks. Spring AI's built-in tool execution loop
handles all iterations automatically. AG-UI tool events are emitted via
`AgUiToolCallbackWrapper`.
When no tools are needed, the agent behaves as a pure streaming agent
(no regression from the previous `SpringAIAgent` behavior for tool-free
conversations).
## Why not the previous fix (PR #4469)?
PR #4469 replaced `SpringAIAgent` with `SyncAgent` which used `.call()`
only — this fixed tools but broke streaming entirely. This PR preserves
streaming for the happy path (no tools) and only falls back to `.call()`
when tool execution is actually needed.
## Files changed
- **New**: `StreamingToolAgent.java` — custom `LocalAgent` with
stream+call hybrid
- **New**: `JacksonConfig.java` — tolerant deserialization for unknown
message roles
- **New**: `MessageListFilter.java` — strips null messages from
deserialized lists
- **Modified**: `AgentConfig.java`, `AgentController.java`,
`AgentConfigController.java`, `A2uiFixedSchemaController.java` — use
`StreamingToolAgent` instead of `SpringAIAgent`
- **Modified**: `SubagentsController.java`,
`SharedStateReadWriteController.java` — add
`MessageListFilter.filterNulls()` at controller boundary
## Test plan
- [x] Docker build passes (`depot build --platform linux/amd64`)
- [ ] D5 agentic_chat features with tool calls (weather, flights, etc.)
complete without infinite loop
- [ ] D5 agentic_chat features without tools stream text correctly
- [ ] D5 subagents feature works (already used `.call()`, just added
null filtering)
- [ ] D5 shared-state-read-write feature works
- [ ] D5 a2ui-fixed-schema feature works
- [ ] D5 agent-config feature works
## Summary
- Restores the Railway `serviceInstanceRedeploy` call after each GHCR
image push in the build workflow
- PR #4471 removed this trigger assuming Railway
`environmentPatchCommit` auto-update would handle deploys, but not all
services have auto-update configured
- Without the explicit trigger, GHCR images get pushed and nothing tells
Railway to pull them
## Changes
- `showcase_build.yml`: Replace the "no deploy step" comment with the
actual Railway redeploy step (matching the old `showcase_deploy.yml`
behavior)
- `showcase_deploy.yml`: Update comments to reflect that the build
workflow triggers deploys (verify workflow only checks health)
## Test plan
- [ ] Merge and observe that the next showcase push to main triggers
Railway redeploys
- [ ] Verify the "Showcase: Verify Deploy" workflow confirms services
are healthy after build completes
Spring AI's ChatClient.stream() does NOT auto-execute tool callbacks
(only .call() has a built-in tool execution loop). The stock AG-UI
SpringAIAgent uses .stream() exclusively, so tools are never invoked
and the CopilotKit runtime re-invokes the agent infinitely.
Replace SpringAIAgent with a custom StreamingToolAgent that:
- Phase 1: streams text via .stream() for real-time UX delivery
- Phase 2: if tool calls detected, re-invokes via .call() with
tool callbacks so Spring AI's internal loop handles execution
- Emits proper AG-UI tool call events via wrapper callbacks
Also fix Jackson deserialization crashes from CopilotKit runtime
messages with unrecognised roles (activity, reasoning) by disabling
FAIL_ON_INVALID_SUBTYPE, with null-filtering at controller boundaries
to prevent NPEs in LocalAgent.combineMessages().
Affects all spring-ai endpoints: agentic_chat, agent-config,
a2ui-fixed-schema, subagents, shared-state-read-write.
The decoupled build workflow (PR #4471) removed the Railway deploy
trigger, assuming environmentPatchCommit auto-update would handle
deploys. Not all services have auto-update configured, so GHCR images
were pushed but Railway never pulled them. Restore the explicit
serviceInstanceRedeploy call after each GHCR push.
## What does this PR do?
Removes the `prepack` script from
`@copilotkit/a2ui-renderer/package.json`, which was causing the canary
publish job to fail with `ENOENT` on the staging tarball.
### The failure
[Failed
run](https://github.com/CopilotKit/CopilotKit/actions/runs/25148053109/job/73712052657):
\`\`\`
✔ Build complete in 3002ms
ENOENT ENOENT: no such file or directory, open
'/tmp/.../copilotkit-a2ui-renderer-1.56.4-canary.tyler/romantic-spence-2261fe.tgz'
Error: Command failed: pnpm publish --no-git-checks --tag canary
--access public
\`\`\`
### Root cause
The release scripts already build the workspace before publishing:
- `scripts/release/prerelease.ts:77` runs `pnpm run build` across the
monorepo
- `scripts/release/publish-release.ts:145` does the same
`a2ui-renderer` was the **only** package in the monorepo with
`"prepack": "pnpm run build"`. That script fired during `pnpm publish`
and rebuilt the package a second time. Combined with tsdown's `exports:
true` (which regenerates `package.json`'s exports field on every build),
the second rewrite happened mid-publish and pnpm's pack step failed
silently — leaving an empty staging directory and the ENOENT we see.
### Why removal is safe
- All 12 other monorepo-scope packages publish without `prepack` and
work fine.
- Both release scripts (`prerelease.ts`, `publish-release.ts`) build the
whole workspace before the publish loop, so dist is always ready.
- No CI workflow or script invokes `pnpm pack` directly.
- Consumer-side installs, workspace deps, and `pnpm link` don't run
`prepack` anyway.
The only behavioral change: a developer manually running `pnpm publish`
or `pnpm pack` from the package directory now needs to `pnpm build`
first — matching the rest of the monorepo.
## Related PRs and Issues
- Failed CI run:
https://github.com/CopilotKit/CopilotKit/actions/runs/25148053109/job/73712052657
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
## Summary
The old "Showcase: Build & Deploy" workflow used a single concurrency
group
with cancel-in-progress. Every push to main cancelled in-flight builds,
which meant rapid-fire PR merges routinely killed deploys mid-run. The
harness logging PR (#4465) never deployed because a subsequent merge
cancelled it.
This splits the workflow into two independent files:
- **showcase_build.yml** ("Showcase: Build & Push") -- triggered on push
to
main, builds Docker images and pushes to GHCR. Has NO concurrency group
so every run completes. Railway auto-update picks up the new :latest
tag.
- **showcase_deploy.yml** ("Showcase: Verify Deploy") -- triggered by
workflow_run from the build workflow. Polls Railway to verify each
service
picked up the new image and is healthy. Uses cancel-in-progress since
verification is idempotent.
Also updates showcase_capture-previews.yml to reference the renamed
build
workflow.
## Test plan
- Workflow YAML validates (checked locally)
- Build workflow: no concurrency group, every push-to-main build
completes
- Verify workflow: triggered by workflow_run, cancel-in-progress is safe
- showcase-aimock included in both workflows (from #4458)