## What does this PR do?
Adds documentation for the **`@copilotkit/channels` SDK** — running a
CopilotKit agent as a bot inside Slack, Teams, WhatsApp, Telegram, and
Discord. New `---Channels---` nav group under `showcase/shell-docs`.
**Guides**
- Overview (agent + JSX + channel, end to end), Quickstart (TanStack
factory agent on the Intelligence adapter)
- UI Library (JSX component rendering, callbacks, component
registration, `{ raw }` escape hatch)
- Interactive flows: Human-in-the-loop (+ agent-initiated
`onInterrupt`), Reactions (+ emoji helpers), Modals
- Commands & global reactions, Files & multimodality, MCP (agent-side),
Channel configuration, Tools & context
- Platforms: overview of what Intelligence handles for you, plus
per-adapter pages (Teams / WhatsApp / Slack / Telegram / Discord) with
built-in tools/context
**API reference** (signatures pulled from the installed `0.2.1` type
definitions): Channel, Thread, JSX callbacks.
All examples use the Intelligence adapter except the platform-specific
pages, which use each platform's own adapter. Every internal link
resolves; MDX validated.
## Related PRs and Issues
- N/A
## Checklist
- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
**Plan C: channels are an Intelligence capability.** A valid
Intelligence configuration (API key) is required to run any channel.
There's a **free tier**, so this is "connect your account," not "pay."
The Intelligence runtime runs every channel: managed (Slack/Teams) over
the gateway, and **direct-adapter** channels (Discord/Telegram/WhatsApp,
or self-hosted Slack/Teams) via their own credentialed transport,
started by the runtime. The SSE runtime type-rejects `channels`, and the
handler builds channel activation for an Intelligence runtime.
> Note: the `channel.ɵruntime.{start,stop,addAdapter}` seam is an
internal (`ɵ`) contract the runtime and the managed launcher drive — not
a public API.
Design doc: [Channels = Intelligence-only (Plan
C)](https://app.notion.com/p/3a63aa381852814e83dafedc0742e7cc)
## What changed
- **`refactor(channels-core)`** — relocate the `Channel` lifecycle onto
an internal `channel.ɵruntime.{start,stop,addAdapter}` seam.
- **`feat(channels-core)!`** — **remove** public
`Channel.start()/stop()/addAdapter()`. Channels are runtime-driven only.
- **`feat(runtime)!`** — the `ChannelManager` (built only for an
Intelligence runtime) now **starts direct-adapter channels** via
`ɵruntime.start()` instead of recording them `"unmanaged"` and skipping.
Dead `"unmanaged"` status removed. Direct channels reuse the same
bounded/idempotent/resilient teardown via a synthetic handle.
- **`fix(channels-core)`** — a channel whose adapters **all** fail to
start now **errors** instead of falsely reporting `online` (a partial
start, ≥1 adapter live, still counts as started).
- **`fix(examples)`** — examples bound `channels.ready({ timeoutMs })`
so a wedged adapter start can't hang readiness.
- **`docs`** — 7 channels READMEs + the teams/slack examples reworked to
`new CopilotRuntime({ intelligence, channels })` +
`handler.channels.ready()/stop()`; no `channel.start()`, no DIY.
Multi-platform slack now runs under Intelligence (one `ɵruntime.start()`
starts all its direct adapters).
**Breaking:** `Channel.start()/stop()/addAdapter()` removed
(`channels-core` 0.2.x) — channels are driven by the runtime (`new
CopilotRuntime({ intelligence, channels })`).
## Validation
**Unit** (per-package raw `tsc` + `vitest`): channels-core **156**,
channels-intelligence **182**, runtime channel-manager **38** (incl. a
new `failStart` regression test), channels-integration 8.
**Independent whole-branch review:** direct-channel lifecycle traced
correct for every start/stop interleaving (no wedge, single-stop,
bounded/idempotent teardown shared with the managed path); public-API
removal complete (zero callers); no DIY/`channelRunner`/guard-relaxation
debris; SSE guard intact.
**Adversarial review** (tried to break it) surfaced two real items, both
handled here:
- **False `online`** — `ɵruntime.start()` swallowed adapter start
failures and resolved, so a dead channel read `online`. **Fixed**
(`fix(channels-core)` + `failStart` test).
- **Overstated invariant** — "no standalone path" is enforced by the
runtime gate + `ɵ` convention, not the type system. **Clarified** (see
the note above). Also bounded example `ready()` (`fix(examples)`).
**Live run** against a real Intelligence key (**7/7**): the gate rejects
channels-without-Intelligence; the runtime starts a direct channel via
`ɵruntime.start()` (a real turn invoked the agent, clean `stop()`); and
the real key **authenticated against the real gateway** (a managed join
was rejected by a project feature-flag, not an auth error — so the key
was accepted).
> Examples typecheck only in CI — the worktree can't build every
`@copilotkit/*` sibling; those `TS2307`/`TS2305` are environmental.
## Related
- **Improvements (fast-follow):**
[OSS-599](https://linear.app/copilotkit/issue/OSS-599) — §2 response
policy, four-mode agent binding, run-correctness (Intelligence-side).
- **Managed-provider parity:**
[OSS-600](https://linear.app/copilotkit/issue/OSS-600) +
[601](https://linear.app/copilotkit/issue/OSS-601)/[602](https://linear.app/copilotkit/issue/OSS-602)/[603](https://linear.app/copilotkit/issue/OSS-603)
(Discord/Telegram/WhatsApp) — hosted transport for the remaining
platforms; also closes the direct-channel key-validation gap (managed =
gateway-validated).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adversarial pass caught that every new CopilotRuntime({...}) snippet omitted the
required non-optional `agents` field (TS2741 on copy-paste) — added `agents: {}`
matching examples/slack/app/managed.ts. Also noted that the managed index/mcp
snippets end at ready() with no server, so a reader's process would exit — added a
keep-alive pointer to the Quickstart's full listener mount.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Channels run only through the Intelligence runtime now (PR #6145): public
channel.start()/stop()/addAdapter() are removed. Updated the run path to
new CopilotRuntime({ intelligence, channels }) + handler/listener.channels.ready()/stop()
across quickstart, index, mcp, configuration, the 5 platform pages, and the
channel reference; reframed the managed-vs-direct table (both require an
Intelligence key — the difference is who holds the platform credentials);
name is now required (the runtime keys lifecycle by it); surfaced the
Intelligence-key (free tier) prerequisite. Standardized the managed pattern on
zero-adapter createChannel({ name }) (runtime attaches the managed transport),
matching examples/slack/app/managed.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tool-rendering-custom-catchall renders the ticker quote in the wildcard card.
fastapi's get_stock_price(ticker) had no scriptable args, so when the aimock
fixture emitted price_usd/change_pct the @tool silently dropped the unknown
kwargs and returned random numbers instead of the scripted quote — the card
showed nondeterministic values, diverging from the north star.
Port LGP's scriptable signature: get_stock_price(ticker, price_usd=None,
change_pct=None) — echoes scripted values verbatim when supplied, random when
omitted (backward-compatible with the other tool-rendering pills). Behavior
byte-aligned to langgraph-python.
D6 tool-rendering-custom-catchall green (two real ~8s runs); shared-backend
siblings tool-rendering, tool-rendering-default-catchall, headless-complete all
re-verified green.
Two compounding defects kept this cell red:
1. Backend missing get_revenue_chart. headless_complete.py never registered
the revenue-chart tool, so the chart-turn never emitted a tool call and the
card never mounted. Port LGP's get_revenue_chart tool + its system-prompt
routing rule (tool return shape byte-identical to LGP).
2. Stale turnIndex fixtures caused turn-2 and turn-4 loops to the recursion
limit. Align to LGP's canonical shape:
- tool-rendering.json (shared): AAPL first-leg turnIndex:0 -> hasToolResult:false
so it stops re-firing at turnIndex>=2 in the multi-pill thread.
- headless-complete.json: rewrite to LGP's structure (narration/toolCallId
fixtures first, tool-call legs after with userMessage+context only, no
stale turnIndex); drop the divergent fastapi-only highlight fixtures,
subsumed by the broader substring fixture.
D6 langgraph-fastapi:gen-ui-headless-complete now green (two real ~29s runs);
tool-rendering re-verified green (shared-fixture no regression).
gen-ui-agent had no dedicated backend graph: langgraph.json lacked a
gen_ui_agent entry and route.ts routed the name through the neutral-assistant
loop to sample_agent, which has no steps state or set_steps tool, so the
progress card never mounted (agent hit the default recursion limit of 25).
- Port LGP's gen_ui_agent.py (byte-identical) and register it in langgraph.json.
- route.ts: bind gen-ui-agent to createAgent("gen_ui_agent") and bake
assistantConfig.recursion_limit (default 100) into every LangGraphAgent —
the graph's Python with_config isn't visible to the server runs API, so the
multi-step set_steps walk overran 25. Mirrors langgraph-python.
- aimock: regenerate d6/gen-ui-agent.json from LGP (adds chunkSize:9999 on the
24 tool-call fixtures) and narrow the over-broad d4 chat.json "summarize" key
to "Summarize the sales pipeline" so it stops substring-shadowing the
competitor set_steps chain (and other summarize prompts). Matches LGP.
D6 langgraph-fastapi:gen-ui-agent now green (two real ~14s runs);
agent-config re-verified green (no regression).
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [docker/login-action](https://redirect.github.com/docker/login-action)
| action | patch | `v4.5.0` → `v4.5.1` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/592) for more information.
---
### Release Notes
<details>
<summary>docker/login-action (docker/login-action)</summary>
###
[`v4.5.1`](https://redirect.github.com/docker/login-action/compare/v4.5.0...v4.5.1)
[Compare
Source](https://redirect.github.com/docker/login-action/compare/v4.5.0...v4.5.1)
</details>
---
### Configuration
📅 **Schedule**: (in timezone America/Los_Angeles)
- Branch creation
- "before 9am every weekday"
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/CopilotKit/CopilotKit).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNzUuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI3NS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Fixes the demos a real-LLM audit found broken (aimock had masked them). Each
verified against real OpenAI (plain next dev / next build; turbopack dev is
broken on Windows — unrelated):
- shared-state-read / shared-state-read-write: seed agent state on [agent] deps
(not []). useAgent returns a provisional agent during /info sync; the real
runtime-synced agent swaps in as a new ref, so a []-deps seed effect never
re-ran and runAgent serialized empty state. NOTE: diverges from LGP's
byte-identical frontend — BIA's stateless in-process backend needs re-seeding;
LGP masks it via server-side state. Documented in PARITY_NOTES.
- set_notes -> STATE_DELTA add /notes in tanstack-factory convertStream (mirrors
set_steps), so shared-state-read-write notes render.
- declarative-gen-ui / a2ui-recovery: drop response_format:json_object (the
Responses-API openaiText ignores it and returned empty) — enforce JSON via
system prompt + stripJsonFences, like the byoc factories. Surfaces now paint.
- reasoning-custom / -default / tool-rendering-reasoning-chain: switch to the
Responses API with reasoning summaries (reasoning:{effort:high,summary:auto})
+ custom STEP_* -> REASONING_MESSAGE_* converter; real OpenAI chat-completions
never streams reasoning_content. Reasoning blocks now render.
- cvdiag/*: drop .js import extensions so /api/copilotkit resolves under dev
bundlers.
- PARITY_NOTES: document all fixes + that auth/voice 500 is next-dev-turbopack
only (verified working under next build/start in prod).
The fastapi agent_config_agent still used the v1 StateGraph pattern reading
tone/expertise/responseLength from RunnableConfig[configurable][properties].
The frontend (identical to LGP) publishes those knobs via the v2
useAgentContext hook and the route uses a plain LangGraphAgent, so the old
graph received nothing on configurable and the run errored (turn never
completed, D6 red).
Port the backend to LGP's v2 shape: create_agent + CopilotKitMiddleware with
a single static system prompt that reads the injected context entry. Update
the route comment to describe the useAgentContext path (runtime code
unchanged: plain LangGraphAgent + AGENT_URL env fallback preserved).
D6 langgraph-fastapi:agent-config now green (two real ~28s runs).
open-gen-ui and open-gen-ui-advanced (both probes load gen-ui-open.json) hung
with done-signal-missing (~12 agent runs, 1.3MB tool-call payload each turn).
Cause: two files (gen-ui-open.json + gen-ui-tool-based.json) both defined leg-1
generateSandboxedUi tool-call fixtures for the SAME userMessages ('3D axis…',
'Inline expression evaluator') under the same context but different toolCallIds.
aimock pools every d6/<slug>/*.json into one substring-match set, and the
turnIndex:0 fixture in gen-ui-tool-based.json got served on follow-up turns too
(turnIndex-relaxed), so the toolCallId-anchored terminal never won → no
finish_reason:stop → loop.
Fix (fastapi fixtures only, aligned to GOTCHAS 'canonical fixtures have no
turnIndex' + toolCallId-terminal-before-userMessage-leg1):
- gen-ui-tool-based.json: strip spurious turnIndex:0 from leg-1 tool-call
fixtures; add chunkSize:9999 so the large args JSON-parse in one chunk. Now
the single source for all open-gen-ui pills.
- gen-ui-open.json: empty its fixtures (keep a documenting _note) to remove the
duplicate leg-1/terminal for the 2 probed pills.
- gen-ui-open-advanced.json: delete orphan (no probe declares it; both open
probes use gen-ui-open.json).
D6 green + stable (two ~4s runs each); regression-checked gen-ui-custom
(gen-ui-tool-based route) still green. NOTE: LGP + likely all integrations have
the same open-gen-ui loop (fleet-wide fixture-shadow bug) — no sibling was green.
fixture[8] (userMessage:"project planning", turnIndex:0) returned a
query_notes toolCall. aimock matches userMessage by substring, so
"project planning" also matched the test prompt "Find my notes about
project planning"; as a turnIndex:0 'scripted turn' it beat the correct
terminal fixture[2] (toolCallId-keyed, content-only) on the follow-up turn,
so the agent kept getting another tool call and looped ~30x, never emitting
the done signal (D6 done-signal-missing).
Remove turnIndex:0 from fixture[8] so it's a plain fallback that loses to
the terminal fixture by registration order (matching the langgraph-typescript
shape). D6 now green: two real ~6s runs (was a 120s timeout).
Note: langgraph-python has the identical bug (still red) — same one-line fix
applies there; tracked separately.
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
|
[zizmorcore/zizmor-action](https://redirect.github.com/zizmorcore/zizmor-action)
| action | patch | `v0.6.0` → `v0.6.1` |
---
> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/592) for more information.
---
### Release Notes
<details>
<summary>zizmorcore/zizmor-action (zizmorcore/zizmor-action)</summary>
###
[`v0.6.1`](https://redirect.github.com/zizmorcore/zizmor-action/releases/tag/v0.6.1)
[Compare
Source](https://redirect.github.com/zizmorcore/zizmor-action/compare/v0.6.0...v0.6.1)
zizmor 1.28.0 is now the default version used by the action.
</details>
---
### Configuration
📅 **Schedule**: (in timezone America/Los_Angeles)
- Branch creation
- "before 9am every weekday"
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Enabled.
♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/CopilotKit/CopilotKit).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNzUuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI3NS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
fe-parity.ts now reads a per-integration allowlist from
integrations/<slug>/PARITY_NOTES.md (an <!-- fe-parity-allow --> block of
'src-relative-path | reason' lines). Listed files report as ALLOWED
(sanctioned per-integration divergence), not DRIFT; the gate passes on zero
UNSANCTIONED drift instead of zero diffs. This stops the byte-identical goal
from being impossible for demos that legitimately must differ (e.g.
a2ui-recovery's per-slug prompt, which has no aimock context routing).
Add langgraph-fastapi/PARITY_NOTES.md documenting the a2ui-recovery frontend
divergence with reasons. D6 remains the correctness gate; fe-parity is the
'where to look' map.
The flagship beautiful-chat cell was a stub (generic all-tools agent on a bare
route) that only passed local aimock D6 because fixtures scripted the tool
calls; against a real LLM (prod) it said query_data/Excalidraw/etc. 'not
available'. Port the real backend mirroring langgraph-python's beautiful_chat:
- New createBeautifulChatAgent() with server tools query_data (sales data),
manage_todos, get_todos, search_flights (A2UI fixed-schema flight cards), and
declaration of the runtime-injected tools (render_a2ui, generateSandboxedUi,
Excalidraw MCP) so the model can actually call them; manage_todos result ->
STATE_DELTA /todos for the app-mode board. Model gpt-5.4.
- Route configures the combined runtime: openGenerativeUI:true,
a2ui.injectA2UITool:true (catalog copilotkit://app-dashboard-catalog),
mcpApps (Excalidraw).
VERIFIED against a real OpenAI LLM (not aimock): all 6 flows fire the tool +
render the component (pie/bar via query_data, calculator OGUI iframe, flight
A2UI cards, Excalidraw MCP iframe, todo board). Shared factory defaults
unchanged. Also gitignore .env/.env*.local (local real-key testing).
## Problem
After #6106 merged, **staging showed every built-in-agent cell capped at
D3** with the backend red:
```
Failure: empty assistant response (level: chat)
Backend: https://showcase-built-in-agent-staging.up.railway.app
```
The D4 chat probe hits the main agent (`agentic_chat` / generic
`createBuiltInAgent`), which used **`gpt-5.5`** → the real OpenAI call
returns empty → the whole D-ladder gates → all cells stuck at D3.
## Root cause
- `gpt-5.5` is **unique to built-in-agent** (introduced in #6106). Every
other integration — including the **LGP reference that works on
staging** — uses **`gpt-5.4`** (38× across the repo).
- Staging's OpenAI account doesn't serve `gpt-5.5`, so every real call
came back empty.
- **aimock ignores the model id**, so this was invisible in local D6
(which is why it passed locally but broke on staging).
- The other factories also still had leftover `gpt-4o` / `gpt-4o-mini`
(a2ui, byoc, mcp-apps, ogui, subagents).
## Fix
Switch **all** `built-in-agent` `openaiText()` calls to **`gpt-5.4`**
(matches LGP, served on staging). `reasoning-factory` keeps `gpt-5.2`
(reasoning-capable; those demos are NSF-quarantined). 10 factory files,
model-string-only changes.
> Note: this deviates from the general "always gpt-5.5" preference
because this is an **LGP-parity** integration and staging only serves
`gpt-5.4` — parity + a working staging both require it.
## Verify
Once merged + redeployed, staging D4 chat should return real text and
the cells should climb past D3. (Can't be verified in local D6 since
aimock ignores the model.)
Staging D4 chat probe returned 'empty assistant response' for every cell,
capping all cells at D3. Root cause: the factories used gpt-5.5 (agentic-chat +
generic createBuiltInAgent) and leftover gpt-4o/gpt-4o-mini (a2ui, byoc, mcp-apps,
ogui, subagents). gpt-5.5 is unique to built-in-agent; every other integration —
including the LGP reference that works on staging — uses gpt-5.4, which staging's
OpenAI account serves. aimock ignores the model id, so this was invisible locally.
Switch all built-in-agent openaiText() calls to gpt-5.4 (matches LGP, unblocks
staging). reasoning-factory keeps gpt-5.2 (reasoning-capable, its demos are
NSF-quarantined). Note: deviates from the general gpt-5.5 house rule because this
is an LGP-parity integration and staging only serves gpt-5.4.
Adversarial-review finding: ready() awaits all channels' startup; an adapter whose
start() hangs would block readiness forever. Both examples now pass timeoutMs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial-review finding: ɵruntime.start() swallowed every adapter start failure
and resolved, so the runtime reported status "online" on a dead channel (revoked
token, port-in-use). Now: if a channel has adapters and NONE started, start() rejects
so ChannelManager surfaces "error" (a partial start, >=1 adapter live, still counts as
started). Adds a channel-manager failStart regression test + updates the telemetry test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The activation path reads the top-level channel.provider; the ɵruntime.provider
copy was never consumed. Keeps the real top-level accessor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What & why
Brings the **built-in-agent** showcase integration to parity with the
**LangGraph-Python (LGP)** reference: byte-identical demo frontends + a
named-agent backend registry (BuiltInAgent + TanStack AI), so every demo
climbs the D0–D6 ladder against the LGP gold standard.
## Changes (4 commits)
1. **P0 pattern** — `agentic-chat` byte-identical + named agent
(`agentic_chat`); proven D6-green locally. Fixed `gpt-4o` → `gpt-5.5` in
the shared factory.
2. **Frontend migration (all demos)** — every LGP `src/app/demos/*`
copied verbatim (`diff -r` clean), plus shared `components/ui` (25
shadcn primitives) + `lib/utils`, byte-identical. Added the 5 demos BIA
lacked (`a2ui-recovery`, `declarative-hashbrown`,
`declarative-json-render`, `shared-state-read`,
`threadid-frontend-tool-roundtrip`); added the frontend deps the copied
UI needs (radix-ui, cmdk, embla-carousel-react, react-markdown,
remark-gfm, yaml, …).
3. **Named-agent backend** — `/api/copilotkit` registers 22 named agents
(generic all-tools, fixture-driven; reasoning trio via the reasoning
adapter). 8 dedicated routes re-keyed `default` → LGP agent id;
`mcp-apps` also serves `headless-complete`; `ogui` serves both
open-gen-ui ids; `byoc-*` routes renamed to `declarative-*`; new
`a2ui-recovery` + `beautiful-chat` routes reuse existing agents. Dropped
BIA-only extras (`byoc-*`, `hitl-in-chat-booking`).
4. **Reconcile** — `manifest.yaml` (37 features / 40 demos;
`generate-registry` + `validate-parity` pass) + `PARITY_NOTES.md`.
> **Note on "byte-identical":** frontends are verbatim LGP **modulo
BIA's `consistent-type-imports` ESLint rule** (type imports split into
`import type {}`) — required for a green lint/PR, semantically & DOM
identical.
## D6 status (local sweep)
- **~33/40 demos GREEN** on the first sweep — byte-identical frontends +
named agents + existing fixtures work broadly.
- **4 RED locally are an aimock-infra issue, not this integration:** the
deployed `ghcr.io/copilotkit/aimock:latest` has no
`context`/`--context-field` fixture scoping, so cross-slug `userMessage`
collisions let earlier-loaded (`ag2`/`d4`) fixtures shadow BIA's own.
BIA's fixtures are **correct** and converge under a context-aware aimock
(present on aimock `origin/main`). Affects
`tool-rendering-custom-catchall`, `headless-complete`, `gen-ui-agent`,
`frontend-tools`. **Action for infra: redeploy aimock from a
context-aware build.** Details in `PARITY_NOTES.md`.
- **2 downstream-host RED (kept as features, informational — mirrors
LGP):** `declarative-gen-ui` (A2UI renderer host) and `mcp-apps` (MCP
iframe host).
- Quarantined NSF unchanged: `gen-ui-interrupt`, `interrupt-headless`,
`shared-state-streaming`, reasoning-trio.
D6 is informational/weekly (not a merge gate); these are documented for
parity tracking.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What
Widens the optional `@anthropic-ai/sdk` peer dependency of
`@copilotkit/runtime` from `^0.57.0` to `>=0.57.0`.
## Why
An outside contributor reported that `@copilotkit/runtime` still
declares an optional peer of `@anthropic-ai/sdk@^0.57.0`, which
conflicts with current SDK versions (e.g. `0.109`) and forces consumers
to install with `--legacy-peer-deps`.
The `^0.57.0` caret pins the range within `0.57.x` (since it's a `0.x`
version), so any newer minor trips the peer-conflict warning even though
the SDK works fine at runtime.
## Safety
The anthropic adapter only relies on stable `@anthropic-ai/sdk` APIs —
client construction (`new Anthropic({})`), `messages.create`, and
ephemeral `cache_control` — which have been stable across `0.x`
releases. The reporter confirms `0.109` works at runtime today; this
only removes the false install-time peer conflict.
## Change
One line in `packages/runtime/package.json`:
```diff
- "@anthropic-ai/sdk": "^0.57.0",
+ "@anthropic-ai/sdk": ">=0.57.0",
```
Both run their Channel through an Intelligence runtime that owns lifecycle:
new CopilotRuntime({ intelligence, identifyUser, channels: [bot] }) + handler.channels.ready()/stop(),
no bot.start(). Direct adapters retained (multi-platform slack now runs under Intelligence). Env + example READMEs note the required Intelligence key (free tier).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Channels require an Intelligence connection (API key, free tier); run via
CopilotRuntime({ intelligence, channels }) + handler.channels.ready()/stop();
no channel.start(), no standalone/DIY path. Adapters keep their credentials.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ChannelManager (only constructed for an Intelligence runtime) now STARTS
direct-adapter channels via channel.ɵruntime.start() instead of recording them
"unmanaged" and skipping — so every channel runs only because Intelligence is
configured; there is no standalone path. Removed the now-dead "unmanaged" status;
direct channels reuse the bounded/idempotent teardown via a synthetic handle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Channels run only through the Intelligence runtime now; there is no public
lifecycle to call. The internal channel.ɵruntime.{start,stop,addAdapter} seam is
retained and is what the runtime drives. Internal callers + guard strings migrated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Fixes#4819.
`processAgentResult` recurses into `runAgent` whenever `needsFollowUp`
is true, with **no depth limit, no circuit breaker, no duplicate
detection**. Any scenario that keeps `needsFollowUp = true` loops
forever:
- The LLM repeatedly calling the same tool
- The backend returning a `RunError` after receiving tool results
(#2416)
- Input processors reprocessing tool messages (#3044)
This silently consumes API quota and can DOS the backend. The only
existing workaround — setting `followUp: false` on a tool — is too blunt
and breaks legitimate multi-step workflows.
## Change
- Add an absolute safety cap `MAX_FOLLOW_UP_DEPTH = 100`, reusing the
existing `_runDepth` counter (incremented in `runAgent`, decremented in
its `finally`).
- In `processAgentResult`, when the depth cap is reached: stop
recursing, emit a `logger.warn` (for debuggability), and return the
current result normally (already-inserted tool messages are preserved).
- The cap is deliberately high so legitimate multi-step workflows
(search → fill form → confirm → update → send email) are never affected;
it only trips on runaway recursion.
This protects against **all** infinite-loop scenarios in the issue, not
just specific triggers, without requiring users to set `followUp:
false`.
## Test
- Added `core-full.test.ts` TEST 9b: an agent that always returns a
fresh tool call (so `needsFollowUp` stays true). Asserts the run
terminates, `runAgent` is capped at ≤100 calls, and a warning is
emitted.
- `nx run @copilotkit/core:test` → 40 files / 431 tests pass.
- `nx run @copilotkit/core:build` → success.
Move start/stop/addAdapter/provider bodies onto channel.ɵruntime.*; the public
methods become thin delegators (behavior identical). Lets a runner drive the
lifecycle without the public API. Pure relocation — tsc + 156 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vi.fn mock factories were 0-arg but invoked with the provider options in
the vi.mock adapters, tripping tsc (TS2554). vitest transpiles without type-
checking, so it passed vitest locally but failed the check-types job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_def.typeName isn't on ZodTypeDef's public type (TS2339 in check-types);
instanceof is the type-safe equivalent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A frontend/client tool (`useFrontendTool`) whose Zod `parameters` use
`z.discriminatedUnion(...)` — or any schema that serializes to a JSON-schema
`anyOf`/`oneOf` node — silently lost the union-typed field when calling OpenAI.
The tool call arrived with that field missing or empty.
Root cause: the runtime has two JSON-Schema -> Zod converters. The shared one
(`@copilotkit/shared`) already handles `anyOf`/`oneOf` as `z.union`, but the
local copy in `packages/runtime/src/agent/index.ts` — used by the classic
`BuiltInAgent` / AI SDK path via `convertToolsToVercelAITools` — never did.
A union node carries no top-level `type`, so it hit the empty-schema guard and
collapsed to `z.object({})`. The reconstructed tool schema therefore dropped
the union entirely (most visibly for a union nested inside array `items`), so
the model was never offered those fields and could not emit them.
Fix: handle `anyOf`/`oneOf` as `z.union` before the empty-schema guard,
mirroring the shared converter. A single-variant union unwraps to that variant.
Backward compatible: only previously-broken union nodes change behavior
(empty object -> real union). Empty `{}` schemas, typed nodes, and the
`isJsonSchema` gate are untouched.
Repro: a `useFrontendTool` with `parameters: z.object({ blocks: z.array(
z.discriminatedUnion("type", [...])) })` against OpenAI — before, `blocks`
items arrived empty; after, both variants survive into the model call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `@copilotkit/react-native` barrel statically re-exports the prebuilt chat
UI (CopilotChat / CopilotModal / CopilotSidebar / CopilotPopup, which import
`@gorhom/bottom-sheet`) and `useAttachments` (which imports
`expo-document-picker` + `expo-file-system`). Those are optional peer deps, but
a static re-export still forces Metro to resolve them at bundle time. A headless
consumer that uses only `CopilotKitProvider` + `useAgent` + `useFrontendTool`
(a fully custom UI) had to install every chat/attachment native dep or stub them
in `metro.config.js`, or the release bundle fails with
`Unable to resolve module expo-document-picker`.
Add a lean `@copilotkit/react-native/headless` entry that re-exports only the
provider, the platform-agnostic hooks, the render-tool registry, and the
core/AG-UI types — none of the chat UI or `useAttachments` — so those native
deps never enter the bundle graph and the metro-stub workaround is retired.
Mirrors `@copilotkit/react-core/v2/headless` (#5883): a standalone entry file,
wired into the tsdown entry list, the package.json `exports` map, and
`sideEffects` (it side-effect-imports the polyfills). The default barrel now
does `export * from "./headless"` and layers the chat UI on top, so it stays
fully backward compatible. Adds a static import-graph regression test asserting
the headless graph never reaches the chat/attachment modules or their native
peer deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UseAgentProps had no `threadId`, yet the shipped V2 React Native demo
calls `useAgent({ agentId: "default", threadId })`. Because the prop
didn't exist, it was silently dropped: the demo's thread state (and its
"New Chat" reset) never reached the agent, which ran under its own
auto-minted UUID. threadId was only ever sourced from a surrounding
`CopilotChatConfigurationProvider`, which the headless RN demo has none.
Accept an optional `threadId` on UseAgentProps and honor it. Resolution
precedence: an explicit `threadId` prop wins; otherwise fall back to the
chat configuration's threadId (gated on hasExplicitThreadId, as before).
When the prop is omitted, behavior is unchanged — fully backward
compatible. This makes the RN demo's existing usage work and unblocks
headless callers that have no chat-configuration provider in the tree.
Extends the threadId-propagation contract test with coverage for the
prop path (no provider, prop-over-config precedence, prop re-sync).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveModel() built each provider with only an apiKey, so a string model
spec (e.g. "openai/gpt-4o-mini") could not target an OpenAI-compatible
endpoint — OPENAI_BASE_URL was silently ignored, and the only workaround was
to construct the LanguageModel yourself and pass the instance. Same gap for
Anthropic and Google.
Now resolveModel passes baseURL from the standard env var
(OPENAI_BASE_URL / ANTHROPIC_BASE_URL / GOOGLE_GENERATIVE_AI_BASE_URL). It is
undefined when unset, so the provider falls back to its default endpoint —
fully backward compatible.
Tests: adds resolve-model-baseurl.test.ts (4 cases). Existing resolveModel
tests unchanged (118 runtime tests pass locally).
Discovered while building an OpenAI-compatible agent on the V2 runtime +
@copilotkit/react-native.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What & why
`@copilotkit/react-core`'s `/v2` entry (and the root entry) re-exports
from a
single **monolithic shared chunk**, so importing *any* symbol — even one
hook —
pulls the built-in chat-message rendering stack (`streamdown` → shiki,
`mermaid`,
`cytoscape`, `katex`) into the consumer's bundle: **~3 MB gzip / ~15 MB
raw**,
with no way to tree-shake it. Consumers who build a fully custom chat UI
and only
use hooks pay the full cost. (Issue #4893.)
A separate lean build entry — `@copilotkit/react-core/v2/headless` —
already
ships those hooks in their own small chunk **without** that stack (it's
how
`@copilotkit/react-native` mounts CopilotKit). This PR makes it usable
for a
custom web UI.
Measured with esbuild (react/react-dom external):
| import | bundled JS |
|---|---|
| hooks from `@copilotkit/react-core/v2` | **~2.96 MB** gzip (≈
importing full `CopilotChat`) |
| same hooks from `@copilotkit/react-core/v2/headless` | **~0.03 MB**
gzip |
> Note on the report: `@copilotkit/a2ui-renderer` doesn't bundle the
rich-text
> stack (its deps are `@a2ui/web_core`, `lit`, `clsx`, `zod`). The
weight is
> entirely `streamdown` (shiki/mermaid/cytoscape) + `katex`, pulled by
the
> built-in `CopilotChat*` message components.
## Changes
- **`headless.ts`** — export `useCopilotKit` + `useRenderToolCall` (both
DOM-free
and rendering-stack-free); fix `UseAgentUpdate` (a runtime `enum`) being
re-exported via `export type`, which stripped its runtime value under
`isolatedModules` (so `useAgent`'s `updates` option was unusable from
headless
— a bug `tsc` can't catch). `useDefaultRenderTool` /
`useRenderCustomMessages`
/ `useRenderActivityMessage` stay in `/v2` (web-only markup or
`a2ui-renderer`).
- **Remove a `tailwind-merge` leak** — extract the tailwind-free ref
helpers
(`shallowEqual` / `useShallowStableRef`) into
`lib/shallow-stable-ref.ts` so the
headless graph no longer pulls `tailwind-merge` via
`CopilotChatConfigurationProvider`.
- **Test** — a small vitest export-surface test guarding the hook
surface and the
`UseAgentUpdate` runtime value.
## Verification
`react-core` typecheck + vitest (1427) and `react-native` typecheck
pass. Shipped
`dist/v2/headless.mjs` imports only `react`, `@ag-ui/client`,
`@copilotkit/core`,
`@copilotkit/shared`, `@copilotkit/react-core/v2/context`, `zod` — no
rendering
stack.
## Notes / follow-ups
- Headless hooks read a **different React context** than the prebuilt
`/v2`
`<CopilotKitProvider>`, so the two can't be mixed — mount a lean
provider over
`/v2/context` (as `@copilotkit/react-native` does). Making the `/v2`
provider
reuse the standalone `/v2/context` singleton would remove that sharp
edge and
is the natural follow-up.
- Existing bundle-size CI (`compressed-size-action`) already tracks
`headless.mjs`,
so no new size tooling is added here.
Addresses #4893.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fixes#5966. Follow-up to #5812 / #5885.
## Problem
After #5885 stopped the post-terminal `TEXT_MESSAGE_END` crash, pressing
**Stop** no longer breaks the chat — but for an agent that emits a
terminal `RUN_ERROR` (code `abort`) as its cancellation signal (e.g.
pydantic-ai's `AGUIAdapter`), the client still surfaces that as an
**error banner** ("This operation was aborted"). A user-initiated stop
is expected cancellation, not a failure.
Traced path (no abort suppression at any hop):
`RUN_ERROR(code:"abort")` → `RunHandler.onRunErrorEvent`
(`run-handler.ts`) → `emitAgentError(AGENT_RUN_ERROR_EVENT)` → `onError`
→ CopilotChat/react-ui `triggerChatError` → banner. The only existing
abort suppression is for the *local* fetch-abort rejection
(`run-handler.ts:318`), a different path.
## Fix
Suppress the error emission in `onRunErrorEvent` when the run was
user-aborted — mirroring the local-abort suppression already on the
`runAgent`/`connectAgent` paths:
```ts
const runWasAborted = this._runAbortController?.signal.aborted === true;
if (runWasAborted || event?.code === "abort") {
return;
}
```
Prefers the client's own `_runAbortController.signal.aborted` (robust —
the agent-supplied `code` isn't standardized across agents; the
`code:"abort"` in the repro comes from the agent, not CopilotKit) with
`code === "abort"` as a secondary signal. Normal `RUN_ERROR`s are
unaffected.
## Testing
- **TDD** (`core-error-handling.test.ts`): two new tests fail pre-fix
and pass after —
- agent emits `RUN_ERROR` code `"abort"` → no `AGENT_RUN_ERROR_EVENT`
surfaced.
- run user-aborted mid-stream (via `agent.abortRun()`, which RunHandler
intercepts to abort the controller) → a subsequent `RUN_ERROR` with a
*non-abort* code is still suppressed (exercises the `signal.aborted`
path).
- The pre-existing test — normal `RUN_ERROR` (code `"bad_request"`)
still emits `AGENT_RUN_ERROR_EVENT` — continues to pass (control against
over-suppression).
- Full `@copilotkit/core` suite green: **578/578**. `tsc` 0 errors;
`oxlint` 0.
Note: this is a UX/product call (a user Stop shouldn't render as an
error). Suppressing in core fixes it for both the default chat banner
and app-level `onError` handlers; the run lifecycle still reflects the
termination.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- folds CopilotKit App Context into `ModelRequest.system_message` during
LangGraph model calls
- stops injecting App Context as a separate `SystemMessage` from
`before_agent`, avoiding non-consecutive system messages after LangGraph
reducers run
- updates middleware regression tests for state context, runtime
context, forwarded-header filtering, and message-history no-op behavior
## Root cause
`before_agent` inserted a new App Context `SystemMessage` into
`state["messages"]`, but the LangGraph message reducer can append new
message IDs rather than preserving positional insertion. With an
existing system prompt and prior conversation turns, this can produce
multiple non-consecutive system messages, which `langchain-anthropic`
rejects.
Fixes#5610.
## Validation
- `uv run pytest -q`
## What does this PR do?
Pins `opentelemetry-resourcedetector-gcp==1.12.0a0` in the ADK starter.
`google-adk` accepts prerelease resource-detector versions, but `uv`
skips transitive prereleases by default. The only stable candidate,
`1.13.0`, was yanked because it breaks imports. That left the ADK
starter with no valid dependency set.
The direct pin selects the last working release without allowing
prereleases for every dependency. The ADK starter image can build again.
## Related PRs and Issues
- Failing job:
https://github.com/CopilotKit/CopilotKit/actions/runs/30027772426/job/89285780349
## Validation
- RED: `docker build --progress=plain -f
examples/integrations/adk/Dockerfile examples/integrations/adk` failed
at `uv pip install --system -e .` because the requirements were
unsatisfiable.
- GREEN: the same command resolved 124 Python packages, installed the
pinned detector, built the Next.js app, and exported the image.
- `pnpm nx affected -t lint,check-types,test,build --base=origin/main
--head=HEAD` found no affected Nx tasks because this starter is built
outside the Nx project graph.
- `oxfmt --check examples/integrations/adk/agent/pyproject.toml` passed.
## Checklist
- [x] I have read the Contribution Guide.
- [x] No documentation change is needed; this restores the existing
starter build.
- [x] Allow edits by maintainers is enabled.