## What does this PR do?
Hooks can now expose a frontend tool to browser agents through the
WebMCP browser API, next to the normal agent registration. Set `webmcp:
true`, or pass `{ annotations }` for WebMCP hints:
```ts
useFrontendTool({
name: "searchOrders",
description: "Search the signed-in user's orders by status",
parameters: z.object({ status: z.enum(["open", "shipped", "delivered"]) }),
handler: async ({ status }) => searchOrders(status),
webmcp: { annotations: { readOnlyHint: true } },
});
```
How it works:
1. `FrontendTool` in `@copilotkit/core` gains the `webmcp` option. A new
`WebMCPRegistry` registers the tool on `document.modelContext` with its
name, description, input schema, and annotations. `execute` runs the
tool's own handler. The handler context has no `agent` there.
2. Every tool registry change in `RunHandler` reconciles the WebMCP
registrations. The same availability rules apply as for the agent tool
list. Removing a tool aborts its registration signal, and the browser
then unregisters it.
3. Each adapter picks the option up from core: v2 `useFrontendTool`
(React, Vue, React Native), the v1 `useCopilotAction` and
`useFrontendTool` wrappers (React, Vue), and Angular's
`registerFrontendTool`. Where WebMCP is not available (SSR, React
Native, browsers without the API), registration is a no-op.
The `webmcp` prop is documented on the React, Vue, and Angular reference
pages in shell-docs.
## Related PRs and Issues
- None.
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [ ] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
## Testing
**Commands run**
- `pnpm nx run-many -t check-types
--projects=@copilotkit/core,@copilotkit/react-core,@copilotkit/vue,@copilotkit/angular`
— all pass.
- Full test suites: core (829 tests), vue (103), and angular pass.
react-core passes standalone (1589 tests). Under the lefthook pre-commit
hook, react-core flakes on pre-existing e2e tests (A2UI, MCP Apps) that
do not touch this code. Those tests pass when run alone.
**Manual test**
Requires Chrome 149+ with the WebMCP origin trial, or the testing flag.
1. Enable `chrome://flags/#enable-webmcp-testing`, then relaunch Chrome.
2. In an app that uses CopilotKit, register a tool with `webmcp: true`.
3. Run `await document.modelContext.getTools()` in DevTools. The tool is
listed with its schema and annotations.
4. Unmount the hook. Run the command again. The tool is gone.
**How this PR makes testing easy**
The behavior has automated tests on this branch:
- `packages/core/src/core/__tests__/run-handler-webmcp.test.ts` — 15
tests with a `document.modelContext` stub: registration, annotations,
unregistration, availability rules, name collisions, stale-rejection
races, and handler execution.
-
`packages/react-core/src/v2/hooks/__tests__/use-frontend-tool-webmcp.test.tsx`
and the mirrored
`packages/vue/src/v2/hooks/__tests__/use-frontend-tool-webmcp.test.ts` —
pass-through, re-registration, and agent-scoped cases at the hook level.
- `packages/vue/src/hooks/__tests__/use-frontend-tool-webmcp.test.ts` —
reactive `webmcp` getters through the v1 Vue API.
## Risk / rollback
Low. The feature is opt-in per tool. Without `webmcp`, no code path
changes. Where WebMCP is unsupported, registration is a no-op. Revert
this PR to roll back.
## Public API change
New optional `webmcp` prop on frontend tool registrations. Existing call
sites do not change.
**Before**
```ts
useFrontendTool({
name: "searchOrders",
description: "Search orders by status",
parameters: z.object({ status: z.string() }),
handler: async ({ status }) => searchOrders(status),
});
```
**After**
```ts
useFrontendTool({
name: "searchOrders",
description: "Search orders by status",
parameters: z.object({ status: z.string() }),
handler: async ({ status }) => searchOrders(status),
webmcp: { annotations: { readOnlyHint: true } },
});
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Tools can now be exposed to browser agents through WebMCP.
* Added support for custom annotations and automatic parameter schema
generation.
* WebMCP registrations stay synchronized as tools are added, removed,
enabled, or updated.
* Available across Angular, React, and Vue tool APIs.
* WebMCP reuses existing handlers and safely does nothing when
unavailable.
* **Documentation**
* Added usage guidance and examples for configuring WebMCP-enabled
tools.
* Documented that WebMCP invocations do not include an agent context.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The Headless UI console notice told developers about "premium features" and
pointed at /premium/overview. The tier is called CopilotKit Intelligence now, so
the notice named a product that no longer exists. It now uses the same sentence
the Headless UI docs page uses.
The docs links in react-core, web-inspector and the runtime skill reference move
from /premium/* to /intelligence/*. They worked through the redirects added in
#6818, but each cost a hop and carried the old name.
One of them was broken, not just stale: the "Show me how" button on the missing
public API key error opened /premium/overview#getting-access. That heading was
deleted on 2026-06-16 in 449237af0c, so the button had been landing at the top
of the page for two and a half months. It now points at #plans-and-access, the
section that answers how to get a key.
Tests assert these hrefs, so they move with the strings.
Refs OSS-1085
The comment described an http/https/mailto/tel allowlist, but ui/open-link uses a
denylist (javascript:/data:/vbscript:/blob:/file:). Align the comment with the
actual contract so it does not mislead a future change to the scheme policy.
- Add e2e tests pinning the ui/initialize contract (the compile-time tie to the
spec): a well-formed initialize returns the host context and the negotiated MCP
Apps protocol version; an initialize missing required fields (e.g.
appCapabilities) is rejected with -32603; a widget sending a different
protocol-version string gets the host's MCP Apps version back, not its own
echoed. (2025-06-18 is a base-MCP-protocol version, independent from the MCP
Apps protocol 2026-01-26; it is what the old hand-rolled host hardcoded.)
- Nit: load the bridge via `import(...).catch(rethrow)` with inferred types
instead of `typeof import(...)` annotations, removing three
consistent-type-imports warnings.
- Nit: restore the "ui/message: No agent available" warning log on the no-agent
path, for parity with the hand-rolled host and the oncalltool guard.
## What changed
- Add standalone `CPK_TELEMETRY_ID` support to Runtime v1 and v2.
- Keep telemetry opt-out, sampling, Segment, and legacy license fallback
behavior.
- Fetch structured Intelligence entitlements and map them to current
client status.
- Share concurrent entitlement lookups, retry short-lived failures, and
reject stale grants.
- Make managed React, Angular, and Vue thread UIs use Runtime
entitlement authority.
- Keep assistant feedback stable when unrelated Inspector settings
change.
- Update Runtime, telemetry, self-hosting, and Web Inspector docs.
## Why
Managed Intelligence projects use a project API key for product access
and a non-secret telemetry ID for attribution. Offline license tokens
remain a self-hosted entitlement concern.
Starter-template and AgentCore changes live in #6188.
## Companion PRs
- Starter templates: #6188
- CopilotKit/Intelligence#628
- CopilotKit/oss-path-to-production#226
## Review corrections
- Scope shared entitlement attempts to one API key and endpoint.
- Ignore stale attempts after credentials change.
- Bound retries after short denials and transport failures.
- Accept telemetry IDs only when they match the public identifier
contract.
- Read Inspector context in its button, so unrelated label changes do
not rerender assistant feedback.
## Validation
- React Core full suite: 1,537 Vitest tests and 47 script tests passed.
- Runtime, Core, Shared, Angular, Vue, and Web Inspector focused suites
passed.
- React Core typecheck and build passed after the final rebase.
- Direct builds and type checks passed for Angular, Core, Runtime,
Shared, Vue, and Web Inspector.
- Shell docs typecheck and production build passed.
- Changed Vue files passed ESLint.
- `git diff --check` passed.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added structured runtime entitlement support for managed and
self-hosted deployments.
- Feature access and usage limits now reflect active entitlements, with
legacy license compatibility.
- Added runtime entitlement diagnostics to the Inspector’s Threads view.
- Added runtime-scoped telemetry identities and configurable telemetry
ID support.
- **Bug Fixes**
- Licensing interfaces remain in a loading state during retryable
entitlement outages.
- Improved recovery after runtime connection, target, or transport
changes.
- Prevented stale entitlement data from granting access after refresh
failures.
- **Documentation**
- Documented entitlement statuses, telemetry identity precedence,
sampling, and Inspector telemetry behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Re-derived against current main from **#4259** (mxmzb), which diagnosed
this in April. That branch is 7,386 commits behind and conflicts, so
this ports the mechanism rather than rebasing it.
## The bug
`useFrontendTool` registered its tool inside a `useEffect`. React
flushes passive effects **child-first in tree order**, so a component
mounted *before* the registering component runs its own `useEffect`
against an empty tool list.
That is the cross-page-navigation failure: a page mounts `CopilotChat`
and its tool-registering components in one commit, `CopilotChat`'s
connect effect fires first, and the connect request goes out carrying no
frontend tools.
## The fix
Register in `useLayoutEffect`. Layout effects run during commit, ahead
of every passive effect regardless of tree order, closing the window.
This is not a new pattern here — **`useAgentContext` already registers
via `useLayoutEffect`** (`use-agent-context.tsx:2`). The context half of
this hook family was fixed; the tool half was not. This makes them
consistent.
## Testing
### The original test did not detect the bug
Worth recording. #4259 shipped `use-frontend-tool-timing.test.tsx`,
which mounts the tool registrar **before** the observing component. I
ported it verbatim and ran it against unmodified main:
```
✓ src/v2/hooks/__tests__/use-frontend-tool-timing.test.tsx (1 test) 10ms
Test Files 1 passed (1)
```
It passes without the fix. React runs the registrar's effect first in
that order, so the observer always sees the tool. The PR's own comment
concedes the point — *"the result depends on component ordering and may
be absent."*
The test here mounts the consumer **first**, which is the shape that
actually breaks, and says so in the file so nobody reorders it back.
### RED → GREEN on the real surface
**RED** (current main, `useEffect`):
```
× registers the tool before an earlier-mounted sibling's useEffect runs
AssertionError: expected [] to include 'timingTestTool'
Tests 1 failed (1)
```
The empty array is the bug: the consumer's effect saw no tools.
**GREEN** (`useLayoutEffect`):
```
✓ src/v2/hooks/__tests__/use-frontend-tool-timing.test.tsx (1 test) 12ms
Tests 1 passed (1)
```
### No regressions
Full `src/v2/hooks` suite, same environment, with and without the
change:
| | Test files | Tests |
|---|---|---|
| without fix | 8 failed / 29 passed | **37 failed** / 283 passed |
| with fix | 7 failed / 30 passed | **36 failed** / 284 passed |
The delta is exactly the new test. The remaining 36 failures are
pre-existing in my local worktree (stale cross-package `dist`
resolution), identical on both sides.
## Notes
- **SSR:** `useLayoutEffect` warns during server rendering. These hooks
run inside `CopilotKitProvider`'s client context, and the sibling
`useAgentContext` already uses a bare `useLayoutEffect`, so this follows
the established pattern rather than introducing an isomorphic wrapper.
- **Scope:** #4259 also carried a second, independent mechanism — an
`ensureToolMiddleware` fallback that injects tools/context into direct
`agent.runAgent()` calls bypassing `copilotkit.runAgent()`
(`run-handler.ts` +44, plus `core.ts`, `agent-registry.ts`, Angular's
`agent.ts`, `use-agent.tsx`). That addresses a **different** failure and
deserves its own PR, tests and review. It is deliberately **not**
included here and should not be considered resolved by this.
Credit to @mxmzb for the diagnosis.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Frontend tools are now registered earlier, ensuring availability
before the interface is displayed.
* Improved consistency when components access frontend tools during
initial effects.
* Renderer cleanup now occurs at the appropriate stage when components
are removed.
* **Tests**
* Added coverage verifying frontend tools are available to
earlier-mounted components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Two frontend-tool defects that share a shape: a tool that was registered
correctly still never reached the agent, or reached the render without
the ability to respond.
## `useFrontendTool` tools dropped when the runtime enables
`openGenerativeUI` (#4952)
Tools reached the core registry through two owners that shared one array
— the provider via `setTools()`, hooks via `addTool()` — and `setTools`
replaced the array wholesale. Any provider re-sync after mount therefore
wiped every hook-registered tool.
A runtime with `openGenerativeUI: true` made it reproduce on **every**
mount: `/info` flips the flag asynchronously, the provider re-derives
its tool list to add `generateSandboxedUi`, and the re-sync dropped the
app's own tools. The agent then received only `generateSandboxedUi`,
exactly as reported.
The fix splits the two owners into their own buckets, merged on read
with hook entries winning. This mirrors what `CopilotKitCoreReact`
already does for render tool calls (`react-core.ts`) — tools simply
never got the same treatment. Because the clobber lived in core rather
than in one provider, **Vue had the identical bug and is fixed by the
same change** — verified end to end, not by shape:
`CopilotKitProvider.vue:461` assigns `runtimeOpenGenerativeUIEnabled`
from the core, `:297` derives `openGenerativeUIActive`, `:328`/`:354`
add the built-in to `allTools`, and `:492` re-syncs it through
`setTools` behind the same `didMountRef` skip. Angular is unaffected: it
never calls `setTools`, passes `tools` only through the constructor
(`copilotkit.ts:152`), and registers just the *renderers* from
`config.tools` (`:224`) — no second registration.
`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. It also no longer pushes onto the
array the provider passed in, and `initialize` copies that array like
`setTools` already did.
**One intentional behavior change worth a reviewer's eye:** `setTools`
now replaces provider-owned tools only, so `setTools([])` no longer
clears tools registered through `addTool`. That narrowing *is* the fix,
but anyone calling `setTools([])` as a "clear everything" would now need
`removeTool` per tool. Nothing in the repo does (core, react-core, vue,
angular suites all pass).
## Catch-all actions could not wait for a response (#1746)
`getActionConfig` short-circuited on `name === "*"` before checking for
a wait-render, so a catch-all declaring `renderAndWaitForResponse` was
silently downgraded to render-only — no `respond`, and in practice a
`render is not a function` throw, since the render-only path reads
`action.render`. Handling N human-in-the-loop tools required N hooks.
A catch-all with a wait-render now routes to the human-in-the-loop path.
Core already had the execution half
(`getWildcardTool`/`executeWildcardTool`), so this is routing, not new
machinery. Two supporting changes make it 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.
Also: a wildcard tool is no longer advertised 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. Latent before
this PR (nothing in React registered a wildcard *tool*); catch-all HITL
activates it.
## Synced with `main` (2026-08-31)
`main` moved react-core's v1 tree under `src/v1-deprecated/` while this
branch was open, so the merge had exactly two conflicts, both
relocations:
- `use-default-tool.ts`'s `DistributiveOmit` change re-applied on the
moved file, keeping main's deprecation banner.
- the catch-all HITL e2e test moved into
`src/v1-deprecated/hooks/__tests__/`, with its `../../v2/...` imports
re-rooted to `../../../v2/...` to match its sibling
`use-copilot-action.e2e.test.tsx`.
The defect is still live on current `main` —
`CopilotKitProvider.tsx:838` still calls `copilotkit.setTools(allTools)`
— and both regression tests still bite there. The suites, the
before/after checks, `tsc --noEmit`, `oxlint` and `oxfmt` below were all
re-run on the merged tree; the browser walkthrough under **Live
verification** is from the pre-merge branch and was not repeated.
## Testing
**New regression tests**
- `packages/core/src/core/__tests__/run-handler-tool-registry.test.ts` —
13 tests: `addTool` survives `setTools`, hook precedence, agent-scoped
vs global, `removeTool` across both buckets, remount re-registration,
capability toggles surviving a re-sync, provider ordering, caller-array
aliasing in both directions, wildcard never advertised.
-
`packages/react-core/src/v2/providers/__tests__/CopilotKitProvider.openGenerativeUIToolLoss.test.tsx`
— drives the **real** core over a stubbed `/info` that returns
`openGenerativeUIEnabled: true`, asserting the hook tool survives.
-
`packages/react-core/src/v1-deprecated/hooks/__tests__/use-copilot-action-catch-all-hitl.e2e.test.tsx`
— end-to-end through the real provider and core: catch-all gets the real
tool name and a live `respond`, the tool result lands on the original
`toolCallId`, the follow-up run fires, and `*` is absent from
`runInputs[0].tools`.
**Both new tests were confirmed to fail before the fix — re-confirmed
after syncing `main`,** by checking the touched sources out at
`origin/main` and rebuilding core's dist:
```
× keeps the hook tool once the runtime turns openGenerativeUI on
→ expected [ 'generateSandboxedUi' ] to include 'sayHello'
```
and before the routing fix, the catch-all test failed with the exact
defect from the issue:
```
× gives the catch-all render a live respond and the real tool name
→ TypeError: render is not a function
❯ render src/hooks/use-render-tool-call.ts:44:22
```
**Suites (all green)**
```
@copilotkit/core 67 files, 799 tests passed
@copilotkit/react-core 137 files, 1558 tests passed
@copilotkit/vue 101 files, 1092 tests passed
@copilotkit/angular 49 files, 317 tests passed (1 skipped)
```
`tsc --noEmit` clean for `core` and `react-core`; `oxlint` 0 errors on
the changed files (16 warnings, all pre-existing); `oxfmt` applied.
**Live verification** — `examples/v2/react/demo` in a browser against
built dists, with a temporary stub AG-UI agent (no LLM key) that echoes
the tool names it receives, a runtime configured `openGenerativeUI:
true`, no `openGenerativeUI` prop on the provider, one
`useCopilotAction` frontend tool and one `useCopilotAction({ name: "*",
renderAndWaitForResponse })`:
```
TOOLS_RECEIVED: ["generateSandboxedUi","sayHello"] <- #4952: hook tool survived; no "*" leaked
catch-all handling: book_call status: executing <- #1746: real tool name, live respond
[click "Pick Tuesday"]
catch-all handling: book_call status: complete
TOOL_RESULT_RECEIVED: "{"slot":"tuesday"}" <- follow-up run received the result
```
0 console errors. The stub route and page were scratch and are not in
this branch.
## Notes for reviewers
- Community PR #4967 also targets #4952 by merging in the provider
instead. I took the core-layer fix because the clobber is in core's
registry and every framework provider hits it — patching one provider
leaves Vue broken. Happy to reconcile.
- Pre-existing and deliberately **not** changed here:
`useCopilotAction({ name: "*" })` render props do not infer, because the
hook's parameter is a union TypeScript cannot contextually type. This
already affected plain `render` before this PR (verified), so the new
tests and docs annotate props explicitly. Fixing it needs a
`useCopilotAction` overload — worth a follow-up.
- Deliberate small duplications, flagged rather than abstracted:
`WILDCARD_TOOL_NAME` is a one-line const in both core and react-core's
v2 HITL hook (sharing it would mean a new public export from core), and
the link between "a catch-all render receives `name`" and "the v1 HITL
wrapper supplies it" is a cast rather than a type — making it typed
means adding `name` to the public `ActionRenderPropsWait`, which is
wider than this fix.
- #4759 is left with contributor PR #5308, and #6101 needs its own
design pass since it introduces new public API.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added catch-all human-in-the-loop actions that can handle unregistered
tools and wait for user responses.
- Catch-all action renderers now receive the actual invoked tool name
and arguments.
- Added support for wait-aware catch-all rendering types.
- **Bug Fixes**
- Preserved frontend tools when provider tool lists are refreshed.
- Prevented duplicate tools and ensured registered tools take
precedence.
- Hidden wildcard tools from agent-advertised tool lists while keeping
them available for handling requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
useFrontendTool now registers the tool renderer in a layout effect, but
useHumanInTheLoop still removed that renderer from a passive effect cleanup.
React runs each phase's cleanups before that phase's effects, but runs the
entire layout phase ahead of the entire passive phase. With the two split
across phases, a keyed remount ordered the outgoing instance's removal after
the incoming instance's registration:
add (new, layout) -> remove (old, passive)
which deleted the renderer that had just been added and left the HITL tool
unrenderable. Moving the teardown to useLayoutEffect restores the correct
remove-then-add ordering.
Caught by the existing 'should maintain executing state across component
remount' test in use-human-in-the-loop.e2e.test.tsx.
The v1 react-core tree moved under src/v1-deprecated/, so the two conflicts
were relocations: use-default-tool.ts's DistributiveOmit change re-applied on
the moved file, and the catch-all HITL e2e test moved into
src/v1-deprecated/hooks/__tests__/ with its v2 imports re-rooted.
- Sandbox handshake race: load the ext-apps bridge (dynamic import) BEFORE
creating and attaching the sandbox iframe. The proxy posts sandbox-proxy-ready
once during srcdoc execution, and the PostMessageTransport must be listening
(connect) when it fires. Awaiting the import after the iframe was attached let
a slow import miss that notification, leaving the widget blank. There is now no
event-loop yield between attaching the iframe and connecting.
- ui/open-link scheme hardening (XSS): the ext-apps schema validates url as a
string only, so a widget could pass javascript:/data:/blob: etc. Parse the url
and refuse a denylist of script-executing / attacker-HTML schemes (javascript,
data, vbscript, blob, file) before window.open. A denylist is used on purpose so
custom-scheme deep links (myapp:, whatsapp:, ...) and https universal links keep
working, since window.open on those hands off to an OS handler rather than
executing in the page. This matches the Anthropic Software Directory policy
(https origins + owned custom URI schemes) and the MCP spec's prudent-host
guidance.
- Tests: reject a disallowed scheme without calling window.open; allow a
custom-scheme deep link.
- @modelcontextprotocol/sdk is now a non-optional peerDependency (matching how
ext-apps declares it) instead of an optional one. ext-apps ships as a direct
dependency and hard-peers the sdk, so the requirement is already inherited by
every consumer; the optional flag only hid that and dropped the install-time
signal. react-core does not use the sdk directly (type import only), so it
stays out of our dependencies; ext-apps remains the direct dependency.
- Guard the lazy bridge import with a try/catch that rethrows naming the packages
and the install command, so a missing or version-skewed peer surfaces as an
actionable error instead of an opaque module-resolution rejection in an effect.
## What does this PR do?
Fixes the v1 compatibility render path so an assistant message can
render every tool call instead of only `toolCalls[0]`.
The returned lazy renderer now:
- matches each tool call with its corresponding tool result;
- renders all registered tool-call renderers in one fragment;
- removes `null` render results and returns `null` when no tool has a
renderer.
Keeping the fragment behind the existing lazy-renderer callback
preserves the exported `useLazyToolRenderer` return signature. Filtering
before returning also avoids attaching empty generative UI, so
caller-provided subcomponents are not suppressed when no renderer is
registered.
Regression tests cover multiple tool calls, per-call result matching,
the all-unhandled case, and a mixed handled/unhandled message.
## Related PRs and Issues
- Fixes#2946
## Verification
- `pnpm nx run @copilotkit/react-core:test` (133 files, 1,530 Vitest
tests plus 47 script tests)
- `pnpm nx run @copilotkit/react-core:check-types`
- `pnpm exec oxfmt --check
packages/react-core/src/v1-deprecated/hooks/use-lazy-tool-renderer.tsx
packages/react-core/src/v1-deprecated/hooks/__tests__/use-lazy-tool-renderer.test.tsx`
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] Documentation is unchanged because this restores existing v1
behavior without changing the public API
- [x] "Allow edits by maintainers" is checked
## Problem
`useAgentNodeName` must update React consumers when AG-UI node events
arrive, and `useLangGraphInterrupt.enabled()` must receive the node
where an interrupt actually occurred.
Current `main` includes the basic ref-to-state reactivity fix from
[ffd1580](https://github.com/CopilotKit/CopilotKit/commit/ffd15801d6d),
but that commit explicitly leaves #1426 open: a later `RUN_FINISHED` can
still replace the interrupting node with `"end"`, and v1 consumers can
still be hidden behind `useCoAgent`'s memoized return value.
## What remains in this PR
Rebased onto current `main` (`e9387e0`) after the v1 source migration,
this PR contains only the remaining behavior:
- Preserve the last active node when `RUN_FINISHED` reports `outcome:
"interrupt"`.
- Preserve it for the legacy `on_interrupt` custom-event flow as well.
- Continue transitioning successful and failed runs to `"end"`; reset
new runs and agent switches to `"start"`.
- Add `nodeName` to the `useCoAgent` return-value memo dependencies so
v1 consumers receive the reactive update.
- Share `INTERRUPT_EVENT_NAME` between the hook and interrupt
implementation.
The public hook signatures and AG-UI protocol remain unchanged.
## Preview workflow
- Disabled pkg-pr-new's generated all-package StackBlitz template;
package preview install URLs remain available.
## Changes
- `packages/react-core/src/v1-deprecated/hooks/use-agent-nodename.ts`
- `packages/react-core/src/v1-deprecated/hooks/use-coagent.ts`
-
`packages/react-core/src/v1-deprecated/hooks/__tests__/use-agent-nodename.test.tsx`
- `packages/react-core/src/v2/types/interrupt.ts`
- `packages/react-core/src/v2/hooks/use-interrupt.tsx`
- `.github/workflows/publish-commit.yml`
## Verification
- Full React Core Vitest suite: **131 files, 1520 tests passed**.
- Preview workflow: Nx formatting and YAML parsing passed.
- `nx run @copilotkit/react-core:check-types --skipNxCache`: passed,
including all 33 dependency tasks.
- `git diff --check origin/main...HEAD`: passed.
- The composite React Core test target then reaches the existing
Windows-only script baseline: 8 path-normalization failures plus 2
symlink-permission failures. These are outside this PR's files; the
complete Vitest suite passes before that script stage.
## Scope
This intentionally does not change the AG-UI event protocol, runtime
event ordering, HITL workflow, v1/v2 compatibility layer, or the
separate node tracking in `use-coagent-state-render-bridge.tsx`.
Fixes#1426
useFrontendTool registered its tool in a useEffect. React flushes passive
effects child-first in tree order, so a consumer mounted before the
registering component runs its own useEffect against an empty tool list.
That is the cross-page-navigation failure: a page mounts CopilotChat and its
tool-registering components in a single commit, CopilotChat's connect effect
fires first, and the connect request carries no frontend tools.
Register in useLayoutEffect instead. Layout effects run during commit, ahead
of every passive effect regardless of tree order, which closes the window.
This matches useAgentContext, which already registers via useLayoutEffect.
Adds a regression test that mounts the consumer FIRST -- mounting it second
passes with either hook and proves nothing.
Re-derived against current main from mxmzb's #4259, which diagnosed this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Load the ext-apps bridge lazily. AppBridge/PostMessageTransport now come from a
dynamic import() inside Effect 1, with a type-only import at the top. A
<CopilotKit> app no longer pays the ~40-50 kB gzipped ext-apps cost unless it
actually renders an MCP App. Verified: the built output has no static ext-apps
import, only import("@modelcontextprotocol/ext-apps/app-bridge").
- Move @modelcontextprotocol/sdk out of dependencies. It is now an optional
peerDependency (mirroring how ext-apps declares it) plus a devDependency for
our own build, so the sdk tree (express, hono, jose, ajv, cross-spawn) is no
longer an install/audit surface for every React consumer. Nothing in the
bundle reaches sdk at runtime; the only sdk usage is a type import.
- Raise the zod peer floor to >=3.25. The ext-apps/sdk schema slice imports
zod/v4 and zod/v4-mini, which only exist in zod >= 3.25; the old >=3.0.0 peer
let a consumer on zod 3.24 hit an unresolvable import at build time.
- Seed the host context at AppBridge construction (hostContext option) instead of
calling setHostContext after connect, so it is deterministically in place when
the widget's ui/initialize is handled (the seam #6689 needs to advertise
displayMode / availableDisplayModes at initialize).
- Restore the full cross-frontend testid surface-contract comment.
- Split the oncalltool guard so "no server hash" and "no agent" report distinctly.
The add-menu ("+") button's tooltip hardcoded the string "Add attachments",
so `labels.chatInputToolbarAddButtonLabel` only retitled the menu item and
the tooltip stayed English. That blocked full localization of CopilotChat
without replacing the whole add-button slot.
Every other tooltip in the v2 chat surface is already label-driven, and the
Angular implementation already derives this tooltip from the same label, so
this was an oversight rather than a deliberate split.
The "/" shortcut glyph stays hardcoded — it is a key name, not prose.
Fixes#6750
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The oxlint CI job failed on the ext-apps migration:
- copilotkit/no-single-arg-zod-record (error): the ui/message _meta field used
the single-arg z.record(z.any()), which is a compile-time error against Zod 4.
Use the two-arg form z.record(z.string(), z.any()).
- Remove the dead hand-rolled JSON-RPC message types left over from before the
AppBridge migration (no longer referenced).
Also normalize type-only imports and formatting in the touched e2e tests.
## Why
An Intelligence integration lost a request-to-row correlation map
partway through a user interaction — no error, no warning. It surfaced
as "our response routing is flaky". OSS-979 filed it as
`CopilotKitProvider` remounting its children.
The provider does nothing of the kind. It renders `{children}`
unconditionally at `CopilotKitProvider.tsx:952` — unkeyed, no early
return, and there is no `Suspense` boundary anywhere in `v2`. Nothing in
the SDK silently re-points the active thread either; every mutation path
(`setActiveThreadId`, `startNewThread`, the drawer row click, the
inspector override) is caller-driven.
The remount was app-side, and it was app-side because this skill told it
to be:
- `references/threads.md:98` teaches `useThreads()` → select →
`<CopilotChat key={activeId}>`, and that recipe is only reachable once
Intelligence is wired.
- `references/switching-agents.md:123` teaches "`key={activeAgent}`
forces remount so thread state doesn't leak" without saying what else
that discards.
- `examples/showcases/reskinnable-demo/src/app/[skin]/layout.tsx:223`
models `<SubagentActivityProvider key={threadId}>` above `{children}`,
commented "Remounting is deliberate".
Follow all three and you key a layout-level provider on a thread id that
changes asynchronously after mount. Everything below it dies
mid-interaction.
Two properties made it invisible:
- Durable threads exist only in Intelligence mode, so with a plain SSE
runtime `useThreads` returns nothing, the selected thread never changes,
and the remount never fires. It appears the moment Intelligence is
wired.
- Whether state survives depends on whether the user acted before the
thread list resolved.
## What changed
Docs only — no library change. Both traps now carry their blast radius,
in the four places an agent actually reads:
| File | Change |
|---|---|
| `SKILL.md` | Two invariants in the load-once section, so they land
before any reference is opened |
| `references/threads.md` | New HIGH entry on keying above app state;
note that `activeId` in the switcher recipe settles asynchronously |
| `references/switching-agents.md` | Existing HIGH entry now states the
blast radius and cross-links the threads trap |
| `references/switching-agents-recipes.md` | Key rule amended — keep it
on `<CopilotChat>`, nowhere higher |
| `references/agent-access.md` | The second route to the same symptom:
`useAgent` swaps a provisional stand-in for the real agent when `/info`
resolves, so an effect keyed on `agent` re-runs once, mid-interaction.
Adds an `isReady` pattern and a HIGH entry |
`isReady` appeared in **zero** shipped skills before this — it was
documented only in `showcase/shell-docs/.../useAgent.mdx` and in JSDoc.
Same shape as OSS-888, where the root cause was the shipped skill rather
than the library.
Also corrects a factual error: the skill claimed `useAgent` returns `{
agent }` only. It returns `{ agent, isReady }`.
The 10-file diff is 5 source files under `packages/react-core/skills/`
plus their 5 mirrors under `skills/`, regenerated with `pnpm
sync:plugin-skills`.
## Verification
- `pnpm check:plugin-skills` — mirror in sync
- `pnpm exec vitest run scripts/__tests__/sync-plugin-skills.test.ts` —
12 passed
- `oxfmt --check` — clean over both skill trees
- Full pre-commit suite green, including `test-and-check-packages`
(`test`, `publint`, `attw` across 2 projects and 20 dependent tasks)
## Not in scope
Whether the run's app keyed on `threadId` or on `agent` is not
settleable from the repo — its source is not in any checkout, and there
is no `2026-08-25` strands run report under
`tools/one-prompt-development/evaluation/runs` on any branch. Both
variants produce the reported symptom and this covers both, so a
first-hand repro is a separate task. The `reskinnable-demo` layout is
left as-is deliberately: it is a legitimate use of the pattern, and it
is now the worked example the guidance warns about.
Scoping detail in the OSS-979 comment.
refs OSS-979
🤖 Generated with [Claude Code](https://claude.com/claude-code)
An Intelligence integration lost a request-to-row correlation map partway
through a user interaction, with no error and no warning. It surfaced as
"our response routing is flaky". OSS-979 filed it as CopilotKitProvider
remounting its children.
The provider does nothing of the kind. It renders `{children}`
unconditionally, unkeyed, with no early return and no Suspense boundary
anywhere in v2. The remount was app-side, and it was app-side because this
skill told it to be:
* `references/threads.md` teaches `useThreads()` -> select ->
`<CopilotChat key={activeId}>`, and that recipe is only reachable once
Intelligence is wired.
* `references/switching-agents.md` teaches "`key={activeAgent}` forces
remount so thread state doesn't leak" without saying what else that
discards.
* `examples/showcases/reskinnable-demo/src/app/[skin]/layout.tsx:223`
models `<SubagentActivityProvider key={threadId}>` above `{children}`,
commented "Remounting is deliberate".
Follow all three and you key a layout-level provider on a thread id that
changes asynchronously after mount. Everything below it dies
mid-interaction.
Two properties made it invisible. Durable threads exist only in
Intelligence mode, so in OSS-only development the selected thread never
changes and the remount never fires. And whether state survives depends on
whether the user acted before the thread list resolved.
Both traps now carry their blast radius, in the four places an agent
actually reads:
* `SKILL.md` -- two invariants in the load-once section, so they land
before any reference is opened.
* `references/threads.md` -- a HIGH entry on keying above app state, plus a
note that `activeId` in the switcher recipe settles asynchronously.
* `references/switching-agents.md` and `switching-agents-recipes.md` --
keep the `key` on `<CopilotChat>`, never on a wrapper or a layout
provider.
* `references/agent-access.md` -- the second route to the same symptom.
`useAgent` swaps a provisional stand-in for the real agent when `/info`
resolves, so an effect keyed on `agent` re-runs once, mid-interaction.
Adds an `isReady` pattern and a HIGH entry. `isReady` appeared in zero
shipped skills before this; it was documented only in shell-docs and in
JSDoc.
Also corrects a factual error: the skill claimed `useAgent` returns
`{ agent }` only. It returns `{ agent, isReady }`.
No library change. The provider behaves correctly; the guidance did not
describe what it costs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the hand-rolled MCP Apps host protocol in MCPAppsActivityRenderer with
the ext-apps host library, so the app<->host protocol is driven by the spec
package instead of string literals and a manual JSON-RPC router (the source of
prior drift: size-change vs size-changed, protocol-version mismatch, etc.).
- Add @modelcontextprotocol/ext-apps + @modelcontextprotocol/sdk deps.
- Per widget, connect an AppBridge over a PostMessageTransport to the sandboxed
iframe (existing sandbox proxy kept as the transport relay). The bridge owns
initialize/capability/version negotiation (protocolVersion 2026-01-26),
host-context, tool input/result, and the sandbox handshake
(onsandboxready -> sendSandboxResourceReady).
- Map app->host requests/notifications to CopilotKit behavior via bridge
handlers: open-link (window.open), tools/call (runAgent proxy), size-changed
(iframe sizing), initialized, logging. Preserve the runAgent queue and the
thread-capture semantics of issue #5819, and tool-input/result delivery.
- ui/message keeps CopilotKit's role/followUp extensions (the ext-apps schema
only allows role "user" and has no followUp) via a custom request handler:
extensions are read from params._meta.copilotkit first (preferred, forward
looking) and from the legacy top-level params.role/params.followUp
(deprecated). Add a test for the _meta channel.
- Remove the hand-rolled PROTOCOL_VERSION, send/response helpers, and the
JSON-RPC switch now owned by the bridge.
Tests: MCP Apps e2e updated to the bridge's behavior (open-link without url is a
schema-validation error surfaced by the bridge). react-core type-check green;
MCP Apps e2e green; validated end-to-end in a browser (widget renders,
initialize/tool-result/ui/message work).
Five comments justified routing thread requests through the instrumented fetch
by saying it lets opening a view restore the status after an outage. It does
not: every binding withholds its thread requests until the status is already
connected, so while it is red nothing is sent. The justification is DETECTION
only, which is what the CopilotChat site already said correctly.
Also:
- Documents both meanings of the Error state and the invariant behind them —
the status reports the last actual contact with the runtime — on the
connection-status reference page, which described only the startup meaning.
- Renames RUNTIME_PROBE_TIMEOUT_MS to ɵRUNTIME_PROBE_TIMEOUT_MS. core/index.ts
re-exports agent-registry wholesale, so a constant whose own doc says
"exported for tests" was public API of @copilotkit/core.
- Guards the Inspector's read of ɵruntimeFetch the way it guards its four other
internal core accessors. A newer Inspector against an older pinned core was
handing the thread store `undefined`, which breaks the Threads view outright
rather than merely losing detection through it.
- Corrects the stop-request comment, which claimed to be the only runtime
destination off the seam; the suggestion route's stateless path, the memory
store and /inspector-metadata are too, just not by design.
- Corrects OSS-904-VERIFY.md, which said scenario 4 had real traffic to work
with and left it off the not-covered list.
Three gaps the reviewers named, all of them behaviour the safety argument
already depends on.
The submission gate through the error state had no automated test in any
binding, though it is the third of three decisions that hold each other up:
if the red state closed the gate, no successful request could be issued and
only a page reload would leave it. The new react-core test drives the real
provider, the real core and the real submit path against a runtime that
goes away mid-session and asserts the state is left through the user
interface. Verified against a mutation that reuses the destructive startup
failure path mid-session: the test goes red.
A mid-session status round trip in a mounted tree was flagged as reasoned
rather than measured. Measured now: the run-activity effect lists the
status in its dependencies, so it does tear down and re-establish, but a
user-initiated run in flight is neither detached nor reconnected, and the
run-activity subscription is back once the status returns. When this chat
owns its run-activity store the round trip does restart it, re-issuing the
thread list and subscribe requests — documented rather than changed: it is
paid on a transition caused by user activity, not while idle.
Both connection-health suites pinned "rest" while the product default is
"auto". Core now covers rest, single and auto; the Intelligence suite
covers a runtime negotiated over the single-endpoint transport.
Thread REST calls go to the runtime, so under the destination rule they
are runtime traffic — but every binding injected the global fetch, so
their outcomes never reached the connection status. The practical effect:
with a dead runtime the Threads view left the status green, and after the
runtime came back, opening the Threads view could not clear it. Only
sending a message could.
React (both the useThreads store and CopilotChat's standalone
run-activity store), Vue, Angular and the Inspector's own owned store now
take `copilotkit.ɵruntimeFetch` instead. It is a pass-through and is
memoized per core, so nothing changes in the healthy case and no extra
request is issued.
Each binding's thread suite asserts the injection at the seam rather than
inferring it, since a regression back to the global fetch is invisible
from the rendered result.