Commit Graph

3049 Commits

Author SHA1 Message Date
Benjamin Taylor c94032f3fa feat(react-ui): report feedback state to onThumbsUp/onThumbsDown
The thumbs callbacks only received the message, so a consumer could not
tell whether a click applied feedback or retracted it. Custom
AssistantMessage implementations that track their own toggle state had
no way to pass that value through, and the built-in feedback state was
write-only: clicking an active button re-applied the same value.

Add an optional second argument reporting the state the click
transitions to. The built-in AssistantMessage derives it from the
message's current feedback, which also makes the built-in buttons
toggle; a custom AssistantMessage may pass its own value. The argument
is optional, so existing one-argument handlers are unaffected.

The toggle logic is extracted to ./feedback so it can be unit tested —
this package's vitest project runs in a node environment and has no
component-rendering harness.

Closes #2615
2026-08-17 17:22:24 -05:00
Benjamin Taylor 1ea18ca864 fix(react-ui): pad the chat header on mobile viewports
The `.copilotKitHeader` rule declared `padding-right` only inside
`@media (min-width: 640px)`, so below that breakpoint the header's
`space-between` children ran flush to the viewport edge and the close
button had no gutter.

Move the horizontal padding into the unconditional rule so it applies at
every viewport. The value matches the 24px the sm breakpoint already
used, so wide layouts are unchanged.

Fixes #2493
2026-08-17 17:22:21 -05:00
Benjamin Taylor 6e3ed6e319 fix(core): copy the tool array handed to initialize
The merged tool view is memoized, so holding the caller's array meant an
in-place mutation of it would go unnoticed. `setTools` already copied;
`initialize` now matches.

Also documents why the memo exists: `tools` is public and returned a stable
array instance until a registry write, which consumers may rely on as an
effect dependency — rebuilding on read would hand out a new array each time.
2026-08-17 12:41:54 -05:00
Benjamin Taylor b17c8556be feat(react-core): let a catch-all action wait for a response
`useCopilotAction({ name: "*" })` could only ever render. `getActionConfig`
short-circuited on the wildcard name before it looked for a wait-render, so
a catch-all declaring `renderAndWaitForResponse` was silently downgraded to
a render-only action: the render was called without `respond`, and in
practice threw `render is not a function` because the render-only path reads
`action.render`. Handling N human-in-the-loop tools therefore required N
separate hooks.

A catch-all now routes to the human-in-the-loop path when it declares a
wait-render, so the tool call stays pending until `respond` is called. Core
already had the execution half (`getWildcardTool`/`executeWildcardTool`),
so this is a routing fix rather than new machinery.

Two supporting changes make the render usable:

- The HITL render props now carry the name of the tool actually being
  called. It equals the registration name for a normal action, but a
  catch-all needs it to tell N tools apart, and `"*"` was being written over
  it in both the v1 wrapper and the v2 hook.
- `CatchAllFrontendAction` accepts `renderAndWaitForResponse`/`renderAndWait`
  alongside `render`, mutually exclusive as on `FrontendAction`, with
  `CatchAllActionRenderPropsWait` exported for annotating the props.

Fixes #1746
2026-08-17 12:41:54 -05:00
Benjamin Taylor 31aa1e2162 fix(core): keep hook-registered tools across provider re-syncs
Frontend tools reached the registry two ways that shared one array:
provider-owned tools via `initialize`/`setTools`, and imperatively-added
ones via `addTool` (`useFrontendTool`, `useHumanInTheLoop`, direct
`core.addTool()` calls). Because `setTools` replaced the array wholesale,
every provider re-sync after mount silently wiped the hook tools.

A runtime with `openGenerativeUI` enabled made this reproducible on every
mount: the `/info` response flips the flag asynchronously, the provider
re-derives its tool list to add `generateSandboxedUi`, and the re-sync
dropped everything the hooks had registered — so the agent received only
`generateSandboxedUi` and the app's own tools were never advertised.

Split the two owners into their own buckets, merged on read with hook
entries taking precedence, mirroring what `CopilotKitCoreReact` already
does for render tool calls. `addTool` now shadows a provider tool of the
same name instead of refusing to register, and only warns when another
imperative registration already holds the name.

Also stop advertising a wildcard ("*") tool to the agent. It is a local
catch-all handler for calls with no exact match, so offering the model a
tool literally named `*` was never meaningful.

Fixes #4952
2026-08-17 12:41:54 -05:00
Lukas Moschitz d45a478a83 fix(channels-slack): rename decimal_allowed to Slack's is_decimal_allowed
`SlackNativeProps` declared `decimal_allowed`. Slack's `number_input` field is
`is_decimal_allowed`. Slack accepts a message whole or not at all, so the
unrecognised key refused the entire `chat.postMessage` call with
`invalid_blocks: invalid field at /blocks/N/element` and ended the Channels
delivery that carried it — no exception reached the caller and nothing arrived
in the channel.

The typed path was therefore the broken one: a developer who bypassed our types
and hand-wrote `is_decimal_allowed` got it right, while everyone using
`Slack.Element.NumberInput` silently lost the message.

Verified live against Slack, one delivery per case:

- `decimal_allowed` alone — refused, `invalid_blocks: invalid field at
  /blocks/0/element`
- `is_decimal_allowed` alone — delivered
- both names present — refused with the same error, which isolates the unknown
  key as the cause: Slack's required field is satisfied and the payload is
  otherwise identical to the one that delivers

Not treated as a breaking change and no alias is kept. The old name never
worked, so no caller can depend on its behaviour, and keeping it would preserve
the trap. Removing it converts a silent message loss into a compile error that
names the right field.

Also adds a test comparing every field name `native.ts` declares against
Slack's own Block Kit declarations in `@slack/types` (already a dependency),
parsed with the TypeScript parser rather than matched as text. Names
`@slack/types` does not cover — `container`'s `blocks`, `rich_text_list`'s
`offset`, `card`'s `slack_icon` and `subtext`, and the whole
`data_visualization` vocabulary — are listed individually with the reason, so
the check states its coverage instead of implying completeness. Floors on the
vocabulary size and on the number of names actually compared keep a collapsed
comparison from passing as success.

