3119 Commits

Author SHA1 Message Date
Jordan Ritter f8160e09b9 fix(react-core): gate suggestion submission on readiness + production-shaped SSE regression test 2026-08-19 14:33:25 -07:00
Tyler Slaton 9087121bd5 fix(web-inspector): preserve state across pop-out 2026-08-19 14:03:12 -07:00
Alem Tuzlak 1ef16b6789 feat(web-inspector): pop the Inspector into its own window
Keep the same live Inspector session in a named browser popup, restore it when the popup closes, and document the workflow.
2026-08-19 14:03:12 -07:00
Jordan Ritter 9b0e3dc88a fix(react-core): gate CopilotChat submission on runtime readiness (empty assistant response) 2026-08-19 13:37:03 -07:00
Martha Kelly Schumann 3fc33398af Merge branch 'main' into codex/ent-1157-shared-clerk-session 2026-08-19 13:28:46 -07:00
Tyler Slaton f1b26e4aa8 fix(react-core): hide local inspector action in production 2026-08-19 13:28:26 -07:00
Tyler Slaton 367e7bda15 feat(react-core): add local message inspector links 2026-08-19 13:28:26 -07:00
Martha Kelly Schumann af6c7ad5e0 Merge branch 'main' into codex/ent-1157-shared-clerk-session 2026-08-19 13:20:24 -07:00
Benjamin Taylor 4df1e3dccd docs(a2ui): require a literal-or-binding union for bound props (refs OSS-857)
Three findings from a LangGraph TypeScript onboarding run, plus the
supporting re-export.

The A2UI binder decides whether to resolve a `{ path }` binding by
inspecting the prop's Zod type: `scrapeSchemaBehavior` classifies a
`ZodUnion` containing an object with a `path` key as DYNAMIC and
everything else as STATIC, and STATIC returns the value untouched. A
bound prop declared as a plain `z.string()` therefore reaches the
renderer as the raw `{ path: "/origin" }` object, and the first thing
that renders it as text throws React error #31. The fixed-schema page
said the opposite — that renderer props are "plain z.string(), not a
path-or-literal union" — so the obvious declaration produced an opaque
crash. The reference cell already declares the union and carries a
comment explaining why, but that comment sits outside the
`definitions-types` region marker and so never reaches the page.

`DynamicStringSchema` is real; it lives in `@a2ui/web_core`, which is a
transitive dependency of `@copilotkit/a2ui-renderer` and so not
reliably importable from application code. Re-exported here with its
numeric/boolean/list siblings and their types.

