120 Commits

Author SHA1 Message Date
tylerslaton 92f704e3b4 chore: release monorepo v1.71.1 2026-09-11 22:01:26 +00:00
tylerslaton cac7cde862 chore: release monorepo v1.71.0 2026-09-09 22:24:05 +02:00
Maxim e69a7c08e2 docs(react-native): cite the shim's removal issue in its deprecation notes
@BenTaylorDev noted that "removal in the next minor" appeared 11 times across
5 files with no issue behind it, which is how a shim becomes permanent. Filed
as CopilotKit/CopilotKit#6976 and Linear OSS-1148, and cited in the shim's
JSDoc, the entry-point comment, and the reference page.

The runtime warning text is deliberately unchanged — a test asserts on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 19:32:46 +02:00
Maxim af0c5628e0 fix(react-native): correct what a tool named * did, and warn on the last silent route
Two things, both in the render-tool shim and its tests.

1. The PR's headline claim was false on current main.

The shim, its tests and `headless.ts` all said the old RN hook "advertised
`*` to the model". Core never has. `buildFrontendTools` filters the name out
of the list it hands the agent (`core/src/core/run-handler.ts`, the
`tool.name !== WILDCARD_TOOL_NAME` clause), with a comment saying that
advertising it would offer the agent a tool named `*`. That filter arrived in
31aa1e2162, hours before this PR was opened; the rebase moved the code forward
and left the prose behind.

Measured by vendoring origin/main's hook verbatim and driving it through this
branch's own recording agent:

  A  old hook, wildcard        in core.tools true   advertised [[]]
  B  old hook, no description  in core.tools true   advertised [["legacyJsShape"]]

Row A is the correction. Isolated further: two `useFrontendTool` registrations
with identical description/parameters/render and only the name differing give
registry `["*","notAWildcard"]` and advertised `[["notAWildcard"]]`, so the
NAME is the sole cause.

The real mechanism is stronger. `*` is core's catch-all HANDLER name: when a
tool call has no matching frontend tool and no result yet, core reaches for
`getWildcardTool()` and runs `executeWildcardTool`, whose tool-result splice
and follow-up return sit OUTSIDE the `if (wildcardTool?.handler)` guard (opens
at :1000, closes at :1089; the splice block is :1091-1120, with
`toolCallResult` initialised to `""`). Driving one turn through it confirms it
rather than only tracing it — an assistant message calling an unregistered
tool yields 2 turns and a spliced `{ toolCallId, content: "" }` through the old
hook, versus 1 turn and no tool result through core's `useRenderTool`. So a
display-only wildcard was auto-answering every otherwise-unanswered tool call
and paying for a follow-up turn. Bounded: a server-side call whose result has
already arrived never reaches that branch.

Two `expect(agent.advertised).toEqual([[]])` assertions were introduced under
comments calling them the ones that catch this "where it would actually hurt".
They cannot: they return the same value on origin/main. Both are kept as
forward guards against someone making `*` advertisable later, with comments
that now say so and point at the registry assertions as the discriminating
ones. Confirmed by mutation: removing rule 1 from `routeFor` kills
`getTool({ toolName: "*" })` and leaves both `advertised` assertions green.

2. The renderer-only route is no longer silent.

`{ name, parameters, render }` is the one shape routing cannot discriminate:
it is both the correct new renderer-only spelling and an old plain-JS call
that registered AND advertised a real tool (`description` was required by the
old types, so only untyped JS could reach it; `FrontendTool.handler` is
optional and `buildFrontendTools` does not filter on it). Rows B and C are
that comparison — `[["legacyJsShape"]]` before, `[[]]` after, zero warnings.
So the one shape the shim could not route was also the one it said nothing
about, which is the exact failure the shim exists to prevent.

`warnRouted` now warns on that route too, deduped per tool name and dev-only
like the others, saying the call registers a renderer only and pointing at
`useFrontendTool` for anyone who relied on the advertisement. Guarded on the
ROUTE rather than merely on the absence of legacy fields, so a frozen
`useFrontendTool` route whose fields were later dropped is not mislabelled.
The pre-existing wildcard-branch message is likewise no longer worded as if
every renderer-only route were the wildcard.

The test that asserted silence on this route is updated rather than left
asserting the old behaviour, and two new tests cover the warning and its
dedup/production gating. All three fail if the unconditional warn is reverted.

Also: dropped the duplicated per-status-spread explanation (it appeared in
both the JSDoc and the body of `toFrontendTool`), and documented that the
renderer-only routes register in `useEffect` where `useFrontendTool` uses
`useLayoutEffect`, so a call that used to land in the layout phase now lands
one phase later. Both hooks' effect types verified in the current source.

react-native vitest 300/300 across 23 files at default settings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XyonXjic9ZEuPTpzgN2uS
2026-09-09 19:32:46 +02:00
Maxim 1cc28f3243 fix(react-native): replace a literal NUL byte in the render-tool shim's warn key
The route-drift `warnOnce` key in the `useRenderTool` shim separated
`${config.name}` from `route-drift` with an actual 0x00 byte, not the escape
sequence `\x00`. It sat at byte offset 14224.

Two consequences, both verified:

  - ripgrep classified the whole file as binary ("binary file matches (found
    "\0" byte around offset 14224)") and printed NO lines, so an `rg` sweep
    over `packages/` returned nothing from this file — including for the very
    identifiers `headless-entry-surface.test.ts` polices by text. `file(1)`
    reported `data`.
  - `git diff` still rendered it as text, because git's binary heuristic only
    reads the first 8000 bytes and this byte is past that. It looked like an
    ordinary space in every diff view, which is how it survived review.

The byte also reached the emitted bundle as part of the dedup key.

Replaced with `:`. Verified at the byte level rather than visually: a scan for
bytes below 0x20 other than tab/newline/carriage-return now returns none,
`file(1)` reports "UTF-8 text", and `rg` prints 28 matching lines where it
previously printed zero. Every other file in the PR's diff was scanned the same
way; none contained a control byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XyonXjic9ZEuPTpzgN2uS
2026-09-09 19:32:45 +02:00
Maxim 95c9c0a5a0 docs(react-native): make the render-tool docs true of the shim, not of the deletion
The deprecated `useRenderTool` shim landed after these pages were written, so
they described a harder break than the code delivers — "React Native has no
render-tool API of its own", and a migration table presenting the old hook as
simply gone.

reference/react-native/hooks/useRenderTool:

- The overview and the migration narrative now say what actually happens: the
  name still exists, still works this release, routes by shape, warns, and goes
  away in the next minor. The routing table is the shim's three rules verbatim
  (src/hooks/useRenderTool.ts `routeFor`), including the unconditional wildcard
  and why it is unconditional.
- The warning is quoted from the source string rather than paraphrased, and gets
  its own "What the warning does not cover" section: dev-only gate, per-name
  dedup for the module's lifetime, silent when no old field is present, and
  emitted from an effect so an unmounted screen never warns.
- "Three shapes the compiler does not catch" is kept, not deleted. It is now
  framed as what the shim exists to route — and as what to audit by hand anyway,
  because the notice is development-only and because all three go back to
  degrading silently once the shim is removed.
- Two new migration rows for losses the table omitted: bare `RenderToolProps`
  is now `TS2314` (core's `S` has no default where RN's `T` did), and the
  `args` -> `parameters` render-prop rename is still `TS2339` even through the
  shim — verified with tsc, the shim restores the old CONFIG fields only. The
  `RenderToolFunction<T>` row now points somewhere instead of saying "gone".
- `parameters` was documented as `Partial<T> | T`, using a generic this page
  renamed to `S` and explicitly warns is the schema rather than the arguments.
  It is `Partial<InferSchemaOutput<S>> | InferSchemaOutput<S>`
  (react-core/src/v2/hooks/use-render-tool.tsx:9-31).

reference/react-native/hooks/useFrontendTool had zero mentions of `ReactElement`,
`ReactNode` or `FlatList` despite being where the migration sends people. It now
documents that its `render` is a `React.ComponentType` and therefore accepts a
bare string that throws on a device, and points at the opt-in
`FrontendToolRenderFunction<T>`.

