The thread-naming sub-run clones the agent without inheriting
agent.user, so any MCP server whose getHeaders resolver requires a
user-id (notably the one returned by CopilotKitIntelligence.toMCPServer)
threw MCPHeaderResolverError on every new chat — naming silently failed
and titles stayed "Untitled".
Opt-in `requiresUser?: boolean` on MCPClientConfigHTTP. When set and
`agent.user` is undefined, the agent skips the server at run-start with
a warn log instead of opening the transport. toMCPServer() sets the
flag by default; the existing resolver throw is retained as
defense-in-depth.
Replaces the 501 stubs in handleGetThreadEvents and handleGetThreadState
with real delegation to CopilotKitIntelligence. Adds two new client
methods (`getThreadEvents`, `getThreadState`) on intelligence-platform/
client.ts that hit the new `/api/_inspect/threads/:id/{events,state}`
routes shipped in Intelligence PR #144 (CPK-7453). Auth flows through
the existing `resolveIntelligenceUser` path; threadId scoping happens
server-side via the API key's org/project resolution.
Wire shapes match the in-memory branch so the inspector consumes both
runtimes identically:
- events: `{ events }` (platform-internal `decodeErrorRowIds` and
`truncated` flags are stripped at the runtime boundary)
- state: `{ state }` where the platform's discriminated `ThreadStateResult`
flattens to the snapshot value for `kind: "snapshot"` and to `null` for
both `no-snapshot` and `snapshot-decode-error`
Replaces the 501 regression-protection tests with real delegation tests
that mock the platform methods, assert call args, and exercise the
no-snapshot / decode-error / throw paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new test areas covering surfaces this PR introduced or relies on:
- fetch-router: matchRoute tests for `/threads/:id/events`,
`/threads/:id/state`, and `/threads/clear` (with and without URL
encoding). Critically pins that "/threads/clear" resolves to
`threads/clear` and does NOT fall through to the more permissive
`threads/update` arm with threadId="clear" — the explicit guard in
the router exists for this reason.
- fetch-handler validation: 405 enforcement for POST/PATCH/DELETE on
the read-only `/threads/:id/events` and `/threads/:id/state`
endpoints, with `Allow: GET` header. Complementary positive case
asserts GET is NOT a 405.
- handle-run: end-to-end test that handle-run.ts:40
(`agent.agentId = agentId`) propagates the registry key onto historic
runs. Without this stamp, InMemoryAgentRunner falls back to "default"
and the agentId filter on `GET /threads?agentId=...` breaks for the
local-dev fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Type quality:
- intelligence/threads.ts handleGetThreadMessages: switch on the Message
discriminant (role) and read narrowed fields directly. Removes
`as Record<string, unknown>` laundering and chained `as` casts on
toolCalls/function/arguments. AssistantMessage's toolCalls always have
`function: { name, arguments }`, so the prior fallbacks (`tc.name`,
`tc.args`) were dead branches.
- in-memory.ts getThreadState: import StateSnapshotEvent and use it
instead of `(event as { snapshot?: unknown }).snapshot`.
Comment / API alignment:
- intelligence/threads.ts handleClearThreads JSDoc no longer claims the
inspector calls this; the actual caller is the demo button.
- in-memory.ts clearThreads JSDoc updated to match.
- in-memory.ts getThreadEvents JSDoc no longer references a SQLite
runner that does not exist; just describes the compaction logic.
- web-inspector lint fix: rename unused `changed` parameter to
`_changed` per oxc no-unused-vars.
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>
- Add the missing protected connect() override returning EMPTY so the
mock matches TestAgent and ThrowingAgent. Without it, a clone() that
ever exercised connect() would fall through to AbstractAgent.connect()
and may try to open a real transport in tests.
- clone() now forwards this.agentId directly instead of coercing
undefined to "". The constructor accepts string | undefined to make
this type-safe — coercion would silently turn "no agent id" into
"empty agent id", a different state per AgentConfig.agentId?: string.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleClearThreads is intentionally synchronous, but neither test
asserted the return type. Add `expect(response).not.toBeInstanceOf(Promise)`
on both branches so a regression that starts awaiting I/O updates the
synchronous call sites.
The handleGetThreadEvents/handleGetThreadState 501 tests asserted the
status code but not that intelligence stayed untouched. Stub spies for
both `listThreads` and a hypothetical `getThreadEvents`/`getThreadState`,
then assert neither was called — so a regression that drops the early
return and falls through to platform calls fails this test even after
the response code changes.
The handleSubscribeToThreads 500 test created an `errorSpy` to silence
console output but never asserted on it. Add `expect(errorSpy).toHaveBeenCalled()`
so a regression that quietly drops the diagnostic log is caught.
The InMemoryAgentRunner getThreadEvents test claimed the synthetic
terminal event was present but only asserted on TEXT_MESSAGE_*. Add an
explicit assertion on `RUN_ERROR` with `code: "INCOMPLETE_STREAM"` so
finalizeRunEvents' contract is locked in — a regression that stops
appending the synthetic event would leave the inspector showing an
in-progress thread forever.
Declare `onNewMessage` on `MessagePopulatingTestAgent.runAgent`'s options
type to match the runner's call site and `TestAgent` above. Without it,
a regression that starts depending on `onNewMessage` here would compile
cleanly even though the mock would silently drop the call.
handle-threads.test.ts:
- handleGetThreadMessages intelligence-path test now asserts the response
body verbatim, so a regression that swaps in a stub body is caught.
- 422-no-intelligence test issues a real DELETE request for the delete
path instead of cloning a POST request.
- handleClearThreads block carries a comment explaining why the handler
is intentionally synchronous (no I/O on either branch).
- The identifyUser-throws test now silences console.error for the
duration of the assertion, matching the pattern already used by the
subscribe-throws test.
in-memory-runner.test.ts:
- ThrowingAgent test asserts RUN_ERROR is the last emitted event and
that no RUN_FINISHED is emitted, locking in terminal-event semantics.
- getThreadEvents test asserts the full TEXT_MESSAGE triple is present
in the persisted event log, and the comment now reflects the real
finalizeRunEvents behaviour (it appends a synthetic terminal event
when the agent does not emit one, so terminal events ARE persisted).
- getThreadState multi-run test gains a cross-thread isolation
assertion: a snapshot on a different thread must not bleed into the
original thread's state.
- Bumped the inter-thread sort delay from 5ms to 20ms to absorb timer
jitter on slow CI runners.
- Removed four redundant `agent.agentId = "test-agent"` reassignments
(the constructor already sets it via super({ agentId })).
- Aligned MessagePopulatingTestAgent.runAgent with TestAgent: `onEvent`
is now required so the runner contract is exercised consistently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TanStack's chat() engine runs a multi-turn agent loop: after the model
returns tool calls, it tries to execute them via processToolCalls().
Frontend-only tools (like render_pie_chart) are unknown to TanStack, so
executeToolCalls() treats them as errors and buildToolResultChunks()
re-emits TOOL_CALL_END without a preceding TOOL_CALL_START. The ag-ui
verify middleware rejects this duplicate.
Fix: track a runFinished flag in convertTanStackStream and discard all
events after the first RUN_FINISHED, which marks the boundary between
the streaming pass and TanStack's internal tool execution loop.
Also adds built-in-agent to docker-compose.local.yml and local-ports.json.
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.
## Summary
- Explicitly defaults `streamSubgraphs: true` in `LangGraphAgent.run()`
forwardedProps so subagent streaming works with `@ag-ui/langgraph`
0.0.31+, which changed the default from `true` to `undefined`
- Uses nullish coalescing (`??`) so explicit user overrides (including
`false`) are preserved
- Fixes 6 showcase integrations failing E2E probes on the `subagents`
feature
## Why
`@ag-ui/langgraph` 0.0.31 removed the `?? true` fallback on
`streamSubgraphs` in `handleStreamEvents()`. The CopilotKit runtime
never explicitly set this prop, so subgraph event forwarding became
silently disabled. This caused all subagent-dependent demos to stop
working.
A previous v1 fix (commit `21e12afca`) handled this in the now-deleted
`agui-action.ts`, but the logic was lost during the v2 migration.
## Additional context
Investigation identified a second issue: `@ag-ui/langgraph` 0.0.34
includes state snapshot fixes needed for shared-state-read/write
features, but 0.0.34 is not yet published on npm (0.0.31 is latest). The
ag-ui team needs to cut a release. Full analysis: [Notion
write-up](https://app.notion.com/p/3513aa381852812a9ecef5fbf1e71739)
## Test plan
- [x] Runtime tests pass (1412/1412)
- [x] 7-agent CR converged Round 1 (0 findings)
- [ ] Verify showcase E2E probes for subagent features turn green after
merge
Express 4 does not support the {*splat} wildcard syntax introduced in
Express 5. Replace string-based wildcard patterns with RegExp routes
that work across both major versions.
The @ag-ui/langgraph 0.0.31+ changed the default for streamSubgraphs
from true to undefined, breaking subagent streaming. This enriches the
run() input's forwardedProps with streamSubgraphs: true as a default,
while preserving any explicit user override via the nullish coalescing
operator.
- mcp-clients: switch to the stable `createMCPClient` import; type
the user-managed-client compat case via
`Awaited<ReturnType<typeof createMCPClient>>`.
- mcp-servers-integration: add a `vi.spyOn(globalThis, "fetch")`
helper to read unredacted Authorization off each outbound request
(aimock's journal redacts it). Use it to assert that
- static `headers` still reach every outbound request
(initialize, tools/list, tools/call),
- per-call `getHeaders` runs on every request and is never cached,
- `getHeaders` overrides static `headers` on key collision,
- `getHeaders` errors surface as a `MCPHeaderResolverError`-tagged
RUN_ERROR.
Plus a regression test that an SSE server with static `headers`
receives them on the wire — pre-existing latent bug from when SSE
was constructed directly off `@modelcontextprotocol/sdk`.
- intelligence-mcp-helper (new): asserts that `toMCPServer()` emits
Authorization (Bearer apiKey) AND X-Cpki-User-Id (from
`agent.user`) on every outbound request, throws when no user has
been resolved, and composes the URL by appending `/mcp` to
`apiUrl` (normalizing a trailing slash).
Pre-wires both auth axes for the Intelligence /mcp endpoint:
the project Bearer (from `apiKey`) as a static header, and the
per-call X-Cpki-User-Id (read off `MCPRequestContext.user.id`,
which the runtime populates via `identifyUser` →
`resolveIntelligenceUser` → `agent.user`). User-side code never
has to know the wire-level header name.
The resolver throws if no user has been resolved for the current
run. Silent fallthrough to an empty user-id would collapse every
browser session into one shared server-side state, which is exactly
the isolation guarantee the per-call header exists to enforce.
After `resolveIntelligenceUser` runs `runtime.identifyUser` on the
incoming HTTP request, the resolved `{id, name}` is attached to the
agent so downstream MCP per-call resolvers can read it off
`MCPRequestContext.user`. Without this hop, an MCP getHeaders running
inside the agent has no way to see who the request was made for.
The user is set on the agent immediately before `agent.run()` is
invoked. `BuiltInAgent.run` then snapshots it (alongside the
forwardable request headers) so the value is stable for the lifetime
of the run, not mutable mid-stream.
`MiddlewareCapableAgent` gains an optional `user` field;
`configureAgentForRequest` stays synchronous (no second
`identifyUser` call) — the existing handler-level resolution remains
the single source of truth.
Adds the agent-side primitive that lets MCP HTTP servers receive
per-outbound-request headers — needed for things like a user-id, a
trace id, or any value that varies per agent run and must not be
cached at client construction time.
- New `MCPClientConfigHTTP.getHeaders` resolver, invoked on every
outbound HTTP request (initialize, tools/list, tools/call, reconnects).
- New `CopilotKitMCPTransport` that implements `@ai-sdk/mcp`'s
stable `MCPTransport` interface and wraps
`StreamableHTTPClientTransport` with a fetch that applies
static `headers` + per-call `getHeaders` results.
- New `MCPRequestContext` (carries `requestHeaders`, `input`,
`mcpServerUrl`, `user`) and `MCPHeaderResolverError` (RUN_ERROR
attribution when a resolver throws).
- `mcpClients` accepts any `Pick<MCPClient, "tools">` so a Vercel-built
client passes through unchanged.
- Re-exports `MCPClient` / `MCPTransport` from
`@copilotkit/runtime/v2` so consumers don't have to add
`@ai-sdk/mcp` to type their wiring.
SSE servers continue to flow through `createMCPClient` directly;
the existing per-call resolver only applies to HTTP transports.
Needed for turnIndex/sequenceIndex fixture matching in D5 multi-turn
conversations. Updated in both packages/runtime (devDep) and
showcase/scripts (dep).
Express 4.20+ uses path-to-regexp v8 which requires named wildcard
params. Bare * is no longer valid — use {*splat} syntax instead.
Updated production code (express.ts) and test (express-fetch-bridge).
Restores express >=4.20.0 override now that code is compatible.
The framework label was only meaningful for Segment's per-event
properties. Scarf receives only the event name, so the label has no
destination. Removes the `framework` option on createCopilotRuntimeHandler
and its pass-through from the hono/express adapters, plus the now-unused
setGlobalProperties/globalProperties/flattenObject on the telemetry
client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>