The LangGraph quickstart's troubleshooting advice told everyone with a
connection problem to swap `localhost` for `0.0.0.0` or `127.0.0.1`.
That is backwards for the Node runtime: `langgraphjs dev` defaults to
`--host localhost`, which Node resolves to IPv6 and binds `::1` only,
so `127.0.0.1` is refused by the same running server. The Python CLI
defaults to `--host 127.0.0.1` and behaves the other way, so the advice
is now split across the page's existing Python/TypeScript language tabs
instead of stated once in shared prose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 14:48:49 -05:00
github-actions[bot] ac22a414ef style: auto-fix formatting 2026-08-19 12:05:07 +00:00
Alem Tuzlak 06cef9e659 feat(web-inspector): add view thread in your app
Let the Inspector load a saved thread into the official React or Vue chat. Core owns a two-way EventClient bridge. Official chat configuration applies an in-memory override that wins over a pinned threadId. Production builds hide the action.
2026-08-19 13:50:39 +02:00
Tyler Slaton f8b5de55ab feat(runtime): report managed Channel drops and recoveries as telemetry (refs OSS-825) (#6465)
## Why

A managed Channel that loses its gateway link is invisible outside the
host process. The only trace is the injected `log` seam, wired to
`logger.warn` — which for a self-hosted or Railway-hosted runtime
reaches nobody who can act on it.

## What

- `oss.runtime.channel_session_dropped` — carries the cause already
computed for the log line (`reason`, and the transport `code` when the
transport named one).
- `oss.runtime.channel_session_recovered` — carries `downForMs`, so
outage duration is measurable rather than inferred from log timestamps.
- The drop cause is now replayed on every "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 scroll back to the first line
— 15 minutes earlier, or hours, given the exponential backoff — to learn
it was an HTTP 502.

## Deliberate choices

- **No Channel name in the events.** It is a customer-chosen identifier
that can carry business meaning, so it stays out of anonymous OSS
telemetry. No message content or credentials either. Per-channel
aggregate counts still work without it.
- **An `online` transition with no preceding drop emits nothing** — a
session can report online without having dropped, and that is not a
recovery.
- **Capture is fire-and-forget with failures swallowed**, the same
contract `fireInstanceCreatedTelemetry` uses. The `try` also covers a
`capture` that throws synchronously. Telemetry must never break a live
session.
- The `gave_up` line's OSS-670 wording is untouched — it deliberately
says retries continue, and that is now accurate.

## Testing

Three tests added to `channel-manager-reconnect.test.ts`, each watched
fail first: the dropped event with its cause, the recovered event with a
positive duration, and the no-bogus-recovery guard. A fourth pins the
cause on the repeat log line.

```
✓ src/v2/runtime/core/__tests__/channel-manager-reconnect.test.ts (11 tests)
Tests  11 passed (11)
```

Wider run: 106 tests pass across `core/__tests__` and `telemetry`. Two
notes, both verified pre-existing by stashing this branch's changes and
re-running:

- `channel-manager-recovery.test.ts` fails to *load* in my worktree
(`Cannot find package '@copilotkit/channels-slack/render'`) — a
subpath-export resolution artifact of a worktree with symlinked
`node_modules`, identical with these changes stashed.
- `tsc --noEmit` reports 11 errors, the same 11 before and after this
change, none in the files touched here.

`oxfmt` and `oxlint` clean on all three files. Lefthook was bypassed on
the commit because of the same worktree `node_modules` symlinking; I ran
both tools manually over exactly the staged files instead.

Refs OSS-825.
2026-08-18 17:49:55 -07:00
copilotkit-qa-bot[bot] ec4439c8d9 Merge origin/main into codex/ent-1157-shared-clerk-session 2026-08-18 10:28:01 -07:00
Ben Taylor 471d74bad4 feat(react-ui): report feedback state to onThumbsUp/onThumbsDown (#6531)
Closes #2615.

## Problem

The v1 thumbs callbacks were typed `(message: Message) => void`, so a
consumer
received the message but not *what the click did*. A custom
`AssistantMessage`
that keeps its own toggle state — the case in the issue — had nowhere to
put
that value:

```tsx
onClick={() => {
  setReactionValue((v) => (v === "like" ? null : "like"));
  onThumbsUp?.(message); // 👈 no way to say whether this applied or retracted
}}
```

The only workaround was counting clicks per message.

The built-in path had the matching gap. `Chat.tsx` already tracked
`messageFeedback`, but `handleThumbsUp` unconditionally wrote
`"thumbsUp"`, so
the state was write-only: clicking an active button re-applied the same
value
and there was no way to un-vote.

These callbacks are live API — `RenderMessage` forwards `onThumbsUp`,
`onThumbsDown` and `feedback` to whatever component is passed as the
`AssistantMessage` prop, which is exactly the customisation the issue
describes.

## Fix

Add an optional second argument reporting the state the click
transitions to:

```ts
onThumbsUp?: (message: Message, isActive?: boolean) => void;
```

- The built-in `AssistantMessage` derives it from the message's current
`feedback`, so a second click on an active button now reports `false`
and
  **retracts** the feedback rather than re-applying it.
- A custom `AssistantMessage` can pass its own value straight through.
- The parameter is optional and appended, so existing one-argument
handlers
  keep type-checking and keep working unchanged.

`onFeedbackGiven` fires only on the applying click — its signature is
`(messageId, "thumbsUp" | "thumbsDown")` and has no way to express a
retraction, so reporting one would be a lie.

Note this is a small behavioural change to the built-in buttons:
previously a
repeat click was a no-op, now it clears the vote. That is what makes the
reported state meaningful, and it matches the toggle UX in the issue.

The toggle logic lives in a new `./feedback` module. This package's
vitest
project runs `environment: "node"` and only collects `*.test.ts`, so
there is no
component-rendering harness here — extracting the pure functions is what
makes
the behaviour testable at all.

## Testing

`packages/react-ui` — full suite, including the 10 new cases:

```
 ✓ src/components/chat/feedback.test.ts (10 tests) 3ms
 ✓ src/components/chat/Markdown.test.ts (29 tests) 5ms
 ✓ src/components/chat/Markdown.xss.test.ts (13 tests) 182ms
 ✓ src/css/sidebar-full-height.test.ts (4 tests) 2ms

 Test Files  10 passed (10)
      Tests  68 passed (68)
```

Coverage: activation from no feedback, deactivation on the
already-active
button, switching to the opposite button, retraction removing the map
entry
rather than storing a falsy value, other messages left untouched, no
mutation of
the previous map, referential stability when nothing changes, and a
toggle
round-trip.

**Mutation checks** — broke each mechanism and confirmed the tests fail,
so they
are not self-fulfilling:

| Mutation | Result |
| --- | --- |
| `isActivatingClick` → `return true` | `Tests 1 failed \| 9 passed` |
| `applyFeedbackClick` retraction branch → `if (false)` | `Tests 3
failed \| 7 passed` |

Both were reverted and the suite returned to 68 passing.

**Types/build** — `tsc --noEmit` clean, `tsdown` clean, and the widened
signature reaches the published types:

```
$ grep -n "onThumbsUp" dist/index.d.mts
102:  onThumbsUp?: (message: Message, isActive?: boolean) => void;
185:  onThumbsUp?: (message: Message, isActive?: boolean) => void;
272:  onThumbsUp?: (message: Message, isActive?: boolean) => void;
```
2026-08-18 08:56:55 -05:00
Ben Taylor 2d0b2838c0 fix(react-ui): pad the chat header on mobile viewports (#6530)
Fixes #2493.

## Problem

`.copilotKitHeader` declared its horizontal padding like this:

```css
.copilotKitHeader {
  padding-left: 1.5rem;      /* no padding-right */
  justify-content: space-between;
}

@media (min-width: 640px) {
  .copilotKitHeader {
    padding-left: 1.5rem;    /* duplicate of the base rule */
    padding-right: 24px;     /* only above the sm breakpoint */
  }
}
```

`padding-right` existed **only** inside `@media (min-width: 640px)`. The
header
lays out `space-between`, so below 640px the title sat against the left
edge
(fine — `padding-left` is unconditional) while the controls ran flush to
the
right edge with no gutter at all. That is the cramped mobile header in
the
report.

Worth noting because it misleads when reading the file: the
`.copilotKitHeader > button { position: absolute; right: 16px }` rule
further
down does **not** apply to the close button. `Header.tsx` nests it
inside
`.copilotKitHeaderControls`, so the button is a grandchild and the child
combinator never matches. The button is in normal flow, which is why it
lands
exactly on the padding edge.

## Fix

Move the horizontal padding onto the unconditional rule and drop the
duplicated
declarations from the media query, leaving it responsible only for the
border
radii. `24px === 1.5rem` at the default root font size, so layouts at
640px and
above are byte-for-byte unchanged; only the sub-640px case gains the
gutter it
was missing.

## Testing

Added `src/css/header-padding.test.ts`, which strips every `@media`
block and
asserts the remaining unconditional `.copilotKitHeader` rule declares
both
`padding-left` and `padding-right` — i.e. that the padding is not
breakpoint-gated
again.

Full `react-ui` suite:

```
 ✓ src/esm-compat.test.ts (1 test) 2ms
 ✓ src/css/sidebar-full-height.test.ts (4 tests) 2ms
 ✓ src/css/header-padding.test.ts (1 test) 1ms
 ✓ src/hooks/__tests__/use-push-to-talk.test.ts (2 tests) 2ms
 ✓ src/components/chat/Markdown.test.ts (29 tests) 5ms
 ✓ src/components/chat/Markdown.xss.test.ts (13 tests) 182ms

 Test Files  10 passed (10)
      Tests  68 passed (68)
```

**Mutation check** — removed `padding-right: 1.5rem` from the base rule
and
re-ran, confirming the new test fails rather than passing vacuously:

```
     37|     expect(base).toMatch(/padding-left:/);
     38|     expect(base).toMatch(/padding-right:/);
       |                  ^
 Test Files  1 failed (1)
      Tests  1 failed (1)
```

**Build** — `tsdown` succeeds and the compiled `dist/index.css` carries
the
declaration:

```
.copilotKitHeader {
  ...
  padding-left: 1.5rem;
  padding-right: 1.5rem;
  ...
}
```

**Visual** — rendered the real `Header.tsx` markup against the compiled
stylesheet at a 390px viewport, with one copy re-gating `padding-right`
behind
the breakpoint to reproduce the previous rule. Before, the close button
sits
flush on the right border; after, it clears it by 24px, matching the
existing
left gutter. (Screenshot to follow in a comment.)
2026-08-18 08:53:16 -05:00
lukasmoschitz 72d57a41ec fix(channels-slack): rename decimal_allowed to Slack's is_decimal_allowed (OSS-794) (#6519)
OSS-794.

## The defect

`SlackNativeProps` declared `decimal_allowed`. Slack's `number_input`
field is **`is_decimal_allowed`** (confirmed in `@slack/types@2.22.0`,
where it is also *required*).

Slack accepts a message whole or not at all. The unrecognised key
refused the entire `chat.postMessage` call and ended the Channels
delivery that carried it — no exception reached the caller, nothing
arrived in the channel.

So the **typed path was the broken one**. Anyone who bypassed our types
and hand-wrote `is_decimal_allowed` got it right by accident; anyone
using `Slack.Element.NumberInput` silently lost the message. That is the
opposite of what an SDK should do.

## Proof, before the change

Verified live against Slack (private `#bot-test`), one delivery per case
— a refusal ends the whole turn's delivery, so batching would have made
the results meaningless.

| Case | Payload | Outcome |
| --- | --- | --- |
| A | `decimal_allowed: true` (our typed spelling) | **refused** —
`invalid_blocks: invalid field at /blocks/0/element` |
| B | no decimals field at all | **refused** — same error |
| C | `is_decimal_allowed: true` (Slack's spelling) | **delivered** |
| E | *both* names present | **refused** — same error |

Case B refuses for its own reason: `@slack/types` marks
`is_decimal_allowed` **required**, so omitting it is independently
invalid. That makes B a poor control, so case E was added — it satisfies
Slack's required field and is otherwise byte-identical to the payload
that delivers, leaving the unknown `decimal_allowed` key as the only
possible cause. A vs C vs E isolates the name as the whole story.

In the refused threads only the caption arrived; the block itself is
simply absent — the silent-loss shape the defect produces in production.

## Is this breaking?

**No, and no alias is kept.** The old name never worked: every payload
carrying it was refused by Slack, so no caller can be depending on
working behaviour. Keeping `decimal_allowed` as an alias would preserve
the trap. Removing it converts a silent message loss into a compile
error that names the right field:

```
error TS2561: Object literal may only specify known properties, but 'decimal_allowed'
does not exist in type 'SlackNativeProps<unknown>'. Did you mean to write 'is_decimal_allowed'?
```

## Proof, after the change

Built locally, copied over the installed `@copilotkit/channels-slack` in
a real OpenTag runtime, confirmed the process held the new build (`tsc`
reads the copied `.d.ts` — it accepts `is_decimal_allowed` with no cast
and rejects `decimal_allowed` with the error above), then re-ran case A
through the fixed typed surface: **delivered**, block present in the
thread.

## Sibling sweep

Every field name `native.ts` declares was compared against Slack's Block
Kit vocabulary. **No other mismatch was found.** Names absent from
`@slack/types` are absent because `@slack/types` does not model those
blocks, not because they are misspelled — they are enumerated in the
guard.

Two adjacent observations, not fixed here: `dispatch_action`,
`min_length` and `max_length` are Slack fields we do not declare at all
(callers reach them via `as never`). Those are gaps rather than
mismatches, so they are out of scope for this PR.

## The guard

`src/__tests__/native-field-names.test.ts` compares every field name
`native.ts` declares against Slack's own Block Kit declarations in
`@slack/types` — already a direct dependency of this package, so no new
dependency was added. Declarations are read with the TypeScript parser
rather than matched as text.

**It fails when it should.** Temporarily restoring the misspelling turns
it red with an actionable message:

> native.ts declares `decimal_allowed`, which @slack/types does not.
Slack refuses a whole message for one unrecognised key, so a wrong name
here deletes every message using the component. Either correct the name
to Slack's, or — if Slack really does accept it and @slack/types is
simply behind — add it to NOT_COVERED_BY_SLACK_TYPES with the reason.

**It is honest about coverage.** `@slack/types` is incomplete, so a
missing name is not proof a name is wrong. Every uncovered field is
listed individually with its reason rather than being waved through:

- `blocks` — `container`'s child slot; `@slack/types` declares no
container block
- `offset` — documented on `rich_text_list`, absent from `@slack/types`'
`RichTextList`
- `slack_icon`, `subtext` — accepted on `card`, not declared by
`CardBlock`
- `chart`, `segments`, `series`, `axis_config`, `categories`, `x_label`,
`y_label`, `data` — `data_visualization` has no counterpart in
`@slack/types` at all

**It cannot pass trivially.** Floors assert the extracted vocabulary is
real (≥100 names, plus spot checks) and that a meaningful number of
names were actually compared (≥30), so a collapsed comparison fails
instead of looking green. Two further assertions keep the exemption list
from rotting: an exemption `@slack/types` has since started declaring,
or one for a name we no longer declare, both fail.

## Verification

| Command | Result |
| --- | --- |
| `oxfmt --check packages/channels-slack` | clean, 66 files |
| `oxlint` on both changed files | 0 warnings, 0 errors |
| `nx run @copilotkit/channels-slack:check-types` | pass (+ 8 dependent
tasks) |
| `nx run @copilotkit/channels-slack:test` | 31 files, 411 tests passed
|
| `nx run @copilotkit/channels-slack:build` | pass |
| lefthook pre-commit (`test-and-check-packages`, all packages) | pass |
2026-08-18 13:45:40 +02:00
xiaoqinvar 2f2481a048 Merge branch 'main' into codex/pr-6253-review-fix 2026-08-18 17:56:16 +08:00
xiaoqinvar 08f05b39f4 fix(core): preserve live message run reassignment 2026-08-18 17:28:33 +08:00
Benjamin Taylor cb11f6fb93 fix(runtime): warn when message content parts are dropped
normalizeMessageContent maps array content and handles only "text" and
"binary" parts. Any other part -- the {"type": "image", ...} case from
#1748 -- maps to "" and is filtered out with no signal at all, so an
agent emitting structured content sees its output silently vanish.

Carrying those parts through needs an AssistantMessage schema change and
is tracked separately in OSS-767. This makes the current drop visible in
the meantime, once per unrecognised part type so streaming does not flood
the log.

Refs #1748

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:25:01 -05:00
Benjamin Taylor ffd15801d6 fix(react-core): re-render consumers when the agent node changes
useAgentNodeName tracked the current node in a ref and returned
nodeNameRef.current. Mutating a ref schedules no render, so a component
reading useCoAgent().nodeName kept showing whichever node was current at
its last render and never updated on its own -- it only appeared to work
when something unrelated happened to re-render it.

Backing the value with state fixes that. Adds the coverage the hook never
had: transitions, run-start reset, run-error, and unsubscribe on unmount.

Note this does NOT explain GH #1426 (interrupt agentMetadata.nodeName
reporting the previous node). useInterrupt only evaluates the `enabled`
predicate from a useEffect/useMemo keyed on its `pending` state, so the
interrupt's own state update always forces a render before the predicate
runs -- and the pre-fix code reads the ref correctly at that point. That
report needs a different explanation; it is left open.

Refs #1426

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:25:01 -05:00
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