Commit Graph

387 Commits

Author SHA1 Message Date
Maximiliano Korp 164b53ebcb fix(channels): isolate factory-returned agents; drop per-run lock/runner keys
Keeps the isolation fix and removes the concurrency mechanism, which two
landings on main superseded while this branch was open.

Isolation (kept, narrowed):
- Clone the result of an `agent: (threadId) => ...` factory as well as the
  singleton config. #6256 already isolated the singleton path; a factory
  returning a shared instance was still handed to every turn un-isolated,
  carrying the previous turn's messages and lifecycle flags and serializing
  those turns behind one identity key in DeliveryAdapter.acquireAgent.
- Reset `isRunning` / `abortController` on the clone as hygiene. Documented as
  such: `runAgent` reassigns `abortController` before every run, so an
  inherited aborted controller cannot by itself kill a follow-up turn.
- Drop the `activeRunDetach$` / `activeRunCompletionPromise` resets. `clone()`
  copies a fixed field list that excludes both, so they were always undefined.
- Document that `AbstractAgent.prototype.clone()` drops subclass-declared
  fields silently, and correct the clone() error text: a factory no longer
  bypasses cloning, so it is not an escape hatch.

Concurrency mechanism (removed):
- Restore `runner.run/stop({ threadId: canonicalThreadId })`.
  IntelligenceAgentRunner does not use this as a local map key; it sends it as
  `thread_id` when joining `ingestion:<runId>`, and the gateway resolves it
  against `cpki.threads`, so a `<threadId>::<runId>` value fails the join with
  `thread_not_found`. Added a regression guard asserting the canonical id.
- Restore the unprefixed product thread lock. The gateway derives this same key
  in four places and reads its value as "which run is active on this thread".
  It is also the only mutual exclusion spanning runtime replicas, since
  DeliveryTransport's per-thread admission gate (#6257) is instance state.

Also:
- Forward `lockKeyPrefix` on cleanup in both the channel path and
  handlers/intelligence/run.ts, so a configured prefix releases the key it
  acquired instead of leaking it.
- Collapse the four copy-pasted clone-patching test scaffolds into one
  `patchAgentAndClones` helper.
2026-07-30 14:08:31 -07:00
github-actions[bot] 47e5d0bcea style: auto-fix formatting 2026-07-30 14:07:35 -07:00
Alem Tuzlak dd26d38406 fix(channels): always isolate agents and scope product locks per run
Singleton and shared-factory agents now clone on every turn so the
prototype is never run (avoids aborted HttpAgent clones after the first
reply). Concurrent Channel deliveries use per-run runner thread ids and
lockKeyPrefix so same-thread turns do not collide locally.
2026-07-30 14:07:34 -07:00
Mike Ryan 4d46e57815 feat(channels): persist managed asset history 2026-07-30 11:56:00 -07:00
Benjamin Taylor 13e575ea6f Revert "refactor(runtime): import the launcher options type instead of mirroring it"
This reverts commit 346329aef8.
2026-07-30 11:57:21 -05:00
Benjamin Taylor 346329aef8 refactor(runtime): import the launcher options type instead of mirroring it
`ChannelsIntelligenceModule` re-declared the launcher's options by hand, so
adding `replyContinuation` to the real launcher type-checked clean here while
the managed path silently ignored it — the mirror had to be edited too or the
option was dropped on the floor. That is a trap for every future launcher
option, not just this one.

The mirror existed for a stated CJS/ESM reason, so I checked whether it still
applies rather than assuming. It does not, for a type:

- `import type` is fully erased. The emitted CJS gains no `require` of
  `@copilotkit/channels-intelligence`; the only references in the build output
  remain the pre-existing non-literal specifier constant and the package.json
  dependency entry.
- `ChannelsIntelligenceModule` is not part of the emitted `.d.cts`/`.d.mts`
  surface (it appears only in sourcemaps), so no CJS consumer resolves the
  ESM-only package — which matters because that package's export map has an
  `import` condition and no `require`.

The constraint is real for the *value* import, which is why the dynamic
specifier stays non-literal. The comment now draws that distinction explicitly
so the next reader does not re-mirror it.

