2330 Commits

Author SHA1 Message Date
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
github-actions[bot] 5baf058e0f style: auto-fix formatting 2026-07-23 21:58:31 +00: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
David McKay 0a582df4dd fix(react-native): add /headless subpath so custom-UI consumers skip chat/attachment native deps
The `@copilotkit/react-native` barrel statically re-exports the prebuilt chat
UI (CopilotChat / CopilotModal / CopilotSidebar / CopilotPopup, which import
`@gorhom/bottom-sheet`) and `useAttachments` (which imports
`expo-document-picker` + `expo-file-system`). Those are optional peer deps, but
a static re-export still forces Metro to resolve them at bundle time. A headless
consumer that uses only `CopilotKitProvider` + `useAgent` + `useFrontendTool`
(a fully custom UI) had to install every chat/attachment native dep or stub them
in `metro.config.js`, or the release bundle fails with
`Unable to resolve module expo-document-picker`.

Add a lean `@copilotkit/react-native/headless` entry that re-exports only the
provider, the platform-agnostic hooks, the render-tool registry, and the
core/AG-UI types — none of the chat UI or `useAttachments` — so those native
deps never enter the bundle graph and the metro-stub workaround is retired.

Mirrors `@copilotkit/react-core/v2/headless` (#5883): a standalone entry file,
wired into the tsdown entry list, the package.json `exports` map, and
`sideEffects` (it side-effect-imports the polyfills). The default barrel now
does `export * from "./headless"` and layers the chat UI on top, so it stays
fully backward compatible. Adds a static import-graph regression test asserting
the headless graph never reaches the chat/attachment modules or their native
peer deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:56:43 -07:00
David McKay 0915ea4d11 fix(react-core): honor threadId prop on useAgent
UseAgentProps had no `threadId`, yet the shipped V2 React Native demo
calls `useAgent({ agentId: "default", threadId })`. Because the prop
didn't exist, it was silently dropped: the demo's thread state (and its
"New Chat" reset) never reached the agent, which ran under its own
auto-minted UUID. threadId was only ever sourced from a surrounding
`CopilotChatConfigurationProvider`, which the headless RN demo has none.

Accept an optional `threadId` on UseAgentProps and honor it. Resolution
precedence: an explicit `threadId` prop wins; otherwise fall back to the
chat configuration's threadId (gated on hasExplicitThreadId, as before).
When the prop is omitted, behavior is unchanged — fully backward
compatible. This makes the RN demo's existing usage work and unblocks
headless callers that have no chat-configuration provider in the tree.

Extends the threadId-propagation contract test with coverage for the
prop path (no provider, prop-over-config precedence, prop re-sync).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:54:58 -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
Ben Taylor a2cdfa6814 fix(react-core): expose lean hooks from /v2/headless to cut bundle bloat (#4893) (#5883)
## What & why

`@copilotkit/react-core`'s `/v2` entry (and the root entry) re-exports
from a
single **monolithic shared chunk**, so importing *any* symbol — even one
hook —
pulls the built-in chat-message rendering stack (`streamdown` → shiki,
`mermaid`,
`cytoscape`, `katex`) into the consumer's bundle: **~3 MB gzip / ~15 MB
raw**,
with no way to tree-shake it. Consumers who build a fully custom chat UI
and only
use hooks pay the full cost. (Issue #4893.)

A separate lean build entry — `@copilotkit/react-core/v2/headless` —
already
ships those hooks in their own small chunk **without** that stack (it's
how
`@copilotkit/react-native` mounts CopilotKit). This PR makes it usable
for a
custom web UI.

Measured with esbuild (react/react-dom external):

| import | bundled JS |
|---|---|
| hooks from `@copilotkit/react-core/v2` | **~2.96 MB** gzip (≈
importing full `CopilotChat`) |
| same hooks from `@copilotkit/react-core/v2/headless` | **~0.03 MB**
gzip |

> Note on the report: `@copilotkit/a2ui-renderer` doesn't bundle the
rich-text
> stack (its deps are `@a2ui/web_core`, `lit`, `clsx`, `zod`). The
weight is
> entirely `streamdown` (shiki/mermaid/cytoscape) + `katex`, pulled by
the
> built-in `CopilotChat*` message components.

## Changes

- **`headless.ts`** — export `useCopilotKit` + `useRenderToolCall` (both
DOM-free
and rendering-stack-free); fix `UseAgentUpdate` (a runtime `enum`) being
  re-exported via `export type`, which stripped its runtime value under
`isolatedModules` (so `useAgent`'s `updates` option was unusable from
headless
— a bug `tsc` can't catch). `useDefaultRenderTool` /
`useRenderCustomMessages`
/ `useRenderActivityMessage` stay in `/v2` (web-only markup or
`a2ui-renderer`).
- **Remove a `tailwind-merge` leak** — extract the tailwind-free ref
helpers
(`shallowEqual` / `useShallowStableRef`) into
`lib/shallow-stable-ref.ts` so the
  headless graph no longer pulls `tailwind-merge` via
  `CopilotChatConfigurationProvider`.
- **Test** — a small vitest export-surface test guarding the hook
surface and the
  `UseAgentUpdate` runtime value.

## Verification

`react-core` typecheck + vitest (1427) and `react-native` typecheck
pass. Shipped
`dist/v2/headless.mjs` imports only `react`, `@ag-ui/client`,
`@copilotkit/core`,
`@copilotkit/shared`, `@copilotkit/react-core/v2/context`, `zod` — no
rendering
stack.

## Notes / follow-ups

- Headless hooks read a **different React context** than the prebuilt
`/v2`
`<CopilotKitProvider>`, so the two can't be mixed — mount a lean
provider over
`/v2/context` (as `@copilotkit/react-native` does). Making the `/v2`
provider
reuse the standalone `/v2/context` singleton would remove that sharp
edge and
  is the natural follow-up.
- Existing bundle-size CI (`compressed-size-action`) already tracks
`headless.mjs`,
  so no new size tooling is added here.

Addresses #4893.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-23 14:56:00 -05:00
Ben Taylor 3edf06339c fix(core): don't surface a user-initiated Stop as an agent error (#5966) (#5967)
Fixes #5966. Follow-up to #5812 / #5885.

## Problem

After #5885 stopped the post-terminal `TEXT_MESSAGE_END` crash, pressing
**Stop** no longer breaks the chat — but for an agent that emits a
terminal `RUN_ERROR` (code `abort`) as its cancellation signal (e.g.
pydantic-ai's `AGUIAdapter`), the client still surfaces that as an
**error banner** ("This operation was aborted"). A user-initiated stop
is expected cancellation, not a failure.

Traced path (no abort suppression at any hop):
`RUN_ERROR(code:"abort")` → `RunHandler.onRunErrorEvent`
(`run-handler.ts`) → `emitAgentError(AGENT_RUN_ERROR_EVENT)` → `onError`
→ CopilotChat/react-ui `triggerChatError` → banner. The only existing
abort suppression is for the *local* fetch-abort rejection
(`run-handler.ts:318`), a different path.

## Fix

Suppress the error emission in `onRunErrorEvent` when the run was
user-aborted — mirroring the local-abort suppression already on the
`runAgent`/`connectAgent` paths:

```ts
const runWasAborted = this._runAbortController?.signal.aborted === true;
if (runWasAborted || event?.code === "abort") {
  return;
}
```

Prefers the client's own `_runAbortController.signal.aborted` (robust —
the agent-supplied `code` isn't standardized across agents; the
`code:"abort"` in the repro comes from the agent, not CopilotKit) with
`code === "abort"` as a secondary signal. Normal `RUN_ERROR`s are
unaffected.

## Testing

- **TDD** (`core-error-handling.test.ts`): two new tests fail pre-fix
and pass after —
- agent emits `RUN_ERROR` code `"abort"` → no `AGENT_RUN_ERROR_EVENT`
surfaced.
- run user-aborted mid-stream (via `agent.abortRun()`, which RunHandler
intercepts to abort the controller) → a subsequent `RUN_ERROR` with a
*non-abort* code is still suppressed (exercises the `signal.aborted`
path).
- The pre-existing test — normal `RUN_ERROR` (code `"bad_request"`)
still emits `AGENT_RUN_ERROR_EVENT` — continues to pass (control against
over-suppression).
- Full `@copilotkit/core` suite green: **578/578**. `tsc` 0 errors;
`oxlint` 0.

Note: this is a UX/product call (a user Stop shouldn't render as an
error). Suppressing in core fixes it for both the default chat banner
and app-level `onError` handlers; the run lifecycle still reflects the
termination.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-23 14:00:01 -05:00
Austin Merrick c89aa21658 docs(showcase): explain existing-app hosted Intelligence setup 2026-07-23 10:50:01 -07:00
Mark Fogle c336514519 chore(deps): widen @anthropic-ai/sdk optional peer range
The optional peer dependency on @anthropic-ai/sdk was pinned to
^0.57.0, which conflicts with current SDK versions (e.g. 0.109) and
forces consumers to install @copilotkit/runtime with --legacy-peer-deps.

The anthropic adapter only relies on stable @anthropic-ai/sdk APIs
(client construction, messages.create, ephemeral cache_control), so
loosen the range to >=0.57.0 to remove the false peer conflict.

pnpm auto-installs this optional peer, so its specifier is mirrored in
the lockfile; update that specifier line to match. The resolved version
(0.57.0) still satisfies the range, so resolution is otherwise unchanged
and `pnpm install --frozen-lockfile` passes.

Reported by an outside contributor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:42:12 +00:00
Ben Taylor f239f6a2cd fix(react-core): forward refs through DropdownMenuTrigger and TooltipTrigger (#5936)
## Summary

`CopilotChatInput`'s `AddMenuButton` renders its trigger as nested Radix
`asChild` slots:

```tsx
<TooltipTrigger asChild>
  <DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>
</TooltipTrigger>
```

Radix `asChild` slots forward a ref to their child.
`DropdownMenuTrigger` and `TooltipTrigger` were plain function
components, so under React 18.3 the forwarded ref triggers this warning
on every render (reported in #5744):

```
Warning: Function components cannot be given refs. Attempts to access this ref will fail.
Did you mean to use React.forwardRef()?
```

## Change

Wrap `DropdownMenuTrigger` and `TooltipTrigger` in `React.forwardRef`,
forwarding the ref to the underlying Radix primitive — mirroring
`forwardRef` usage already present in this package (e.g. `Button`,
`CopilotChatInput.TextArea`). No behavior change; the ref now reaches a
DOM node and the warning is gone.

## Scope

Limited to the two primitives in the reported warning path. The other
shadcn/ui primitives in `react-core` follow the same React-19
(no-`forwardRef`) style and would warn identically when used as
`asChild` children under React 18.3 — happy to extend this to the rest
if you'd prefer full React 18.3 coverage.

## Verification

- `nx build @copilotkit/react-core` succeeds (compiles source +
generates type declarations).
- Pre-commit `test-and-check-packages` passes.

Fixes #5744
2026-07-23 12:30:42 -05:00
Ben Taylor b038115174 fix(sdk-js): preserve AIMessage metadata in middleware (#6091)
## What does this PR do?

- Preserve non-tool `AIMessage` fields when frontend tool calls are
intercepted in `afterModel` and restored in `afterAgent`, including
`additional_kwargs`, `response_metadata`, `usage_metadata`,
`invalid_tool_calls`, `id`, and `name`.
- Use a shared rebuild helper with `tool_calls` as the source of truth.
For LangChain v1 content blocks, remove stale `tool_call` and
`tool_call_chunk` blocks before reconstructing the message.
- Add regression coverage for the `reasoning + backend/frontend tool
calls -> afterModel -> afterAgent` flow, including metadata preservation
and v1 content block synchronization.

## Related PRs and Issues

- Closes #6087
- Related to #4759 and #5308

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] Documentation is not required because this is an internal
middleware bug fix with no public API changes
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
2026-07-23 12:01:03 -05:00
MikeRyanDev cb4ab7fd8e chore: release angular v0.3.0 2026-07-23 16:47:06 +00:00
MikeRyanDev 69861f13df chore: release monorepo v1.63.2 2026-07-23 16:15:51 +00:00
Mike Ryan 053b00e846 fix(angular): match React MCP Apps sandbox 2026-07-23 08:51:34 -07:00
Mike Ryan cd0b5b4061 fix(angular): address SDK review findings 2026-07-23 07:48:25 -07:00
Mike Ryan fec70d086f feat(angular): checkpoint 2 - core and package 2026-07-23 07:14:55 -07:00
Manfred Steyer b3de484be1 feat(angular): MCP Apps activity renderer
Adds the opt-in secondary entry point @copilotkit/angular/mcp-apps for
rendering MCP Apps (MCP ext-apps) inline in the chat:

- CopilotMCPAppsActivityRenderer plus a ready-to-register
  mcpAppsActivityRendererConfig for activityType mcp-apps
- CopilotMCPAppsWidget loads the app's ui:// resource from the configured
  MCP server, embeds it in a sandboxed iframe, and connects an AppBridge
  that relays tool input/result, size changes, links, and log messages
- provideMCPApps registers server URLs and the host identity, capabilities,
  and context announced to embedded apps
- @modelcontextprotocol/sdk and @modelcontextprotocol/ext-apps are optional
  peer dependencies; the main entry point stays free of MCP imports

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 07:14:55 -07:00
shixi-li abcefa1bdb fix(react-core): forward credentials to provisional agents 2026-07-23 16:13:51 +08:00
BenTaylorDev a90ff38b46 chore: release angular v0.2.0 2026-07-21 16:09:22 +00:00
xiaoqinvar d5186bfd60 test(sdk-js): consolidate AIMessage middleware regression 2026-07-21 21:07:38 +08:00
xiaoqinvar a09d05bbdc docs(sdk-js): explain AIMessage rebuild behavior 2026-07-21 20:12:56 +08:00
xiaoqinvar c336f981d1 refactor(sdk-js): clarify v1 content block filtering 2026-07-21 19:47:41 +08:00
xiaoqinvar 68485c2d63 fix(sdk-js): preserve AIMessage metadata in middleware 2026-07-21 19:19:41 +08:00
Ben Taylor 7fb0abf60e fix(vue): keep attachment sources structuredClone-safe (#5933)
## What does this PR do?

Fixes a Vue `DataCloneError` that occurred when uploaded attachment
sources crossed the `structuredClone` boundary in core. Vue’s deep
`ref()` conversion wrapped nested attachment sources in reactive
proxies; `useAttachments` now keeps the attachment container shallow
with `shallowRef()`, preserving externally supplied sources as raw
cloneable values before they reach AG-UI/core payloads.

The change is intentionally Vue-only: core and React are untouched
because the defect is caused by Vue’s reactivity behavior at the
framework boundary. Focused regressions cover both the attachment hook
and `CopilotChat` submission path, including non-reactivity and
successful `structuredClone` behavior.

## Related PRs and Issues

- [CopilotKit issue
#3](https://github.com/enekesabel/CopilotKit/issues/3)

## Verification

- `pnpm nx run @copilotkit/vue:check-types` — passed.
- `pnpm nx run @copilotkit/vue:test --
src/v2/hooks/__tests__/use-attachments.test.ts
src/v2/components/chat/__tests__/CopilotChat.attachments.test.ts` —
passed, 18 tests in 2 files.
- `pnpm nx run @copilotkit/vue:build` — passed.
- Pre-commit package gate (`test-and-check-packages`) — passed: 1073
tests, publint, and attw.
- `pnpm nx run @copilotkit/vue:lint` — remains blocked by 171
pre-existing errors across unrelated Vue files; no lint errors were
introduced in the changed files.
- `git diff --check upstream/main...HEAD` — passed.

## Scope and exclusions

- Changed files are limited to
`packages/vue/src/v2/hooks/use-attachments.ts`, its focused hook and
`CopilotChat` tests, and the related `packages/vue/PARITY.md` and
`packages/vue/AGENTS.md` guidance.
- No core, React, workflow, or package-wide lint cleanup is included.
- The `PARITY.md` change removes accidental table-format churn and
retains only the meaningful attachment parity note.

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/CopilotKit/CopilotKit/blob/main/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
2026-07-20 11:43:28 -05:00
Alem Tuzlak bc2916efaa fix(runtime-client-gql): guard abort errors without message (#5437)
Fixes #2596

## Summary
`@copilotkit/runtime-client-gql` was still assuming abort-shaped errors
always expose a string `message`, which could turn an early-stop path
into a secondary TypeError instead of a clean abort suppression or the
original failure. This branch centralizes abort detection behind a
null-safe helper and keeps the existing abort phrases unchanged.

## Changes
- Replaced the duplicated abort checks in
`packages/runtime-client-gql/src/client/CopilotRuntimeClient.ts` with a
shared `isAbortError(unknown)` helper that only inspects string
messages.
- Added focused regression coverage in
`packages/runtime-client-gql/src/client/__tests__/CopilotRuntimeClient.test.ts`
for a string abort cause, an object without `message`, known abort
suppression in the stream path, and non-abort stream errors surfacing
normally.
- Added `.changeset/guard-abort-error-message.md` for the patch release
note.

## Scope
Only `packages/runtime-client-gql` and its local changeset are touched.
The abort phrases, caller-facing API, structured GraphQL error handling,
and stream close behavior for known aborts are unchanged.

## Test Plan
- [x] `npx nx run @copilotkit/runtime-client-gql:graphql-codegen` -
regenerated the package GraphQL artifacts used by the client imports.
- [x] `pnpm run build` - workspace build completed successfully.
- [x] `pnpm -C packages/runtime-client-gql exec vitest run
src/client/__tests__/CopilotRuntimeClient.test.ts` - 4/4 passed. Covers
a string abort cause in the fetch path, an object with no `message` in
the fetch path, known abort suppression in the stream path, and
non-abort stream errors surfacing.
- [x] `pnpm exec oxfmt --write
packages/runtime-client-gql/src/client/CopilotRuntimeClient.ts
packages/runtime-client-gql/src/client/__tests__/CopilotRuntimeClient.test.ts`
- formatted the touched source and test files.
- [x] `pnpm exec oxlint
packages/runtime-client-gql/src/client/CopilotRuntimeClient.ts
packages/runtime-client-gql/src/client/__tests__/CopilotRuntimeClient.test.ts`
- 0 warnings, 0 errors.
2026-07-20 16:17:58 +02:00
Aria Zhao e5c1af418a fix(react-core): expose isReady from useAgent so subscriptions can target the real agent (#5000)
`useAgent` always returns a fully-constructed `AbstractAgent`: a provisional
stand-in while the runtime is still connecting (or in an error state), swapped
for the real agent once the `/info` sync resolves. The returned type claimed
`agent` was always the real agent, giving consumers no way to tell the two
apart — so one-time subscriptions (e.g. `onRunFinalized`) registered during the
provisional window landed on the placeholder and missed events until the effect
re-ran after the swap.

Add an `isReady` flag to the return value: `false` while the agent is
provisional, `true` once the real (or locally-registered) agent is bound.
Additive and backward compatible.

Also fix the docs' "Event Subscription" example, which used an empty
`useEffect` dependency array and therefore never re-subscribed when the agent
reference changed.

Note: the original crash from #5000 ("Cannot read properties of undefined
(reading 'subscribers')") no longer reproduces on `main` — the provisional-agent
work (#5533/#5635) guarantees a fully-constructed agent, so `subscribe()` is
always safe. The added tests lock in that no-crash behavior and cover the new
`isReady` transition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 04:39:46 +00:00
Tyler Slaton b7d2fdcc25 fix(channels-intelligence): HTTP-transport robustness cluster (OSS-497) (#6037)
Six pre-existing `@copilotkit/channels-intelligence` HTTP-transport
robustness items surfaced by the pre-merge CR of #5983 (Linear OSS-497).
All confirmed against `main` after #5983 merged; none introduced by it.
Each is an independent, focused commit with a test.

## Fixes

1. **Heartbeat starvation mid-turn** — `HttpDeliverySource.runLoop`
heartbeated only at the top of each iteration, then blocked on
`onDelivery` for up to `turnTimeoutMs` (120s). With a 15s cadence, a
turn longer than the cadence sent no heartbeat, so app-api could mark a
healthily-working runtime stale mid-turn and withhold new deliveries.
Heartbeating now runs on a standalone recurring timer, independent of
the claim loop.
2. **`stop()` shutdown latency** — `stop()` set `running=false` then
awaited the loop, which only rechecked after its current sleep (≤15s
idle) or `onDelivery` (≤120s mid-turn); sleeps were `unref`'d but not
interruptible. Added a `stopWait` promise that the poll-sleep and
turn-wait both race, so shutdown is prompt. A mid-turn stop leaves the
lease for app-api to re-lease and does **not** nack (the turn didn't
fail); the turn's eventual settlement is always handled so a post-stop
rejection never surfaces as unhandled. *(1 & 2 share the runLoop
lifecycle, so they land in one commit.)*
3. **Empty-text `update` silently acked** — the `if (!text) return { ok:
true }` guard ran for both `post` and `update`. An empty POST is a legit
no-op, but an empty UPDATE (e.g. clearing a message body) that this
post-only fallback egress can't express was acked as success. Now
returns `{ ok: false, code: "empty_update" }`; empty posts still no-op.
4. **Static `adapter` on egress** — `emit` posted `this.cfg.adapter`
(default `"slack"`) alongside a possibly-Teams `replyTarget`. Confirmed
app-api's egress route (`sendChannelEgressMessage`) routes on
`replyTarget.adapter` + `channelName` and ignores this field, so it's
latent — but now derived from the delivery's own reply route to avoid
the contradiction. *(Note: the listener heartbeat's
`declaredChannels[].adapter` is intentionally left alone — app-api
genuinely consumes it for per-adapter health/conflict via the
`channel_adapter_configs.provider` join, and declaring the bot's full
adapter set is a separate design change, not a robustness cleanup.)*
5. **`projectId` strict-`number` only** — the realtime scope build
accepted `projectId` only as a JS `number` while org/channel were
`String()`-coerced, so a numeric-string on the untyped wire silently
fell back to the transport-default projectId, defeating per-delivery
scope authority. Extracted a `coerceWireProjectId` helper (number or
numeric-string → positive integer) with fallback.
6. **File/history client ignored injectable `fetch`** —
`fetchFile`/`getHistory`/`uploadFile` hardcoded `globalThis.fetch`, so
an injected `config.fetch` was honored everywhere except
binary/history/upload. The transport's `FetchLike` is text-only and
can't satisfy the binary client, so the file/history client got its
**own** injectable full `fetch` (`typeof fetch`), resolved via a single
helper with a global fallback.

Plus a small `docs` commit fixing a doc-comment placement displaced by
the item-5 export.

## Testing

- `nx run @copilotkit/channels-intelligence:test` — **178 passed** (170
baseline + 8 new).
- `nx run @copilotkit/channels-intelligence:check-types` — clean.
- lefthook (full package test + typecheck) ran on every commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-17 17:16:32 -07:00
Benjamin Taylor 15a8ddb0f7 fix(channels-intelligence): address adversarial CR findings (OSS-497)
Follow-up to the OSS-497 robustness cluster after a 3-agent adversarial review.

- Empty-text update: reclassify from a hard {ok:false} failure to a logged
  no-op. The failure path threw up the run and nack'd the WHOLE turn retryably,
  re-running valid work to dead-letter for a condition a retry can't fix (an
  agent clearing a message body the post-only fallback can't express). Now it is
  skipped with a distinct warning log — surfaced, not silent, and not turn-fatal.
- Injectable fetch: plumb it through the transports. Both HttpDeliverySource and
  RealtimeGatewayTransport now accept a binary-capable /
  and forward it to IntelligenceFileHistoryClient, so a consumer injecting a
  fetch has it honored on the download/history/upload paths (previously the
  client-level injectable was reachable only by constructing the client directly).
- coerceWireProjectId: use Number.isSafeInteger so a numeric string beyond
  MAX_SAFE_INTEGER is rejected (was silently coerced to a lossy integer).
- Malformed-present projectId: log before falling back to the transport scope
  (an absent projectId still falls back quietly), mirroring the loud drops in
  the same function instead of silently masking a wire-corruption signal.
- Restart safety: guard the recurring heartbeat with a run generation so a
  stop->start while a heartbeat POST is in flight can't leave two reschedule
  chains doubling the heartbeat rate; bump the generation on stop().
- Remove now-dead lastHeartbeatAt state (only reader was the removed in-loop
  heartbeat check).

Tests: empty-update now asserts log + no-op; added transport fileFetch
wire-through, adapter-from-route fallback, no-unhandled-rejection after a
mid-turn stop, heartbeat-stops-after-stop, elapsed-time bounds on the stop()
promptness tests, and coerceWireProjectId edge cases (leading zeros, > safe int).
nx test (green) + check-types (clean).
2026-07-17 15:22:10 -05:00
Benjamin Taylor 81562f5b24 docs(channels-intelligence): restore assertValidChannelRealtimeScope doc placement
The coerceWireProjectId export (added for OSS-497) was inserted directly above
assertValidChannelRealtimeScope, displacing that function's doc comment onto the
helper. Reorder so each function carries its own docblock.
2026-07-17 14:46:27 -05:00
Benjamin Taylor 0d412f3bb8 fix(channels-intelligence): honor an injectable fetch in the file/history client
IntelligenceFileHistoryConfig's fetchFile/getHistory/uploadFile hardcoded
globalThis.fetch, so a consumer (or test) injecting a fetch had it honored
everywhere EXCEPT the binary download, history, and upload paths. The transport's
FetchLike is text-only ({ ok, status, text() }) and cannot satisfy the binary
client (.body/.arrayBuffer()/.headers/.json()), so add the client's OWN
injectable full `fetch` (typeof fetch) on its config, resolved via a single
resolveFetch() helper that falls back to the global fetch. Each caller keeps its
existing degrade/throw contract for the no-fetch runtime.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:45:34 -05:00
Benjamin Taylor ba38ee0e5f fix(channels-intelligence): heartbeat during long turns and make stop() prompt
Two coupled HttpDeliverySource runLoop lifecycle robustness fixes:

Heartbeat starvation: the loop heartbeated only at the top of each iteration,
then blocked on onDelivery for up to turnTimeoutMs (120s). With a 15s cadence,
a turn longer than the cadence sent no heartbeat for its whole duration, so
app-api could expire the activation and mark a healthily-working runtime stale
mid-turn, withholding new deliveries. Move heartbeating to a standalone
recurring timer that fires on cadence regardless of what the loop is doing
(rescheduling only after each heartbeat settles, so no overlap).

Shutdown latency: stop() set running=false then awaited the loop, but the loop
only rechecked after its current sleep (up to cadence, idle) or onDelivery (up
to turnTimeoutMs, mid-turn) — the sleeps were unref'd but not interruptible, so
shutdown could take up to 15s idle or 120s mid-turn. Add a stopWait promise that
stop() resolves; the poll-sleep and the turn-wait both race it, so shutdown is
prompt. A mid-turn stop leaves the lease for app-api to re-lease and does NOT
nack (the turn didn't fail); the turn's eventual settlement is always handled so
a post-stop rejection never surfaces as unhandled.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:42:39 -05:00
Benjamin Taylor 787264fa8f fix(channels-intelligence): accept a numeric-string projectId in the realtime scope build
RealtimeGatewayTransport.toIngressEnvelope built the per-delivery scope with
`typeof delivery.projectId === 'number' ? delivery.projectId : this.scope.projectId`,
while organizationId/channelId were String()-coerced. The delivery.available
payload is untyped JSON, so a numeric-string projectId ('9' on the wire) failed
the strict check and silently substituted the transport-default projectId —
defeating the per-delivery scope authority and routing the render-accept under
the wrong project.

Extract a `coerceWireProjectId` helper (number or numeric-string -> positive
integer, else undefined) and use it with a fallback to the transport default.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:35:49 -05:00
Benjamin Taylor 46517eecd9 fix(channels-intelligence): derive egress adapter from the reply route
HttpEgressSink.emit posted the static `this.cfg.adapter` (default "slack")
alongside a possibly-Teams `replyTarget`, so a Teams delivery through the
fallback egress carried a contradictory `adapter: "slack"`. app-api's egress
route (sendChannelEgressMessage) routes on `replyTarget.adapter` + channelName
and ignores this top-level field, so this is latent — but one provider-agnostic
runtime serves every adapter its bot has attached, so posting the config default
is misleading. Derive the posted adapter from the delivery's own reply route,
falling back to the config adapter when the route carries none.

Note: the listener heartbeat's declaredChannels[].adapter is NOT changed here —
app-api genuinely consumes it (the channel_adapter_configs.provider join) for
per-adapter health/conflict reporting, and the runtime only knows its single
configured adapter; declaring the bot's full adapter set is a separate design
change, not this robustness cleanup.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:32:42 -05:00
Benjamin Taylor 060cebf304 fix(channels-intelligence): fail loud on empty-text egress update
HttpEgressSink.emit ran the `if (!text) return { ok: true }` no-op guard for
both post and update ops. An empty POST is a legitimate no-op (nothing to say),
but an empty UPDATE — a real intent such as clearing a message body that this
post-only fallback egress cannot express — was acked as success while nothing
was sent, inconsistent with the module's fail-loud posture. Return
`{ ok: false, code: 'empty_update' }` for that case; empty posts still no-op.

Surfaced by the pre-merge CR of #5983 (OSS-497).
2026-07-17 14:28:25 -05:00
Benjamin Taylor 0b69bb4b4d docs(channels): clarify read_thread getMessages is text-only (OSS-488)
The getMessages JSDoc cited "what was in the image" as a use case, but
image/file parts contribute no text in this mapping — read_thread is
text-only. Image content reaches the model only via conversationStore's
seeding of agent.messages. Tweak the comment so it no longer implies
read_thread can see image content.
2026-07-17 13:52:36 -05:00
AlemTuzlak 21b9a7138b chore: release channels v0.2.1 2026-07-17 09:13:35 +00:00
Ben Taylor 0c9d639b13 chore: release monorepo v1.63.1 (#6019)
## Release monorepo v1.63.1

**Scope:** `monorepo` | **Bump:** `patch`

---

### How this release process works

1. **This PR was created automatically** by the "release / create-pr"
workflow.
   It bumped the `monorepo` packages to `1.63.1`
   and generated AI-enhanced release notes.

2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
   must pass before merging. This is the review gate.

3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.

4. **When this PR is merged**, the `release / publish` workflow
automatically:
   - Builds all packages
   - Publishes the `monorepo` packages to npm at version `1.63.1`
   - Creates git tag `monorepo/v1.63.1`
   - Creates a GitHub Release with the final release notes

### Before merging

- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)

---

> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
2026-07-16 13:43:24 -05:00
tylerslaton a7459f4fb2 chore: release monorepo v1.63.1 2026-07-16 18:23:58 +00:00
Benjamin Taylor 052dc0948e fix(channels): address CR — case-insensitive Teams hex + test/comment hygiene
- emoji: match Teams codepoint hex case-insensitively (providers may send
  upper-case) + document single-codepoint-only parsing; cover 1F504_refresh.
- emoji: note the dormant outbound Teams asymmetry in toPlatformEmoji
  (unicode vs `<codepoint>_<name>`) as a TODO for when outbound reactions land.
- channels-slack: split the tool-only status-clear test so one mirrors the
  real event order (RUN_FINISHED before finish()) and one isolates the
  finish() backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 12:55:57 -05:00
Alem Tuzlak db65524aad fix(channels-slack): clear the "is thinking" status on finish for tool-only replies
The run renderer only cleared Slack's native "is thinking…" assistant
status from onFirstReply() (streamed-text paths) or the error/interrupt
paths. A turn whose reply streamed NO text — a tool-only / file-only
reply such as a posted chart — never triggered onFirstReply, so the
status indicator lingered forever.

finish() now clears the status as a backstop when statusMode is on and
no reply was posted. The postedReply guard prevents a redundant clear on
the normal streamed-text path (onFirstReply already cleared it).

Adds regression tests covering both the tool-only clear and the
postedReply guard.
2026-07-16 12:55:57 -05:00
Alem Tuzlak a06c8cf1be feat(channels): canonical cross-platform reaction normalization for Teams & WhatsApp
Normalize inbound reaction emoji to a canonical cross-platform name at the
central channels-core ingress, so onReaction handlers match one value
regardless of provider.

- channels-ui/emoji: add "teams" and "whatsapp" to EmojiPlatform and canonical
  entries for refresh/laugh/surprised/sad/angry. Teams normalizes its
  <codepoint>_<name> emoji codes (1f504_refresh -> refresh) and classic bare
  names (like -> thumbs_up); an out-of-range codepoint degrades to passthrough
  instead of throwing. WhatsApp uses the unicode path like Discord/Telegram.
- channels-core: add teams+whatsapp to EMOJI_PLATFORMS; IncomingReaction gains
  an optional source `platform`; onReaction normalizes by
  evt.platform ?? adapter.platform.
- channels-intelligence: the managed reaction dispatch passes the delivery's
  source platform (env.platform) so managed reactions normalize too.
2026-07-16 12:55:57 -05: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
tylerslaton 6c354037fc chore: release monorepo v1.63.0 2026-07-15 22:18:07 +00:00
tylerslaton add6539dcd chore: release channels v0.2.0 2026-07-15 20:29:29 +00: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
github-actions[bot] 8e40a7e33c style: auto-fix formatting 2026-07-15 19:18:36 +00:00