docs/frontends/react-native and packages/react-native/USAGE.md (which ships in
the npm tarball — no `files` array, no `.npmignore`) get the same corrections at
their own length.

Every code sample added here was typechecked by pasting it into the package and
running `tsc --noEmit`, including the ones asserted to FAIL. Internal links and
heading anchors were checked mechanically against the content tree.
2026-09-09 19:32:45 +02:00
Maxim 2386b5ab65 feat(react-native): add an opt-in element-only render type for useFrontendTool
The convergence deleted RN's `RenderToolFunction`, which was the only thing
narrowing an RN render function to `ReactElement | null`. The migration points at
`useFrontendTool`, whose `render` is `ReactToolCallRenderer<T>["render"]` — a
`React.ComponentType`, so it returns `ReactNode`. Verified with tsc: a `render`
returning a bare string compiles inline in a `useFrontendTool` call today, and
then throws "Text strings must be rendered within a <Text> component" on a
device. Before the rename that call site was a compile error, so the migration
traded away a real crash-prevention property.

`FrontendToolRenderFunction<T>` gives it back. Named for the hook it annotates
rather than reusing `RenderToolFunction`: that name was just deleted, and
reviving it against a DIFFERENT hook's props would be the same defect this PR
exists to fix — a name whose meaning silently moved.

DERIVED from core's contract, not re-declared: props come from
`React.ComponentProps<ReactToolCallRenderer<T>["render"]>` unchanged and only the
return type is React Native's. That is the precedent the deleted file set, and
the reason is on record — the last time this package declared its own render-prop
shape it drifted to `{ args: T; status: "executing" | "complete"; result?: string }`
(commit ebf0f94fb8^), with no `name`, no `toolCallId`, no in-progress arm and
`args` unconditionally complete. `RenderToolProps` / `UseRenderToolOptions` are
NOT reintroduced.

It is opt-in and the JSDoc says so: it changes no hook signature, so an
unannotated inline renderer is still checked against core's `ReactNode` contract.

The type test is a `.test-d.tsx`, which `tsc --noEmit` compiles (the package
tsconfig has `include: ["src"]`) but vitest's
`src/**/__tests__/**/*.{test,spec}.{ts,tsx}` glob does not collect. Both
directions bite, proven by mutation: widening the return to `ReactNode` fails
with 4x TS2578 (unused directive), and replacing the derived props with `any`
or `unknown` fails too — so "derived, not re-declared" is asserted, not just
asserted about.

Also trims the 21-line comment over the render-tool re-exports down to what a
reader needs at that line: the shim is temporary, and do not reintroduce a local
hook under either name. The rest now lives on the reference page and in the
guard's own comment.
2026-09-09 19:32:44 +02:00
Maxim 29d16e4db3 test(react-native): make the shim's warn-once dedup and frozen route bite
Two gaps found by mutating the shim.

The "fires once per distinct tool name" test passed with the module-level dedup
set DELETED, because it only re-rendered: the warning lives in an effect whose
deps are the tool name and the routed hook, so a re-render never re-runs it and
the effect's dependency array was doing all the work. It now unmounts and mounts
again — a fresh mount runs a fresh effect, which is navigating back to a screen
on a real device — so the dedup set is what the assertion rests on. Deleting it
now fails with 2 calls instead of 1.

The frozen route had no test at all. A config whose shape changes between
renders (`handler: enabled ? fn : undefined`) keeps the route it was first
registered under, because a hook cannot be called conditionally unless the
condition is stable for the component's lifetime. That limitation is now
asserted from both ends: the drift warning fires, and the tool is still not
registered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XyonXjic9ZEuPTpzgN2uS
2026-09-09 19:32:44 +02:00
Maxim 2b34d975e1 feat(react-native): reintroduce useRenderTool as a loud, deprecated shim
The convergence removed RN's `useRenderTool` outright. That break ships in a
MINOR — @copilotkit/react-native is in the 16-package lockstep `monorepo`
release scope, so a major is not on the table — which leaves the compiler as
the only signal reaching consumers, and there are three call shapes it cannot
see: a hoisted config object whose `render` ignores its props (excess-property
checking needs a fresh literal), the old fields arriving via a spread into an
otherwise fresh literal, and an untyped or `@ts-nocheck` call site (plain-JS RN
screens are common).

In all three, core's `useRenderTool` silently ignores `description` and
`handler`, and because core's bridge spreads `{ ...props, parameters: props.args }`
the old `render: ({ args }) => …` keeps painting exactly as before. The screen
looks unchanged while the tool stops being registered and advertised and the
handler never runs again. This shim is the softer landing @BenTaylorDev asked
for: it routes those calls the way the old hook did, and says so out loud. It
is deprecated, `@deprecated`-marked on every overload, and scheduled for
removal in the next minor; react-core is untouched.

Routing, exactly:

1. `name === "*"` wins UNCONDITIONALLY -> core's `useRenderTool` (renderer-only,
   schema-less wildcard path), then warns about the old fields it ignored. This
   is deliberately not the obvious reading. The old hook made `description`
   REQUIRED, so every wildcard renderer anyone ever wrote carries the old tool
   fields; routing "has old fields" to `useFrontendTool` would recreate the
   original `*`-named-tool bug for precisely the people who had tried hardest to
   use the wildcard.
2. otherwise `handler` or `description` present -> core's `useFrontendTool`
   (tool AND renderer, which is what the old hook actually did), warning that
   the call should be renamed.
3. otherwise -> core's `useRenderTool`, unchanged.

The route is frozen at first render, because a hook cannot be called
conditionally unless the condition is stable for the component's lifetime; a
config that changes shape mid-life keeps its original route and gets its own
warning rather than being silently re-registered elsewhere. Warnings are
dev-only (`process.env.NODE_ENV`), `[CopilotKit]`-prefixed and deduped through a
module-level Set — once per distinct tool name, never once per render, matching
react-core's `warnedUnknownStatuses` idiom.

Registration is DELEGATED on every path: this package still owns no registry.

The entry-surface suite asserted that RN's `useRenderTool` IS core's binding,
which the shim makes false. Rather than weaken that guard — it closed a blind
spot in which every presence check stayed green through the whole convergence
while verifying nothing — its two halves are replaced at equal strength:
`useFrontendTool`'s identity assertion stays as-is, and `useRenderTool` is now
policed by (a) a delegation test that reads which module the entry exports the
name from, asserting runtime identity when that is core's entry and otherwise
requiring the local module to import AND CALL both core hooks, and (b) a
package-wide deny-list that fails if any module in the headless graph touches
`addTool` / `removeTool` / `addHookRenderToolCall` / `renderToolCalls` or
`createContext` — i.e. if RN ever rebuilds a local registry.

