The two real-timer tests in the launcher's error-signal suite each asserted a
pre-beat state 200ms after breaking the connection. The beat is 400ms
(ERROR_GESTURE_MS.beat), so both assertions had a 200ms margin against a wall
clock on a shared runner. When the runner is loaded the 200ms sleep overshoots
400ms, the beat has already fired, and the assertion reports the next phase.
Both claims are already pinned deterministically on the fake clock, where every
phase boundary of the gesture is asserted. The real-timer tests exist only to
show the beat and the gesture run to their end on their own, so the racy
preconditions are removed rather than retimed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last mix made the HUD arrow a bit too light.
An empty 28px SW resize handle sat on the collapsed sidebar toggle.
In dark mode that handle painted as a black box.
The south and west edges still resize the floating window.
The SE grip is unchanged.
Review asked for fewer borders and boxes, and for the dot's heavy border to
become a subtle shadow.
The launcher had two concentric lilac hairlines: the border, and a second
ring 1px outside it as a box-shadow, which also hardcoded the lilac instead
of reading the edge token. The outer one goes. What replaces it is a
one-pixel light edge along the top, which keeps the face from reading flat
without drawing a frame.
The border itself stays, and it is not decoration: against a dark host page
the face measures 1.10:1 (GitHub dark) and 1.04:1 (Tailwind slate-900), so
without it the launcher has no outline there at all.
`backdrop-blur-md` goes too. It sat behind a 95%-opaque fill and bought
almost nothing, while mounting a permanent blur layer over a customer's
page. It did have one real side effect -- promoting the launcher to its own
compositing layer -- and without that the hover scale re-rasterises the mark
every frame and it visibly jitters. `will-change: transform` asks for the
layer directly, and the jitter is gone.
The dot loses its opaque 1.5px collar. That collar was 21% of the dot's
footprint, and because the dot's centre sits *on* the rim, its outer half
painted a hard dark crescent onto the host page rather than onto the
launcher -- which is what read as "heavy". A hairline plus a soft drop
separates it just as well. The fill becomes a lens lit from the upper left,
both stops derived from the signal colour so a new tone needs no new values.
Considered and dropped: tinting the border in the signal colour. On a dark
page the border is the whole silhouette, so tinting it recolours the entire
launcher for a state that can persist for hours -- and the launcher already
pulses in that colour when the failure is new.
Six Tailwind utilities on the launcher set properties the unlayered `css`
block sets again, and unlayered declarations beat layered ones regardless of
specificity -- so `bg-slate-950/95`, `border-white/20`, `ring-1`,
`ring-white/10` and the two hover variants have never had any effect. Each
was the package's only use, so the checked-in stylesheet drops 939 bytes.
No visual change: verified the three rules are gone from the generated sheet
and the 611 tests still pass.
Kept deliberately: plain `border` (the hand CSS sets only `border-color`),
`rounded-full` (nothing else sets the radius) and the focus-visible trio
(the hand CSS sets only `outline-color`).
The face moves to `#181C1F` at 95%, which review asked for. It is a hair
darker than the `#1C1F24` this replaces -- 17.2:1 against white rather than
16.5:1, a ΔE of 2.3, at the floor of what an eye can separate -- so this
settles the question rather than changing the look.
## Problem
`runner` and `intelligence` are mutually exclusive by construction, but
the exclusivity was enforced in only one direction and only for object
literals.
`CopilotIntelligenceRuntime` hardcodes `new
IntelligenceAgentRunner(...)` into its `super()` call
(`runtime.ts:582`), and `runner?` is declared only on
`CopilotSseRuntimeOptions` (`runtime.ts:239`). The type system catches a
`runner:` key on an Intelligence-shaped **object literal** via
excess-property checking — but that is the only barrier. A JS caller, an
`as any`, or a non-literal options object routes through
`CopilotRuntimeShim`'s `hasIntelligenceOptions()` dispatch into the
Intelligence constructor and has `runner` **silently dropped**, with no
diagnostic.
The mirror case is already guarded: `CopilotSseRuntime` throws on
`channels`, and the comment there states the exact reasoning that
applies here — "the type forbids it, but a JS / `as any` caller ...
would otherwise land here and have `channels` silently dropped — fail
loud instead." The Intelligence constructor validates `identifyUser`,
`channels`, `memory`, and `ɵlearning`. Same file, same pattern, one case
missing.
### It also made a shipped skill lie
`packages/runtime/skills/runtime/SKILL.md:87` asserted:
> Passing both `runner` and `intelligence` to `CopilotRuntime` is
rejected at construction.
It was not. And that contradicted the skill's own reference page,
`references/agent-runners.md`, which correctly described the silent
drop. Two files in the same shipped skill said opposite things about the
same behaviour.
## Change
- **Guard** (`runtime.ts:512`) — `CopilotIntelligenceRuntime` now throws
when `runner` is present, mirroring the `channels` guard in
`CopilotSseRuntime`. The message names the exclusivity and points out
that an in-memory/SQLite runner is unnecessary in Intelligence mode,
where durability is managed by the service.
- **`SKILL.md:87` unchanged** — the guard makes it accurate.
- **`references/agent-runners.md` updated** — this is *not* optional.
That page was the accurate one before this change; adding the guard
makes its "the auto-wired Intelligence runner wins regardless of what
you pass" false. Leaving it would fix SKILL.md's lie by creating the
same lie in the reference — rotating the contradiction rather than
resolving it. Its stale `:149-173,285-294` source citation is corrected
to the real line numbers too. Root `skills/` mirror regenerated via
`pnpm sync:plugin-skills`.
## Behavior notes
- Explicit `runner: undefined` still constructs. This matches how the
sibling `identifyUser` / `channels` / `memory` guards treat `undefined`,
and avoids breaking callers that spread an options object.
- The v1 deprecated compat path is unaffected:
`copilot-runtime.ts:492-509` already omits `runner` from its
Intelligence branch, so nothing routes a `runner` into this constructor
from v1.
## Tests
Two tests in `channels-option.test.ts`, alongside the existing `sse
runtime rejects channels` mirror:
- `intelligence runtime rejects a caller-supplied runner` — written
first and confirmed **red** against the unpatched constructor
(`AssertionError: expected [Function] to throw an error`), green after.
- `intelligence runtime tolerates an explicitly undefined runner` — pins
the undefined-tolerance above so the guard cannot over-throw.
## Verification
| Gate | Result |
|---|---|
| `nx test @copilotkit/runtime` | 144 files, **2080 passed, 0 failed** |
| `nx check-types @copilotkit/runtime` | Successfully ran (+22 deps) |
| `oxlint` (changed files) | 0 warnings, 0 errors |
| `oxfmt --check` | no issues in changed files |
| lefthook pre-commit + commit-msg | all green |
Closes OSS-933.
"no telemetry payload anywhere carries the failure message" serialises the
whole property bag and asserts it does not contain "503". Two of those
properties are the anonymous distinct id -- random hex -- so a three-digit
numeric needle lands inside one roughly once in a few hundred runs. It just
failed a CI job that way, which reads like a privacy breach and is not one.
The ids are excluded by name and asserted to still be strings, rather than
weakening the needles, which are the point of the test. Proven both ways:
with an id seeded to contain "503" the test passes with this change and
fails without it, and it still goes red when a real message is attached to
the payload.
`refocusEventErrorLanding` reads the subject back out of the card's
`data-cpk-event-error` attribute, where the DOM can only offer
`string | undefined`, and handed that to a guard typed for
`LauncherSignalKey`. Narrowing untrusted input is what the guard is for, so
it takes a string; every caller that already holds a key still satisfies it.
Tests do not typecheck, so the suite stayed green and only `check-types`
saw it -- which failed three CI jobs on the same one line.
## What does this PR do?
Lets a developer open a saved Inspector thread in the live official
chat.
- New header action: **View in your app**
- Official React and Vue chat switch to that thread
- A pinned `threadId` does not block the switch
- **Stop viewing** or an app thread change restores the previous thread
- Example threads have no action
- Production builds hide the action
- Same agent only. No matching official chat shows an error in the
Inspector
Core owns a two-way EventClient bridge
(`@tanstack/devtools-event-client`). The root import is a no-op in
production.
Docs: Inspector guide, section **View a thread in your app**.
## Related PRs and Issues
-
https://linear.app/copilotkit/issue/OSS-871/new-features-add-a-new-view-thread-in-your-app-feature
## 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
`handleMenuSelect` applied the event error's landing on every click on Agent
or AG-UI Events. A landing selects the failed agent, clears the event
filters and re-expands the failed row -- help for a reader who came
*because* of that error, vandalism for one who did not.
`lastEventError` outlives being read on purpose, so the how-to-fix card
survives while it is being read. Together that meant: one run error, and
for the rest of the session every return to AG-UI Events silently wiped the
reader's own search filter and re-expanded a row from hours ago. It also
undid the `all-agents` restore eight lines above it, whose comment says it
exists "so the Events tab isn't silently filtered to one agent".
The three arrivals that really are landings keep it -- pressing the
launcher, pressing the card, and an error arriving while its view is
already open. The new test fails with the old call in place, on the value
the reader can see: expected '' to be 'bookFlight'.
Open Inspector Event Snippets on localhost. You can compile, save, and
replay AG-UI events in chat. Chat shows a bookmark icon next to a tool
call, an A2UI block, or generative UI. Click the icon to save that turn
as a snippet.
## What does this PR do?
This PR adds the Inspector Event Snippets pane.
You can:
- Compile a snippet from a recipe (tool-call, reasoning, text, activity,
raw)
- Save snippets in origin-scoped localStorage
(`cpk:inspector:event-snippets`)
- Import and export snippets from the pane header
- Replay a snippet into live chat through Inspector-only Core inject
Each Run remints `messageId`, `parentMessageId`, `toolCallId`, and
`runId`. The second Run of the same snippet is a new turn.
On localhost, chat shows a bookmark icon beside a tool call, A2UI block,
or generative UI. The icon is absolutely positioned. It hangs to the
right when there is room. Otherwise it hangs to the left. The card stays
full chat width.
The React demo adds `sayHello`, `getTime`, `addNumbers`, and a **Call 3
tools** suggestion.
## Related PRs and Issues
- Linear
[OSS-874](https://linear.app/copilotkit/issue/OSS-874/new-features-also-allow-users-to-emit-specific-events-from-the)
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
## Testing
### Commands run
1. Lefthook pre-commit ran `nx` targets `test`, `publint`, and `attw`
for 27 affected projects. All passed.
2. I did not run `pnpm test:pr` (full repo). Lefthook ran the affected
package matrix only.
### Manual test
1. Run `pnpm demo:react` from the repo root.
2. Open http://localhost:3000
3. Open Inspector and select Event Snippets.
4. In chat, click **Call 3 tools**. Confirm three tool cards at full
chat width, with the bookmark hanging outside the card.
5. Click a bookmark, then click Run twice. Chat shows a second turn with
new IDs.
### How this PR makes testing easy
- `packages/web-inspector/src/lib/__tests__/event-snippets.test.ts`
- `packages/core/src/__tests__/inspect-inject.test.ts` (covers two
injects)
- React demo: `examples/v2/react/demo/src/app/page.tsx`
## Linked issues
Linear
[OSS-874](https://linear.app/copilotkit/issue/OSS-874/new-features-also-allow-users-to-emit-specific-events-from-the)
## Risk / rollback
- If ID remint is wrong, a second Run can no-op or duplicate a turn.
- The save icon shows on localhost Inspector (or when `showDevConsole`
is `true`).
- Rollback: revert this PR.
## Public API change
**Before**
Angular has no Inspector service.
```ts
// no CopilotInspector export from @copilotkit/angular
```
**After**
```ts
import { CopilotInspector } from "@copilotkit/angular";
const inspector = inject(CopilotInspector);
inspector.openInspector({
messageId: "msg-1",
menu: "event-snippets",
});
```
React and Vue apps that already mount Inspector on localhost need no new
caller code. Chat wires the bookmark through Inspector context.
`@copilotkit/core` exports `ɵinjectInspectorEvents` for Inspector only.
App code must not call it. There is no public Core emit API.
- Raw recipe: the Events JSON input now calls requestUpdate, so Run and
Save stop being permanently disabled.
- Tool args recovery: one depth scan replaces the parse-every-prefix loop.
Truncated args from a streaming tool call now fail at once, not after
seconds of blocked main thread.
- Chat bookmark: hidden while the tool arguments are incomplete, so a
partial payload cannot be captured.
- saveEventSnippet: React, Vue, and Angular wrap the body, so a compile or
storage failure is reported instead of becoming an unhandled rejection.
- Vue and Angular now gate the in-chat affordances on a dev build plus
localhost, the same as React. showDevConsole: true on a staging URL no
longer puts a bookmark into a production chat.
Closes the remaining half of OSS-899.
PR #6644 removed the `require("reflect-metadata")` banner from the
published
`.d.cts` files, taking a bare strict-mode `import { CopilotRuntime }`
from 81
errors to 10. This takes it to **1**, and that one is not ours.
## Result
Measured by overlaying the built `dist` onto a real consumer install of
1.69.0
and typechecking a bare import under `strict` + `skipLibCheck: false`:
| entry point | before | after |
| -- | --: | --: |
| `@copilotkit/runtime` | 10 | **1** |
| `/v2` | 1 | **1** |
| `/v2/express` | 1 | **1** |
| `/v2/hono` | 1 | **1** |
| `/v2/node` | 1 | **1** |
| `/langgraph` | 7 | **6** |
The single remaining error on every entry point is the same one, and it
is in a
third-party package — see "Not fixed here" below.
## The gate
`scripts/validate-dts-imports.ts` is the companion to
`validate-dts-ambient.ts`.
That one checks the *shape* of published declarations; this one checks
what they
*reach for*, against a single invariant:
> Every module a published `.d.ts` imports must be resolvable by someone
who
> installed this package and nothing else.
It flags devDependencies, optional peers, dependencies whose types live
in a
devDependency `@types/*`, relative imports of JS-only bundler chunks,
and an
explicit ban on `graphql-yoga`. It started red on 18 violations and is
now clean
on all 175 declaration files. Wired into the existing `check-dts`
target, so it
runs in `check:packages` and `static_quality.yml`.
It caught two problems the typecheck probe missed, because they happened
to
hoist in that particular install: `@whatwg-node/server` and
`@langchain/langgraph-sdk`.
## The fixes
- **`graphql-yoga` (5× TS2416)** — `GraphQLContext` was
`YogaInitialContext & {...}`.
That one reference pulled the Yoga barrel into every consumer program,
and with
it `lru-cache@10`, whose `implements Map` clause is what actually
errors.
Declared locally instead; structurally identical, so a real Yoga context
still
satisfies it. Nothing in this package serves GraphQL any more — every v1
integration entry point delegates to the v2 Hono endpoint.
- **`@copilotkit/channels` (1× TS7016)** — a devDependency and a pure
re-export of
`channels-core`, so tsdown inlined its prebuilt declarations plus a
rolldown
helper chunk that ships JavaScript only. Types now come from
`channels-core`,
which is a real dependency and stays external.
- **`cors` / `express` (1× TS7016)** — public declarations use their
types, but both
`@types` packages were devDependencies. Promoted to dependencies.
- **`@anthropic-ai/sdk` / `groq-sdk` (2× TS2307)** — optional peers
named in exported
signatures. Params and getters now use `SdkClientLike`, a structural
type a real
client satisfies; the SDK types stay at the two call sites that need
them, inside
function bodies where they are never emitted.
- **`@langchain/langgraph-sdk`** — was deep-importing
`dist/types.messages`, an
internal file of an optional peer. Now derived from the base class
signature in
`@ag-ui/langgraph`, a real dependency.
- **`@whatwg-node/server`** — an `export type {}` workaround for
microsoft/TypeScript#42873 whose own comment said it was waiting on
TypeScript
5.5. We are on 5.9.2 and it typechecks without it.
## Not fixed here
**1× TS2694** — `@copilotkit/license-verifier@0.5.0` references zod's
`core`
namespace, which only exists in zod v4; this package pins `zod ^3.23.3`.
It is an
external package (0.5.0 is the latest published), so the fix is either a
license-verifier release or migrating this package to zod v4. Both are
larger than
this PR and deserve their own ticket.
Also unchanged: the 6 remaining `/langgraph` errors, all inside
`@ag-ui/langgraph`,
`@langchain/core`, and `@langchain/langgraph-sdk`. The `TS2416` on `run`
is present
identically in pristine 1.69.0 — it is downstream of
`@ag-ui/langgraph`'s own
unresolvable deep imports, not something this branch introduced.
## Reviewer note
The `SdkClientLike` swap narrows `adapter.anthropic` and `adapter.groq`
for anyone
reaching past `baseURL` and `apiKey`. Both adapters are v1-deprecated,
and the
alternative is a type error for every consumer who does not install
those optional
peers — but it is a public-surface change and worth a second opinion.
## Verification
- `nx test runtime` — 2078 tests, 144 files, all passing
- `nx build runtime` + `npm run check-dts` — both validators clean on
175 files
- `tsc --noEmit` on the package — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
# 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.
The `memory` latch shipped with no test at all, and the one test that
mentions it still explained the old behaviour: "the Memory store is never
even subscribed from here". That is no longer true -- opening the view
does subscribe it -- so the test passed while teaching the wrong thing.
The comment now says what actually holds: without an Intelligence context
the store never fetches, so there is nothing to fail.
Three tests: a refused load names itself and lands on Learning, a refused
load arms nothing while the view has never been opened, and a resolved one
clears itself. The middle one is the important one -- it is the constraint
that makes `memory` an unread event rather than a state the launcher could
mirror from a cold start.
The harness needed two things. `intelligence` is now settable, because the
view is a locked teaser without it and the latch is unreachable. And the
memory store stub replaces its state rather than mutating it: the store's
selectors memoize on the state they were handed, so an in-place edit
reports the stale value forever and looks exactly like a signal that never
fires.
Also hoists the selector-escape pattern so `no-control-regex` can be
suppressed on it with a reason -- the control range is the point, since an
attribute selector escapes those too. Lint returns to its 5359 baseline.
Arming used to wait 2000ms so a blip produced neither dot nor beat. The
window is gone and that is deliberate, but nothing said so, and the tests
now assert immediate arming as the contract -- so the next reader would
find no trace of the trade-off, only the behaviour.
What the note holds: `threads` can flap and plays a whole gesture for a
blip, bounded by the single pending-beat slot to one gesture per gesture
length; `connection` cannot flap on its own because nothing retries the
handshake; and the `connecting` branch already covers the retry case if a
fix for the mid-session gap adds polling. Mirrored on OSS-904, which is
where such a fix would land.
@anthropic-ai/sdk and groq-sdk are optional peers, so a consumer who never uses
those adapters does not install them -- yet both were named in exported
signatures, which is a TS2307 on a bare `import { CopilotRuntime }`. The
LangGraph agent had a third case, and a worse one: it deep-imported
@langchain/langgraph-sdk/dist/types.messages, an internal file of an optional
peer.
The adapter params and getters now use SdkClientLike, a structural type a real
Anthropic or Groq instance satisfies. The SDK types stay for the two call sites
that need them, inside function bodies where they are never emitted. The
LangGraph message type is derived from the base class signature in
@ag-ui/langgraph, which is a real dependency.
Narrows adapter.anthropic and adapter.groq for anyone reaching past baseURL and
apiKey. Both adapters are v1-deprecated and the alternative is a type error for
every consumer.