Net: 42 lines of duplicated type removed, and the managed path can no longer
drift from the launcher it calls.
2026-07-30 11:55:41 -05:00
github-actions[bot] a85760e06b style: auto-fix formatting 2026-07-30 16:45:15 +00:00
Benjamin Taylor b2fc4c0706 feat(channels): make Slack reply-continuation limits configurable (refs OSS-689)
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.
2026-07-30 11:43:04 -05:00
Alem Tuzlak 9250f22d88 fix(channels): stream cleanup, digest parity, postFile fail-loud
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.
2026-07-30 12:55:03 +02:00
Alem Tuzlak d64ca464dd fix(channels): stream stop, provider error propagation, lock prefix
Stop Slack streams in finally; rethrow ChannelProviderDeliveryError from
postFile; treat delivery join failures as permanent; plumb lockKeyPrefix
into channel canonical locks; export resolveChannelActivationEnv and
treat blank env as unset; align deploy URL guidance.
2026-07-30 12:25:55 +02:00
Alem Tuzlak e2a5dc9219 fix(channels): seal packet path without blocking terminal recovery
Allow a failed/uncertain terminal after effect or complete-terminal push
failures; seal only after a successful terminal apply. Leave Phoenix child
channels on failed join, re-arm delivery handlers on restart, replay
onStateChange health, skip empty Teams stream deltas, and align docs/tests.
2026-07-30 12:09:32 +02:00
Alem Tuzlak 155d488bb7 fix(channels): harden delivery protocol and lock cleanup
Close packet path after permanent push/ack failures so a later effect
cannot mint a new effectId on the same seq. Refresh owner generation on
join_token reconnect, add reconnect backoff, require claimed on claim
assert, reject unknown turn kinds, skip empty Slack stream deltas, and
surface missing file-client attachments instead of dropping them.

Always release the product thread lock after a Channel canonical run.
Align connectTimeoutMs docs, projectId validation, ops error guidance,
and test fixtures with the delivery ID contract.