Proven by mutation, with the routing suite reading core's own observable state
(`getTool`, `core.tools`, the tool list core hands an agent on a real run)
rather than a spy's arguments.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015XyonXjic9ZEuPTpzgN2uS
2026-09-09 19:32:43 +02:00
Maxim 62cf3878d7 docs(react-native): document core's useRenderTool as React Native's own
Rewrites the RN reference page for core's hook, points tool-plus-renderer users
at `useFrontendTool`, and carries the migration table. That table is the actual
consumer-facing channel for this break: the release-note collector
(scripts/release/lib/changes.ts) reads `git log --format=%H %s` into a
`{ hash, subject }` type, so a `BREAKING CHANGE:` footer has nowhere to land
(#6479).

The migration's loud-failure guarantee is stated precisely rather than
absolutely, because the absolute form is false. TypeScript's excess-property
check rejects an old call site (`TS2769` on `description`) only when the config
is a fresh object literal with those fields written inline; the renamed render
prop gives `TS2339`. Three shapes escape it, each named on the page: a hoisted
config whose `render` ignores its props, the same fields arriving via a spread,
and an untyped or `@ts-nocheck` call site. The last is the one to worry about —
core's bridge spreads `{ ...props, parameters: props.args }`, so `args` still
arrives at runtime and an old renderer keeps painting correctly while the tool
has silently stopped being registered.

Retires three § Known limitations entries this convergence closes (the wildcard,
`followUp`/`available` forwarding, and `handler`'s missing context argument) and
leaves the unrelated ones intact.

Deletes a false claim the previous docs shipped: that comparing `status` against
a bare string literal does not typecheck. A string-enum member is assignable to
its own literal type, so the comparison compiles and narrows in every direction
— verified with tsc in all four combinations. RN's `status` moves from the
`ToolCallStatus` enum to core's string-literal union, and that is explicitly not
a break; no migration work follows from it.

Also updates packages/react-native/USAGE.md, which still taught the deleted API.
That file ships in the published tarball — package.json declares no `files` array
and there is no .npmignore — so the package was documenting an API the package no
longer has. Its remaining samples were compiled against the shipped overloads.

Corrects the primary guide's claim that a `ReactElement | null` render return is
React-Native-specific: it is identical on the web. What is RN-specific is the
consequence of `useFrontendTool`'s looser render type, where a bare string
typechecks and then throws inside a `FlatList`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 19:32:43 +02:00
Maxim b2b6bfbc3e refactor(react-native)!: converge render-tool hooks onto react-core
React Native's `useRenderTool` was not react-core's `useRenderTool`. Its entire
body forwarded to a different hook, `useFrontendTool`, while wearing the other
one's name. core has two: `useFrontendTool` registers a tool AND its renderer
via `addTool`; `useRenderTool` registers a renderer ONLY via
`addHookRenderToolCall`, and special-cases `"*"` into a schema-less fallback.

Because RN's alias took the `addTool` path, `name: "*"` registered a frontend
tool literally named `*` and advertised it to the model. Eight further symptoms
share that single cause: a render-only registration was advertised and shadowed
a same-named server tool; `addTool` evicted an existing same-named
`useFrontendTool` handler with only a `console.warn`; `description` and
`parameters` were required though both are tool fields; `followUp` and
`available` were accepted upstream but not forwarded; `handler` dropped core's
second (context) argument, leaving `stopAgent()`'s abort signal unreachable;
`agentId` changes never re-registered; two structurally incompatible
`RenderTool*Props` families shipped side by side; and `hooks/index.ts` was a
dead barrel with no build entry, no exports mapping and no importer.

Deletes RN's hook and re-exports core's two instead, so each capability has
exactly one implementation and RN carries no render-tool API of its own.

Deleting rather than re-pointing the name is deliberate. Re-pointing is the
dangerous shape: a `{ name, parameters, render }` call with no handler would
keep compiling and silently stop registering the tool.

Two of the nine are documented rather than fixed, so this is not a clean sweep:
`agentId` is still absent from both core hooks' re-registration check, so the
`deps` workaround stands; and `useDefaultRenderTool` keeps a narrower render
return than the hooks converged here.

Adds the guard the entry-surface suite was missing. It asserted only that
`useRenderTool` was *present* on the headless entry, never which hook it was, so
it would have stayed green through this entire change while verifying nothing
about it — proven by mutation: an RN-local hook re-grown under the name fails the
new identity assertion while 18 other guards in that file stay green.

Also rewrites the RN render-tool suite around what RN still owns. The previous
file tested forwarding into `useFrontendTool` through a double that modelled so
little its own header comment recorded that deleting the `deps`, `handler` and
`agentId` forwarding left it fully green. Assertions now read core's observable
state: `getTool`, `core.tools`, a real `runTool` rejection, painted DOM through
the real `useRenderToolCall`, and the tool list core hands an agent on a real
run.

BREAKING CHANGE: removes `useRenderTool`, `RenderToolProps`,
`RenderToolFunction` and `UseRenderToolOptions` from @copilotkit/react-native.
A tool-plus-renderer registration becomes `useFrontendTool` with an otherwise
identical object; renderer-only registration and the `"*"` wildcard become
core's `useRenderTool`; render props rename `args` to `parameters`. The
migration table, and the analysis of which call shapes fail loudly versus
silently, live in
showcase/shell-docs/src/content/reference/react-native/hooks/useRenderTool.mdx —
this repo's release-note collector reads only commit subjects, so a footer is
not a consumer-facing channel (see #6479) and the docs page is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 19:32:42 +02:00
tylerslaton 69a940c70e chore: release monorepo v1.70.3 2026-09-08 23:22:32 +00:00
MikeRyanDev 16514e9424 chore: release monorepo v1.70.2 2026-09-08 20:03:39 +00:00
tylerslaton 71b2f481f9 chore: release monorepo v1.70.1 2026-09-03 15:49:49 +00:00
maxkorp 3a64564508 chore: release monorepo v1.70.0 2026-08-31 19:34:27 +00:00
Tyler Slaton d477ca7396 chore: merge main into AG-UI dependency bump 2026-08-31 09:26:33 -07:00
Ben Taylor 4ccae6fe20 fix(react-native): keep the polyfill imports in the built barrel (closes OSS-1002) (#6744)
## The bug

`src/polyfills.ts` is five side-effect-only imports plus
`installStreamingFetch()`. The published barrel was 195 bytes:

```js
// node_modules/@copilotkit/react-native/dist/polyfills.mjs — 1.69.2
import { t as installStreamingFetch } from "./streaming-fetch-BnQh3vBz.mjs";
installStreamingFetch();
export {  };
```

Every React Native app following the documented setup died on its first
runtime call:

```
E ReactNativeJS: '[CopilotKit] Error (runtime_info_fetch_failed):',
  [ReferenceError: Property 'ReadableStream' doesn't exist]
```

## Root cause

The `sideEffects` field, but not in the way it first looks. It is
correct for *consumers* and wrong for *this package's own build*:

```json
"sideEffects": ["./dist/index.*", "./dist/headless.*", "./dist/polyfills.*", "./dist/polyfills/**/*"]
```

tsdown/rolldown reads the package's own `sideEffects` while bundling and
matches it against **source** paths. `src/polyfills/streams.ts` matches
none of those `dist` globs, so it is declared side-effect-free — a hard
assertion that lets rolldown drop the import without analysing the
`globalThis` assignments inside.

Reproduced in isolation at the pinned tsdown (0.20.3):

| `sideEffects` | built barrel |
|---|---|
| `["./dist/polyfills.*", "./dist/polyfills/**/*"]` | `export { };` —
empty |
| same + `["./src/polyfills.*", "./src/polyfills/**/*"]` | `import
"./polyfills/streams.mjs";` |
| field absent | `import "./polyfills/streams.mjs";` |

**Wider than the ticket recorded:** `dist/index.mjs` and
`dist/headless.mjs` also had zero polyfill code, so the package's
advertised auto-install on first import did not happen either. Not
RN-specific in principle — but I surveyed every package at `origin/main`
and this is the only one exposed. The other `sideEffects` arrays
(`react-core`, `react-ui`, `react-textarea`) are `["**/*.css"]`, which
matches source and works.

## The fix

Add matching `./src/**` globs. Barrel goes 195B → 362B with all five
imports; `headless.mjs` now leads with `import "./polyfills.mjs"`.

## The test, and why the existing one didn't catch this

`src/__tests__/polyfills.test.ts` has ~20 assertions covering all five
groups and was green the whole time — it imports `"../polyfills"`, the
TypeScript **source**, which vitest transpiles without bundling and
therefore without tree-shaking. It exercises a graph the published
package does not contain.

So the new check runs against `dist/`. Two things it has to get right to
be honest:

- **Node ships these globals natively.** Asserting `ReadableStream` is
"defined" after import passes on an empty barrel. The probe clears all
nine first, emulating Hermes.
- **The two formats need different treatment.** CJS is executed for real
in a child realm. ESM is checked structurally — it cannot be executed
here because `encoding.mjs` takes a named import from CommonJS
`text-encoding`, which Metro rewrites to a `require()` but bare Node ESM
rejects.

It is wired into `build`, so a dead barrel fails the build rather than
reaching npm — which matters, because this shipped through a fully green
suite.

## Docs

Added the `Property 'ReadableStream' doesn't exist` symptom to
troubleshooting, which previously covered only the inverse case (a
polyfill *conflict*).

I deliberately left the reference docs' "auto-installs on first import"
claims and the crypto import-order callout alone: both become **true**
once the build is fixed, and I verified the auto-install behaviourally.

## Verification

- **Red/green proven, not assumed:** reverted the `sideEffects` change,
rebuilt → 5/5 groups FAIL in both formats. Restored → 5/5 PASS. There is
also a test for a *single* group regressing, which a whole-barrel
assertion would wave through.
- **Packed tarball** (`pnpm pack`) verified behaviourally: all nine
globals install.
- 289 vitest + 26 script tests pass; `check-types` clean; `attw` green;
`publint` clean apart from a pre-existing `repository.url` suggestion;
oxfmt/oxlint clean.
- Added `{projectRoot}/scripts/**` to the package's `test` inputs and
confirmed cache invalidation (19/19 cached → 18/19 after touching the
verifier); without it, editing the verifier alone would restore a cached
pass.

**Not verified:** the on-device round trip — no emulator in this
environment. The bare-realm equivalent passes on the packed tarball.

## Follow-up worth its own ticket

`dist/polyfills/encoding.mjs` uses a named import from CommonJS
`text-encoding`. Metro handles it; a true-ESM consumer would not.
Pre-existing and not RN-facing, so left out of this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-28 12:24:39 -05:00
Markus Ecker 71d9731d45 chore(deps): bump @ag-ui/* to 0.0.59
Moves the published packages from 0.0.57 to the current AG-UI release across
@ag-ui/client, core, encoder and proto — 27 declarations in 18 packages.

0.0.59 is the first release carrying the subagent protocol surface
(SUBAGENT_STARTED/FINISHED/ERROR, subagentRunId) along with the null-omission
cleanup, so this is the dependency CopilotKit's subagent work needs.

Scope is packages/** plus the release script noted below. The examples and
showcases sit on a spread of older pins (0.0.40 through 0.0.58) and are left
alone.

One behavioural change comes with the bump. channels-core ships
sanitizeAgentEventStream because @ag-ui/client used to reject a TOOL_CALL_START
carrying parentMessageId: null — the shape @ag-ui/langgraph emits for an
interrupt-triggering tool call. 0.0.59 accepts that null and treats it as
absent, so the two tests asserting the run dies WITHOUT the sanitizer no longer
hold. They now assert the run survives, and the one at agent level still checks
the tool call actually arrives so it cannot pass vacuously. The sanitizer is
untouched and its coercion tests are unchanged; it is simply no longer the
thing keeping such a run alive.

The bump also broke the packed Angular consumer matrix. That job generates a
smoke app from scripts/release/lib/angular-package.ts, whose manifest restated
"@ag-ui/client": "0.0.57" as a literal while packages/angular moved to 0.0.59.
pnpm then installed both copies and the app failed to compile:

  TS2322: Type 'SmokeAgent' is not assignable to type 'AbstractAgent'.
    Types have separate declarations of a private property '_debug'.

The smoke app imports AbstractAgent directly, so it has to resolve the identical
copy the library ships against. Read that version off the packed manifest --
which verify-angular-package.ts already parses for the Angular support contract
-- instead of restating it, so no future AG-UI bump can desynchronise it.
2026-08-28 16:18:52 +02:00
Benjamin Taylor 84dea7bfbb fix(react-native): keep the polyfill imports in the built barrel (closes OSS-1002)
`src/polyfills.ts` is five side-effect-only imports. The `sideEffects` globs only matched
`./dist/**`, and rolldown matches that field against SOURCE paths while bundling, so every
`src/polyfills/*.ts` was declared pure and dropped. Every published version through 1.69.2
shipped a 195-byte barrel installing nothing but streaming fetch, so an app following the
documented setup died on its first runtime call with `Property 'ReadableStream' doesn't exist`.

`dist/index.mjs` and `dist/headless.mjs` lost the same imports, so the package's advertised
auto-install on first import did not happen either.

Add matching `./src/**` globs. The barrel goes 195B to 362B with all five imports, and
`headless.mjs` now leads with `import "./polyfills.mjs"`.

`src/__tests__/polyfills.test.ts` stayed green throughout this, because it imports the source,
which is never bundled and so is never tree-shaken. Add `scripts/verify-polyfill-barrel.mjs`,
which checks `dist/` instead. It clears the nine globals first (Node ships them natively and
Hermes does not, so asserting they are merely "defined" would pass on an empty barrel), then
executes the CJS barrel in a child realm and checks the ESM barrel structurally. ESM cannot be
executed here: the encoding polyfill takes a named import from CommonJS `text-encoding`, which
Metro rewrites to a require() but bare Node ESM rejects.

The check runs from `build`, so a dead barrel fails the build rather than reaching npm.

Also document the `ReadableStream doesn't exist` symptom in troubleshooting, where only the
inverse case (a polyfill *conflict*) was covered before.

Verified: reverting the sideEffects change and rebuilding turns the check red in both formats,
5 of 5 groups; restoring it turns it green. A behavioural check on the packed tarball installs
all nine globals. 289 vitest + 26 script tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:22:02 -05:00
MikeRyanDev 8617f5b76b chore: release monorepo v1.69.3 2026-08-27 16:47:48 +00:00
tylerslaton 9629e930d1 chore: release monorepo v1.69.2 2026-08-26 00:18:42 +00:00
MikeRyanDev 6053e4e262 chore: release monorepo v1.69.1 2026-08-25 18:50:37 +00:00
Ben Taylor 105ac3cfb9 fix(core): keep exactly one tool result per tool call across message snapshots (#6294)
# fix(core): keep exactly one tool result per tool call across message
snapshots

## Summary

When an agent emits a MESSAGES_SNAPSHOT, AG-UI merges it by message id
and can drop a tool message that TOOL_CALL_RESULT created. The next turn
then sends an assistant tool call without its paired result, which
providers reject.

This PR records observed tool results for the current input and repairs
history through AG-UI's returned-messages mutation channel. It keeps one
tool message per toolCallId and composes with current main's first-seen
message provenance.

## Root cause

@ag-ui/client applies events against its own cloned messages array.
TOOL_CALL_RESULT creates a tool message and inserts it after its
assistant owner. A later MESSAGES_SNAPSHOT is a replace-by-id merge, so
a missing tool entry can remove that result. packages/core had no record
of the result event, so there was nothing to restore it from.

## What changed

- StateManager records one ToolCallResultEvent per toolCallId on the
current RunAgentInput. Events keep flowing through AG-UI's normal path.
- At RUN_FINISHED, RUN_ERROR, and onRunFailed, reconciliation returns a
fresh message array only when a repair is needed. AG-UI applies it
through its normal mutation chain.
- Pending results are released at each finished server-run boundary and
at finalization, so a later server run under one input cannot resurrect
a result removed by its snapshot.
- The "Forwarded to client" sentinel classifier now lives in one
internal module used by StateManager and run-handler.ts.

## The reconciliation rule

toolCallId is the decisive identity:

- No assistant owner for the call: do nothing.
- A real tool message already exists for the call under any message id:
keep one result and remove duplicate same-call representations.
- Only placeholders exist: promote one to the canonical result and drop
the rest.
- Nothing exists: insert the result after its assistant owner and its
contiguous tool messages.

## On the LangGraph duplicate

LangGraph can represent one result with different streamed and
checkpoint message ids. The regression fixture keeps the streamed result
before the snapshot and places both representations in the snapshot. The
final history and next-turn input contain one result for that
toolCallId.

Two tool messages for one toolCallId are one malformed history class.
Keying reconciliation by toolCallId makes that duplicate unrepresentable
while preserving normal event delivery.

## What this does not do

- No direct agent.messages mutation, setMessages from a subscriber, or
stopPropagation. AG-UI remains the owner of message application,
ordering, and publication.
- Reconciliation does not infer or rewrite run identity. Current main's
event-derived run identity and first-seen snapshot provenance remain
intact.
- No message-to-run association is performed inside reconciliation.
- No public API change, export, version bump, or changeset.

## Relationship to #3884

Related to #3884, but not marked as closing it. The event sequence in
that issue has no MESSAGES_SNAPSHOT, and it already produces a correct
turn-2 history on current main. Snapshot-dropped results are a real bug
worth fixing independently, while the reporter's case still needs a raw
event trace.

## Test plan

All cases drive CopilotKitCore.runAgent() against real AbstractAgent
subclasses.

- Two-turn reproduction: a snapshot omits the result, and the next turn
receives exactly one result.
- LangGraph shape: differing message ids under one toolCallId produce
one surviving tool message in real event-before-snapshot order.
- Repeated server runs under one input do not resurrect a result removed
by a later snapshot.
- Terminal mutation, normal result propagation, duplicate results,
placeholders, ownerless results, ordering, RUN_ERROR, local failure, and
run ownership remain covered.
- The focused core tests pass 32/32. Core typecheck, build, formatting,
lint, and diff checks pass. CI checkboxes remain for GitHub.
2026-08-24 11:19:31 -05:00
MikeRyanDev 71977ddfce chore: release monorepo v1.69.0 2026-08-21 18:09:45 +00:00
Rod Boev 1cde1d8f04 fix(core): align tool result history with current main 2026-08-20 20:44:16 -04:00
BenTaylorDev aa3fb29dce chore: release monorepo v1.68.3 2026-08-20 10:27:07 -07:00
contextablemark b0233c4eb0 chore: release monorepo v1.68.2 2026-08-20 02:19:06 +00:00
tylerslaton 1f9b60b231 chore: release monorepo v1.68.1 2026-08-14 21:05:45 +00:00
tylerslaton e6864b6bdd chore: release monorepo v1.68.0 2026-08-14 20:11:31 +00: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
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
Maxim 4b25cf34b8 test(react-native): stop the entry guard racing its own module load
The #4893 entry-surface guard timed out nondeterministically at full test
parallelism ("Test timed out in 5000ms" on `await import("../headless")`),
which four independent agents each worked around with --testTimeout or
--maxWorkers=2. A flaky hard gate is a gate people learn to ignore.

Measured, not guessed. The import is a one-time module-graph load whose
VARIANCE — not its mean — broke the default budget: ~0.7-1.1s for this file
alone and ~0.9-1.8s inside the full 22-file suite (n=8 each), but 4568ms on
the run straight after a cold `nx build`, i.e. 91% of the 5000ms budget spent
on an otherwise idle machine. The cost is resolve/transform plus cold-page-
cache I/O over the ~283 KB of workspace dist that vitest.config.mjs inlines
via `server.deps.inline: [/@copilotkit/]` (core ~218 KB, react-core
v2/headless ~55 KB, v2/context, shared); bare-Node `import()` of the
equivalent prebuilt dist is 461ms, so evaluation is not the expensive part.

Four separate tests each awaited that same import, so all four raced one
cost against one budget — and when the first lost the race the other three
inherited its in-flight import and timed out with it, which is why the
observed signature was three simultaneous failures rather than one. They now
share a single explicitly-budgeted `beforeAll`, so the cost lives in exactly
one place and each test reports ~0ms.

The hook is nested rather than top-level on purpose: its failure domain must
cover only the tests that need the module, or an import failure would take
down the fs-only graph tests too — the same blast-radius problem as blind
spot #4, just relocated into a hook.

No assertion is weakened: comment stripping, import()/require() extraction,
the exact resolved-graph pin and the revived existence test are untouched,
and the guard still fails on a real violation (injecting a lazy
`import("@copilotkit/react-core/v2")` into src/streaming-fetch.ts trips 3
assertions; reverted).

Verification: 5 consecutive full-suite runs at DEFAULT parallelism, no
--maxWorkers or --testTimeout override, 271/271 passing in 3.97-5.87s wall
each; plus 3 concurrent full suites (30 workers on 10 cores) all green, and
one pass at load average 235. `check-types` clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:20:50 +02:00
Maxim 60d3ef1071 fix(react-native): externalize react-dom in the headless size measurement
The esbuild `external` list omitted `react-dom`, unlike react-core's
measure-copilotchat.mjs, so a stray web-oriented edge could be absorbed
into the figure the PR's bundle claim rests on.

Checked empirically before changing anything: `react-dom` is NOT reachable
from @copilotkit/react-native/headless today. A metafile run shows 0 of the
653 input modules are react-dom, and no module references it even
pre-resolution. The reported figure is therefore UNCHANGED — 94941 B gzip
(92.7 kB) before and after, byte for byte. The headline "92.8 kB -> 92.7 kB,
flat" claim is unaffected and stays comparable with previously reported
numbers.

The guard is still worth having. Simulating a stray edge measures the
inflation it prevents: +56.3 kB gzip via react-dom/client, +57.3 kB via
react-dom/server (not the ~130 kB estimated in review — that is closer to
the raw magnitude; react-dom-client.production.js is 536 kB raw). The
subtler case is a bare `react-dom` edge at +1.4 kB, small enough to read as
noise while still being a real regression.

No subpath entries: esbuild prefix-matches package paths, so `react-dom`
already covers react-dom/client and react-dom/server (verified on the
pinned 0.27.3; esbuild CHANGELOG 0.5.14 and 0.14.13). Listing them would
imply they were required.

Hoisted the list to an exported HEADLESS_EXTERNAL with per-entry rationale,
mirroring the sibling's DEFAULT_EXTERNAL, and made `external` an overridable
option so the new test can A/B it rather than assert on a literal. Both new
tests fail if react-dom is removed from the list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:16:32 +02:00
Maxim 4315adb1e7 test(react-native): make the tool-result assertions read the result
Two tests claimed to prove a tool RESULT reaches its renderer and neither
read it. "reports complete and passes the result through" rendered through a
registrar printing only status and args, so replacing the correlated tool
message's content with a constant left it green; its in-place-mutation twin
had the same hole. The integration test's only result assertion built its
tool message as `{ content: "ok" }` behind an `as never`, so it carried no
toolCallId — the id production correlates a result to a call by.

Both now render status, args AND result together, and a new negative case
gives a tool call a result belonging to a DIFFERENT call. That last one is
the only detector for a lookup that ignores the map key: every fixture in
the file matched on id, so an unkeyed "hand out any tool result we have"
lookup passed the whole suite unchanged.

Fixtures move to src/__mocks__/tool-fixtures.ts. toolMessage() takes
toolCallId as a required positional argument, so no fixture can omit the
correlation, and assistantToolCall() returns a typed AssistantMessage —
which retires the `as never`, an `as unknown as Message`, and three `any`s
in the touched files. A properly-typed ToolMessage typechecks at that call
site unchanged; the cast was convenience, not a type-system limit.

Test-only: CopilotChat.tsx is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:10:00 +02:00
Maxim d144757d8a test(react-native): guard the entry surface's export kinds against type-only stripping
`export type { X }` strips X's runtime binding. Five runtime values shipped from
`src/headless.ts` inside `export type` blocks — the `ToolCallStatus`,
`UseAgentUpdate`, `CopilotKitCoreErrorCode` and
`CopilotKitCoreRuntimeConnectionStatus` enums, and the `AbstractAgent` class —
while the reference docs told consumers to import and branch on them. Nothing in
the repo could see it: the package built, typechecked, linted and passed its
suite, because nothing here consumed its own entry the way a consumer does.

react-core's `headless-type-exports.test-d.ts` cannot cover this. Export kind is
a property of the re-exporting module, and that guard reads react-core's entry —
react-core's own `UseAgentUpdate` was already a correct value export while RN's
was wrong. The guard has to live on the RN side and read RN's own entries.

Adds `src/__tests__/headless-value-exports.test.ts`, in three layers:

- §1 asserts each of the five is a present runtime binding of the expected
  `typeof`, with its enum members nameable, on BOTH `@copilotkit/react-native`
  and `@copilotkit/react-native/headless`. A stripped export is an absent module
  binding, so a runtime test is the direct instrument and cannot be faked by a
  cast or an expect-error.
- §2 needs no symbol list: it parses both entry sources, and for every symbol
  re-exported type-only it imports the module that symbol came from and fails if
  that module has a runtime binding for it. A future contributor who adds a new
  enum re-export inside an `export type { … }` block is caught without anyone
  updating §1, and the failure names the symbol, the source module and the fix.
  A floor on the parsed specifier count keeps a rotted parser from passing
  vacuously.
- §3 type-checks the consumer-visible symptom (enum-member comparison on a
  render-prop `status`, `extends AbstractAgent`, `instanceof`). The file lives
  under `src/`, so `check-types` compiles it and a regression also fails there
  with TS1362 naming the symbol. Its bodies are lazy on purpose: a module-scope
  `extends` would crash collection and hide §1/§2's guided messages.

Proven by transiently restoring the defect three ways — the `AbstractAgent`
class, `ToolCallStatus` moved into an `export type` block, and `UseAgentUpdate`
via the inline `type ` prefix. Each produced 4 failing tests plus TS1362;
`src/headless.ts` is byte-identical to before.

RN suite 23 files / 276 tests (baseline 22 / 261, so +15 and no change
elsewhere); `nx run @copilotkit/react-native:check-types` clean; oxfmt and
oxlint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:08:19 +02:00
Maxim 63a1c94fbb fix(react-native): make the headless size measurement fail loudly, not print 0.0 kB
measure-headless.mjs prints the number the PR's bundle claim rests on, and it
had three ways to report a broken run as a good one. All three reproduced:

1. No zero-output guard (the react-core sibling has one). A run whose bundle
   collapses to nothing measures ~20-35 B of gzip envelope, prints "0.0 kB"
   and exits 0 — reported into the CI job summary as a spectacular win. Note a
   zero-ONLY guard would not have caught the reproduction (35 B, not 0), so
   this adds a plausibility FLOOR of 8 kB alongside the zero check: ~11x below
   the real 92.7 kB, so legitimate size work can never trip it.

2. `logLevel: "silent"` discarded `result.warnings` and there was no
   try/catch, so esbuild resolution problems escaped as an unhandled rejection
   printing esbuild's internal frames and `errors: [Getter/Setter]` instead of
   the messages. Silent is kept (as in the sibling) so stdout stays the single
   figure line CI quotes; warnings are now formatted to stderr and errors are
   re-thrown with esbuild's own formatted diagnostics.

3. An unbuilt dist died on a raw "Could not resolve" stack. A preflight on
   dist/headless.mjs now names `npx nx run @copilotkit/react-native:build`,
   and the catch adds the same hint when the entry specifier is what failed.

The measurement itself is untouched — same esbuild options, same synthetic
entry, same six symbols, same external list — and still reports 92.7 kB, so
comparability across PRs is preserved. A moved figure would have meant the
measurement changed rather than its guards.

Also adds the test hook RN lacked, mirroring react-core exactly:
scripts/__tests__/measure-headless.test.mjs under `node --test`, wired as
`test:scripts` and chained into `test`. Coverage targets the failure modes,
not the happy path. Both packages' vitest `include` globs are scoped to
`src/**`, so the .mjs test cannot collide with the jsdom setup — the reason
the sibling runs under node --test in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 23:05:01 +02:00
Maxim 0ea71fc684 test(react-native): make the #4893 entry guard see what it claimed to see
The headless-entry import-graph guard was weaker than the PR claimed. Four
blind spots, each verified to let a real violation pass (or to flag a
non-violation), each now covered by a test:

1. Only `import … from "x"` was matched, so a lazy optional-peer
   `require("@copilotkit/react-core/v2")` or `await import(…)` — which Metro
   follows and bundles identically — defeated the guard entirely. Static,
   bare side-effect, dynamic `import()` and `require()`/`require.resolve()`
   are all extracted now, and a loader whose argument is not a string
   literal is reported as unanalyzable rather than silently skipped.

2. Matching ran on raw text, so doc comments counted as imports. Not
   hypothetical: the guard was harvesting EIGHT specifiers
   (`@copilotkit/react-native`, `…/headless`, `…/polyfills` and its five
   subpaths) that no source file imports — half the reported bare-specifier
   set — purely from JSDoc examples. In the other direction, writing a
   "don't do this: import from @copilotkit/react-core/v2" counter-example
   in a doc comment failed the build. Comments are stripped first now, via
   a single left-to-right pass that matches string/template literals with
   the same alternation so a `//` inside a string stays a string.

3. `resolveLocal` returned null for an edge it could not resolve and the
   caller dropped it, so an unresolvable specifier read as "clean" while
   hiding the whole subgraph behind it. Proven: a real
   `export … from "@copilotkit/react-core/v2"` reached through an ESM-style
   `"./probe-heavy.js"` edge passed the old guard. Emitted-extension
   specifiers now resolve, and anything still unresolvable FAILS LOUDLY
   instead of vanishing. The resolved file set and bare-specifier set are
   also asserted EXACTLY, so a new edge has to be looked at deliberately
   rather than only being caught if someone thought to deny-list it.

4. The graph was walked in the `describe` body, so a missing entry file
   threw at collection time and every test in the file — including the one
   asserting the entry exists — never ran (`Tests  no tests`). The walk is
   lazy and memoized per entry now, and the existence assertion reports.

Every fix was proven by mutation in both directions: the violation passes
the old guard, fails the new one, and clean source still passes. Also drops
`localFiles`, which no test ever read.

Scope note: the ~5s `await import("../headless")` timeout flake in this
file is deliberately untouched — it is owned separately. Runs used
`--testTimeout=60000`.

RN suite 267 passed / 22 files (was 261; +6 new tests);
`nx run @copilotkit/react-native:check-types` clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:58:20 +02:00
Maxim 025b8d5979 test(react-native): make the useRenderTool suite detect its own forwardings
`useRenderTool` is a thin forwarder onto react-core's `useFrontendTool`, and
its suite mocked exactly that hook. The double only modelled `name` and
`render`, so deleting the `deps`, `handler` AND `agentId` forwarding from the
hook each left the suite fully green — it could not detect a regression in any
of the three things the hook exists to forward.

Drop the four `vi.mock` blocks and drive a real `CopilotKitCoreReact` through
the shared `TestCopilotKit` harness, the way the sibling
`render-tool-call.integration.test.tsx` already does, then assert on core's own
observable behaviour instead of a mock's call arguments:

- handler — `core.runTool()`, i.e. core's real `executeToolHandler` path, so the
  handler is proven to RUN and its return value proven to become the tool result
- agentId — the tool resolves for its agent and must NOT resolve as a global
  tool, and the renderer entry carries the agentId that keys it
- deps — a render closure over a serialisable dep re-registers and the PAINTED
  text changes, observed through react-core's real `useRenderToolCall`

Each mutation now fails exactly one test. Also pins the documented sharp edge
that `useFrontendTool` compares deps with `JSON.stringify`, so a function dep
collapses to a constant and can never re-register — a test asserting otherwise
would assert a behaviour the code cannot deliver, and pinning it makes a change
of comparator fail loudly.

Test-only: `useRenderTool.ts` is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:57:09 +02:00
Maxim 63e7fa7fda fix(react-native): make the chat list's extraData contract true and explicit
CopilotChat's `extraData` memo carried a comment claiming it held "the exact
inputs renderItem reads", but it listed only { isRunning, renderToolCall,
toolMessages } while renderItem also read `listItems` — it answered "am I the
last row?" by index-reading the array's tail, and depended on `listItems` in
its own useCallback deps. The comment was false and the stated
row-memoisation contract was incomplete.

The defect is documentation and fragility, NOT observable behaviour. Verified
against the real react-native 0.85.2 sources in the pnpm store:

- FlatList is a PureComponent (Libraries/Lists/FlatList.js:307), and `data`
  is one of the props it shallow-compares. `data={listItems}` is the same
  reference, so any rebuild of `listItems` re-renders FlatList on its own.
- In the default non-strictMode path FlatList's render() uses `this._renderer`
  rather than `this._memoizedRenderer` (FlatList.js:682), allocating a fresh
  `renderProp` on every render, which is handed to every cell.
- VirtualizedList._pushCells passes that `renderItem` plus `item` to each
  CellRenderer, which is itself a PureComponent
  (VirtualizedListCellRenderer.js:63). `extraData` is NOT a cell prop.
- The `listItems` memo allocates fresh item objects on every rebuild, so each
  cell's `item` prop also differs. Cells therefore invalidate through
  `data`/`item` even under the narrowest path (strictMode with a memoizeOne
  hit on renderItem and extraData).

So no stale last-row / stranded-loading-indicator state is reachable, and no
covering test is added: the behaviour is unchanged, and the package's test
FlatList is a mock that re-invokes renderItem for every row on every parent
render, so it cannot express cell memoisation in the first place.

Instead, make the contract honest. The tail id becomes a named `lastItemId`
memo; renderItem reads that scalar and deps on it rather than closing over
`listItems` and indexing it; `extraData` now lists exactly the four values
renderItem closes over, and the comment states why `listItems` is absent
(it is the `data` prop, which already invalidates cells). As a side benefit
renderItem's identity is now stable across `listItems` rebuilds that do not
move the tail.

Call-Site Enumeration (Procedure 2 step 8):
- `extraData` — grep over packages/react-native/src shows exactly two sites,
  the memo itself and the `extraData={extraData}` prop on the list. No test
  and no other module reads its keys. A caller-supplied `FlatListComponent`
  (the documented BottomSheetFlatList case) receives it, but RN treats
  extraData as an opaque re-render marker and never inspects its shape, so
  adding `lastItemId` is not observable to any consumer.
- `renderItem` / `lastItemId` — local to CopilotChat; neither is exported.
- `isLoading` on AssistantMessage — value-identical by construction, since
  `lastItemId` IS `listItems[listItems.length - 1]?.id`.
- No change to CopilotChatProps or to any entry-point export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:31:47 +02:00
Maxim 7baed27370 fix(react-native): export the headless entry's runtime values as values
`src/headless.ts` re-exported five runtime values inside `export type { … }`
blocks. A type-only re-export strips the runtime binding, so the symbol is
unimportable and — for the enums — the field it types cannot be compared
against at all, because an enum-typed field rejects a bare string literal.
`RenderToolProps["status"]` is `ToolCallStatus`, so a consumer of this PR's own
render-prop contract had no working way to branch on `status`.

Determined empirically, not by reading: a throwaway probe imported all 32
re-exported symbols as values under the package's real tsconfig. The 27 that
are genuine types reported TS2693 ("only refers to a type"); five did not, and
those five are the ones moved. Declaration sites confirm each:

  ToolCallStatus                        packages/core/src/types.ts:14      export enum
  CopilotKitCoreErrorCode               packages/core/src/core/core.ts:99  export enum
  CopilotKitCoreRuntimeConnectionStatus packages/core/src/core/core.ts:320 export enum
  UseAgentUpdate                        packages/react-core/src/v2/hooks/use-agent.tsx:13
                                                                           export enum
  AbstractAgent                         @ag-ui/client                      declare abstract class

Nothing else changed kind: Suggestion, FrontendTool, Message, ToolCall,
ToolMessage, AgentCapabilities, ResumeStatus, Interrupt, ResumeEntry, the
Interrupt*/RenderTool*/Thread*/CopilotChat* prop and config types,
ReactFrontendTool, ReactHumanInTheLoop, ReactToolCallRenderer and
CopilotKitContextValue are all genuine types and stay `export type`.

`src/index.ts` does `export * from "./headless"`, which republishes values and
types alike, so both published entry points are fixed. Verified in the built
output: all five appear without a `type` prefix in dist/headless.d.mts and
dist/index.d.mts, and as runtime bindings in headless.mjs, index.mjs and
index.cjs.

Negative control: with the pre-fix headless.ts the same probe produced ten
TS1362 errors ("cannot be used as a value because it was exported using
'export type'") across both entries; with the fix, zero.

This makes the already-merged docs on this branch true. The RN reference pages
write `import { ToolCallStatus } from "@copilotkit/react-native"` and
`status === ToolCallStatus.Executing` (useRenderTool.mdx:170, useFrontendTool.mdx:129,
useHumanInTheLoop.mdx:86) and `import { useAgent, UseAgentUpdate }` with
`updates: [UseAgentUpdate.OnMessagesChanged]` (useAgent.mdx:234). None of those
imports resolved before this commit.

AbstractAgent is a deliberate inclusion, not scope creep: it is a runtime class
and the AG-UI extension point consumers subclass, and @ag-ui/client is a
dependency of this package rather than a peer, so a consumer cannot reliably
import it from there directly. It carries no bundle cost — the headless entry
already imports @ag-ui/client transitively through
@copilotkit/react-core/v2/headless, and esbuild tree-shakes an unused
re-export, so scripts/measure-headless.mjs still reports 92.7 kB gzip.

Forced test change, in scope only because the fix causes it: the value
re-export makes headless.ts the first runtime importer of @copilotkit/core in
this package's graph, so `import "../index"` now evaluates real core, which
named-imports RUNTIME_MODE_SSE and friends from @copilotkit/shared.
headless-integration.test.tsx replaced that module wholesale with a two-key
factory, so the import threw. Fixed by spreading importOriginal() instead of
replacing — the form vitest's own error message prescribes — leaving
createLicenseContextValue the only stubbed member. No assertion, case or
coverage changed.

Call-Site Enumeration (Procedure 2 step 8) — `grep -rn` per symbol across
packages/ plus every importer of @copilotkit/react-native in the repo. Every
site holds, because type -> value is a widening: an `import type` of a value
export is still legal.

  ToolCallStatus
    packages/react-native/src/headless.ts:94 — the changed export. Holds.
    No other site in packages/ or examples/ names it. Nothing imported it
    before, which is the bug.
  CopilotKitCoreRuntimeConnectionStatus
    packages/react-native/src/headless.ts:95 — the changed export. Holds.
    No other site.
  CopilotKitCoreErrorCode
    packages/react-native/src/headless.ts:96 — the changed export. Holds.
    CopilotKitProvider.tsx:12,37 / CopilotChat.tsx:13,93 / CopilotPopup.tsx:26,223
    — all `import type … from "@copilotkit/core"`, used only in a `code:` field
    position. They import from core directly, not through this entry, and a
    type position is unaffected by the re-export kind. Hold.
  UseAgentUpdate
    packages/react-native/src/headless.ts:65 — the changed export. Holds.
    packages/react-core/src/v2/headless.ts:41 — already a value export there,
    with a comment giving this exact reason; this commit makes RN agree with it
    rather than diverge. Holds.
  AbstractAgent
    packages/react-native/src/headless.ts:107 — the changed export. Holds.
    packages/react-native/src/__mocks__/test-copilotkit.tsx:22,41,42 — already
    imports the class as a VALUE from @ag-ui/client and subclasses it, i.e. it
    had to bypass this entry to do what the entry now permits. Unchanged and
    still passing. Holds.
    packages/react-core/src/v2/**, packages/channels-telegram/** — all import
    from @ag-ui/client directly; none route through @copilotkit/react-native.
    Hold.
  Importers of @copilotkit/react-native outside the package
    examples/v2/react-native/demo/{App.tsx,src/ChatScreen.tsx,index.js} — import
    CopilotKitProvider, useAgent, useCopilotKit, useFrontendTool and the
    polyfills entry. None of the five symbols appears anywhere in the demo, so
    nothing to break; the demo is now able to import them. Holds.
  Surface guards
    src/__tests__/headless-entry-surface.test.ts — its `not.toHaveProperty`
    denylist covers the chat/attachment exports and the two removed registry
    symbols; none of the five is listed, and its bare-specifier bans
    (@gorhom/bottom-sheet, expo-*, shiki/mermaid/katex/a2ui-renderer, non-headless
    react-core entries) are unaffected by adding @copilotkit/core and
    @ag-ui/client edges. Passes unchanged.

Verification: `pnpm nx run @copilotkit/react-native:check-types` succeeds
(tsc --noEmit, 0 errors). `npx vitest run --reporter=dot` — 22 files, 253
tests, all passing. `npx oxfmt --check` clean; `npx oxlint` 0 errors and 2
warnings, both pre-existing in the touched test file (no-shadow on a mocked
`React`, no-this-in-sfc).

Note on a pre-existing flake: headless-entry-surface.test.ts hits the 5000ms
default testTimeout on `await import("../headless")` when the machine is loaded.
Measured 6 serial runs each way on the same box — pre-fix headless.ts failed
4 of 6, post-fix 3 of 6, identical timeout signature — so it predates this
change and is load-induced, not caused by it. Every run above used
`--testTimeout=60000`; raising that default (or making those assertions
static) is worth a follow-up, and is not this commit's to make.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:31:46 +02:00
tylerslaton 10d8f43829 chore: release monorepo v1.67.1 2026-08-10 20:28:46 +00:00
Maxim 5c09f51967 fix(react-native): stop a tool result RN cannot type from reading as empty
`toolMessages` rebuilt every tool message with
`content: typeof m.content === "string" ? m.content : ""`, so any non-string
content became `""` — indistinguishable from a tool that genuinely returned
nothing, with nothing logged. Renderers receive `result: string` and cannot tell
the two apart.

Static typing says the branch is unreachable: `ToolMessageSchema.content` is
`z.string()`, the SSE transport zod-parses every TOOL_CALL_RESULT before it
reaches `agent.messages`, and core stringifies non-string handler results itself
(`JSON.stringify(result)`, run-handler.ts:831/1014) before inserting the tool
message. So this is a defensive branch, not a live data-loss path — but core
keeps the same hedge (`normalizeToolResultContent` accepts `unknown` and unwraps
arrays of text parts), because unvalidated producers exist: restored thread
history, a non-SSE transport, and app code casting on `addMessage`. Rather than
delete the branch, make it loud and lossless: serialise non-string content the
way core already represents non-string results, and warn in dev (`__DEV__`
guard, matching src/CopilotChat.tsx and streaming-fetch.ts). null/undefined
still render as `""` — nothing to lose — but now warn instead of passing
silently. Never throws from the render path.

Call-Site Enumeration (semantics of ToolMessage.content in RN's map):
- Producer: the `toolMessages` memo, packages/react-native/src/components/CopilotChat.tsx.
  Module-local const; no other module imports it.
- In-file consumers: the `extraData` memo (map identity only, never reads
  content) and `renderItem`, which passes `toolMessages.get(tc.id)` to
  `renderToolCall`.
- react-core: `useRenderToolCall`
  (packages/react-core/src/v2/hooks/use-render-tool-call.tsx) forwards
  `toolMessage.content` as `result` (:53) and compares it in the memo
  comparator (:91-93). Both still receive a string.
- Downstream: app renderers registered through RN's `useRenderTool` ->
  `useFrontendTool`, typed by `ReactToolCallRenderer` whose Complete branch
  declares `result: string`. That contract is unchanged.
- Behaviour delta is confined to non-string content; the string path is
  byte-identical, and `""` stays `""` and stays silent.

Tests: 6 cases in CopilotChatToolCalls.test.tsx cover verbatim strings, an
empty result staying empty and silent, array/object serialisation with one
warning, null warning, and non-serialisable content not throwing.
RN suite 259/259, check-types clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:14:33 +02:00
Maxim 77ed31c437 fix(react-native): key the chat's message memos on content, not array identity
`CopilotChat` derived both `toolMessages` (toolCallId -> ToolMessage) and
`listItems` from `useMemo(..., [messages])`, where `messages` is `agent.messages`.
That dependency never changes on the path the memos exist to serve, so the
headline fix of this PR was inert:

- Core inserts tool results by MUTATING IN PLACE — `agent.messages.splice(insertAt,
  0, toolMessage)` (packages/core/src/core/run-handler.ts:931, :1080). Nothing in
  production reassigns `.messages`; grepping for that assignment in
  packages/core/src hits test files only.
- `AbstractAgent.addMessage` is a `this.messages.push(...)`, and AG-UI's apply
  pipeline reassigns the SAME array object for the whole run, so identity changes
  at most once per run and then never again.
- `useAgent` re-renders with a bare `forceUpdate()` on `onMessagesChanged`
  (packages/react-core/src/v2/hooks/use-agent.tsx:385-397); it does not hand down
  a new array either.

Net effect: both memos froze at whatever the first render of a run saw. Tool
renderers kept receiving `result: undefined` with a status that never reached
`complete`, and any assistant message or tool call appended mid-run never reached
the flat list at all (`listItems` only ever recomputed when `isRunning` flipped).

Both memos are now keyed on a lightweight content fingerprint — ids, roles,
content length, `toolCallId`, and tool-call ids plus argument lengths — mirroring
react-core's web `messagesMemoKey`
(packages/react-core/src/v2/components/chat/CopilotChat.tsx:983). Length rather
than value so large text and base64 attachment payloads are not re-serialized
every render, and the fingerprint is recomputed per render so typing in the
composer still does not rebuild the transcript.

Why 253 tests were green over this: every RN chat suite drives messages by
re-rendering `TestCopilotKit` with a NEW array, which DOES change identity, so
those tests pass regardless of the dependency. The two added tests mutate in
place through `agent.addMessage` instead — the same push core's paths bottom out
in — and fail against the pre-fix source:

  AssertionError: expected 'inProgress:Rooftop' to be 'complete:Rooftop'
  TestingLibraryElementError: Unable to find an element by: [data-testid="places"]

`TestCopilotKit` gains an optional `agentRef` prop to publish the stable agent so
a test can reach that path.

Call-Site Enumeration
- `TestCopilotKitProps` (new OPTIONAL `agentRef`; no existing site needs a change):
  - packages/react-native/src/components/__tests__/CopilotChatToolCalls.test.tsx (10 uses)
  - packages/react-native/src/hooks/__tests__/render-tool-call.integration.test.tsx (3 uses)
  - no other `<TestCopilotKit` in packages/, examples/ or showcase/
- `messagesFingerprint`: new module-private helper, 1 definition + 1 call, both in
  packages/react-native/src/components/CopilotChat.tsx. Not exported.
- No public RN export or `CopilotChatProps` field changed; `extraData` and
  `renderItem` keep their existing shapes and pick the corrected values up
  through their existing deps.

Verification: @copilotkit/react-native 255/255 tests in 22 files; `tsc --noEmit`
clean. Left untouched by design: the non-string tool-content coercion and
`extraData` omitting `listItems`, both owned by other changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:09:19 +02:00
Maxim 8bafd4870c docs(react-native): narrow the render-prop drift guarantee to what holds
The header claimed deriving `RenderToolProps` from `ReactToolCallRenderer` made
drift between RN and web impossible, and that `check-types` would catch any
divergence. Both halves are false as written.

react-core publicly exports its own `RenderToolProps<S>`
(src/v2/hooks/use-render-tool.tsx:9-36) which is generic over a schema, carries
arguments under `parameters` rather than `args`, and types `status` as the string
literals "inProgress" / "executing" / "complete" rather than as `ToolCallStatus`
members. RN's derived type differs from it in both the payload field name and
the `status` type, today, on the same branch that made the claim.

`check-types` cannot see that divergence: nothing relates the two types, and the
one place they meet — react-core's bridge at use-render-tool.tsx:178-186 —
spreads the enum-typed props into the literal-typed slot and compiles, because a
string-enum member is assignable to its own literal type. Web's public `status`
is a widening of the canonical contract, not a derivation from it.

Rewritten to claim only the defensible guarantee: RN's props cannot drift from
`ReactToolCallRenderer`, the contract renderers are actually invoked against.
The residual divergence from web's public type is now stated explicitly, along
with the fact that RN's entry point re-exports web's three `RenderTool*Props`
arms so both shapes ship under similar names. The trailing paragraph now names
the states as `ToolCallStatus` members, matching how the reference page
describes them.

Comment-only; no type declaration changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:50:37 +02:00
Maxim 803ef2d7ab docs(react-native): correct the inverted pre-refactor history in render-tool-types
The header JSDoc on the new derived `RenderToolProps` claimed the deleted RN
type had "args unconditionally partial". The opposite was true: the old
`RenderToolContext.tsx` declared `args: T`, so RN promised the FULL argument
object at every status — the drift this refactor fixes is that the canonical
contract ADDS an `"inProgress"` state in which `args` narrows to `Partial<T>`.

Stating the drift backwards in the very file that defines the contract misleads
anyone reasoning about the migration, so the historical claims are now spelled
out concretely and verified against the deleted type:

- old `status` was `"executing" | "complete"` with no `"inProgress"` member
- old type omitted `name` and `toolCallId` entirely
- old `args` was unconditionally `T`, never `Partial<T>`

Comment-only; no type declaration changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:38:17 +02:00
onsclom 48312f4d65 chore: release monorepo v1.67.0 2026-08-10 18:32:14 +00:00
github-actions[bot] 8c9bcb9140 style: auto-fix formatting 2026-08-08 15:33:51 +00:00