Three names for one value were live in CopilotKit's own documentation, and
following the wrong one with a CLI-provisioned project yields an undefined
key:
- `INTELLIGENCE_API_KEY` — what `copilotkit project select` writes, used by
all 34 integration examples and the docs site.
- `COPILOTKIT_INTELLIGENCE_API_KEY` — the seven Channels package READMEs and
the packaged skills. Nothing ever read it.
- `COPILOTKIT_API_KEY` — the Slack and Teams examples, and the TSDoc on
`CopilotKitIntelligence` itself, which is what an IDE shows on hover.
`INTELLIGENCE_API_KEY` wins, because it is the name the CLI provisions and
changing it would break every scaffolded project in the wild.
`COPILOTKIT_INTELLIGENCE_API_KEY` is retired outright — no code read it.
`COPILOTKIT_API_KEY` stays readable as a deprecated alias in the two
examples that consume it, so an existing `.env` keeps working, and is
documented as deprecated everywhere it appears.
The skills reference also documented `organizationId`, sourced from a fourth
and fifth env name, as a `CopilotKitIntelligence` option. It is not one:
`CopilotKitIntelligenceConfig` has no such field, so the copy-pasteable
sample it appeared in would not compile. Removed from the samples, and the
prose that told readers to fetch a value for it corrected.
The Intelligence wiring itself was published only inside
`node_modules/@copilotkit/runtime/skills/`, and the only docs pages showing
`CopilotKitIntelligence` were the two Channels frontends — so a developer on
the plain web path had no page to reach it from. Adds
`/premium/connect-your-runtime`, which covers the wiring, how to confirm the
credential is actually consumed, and the self-hosted two-URL rule.
`scripts/validate-intelligence-env-names.ts` keeps this from drifting back.
It runs unfiltered in CI on purpose: the two workflows that would otherwise
cover it filter paths, and static/quality ignores `examples/**` — exactly
where the deprecated alias lives.
Drop the defensive parentheticals around Intelligence pricing; say
"available on a free plan" and move on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Note in the Channels overview and package READMEs that building your own
channel runner on the open-source SDK primitives is a supported path with
no CopilotKit Intelligence dependency; teams choosing it own their state,
persistence, concurrency, locking, retries, and race-condition handling.
Intelligence remains the managed runner, with analytics, learning, and
governance in addition.
Also updates the production self-hosting note: Enterprise Intelligence can
be fully self-hosted today, onboarding guides are still to come.
Refs FAC-155
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What does this PR do?
Adds the CopilotKit consumer side of ENT-1173 across Shared, Runtime,
Core, Web Inspector, and the existing Shell Docs pages.
- Defines and parses optional trusted Inspector metadata for identity,
plan, license, action, usage, and expiry. Runtime proxies it through a
private, failure-isolated route, and Core refreshes it without changing
connection state.
- Groups Inspector navigation into Threads, Agents, and Learning.
Threads renders finite, unlimited, unknown, overage, and expiring usage
states plus matching trusted plan or license actions.
- Keeps explicit `threadEndpoints` as the only authority for Thread
requests. Locked or absent capability states make no list, subscription,
detail, message, event, or state calls.
- Keeps the zero-thread video, three example Threads, detail tabs, and
guided tour in empty and locked states. General Intelligence remains the
default onboarding path; only trusted `team_self_hosted` metadata uses
self-hosted onboarding.
- Gives an active license with missing Runtime routes a short **Finish
setting up Rich Threads** state. Users can copy a safe coding-agent
prompt or open the public Runtime setup guide. The same copy control
appears in that guide, and raw Markdown/LLM views include the full
prompt.
- Keeps finite usage green below 90%, orange from 90% to the limit, and
red at or above the limit. At 90%, a trusted plan action changes from
**Manage Your Plan** to a purple **Upgrade Your Plan** without changing
its trusted URL, action kind, or telemetry contract.
- Adds a deterministic 33-state loopback lab for CopilotKit developers.
It has no production route or export, is absent from public docs and
package metadata, and is excluded from the npm tarball.
`Expiring Soon` is display-only; this PR does not enable the thread
culler. Managed Enterprise receives no manage-plan action, and Team
Self-Hosted receives no hosted plan action. Optional metadata and the
additive expiry field remain compatible across mixed producer, Runtime,
Core, and Inspector versions.
A small Channels test-only change updates fetch mocks for current
TypeScript types. It changes no Slack or Teams docs or runtime behavior.
## Related PRs and issues
- Refs
[ENT-1173](https://linear.app/copilotkit/issue/ENT-1173/ship-plg-ready-inspector-navigation-metadata-and-locked-threads)
- Producer:
[CopilotKit/Intelligence#696](https://github.com/CopilotKit/Intelligence/pull/696)
## Validation
- `@copilotkit/web-inspector`: 20 files and 372 tests passed; typecheck
and production build passed.
- Shell Docs: 57 files and 383 tests passed; lint, typecheck, and
production build passed. The build generated all 222 static pages.
- Browser checks cover the copy-prompt flow, unchanged white **Manage
Your Plan**, purple **Upgrade Your Plan**, orange 4,500/5,000 usage, and
red 5,000/5,000 usage.
- Independent review found no Critical or Important issues.
- The broader Runtime, React Native, Channels, package-quality,
compatibility, and Node-version checks from the prior pushed head remain
green.
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] I updated the relevant documentation
- [ ] "Allow edits by maintainers" is checked
Review follow-up: resolve the default prompt after the implicit inbound
prompt, so real user input outranks the welcome default and the
implicit-inbound-consumed flag can never mark a turn consumed that was
never injected. Pin the seeded-store welcome path (all shipping
adapters) and the inbound-over-default precedence with tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isolateAgentInstance threw when an agent's clone() did not carry the subclass's
own fields. That rejected every turn on a Channel built with LangGraphAgent --
its clone() drops emittedToolCallStartIds and eventsStreamActive -- so the
reference starter could not answer a single message.
The refusal was wrong because whether a dropped field matters depends on what it
HOLDS, and the check cannot see that:
- Config read during the run and never rewritten (an auth client, a URL) does
gut the agent when it is lost.
- Per-run scratch state is re-initialized at the start of every run, so losing it
changes nothing. LangGraphAgent's two fields are exactly this: both are reset
when a run binds its subscriber, before anything reads them.
The tell is that the identical clone happens on every ordinary runtime request --
agent-utils.ts clones per request for SSE and Intelligence alike -- and has never
caused a problem. Channels differed only in asserting at clone time, before the
run that would have repopulated the fields. Confirmed against a real Slack round
trip: with the throw downgraded, the same Channel that could not take a turn ran
the agent and replied.
So it warns and continues, naming the fields and both readings. The check still
earns its place: A2AMiddlewareAgent's base clone() drops orchestrationAgent,
agentClients and agentCards, which are config, and that is worth seeing.
Deliberately not done: copying the dropped fields onto the clone. That shares one
mutable object across concurrent turns, the exact hazard the isolation exists to
prevent.
Upstream fix to follow in @ag-ui/langgraph, whose clone() should carry them.
Resolves [OSS-641](https://linear.app/copilotkit/issue/OSS-641). Mike's
report: *"You have to `await channels.ready()` for it to connect to the
Realtime Gateway. Seems like there's some clunkiness to creating the
runtime and getting it connected."* He then picked the fix: *"I think it
should autostart in the long running wrappers."*
## What changes
**`createCopilotNodeListener` and `createCopilotExpressHandler` start
activation at creation.** A declared Channel connects because it was
declared; `channels.ready()` becomes await-and-observe rather than the
call you must remember. Failure-mode asymmetry is the argument:
forgetting `ready()` today gives you a process that serves HTTP, looks
healthy, and is silently disconnected with **zero output**, while
auto-start's worst case is an activation error in the logs.
**`createCopilotRuntimeHandler` and `createCopilotHonoHandler` stay
lazy.** The generic Fetch handler is the serverless/edge entry point —
isolates freeze and recycle per request, so separate cold starts would
mint competing listeners for the same Channel (the reason activation was
deferred in `fbf35ac59` in the first place). Hono keeps that behavior
because it is our Next.js App Router surface in practice: every route
handler in `examples/showcases/*` (banking, mcp-apps,
generative-ui-playground, oracle-agent-memory) plus the vue/nuxt demo
builds one at module scope. Its TSDoc now states why, loudly, so nobody
"finishes the job" later.
`activateChannels: false` remains the clean opt-out that opens no
socket.
## Consequence for host code: the shutdown boundary moves earlier
Signal handlers must now 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. `examples/slack`, `examples/teams`, and the docs snippets are
restructured to wire teardown before the listener exists (a
`stopChannels`/`teardown` binding assigned in the same tick as
creation). **Worth calling out in the changelog** — it is the general
hazard for any user code that registers shutdown after mounting.
## Failure semantics
Fire-and-forget by necessity, since a factory is synchronous. Set-level
failures log at `error`; per-Channel failures keep their existing `warn`
breadcrumbs; an up-front misconfiguration (duplicate/missing Channel
names) now surfaces as a logged error at creation rather than a throw
out of the factory — the factory still never throws. `ready()` stays
idempotent and one-shot, so a host that *does* await it observes this
activation's outcome, including its rejection, rather than triggering a
second one.
## READMEs
Every `channels-*/README.md` quickstart built the *generic* handler and
needed `await handler.channels.ready()` — for a socket-mode Slack bot, a
request handler you construct and never serve, which is likely closer to
what actually felt clunky. All seven now use the Node listener, so they
inherit auto-start and agree with the docs-site quickstarts. No new
public surface: a bot-only `startChannels(runtime)` host was the
alternative and is deliberately not taken here.
## Testing
- **`packages/runtime` unit suite: 1815 passed / 128 files** (`npx
vitest run`), including 9 tests in `endpoints-channels.test.ts`
covering: auto-start on node + express; Hono still lazy;
`activateChannels: false` opens no socket; a failed auto-start logs
instead of leaving an unhandled rejection (asserted via an
`unhandledRejection` listener) and the reason survives to a later
`ready()`; a duplicate-name misconfig logs without throwing; and two
wrappers over one runtime activate once (the per-runtime manager cache
is load-bearing now that *construction* activates).
- **`examples/slack`: 63 passed / 12 files; `examples/teams`: 2 passed /
1 file** (`npm test` in each).
- **Typecheck clean:** `examples/slack` and `examples/teams` (`tsc
--noEmit`), plus a full `@copilotkit/runtime` tsdown build.
- **Lint/format clean:** `oxlint` reports 0 findings in every changed
file (the 21 warnings in that run are pre-existing, all in untouched
example render/tool files), `oxfmt --check` passes on all 9 changed
source files.
- **Docs:** verified no stale lifecycle claims remain (`opens no
connection` / `ready() is required` / `control surface` guards) across
`docs/channels/**` and the Slack + Teams platform guides.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Strengthen the shared-instance-factory test to assert the user-visible defect
rather than object identity. Two overlapping turns through
`agent: (id) => shared` both read the one shared `messages` array, so each run
is prompted with the other user's question too. Against main's source the test
now reports exactly that:
expected [ 'first+second', 'first+second' ] to deeply equal [ 'first', 'second' ]
The symptom is asserted before the mechanism so a regression names the defect
instead of posing an object-identity puzzle.
This also settles what the original "only the first mention gets a reply" report
was: overlapping mentions dropped by the old `onLockConflict: drop` default plus
the managed per-thread exclusive gate, both already fixed in #6256. Both turns
do reply here; what was left was cross-contaminated context, not a dead turn.
Remove `ChannelAgentConcurrencyError`, unthrown since #6256. Its doc claimed it
was kept so older importers would not break, but it was never re-exported from
`channels-intelligence`'s entrypoint — `git log -S` over index.ts confirms it was
never reachable from outside the package, whose only export is `.`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`createChannel` resolved an agent per turn through `agentFactory`, which
cloned the singleton config but returned a factory's result raw. A factory
is free to hand back the same object every call — `agent: (threadId) =>
shared` — which is easy to write by accident and is what a singleton
becomes when someone needs the `threadId`.
Turn concurrency defaults to `"parallel"`, and only the managed adapter
serializes same-thread deliveries, so on a directly connected adapter two
turns in one conversation can run at once. On one shared instance they
corrupt each other: `messages` is a single array both runs append into, so
each run's new-message diff picks up the other's, and `isRunning` /
`activeRunDetach$` / `activeRunCompletionPromise` are single-slot fields
the second run overwrites while the first is still streaming. Managed
delivery instead serializes on object identity, head-of-line blocking two
different conversations that share one instance.
Clone for both shapes so the object a turn runs on is never one the caller
still holds. A fresh factory is unaffected beyond an unused instance.
Because cloning is now mandatory everywhere, add a guard for the failure it
introduces: `AbstractAgent.prototype.clone()` copies a fixed field list, so
a subclass declaring its own state gets it back as `undefined` with no
error — the base method always exists and returns a correctly-typed
instance. Comparing own enumerable keys catches that and names the dropped
fields. Own functions are exempt: assigning a method on the instance is how
spies and instrumentation wrap an agent, and losing that wrapper leaves the
prototype method intact.
Also reset `isRunning` and `abortController` on the clone as hygiene — not a
fix for a dead turn, since `runAgent` assigns a fresh controller before each
run and the run loop passes none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The PR claims wrapping the transport (rather than replacing the event
transform) preserves the stock AbortError -> RUN_ERROR conversion. That was
read off the @ag-ui/client bundle, not tested. Now it is: a mid-stream
AbortError surfaces as RUN_ERROR{code:'abort'} and the run resolves.
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.
@ag-ui/langgraph emits a TOOL_CALL_START whose parentMessageId is null --
notably the tool call that triggers an interrupt. The AG-UI schema declares
that field optional but never nullable, so HttpAgent's transform re-validates
the streamed event, Zod rejects it, and one rejected event aborts the whole
run, breaking human-in-the-loop.
Until that is fixed upstream (OSS-691), Channels tolerate it by default:
createChannel coerces the field on the wire, so no call site needs a special
agent class. Opt out with sanitizeAgentEvents: false.
Applied at the transport rather than by replacing the event transform, which
keeps protobuf content-type negotiation, the graceful AbortError -> RUN_ERROR
conversion, and strict validation of every other field -- all of which
SanitizingHttpAgent gives up. It also survives the per-run clone() of a
singleton agent, which an own-property run() override would not.
SanitizingHttpAgent is deprecated but unchanged, so existing code keeps
working. The examples and READMEs now wire up a plain HttpAgent.
## What
PR #6244 taught the Slack renderer to split a long reply across
continuation messages instead of silently truncating it. Its tuning was
hardcoded. Three of those constants are genuinely caller-dependent; this
exposes them through one `replyContinuation` option on both the direct
and managed surfaces.
```ts
// direct
slack({ replyContinuation: { maxMessages: 5 } });
// managed
createChannel({
name: "support",
replyContinuation: {
messageByteLimit: 11_000,
maxMessages: 20,
truncationMarker: "\n\n_…réponse tronquée._",
},
});
```
## Which constants, and why only these
| exposed | why it is a caller's decision |
| --- | --- |
| `messageByteLimit` | Slack's cumulative per-message ceiling is
**undocumented**. 11k is inferred from a single production datapoint
(11,607 bytes observed accepted) and deliberately conservative. If the
real ceiling differs by plan or workspace, an operator needs a knob, not
a release. |
| `maxMessages` | How many messages one reply may occupy is a product
decision, not a platform fact. 20 was chosen to bound a runaway (500k
chars → 46 messages); a support bot and an internal ops bot want
different answers. |
| `truncationMarker` | Hardcoded **English** copy posted into the
customer's channel. The one constant with no correct default. |
Deliberately **not** exposed, because they are correctness rather than
preference:
- `APPEND_CHAR_LIMIT` — a documented Slack per-call limit. A provider
fact; exposing it only invites `msg_too_long`.
- `MIN_MESSAGE_PROGRESS_BYTES` — loop-safety invariant. Exposing it lets
a caller reintroduce the unbounded-message bug #6244 fixed.
- `MAX_FENCE_LANG_CHARS`, `FINISH_DRAIN_ATTEMPTS` — internal heuristics.
If either is wrong that is a bug to fix, not a knob.
## Shape
Grouped under one nested option rather than three flat fields.
`maxMessages` sitting bare on a Channel reads ambiguously (thread
history?), and the group keeps the next continuation knob from adding
another top-level field. The trade-off is that it diverges from
`showToolStatus`'s flat precedent — happy to flatten if reviewers prefer
consistency over disambiguation.
The shared `ReplyContinuationOptions` type lives in `channels-core`, the
common ancestor of all four packages that touch it.
## Plumbing
Both surfaces follow `showToolStatus` exactly:
- **Direct:** `slack({ replyContinuation })` → `adapter.ts` →
`event-renderer.ts` → `NativeMessageStream`, covering both the renderer
path and `adapter.stream()`'s own stream.
- **Managed:** `createChannel({ replyContinuation })` → `Channel` →
`ChannelActivationConfig` → `channel-manager` →
`realtime-gateway-launcher` → `DeliveryAdapter` → the renderer's
`nativeStreaming` block.
**No gateway or Intelligence change is required.** Managed Slack renders
in the SDK process over a gateway live session and only emits
`slack.stream.*` effects; the Elixir `provider_executor` is a dumb
effect applier that owns no message boundaries. Render config therefore
never has to cross into Intelligence.
One non-obvious touchpoint: `channel-manager.ts` keeps a **hand-written
structural mirror** of the launcher's options, so the field has to be
declared there as well or the managed path type-drifts silently.
## Testing
```
channels-core 171 passed
channels-slack 318 passed
channels-intelligence 67 passed
runtime 1809 passed, 3 failed
```
The 3 runtime failures are pre-existing Gemini `AIMessage` filtering
tests, unrelated to this change — confirmed by re-running them with
these changes stashed on `main`. `check-types` passes for all four
packages.
New coverage:
- the marker override at the leaf (`native-stream`);
- the renderer's pass-through — this one fails without the change, since
the 11k/20 defaults would keep that reply in a single message;
- the managed chain end to end via `createChannel` → activation config →
launcher opts, plus the negative case that an unset option adds no
property anywhere.
## Follow-ups (tracked on OSS-689, not in scope here)
- Confirm the byte-vs-char question with a manual >12k
mostly-CJK/Cyrillic reply against a real workspace. It decides whether
`messageByteLimit`'s default is right; the option makes it adjustable
either way.
- Under the managed path's `minIntervalMs: 0` cadence a table row can
still be cut mid-row, so a continuation's re-emitted header is followed
by a malformed row.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Overlapping turns on the same conversation now run concurrently by default
so multi-user Slack threads get parallel replies. Singleton agents are
isolated via clone() per run; store.concurrency serial/drop remain opt-in.
PR #6244 taught the Slack renderer to split a long reply across continuation
messages, but its tuning was hardcoded. Three of those constants are genuinely
caller-dependent and are now configurable through a single `replyContinuation`
option; the rest stay internal on purpose.
Exposed:
- `messageByteLimit` — Slack's cumulative per-message ceiling is undocumented.
11k is inferred from one production datapoint and deliberately conservative;
operators need a knob if the real ceiling differs rather than a release.
- `maxMessages` — how many messages one reply may occupy is a product decision,
not a platform fact. A support bot and an internal ops bot want different
answers.
- `truncationMarker` — hardcoded English copy posted into the customer's
channel. The one constant with no correct default.
Deliberately NOT exposed, because they are correctness rather than preference:
`APPEND_CHAR_LIMIT` (a documented Slack per-call limit),
`MIN_MESSAGE_PROGRESS_BYTES` (loop-safety invariant — exposing it lets a caller
reintroduce the unbounded-message bug #6244 fixed), `MAX_FENCE_LANG_CHARS`, and
`FINISH_DRAIN_ATTEMPTS`.
Grouped under one nested option rather than three flat fields: `maxMessages` on
a Channel reads ambiguously on its own (thread history?), and the group keeps
the next continuation knob from adding another top-level field.
Both surfaces are wired, following `showToolStatus` exactly:
- direct: `slack({ replyContinuation })` → adapter → event-renderer → stream,
covering both the renderer path and `adapter.stream()`.
- managed: `createChannel({ replyContinuation })` → `Channel` →
`ChannelActivationConfig` → channel-manager → launcher → `DeliveryAdapter` →
the renderer's `nativeStreaming` block.
No gateway or Intelligence change is needed. Managed Slack renders in the SDK
process over a gateway live session and only emits `slack.stream.*` effects, so
render config never has to cross into Intelligence — the Elixir provider
executor is a dumb effect applier that owns no message boundaries.
`channel-manager.ts` carries a hand-written structural mirror of the launcher
signature, so the new field is declared there too or the managed path silently
type-drifts.
Tests: the marker override at the leaf, the renderer's pass-through (fails
without it — the defaults would keep that reply in one message), and the managed
chain end to end via `createChannel` → activation config → launcher opts, plus
the negative case that an unset option adds no properties anywhere.
CR r5 bucket (a): always stop native Slack streams on failure (thread
finish + NativeMessageStream queue drain), advance append/replace text
only after apply, rethrow permanent postFile gateway errors, exclude
stream.stop from provider-output tracking, classify errors by message,
validate prepared turn fields per kind, and stop unit tests from hitting
live lock cleanup HTTP.