Bump @ag-ui/core, @ag-ui/client, @ag-ui/encoder from 0.0.53 to 0.0.56
across all packages.
@ag-ui/client 0.0.56 changed runHttpRequest from (url, requestInit) to a
fetch-thunk signature (() => Promise<Response>). Update the single-route
and connect transport paths in ProxiedCopilotRuntimeAgent to wrap the
request in () => this.fetch(url, init), restoring the broken envelope
transports.
Add @ag-ui/core, client, encoder, proto to minimum-release-age-exclude
in .npmrc so the freshly published 0.0.56 (under the 24h release-age
gate) installs in CI.
## Summary
`setRuntimeTransport` was not idempotent on the **requested** transport
mode. Auto-detect resolves the requested `"auto"` to a concrete
transport (`"rest"`/`"single"`) and writes that back to
`_runtimeTransport`; the guard then compared against that **resolved**
value. So re-applying the same requested mode — which the provider
effect does on **every render** — compared unequal and **re-ran the
entire `/info` handshake**, rebuilding the runtime agents mid-session.
When that re-sync lands during a turn, `useAgent` hands the UI a
freshly-rebuilt (empty) agent for a render, **blanking the whole
transcript** (and any per-message UI bound to it — e.g. the intelligence
indicator) until it replays.
## Fix
Track the **requested** mode separately (`_requestedTransport`) and
guard on it. Re-applying an unchanged requested transport — including
`"auto"` after auto-detect has resolved it — is now a no-op, so no
redundant `/info` re-sync fires.
## Tests
`packages/core/src/__tests__/agent-registry-resync.test.ts`: re-applying
`"auto"` after auto-detect resolves it does **not** refetch `/info`.
## Scope / risk
Touches only `packages/core` (`core/agent-registry.ts` + the test). **No
public API change.** Genuine transport *changes* still re-sync exactly
as before; only redundant re-applications of the same requested mode are
skipped.
> **Verified live:** this idempotency guard *alone* eliminates the
mid-turn re-sync in the e-commerce intelligence demo — instrumentation
shows no `/info` handshake fires during a turn, and the
transcript/indicator no longer flickers. (An earlier draft also made the
re-sync itself non-destructive as defense-in-depth; that was dropped
because, with this guard, no mid-turn re-sync occurs for it to protect
against.)
Replace the brittle `"activeRunCompletionPromise" in agent` probe + double
`as unknown as` casts in CopilotChat with a typed `RunCompletionAware`
contract plus an `isRunCompletionAware` type guard, both exported from core.
IntelligenceAgent now declares the property and implements the interface, so
the in-flight-run await is reachable without a cast and non-Intelligence
agents still degrade safely. Cast sites in the attachments/e2e tests and the
MockStepwiseAgent helper are updated to the typed accessor.
Factor the await-then-send logic into a shared `waitForActiveRunToSettle`
helper and call it from BOTH onSubmitInput and handleSelectSuggestion. This
closes the suggestion-path in-flight gap: selecting a suggestion mid-run no
longer pre-empts/aborts the active run (e.g. an interrupt RESUME) — the same
regression PR #5195 fixed for the typed-Enter path. Adds a red-green
regression test that parks the suggestion send on the in-flight promise.
Auto-detect resolves the requested transport ("auto") to a concrete value
("rest"/"single") and writes it back to _runtimeTransport. setRuntimeTransport
then compared against that resolved value, so re-applying the same requested
mode — which the provider effect does on every render — compared unequal and
re-ran the entire /info handshake, rebuilding the runtime agents mid-session
and blanking the transcript (and any per-message UI bound to it).
Track the requested mode separately (_requestedTransport) and guard on it, so
re-applying an unchanged requested transport is a no-op.
Adds coverage: re-applying "auto" after auto-detect resolves it does not
refetch /info.
Adds two CI signals for keeping the published packages small and broadly compatible:
- Bundle size: size-limit file-mode config across packages plus a
CopilotChat import-size regression signal (gzip) so growth in the
headline consumer entrypoint is visible on every PR. A bundle-size
workflow comments results on the PR (Phase 1: no hard-fail).
- ES compatibility: a compat-check (es-check) script across 9 packages
with a root .browserslistrc, validating built .mjs/.cjs against the
es2022 build target.
The measure script is importable (measureBundle) and unit-tested. Dev
docs live under dev-docs/ (bundle-size.md, browser-compat.md). All
action refs are pinned to full commit SHAs for supply-chain safety.
Packages without repository.url fail npm OIDC provenance verification.
Adds the field to agentcore-runner, core, sqlite-runner, voice, and
web-inspector. Includes a one-shot workflow to publish the 14 remaining
v1.57.4 packages (a2ui-renderer already published via OIDC).
- Add telemetryDisabled to RuntimeInfo from COPILOTKIT_TELEMETRY_DISABLED/DO_NOT_TRACK env vars
- Mirror through AgentRegistry and expose via CopilotKitCore getter
- Guard track calls, URL param appending, and console disclosure on core.telemetryDisabled
- Move maybeShowDisclosure() to onRuntimeConnectionStatusChanged (fires after core attaches)
- Update docs to replace localStorage toggle description with env var approach
- Add telemetryDisabled test suite to get-runtime-info tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the chat's connect effect re-fires due to React effect-dep churn, it
calls copilotkit.connectAgent({ agent }) on the same thread again. The
RunHandler previously called agent.setMessages([]), agent.setState({}),
and (transitively) clearReconnectCursor on every such call. That forced
the realtime gateway to replay the topic's full event history on every
churn re-connect — sending the same persisted cpki_event_ids 2-3 times
per thread switch — and produced both halves of Tyler's bug: duplicate
rows in the inspector AG-UI Events tab plus "Message not found" toasts
when the next runAgent fired with an empty agent.messages.
This change makes the orchestrator detect actual thread switches:
- RunHandler tracks _lastConnectedThreadId across connectAgent calls.
On a fresh restore (different threadId from last call) it does the
reset and clears the replay cursor. On churn (same threadId) it
skips the reset entirely so local messages/state are preserved and
the gateway resumes from lastSeenEventId instead of replaying.
- IntelligenceAgent.connect() no longer auto-clears the cursor;
cursor management is the caller's decision now. clearReconnectCursor
is made public so RunHandler (and tests) can call it explicitly.
- ProxiedCopilotRuntimeAgent exposes a clearReplayCursor(threadId)
method that delegates to the IntelligenceAgent. Non-Intelligence
runtime modes are a safe no-op.
This supersedes #4720, which attempted to solve only the inspector
duplicate-row symptom by adding a dispatcher dedup. That approach
introduced a blocking regression in the A → B → A restore path: the
dedup persisted across thread switches and suppressed the gateway's
restore replay, leaving agent.messages at [] and triggering the very
"Message not found" toast we were trying to prevent. Mike's review
caught it. The dispatcher dedup is removed entirely; the
orchestrator-level gate is sufficient and a smaller surface change.
Tests:
- intelligence-agent.test.ts: updated 4 tests that codified the old
"always clear cursor" semantics. Added an explicit test that
clearReconnectCursor() empties the cursor for the next connect, and
Mike's regression — replay the same cpki_event_id after A → B → A
and verify the rehydrate still produces the user message.
- core-connect-thread-switch.test.ts (new): four tests for the
RunHandler gate covering churn, single switch, A → B → A, and the
no-clearReplayCursor non-Intelligence path.
All 430 core tests pass.
Renames the proxy-config field, the field on the agent instance, and
all matching references in tests and the useCopilotKit reference page.
"runtime" reads more naturally now that the proxy concept is documented
as "a local agent that delegates to a runtime agent" rather than
"remote agent" — the latter conflates with `remoteAgents` (the
registry of agents fetched from the runtime), which keeps its name.
No behavioral change; the field still controls the outbound REST URL
used by the proxy.
Round-2 review surfaced two cheap improvements; this commit lands them.
All 7 round-1 findings were confirmed fixed by round 2.
- core.ts onAgentsChanged: each iteration of the unregister loops is
now wrapped in try/catch. A throw on iteration [0] no longer stalls
cleanup for [1..n]; both registries' unregister paths are
idempotent so re-attempts on the next onAgentsChanged are safe.
- agent.ts abortRun: removed dead `if (!routedId) return;` after
`routedAgentId()` — that method now throws (or returns a non-empty
string), so the guard is unreachable.
Tests: core 425 — all green.
Round-1 review found seven actionable items; this commit lands fixes for
all of them. Tests: core 425, react-core 1151, web-inspector 7 — all green.
Real bugs fixed:
- run-handler.ts: dropped the stale `agent` argument on the
`reloadSuggestions(agentId, agent)` call. The signature was tightened
to `(agentId)` when the consumerAgent parameter was removed; the call
site wasn't updated, leaving a TS-2554 build break.
- agent.ts: tightened `routedAgentId(): string` to throw when both
`agentId` and `remoteAgentId` are unset, instead of returning
`string | undefined`. Removes two `!` non-null asserts in
`#runViaHttp` / `#connectViaHttp` and the silent
`/agent/undefined/connect` URL path.
- agent.ts: marked `remoteAgentId` `readonly`. The field was publicly
mutable but `super.url` is baked at construction — mutating
`remoteAgentId` post-construction silently desyncs the REST run URL
from the routing decision elsewhere. `readonly` prevents.
- agent-registry.ts: the registerProxiedAgent collision check now uses
`Object.prototype.hasOwnProperty.call(this._agents, agentId)` instead
of `agentId in this._agents`. The `in` operator walks the prototype
chain, so an agentId of `"__proto__"`, `"constructor"`, etc. would
falsely test as already-registered.
- core.ts: the onAgentsChanged handler now mirrors the thread-store
unregister loop with a parallel
`stateManager.unsubscribeFromAgent(agentId)` for any agentId in
previousAgentIds but absent from the current snapshot. Without this,
`unregister()`'s state-manager subscription leaked.
Comment / test cleanup:
- CopilotChatView.tsx:92: stale "empty cloned agent" reference in the
`isConnecting` JSDoc rewritten to "empty agent instance" — clones are
gone.
- core-register-proxied-agent.test.ts: split the misleading "registering
before runtime connects yields a proxy in pending runtimeMode" test
(which exercised the no-runtimeUrl path, never the pending path) into
two: one for the no-runtimeUrl case, one that actually constructs a
core with a runtimeUrl and asserts `runtimeMode === "pending"`.
Re-adds the isolation coverage that the per-thread cloning revert deleted,
rewritten against the explicit-registration model:
- 9 new core-level tests in core-register-proxied-agent.test.ts cover the
cases from the deleted use-agent-thread-isolation.test.tsx — distinct
instances when two proxies target the same remoteAgentId, message and
state isolation between proxies, independent threadId per proxy, shared
outbound URL (both route to remoteAgentId), getAgent identity, pending
registration before runtime connect, registration with a remote id the
runtime doesn't yet know, and re-register-after-unregister yielding a
fresh proxy.
- 1 new react-core test in CopilotChatActivityRendering.e2e.test.tsx
replaces the deleted "passes the per-thread clone to activity message
renderers" regression test. The clone-vs-registry trap is gone in the
new model, so the test is reframed: the renderer must receive the agent
registered under the local agentId, not any other agent in the registry
(e.g. the runtime-side id a proxy might route to).
Total: +10 tests. Core 415 → 424, react-core 1150 → 1151.
Adds a public CopilotKitCore.registerProxiedAgent({ agentId, remoteAgentId }) API
that mints a ProxiedCopilotRuntimeAgent under a local registry id and routes its
outbound HTTP requests to the named runtime agent. Returns { agent, unregister }
so React callers can clean up via useEffect.
Throws when agentId is already taken — collisions with agents__unsafe_dev_only or
a previous registerProxiedAgent are loud, not silent.
ProxiedCopilotRuntimeAgent gains a remoteAgentId field used only for outbound
routing — URL paths (/agent/<id>/run, /connect, /stop), single-route envelopes,
and the IntelligenceAgent delegate's agentId. The local agentId remains the
registry key and the source of truth for state-manager subscriptions, useAgent
caching, and onAgentsChanged. So multiple proxies (e.g. chat-1, chat-2) can
target the same runtime agent ("default") without cross-talk in any subscriber
bookkeeping.
Use case: replaces the implicit per-thread cloning previously offered by
useAgent({ threadId }). Callers now opt into multiple frontend agents
explicitly.
Includes 6 unit tests covering routing, duplicate-throw, idempotent unregister,
onAgentsChanged notification, and header inheritance.
Reverts the cloning design from #3525 (useAgent per-thread clones, getThreadClone,
globalThreadCloneMap, cloneForThread) and #3630 (clone routing in activity renderers),
plus the inspector machinery that existed only to handle clones (onAgentRunStarted
subscriber + run-handler emissions from #3869, the connect-time emission from #3872,
and the agentRunThreadId map that read from it).
State-manager isClone composite-key path and SuggestionEngine consumerAgent param —
both added in #3525 to keep clones visible to bookkeeping — are gone too.
Restores agent.threadId = resolvedThreadId in CopilotChat (pre-#3525 behavior) and
swaps the inspector's agentRunThreadId map for a direct agent.threadId read.
Removes the DemoButtonAgent and /a2ui-demo page from the demo (added by #3630 as a
clone-fix repro).
Re-opens the original issue #2957 (CPK-7155): two CopilotChat instances with the same
agentId and different threadIds will share message state again. The follow-up is a
public registerProxiedAgent API so callers can opt into multiple frontend agents
proxying to the same runtime agent, without implicit per-thread cloning.
Pins three integration cases for the thread-store auto-unregister branch
in CopilotKitCore's internal onAgentsChanged subscriber:
1. agent removed from agents → store IS unregistered; subscriber receives
the previous store via prevStore.
2. FIRST onAgentsChanged({ agents: {} }) on a published-style core where
the store was registered before any agents arrived → store SURVIVES.
Reproduces the race that the "previously had" guard exists to
prevent.
3. add → register → remove cycle → store IS unregistered. Complements (2)
by exercising the same code path's positive branch.
Test (1) surfaced a real bug in the seed: agentRegistry.initialize does
NOT emit onAgentsChanged, so the constructor's internal subscriber never
saw the agents__unsafe_dev_only set, and `previousAgentIds` stayed empty.
A later removeAgent__unsafe_dev_only call would then be guarded into a
no-op because the agentId looked "new". Fixed by seeding
`previousAgentIds = new Set(Object.keys(agents__unsafe_dev_only))` right
before wiring the subscriber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CopilotKitCore subscribes to onAgentsChanged and unregisters thread
stores for any agentId not in the new agents map. For published cores,
core.agents is asynchronously populated, so the FIRST
onAgentsChanged({ agents: {} }) notification fires BEFORE published
agents are merged in. Without a guard, that empty notification rips out
a thread store that a consumer (e.g. useThreads) just registered.
Track previousAgentIds and only unregister an agentId that was present
in the previous snapshot AND missing from the new one. The first
empty-agents notification (where the agentId was never previously
present) becomes a no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- thread-store-registry: makeStore now attaches a __testId via
intersection so callers can distinguish stubs at a glance during
debugging instead of relying on identity-by-allocation alone.
- thread-store-registry subscriber-isolation test: assert the
diagnostic content ("Subscriber onThreadStoreRegistered error") and
Error argument, not just that some error was logged.
- handle-threads identifyUser-throws test: assert
"Error identifying intelligence user" with an Error argument, since
the throw originates inside resolveIntelligenceUser which logs and
returns 500 before subscribeToThreads is reached.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
R1's notify-ordering fix only made the sync subscriber path safe; async
subscribers still race against the next register(). When notifySubscribers
awaits inside Promise.all, control returns to register() which assigns the
new store to the same agentId before the async handler resumes — at which
point registry.get(agentId) returns the new store, not the unregistered one.
Carry the previous store on the onThreadStoreUnregistered payload so
subscribers tearing down state don't need to consult the registry. Same
treatment for unregister(). Doc the "do not call registry.get(agentId) in
this callback" contract on the subscriber type itself.
Also tighten getAll(): cache the snapshot, freeze it (so the Readonly<>
claim is honest at runtime), and return the same reference between
mutations. Stable identity matters for useSyncExternalStore consumers that
compare snapshots to skip re-renders. Tests updated to assert prevStore
delivery, frozen-snapshot mutation throws, and reference stability across
calls. Mock notifySubscribers now uses Promise.all to mirror production
parallel dispatch.
vitest types invocationCallOrder as `number[]`, but TS narrows
indexed access to `number | undefined` under noUncheckedIndexedAccess.
Capture the entries first, assert each is defined, then compare —
keeps the ordering check intact while satisfying tsc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tests in @copilotkit/react-core need the WebSocket retry budget so they
can validate teardown semantics without hardcoding the threshold
separately from production. Exposing it under the ɵ-prefixed internal
namespace keeps it out of the public API surface while letting the
test bench import a single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-part welcome-screen regression for `available: "always"` suggestion configs
when the chat connects to agents via `runtimeUrl` instead of registering them
locally:
1. SuggestionEngine.reloadSuggestions bailed early when the agent wasn't yet
in the registry. With runtimeUrl, the registry is empty during the initial
/info fetch, so the very first reload (fired by useConfigureSuggestions on
mount) skipped every config — static pills never appeared on the welcome
screen, dynamic pills never started generating. Now: don't bail, default
`messageCount` to 0, run static configs anyway. Dynamic configs still need
a real agent and skip until one arrives.
2. useConfigureSuggestions's global-config path only iterated the current
agents map, which compounded the problem above — the empty map meant zero
reloads. Now: also reload for the chat's resolved consumer agent (covers
the empty-map case), and subscribe to onAgentsChanged for dynamic configs
only, firing exactly once when the target agent first appears (so dynamic
pills catch up after the runtime fetch completes, without piling up
overlapping generations as multiple useConfigureSuggestions hooks mount).
`hasSuggestions` keeps the `!isConnecting && !isRunning` UI gate. `available:
"always"` controls eligibility windows (welcome screen vs. after first
message), not whether to render through connect/replay or through a run —
those still hide and the end-of-run reload regenerates against the new
context.
Tests:
- Engine: added unit coverage for reloadSuggestions when no agent is present.
- React: 4 integration tests covering welcome screen (specific + global
consumerAgentId) and the run lifecycle (hide during run, reappear after) in
default and pin-to-send modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- register() now snapshots and clears the previous slot before queuing the
unregister notification, then assigns the new store before queuing the
register notification. The unregister microtask is queued first so
subscribers tear down stale subscriptions before receiving the
replacement.
- All notifySubscribers calls now attach .catch handlers so notification
failures surface via console.error instead of being silently swallowed
by `void`.
- getAll() returns a shallow copy so callers cannot mutate the registry's
internal state through the returned reference.
- Test mock now mirrors the real notifySubscribers signature (handler +
errorMessage) and wraps each subscriber call in try/catch, exercising
the same error-isolation behaviour as production.
- Replaced the `as unknown as ɵThreadStore` cast with a satisfies-typed
minimal stub matching the real interface.
- Added tests for ordering (unregister before second register), throwing
subscriber isolation, and getAll() snapshot isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up ag-ui-protocol/ag-ui#1578 — `import * as jsonpatch from
"fast-json-patch"` produced an empty namespace under Node native ESM
because fast-json-patch@3.x populates exports via Object.assign, which
the CJS→ESM named-export detector cannot see. Result: every STATE_DELTA
and ACTIVITY_DELTA event threw "applyPatch is not a function", and
LangGraph generative UI streams floods the console with the failure on
each patch.
0.0.53 switches to a default import so the emitted bundle works under
both ESM and CJS consumers. Bumped @ag-ui/core and @ag-ui/encoder in
lockstep since they share the release.