Move the Intelligence-delivered managed-bot surface out of @copilotkit/bot into
its own package so the adapter, transports, contracts, and lifecycle ship
independently of bot core.
- New @copilotkit/bot-intelligence: intelligenceAdapter + DeliverySource/EgressSink
(+ in-memory impls) + placeholder contracts + startManagedBots/validation/
activation metadata. Production code imports only types from @copilotkit/bot
and @copilotkit/bot-ui.
- @copilotkit/bot keeps the generic PlatformCodec seam (moved to src/codec.ts) and
all core createBot changes (addAdapter, deferred store, id fields,
__managed/skipIngressDedup, exclusive guard). It now also exports the
FakeAdapter/FakeAgent test utilities for downstream adapter-package tests.
- Registered the new release scope: release.config.json, scripts/release/lib/
config.ts, and the canary/publish/stable release workflows.
Tests preserved: bot 150 + bot-intelligence 19 (= the prior 169); bot-slack 261.
Builds typecheck across bot/bot-intelligence/bot-slack/runtime; publint/attw/
oxlint/oxfmt clean.
A shared, pure, per-platform codec so platform rendering semantics live in one
place instead of being duplicated between the local adapter and the
Intelligence/Connector-Outbox side.
- PlatformCodec interface in @copilotkit/bot (renderEgress: IR -> native; pure,
no transport, no credentials)
- slackCodec in @copilotkit/bot-slack composing the existing pure
renderSlackMessage, importable by the Connector Outbox without Bolt or creds
Ingress normalization is marked TODO(OSS-363): extracting the pure Slack event
-> neutral mapping from the Bolt listener (so the local adapter and Intelligence
webhook ingress share it) is the remaining, higher-risk half of that ticket.
Expose new CopilotRuntime({ intelligence, bots }) -- the Mode B entry point
for managed bots:
- bots is accepted only on the Intelligence runtime variant (bots?: undefined
on the SSE variant), so TypeScript rejects bots without intelligence
- CopilotIntelligenceRuntime stores the declared bots; the facade exposes them
via the existing isIntelligenceRuntime getter pattern
- @copilotkit/bot is imported type-only (it is pure-ESM; a value import would
break this package's CJS output). Name validation + transport wiring happen
in startManagedBots (called by the managed-listener bootstrap), not here.
Adds a type-only @copilotkit/bot workspace dependency.
The SDK slice of OSS-360, fully unit-tested with in-memory transports:
- createBot({ name }): project-unique id exposed as bot.name (optional for
local bots; required + validated for managed)
- managed/runtime.ts:
- assertValidBotNames: required, identifier-style, unique-per-runtime; fails loud
- buildActivationMetadata: declared bot names + runtime env/versions (pure)
- startManagedBots: validate -> attach intelligenceAdapter per bot (wired to its
resolved transport) -> start; returns metadata + stop handle
- intelligenceAdapter now accepts a `store` it exposes as `stateStore`, so the
Intelligence-backed backend flows through createBot's resolveBackend
Transports are injected by the caller (closed Gateway/Outbox in production,
in-memory in tests); this module owns no Slack creds, ingress, or outbox.
Widen the placeholder ManagedIngressEnvelope to a discriminated union and route
every kind to the matching bot-core sink call, so managed bots reuse normal
routing for messages, commands, interactions, thread-starts, and reactions
(OSS-361) — not just plain turns.
- contracts: ManagedIngressBase + union over turn|command|interaction|
thread_started|reaction
- IntelligenceAdapter.dispatchTo: switch on kind -> onTurn / onCommand /
onInteraction / onThreadStarted / onReaction
- tests cover each kind dispatching to its handler and emitting egress
## CopilotDrawer — SDK polish + thread-management fixes
Polish and fixes on top of the shipped `<copilotkit-drawer>`
([#5701](https://github.com/CopilotKit/CopilotKit/pull/5701)), surfaced
by real de-fork pilots run against a locally-built SDK + the hosted
Intelligence runtime: **langgraph-js** (rich canvas / inline
`CopilotChat` / dark theme) and **mastra** (the proverbs CoAgents demo /
`CopilotSidebar` / light theme / controlled→uncontrolled provider).
Spans **`@copilotkit/web-components`** and **`@copilotkit/react-core`**
— no example/de-fork changes here (those wait on a release that
publishes web-components + a `react-core` bump that includes
`CopilotDrawer`). Captured as Phase-5 acceptance requirements in the
[design doc](https://app.notion.com/p/3883aa3818528190b5d1f9b6ba26dca0).
### `react-core` — thread management
- **Thread switching works under `<CopilotKit>`.**
`CopilotChatConfigurationProvider` treated any `threadId` prop as
controlled, so the auto-minted, non-explicit threadId that
`<CopilotKit>`'s v1 bridge seeds blocked the drawer's imperative
`setActiveThreadId` / `startNewThread` — thread switching and "+ New"
were dead in every app wrapped in `<CopilotKit>`. A non-explicit
(`hasExplicitThreadId={false}`) threadId is now overridable; a genuine
caller-supplied threadId stays controlled.
- **"+ New" resets the chat.** `CopilotChat` clears messages when
switching to a fresh non-explicit thread so the welcome screen shows
instead of the prior thread's messages — including when a still-loading
`/connect` is superseded by the switch (the aborted connect's stale
snapshot is dropped instead of repopulating the view).
- **No upsell flash/stick.** `CopilotDrawer` treats a pending (`null`)
license status as loading, not unlicensed, so the upgrade upsell no
longer flashes (or sticks) before the runtime reports the license.
- **In-header drawer launcher is mobile-only.**
`CopilotModalHeader.DrawerLauncher` toggles `drawerOpen`, which only
drives the off-canvas *mobile* drawer; on desktop the drawer is a
persistent in-flow panel that ignores `open`, so the launcher was a dead
no-op there. It now renders only at ≤767px. On desktop the chat's own
open/close (the toggle FAB + the header close, both lucide `X`) is the
consistent pair.
### `web-components` — theming, launcher, icons, tooltips
- **Host-theme inheritance (incl. dark mode).** Each token now resolves
`--cpk-drawer-* override → host theme var
(`--background`/`--card`/`--foreground`/`--border`/…) → built-in light
default`. The drawer follows the host app's light/dark theme by
inheritance, instead of baking light-only values (it showed a light
drawer on a dark app). Custom properties aren't reset by the `:host {
all: initial }` guard, so the standalone fallback still holds.
- **Row actions are icon buttons with instant, on-brand tooltips.**
Archive / Unarchive / Delete render inline lucide-style SVGs
(`currentColor`). Native `title` is replaced by an instant CSS tooltip
(matches the React components' Radix `delayDuration: 0`; `title`'s
show-delay is browser-fixed at ~1.5s), styled to match the standard
CopilotKit tooltip — a primary-colored bubble with an arrow, not a
surface/bordered box — and positioned to the side so the list's
scroll-overflow never clips it. The tooltip is suppressed while the
delete-confirm dialog is open (the clicked trash button keeps
`:focus-visible` otherwise, leaving its "Delete" tooltip floating over
the dialog).
- **Clipped thread names get a tooltip.** A truncated name exposes its
full text via the same instant primary-bubble tooltip as the row actions
(not the native `title`), shown only when actually clipped. The name
text moved to an inner span (so the outer can host the un-clipped
bubble) and the hovered row is z-lifted (each row is its own stacking
context from the entry-animation transform, which otherwise painted the
bubble under later rows).
- **Archived rows are muted**, no longer struck through.
- **Refetch preserves the list.** A refetch (e.g. Active↔All toggle)
keeps the known threads visible; the full loading state only renders on
the initial empty fetch.
- **Empty `footer` region hides** (it rendered as a stray bordered box
at the bottom). Driven by a `slotchange` listener, mirroring the
`memories` region.
- **Self-owned mobile launcher.** When the drawer is a closed off-canvas
mobile modal it renders its own floating "open threads" launcher, so
it's always openable without the host wiring a header button. The icon
is swappable via a `launcher-icon` **slot**, position is themeable via
`--cpk-drawer-launcher-top` / `--cpk-drawer-launcher-left` (so a host
can center it on its own header controls), and it exposes
`part="launcher"`. The richer in-header
`CopilotModalHeader.DrawerLauncher` remains an optional integration.
### Tests
- **react-core**: thread-control + non-explicit-override cases on
`CopilotChatConfigurationProvider`, the "+ New" message reset on
`CopilotChat`, license-pending gating on `CopilotDrawer`, and the
mobile-only / desktop-hidden launcher on `CopilotModalHeader`. New
regression tests verified red-green; chat + provider suites green, `tsc
--noEmit` clean, build green. (The superseded-connect *race* guard is
covered by live verification + its synchronous sibling test — an
async-timing unit test would be flaky, which the repo treats as a bug.)
- **web-components**: element tests for the icon tooltips +
confirm-dialog suppression, the clipped-name tooltip (`data-tooltip` +
placeholder), archived styling, refetch-preserves-list, footer
hide-when-empty, and mobile launcher render/open + desktop-absent.
Drawer suite green, `tsc --noEmit` clean, build + `es-check` green under
`CI=true`. (Truncation detection + tooltip stacking are layout/CSS —
verified live, not in jsdom.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
_syncNameClipping toggled name-clipped on .row-name, but the stacking fix in
styles.ts targets .row.name-clipped:hover — a different element — so the z-index
lift never matched and clipped-name tooltips still painted under later rows
(each row is its own transform stacking context). Stamp the flag on the owning
.row too; the tooltip bubble stays scoped to .row-name:hover so a row-action
hover never surfaces it. Adds a red-green contract test asserting the class
lands on both the row-name and the row.
Addresses MikeRyanDev review on #5707.
A long thread name is clipped with an ellipsis; expose the full text via the
same instant primary-bubble tooltip as the row actions (not the native `title`),
shown only when the name is actually truncated. The name text moved to an inner
span so the outer `.row-name` can host the un-clipped bubble, and the hovered
row is z-lifted so the bubble isn't painted under later rows (each row is its
own stacking context from the entry-animation transform).
The in-header thread-drawer launcher toggles `drawerOpen`, which only drives the
off-canvas MOBILE drawer; on desktop the drawer is a persistent in-flow panel
that ignores `open`, so the launcher was a dead no-op there (it appeared to do
nothing on click). Gate it on a reactive mobile viewport check so it renders
only at ≤767px. On desktop the chat's own open/close — the toggle FAB + the
header close (both lucide X) — is the consistent pair; nothing masquerades as a
chat control.
Row-action tooltips used a surface background + border, which read as a button.
Restyle to the standard CopilotKit tooltip: primary bubble, primary-foreground
text, an arrow, no border. Also suppress the tooltip while the delete-confirm
dialog is open — the clicked trash button keeps :focus-visible, which otherwise
left its "Delete" tooltip floating over the dialog.
CopilotChatConfigurationProvider treated any `threadId` prop as controlled, so
the auto-minted non-explicit threadId that <CopilotKit>'s v1 bridge seeds
blocked the drawer's imperative setActiveThreadId/startNewThread — thread
switching and "+ New" were dead in every app wrapped in <CopilotKit>. A
non-explicit (`hasExplicitThreadId={false}`) threadId is now overridable; a
genuine caller-supplied threadId stays controlled.
Also, surfaced during the langgraph-js de-fork validation:
- CopilotChat clears messages when switching to a fresh non-explicit thread
("+ New") so the welcome screen shows instead of the prior thread — including
when a still-loading /connect is superseded by the switch.
- CopilotDrawer treats a pending (null) license status as loading, not
unlicensed, so the upgrade upsell no longer flashes or sticks before the
runtime reports the license.
Polish on top of the shipped <copilotkit-drawer> (#5701), surfaced by the first
real de-fork pilot (langgraph-js):
- Theming follows the host app's light/dark theme: each token resolves
override -> host theme var (--background/--card/...) -> built-in light default,
instead of baking light-only values (drawer was light on a dark app).
- Row actions are icon buttons (inline lucide-style SVG, currentColor) with
instant tooltips. Native title is replaced by a CSS tooltip (matches the
react components' delayDuration:0; title's show-delay is browser-fixed) and is
positioned to the side so the list's scroll-overflow never clips it.
- Archived rows are muted, no longer struck through.
- A refetch (e.g. Active<->All filter toggle) keeps the known list visible;
the full loading state shows only on the initial empty fetch.
- The reserved footer region hides when nothing is slotted into it (it rendered
as an empty box at the bottom); mirrors the memories region.
- Mobile gets a self-owned floating launcher so the drawer is always openable
with no host header wiring. Its icon is swappable via a launcher-icon slot and
its position via --cpk-drawer-launcher-top/left (host can center it on its own
header controls); exposes part="launcher".
Adds element tests for icons+tooltips, archived styling, refetch-preserves-list,
footer hide-when-empty, and the mobile launcher (render + open + desktop-absent).
The StateStore interface and the in-memory MemoryStore default remain;
durable backends can be reintroduced as a follow-up. Both adapter packages
were merged in #5613 but never published to npm, so removal is a clean
delete with no consumer impact.
- Delete packages/bot-store-redis and packages/bot-store-postgres.
- Revert the bot release scope and drift guard to bot + bot-ui.
- Strip the Redis dep, demo:restart script, restart demo, docker-compose,
and REDIS_URL env from examples/slack.
- Rewrite the bot persistence/transcripts docs around "MemoryStore default
+ implement the StateStore interface yourself for durability".
Adds a monorepo-invariant test that scans sibling bot-* adapter packages for their declared `platform` literal and fails if any is missing from normalizePlatform's allow-list. It immediately caught the new bot-teams adapter (platform "teams") that landed on main — now added to the allow-list.
Address PR review: bound the free-form adapter platform label to slack|discord|telegram|whatsapp|custom (no tenant/project leakage, capped cardinality); emit oss.bot.agent_run only after the transcript-append + renderer.finish() steps succeed, with a finalize stage on oss.bot.agent_run_failed for late failures.
createBot emits configured + started/start_failed; Thread emits agent_run/agent_run_failed around runAgentLoop. Zero new env vars. Includes mocked-wiring unit tests and a real-BotTelemetry e2e test.
BotTelemetry posts 5 oss.bot.* events at 100% via @copilotkit/shared's lambdaClient. Anonymous (no telemetry_id/license/key), opt-out via COPILOTKIT_TELEMETRY_DISABLED/DO_NOT_TRACK, suppressed under test. 3-tier anonymous_id (durable store -> project cache file -> per-process UUID). errorClass() maps errors to a bounded category, never a raw message.
@internal, undocumented bridge that runs the bot SDK from Intelligence-
delivered events. Pure plumbing over injected DeliverySource (inbound) and
EgressSink (outbound) transports; production wires Realtime Gateway +
Connector Outbox, tests/headless wire the in-memory implementations.
- src/managed/: placeholder contracts (TODO(OSS-377)), transport interfaces,
in-memory transports, IntelligenceAdapter + intelligenceAdapter() factory
- ingress: managed turn -> onTurn; ack on success, nack on throw (at-least-once)
- egress: deterministic operationId = `${turnId}:${seq}` (reset per turn, so a
redelivered turn reproduces the same ids for Connector Outbox dedupe); minted
MessageRef carries routing for update/delete; buffering run-renderer emits a
post op for agent text
- create-bot: skipIngressDedup capability (managed skips ingress dedup and relies
on egress idempotency; local adapters unchanged) + assertExclusive guard
(intelligenceAdapter must be the only adapter on a bot in V1)
Foundations for Intelligence-delivered managed bots (OSS-360 SDK slice),
all backwards-compatible:
- createBot `adapters` is now optional + new `bot.addAdapter()` (throws if
called after start), so the runtime can attach a delivery adapter late
- backend/transcripts/registry resolution and component registration are
deferred from construction to start(); new resolveBackend precedence
(explicit store.adapter -> adapter-provided stateStore -> MemoryStore),
warning when more than one adapter provides a store
- bot.transcripts throws if accessed before start()
- optional stateStore on PlatformAdapter; optional turnId/deliveryId on
IncomingTurn/InteractionEvent/IncomingCommand; eventId/turnId/deliveryId
on IncomingMessage, propagated from the turn for customer-side idempotency
Add an optional onAction interceptor to createA2UIMessageRenderer so apps
can handle A2UI actions client-side (e.g. navigate) instead of forwarding
every action to the agent.
- onAction(action, forward) runs before forwarding. Return null to handle
client-side and stop forwarding, return a modified action to forward it,
or return undefined to forward unchanged.
- Threaded from createA2UIMessageRenderer options through ReactSurfaceHost.
- Extracted runA2UIAction helper for deterministic unit testing.
- Default behavior is unchanged when onAction is not supplied.
The reaction handler now receives the conversation `thread` and an
update-capable `messageRef` for the reacted message — the same surface an
`onClick` gets via `ctx.thread`/`ctx.message.ref`. A reaction can now post new
UI (`thread.post`), swap the message in place (`thread.update(messageRef, …)`),
run the agent, or block on a human choice (`thread.awaitChoice`, HITL).
- bot-ui: `MessageReaction` gains `thread` and `messageRef`.
- platform-adapter: `IncomingReaction` gains an optional adapter-provided
`messageRef` (engine falls back to `{ id: messageId }`).
- create-bot: threads `thread` + `messageRef` into both the global
`ReactionEvent` and the per-message handler.
- Slack/Discord/Telegram reaction decoders emit a platform-specific,
update-capable `messageRef` (channel+ts / channelId+id / chatId+messageId).
Type the bot UI surface and broaden the cross-platform component vocabulary,
adding only capabilities that more than one adapter can express.
- bot-ui: type the `Thread` interface's `ui` params (`post`/`update`/
`awaitChoice`/`postEphemeral`) as `Renderable` instead of `unknown`, so JSX
is checked at the call site and the `{ raw }` escape hatch is explicit.
- `<Message onReaction>`: per-message reaction callback `(emoji, { added, user,
rawEmoji, messageId })`. Stripped from the IR before it reaches the adapter,
routed from reaction ingress by message id. Durable on the same terms as a
component `onClick` — a `{ component, props }` snapshot is persisted and the
component is re-rendered to re-derive the handler after a restart; inline
handlers route in-process only.
- `Button.url` link buttons — Slack, Discord, Teams, Telegram.
- `Field.label` — typed (Discord and Telegram already consumed it untyped);
newly rendered on Slack.
- `Select.multi` multi-select — Slack (`multi_static_select` in an input block
+ `selected_options` decode), Discord (min/max values + `component` bounds
decode), Teams (`isMultiSelect`); Telegram/WhatsApp degrade to single-select.
Slack-only capabilities (Button.confirm, Image.title, Input label/initialValue/
required, Select.initialValue) were intentionally left out.
## What
Bake a heap ceiling into the `@copilotkit/core` build script:
```diff
-"build": "tsdown",
+"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 tsdown",
```
(`cross-env` is already used elsewhere in the repo; added here as a core
devDependency.)
## Why
The core build (tsdown + dts generation) has a ~3.45GB working set and
OOMs under `nx run-many`, where it competes with sibling builds and GC
falls behind.
This mostly bites **local development**. Agents (and humans) routinely
have to prefix commits with `NODE_OPTIONS=--max-old-space-size=8192`
just to get the build through pre-commit hooks. CI already sets this
flag at the workflow level, so baking it into the build script means
every invocation path gets the same headroom without anyone remembering
to add it: local lefthook hooks, `nx run-many`, a direct `pnpm build`,
CI, and Windows (hence `cross-env`).
Scoped to core only, since it's the single package that OOMs.
The memory store's socket effect created the `user_meta:memories:<code>`
channel and observed its events, but never subscribed the join-outcome
stream — and `ɵphoenixChannel$` only calls `channel.join()` when that stream
is subscribed. So the socket connected but no `phx_join` was ever sent: the
gateway saw the connection, the channel was never joined, and saved memories
never pushed to the client (REST snapshot worked; live deltas silently never
arrived). The thread store joins because its merged `joinOutcome$` subscribes
the stream.
Merge `ɵjoinPhoenixChannel$(channel$)` into the socket effect's output so the
join actually fires; swallow a failed/timed-out join so a transient rejection
doesn't tear down the metadata stream (Phoenix re-attempts on reconnect). Add
a `joinCount` spy to MockChannel and assert the realtime test actually joins —
this regression passed before because the mock delivered events without a join.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The @copilotkit/core build (tsdown with dts generation) has a working set
of about 3.45GB. Node's default old-space ceiling is roughly 4GB and CI pins
NODE_OPTIONS to 4096, leaving almost no headroom. Under nx run-many the build
competes for CPU with sibling builds, GC falls behind, and the process tips
over the heap limit. nx flags it as a flaky task and pre-commit/CI builds fail
intermittently with a V8 heap OOM.
Bake the ceiling into the core build script with cross-env so every invocation
path gets consistent headroom: local lefthook hooks, nx run-many, CI, and
direct pnpm build, on every platform including Windows. 8192 matches the value
already used by the e2e workflow and gives roughly 2x headroom over the working
set.
Scoped to core only; it is the single package that OOMs.
Adds the @copilotkit/bot-teams package: a PlatformAdapter that bridges
the CopilotKit runtime to Microsoft Teams. Renders agent output as
Adaptive Cards (including native <Chart> components via bot-ui), streams
replies with a typing-indicator heartbeat, supports human-in-the-loop
confirmations, and handles inbound files via the Graph API with channel
file support.
ɵsubscribeToMemories mirrored ɵsubscribeToThreads and sent userId in the
body, but the platform's memory routes resolve the app user from the
x-cpki-user-id header (like listMemories/createMemory), not the body — so
POST /api/memories/subscribe returned 401 AUTH_UNAUTHENTICATED. Send the
user via the header instead; the body is now empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The memory store client POSTs /memories/subscribe to mint realtime join
credentials, but the runtime router had no such route — the path fell
through to /memories/:id (memories/mutate), so a POST returned 405 Method
Not Allowed. Threads had the full chain (route → handleSubscribeToThreads →
platform); memory was missing the runtime proxy between the client and the
platform's POST /api/memories/subscribe.
Add `memories/subscribe` to the route union, match it before /memories/:id
(and exclude "subscribe" from the :id rule, mirroring threads), add
handleSubscribeToMemories returning { joinToken, joinCode } (memory delivers
the join code here, unlike threads where it rides the thread-list response),
and ɵsubscribeToMemories on the platform client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mobile drawer is a modal dialog, but the Tab trap was bound to .root only,
while the backdrop button renders as a sibling OUTSIDE .root — so Tab from the
backdrop (or any out-of-root node) could escape the modal. Move the trap to the
host keydown listener (keydown is composed, so it catches the backdrop too) and
include the backdrop in the focusable cycle; keep initial focus on the first
real control. Adds a regression test for backdrop/outside-root Tab containment.
The UMD bundle inlines lit for a self-contained CDN <script>; inlining lit also
pulls in its runtime deps (@lit/reactive-element, lit-html, lit-element,
@lit-labs/ssr-dom-shim). tsdown flags that transitive bundling as 'unintended'
and promotes the warning to a fatal error under CI, breaking
@copilotkit/web-components:build (and every dependent). Declare all lit packages
in noExternal and set inlineOnly: false to mark the bundling intentional.
Angular signal wrapper over the core thread store: reactive runtime context
(re-dispatches when wsUrl/threadEndpoints arrive), enabled-gated single-slot
registration, bound result methods, and synthesized error/loading parity with
react-core.