Creating a Node listener or an Express handler now STARTS activation of the
runtime's declared managed Channels, so `channels.ready()` becomes
await-and-observe instead of the thing you must remember to call. A declared
Channel connects because it was declared.
The failure mode this removes: forget `ready()` and you get a process that
serves HTTP, looks healthy, and is silently disconnected with zero output.
Auto-start's worst case is an activation error in the logs.
The generic Fetch handler stays LAZY — it is the serverless/edge entry point,
where isolates freeze and recycle per request and separate cold starts would
mint competing listeners for the same Channel. `createCopilotHonoHandler` stays
lazy for the same reason: it is our Next.js App Router surface in practice
(every `examples/showcases/*` route handler builds one at module scope), and its
TSDoc now says so loudly. `activateChannels: false` remains the opt-out that
opens no socket.
Consequence for host code: the shutdown-handler boundary moves earlier. Signal
handlers must be registered before the listener is CREATED, not merely before
`ready()` — otherwise a Ctrl-C during the connect window hits Node's default
handler and leaks a live gateway session. The slack and teams examples and the
docs snippets are restructured accordingly.
Also migrates the seven channel-package README quickstarts off the generic
handler (a request handler a socket-mode bot constructs and never serves) onto
the Node listener, so they inherit auto-start and agree with the docs site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the previous commit, whose wiring was left out of it by mistake.
createChannel applies sanitizeAgentEventStream at the agentFactory seam, with
sanitizeAgentEvents: false to opt out; HttpAgent is re-exported from
@copilotkit/channels so the examples need no @ag-ui/client dependency; the
Slack + Teams examples and READMEs now wire a plain HttpAgent; and
SanitizingHttpAgent is deprecated (unchanged) in both adapter packages.
Also swaps a stray pair of raw control bytes in the protobuf test fixture for
escapes, so git sees the test file as text.
The report's third column was budget-usage bars: the same three team totals the
donut beside it already showed, redrawn with limits added. Two charts, one fact.
It now ranks the largest individual charges, which changes the unit of analysis
from team to transaction. A team aggregate cannot distinguish one $15,000
charge from thirty $500 ones, so this is information the donut genuinely cannot
carry. Bars stay coloured by owning team, so a row still ties back to its slice
without the two charts duplicating each other.
Budget-vs-limit is not lost: it remains the "Over policy limit" KPI above and
the "Needs a decision" rows below.
Invoice-derived line items now carry the policy id of the team they belong to,
so they colour like any other charge. Without it they fell back to a generic
swatch and a Marketing charge did not match Marketing's slice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five presentation fixes to the Northwind Finance demo, all from running the
beats live.
PIN change now resolves into a card rather than the sentence "New PIN saved."
It shows the card face, brand and last4, a masked new-PIN row and an active
badge. Digits are never rendered: they are never sent to the agent, so the mask
is the honest representation.
Reopening a thread replays setCardPin with status "inProgress" and no result,
so the answered card sat on "Loading..." forever. Other human-in-the-loop tools
here (showCharges) do replay their result, so this is specific to that call.
The outcome is now remembered per tool call id for the session and consulted
ahead of the replayed status. Both this card and the charges card key their
resolved state on the RESULT rather than the status, so an answered call can
never replay with live buttons.
setCardPin also registers once via a ref instead of depending on `cards`:
useFrontendTool re-registers whenever JSON.stringify(deps) changes and
re-registration removes the tool, so the PIN write tore down the very tool that
was servicing it.
showCharges becomes human-in-the-loop. Opening a filtered list is safe, but it
replaces the whole screen, and an agent that does that unasked reads as the
agent being in charge. The confirm card names the sort and filters before the
page changes, and on arrival the Sort and Show controls carry the brand tint
whenever they are non-default, so what the agent set is what lights up.
The Q2 report shows three different chart forms (share-of-total pie, time
series, budget bars) instead of three bar charts, and the seed is rebalanced so
team shares read 42/28/30 instead of 98/2/2 while all three pending charges
still exceed their limits. This drops the income-vs-expenses chart that was
showing $0.00.
Notes about reported charges carry a leading alert emoji so they cannot be
skimmed past. The seeded procedure asks for it and the handler applies it
regardless, because a model is not a reliable emoji emitter.
Finally, prose answers get a house style. The agent was formatting the first
few bullets of a list and then lapsing into plain text, which reads as a
rendering bug rather than a style choice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What
Bumps `ag-ui-adk` from `0.6.3` to `0.7.0` in the ADK starter templates
(`examples/integrations/adk` and `examples/integrations/adk-angular`),
and
regenerates both `uv.lock` files.
## Why
`npx create-ag-ui-app@latest` → ADK scaffolds from
`examples/integrations/adk` (via `copilotkit create -f adk`, which
resolves
`-f adk` to `copilotKitStarter("examples/integrations/adk")`).
That starter pins `ag-ui-adk==0.6.3`. A2UI generative-UI rendering for
ADK
landed in `ag-ui-adk` **0.7.0** (OSS-158, ag-ui#1955), so every ADK
project
scaffolded today ships a backend with no A2UI support at all.
## Compatibility
`ag-ui-adk` 0.7.0 requires `ag-ui-protocol>=0.1.15` (starter pins
`0.1.18` ✓),
`google-adk>=1.28.1,<3.0.0` (unpinned in the starter ✓), and pulls in
two new
transitives: `ag-ui-a2ui-toolkit>=0.0.3` and
`a2ui-agent-sdk>=0.2.4,<0.3.0`.
No manifest changes beyond the `ag-ui-adk` pin were needed.
Note on the large `uv.lock` diff: regenerating the lockfiles re-resolved
`google-adk` from `1.26.0` → `2.5.0`. The previous lock pinned
`google-adk`
below 0.7.0's new `>=1.28.1` floor, so it *had* to move; `2.5.0` is the
latest
release inside the `<3.0.0` ceiling. That major re-resolve (and its
leaner 2.x
dependency tree) accounts for the bulk of the lockfile churn. Both
starters
`uv sync` and boot (`main.py` imports cleanly) against the new tree.
## Verified
- `uv lock --check` clean on both starters
- `uv sync` resolves `ag-ui-adk 0.7.0`
- `from ag_ui_adk import get_a2ui_tool` imports (symbol does not exist
in 0.6.3)
## ⚠️ Follow-up required — this PR alone does not reach users
The `copilotkit` CLI pins the template ref at **build time**:
```js
function getTemplateRef() {
return true ? "a1c9b3147829ac358bae82df651f45a1aea2a437" : "main";
}
```
`copilotkit@4.5.0` is currently pinned to `a1c9b31`, which predates this
change. Merging this PR does **not** change what `npx
create-ag-ui-app@latest`
produces — the CLI will keep serving `ag-ui-adk==0.6.3` until a new
`copilotkit` CLI release is cut whose `getTemplateRef()` points at a
commit
containing this fix.
**A CLI release is required to ship this.**
## Out of scope
Bumping the pin gives the starter the A2UI *capability*. Whether the
scaffolded frontend registers an A2UI catalog (required for anything to
actually render) was not audited here and is left to a follow-up.
The approval loop could never pass, independent of the demo's behavior:
for (let step = 0; step < 4; step++) {
const approve = page.getByRole("button", { name: /^approve$/i });
await approve.first().click({ timeout: 30_000 }).catch(...)
}
Two compounding problems. The recall path emits at most three HITL cards
(openPolicyException -> finalizePolicyException -> approveTransaction) but the
loop demanded four, so the last iteration always waited on a card that never
arrives. And the per-click timeout (30s) equalled the whole test budget
(playwright.config.ts sets no `timeout`, so Playwright's 30s default applied),
so the first miss consumed the entire test and the `.catch()` fallback to the
other APPROVE_LABELS was unreachable dead code.
Now: poll every label for the next visible approve control, treat "no card
left" as flow completion rather than a timeout, keep a runaway guard that is
explicitly not the expected card count, and give the test a budget that can
actually hold one agent turn plus the closing server poll.
Confirmed against a local run (aimock + the docker Intelligence stack): the
agent recalls the procedure, files the EXC-BOARD-APPROVED exception, and
approves the charge, and the "Record a workflow?" card never appears. The
header's "verify on first green run" notes are updated to record what that
run settled and what it did not.
The spec still fails at its closing assertion: PUT /api/v1/transactions/t-3
{status:"approved"} keeps returning 422, i.e. store.hasApprovedException() is
false, so the over-limit gate is never lifted even though the (fixture-scripted)
agent text claims it was. That is pre-existing and out of scope here — this PR
touches no part of the approval path (lib/store, the exceptions/transactions
routes, copilot-context, components/wow are all untouched). Left failing rather
than skipped so it stays visible.
Co-Authored-By: Claude <noreply@anthropic.com>
The reference page's Parameters section covered only `agentId` and `updates`, so
both props this branch adds were undocumented.
Adds an entry for each, and notes on `agentId` that passing `runtimeAgentId`
makes it required and turns it into a name the hook registers an agent under
rather than one it retrieves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes OSS-646. Split out of OSS-641 as the unambiguous half. This PR
does **not** change when activation happens — whether the long-running
wrappers should auto-connect stays open on OSS-641.
## Why
`createCopilotRuntimeHandler` builds the `ChannelManager` but opens no
connection; activation is lazy, triggered by the first
`channels.ready()`. That is deliberate (`fbf35ac59`, OSS-473) —
Cloudflare/Next isolates freeze and recycle per request, so cold starts
would mint conflicting listeners. Two things were left inconsistent with
it:
1. `endpoints/node.ts` still documented the pre-`fbf35ac59` world — "the
same `ChannelsControl` surface the underlying fetch handler **activates
at creation time**" — and labelled the one required call as `//
Optional:`. That's the TSDoc developers and coding agents see in-editor,
and it contradicted every channel-package README. Same failure class as
OSS-634.
2. `68349bc1f` gave the fetch handler a branded overload so
`handler.channels.ready()` type-checks without `?.`, but the node
wrapper never got it — so every call site, including our own example and
all nine showcase docs pages, was written defensively.
### A live consequence, found en route
`examples/slack/app/managed.ts` never called `ready()`. It built the
runtime, mounted the listener, logged `[channel] started managed Channel
"…"`, and only ever called `stop()` — so since activation went lazy it
has connected nothing while reporting success. It was written against
exactly the creation-time model the TSDoc described. Fixed here, with a
regression assertion.
## What changed
- **Types** — `createCopilotNodeListener` gets the branded overload pair
mirroring `createCopilotRuntimeHandler`: a runtime with at least one
declared Channel yields non-optional `.channels`; `activateChannels:
false` and channel-less runtimes keep the optional shape. Adds
`NodeCopilotListenerWithChannels`; both listener types are now exported
from `@copilotkit/runtime/v2/node`.
- **Docs** — node/express/hono TSDoc corrected: creation opens no
connection, `ready()` is what activates, and it is required on a
long-running host. Same stale claim fixed in the three example comments
and `examples/slack/README.md` that repeated it.
- **Call sites** — `?.` dropped from `examples/slack`, `examples/teams`,
both READMEs, and the nine `showcase/shell-docs` channel pages.
## Deliberate scope choices, called out
- **Express/Hono keep an optional `.channels`.** Only their TSDoc is
corrected here. Their own type docs name Node as the lifecycle-owning
surface and attach `.channels` best-effort, so the branded overload is
Node-only for now; `endpoints-channels.test.ts` still uses `!` for those
two. Say the word if the overload should extend to them.
- **The non-optional shape requires a literal `channels` tuple**
(`readonly [Channel, ...Channel[]]`). A runtime built from a
dynamically-assembled `Channel[]` is unbranded and still needs `?.`. Now
stated in the node TSDoc.
- **`examples/slack/app/managed.ts` now exits nonzero if activation
fails**, where before it stayed up serving HTTP with nothing connected.
Intentional — fail loud, and it matches `index.ts`. Note that `ready()`
resolves for `setup_required`, so a declared-but-unprovisioned channel
still logs as started.
- **Signal handlers are registered before awaiting activation** in
`managed.ts`, so a Ctrl-C inside the 30s activation window still tears
the Channel down instead of hitting Node's default handler.
## Verification
- **Type contract, red → green:** the new `KeyIsRequired<typeof
listener, "channels">` assertion in `handler-channels-types.test.ts`
failed to compile before the overload (`error TS2344: Type 'false' does
not satisfy the constraint 'true'`) and passes after.
- **Example bug, red → green:** stashing only `managed.ts` fails the new
guard with `expected "vi.fn()" to be called once, but got 0 times`.
- **Strict-null proof:** `slack-example` and `teams-example` both `tsc
--noEmit` clean under `strict: true` with the `?.` removed. This matters
because the runtime package compiles with `strict: false`, so its own
type test can only probe the optionality modifier structurally.
- Runtime channel suites 54/54; slack example 63/63.
- **Coverage limit:** the `managed.ts` guard is mocked — it proves the
example *calls* `ready()` with a bound, not that a Channel connects.
Nothing in CI exercises a real gateway connect for these examples.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Realigns the inspector/memory work onto the banking demo as it shipped in
#6136 (ChatGPT-style shell, gen-UI beats, durable-memory self-learning) and
#6202 (README refresh).
All six conflicts were the same collision: this branch removes the bespoke
Glass Engine inspector, while #6136 kept and rebuilt around it.
- run-handler.ts: kept both sides (our CopilotKitCoreCatalogComponent and
main's MAX_FOLLOW_UP_DEPTH landed at the same spot).
- wrapper.tsx / layout.tsx: took main's rewritten provider tree and
right-hand icon rail, minus the Glass Engine providers, pane, and
telescope toggle. Also dropped main's `padClass` (it reserved space for
the Glass pane and referenced a now-removed `glassActive`) and
`<ProactiveNotice />` (main removed it; the import is already gone).
- memory-tab.tsx, lib/intelligence/memory.ts: confirmed the deletions.
Their only remaining importers were the bespoke inspector and the
banking-local /api/memories routes, all removed here. seed-memories.ts
is unaffected: it POSTs to INTELLIGENCE_API_URL, not the local route.
- README.md: kept our product-inspector section over main's Glass Engine
availability/activation prose, and documented the Capabilities tab.
Drive-by fixes to comment rot the migration created: user-id.ts and the
copilotkit route doc comments referenced the deleted Memory-panel proxies,
and the README pointed the presenter-reset control at the removed
telescope toggle.
Also replaces a literal NUL byte in capabilityKey() with a unicode escape.
The raw control character made tsc/grep/diff treat run-handler.ts as a
binary file, which hid this very merge's conflict markers from grep.
Behavior is unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up to #6136, which merged while this was being written. That PR
changed the demo substantially and the README still described an older
build.
## Corrected
| Was | Now |
| --- | --- |
| `CopilotPopup` | Left-docked `CopilotSidebar`, two columns (thread
rail + conversation), app nav rail on the right |
| learned memory is `procedural` | `operational` — what the code
actually writes |
| Memory tab is "top-k recall, not a full enumeration" | "Stored
memories (N)" — a complete enumeration aggregated across identities;
semantic search is a separate box |
| per-run reset is "a separate follow-up" | Ships behind
`PRESENTER_RESET_ENABLED`; also re-seeds memory and returns to the bare
root |
| `showTransactions` is a `useFrontendTool` | `useComponent`, so it
persists in the transcript |
| "change my Visa PIN from the team page" via navigation | PIN is a
component in the chat; the agent never receives the digits |
## Added
- The **eight-pill demo script** as a table, with which beats need
Intelligence mode
- The `/charges` page and the **PDF / Q2 multimodal** beat
- **Seeded memory** — and why the over-limit procedure is deliberately
*not* seeded (seeding it would remove the teach beat entirely)
- The **components-not-markdown-tables** rule, including why `showTable`
renders nothing below two rows
- Per-team chart colour via `teamColor()`
- The two **incompatible helper families** in `actions.ts` —
destructuring `ok` off the wrong one reports a success as a failure,
which bit twice during #6136
## Screenshots
All three replaced. They showed the old right-docked chat layout, so
they contradicted the first paragraph of the README. New captures are
from the shipped build (filed report dashboard, chat with components +
tool-activity line, recording vignette).
Docs only — no source changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`CopilotKitIntelligence` required `apiUrl` and `wsUrl` on every construction,
so the two correct hosts had to be found and copied by hand — which is how an
agent came to invent them. Both now default to CopilotKit's managed platform,
making `new CopilotKitIntelligence({ apiKey })` the whole managed-service setup.
Overrides are unchanged for self-hosted and non-production deployments, with two
guards that the previous required-field signature made unnecessary:
- A blank value counts as unset. These URLs are usually wired from env vars, and
a declared-but-empty variable arrives as `""`, which would otherwise produce
host-relative requests instead of falling back to the managed platform.
- Setting only one of the pair warns. The API and realtime planes are separate
hosts, so a lone override silently splits the client across two deployments —
and that failure surfaces as a hang, not an error.
Sweeps the doc, skill, README, and example surfaces to the short form so the
copy-paste path no longer hands anyone URLs to get wrong, and reattaches the
`CopilotKitIntelligence` class JSDoc, which was orphaned above an interface and
so never appeared on hover.
Linear: OSS-638
`createCopilotNodeListener` now mirrors `createCopilotRuntimeHandler`'s branded
overload pair, so a runtime with at least one declared Channel yields a listener
whose `.channels` is non-optional and the documented `listener.channels.ready()`
call type-checks with no `!` and no `?.`. `activateChannels: false` and
channel-less runtimes keep the optional shape. Both listener types are exported
from `@copilotkit/runtime/v2/node`.
Corrects TSDoc on the node, express, and hono wrappers that still claimed
activation happens "at creation time" and labelled `ready()` as optional — stale
since activation was deferred to make the Fetch handler serverless-safe. On a
long-running host that call is required, not optional.
Fixes a live consequence of that stale model: `examples/slack/app/managed.ts`
never called `ready()`, so it mounted a listener, logged "started managed
Channel", and connected nothing. Covered by a regression assertion.
Drops the now-unnecessary `?.` from the examples, READMEs, and channel docs, and
adds compile-time contracts for the listener shape alongside the existing
handler ones. The examples compile with `strict: true`, so they prove the
`?.`-free call under strict null checks, which the runtime package (strict:
false) cannot.
#6136 changed the demo substantially and left the README describing an older
build. Corrections:
- It documented CopilotPopup; the chat is a left-docked CopilotSidebar rendering
two columns (thread rail + conversation) with the app's nav rail on the right.
- It called the learned over-limit memory `procedural`; the code writes
`operational`.
- It said the Glass Engine Memory tab was "top-k semantic recall, not a full
enumeration". It is now a complete enumeration ("Stored memories (N)"),
aggregated across the demo's identities, with semantic search as a separate
box.
- It listed per-run reset as a future follow-up. It ships, gated behind
PRESENTER_RESET_ENABLED, and also re-seeds memory and returns to the bare root.
- It described showTransactions as a useFrontendTool; it is a useComponent so it
persists in the transcript.
- The "change my Visa PIN from the team page" example described a navigation
fallback; PIN entry is now a component in the chat and the agent never sees the
digits.
Adds what was missing: the eight-pill demo script, the /charges page, the PDF/Q2
multimodal beat, seeded memory (and why the over-limit procedure is deliberately
NOT seeded), the components-not-markdown-tables rule, per-team chart colour, and
the two incompatible helper families in actions.ts that make destructuring `ok`
report a success as a failure.
All three screenshots are replaced — they showed the old right-docked chat, so
they contradicted the first paragraph.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes the Channels docs/examples pointing at an Intelligence host that
does not serve the API, and the websocket URL guidance that can never
produce a working prod value. Linear:
[OSS-621](https://linear.app/copilotkit/issue/OSS-621).
## The two bugs
**1. The documented host does not serve the API.** Probed every
plausible path, not just `/`:
| URL | Result |
| --- | --- |
| `api.copilotkit.ai` — `/`, `/api`, `/api/health`, `/health`,
`/api/threads`, `/api/v1/threads` | **404 on all**, `server:
awselb/2.0`, `content-length: 0` — an ALB with no target-group rule
behind it |
| `realtime.copilotkit.ai` | **no DNS record at all** |
| `api.intelligence.copilotkit.ai/` and `/api/health` | 200,
`x-powered-by: Express` |
| `api.intelligence.copilotkit.ai/api/threads` | **401** — a real,
auth-gated endpoint |
| `realtime.intelligence.copilotkit.ai/runner/websocket` | **403** —
mounted and auth-gated (a 404 would mean unmounted) |
So the documented host is not merely returning 404 at the root — nothing
is routed there on any path, and it is not the app (no `x-powered-by`).
The working pair, matching the CLI's baked-in prod defaults and
`gitops/environments/prod/values.yaml`, is
`https://api.intelligence.copilotkit.ai` +
`wss://realtime.intelligence.copilotkit.ai`.
**2. `wsUrl` was documented as derivable from `apiUrl`.** Prod splits
the API and realtime planes across *different hosts*, so a scheme-only
swap yields `wss://api.intelligence.copilotkit.ai` — wrong host. That
failure is silent: a wrong `apiUrl` returns a clean HTTP error, but a
wrong `wsUrl` sits in `connecting` until the settle timeout and reports
only "did not settle in time". The derive is not even correct locally,
where the API and gateway are on different ports (4201 vs 4401) and the
swap preserves the port. It is essentially never right, so it is deleted
rather than relabelled.
Demonstrated end to end rather than asserted — running `examples/teams`
with the WS URL unset:
```
# on main: derive(https://api.intelligence.copilotkit.ai) -> wss://api.intelligence.copilotkit.ai (wrong host, 30s hang)
# on this branch:
exit code: 1
Missing COPILOTKIT_INTELLIGENCE_WS_URL.
export COPILOTKIT_INTELLIGENCE_URL=https://api.intelligence.copilotkit.ai
export COPILOTKIT_INTELLIGENCE_WS_URL=wss://realtime.intelligence.copilotkit.ai
The API and websocket URLs are DIFFERENT hosts (api.… vs realtime.…), so
the websocket URL cannot be derived from the API URL — set both.
```
## Scope note
The ticket listed 8 sites; the actual blast radius was 30 across 22
files. Beyond the ticket's list:
- **Five more channels package READMEs** — `channels`, `channels-core`,
`channels-discord`, `channels-slack`, `channels-teams` (the ticket named
only telegram + whatsapp).
- **The live product docs** —
`showcase/shell-docs/src/content/docs/channels/` taught the broken
derive in 8 files. These escaped the ticket's grep because their host
was already a `your-intelligence-url` placeholder; only bug 2 was
present. This is the surface developers actually read.
- **A generated skills mirror** — `skills/runtime/` is produced from
`packages/runtime/skills/runtime/` by `pnpm sync:plugin-skills`; fixing
one without the other leaves the bug live and fails the
`check-plugin-skills` gate.
- `skills/copilotkit-debug/references/runtime-debugging.md` and a third
occurrence in `client.ts`.
## Acceptance item 4 — resolved, chain verified
The ticket flagged a contradiction: `realtime-gateway.ts` documented
`wss://gateway.example/socket` while the runtime skill listed `/socket`
as a mistake. **The skill is right**, confirmed by tracing the real
runtime path rather than inferring it:
1. `channel-activation-config.ts:145` — `const wsUrl =
intelligence.ɵgetRunnerWsUrl()`, i.e. base + `/runner`.
2. → `channel-manager.ts:237` `wsUrl: config.wsUrl` →
`startChannelsOverRealtimeGateway` → `connectRealtimeGateway`.
3. `realtime-gateway.ts:253` hands that to Phoenix's `Socket`, which
appends `/websocket`.
4. The gateway mounts exactly `/runner` and `/client`
(`realtime_gateway/endpoint.ex:10,17`). There is no `/socket`.
The two docs survived contradicting each other because they describe
**different layers**: the public `wsUrl` is a bare base, while
`connectRealtimeGateway` receives the already-derived runner URL. Its
doc comment and the test fixtures move to `/runner` and now name the
layer. Independently, `get-runtime-info.ts:93` uses `ɵgetClientWsUrl()`,
which confirms the `/info` sample in the debug skill correctly keeps its
`/client` suffix — only the host there was wrong.
## Acceptance item 3 — split out
Failing loudly instead of hanging is a behavior change in the launcher's
connect path that overlaps OSS-622's error classification, so it is
[OSS-623](https://linear.app/copilotkit/issue/OSS-623) rather than
smuggled into a docs fix.
## Verification
- 44/44 gateway tests in `packages/channels-intelligence`;
`examples/slack/app/managed.test.ts` passes; `examples/teams` typechecks
clean.
- `nx run-many -t test,publint,attw` across the 15 affected projects
passed via the pre-commit hook; full CI green (38 checks, including
`build-check (shell-docs)`).
- Behavior proven by running the example, not by mocks (above).
- 45 commits behind `main` at time of review with **zero overlap** on
changed files, and the diff covers every dead-host and derive site
present on *current* `main`.
- No logic changes in either published package — `client.ts` and
`realtime-gateway.ts` are JSDoc/field-comment only. Behavior changes are
confined to the three example apps.
## Self-review pass
An adversarial pass over this PR tried to falsify its central claim
(that `api.copilotkit.ai` does not serve the API) by probing non-root
paths — the evidence got stronger, not weaker. It also cleared a
suspected regression: three `channels-intelligence` files read
`COPILOTKIT_INTELLIGENCE_URL` without the WS var, but they are the
HTTP-only transport path and need no socket URL. Two genuine gaps it
*did* find are fixed in the last commit: the five platform pages had
lost deployment neutrality (managed hosts now carry a self-hosted note),
and `index.mdx`/`mcp.mdx` referenced the new variable without telling
the reader where it comes from.
**One product decision for the reviewer:** `api.copilotkit.ai` is a live
ALB that routes nothing. If it is meant to become the public API alias,
this PR is documenting the wrong long-term string and the ALB wants a
listener rule instead. I used the host the product actually hands users
today (CLI prod defaults + gitops).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Turns the Northwind Finance banking demo into a presentable end-to-end demo.
Every beat was driven live and verified against real app state, not just the
agent's own claims.
The demo is eight suggestion pills, in order: show a chart; change a card PIN in
an interactive in-chat component; ask what is on screen; open the 10 most
expensive charges; prep the Q2 report from an attached PDF invoice; summarize
spend using a remembered preference; handle an unrecognized charge using a
remembered procedure; and finally learn a brand-new procedure by watching the
officer clear an over-limit charge. The last three are sequenced deliberately:
already-knows-a-preference, already-knows-a-procedure, learns-a-new-one — and on
a fresh thread the newly learned procedure is recalled and applied to a
different charge unaided.
Shell and UI:
- Chat docks LEFT as two columns (thread rail + conversation) with the app's
icon rail moved right via flex `order`, so nav still precedes <main> in the
DOM. The chat surface runs ChatGPT's neutral palette while the app keeps its
violet identity.
- Tool activity renders as ChatGPT-style expandable lines; internal AG-UI
plumbing is filtered out of the transcript.
- Tabular data always renders as components, never markdown tables; a one-row
table is blocked structurally because a single-record table is narration.
- Filed reports are a dashboard (KPI tiles, three charts, decision rows) rather
than a memo, with one colour per team across every chart and ranked bars in
place of a donut that collapsed when an invoice pushed one team to 96%.
- PIN entry happens in the chat component; the agent never asks for or receives
the digits.
Fixes worth review attention:
- Anything a RECALLED procedure can call must be registered globally. Four
action tools were registered on the cards route, so a saved procedure only
worked from one page.
- An unanswered human-in-the-loop card poisons the thread: skipping it left an
unresolved tool call and the next message failed with "Tool result is missing
for tool call ...". The procedure's closing step no longer gates; approvals
that move money still do.
- actions.ts has two helper families that are not interchangeable —
openPolicyException / finalizePolicyException / changeTransactionStatus return
{ok, data/error}, while changePin / addNoteToTransaction resolve with the
updated record. Destructuring `ok` off the second family reports success as
failure.
- A stale canvas surface hid the page: it is derived from the last a2ui-surface
message and replaces the page body until dismissed, but the dismiss effect
keyed on pathname only, so a ?tab= switch left it covering the page.
- Suggestion pills ignore clicks while a run is in flight, since a double-click
orphaned the previous run's tool call.
Deliberately NOT included: the @copilotkit/license-verifier pnpm patch (local
workaround for the "Invalid license token" banner) stays uncommitted, because it
patches a dependency for every install and is not demo scope.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Slack and Teams examples derived COPILOTKIT_INTELLIGENCE_WS_URL from
COPILOTKIT_INTELLIGENCE_URL with a scheme-only swap when it was unset, and
documented it as optional. That derive preserves host and port, so it is only
correct on a deployment that puts both planes behind one host+port — which is
neither prod (api.intelligence… vs realtime.intelligence…) nor local dev (4201
vs 4401). In practice it was always wrong, and wrong in the worst way: the
resulting join hangs in `connecting` for 30s and reports only a timeout.
All three call sites (slack native, slack managed, teams) now require both
URLs explicitly and deriveWsUrl is gone. The Teams startup error names both
vars and says why one cannot be computed from the other.
managed.test.ts previously deleted the WS var to exercise the derive; it now
sets a host+port deliberately different from the API URL, so the test encodes
that the planes are deployed apart rather than assuming they are not.
Refs OSS-621
Writing a per-hook threadId onto an agent resolved by agentId alone mutates a
shared singleton, so two useAgent calls that share an agentId clobber each
other's thread (review feedback from @mme). Require runtimeAgentId when threadId
is provided: the hook then registers a private proxied agent (agentId ->
runtimeAgentId via CopilotKitCore.registerProxiedAgent) and scopes the threadId
to that instance instead of a shared one. Register/unregister run as one
balanced, StrictMode-safe effect, exposing the proxy via state so the hook
swaps from the provisional stand-in deterministically. Passing threadId without
runtimeAgentId now throws. Updates the React Native demo to the new API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Two issues found after the example merged:
1. The closed chat panel is only translated off-screen, so its inputs/buttons
stayed in the tab order and the a11y tree. Mark the <aside> `inert` while
closed (`[attr.inert]`), so focus/AT skip it; removed when open.
2. The dev web inspector (and its @copilotkit/web-inspector dep, ~660 kB) shipped
in the production initial bundle. Gate it behind `@defer (when isDev)` so it's
split into a lazy chunk that a prod build (isDev=false) never loads.
Verified: prod `ng build` initial bundle 4.59 MB -> 3.93 MB with web-inspector now
a lazy chunk; dev serve still mounts the inspector; closed-panel focus is blocked
(activeElement falls back to body), open panel unaffected.
When the viewport is wide enough (>=1200px) the chat starts expanded and DOCKS:
.layout--pushed adds a right margin equal to the chat width so the proverbs
content reflows beside the chat instead of being covered (mirrors React's
CopilotSidebar margin-inline-end push). Below the breakpoint the chat starts
closed and OVERLAYS on open, so narrow screens aren't smushed.
- DOCK_BREAKPOINT_PX (1200) gates the initial open state; a matching
@media (min-width:1200px) rule applies the push margin
- --chat-width var keeps the panel width and push margin in sync
Verified live: 1400px starts expanded, content pushed beside the chat (right
929 <= chat left 960), 0 overflow; 1000px starts closed, opening overlays with
no push and no overflow.
Previously the FAB swapped to an X when open and sat above the panel, so an open
chat showed TWO X's (header + bottom-right). React never does that: its toggle
FAB is always a MessageCircle, and the sidebar (z-1200) covers the FAB (z-1100)
when open, so the only visible close is the header X.
- FAB icon is always MessageCircle (no X swap)
- chat panel z-index 1200 > FAB 1100, so an open panel covers the FAB
Verified live: OPEN shows only the top-right header X (FAB covered); CLOSED shows
the bottom-right MessageCircle FAB to reopen. No bottom-right X in either state.
Replace the hand-rolled unicode/emoji controls with lucide icons matching React:
- header close = lucide X (in a title-left/close-right, border-b bar like CopilotModalHeader)
- toggle FAB = lucide MessageCircle (open) / X (close), ALWAYS visible, fixed
bottom-6 right-6, h-14 w-14, rounded-full, primary-dark — matches CopilotChatToggleButton
Verified live: FAB shows X when open / MessageCircle when closed and stays visible
(React's toggle behavior); close + FAB both drive chatOpen; 0 console errors.
Replaces the permanent 420px chat column with a collapsible slide-over panel
(toggle FAB + close button), mirroring React's CopilotSidebar. The chat now
OVERLAYS rather than reserving a grid track, so the threads drawer's desktop
push no longer smushes the content and the medium-width layout has room.
- app.ts: 2-track grid (drawer + content); fixed .chat slide-over + open FAB;
chatOpen signal (defaults open, like React defaultOpen). Theme var hoisted to
the component HOST so the weather card (now in the fixed chat) still inherits it.
- styles.css + web-inspector.ts: move the dev inspector FAB to bottom-left; its
default top-right anchor (max z-index, transform-positioned) sat over the chat's
close button and ate the click.
Verified live (real Gemini key): open/close/reopen slide-over; content 880px at
1200w (no smush); 0 horizontal overflow + no clipping at 580/1050/1200; inspector
clears both the close button and open-FAB; 'set theme to teal, then weather in
Paris' recolors the panel AND renders a teal-themed weather card in the floating chat.
The proverbs card is width:100% + padding:2rem; with the default content-box the
padding was added ON TOP of 100%, making the card wider than its column and
getting clipped by the layout's overflow:hidden (visible as cut-off content on
narrow/medium screens). React/Next examples get border-box from Tailwind's
preflight; the Angular app had no global reset.
Verified live at 580px (card 578px, no clip, 0 overflow) and 1050px (card fits
its grid track, no overlap with the chat). Matches the React adk's overflow-free
responsive behavior.
setThemeColor (and any frontend-registered tool) never reached the LLM: the ADK
agent's tools were static [set_proverbs, get_weather], and ag_ui_adk only injects
the run's forwarded client tools when the agent's tools include an AGUIToolset
placeholder (it swaps it for a ClientProxyToolset wired to input.tools). Added
AGUIToolset() so gemini-2.5-flash sees the forwarded setThemeColor and calls it.
Verified LIVE (real Gemini key): 'Set the theme to green' now recolors the panel
(--app-theme-color: green) — previously the agent declined ('I can only help with
proverbs or the weather'). Same root cause affects the React adk example (fixed
in a companion change).
The get_weather generative-UI card never rendered: a CR round-3 "consistency"
tweak added agentId: AGENT_ID to the renderToolCalls entry, which scoped the
renderer such that the incoming (server-side) tool call didn't match and the
card was filtered out. The internal examples/v2/angular/demo renderer sets no
agentId; omitting it here restores rendering.
Verified LIVE against the running app (real Gemini key): the WeatherCard now
renders for 'Get the weather in San Francisco' (location + weather chrome),
matching the React adk reference. Caught by a live React-vs-Angular comparison
that the static CR loop could not.
(Note: setThemeColor NOT calling the agent is inherited — the React adk example
behaves identically; ag_ui_adk doesn't bridge forwarded client tools into the
ADK LLM tool set. Separate upstream concern, tracked as bucket-d follow-up.)
Mounts the framework-agnostic cpk-web-inspector web component (dev aid for
watching AG-UI events / agent state / runtime connectivity), handing it the
shared CopilotKit.core. Mirrors the internal examples/v2/angular/demo pattern —
@copilotkit/angular does not integrate the inspector via the provider the way
React's <CopilotKit inspectorDefaultAnchor> does. Matches the richer React
integration examples (langgraph-python et al.); a deliberate extra vs. the
leaner adk reference.
- add @copilotkit/web-inspector@1.63.1
- src/app/web-inspector.ts + <app-web-inspector /> in app.ts
- README file map updated
ng build green; oxlint 0/0.
- app.ts: setThemeColor comment notes it also recolors the weather card
- server.ts: identifyUser comment notes the id must exist in the platform
- README: document that INTELLIGENCE_API_KEY must accompany COPILOTKIT_LICENSE_TOKEN
Comment/doc only — no behavioral change.
- proverbs.ts: latch the seed one-shot UP FRONT (per agent instance) so a first
snapshot with defined proverbs can't leave the effect subscribed and re-seed on
a later transient undefined; spread existing state in the seed too. Comment now
states the new-thread-hydration race honestly (inherited from React's [agent])
- server.ts: fix the runtime port at 8200 (matches the hardcoded UI runtimeUrl);
don't read env PORT — resolves both the client/server port drift and the
shared-PORT collision with the Python agent
- app.config.ts: pass agentId to the get_weather renderToolCalls entry (parity with
the setThemeColor frontend tool; future-proofs multi-agent)
- app.ts: correct the theme comment (center panel themes via its input, not the var)
Verified: ng build green; oxlint 0/0. Deferred (bucket c/d, inherited from React adk):
Intelligence empty-API-key fail-loud, .sh||.bat + dev:debug + postinstall-uv script
patterns, demo-user stub, dead ProverbsState — aggregated for a cross-example follow-up.
- app.ts: theme via a demo-specific --app-theme-color, not --copilot-kit-primary-color
(the chat re-declares that token on [data-copilotkit] hosts, shadowing the layout
value, so the weather card never themed — round 1's fix was ineffective)
- app.ts: mobile overrides the desktop 100dvh on .center/.chat so the chat row isn't
starved to 0 and clipped (regression introduced by round 1's mobile stack)
- proverbs.ts: seed ONCE per agent instance (mirror React [agent]) instead of on every
state emission — stops transient undefined mid-run/on thread-restore from re-seeding
- proverbs.ts: remove() spreads existing state (setState is a full replace; was wiping
any non-proverbs state)
- server.ts: read RUNTIME_PORT (not the shared PORT the Python agent also reads)
- main-content.ts: drop the redundant/shadowed --copilot-kit-primary-color binding
- weather-card.ts: location required (matches React/backend) + 'Weather' fallback label
- real favicon.ico (was a 12-byte stub); README file map + Node 20.19+/≥22 notes
Verified: ng build green; oxlint 0/0. Refuted in review: no dual @copilotkit/core
(published angular@0.2.0 depends on core/shared@1.63.1 exactly).
- server.ts now loads .env (import dotenv/config) so COPILOTKIT_LICENSE_TOKEN /
INTELLIGENCE_* / AGENT_URL / PORT are honored — Intelligence/threads were
unreachable before (tsx does not auto-load .env like Next.js did)
- .env.example: drop PORT=8200 (the agent's load_dotenv read it and bound 8200,
colliding with the runtime); align AGENT_URL trailing slash
- app.ts: set --copilot-kit-primary-color on the .layout root so setThemeColor
reaches the chat-rendered weather card (was scoped to .main-content only)
- app.ts: mobile stacks center+chat instead of hiding the chat (kept assistant reachable)
- proverbs.ts: re-seed on every fresh thread (drop lifetime latch); mirror React
- agent-state.ts: proverbs?: string[] (code depends on undefined-before-seed)
- package.json: drop unused hono/@hono/node-server/@angular/forms/@angular/router; add dotenv
- .gitignore: !.env.example so the tracked template can't be dropped by the .env* rule
- scripts/run-agent.bat: quote cd path + exit guard (match setup-agent.bat)
- agent/main.py: fix garbled prompt rule 5, stray quote, wether/provers/incude/recipe typos
- README: Intelligence path needs Node >=22 + a real provisioned user (not demo-user)
Verified: ng build green; server.ts binds .env PORT (dotenv loads); oxlint 0/0