Sweeping the remaining names turned up no other mismatch.
2026-08-17 12:49:10 +02:00
Murat Sari 703944880d feat(angular): expose agent capabilities 2026-08-16 22:11:36 +02:00
Murat Sari 5720cd7fdc feat(angular): support fetch credentials 2026-08-16 21:03:52 +02:00
Benjamin Taylor e86e7be328 fix(packages): declare the MIT SPDX license on five published packages
@copilotkit/core, /agentcore-runner, /sqlite-runner, /voice and
/web-inspector all publish to npm without a "license" field, so registry
metadata and automated license scanners report them as Unknown. The repo is
MIT (see LICENSE) and every other published @copilotkit/* package already
declares it; these five were simply missed.

Reported downstream in #2860, where a corporate procurement scan blocked on
Unknown-license dependencies. Note that the @ag-ui/* packages named in that
report are NOT affected -- they have carried "license": "MIT" since at
least 0.0.35, including every version the reporter's scanner flagged.

Placed before "repository" to match the sibling packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 11:45:18 -05:00
JYbill 12e89e60fa Merge branch 'main' into fix/state-manager-run-id 2026-08-15 17:48:03 +08:00
tylerslaton 1f9b60b231 chore: release monorepo v1.68.1 2026-08-14 21:05:45 +00:00
tylerslaton f8cb4d2447 chore: release channels v0.9.0 2026-08-14 20:42:01 +00:00
tylerslaton e6864b6bdd chore: release monorepo v1.68.0 2026-08-14 20:11:31 +00:00
Lukas Moschitz f248a7eb30 feat(channels-slack): render table cells as rich_text when they carry markup
Portable <Cell> content was always emitted as a Slack `raw_text` cell, which
is literal: markdown links, Slack link syntax and bare URLs all rendered as
plain characters, so there was no way to get a clickable link or bold text
into a table cell through the portable vocabulary.

Body cells whose content contains a link, bold, italic, strikethrough or
inline code are now emitted as a `rich_text` cell. Plain content still
produces the byte-identical `raw_text` payload, and header cells always stay
`raw_text` (Slack renders them bold already, and `rich_text` is not allowed
in a `data_table` header cell).

The conversion reuses `markdownToMrkdwn` — the package's single source of
truth for the portable dialect — and tokenizes its `mrkdwn` output into
rich-text runs, so the package keeps one markdown parser. The 2000-char cell
budget now applies to the visible text of a rich cell.
2026-08-14 11:52:53 -07:00
Maximiliano Korp eb3f430ae1 feat(runtime): mark Learning config experimental 2026-08-14 10:43:51 -07:00
Mike Ryan 99da13de53 fix(runtime): preserve Learning compatibility contracts 2026-08-14 10:34:44 -07:00
Mike Ryan a9f283ab55 feat(runtime): assign threads to Learning Containers 2026-08-14 10:34:27 -07:00
Benjamin Taylor e7e8f7dc37 fix(runtime): only treat a request stream as consumed once it is drained
`isStreamConsumed` checked `req.complete` and the private
`_readableState.ended`/`endEmitted` alongside `req.readableEnded`. The first
two are set by the Node HTTP parser once all network bytes reach the socket,
which happens before the route handler reads anything. Any framework that
awaits between socket read and dispatch — notably the Next.js pages router —
therefore reported an unread body as already consumed.

With `bodyParser: false` there is no `req.body` to rebuild the request from
either, so `copilotRuntimeNodeHttpEndpoint` logged "Request stream consumed
with no available body" and forwarded an empty payload upstream, and the
request failed with `400 Invalid JSON payload`.

Rely only on `readableEnded`, which flips true after the `end` event fires from
genuinely draining the stream. The `parsedBody !== undefined` check at the call
site still covers the body-parser case.

Diagnosed by @AlexNti in #3489, which patched the since-retired
`packages/v1/runtime` path; re-applied here on `packages/runtime` with the
tests ported and a live `http.IncomingMessage` regression test added.
2026-08-14 08:37:25 -05:00
Alem Tuzlak 4e9eee3094 feat(runtime): add MiniMax built-in models (#6464)
Reason: Add the current MiniMax text models to BuiltInAgent model
resolution.

- Register MiniMax-M3 and MiniMax-M2.7 as built-in model identifiers.
- Resolve MiniMax model strings through the global endpoint with API key
and regional base URL configuration.
- Document both model specifiers and cover global and China endpoint
selection.

Checks:
- `node_modules/.bin/nx run @copilotkit/runtime:test --
src/agent/__tests__/resolve-model-baseurl.test.ts`
- `node_modules/.bin/nx run @copilotkit/runtime:check-types`
- `pnpm validate:model-names`
- `node_modules/.bin/nx format:check
--files=packages/runtime/src/agent/index.ts,packages/runtime/src/agent/__tests__/resolve-model-baseurl.test.ts`
- `git diff --check`
2026-08-13 18:57:51 +02:00
Ben Taylor 7187a0aa19 fix(channels-slack): three defects that silently broke Slack Block Kit (#6462)
Closes OSS-819. Part of OSS-794, which stays open for the OpenTag
demonstration (OSS-820).

*Reopened from #6454 — the branch was renamed so Linear links the right
sub-issue, and GitHub closed the original rather than retargeting it.
Same three commits, unchanged.*

Three defects in the Slack Block Kit catalog, each verified against a
real workspace. **99 lines changed across three files.**

The reason these sat undetected matters more than their size: **a
payload Slack refuses produces no error anywhere.** No log line, no
exception, no failing test — the message simply never arrives, which is
indistinguishable from a bot that had nothing to say. The renderer
compounds it by design, dropping unknown nodes silently so one bad node
cannot fail a whole message.

## 1. `container` was refused on every send

Its children serialized into `blocks`; Slack reads `child_blocks`.

## 2. Every menu, checkbox, radio group, overflow and confirm dialog was
refused

The codec stamped `type` onto every catalog entry, including composition
objects whose schema has none — Slack's option object is `{text,
value}`, and the same holds for `confirm`, `option_group`,
`conversation_filter`, `dispatch_action_config`, `slack_file`, `trigger`
and `workflow`. An unknown field makes Slack reject the entire message,
so the whole interactive surface was unusable through `Slack.Object.*`.

Measured against a live workspace: **1 of 26 block elements delivered
before this fix, 23 after.**

Note the existing `native-catalog.test.ts` asserted the very assumption
that was wrong — that every entry serializes its discriminator. It was
green while the product was broken. It now asserts the corrected rule.

## 3. An image could not use a file already in the workspace

The required-field check demanded `image_url` unconditionally; Slack
accepts `image_url` *or* `slack_file`. An image needs alt text plus
either source now, and passing neither is still an error.

## Two catalog corrections

`file` leaves the authorable manifest. Slack: *"You can't add this block
to app surfaces directly, but it will show up when retrieving messages
that contain remote files."* The same sentence appears verbatim in
`@slack/types`' own doc comment. It is an inbound shape; offering it as
a component meant offering something that can never succeed.

`alert` stays out with its citation — *"Alert blocks are currently only
supported in modals."* Verified rather than assumed: Slack's own example
payload posted verbatim into a message is refused, while a plain section
in the same delivery seconds later arrives.

## How these were found

A fixture per catalog entry — 19 authorable blocks, 26 elements, 15
composition objects — with the expected payload **transcribed from
`docs.slack.dev`, not captured from our serializer**, delivered through
a managed Channel into a real workspace. **55 of 60 deliver.**

That corpus is a working instrument, not a deliverable, so it is
deliberately not part of this PR — ~1700 lines of fixtures to maintain
against a 99-line change is a bad trade for reviewers. It lives with the
team and gets re-run when the catalog moves.

One methodological note, because it changed what we count as proof: the
first live run passed entries that demonstrated nothing. A rich-text
block with one unstyled run renders exactly like a plain section; a
carousel with one card renders like a card. Both were accepted and
worthless as evidence — caught by a human looking at the output, not by
the harness. Fixtures had to *exercise* each entry, and that is what
surfaced defect 2.

## Found in the same pass, tracked separately

- **OSS-817** — the managed path dropped every picker's value (9 of 26
elements). Fixed and confirmed live.
- **OSS-818** — handler ids collide across structurally identical
messages.

## Verification

`test`, `check-types` and `build` green across `channels-slack`,
`channels`, `channels-intelligence` and `runtime`, both with and without
the fixture corpus present. Every block, element and object was
delivered into a live Slack workspace and reviewed by eye.
2026-08-13 11:56:14 -05:00
Alem Tuzlak 44d54c65d6 fix(react-core): repair useCopilotReadable effect deps, convert args, and dependencies (#6409)
Fixes #6383. Fixes #6243.

Both issues land in the same 35 lines of `useCopilotReadable`, so they
are fixed together. This PR also covers a third defect neither issue
reports.

All of it traces to a single commit: 80dffec4e7 ("feat: Reimplement
CopilotKit on top of refreshed internals (v1.50.0)", #2638), which
repointed the hook from the v1 context tree onto the v2 flat context
store. The pre-1.50 implementation was correct on every count below.

## Fixes

**`available` was missing from the effect deps** (#6383)
The effect body read `available` but the deps were `[description, value,
convert]`, so toggling between `"enabled"` and `"disabled"` after mount
did nothing. It is back in the deps, along with the `available =
"enabled"` default the port dropped.

**`convert` was called with one argument** (#6243)
`(convert ?? JSON.stringify)(value)` invoked a user's `(description,
value) => string` as `convert(value)`, so it received the value as
`description` and `undefined` as `value`. The branches are now split
rather than passing two arguments to the combined expression —
`JSON.stringify(description, value)` would treat the second argument as
a *replacer*, not a value.

**`dependencies` was accepted and ignored** (#6243)
The second positional argument was destructured but never reached the
deps array. Now spread, matching `useCopilotAdditionalInstructions`.

**The `found` dedup branch was dead code** (unreported)
It compared `JSON.stringify({ description, value })` against a stored
entry whose `value` had already been serialized by `addContext`
(`packages/core/src/core/context-store.ts:36`). That never matches — for
objects or strings — so the branch and its cleanup-skipping early return
were unreachable. Deleted rather than repaired: making the comparison
work would newly let component A's unmount remove a context entry
component B is still relying on. The test `keeps separate entries for
identical readables in two components` locks that in, and it passes
against the pre-fix hook, which is what confirms the branch never fired.

## `parentId` / `categories`

Both are still in `UseCopilotReadableOptions` and were still documented
— the top-of-file JSDoc example was a `parentId` tutorial — but the same
v1.50 commit dropped them from the hook body. They have been no-ops
since.

This PR does not implement them. Real support needs parent/child
modelling in the v2 context store, which is flat by design
(`getContextForAgent` emits `{ description, value }` only). Instead both
are marked `@deprecated` and the JSDoc example is rewritten to document
behavior that exists. Tracked in #6408.

## Not addressed

Two pre-existing behaviors left alone to keep this a bugfix:

- `value` is in the deps raw, so an inline object literal re-registers
the entry on every render. Pre-1.50 depended on the serialized string
instead.
- The hook returns `undefined` on first render, since the ref is
assigned inside the effect.

## Testing

`useCopilotReadable` had no test file. This adds one — 12 tests, using a
fake that mirrors `ContextStore` semantics (`addContext` assigns an id
and stores the already-serialized value).

Full project suite — `nx run @copilotkit/react-core:test`:

```
 Test Files  124 passed (124)
      Tests  1487 passed (1487)
 NX   Successfully ran target test for project @copilotkit/react-core and 17 tasks it depends on
```

Each fix is covered by a test that fails against the pre-fix hook.
Reverting only `use-copilot-readable.ts` and re-running the new file:

```
   ✓ registers the context on mount
   ✓ removes the context on unmount
   ✓ available > registers nothing when mounted as disabled
   × available > removes the context when flipped to disabled after mount
     → expected [ { description: 'employees', …(1) } ] to deeply equal []
   × available > re-adds the context when flipped back to enabled
     → expected [] to deeply equal [ { description: 'employees', …(1) } ]
   × convert > is called with (description, value) in that order
     → expected "spy" to be called with arguments: [ 'employees', …(1) ]
   × convert > is used in place of JSON.stringify
     → Cannot read properties of undefined (reading 'map')
   ✓ convert > serializes the value alone when convert is omitted
   × dependencies > re-runs the effect when a dependency changes
     → expected "spy" to be called 2 times, but got 1 times
   ✓ dependencies > does not re-run the effect when the dependency is unchanged
   ✓ re-registers when the description changes
   ✓ keeps separate entries for identical readables in two components

 Test Files  1 failed (1)
      Tests  5 failed | 7 passed (12)
```

The two that still pass pre-fix are deliberate: `serializes the value
alone when convert is omitted` guards the `JSON.stringify` replacer trap
in the fix itself, and `keeps separate entries…` is the evidence that
the `found` branch was dead.

With the fix applied:

```
 ✓ src/hooks/__tests__/use-copilot-readable.test.tsx (12 tests) 15ms

 Test Files  1 passed (1)
      Tests  12 passed (12)
```

Types — `pnpm --filter @copilotkit/react-core check-types`:

```
> @copilotkit/react-core@1.66.2 check-types
> tsc --noEmit
```

(no diagnostics)

Formatting — `oxfmt --check` on both files:

```
Checking formatting...
All matched files use the correct format.
Finished in 16ms on 2 files using 18 threads.
```

`oxlint` reports one warning, on `...(dependencies || [])` in the deps
array. The same pattern already warns in
`use-copilot-additional-instructions.ts`, `use-frontend-tool.ts` and
`use-coagent-state-render.ts`; CI runs `oxlint .` without
`--deny-warnings`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

The `convert` and `dependencies` fixes were independently found and
fixed first by @jwgrsol in #6246, opened a week before this PR. Credited
below.

Co-authored-by: jwgrsol <wefhio1985@gmail.com>
2026-08-13 18:51:54 +02:00
Alem Tuzlak 47ad5e34a3 refactor(react-native)!: converge tool-call rendering onto CopilotKit's shared registry (#6438)
## What does this PR do?

`@copilotkit/react-native` maintained a **private tool-call render
registry** (`hooks/RenderToolContext.tsx`) alongside the canonical one
that `CopilotKitCoreReact` already provides — and which every React
Native app already ships, unused. This PR deletes the fork and points
React Native at the shared registry.

That fork caused three bugs:

| Bug | Symptom | Cause |
|---|---|---|
| **Tool renders never streamed** | A component registered with
`useRenderTool` / `useComponent` painted nothing until the tool call
completed | `CopilotChat` used `JSON.parse` on the argument buffer.
While a model writes a tool call that buffer is *invalid JSON by design*
— AG-UI delivers `TOOL_CALL_ARGS` deltas that are concatenated
client-side — so the parse threw on every delta, warned, and fell back
to `{}` |
| **`useComponent` rendered nowhere** | Silently, with no error | It
writes to core's registry; React Native's chat read React Native's
private `Map` |
| **Chat history degraded** | Navigating away from the registering
screen turned earlier tool calls into a `Called: <name>` placeholder |
The private `Map` deleted renderers on unmount; core deliberately keeps
them |

`@copilotkit/react-core` has used `partialJSONParse` on this path since
v2 shipped. React Native diverged because `useRenderToolCall` was
excluded from its re-exports on the stated grounds that it "depends on
DOM elements via `DefaultToolCallRenderer`" — a claim that was never
true of the hook itself. It was only ever reachable through the fat
`/v2` entry, whose weight is the real hazard (#4893). #5883 moved it
into `/v2/headless` on 2026-07-23; the exclusion comment was rewritten
the next day without revisiting the reason.

### What changed

- **One registry.** `useRenderTool` registers through `useFrontendTool`
into `CopilotKitCoreReact.renderToolCalls`. `CopilotChat` and any custom
surface consume react-core's `useRenderToolCall`.
- **Types are derived, not declared.** `RenderToolProps` is now
`React.ComponentProps<ReactToolCallRenderer<T>["render"]>`, so React
Native cannot drift from `ReactToolCallRenderer` — the contract every
registered renderer is actually invoked against. Change that contract
and `check-types` names every React Native renderer the change breaks.
React Native narrows only the *return* type to `ReactElement | null`,
which `FlatList`'s `renderItem` genuinely requires.
_Scope of that guarantee (corrected during review):_ it does **not**
extend to the type react-core publicly exports under the same name.
Web's `RenderToolProps<S>`
(`react-core/src/v2/hooks/use-render-tool.tsx`) is a separate
hand-declared union, generic over a schema, carrying arguments under
`parameters` (not `args`) and declaring `status` as string literals
rather than `ToolCallStatus` members. Both divergences are live today
and nothing type-checks them shut — the one place the shapes meet,
react-core's own bridge, compiles because a string-enum member is
assignable to its own literal type but not the reverse. Aligning web's
alias is a breaking web API change, filed separately.
- **`RenderToolContext.tsx` deleted** (−150 lines), along with 15 tests
that described the removed subsystem. One of them — `unregisters the
render function on unmount` — asserted the chat-history bug as a
requirement.
- **Two structural CI guards for #4893**, in opposite directions: a test
failing if any React Native source imports the fat `/v2` entry, and a
script failing if react-core's `/v2/headless` or `/v2/context` chunks
ever link shiki/mermaid/cytoscape/katex/streamdown. Both were verified
able to fail by deliberately introducing the regression. These are
*structural* assertions, not size budgets — `dev-docs/bundle-size.md`
freezes `limit` fields until OSS-122.
- **`react-native` added to the bundle-size glob**, which it had never
been in, plus a `size:headless` measurement.

React Native also gains capabilities it lacked: render props inferred
from your schema, `name`/`toolCallId` on render props, and `result` on
completed calls.

**Corrected during review — two capabilities this originally claimed are
not delivered:**

- **Wildcard (`"*"`) renderers do not work on React Native.** Because
`useRenderTool` routes through `useFrontendTool` (which calls
`addTool`), `name: "*"` registers a frontend tool literally named `*` —
advertised to the model, and colliding with core's separate
wildcard-executable-tool path. react-core's `useRenderTool` is
renderer-only and special-cases the wildcard; React Native's is not. The
guide now advises against it.
- **`followUp` (and `available`) are not forwarded**, and the handler's
`context` argument is dropped, so `stopAgent()`'s abort signal is
unreachable from an RN handler.

Both are tracked in § Known limitations for the follow-up that converges
React Native onto react-core's hooks — deleting RN's `useRenderTool` in
favour of re-exporting `useFrontendTool` (tool + renderer) and
react-core's `useRenderTool` (renderer-only, wildcard-capable). That is
an API change with its own migration note, so it is not in this PR.

### ⚠️ Breaking (in a minor)

`useRenderToolRegistry` and `RenderToolProvider` are **removed**. Both
are documented on the docs site, so this is a real break — see the
`BREAKING CHANGE:` footer on `db67ccf`, which is what the release notes
derive from, plus the rewritten reference pages.

```diff
- const registry = useRenderToolRegistry();
- const renderer = registry.get(toolCall.function.name);
- return renderer ? renderer({ args, status }) : null;
+ const renderToolCall = useRenderToolCall();
+ return renderToolCall({ toolCall });
```

Also note two semantic changes: `args` is `Partial<T>` **only** while
`status` is `"inProgress"`, and a render function is now captured at
registration — if it closes over changing values you must declare them
in `deps` (React Native previously refreshed the closure on every
render).

**Known limitation:** agent-scoped renderer resolution does not take
effect on React Native. `CopilotChatConfigurationProvider` is not in
RN's provider tree, so `agentId` always resolves to the default.
Renderers still resolve by name; two agents registering the same tool
name resolve arbitrarily. Filed separately.

### A data point worth recording

Adding `useRenderToolCall` to the measured headless entry moved the
bundle **92.8 kB → 92.7 kB**. Flat. The hook React Native spent months
not using was already inside the chunk every RN app resolves whole —
Metro doesn't tree-shake, so the fork never saved a byte. It cost them.

### Testing

- `@copilotkit/react-native`: **253 passing / 22 files** ·
`@copilotkit/react-core`: **1480 passing / 123 files** · `check-types`
and `build` green for both.
- Each of the three bugs has a deterministic test driving a real
`CopilotKitCoreReact` — no mocking of the code under test.
- Both #4893 guards carry mutation evidence: introduce the regression,
watch them fail, revert, watch them pass.

### Follow-up

`useRenderTool`'s JSDoc is split across two blocks, which orphans the
primary description from IDE hover (the `@param deps` warning still
surfaces). One-line fix, deliberately left out of the final fix wave.

## Related PRs and Issues

- **Supersedes #6346** (@davidmckayv) — its diagnoses were correct and
its test assertions are ported here, re-driven through the real registry
rather than a mocked local one. Credited via `Co-Authored-By` on
`4104bd1`.
- Addresses the React Native half of **#4893**.
- Builds on **#5883**, which created the lean `/v2/headless` entry this
PR consumes.

## Checklist

- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-13 18:49:32 +02:00
Ben Taylor 3a80c2696e feat(vue): mirror React's useAgent thread scoping, remove thread cloning (#6234)
## Problem

Vue's `useAgent` implemented per-thread agent **cloning** — a mechanism
React never had. Passing a `threadId` silently handed you a copy of the
agent:

```ts
useAgent({ agentId: "assistant", threadId: "thread-1" })  // → a clone, keyed (agent, threadId)
```

The clones lived in a module-level `WeakMap` (`globalThreadCloneMap`),
so:

- Nothing tied a clone's lifetime to the scope that created it — they
were never released.
- Components had to *look up* which copy was live.
`CopilotChatMessageView` called `getThreadClone(registryAgent,
config.threadId) ?? registryAgent` just to find the agent actually being
rendered.
- `getThreadClone` / `globalThreadCloneMap` were exported from the
module purely so components could do that lookup.

Meanwhile React grew an explicit contract for the same use case in
#6141: a private *proxied* agent, registered under a local `agentId` and
routed to a `runtimeAgentId`.

## Change

Deletes cloning entirely and ports React #6141's contract to Vue.

`cloneForThread`, `getOrCreateThreadClone`, `getThreadClone` and
`globalThreadCloneMap` are gone — zero references remain, including in
prose.

`UseAgentProps` becomes a base plus a two-branch union, with the same
all-or-nothing rule React now enforces:

```ts
useAgent()                                       // shared registry agent
useAgent({ agentId })                            // shared registry agent
useAgent({ agentId, runtimeAgentId, threadId })  // private proxied agent
```

Every partial set — `{ agentId, threadId }`, `{ agentId, runtimeAgentId
}`, `{ runtimeAgentId, threadId }` — is a compile error, backed by the
same three runtime guards with the same messages for callers TypeScript
doesn't reach.

### Parity with #6141

| | React (#6141) | Vue (this PR) |
|---|---|---|
| scoped branch | `agentId` / `threadId` / `runtimeAgentId`, all
required `string` | same, as `MaybeRefOrGetter<string>` |
| unscoped branch | `agentId?: string`, `threadId?: undefined`,
`runtimeAgentId?: undefined` | identical |
| runtime guards | 3 | same 3, same messages |
| thread resolution | prop → chat config, gated on `hasExplicitThreadId`
| identical |
| proxy registration | balanced effect on core + both ids | same deps |

## Two Vue-specific details

Both are load-bearing and were found by tests failing, not by
inspection:

**The pin watcher's first source is `() => agent.value`, not `agent`.**
Vue sets `forceTrigger` when any array watch source is a shallow ref, so
passing the ref directly re-ran the pin on *every* `triggerRef(agent)` —
i.e. every streamed message — re-pinning the inherited thread over one
`CopilotChat` had deliberately set for the chat it renders. Two existing
suites cover this (`uses the explicit agentId and threadId over
inherited configuration`). React has no equivalent hazard because effect
deps compare by identity.

**`CopilotChat` assigns `agent.threadId` inside its `/connect`
watcher**, not a separate one. `CopilotKitCore.connectAgent` reads that
field *synchronously* (`run-handler.ts`) to decide whether a restore is
fresh, so a later assignment lets `/connect` address the previous thread
— skipping the messages/state reset and re-stamping its restore key with
the stale id. Same placement as React's `CopilotChat`.

`CopilotChatMessageView` now resolves the registry agent directly
instead of consulting the clone map, and reads `copilotkit.agents` so it
recomputes when the registry changes.

## What callers see

**One agent per `agentId`** — the model React has always had. Thread
isolation is now explicit instead of implicit: ask for it and you get a
real, separately-registered agent rather than a copy that appears out of
nowhere.

```ts
// before — silently produced a copy of the "assistant" agent
useAgent({ agentId: "assistant", threadId: "thread-1" })

// now — an explicit private agent of your own, routed to "assistant"
useAgent({ agentId: "chat-1", runtimeAgentId: "assistant", threadId: "thread-1" })
```

Nothing in this repo needed updating: `CopilotChat`, `use-capabilities`,
`use-interrupt` and all six example apps already used `{ agentId }`.
`<CopilotChat agentId threadId>` is unchanged for consumers.

## Tests

`use-agent-thread-isolation.test.ts` (433 lines) covered clone semantics
that no longer exist; it's replaced by
`use-agent-thread-pinning.test.ts`, which pins the new invariants — one
instance per `agentId` never a copy, config-thread pinning gated on
explicitness, and all three all-or-nothing guards.

Four component suites used `getThreadClone` purely as a lookup to find
the agent under test and now read from the registry.

`MockMCPProxyAgent` recorded `addMessage` **only inside its `clone()`
override**, so those assertions were passing only because cloning
existed. The recording moves onto the class. `clone()` itself is left
intact everywhere — `CopilotKitCore`'s `SuggestionEngine` still clones
agents (`packages/core/src/core/suggestion-engine.ts`), so removing
those overrides would have planted a latent trap.

## Deliberately not included

Found while reviewing this area, real, but out of scope — each wants its
own change:

- `useAgent`'s header watcher **replaces** `agent.headers` instead of
calling `copilotkit.applyHeadersToAgent()`, dropping per-agent
construction-time headers. Regresses #5635 in Vue; React does this
correctly.
- `credentials` never reach a provisional agent.
- No `onAgentsChanged` subscription anywhere in `packages/vue`, so `()
=> copilotkit.value.agents` as a watch source never re-evaluates on
registry change.
- `/connect` is skipped for a plain `HttpAgent` — the `hasCustomConnect`
prototype comparison matches every real agent. Vue-only, no React
equivalent.
- `CopilotThreadsDrawer.ssr.test.ts` is a latent flake (5s timeout on a
dynamic import; passes in isolation).
2026-08-13 11:43:33 -05:00
Maxim 4b17ea7d35 fix(scripts): tokenize before hunting loader calls in the purity gate
The #4893 hard-fail gate's loader-call detector gave WRONG VERDICTS IN BOTH
DIRECTIONS. It layered two regexes — a comment/string/template alternation that
blanked only the comment branch, and `\b(?:import|require(?:\.resolve)?)\s*\(`
over the result — then classified an argument as static from the FIRST CHARACTER
after the paren. All nine shapes below were reproduced against the real gate
before the rewrite:

  false FAIL  throw new Error("use require(path) instead")
  false FAIL  `import(${x})` inside a template
  false FAIL  o.import(y) / mod.require(x)          (member calls, not loaders)
  false PASS  /https:\/\//; …import(n)              (the regex's `//` blanked the
                                                     rest of the line, hiding a
                                                     real dynamic call)
  false PASS  import(`stream${n}`)                  (merely STARTS with a quote)
  false PASS  import("zo" + n)                      (same)
  false PASS  import(`${base}/v2/index.mjs`)        (same — the fat entry)
  false PASS  __require(name)                       (no \b inside `__require`)

Replaced with `scanSource`, a single-pass tokenizer that classifies every
character as code / comment / string / template / regex and returns a
length-preserving masked view plus a literal-span list. The one surviving regex
now only ever sees code, so import-shaped TEXT cannot reach it at all; an
argument counts as static only when it is one COMPLETE literal with no
concatenation or interpolation; `__require` is matched; and a member call is
rejected both by lookbehind and by a whitespace-skipping back-scan (so
`m\n  .import(x)` is not a loader either).

Proven in both directions: nine innocent/violation pairs run through the real
`assertEntryPurity`, each innocent form CLEAN and each matching real violation
FAIL. Re-proved end-to-end by prepending `import "streamdown"` to the real
dist/v2/headless.mjs — exit 1 naming all five families — then restoring it
byte-identically. On the untouched dist the scan sees 66 loader calls in the
`.cjs` graph and classifies all 66 static, so it passes because it LOOKED.

Also adds the first `.cjs` fixtures: every existing fixture was `.mjs`, leaving
the script's `format: "cjs"` branch and the `require()` shape asserted by
nothing. Tests 24 → 47.

`stripComments` is renamed `maskNonCode`, since it now blanks literals and
regexes too; it had no caller outside this script and its test. The RN guard
keeps its own copy, untouched.

dev-docs/bundle-size.md: the four holes a sibling agent documented as known
limitations this round are closed and removed from that list; what genuinely
remains (regex-vs-division heuristic, no JSX/TS, indirect loaders) replaces them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:27:47 +02:00
Maxim 4c17a8fe8c fix(react-native): key the messages fingerprint on object content
`messagesFingerprint`'s content key collapsed every object to 0, so an
in-place content replacement that kept the same message id was invisible to
every memo derived from it. Its comment claimed to mirror react-core's
`messagesMemoKey`, which stopped being true when react-core #6325
(de0a659b2d) taught that key to serialize object content.

Serialize object content here too, keeping the length-not-value treatment for
string and array content so large text and base64 attachment payloads are
still never re-serialized per render. Serialization is guarded: the
fingerprint runs on every render and `JSON.stringify` throws on a circular
structure, which this component is already required to tolerate (see the
existing "does not throw on tool content that cannot be JSON-serialised"
assertion) — react-core stringifies unguarded, so the guard is a deliberate
and documented divergence.

Not a live stale-render bug via the activity path: same-id object content
comes from an ACTIVITY_SNAPSHOT replace, and `role: "activity"` never reaches
`listItems`, which builds rows for `user` and `assistant` only. Object content
DOES reach a renderer through the `role: "tool"` correlation, though, which is
what the added test drives: an object tool result replaced in place used to
leave the renderer showing the first object's serialization.

The comment no longer claims to mirror a moving target — it records what the
key captures, why the object branch exists, and that the two implementations
are independent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:09:29 +02:00
Maxim 1b39c12e36 fix(scripts): stop the headless CLI gates skipping themselves on odd paths
Both scripts decided "am I the entrypoint?" by comparing `import.meta.url` to a
`file://`-concatenated `process.argv[1]`. `import.meta.url` is percent-encoded
and symlink-resolved; raw argv[1] is neither. So the comparison was false for
any checkout path containing a space, for any invocation through a symlink
(macOS /tmp is one), and on Windows — and a false guard skipped the whole CLI
block. Reproduced before fixing: the #4893 purity gate and the bundle-size
measurement both exited 0 having printed nothing and asserted nothing, which is
worse than a gate with holes because it manufactures confidence. The guard was
added by this PR so the modules could export internals to their new negative
tests; making the gates testable introduced a way for them not to run.

Both now compare real filesystem paths through an exported `isEntrypoint`:
`fileURLToPath` defeats the encoding and Windows forms, `fs.realpathSync` on
both sides defeats symlinks, and a `path.resolve` fallback keeps a nonexistent
argv[1] from throwing.

Each `node --test` suite gains five entry-guard tests, including an end-to-end
spawn of the real script through a symlinked package-root alias whose name
contains a space — the only case that catches the call site regressing back to
a string comparison (verified: it fails against the old expression). The unit
cases assert the naive comparison really would have failed, so none of them can
pass vacuously. Both negative gates were re-proven to still bite: a doctored
dist entry pulling streamdown fails the purity gate, and a stubbed dist entry
trips the measurement's plausibility floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:09:29 +02:00
Maxim c7d176f264 test(react-native): make the #4893 entry guard fail on violations, not on growth
The headless import-graph guard pinned the resolved graph EXACTLY — the
11-module list and the 8 bare specifiers, both `toEqual`. That catch-all was
deliberate (a heavy dependency nobody enumerated still had to be looked at),
but it also went red on innocent growth: adding any first-party `src/` module
to the headless graph failed it, on someone else's unrelated PR. A guard that
fails on innocent changes gets deleted by the third person who hits it, and
then it guards nothing.

Express the catch-all over PACKAGES instead of MODULES: the graph may only
reach packages a headless consumer is guaranteed to be able to resolve — this
package's `dependencies` plus its NON-optional `peerDependencies`, read from
package.json rather than hand-copied. That is precisely the promise the
headless entry sells ("bundles with nothing stubbed in metro.config.js"), so
it still fails on any new third-party edge, on every optional peer, on a
devDependency, and on a Node builtin — while a new first-party module or
another import of an already-sanctioned package is free.

The two other things the pin bought are kept explicitly:

- Comment stripping. The eight phantom specifiers JSDoc examples used to
  harvest were all self-references, and this package's own name is not in the
  guaranteed set, so a `stripComments` regression still fails here.
- Non-vacuity. Every remaining graph assertion is a deny-list, and a deny-list
  over a truncated graph passes for the wrong reason, so a subset floor
  asserts the walk still reaches the provider, the polyfills and the
  react-core headless edge.

Not changed: comment stripping itself, the import()/require()/require.resolve
extraction, non-literal loader flagging, emitted-extension resolution, the
loud failure on unresolvable edges, the entry-presence tests, the #4893
fat-entry ban (still the assertion that catches `@copilotkit/react-core/v2`)
or the heavy-dependency ban. The runtime-export `beforeAll` is untouched.

Proven both directions: a new first-party module passes the loosened guard and
fails the old pin; `@copilotkit/react-core/v2`, `shiki`, an unenumerated
devDependency edge and a truncated walk each fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:08:29 +02:00
Maxim 00caf5fa7b docs(react-native): fix the chat memo comment's identity rationale
`messagesFingerprint`'s header JSDoc and the matching inline comment near the
`listItems` memo justified keying on message CONTENT with a claim that is false:
that `agent.messages` is mutated in place throughout, and that "the AG-UI apply
pipeline reuses one array for a whole run".

It does not. `@ag-ui/client`'s `AbstractAgent.processApplyEvents` REASSIGNS
`this.messages = applied.messages` for every applied event, so a streaming run
hands down a new array — and new message, `toolCall` and `function` objects — per
delta. Verified against a real AG-UI run by the PR reviewer, and confirmed here in
@ag-ui/client 0.0.57's `AbstractAgent`. The old grep behind the claim ("assigning
`.messages` in packages/core/src hits test files only") is accurate but proves
nothing: `@ag-ui/client` is a dependency, outside that tree.

The fix itself stands. Identity is unreliable in BOTH directions, which is the
actual rationale: it changes on the apply path, and it does NOT change on the
paths these memos exist to serve — core splices tool results in place
(`agent.messages.splice(insertAt, 0, toolMessage)`,
packages/core/src/core/run-handler.ts:931, :1080), `AbstractAgent.addMessage` is a
`this.messages.push(...)`, and `useAgent` re-renders with a bare `forceUpdate()`
(packages/react-core/src/v2/hooks/use-agent.tsx:382-396). A signal that both
misses changes and fires without them cannot be a dependency, so the derivations
must key on content.

Comments only: three sites reworded (the JSDoc, the "cannot be used" pointer at
the `messagesKey` call, and the inline note on the `listItems` memo). `git diff`
touches no behaviour, type or dependency array — every changed line is a comment.
The `contentKey` length-vs-value paragraph is left alone; another change owns it.

Note: the same false claim is in commit 77ed31c437's body, which cannot be
rewritten, and in a GitHub review comment.

Not run in this worktree: it has no node_modules, and the change is comment-only,
so it cannot affect types, lint or tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:00:24 +02:00
Ben Taylor a3b814a041 fix(core): refresh Intelligence delegate headers before every join (#6469)
## Summary

`ProxiedCopilotRuntimeAgent` builds its `IntelligenceAgent` delegate
**once** and caches it for the proxy's lifetime, copying `headers` into
the delegate's constructor config. Nothing ever refreshed that copy, so
**a header that changed after the delegate was created never reached
`/connect` or `/run`** — for the life of the agent.

For a multi-tenant app carrying the active tenant in a header, the join
was attempted under the *previous* tenant's identity with the *new*
tenant's thread id, and the platform correctly answered
`THREAD_NOT_FOUND`. Only a full page reload cleared it, because that
rebuilds the delegate. A rotated or refreshed `Authorization` bearer has
the same exposure.

Reported by Sameday against 1.67.1 with a deterministic staging repro:

```
19:45:23.554 | /copilotkit/runtime/threads            | hdr=<tenant B> | 200
19:45:23.886 | /copilotkit/runtime/threads/subscribe  | hdr=<tenant B> | 200
19:45:23.893 | /copilotkit/runtime/agent/<id>/connect | hdr=<tenant A> | body.companyId=<tenant B> | 404
```

`/threads` carries **B** while `/connect` carries **A**, ~340ms apart in
the same switch. Not a race — a stale copy with no refresh path.

## Root cause

`setHeaders` / `applyHeadersToAgent` could not fix this: they write an
agent's `.headers`, and `IntelligenceAgent` exposed only `private
config`. `syncDelegate` *looks* like the refresh path, but its
`hasHeaders` probe is `"headers" in agent` — false for the delegate,
since `headers` is declared on `HttpAgent`, not on `AbstractAgent`. So
`config.headers` was the sole header source for Intelligence REST calls,
with no refresh path at all.

## The fix

Expose `headers` as a public accessor pair backed by `config`, and read
it in `requestJoinCredentials$`.

**The accessor is the entire fix**: it makes `hasHeaders` true, so
`syncDelegate` — which already runs on every `resolveDelegate()`, and is
preceded by `applyHeadersToAgent` in `RunHandler.connectAgent` — starts
actually refreshing the delegate before each join. No new plumbing.

Two things worth flagging for reviewers:

1. **The originally-suggested fix ("make `requestJoinCredentials$` read
live headers") does not work on its own** — and is actively harmful.
There was no live header source on the class to read: without the
accessor, `this.headers` is `undefined` and **every header is dropped**
(verified: only `Content-Type` survives). The read here goes through the
accessor for a single source of truth, not because that read carries the
fix.

2. **The setter replaces the config object rather than mutating it**,
because `clone()` shares the config reference. The join path alone would
mask an in-place write (`syncDelegate` rewrites headers just before
every join), but the credential re-acquisition inside a running pipeline
(`intelligence-agent.ts:563`) does not re-sync — so a clone's tenant
could ride out on the original's socket-error refresh. That's the same
cross-tenant leak this accessor exists to prevent.

`credentials` had the identical defect via `config.credentials`
(`hasCredentials` was false too) and gets the same treatment.

## Testing

**Unit tests (5 new, each written first and watched fail).** The pre-fix
failure is the staging symptom reproduced:

```
FAIL > sends a header changed after the delegate was created
AssertionError: expected { …(2) } to match object { 'X-Tenant': 'tenant-b' }
-   "X-Tenant": "tenant-b",
+   "X-Tenant": "tenant-a",
```

Coverage: a header changed post-construction reaches `/connect`; the
same on the `/run` path (which was independently verified broken
pre-fix, sending tenant A where B was expected); credentials likewise; a
clone's header update must not reach the original
(`IntelligenceAgent.clone()` invariant — this one fails under in-place
config mutation); and a per-thread clone and its original each send
their own tenant.

**Verified beyond the unit tests.** Because the mocked-harness result
alone doesn't prove the production wiring, I drove the real chain —
`CopilotKitCore.setHeaders` → registry → proxy → delegate → outbound
POST — in a plain Node process with no vitest and no `vi.mock`, stubbing
only `fetch` at the network boundary. Same script against the unfixed
file, then the fix:

```
BEFORE (origin/main)                      AFTER (this PR)
"headers" in delegate: false              "headers" in delegate: true
delegate.headers: undefined               delegate.headers: { X-Tenant: tenant-b }
proxy.headers after setHeaders(B):        proxy.headers after setHeaders(B):
  { X-Tenant: tenant-b }                    { X-Tenant: tenant-b }

0: POST /connect  X-Tenant=tenant-a       0: POST /connect  X-Tenant=tenant-a
1: POST /connect  X-Tenant=tenant-a  <--  1: POST /connect  X-Tenant=tenant-b  credentials=include
FAIL (stale headers)                      PASS (live headers reach /connect)
```

The "before" column reproduces the report's tell exactly:
`proxy.headers` correct at tenant B while `/connect` still sends tenant
A, through the very API the report found ineffective.

**Gates** (run in a worktree with a freshly built `@copilotkit/shared`,
since a stale dist otherwise produces 20 unrelated
`core-inspector-metadata` failures and 4 `tsc` errors):

| Gate | Result |
| --- | --- |
| `@copilotkit/core` vitest | **654 passed / 654**, 59/59 files |
| `tsc --noEmit` | clean |
| `oxlint` | 0 errors (2 warnings, both pre-existing test helpers) |
| `oxfmt` | no reformatting needed |

**Not covered:** `fetch` is stubbed, so this does not exercise a live
Intelligence gateway or a browser tenant switch — it proves the outbound
header is correct, not the platform's response to it.

## Note for whoever merges

#6450 and #6468 also touch `intelligence-agent.ts` (thread-restore work)
but neither goes near the header path, so conflicts should be textual at
worst.

## Follow-up left out of scope

Two separate pre-existing defects surfaced while verifying this one.
Neither is touched here.

**1. `credentials` passed to a `ProxiedCopilotRuntimeAgent` constructor
are dropped at registration.** `applyCredentialsToAgent` overwrites
`agent.credentials` from core unconditionally, with no per-agent
baseline — unlike `applyHeadersToAgent`, which merges over the
`agentOwnHeaders` baseline captured for exactly this reason (#5635).
Probed in a real process: an agent constructed with `credentials:
"include"` in a core with none configured reports `undefined`
immediately after registration, and every join goes out without
credentials. Identical before and after this PR, so it is not a
regression from this change — but the headers/credentials asymmetry
looks unintended, given #5433 was specifically about preserving proxied
runtime credentials.

**2. `buildRuntimeUrl` reads `config.agentId`
(`intelligence-agent.ts:770`), (`intelligence-agent.ts:770`), so
`syncDelegate`'s `delegate.agentId = routedAgentId()` is cosmetic for
the REST URL. Same root-cause class as this bug, but latent rather than
live (routing is fixed per proxy instance).

Happy to file both separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-13 09:12:58 -05:00
Benjamin Taylor 70545f072a test(core): correct an overclaiming comment, tighten the credentials assertion
The per-thread-clone test's comment claimed it guards the copy-on-write
setter. It does not: syncDelegate rewrites headers before every join, so
it passes even with an in-place write (verified). Say what it actually
pins — each proxy's joins carry its own tenant — and point at the
clone-invariant test that does guard the setter.

Also assert the pre-change join carried no credentials, so the
credentials test shows a transition rather than a single end state.
2026-08-13 08:15:52 -05:00
Murat Sari dde84a5804 fix(angular): prevent duplicate OpenGenerativeUI sandboxes 2026-08-13 14:32:40 +02:00
JYbill c06709aff7 Merge branch 'main' into fix/state-manager-run-id 2026-08-13 17:52:08 +08:00
Murat Sari 01c7283210 fix(core): prevent duplicate interrupt tool results (#6201) 2026-08-13 01:18:16 +02:00
Benjamin Taylor 7d1cdc15df test(core): pin the run path against stale Intelligence headers
The report names both /connect and /run. The run path reaches the
delegate through #runViaDelegate, which shares resolveDelegate with the
connect path, so the accessor fixes both — but that was inferred from the
shared call site rather than pinned. Verified failing against the
pre-fix file (sent tenant-a where tenant-b was expected).
2026-08-12 17:01:21 -05:00
Benjamin Taylor a3562c20a6 fix(core): refresh Intelligence delegate headers before every join
`ProxiedCopilotRuntimeAgent` builds its `IntelligenceAgent` delegate once
and caches it for the proxy's lifetime, copying `headers` into the
delegate's constructor config. Nothing ever refreshed that copy, so a
header that changed later never reached `/connect` or `/run` — for the
life of the agent.

`setHeaders`/`applyHeadersToAgent` could not fix it: they write an
agent's `.headers`, and `IntelligenceAgent` exposed only `private
config`. `syncDelegate` looked like the refresh path but its `hasHeaders`
probe is `"headers" in agent`, which was false for the delegate.

Multi-tenant apps that carry the active tenant in a header saw the join
attempted under the previous tenant's identity with the new tenant's
thread id, answered THREAD_NOT_FOUND. A rotated `Authorization` bearer
has the same exposure. Only a full reload cleared it.

Expose `headers` as a public accessor pair backed by `config`. The
accessor is the entire fix: it makes `hasHeaders` true, so `syncDelegate`
— which already runs on every `resolveDelegate()` — starts actually
refreshing the delegate before each join. Note that changing
`requestJoinCredentials$` to read live headers, as the report suggested,
does nothing on its own: there was no live source on the class to read,
and without the accessor `this.headers` is `undefined`, which drops every
header. It reads through the accessor here for a single source of truth,
not because that read carries the fix.

The setter replaces the config object rather than mutating it, because
`clone()` shares the config reference. The join path alone would mask an
in-place write (syncDelegate rewrites headers just before every join),
but the credential re-acquisition inside a running pipeline does not
re-sync, so a clone's tenant could ride out on the original's
socket-error refresh.

`credentials` had the identical defect via `config.credentials`
(`hasCredentials` was false too) and gets the same treatment.

Verified beyond the unit tests by driving the real chain
(`CopilotKitCore.setHeaders` -> registry -> proxy -> delegate ->
outbound POST) in a plain Node process with only `fetch` stubbed:
before, `"headers" in delegate` was false and the join after a tenant
switch still sent tenant A; after, it sends tenant B.

Reported by Sameday against 1.67.1 with a deterministic staging repro.
2026-08-12 16:36:11 -05:00
Mike Ryan 528dea6483 fix(react-core): ship a single v2 context instance (#6440)
## Problem

`@copilotkit/react-core` ships **two independent copies** of the v2
context module, so `useLicenseContext` imported from
`@copilotkit/react-core/v2/context` returns the default forever —
`status: null` even when `/info` reports `licenseStatus: "valid"`.
Reported downstream as a chat-history sidebar that never loads, because
`useThreads` is gated on license status.

`src/v2/context.ts` is compiled by two separate tsdown builds:

| Build | Output | Contains |
|---|---|---|
| `entry: ["src/index.tsx", "src/v2/index.ts"]` | `dist/` shared chunk |
inlined copy **A** |
| `entry: {context: "src/v2/context.ts"}` | `dist/v2/context.*` |
standalone copy **B** |

There is no import edge between them, so `createContext()` runs twice.
`CopilotKitProvider` lives in the shared chunk and publishes to **A**;
`@copilotkit/react-core/v2/context` exports **B**, which nothing ever
provides.

Verified against the published 1.66.4 artifact:

```
$ grep -n "createContext" dist/v2/context.mjs
104:const CopilotKitContext = createContext(null);
124:const LicenseContext = createContext({

$ grep -n "createContext" dist/copilotkit-nRjRp2_5.mjs   # inside //#region src/v2/context.ts
1522:const CopilotKitContext = createContext(null);
1544:const LicenseContext = createContext({

$ grep -E '^import .*from "[^"]*context[^"]*"' dist/copilotkit-nRjRp2_5.mjs
                                                          # (empty — no import edge)
```

`CopilotKitContext` is duplicated identically, so `useCopilotKit`
imported from that subpath throws `"useCopilotKit must be used within
CopilotKitProvider"`. The subpath was effectively unusable for web
consumers; license was just the *silent* failure mode.

**Compounding defect:** `src/v2/providers/index.ts` enumerates its
exports by name and omits `useLicenseContext` (even though
`CopilotKitProvider.tsx:19` re-exports it). So the live copy had **no
public import path at all**, leaving consumers with no correct
alternative.

Not a 1.66.x regression — broken since c3c30969e4 (2026-05-06), the
commit that introduced the split.

## Fix

1. **`tsdown.config.ts`** — hoist the existing `externalize-context`
plugin and apply it to the `dist/` build. The headless build already
used it for exactly this reason ("ensuring a shared React context
instance at runtime"); it was simply never applied here. One instance
now. UMD builds stay self-contained by design.
2. **`src/v2/providers/index.ts`** — export `useLicenseContext`.
3. **`scripts/context-singleton-preflight.mjs`** *(new)* — build-time
guard, wired into `build`.
4. **`src/v2/providers/__tests__/providers-exports.test.ts`** *(new)*.

### Why a guard

This bug class is invisible to every gate we have. On the broken build,
`tsc`, 1471 vitest tests, `publint` and `attw` were **all green** while
the published package shipped two contexts — vitest imports *source*,
where only one module exists. The guard keys off the `//#region
src/v2/context.ts` banner tsdown emits per inlined module, and
self-checks: if that banner convention ever changes it fails loudly
rather than silently passing everything.

## Testing

**End-to-end reproduction against the built dist** — provider from
`/v2`, hook from `/v2/context`, exactly as a consumer app wires it. This
is the test that most directly encodes the reported bug.

Against a **pre-fix** build (rebuilt from the parent commit's
`tsdown.config.ts`):

```
× useLicenseContext sees server-reported 'valid', not the default
  → expected 'null' to be 'valid'
× useLicenseContext sees server-reported 'expired', not the default
  → expected 'null' to be 'expired'
```

That `'null'` is precisely the reported symptom — a valid license read
as `status: null`, permanently disabling license-gated features.

Against this branch:

```
✓ src/v2/__tests__/dist-context-singleton.test.tsx (2 tests)
```

It also confirms the self-reference resolves under a real bundler
(Vite), and it degrades to a loud skip when no dist is present (verified
by removing `dist/v2/index.css`): nx `test.dependsOn` is `^build`, so
this package's own build is not guaranteed to have run before `test`.
The hard gate is therefore the preflight, which runs as part of `build`.

**Both build-level guards proven red→green — not merely green.**

Preflight against the **actual published 1.66.4 dist** (expected fail):

```
$ node scripts/context-singleton-preflight.mjs .../copilotkit-react-core-1.66.4/dist
context-singleton-preflight: src/v2/context.ts is bundled into 2 unexpected file(s):
  - copilotkit-nRjRp2_5.mjs
  - copilotkit-sitn7Oe8.cjs
exit=1
```

Preflight on this branch's build (expected pass):

```
$ node scripts/context-singleton-preflight.mjs
context-singleton-preflight: OK — src/v2/context.ts bundled only into 4 allowed target(s).
exit=0
```

New export test with the fix line removed (expected fail):

```
× exports the provider hooks as runtime functions
  → useLicenseContext should be exported as a runtime function: expected 'undefined' to be 'function'
```

Emitted-bundle verification after the fix:

```
$ grep -c "checkFeature: () => true" dist/copilotkit-*.mjs   # shared chunk no longer defines it
0
$ grep -o 'from "@copilotkit/react-core/v2/context"' dist/copilotkit-*.mjs | head -1
from "@copilotkit/react-core/v2/context"
$ grep -o 'require("@copilotkit/react-core/v2/context")' dist/copilotkit-*.cjs | head -1
require("@copilotkit/react-core/v2/context")
```

UMD must stay self-contained (own copy, no external import) — confirmed
unchanged:

```
dist/index.umd.js:    ownCopy=1 externalImport=0
dist/v2/index.umd.js: ownCopy=1 externalImport=0
```

Full gates:

```
$ vitest run
 Test Files  124 passed (124)
      Tests  1475 passed (1475)

$ tsc --noEmit           # exit 0
$ oxlint <changed files> # Found 0 warnings and 0 errors.
$ oxfmt                  # clean
$ publint .              # clean (only pre-existing repository.url suggestion)
$ attw --pack . --profile node16
"@copilotkit/react-core"             node16 CJS/ESM 🟢  bundler 🟢
"@copilotkit/react-core/v2"          node16 CJS/ESM 🟢  bundler 🟢
"@copilotkit/react-core/v2/context"  node16 CJS/ESM 🟢  bundler 🟢
"@copilotkit/react-core/v2/headless" node16 CJS/ESM 🟢  bundler 🟢
```

Bundle-size impact is negligible: `dist/v2/context.mjs` is 4.6 KB, and
the `bundle-size` / `copilotchat-import-size` CI checks both pass.

## Reviewer note — one behavioral trade-off

v1 and v2 share the emitted chunk, so `@copilotkit/react-core` (v1) now
**transitively depends on package self-reference**. I verified this
resolves under both ESM and CJS (above), and it's the same mechanism
`/v2/headless` already ships. Every `exports`-map-aware resolver handles
it, but a legacy `main`-only resolver (webpack 4) would not. Flagging
explicitly rather than assuming, since v1 is fully supported.

Avoiding it entirely would mean splitting v1 and v2 into separate
bundles, which duplicates the whole shared chunk — strictly worse. Happy
to take that route if we still support webpack-4-era consumers.

## Workaround for consumers on 1.66.4

```tsx
import { useCopilotKit } from "@copilotkit/react-core/v2"; // NOT /v2/context

export function useLicenseStatusCompat() {
  const { copilotkit } = useCopilotKit();
  const [status, setStatus] = useState(copilotkit.licenseStatus);
  useEffect(() => {
    const sync = () => setStatus(copilotkit.licenseStatus);
    const sub = copilotkit.subscribe({ onRuntimeConnectionStatusChanged: sync });
    sync(); // catch-up — /info may resolve before we subscribe
    return () => sub.unsubscribe();
  }, [copilotkit]);
  return status;
}
```

The `sync()` catch-up matters: `useCopilotKit` registers its re-render
subscription in an effect with no catch-up read, and a provider-only
catch-up (`CopilotKitProvider.tsx:670-696`) won't re-render a
`useCopilotKit`-only consumer since `contextValue` doesn't change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-12 14:01:58 -07:00
Benjamin Taylor 20da9fae41 feat(runtime): report managed Channel drops and recoveries as telemetry (refs OSS-825)
A managed Channel that loses its gateway link was invisible outside the
host process: the only trace was the injected `log` seam, which for a
self-hosted or Railway-hosted runtime reaches nobody who can act on it.
One 2026-08-12 outage ran 2.7 days on `kite-community` before a customer
reported it, and reconstructing it needed pod forensics.

Emit `channel_session_dropped` (carrying the cause we already compute for
the log line) and `channel_session_recovered` (carrying how long the
outage lasted). A session may report `online` without a preceding drop,
which is not a recovery and is not reported as one.

Events deliberately omit the Channel name — it is a customer-chosen
identifier — and carry no message content. Capture is fire-and-forget
with failures swallowed, the same contract fireInstanceCreatedTelemetry
uses: telemetry must never break a live session.

Also replay the drop cause on each "still down" reminder. In prod those
lines read `still down after 233134s; Phoenix is retrying` with no cause
at all, so an operator had to find the first line, 15 minutes earlier, to
learn it was an HTTP 502.
2026-08-12 10:27:40 -05:00
Ben Taylor e8d5fa71d6 fix(runtime): bump uuid off deprecated v10.0.0 (#6118)
## Summary

`packages/runtime` declares its own `"uuid": "^10.0.0"` dependency, but
nothing in the package's source actually imports it directly — id
generation in `@copilotkit/runtime` goes through `randomUUID()`
re-exported from `@copilotkit/shared`, which already depends on
`uuid@^11.1.0`. The unused v10 pin just adds an npm deprecation warning
for every consumer installing `@copilotkit/runtime`:

```
npm warn deprecated uuid@10.0.0: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
```

This bumps it to `^11.1.0` to match `@copilotkit/shared` and clears the
warning.

## Test plan

- [x] `pnpm --filter "@copilotkit/runtime^..." run build` — all
workspace dependencies build cleanly
- [x] `pnpm --filter @copilotkit/runtime run check-types` — no type
errors
- [x] `pnpm --filter @copilotkit/runtime run test` — 126 test files /
1746 tests passing
- [x] Confirmed no file in `packages/runtime/src` imports `uuid`
directly (grepped for both `from "uuid"` / `from 'uuid'` and
`require("uuid")` — zero matches)
- [x] Confirmed `pnpm-lock.yaml` now resolves `uuid@11.1.0` for this
dependency, which is not on npm's deprecated-versions list

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-12 09:43:17 -05:00
Ben Taylor d6ee68706f feat(runtime): add agentId to AgentRunnerConnectRequest (#6120)
Closes #5911

Adds an optional `agentId` field to `AgentRunnerConnectRequest` so
custom agent runners can hydrate messages when the cache is empty. Also
passes the already-available `agentId` in `handleSseConnect` through to
`runner.connect()`.

## Changes

- `packages/runtime/src/v2/runtime/runner/agent-runner.ts`: Added
`agentId?: string` to `AgentRunnerConnectRequest` interface
- `packages/runtime/src/v2/runtime/handlers/sse/connect.ts`: Pass
`agentId` to `runner.connect()`

## Verification

- Runtime package builds successfully (`pnpm --filter
@copilotkit/runtime build`)
- The change is purely additive — the field is optional and does not
affect existing runners
2026-08-12 09:19:08 -05:00
Ben Taylor 4383a86198 fix(runtime): populate request headers in runtime error context (#6287)
## Summary

`CopilotRuntime` exposes an `onError` callback, but the v1-to-v2
delegation drops it before server request processing. Runtime failures
therefore can't provide the incoming request headers that applications
use to correlate errors with a user or tenant.

## Root cause

The legacy constructor retains the callback type, while delegated
runtime options omit it. Common, SSE, and Intelligence handlers consume
failures inside their own boundaries, before a generic endpoint hook can
reconstruct the legacy event.

## Changes

- Add one internal runtime reporter that snapshots incoming Fetch
request headers.
- Attach the configured legacy callback to the delegated runtime
instance.
- Route common, SSE, and Intelligence agent-run failures, including
standard `RUN_ERROR` events, through that reporter exactly once.
- Redact sensitive request headers (authorization, proxy-authorization,
cookie, set-cookie, x-api-key, api-key, and the CopilotKit public-key
header) before they reach the `onError` event.
- Preserve responses, stream close behavior, telemetry, cleanup,
logging, endpoint hooks, and callback isolation.
- Add production-path regressions.

## Compatibility

The existing `CopilotErrorEvent` type and optional
`context.request.headers` field remain unchanged. Header names and
values come from the failing request's Fetch `Headers` object, with
sensitive credentials stripped before the event is emitted. The callback
receives a fresh record, so mutation cannot alter the request or a later
event.

## Out of scope

React provider propagation, Chat, CopilotMessages, the deprecated
runtime-client hook, redaction policy, HTTP response exposure, v2
endpoint-hook semantics, and non-agent runtime routes remain outside
this slice.

## Related PRs and Issues

Addresses #2716.

Scope follows
https://github.com/CopilotKit/CopilotKit/issues/2716#issuecomment-5086936254.
Runtime error-routing precedent:
https://github.com/CopilotKit/CopilotKit/pull/2143.

## Test plan

- [x] Runtime error regression and reporter tests, 12 + 4 tests passed.
Covers public routing, sanitized header propagation with credential
redaction, callback rejection containment, snapshots, mutation
isolation, and malformed requests.
- [x] SSE and Intelligence telemetry tests, 7 + 14 tests passed. Covers
agent-run failures, `RUN_ERROR`, setup boundaries, exact-once reporting,
telemetry, cleanup, and response preservation.
- [x] Runtime preservation suites, 57 + 129 + 56 tests passed. Existing
request, endpoint-hook, and runtime-library behavior remains intact.
- [x] Typecheck, formatting, and lint passed on changed runtime files;
lint reported four pre-existing warnings.
- [ ] CI green (`static / quality`, `test / unit` on Node 20/22/24).
2026-08-12 08:59:06 -05:00
octo-patch 30986613d8 feat(runtime): add MiniMax built-in models 2026-08-12 21:51:55 +08:00
Murat Sari 8c670653ce fix: align Angular 20 support and resolve packed smoke paths (#6452)
## What broke

The regression was introduced by commit fec70d086 (feat(angular):
checkpoint 2 - core and package), merged through PR #6109 as b07482da5.

That commit established Angular 20 as the package’s compiler and support
floor, but the demo remained on Angular 21 after 8b13fbcb7 (build:
update ng).
It also introduced the packed smoke runner without canonicalizing macOS
temporary paths, allowing /var/... and /private/var/... to resolve
  inconsistently.

  ## Why I made this change

I moved the demo back to Angular 20 so it exercises the lowest supported
Angular version, aligned the Angular 20 dependencies and support
contract on
20.3.27, and canonicalized the packed consumer directory before starting
the SSR server.

This keeps the demo, package metadata, tests, and lockfile consistent
while making the packed smoke test reliable across symlinked temporary
  directories.

  ## Changes

  - Align the Angular demo with the Angular 20 support floor.
  - Update Angular 20 dependencies and support-policy tests to 20.3.27.
- Resolve the packed consumer directory to its real path before
launching SSR.
2026-08-12 11:28:22 +02:00
Ben Taylor 04c4198a14 docs: fix Copilot Runtime reference links (#5296)
## What does this PR do?

Fixes stale Copilot Runtime documentation links that still point to
`/concepts/copilot-runtime` and now route users to the existing
`/backend/copilot-runtime` page.

This updates both the source JSDoc and the generated reference MDX so
the current docs content and future regenerated reference docs stay
aligned.

## Related PRs and Issues

- Closes #2082

## Testing

- `rg -n "concepts/copilot-runtime" packages
showcase/shell-docs/src/content` returns no matches
- `rg -n "backend/copilot-runtime"
packages/runtime/src/lib/runtime/copilot-runtime.ts
packages/react-core/src/components/copilot-provider/copilotkit-props.tsx
showcase/shell-docs/src/content/reference/v1/classes/CopilotRuntime.mdx
showcase/shell-docs/src/content/reference/v1/components/CopilotKit.mdx`
- `git diff --check`

## Checklist

- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/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-08-11 22:14:00 -05:00
Murat Sari cbf79ef52a fix: align Angular 20 support and resolve packed smoke paths 2026-08-11 21:49:36 +02:00
Lukas Moschitz 9303fabcdd fix(channels-slack): let an image use a file already in the workspace
Slack's image block takes either an external `image_url` or a `slack_file`
pointing at a file that already exists in the workspace. The required-field check
demanded `image_url` unconditionally, so the `slack_file` form could not be built
at all — an image sourced from Slack itself was unreachable through the catalog.

An image now needs alt text plus *either* source. Passing neither is still an
error: the check moved, it did not disappear, and the test covers both halves.
2026-08-11 18:22:12 +02:00
Lukas Moschitz 078116ee8d fix(channels-slack): stop tagging untyped composition objects with a type
Slack's option object is `{text, value}` — it has no `type` field, and neither do
confirm, option_group, conversation_filter, dispatch_action_config, slack_file,
trigger or workflow. The codec tagged every catalog entry with its manifest type
regardless, so each of those carried an unknown field and Slack refused the
entire message containing it.

That took out every select, multi-select, checkbox, radio group, overflow menu
and confirmation dialog authored through `Slack.Object.*` — the whole interactive
surface. Measured against a real workspace: 1 of 26 block elements was delivered
before this change, 23 after.

It went unnoticed because a refused payload produces no error. There is no log
line and no exception; the message simply never arrives, which looks exactly like
a bot that had nothing to say.

The existing catalog test asserted the very assumption that was wrong — that
every entry serializes its discriminator — so it was green while the product was
broken. It now asserts the corrected rule and guards against a silent relapse.
2026-08-11 18:21:23 +02:00
Lukas Moschitz 81e7fefb18 fix(channels-slack): correct the container slot and drop the unpostable file block
Two catalog errors, both surfaced by delivering every documented block into a
real Slack workspace rather than reading the reference again.

container serialized its children into `blocks`; Slack reads `child_blocks`, so
every container an app sent was refused — silently, because a refused payload
produces no error anywhere, just a message that never arrives.

file leaves the manifest. Slack states you cannot add it to app surfaces
directly; it only appears when *reading* messages that contain remote files, and
the same sentence appears verbatim in `@slack/types`' own doc comment. Keeping it
in an authorable catalog offered a component that could never succeed. Sending a
file remains thread.postFile(). alert stays out for the same class of reason
(modals only), and both exclusions now carry their citation so "missing" and
"deliberately not authorable" stay distinguishable.
2026-08-11 18:20:27 +02:00
Sam Julien 6540848745 chore: refresh agent artifacts for 1.67.1 2026-08-10 20:35:22 -07:00
Sam Julien 1853a24d00 fix(skills): align public guidance with current APIs 2026-08-10 20:32:31 -07:00
Maxim b4cfcf6f98 fix(react-core): stop the purity gate crashing opaquely on the declared Node floor
`assert-headless-purity.mjs` resolved its dist directory with
`import.meta.dirname`, which landed in Node 20.11 and is `undefined` below it.
The root package.json declares `engines: { "node": ">=18" }`, so a contributor
or runner on Node 18 hit this hard-fail CI gate as:

    TypeError [ERR_INVALID_ARG_TYPE]: The "paths[0]" argument must be of type
    string. Received undefined
        at Object.resolve (node:path:1115:7)
        at .../scripts/assert-headless-purity.mjs:71:19

— a stack trace into node internals, at module load, that names neither the
gate nor the real problem. Reproduced against a real Node 18.20.8.

Switch to `path.dirname(fileURLToPath(import.meta.url))`, which both sibling
scripts in this CI job already use (react-core's measure-copilotchat.mjs and
react-native's measure-headless.mjs), so all three read the same and none of
them carries a hidden runtime floor its own package does not declare.

Verified under real Node 18.20.8: the script now walks all four entries (650 /
646 / 649 / 645 modules) and exits 0, and still exits 1 with the full
`links the heavy render stack` report when a forbidden dep is injected into a
dist entry. The metafile-driven graph walk, the loud failure on unresolvable
edges and all 17 negative tests are untouched (`test:scripts`: 19 pass).

Skill-staleness check (reskinnable-demo CLAUDE.md rule): not applicable — this
touches packages/react-core, nothing under .claude/skills/reskin/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:27:41 +02:00