Note: local lefthook skipped (no node_modules in this worktree); CI will
validate. CR findings addressed from PR #6249 review.
2026-07-30 11:20:44 +02:00
Mike Ryan 8f166577ce feat(channels): replace live sessions with realtime boundary 2026-07-29 22:23:30 -07:00
Mike Ryan d545eba10e fix(channels): preserve live-session run semantics 2026-07-29 17:22:56 -07:00
Mike Ryan d5beb8e5de fix(channels): enforce live-session delivery boundaries 2026-07-29 16:42:17 -07:00
Mike Ryan 624bf3c360 fix(channels): cancel drained canonical runs 2026-07-29 16:42:03 -07:00
Mike Ryan 243caa2ed7 fix(channels): preserve one canonical run lifecycle 2026-07-29 16:42:02 -07:00
Mike Ryan 5ddcb14233 refactor(channels): use gateway live sessions 2026-07-29 16:42:01 -07:00
Tyler Slaton 0ea0c1bd2e fix(channels): hide Slack tool status by default (#6204)
## Problem

Slack threads render every tool invocation as visible progress, leaving
noisy `Used …` rows ahead of the final answer.

## Why

The direct Slack renderer treated tool status as enabled when
`showToolStatus` was omitted, while the managed Intelligence renderer
always emitted tool lifecycle frames. The managed renderer's opt-in was
not reachable through the supported `createChannel()` and
`CopilotRuntime` API.

## Fix

- Hide tool-call progress by default for direct and managed Slack
routes.
- Expose `createChannel({ showToolStatus: true })` as the managed
Channels opt-in and carry it through runtime activation to the
Intelligence launcher.
- Preserve `slack({ showToolStatus: true })` as the direct Slack opt-in.
- Keep existing managed behavior for non-Slack routes.
- Continue capturing and executing tool calls while their status frames
are hidden.
- Add regression coverage for the public managed API, legacy and
native-streaming Slack, and Realtime Gateway paths.

Paired with https://github.com/CopilotKit/Intelligence/pull/632.
2026-07-29 15:50:15 -07:00
Tyler Slaton 124f4e29ef fix(channels): expose managed tool status option 2026-07-29 18:14:25 -04:00
Benjamin Taylor af7b64271b feat(channels): log why a managed session dropped and keep logging while it is down (refs OSS-670) 2026-07-29 16:01:24 -05:00
Benjamin Taylor 6c0b533b7d feat(channels): carry the drop cause on managed-session state transitions (refs OSS-670) 2026-07-29 15:52:40 -05:00
Maxim 1c4676c687 Merge remote-tracking branch 'origin/main' into blitz/glass-inspector/integration 2026-07-29 20:19:10 +02:00
Maxim fa40e0424b Merge remote-tracking branch 'origin/blitz/glass-inspector/integration' into blitz/glass-inspector/integration 2026-07-29 15:28:16 +02:00
Tyler Slaton b919b0e927 docs(runtime): direct Channels stay below the canonical/reliability layer by design (#6155)
## Problem

All five `OSS-599` references in
`packages/runtime/src/v2/runtime/core/channel-manager.ts` describe the
missing gateway/canonical/reliability wiring for **direct** Channels as
*"deferred"*:

> it is NOT wired into the Intelligence gateway/canonical/reliability
layer (deferred, OSS-599)

That reads as pending work — as though a direct Channel eventually
reaches managed parity.

**OSS-599 says the opposite.** Its boundary discipline places
run-correctness (canonical cross-surface history, fenced
outer-run/single-terminal, durable HITL-resume-across-restart, selection
pinning) and the reliability layer **Intelligence-side only**, and
states plainly that shipping an SDK-side equivalent *"collapses the
build-vs-buy moat"*. A direct Channel's ceiling is the SDK's in-process
run loop, permanently.

So the comments point the next reader at implementing precisely the
thing the ticket forbids.

## Change

Reword all five sites to say the boundary is by design, not pending:

| Site | Was | Now |
|---|---|---|
| `ChannelStatus` doc | "(deferred, OSS-599)" | "BY DESIGN, not a
deferral" + why, + "do not 'finish' this by pulling the layer into the
SDK" |
| `ChannelManager` class doc | "wiring … is deferred" | "stay below the
canonical/reliability layer by design" |
| `activate()` inline | "is deferred (OSS-599)" | "by design, not
pending work (OSS-599)" |
| `startDirectChannel` doc | "(deferred, OSS-599)" | "that boundary is
permanent, not a deferral" |
| direct-start log string | "wiring deferred (OSS-599)" | "stay below
the canonical/reliability layer by design (OSS-599)" |

Comments and one log string only. **No behavior change.**

## Testing

- **Log-string assertion preserved.** `channel-manager.test.ts:611-619`
asserts the direct-start breadcrumb contains `"direct adapter"` and
`"ɵruntime.start()"`. Both substrings survive the reword — only the
parenthetical changed.
- **Test run + control.** Ran `channel-manager.test.ts`,
`channel-manager-reconnect.test.ts`, and
`channel-activation-config.test.ts` in the worktree: `4 failed | 56
passed`. Ran the same suite on an **unmodified `origin/main`** worktree
as a control: `4 failed | 34 passed` — the *identical* four failures.

The four are a worktree artifact, not a regression:
`@copilotkit/channels` resolves to the outer checkout's pre-#6145 build,
which has no `ɵruntime`, so every "real direct transport" test fails
there. Same failures before and after the change; this diff adds none.
CI (with a correct install) is the real gate.
- **Formatted** with `oxfmt`.

## Notes

Pre-commit hooks were bypassed: `test-and-check-packages` runs `nx` in
the worktree, where `@copilotkit/core:build` fails for unrelated
environment reasons. The change is comments-only.

Follow-up, not in this PR: OSS-599 was written the day before Plan C
shipped, so its own framing ("a DIY runner is ~15 lines over
`ɵruntime.start()`", "a DIY runner gets this") is stale now that the DIY
path is removed. Its §2 response-policy and four-mode-binding scope is
unaffected. I'm leaving a reconcile note on the ticket.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-28 13:15:09 -07:00
Ben Taylor 8a8e52d9e9 fix(runtime): non-optional listener.channels and honest lifecycle docs (OSS-646) (#6207)
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)
2026-07-28 14:11:53 -05:00
Maxim 0850c5e522 Merge remote-tracking branch 'origin/main' into blitz/glass-inspector/integration
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>
2026-07-28 21:10:21 +02:00
Benjamin Taylor a26990971e docs(runtime): note that the non-optional channels shape needs a literal channels tuple (OSS-646) 2026-07-28 13:26:05 -05:00
Benjamin Taylor 3f0bbe4a7b feat(runtime): default the Intelligence platform URLs to the managed service
`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
2026-07-28 13:12:15 -05:00
Benjamin Taylor 956c6448a7 fix(runtime): non-optional listener.channels and honest lifecycle docs (OSS-646)
`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.
2026-07-28 13:11:55 -05:00
Tyler Slaton 1d1ca9ea8b fix(docs): correct the documented Intelligence endpoints for Channels (#6189)
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)
2026-07-28 08:46:39 -07:00
David McKay 6632e48a89 Merge branch 'main' into david/fix-cpk-7634-activity-snapshot-order 2026-07-27 15:21:27 -07:00
Benjamin Taylor 3c1c85937b fix(docs): point Intelligence docs at the hosts that actually serve prod
Every documented Intelligence endpoint used api.copilotkit.ai for both the
REST and websocket planes. That host returns 404, and the realtime host it
implied (realtime.copilotkit.ai) has no DNS record at all, so a developer
copying any of these snippets could not connect.

The real managed endpoints are split across two hosts, matching what the CLI
bakes in as its prod defaults and what gitops deploys:

  apiUrl: https://api.intelligence.copilotkit.ai
  wsUrl:  wss://realtime.intelligence.copilotkit.ai

Because the planes are separate hosts, wsUrl cannot be produced from apiUrl by
swapping the scheme, so each site now says so explicitly. The runtime skill's
"common mistakes" section gains that as a third failure mode, and the
CopilotKitIntelligenceConfig JSDoc documents it on the field itself — a wrong
apiUrl fails fast with an HTTP error, but a wrong wsUrl only hangs until the
settle timeout, which is what made this expensive to diagnose.

skills/runtime is the generated mirror of packages/runtime/skills/runtime,
regenerated with pnpm sync:plugin-skills. skills/copilotkit-debug is
standalone; its /info sample keeps the /client suffix, which is correct there
because that response carries the already-derived client URL.

Refs OSS-621
2026-07-27 16:44:38 -05:00
Ben Taylor 8806f668d1 fix(runtime): coalesce consecutive same-role Anthropic messages before dispatch (#5360)
## Summary

In multi-turn conversations using the Anthropic adapter, a model turn
that contains both assistant text and a tool call can be replayed as two
consecutive `{role: "assistant"}` entries in the Anthropic payload.
Anthropic expects one message object per turn with alternating roles, so
that split payload can blur turn boundaries on the next request. This PR
coalesces same-role Anthropic messages before dispatch so one assistant
turn stays one assistant message.

## Root cause


[`convertMessageToAnthropicMessage`](https://github.com/CopilotKit/CopilotKit/blob/005aebbededbfdd7978b3c1ee221580b74dd088d/packages/runtime/src/service-adapters/anthropic/utils.ts#L142-L219)
maps each CopilotKit message independently. A mixed assistant turn, a
`TextMessage(role=assistant)` followed by an `ActionExecutionMessage`,
therefore becomes two separate assistant entries. The Anthropic Messages
API requires all content blocks for one assistant turn to be sent in a
single message object: https://docs.anthropic.com/en/api/messages


[`AnthropicAdapter.process()`](https://github.com/CopilotKit/CopilotKit/blob/005aebbededbfdd7978b3c1ee221580b74dd088d/packages/runtime/src/service-adapters/anthropic/anthropic-adapter.ts#L282-L366)
already deduplicates `tool_result` blocks on the user side, but it
previously forwarded the mapped assistant-side payload without a
coalescing pass. Rebasing onto current `main` also exposed test-fixture
drift in the two new regression tests, so the final branch updates those
fixtures to the current object-form `TextMessage` constructor and
removes one stale `ActionInput` field while keeping the production fix
unchanged.

## Changes

- `packages/runtime/src/service-adapters/anthropic/utils.ts`: add
`coalesceConsecutiveSameRoleMessages(messages)` to merge adjacent
equal-role
  Anthropic messages by concatenating their `content` arrays.
-
`packages/runtime/src/service-adapters/anthropic/anthropic-adapter.ts`:
call
`coalesceConsecutiveSameRoleMessages` before
`limitMessagesToTokenCount`.
-
`packages/runtime/tests/service-adapters/anthropic/anthropic-adapter.test.ts`:
add same-role regression coverage, align the new fixtures with the
current
object-form `TextMessage` constructor, and remove the stale `parameters`
field
  from the action fixture.
- `.changeset/coalesce-anthropic-same-role-messages.md`: patch changeset
for
  `@copilotkit/runtime`.

## Scope

The `tool_result` allowlist deduplication stays unchanged. Token
trimming and the orphan-removal post-processor still run after
coalescing, so `tool_use` and `tool_result` pairing remains intact. The
OpenAI, Google, LangChain, and Groq adapters are unaffected.

The reported regression and the explicit regression coverage are
assistant-side. The coalescing helper itself is role-agnostic for
adjacent equal-role array content, but this PR does not add a separate
user-side regression case.

## Related PRs and Issues

Prior attempted fix: #2864, which addressed unrelated message callbacks
rather than the adapter payload.

## Test plan

- [x] `pnpm -C packages/runtime exec vitest run
tests/service-adapters/anthropic/anthropic-adapter.test.ts`
  10/10 tests pass, including the two same-role coalescing cases.
- [x] `pnpm -C packages/runtime exec vitest run
tests/service-adapters/anthropic/utils-token-trimming.test.ts`
  9/9 tests pass.
- [ ] CI green (`static / quality`, `test / unit` on Node 20/22/24)

## Upstream

Closes #2910.
Reported by @alonronin.
2026-07-25 08:47:11 -05:00
Tyler Slaton bc5454f848 fix(channels): surface fenced listener sessions 2026-07-24 19:14:58 -04:00
Rod Boev b94c1be8c2 fix(runtime): coalesce consecutive same-role Anthropic messages before dispatch (#2910) 2026-07-24 18:45:10 -04:00
Benjamin Taylor 05b3b2b55b docs(runtime): direct Channels stay below the canonical/reliability layer by design
All five OSS-599 references in `channel-manager.ts` described the missing
gateway/canonical/reliability wiring for DIRECT Channels as "deferred",
implying a direct Channel gets pulled up to managed parity later.

OSS-599 says the opposite. Its boundary discipline puts run-correctness
(canonical cross-surface history, fenced outer-run/single-terminal, durable
HITL-resume-across-restart, selection pinning) and the reliability layer
Intelligence-side ONLY, and states that reproducing them in the SDK
"collapses the build-vs-buy moat". A direct Channel's ceiling is the SDK's
in-process run loop, permanently.

Reword all five sites so the next reader does not treat the gap as pending
work — which would lead them to implement exactly what OSS-599 forbids.

Comments and one log string only; no behavior change. The log string keeps
the `direct adapter` / `ɵruntime.start()` substrings that
`channel-manager.test.ts` asserts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 17:07:46 -05:00
David McKay 7cbc2b5d4f fix(runtime): preserve anyOf/oneOf unions in frontend tool schemas (#6143)
## Problem

A frontend/client tool (`useFrontendTool`) whose Zod `parameters` use
`z.discriminatedUnion(...)` — or any schema that serializes to a
JSON-schema `anyOf`/`oneOf` node — silently loses the union-typed field
when calling OpenAI. The tool call arrives with that field missing or
empty. Switching the same tool to a flat object with an enum
discriminant works, which points at schema conversion rather than the
model.

## Root cause

The runtime has **two** JSON-Schema → Zod converters:

- `@copilotkit/shared`'s `convertJsonSchemaToZodSchema` already handles
`anyOf`/`oneOf` as `z.union` (and `$ref`, null-unions, graceful
fallback).
- The **local copy** in `packages/runtime/src/agent/index.ts` — used by
the classic `BuiltInAgent` / AI SDK path via
`convertToolsToVercelAITools` — never did.

A union node carries no top-level `type`, so it hit the empty-schema
guard (`if (!jsonSchema.type)`) and collapsed to `z.object({})`. The
reconstructed tool schema therefore dropped the union entirely — most
visibly for a union nested inside array `items` — so the model was never
offered those fields and could not emit them. (The legacy GraphQL OpenAI
adapter is unaffected: it forwards the JSON schema directly.)

## Fix

Handle `anyOf`/`oneOf` as `z.union` **before** the empty-schema guard,
mirroring the already-proven shared converter. A single-variant union
unwraps to that variant.

## Backward compatibility

Only previously-broken union nodes change behavior (empty object → real
union). Empty `{}` schemas, typed nodes, and the `isJsonSchema` gate are
untouched. Two regression tests added: a direct `anyOf` conversion and
the exact nested-`items` `oneOf` trap.

## Repro

A `useFrontendTool` with

```ts
parameters: z.object({
  blocks: z.array(z.discriminatedUnion("type", [
    z.object({ type: z.literal("heading"), level: z.number() }),
    z.object({ type: z.literal("paragraph"), content: z.string() }),
  ])),
})
```

against OpenAI: before, `blocks` items arrived empty; after, both
variants survive into the model call.

## Note on OpenAI strict mode

This path does not enable OpenAI strict function-calling, so once the
union survives conversion it serializes back to `anyOf` and OpenAI
accepts it. The data loss was upstream, in CopilotKit's own converter,
not an OpenAI strict-mode limitation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 11:49:50 -07:00
David McKay e5493bca5f fix(runtime): honor *_BASE_URL env vars in resolveModel for OpenAI-compatible endpoints (#6140)
## Problem

`resolveModel()` builds each provider with only an `apiKey`, so a
**string model spec** (e.g. `"openai/gpt-4o-mini"`) cannot target an
**OpenAI-compatible endpoint** — Azure OpenAI, OpenRouter, an LLM
gateway, or a local server (vLLM / LM Studio / Ollama).
`OPENAI_BASE_URL` is silently ignored, and the only workaround is to
drop the ergonomic string form and construct a `LanguageModel` instance
yourself. The same gap exists for Anthropic and Google.

## Fix

`resolveModel` now passes `baseURL` from the standard env var for each
provider:

| Provider | Env var |
|---|---|
| OpenAI | `OPENAI_BASE_URL` |
| Anthropic | `ANTHROPIC_BASE_URL` |
| Google | `GOOGLE_GENERATIVE_AI_BASE_URL` |

It is `undefined` when the var is unset, so each provider falls back to
its default endpoint — **fully backward compatible** (no behavior change
unless you opt in).

## Tests

Adds `resolve-model-baseurl.test.ts` (4 cases): each provider forwards
its env var to the SDK factory, and an unset var leaves `baseURL`
undefined. All existing `resolveModel` tests pass unchanged (118 runtime
tests green locally).

## Context

Found while building an OpenAI-compatible agent on the V2 runtime +
`@copilotkit/react-native`, where the string model form couldn't reach
the configured endpoint.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 11:49:23 -07:00
Benjamin Taylor 476682330b fix(channels-core): a channel whose adapters ALL fail to start now errors, not false-online
Adversarial-review finding: ɵruntime.start() swallowed every adapter start failure
and resolved, so the runtime reported status "online" on a dead channel (revoked
token, port-in-use). Now: if a channel has adapters and NONE started, start() rejects
so ChannelManager surfaces "error" (a partial start, >=1 adapter live, still counts as
started). Adds a channel-manager failStart regression test + updates the telemetry test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:20:30 -05:00
Benjamin Taylor e6dba4519b feat(runtime)!: Intelligence runtime starts direct-adapter channels (Plan C)
The ChannelManager (only constructed for an Intelligence runtime) now STARTS
direct-adapter channels via channel.ɵruntime.start() instead of recording them
"unmanaged" and skipping — so every channel runs only because Intelligence is
configured; there is no standalone path. Removed the now-dead "unmanaged" status;
direct channels reuse the bounded/idempotent teardown via a synthetic handle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:26:24 -05:00
github-actions[bot] 814aa40d4e style: auto-fix formatting 2026-07-23 22:33:28 +00:00
David McKay 87c6b114db test: fix mock spy arity so check-types (tsc) passes
The vi.fn mock factories were 0-arg but invoked with the provider options in
the vi.mock adapters, tripping tsc (TS2554). vitest transpiles without type-
checking, so it passed vitest locally but failed the check-types job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:31:47 -07:00
David McKay 885e392e70 test: assert union via toBeInstanceOf(z.ZodUnion) instead of untyped _def.typeName
_def.typeName isn't on ZodTypeDef's public type (TS2339 in check-types);
instanceof is the type-safe equivalent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:29:14 -07:00
David McKay 4a7e4c8bdc fix(runtime): preserve anyOf/oneOf unions when converting frontend tool schemas
A frontend/client tool (`useFrontendTool`) whose Zod `parameters` use
`z.discriminatedUnion(...)` — or any schema that serializes to a JSON-schema
`anyOf`/`oneOf` node — silently lost the union-typed field when calling OpenAI.
The tool call arrived with that field missing or empty.

Root cause: the runtime has two JSON-Schema -> Zod converters. The shared one
(`@copilotkit/shared`) already handles `anyOf`/`oneOf` as `z.union`, but the
local copy in `packages/runtime/src/agent/index.ts` — used by the classic
`BuiltInAgent` / AI SDK path via `convertToolsToVercelAITools` — never did.
A union node carries no top-level `type`, so it hit the empty-schema guard and
collapsed to `z.object({})`. The reconstructed tool schema therefore dropped
the union entirely (most visibly for a union nested inside array `items`), so
the model was never offered those fields and could not emit them.

Fix: handle `anyOf`/`oneOf` as `z.union` before the empty-schema guard,
mirroring the shared converter. A single-variant union unwraps to that variant.

Backward compatible: only previously-broken union nodes change behavior
(empty object -> real union). Empty `{}` schemas, typed nodes, and the
`isJsonSchema` gate are untouched.

Repro: a `useFrontendTool` with `parameters: z.object({ blocks: z.array(
z.discriminatedUnion("type", [...])) })` against OpenAI — before, `blocks`
items arrived empty; after, both variants survive into the model call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:57:47 -07:00
github-actions[bot] 93173c672b style: auto-fix formatting 2026-07-23 21:36:53 +00:00
David McKay 5952ea8943 fix(runtime): honor *_BASE_URL env vars in resolveModel for OpenAI-compatible endpoints
resolveModel() built each provider with only an apiKey, so a string model
spec (e.g. "openai/gpt-4o-mini") could not target an OpenAI-compatible
endpoint — OPENAI_BASE_URL was silently ignored, and the only workaround was
to construct the LanguageModel yourself and pass the instance. Same gap for
Anthropic and Google.

Now resolveModel passes baseURL from the standard env var
(OPENAI_BASE_URL / ANTHROPIC_BASE_URL / GOOGLE_GENERATIVE_AI_BASE_URL). It is
undefined when unset, so the provider falls back to its default endpoint —
fully backward compatible.

Tests: adds resolve-model-baseurl.test.ts (4 cases). Existing resolveModel
tests unchanged (118 runtime tests pass locally).

Discovered while building an OpenAI-compatible agent on the V2 runtime +
@copilotkit/react-native.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:35:12 -07:00
Martha Schumann 9e9ce128dd test(runtime): verify packed managed channels dependency 2026-07-16 10:44:10 -07:00
Martha Schumann fea464de52 fix(runtime): install managed channels activation dependency 2026-07-16 10:27:31 -07:00
Tyler Slaton 9504e0666c refactor(channels): add batteries-included package architecture (#5948)
## Summary

- Extract the platform-neutral Channels foundation into
`@copilotkit/channels-core`.
- Make `@copilotkit/channels` the batteries-included consumer entry
point, with adapter and UI subpaths.
- Release the umbrella, core, UI, and all six adapters as one shared
`channels` version scope.
- Verify the packed consumer contract and migrate the Slack and Teams
examples to the umbrella.

## Why

Consumers should be able to install one Channels package without making
the runtime or selective integrations depend on every platform SDK.
Shipping the complete Channels family together prevents adapter/core
version drift and makes the umbrella's exact dependency set release as a
compatible unit.

## How

- Move shared bot/runtime primitives into `channels-core` and invert
adapter/runtime dependencies.
- Add exact workspace dependencies and export subpaths from the umbrella
package.
- Consolidate the existing release configuration and all three release
workflow selectors into one shared `channels` scope containing all nine
packages.
- Use `@copilotkit/channels` as the version source: the next minor
release resolves to `0.2.0` and bumps every Channels package together.
- Publish scoped packages in dependency order for both stable and canary
releases: UI, core, adapters, then the umbrella.
- For a stable Channels release, publish the UI/core/adapters first, run
the registry-backed packed-consumer verifier against those newly
published exact versions, then publish the umbrella.
- Generate Channels release notes from `channels/v*` tags rather than
the monorepo `v*` tags.
- Verify builds, type checks, tests, package artifacts, examples, and
release workflow scope synchronization locally.

### First stable release sequence

1. Bootstrap the currently unpublished `@copilotkit/channels-core`
package on npm and configure npm trusted publishing for every Channels
package against this repository's `release / publish` workflow and `npm`
environment.
2. Create and merge the `channels` minor release PR. The stable workflow
publishes the Channels family in the staged order above and validates
the packed umbrella from the registry before the umbrella is released.
3. Create and merge a subsequent `monorepo` release PR so the published
`@copilotkit/runtime` switches from the historical umbrella dependency
to `@copilotkit/channels-core`.
2026-07-15 13:12:16 -07:00