Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
865991b
feat(sdk): add optional `id` parameter to `copilotkit_emit_tool_call`
mxmzb May 19, 2026
174be4f
style: auto-fix formatting
github-actions[bot] May 19, 2026
c7693e9
fix(sdk): validate id param, rename to tool_call_id, use options bag …
mxmzb May 19, 2026
d2070b7
style: auto-fix formatting
github-actions[bot] May 19, 2026
09ede29
fix(sdk): align whitespace validation, improve error handling and docs
mxmzb May 19, 2026
50301b7
fix(sdk): restore error type, rename options.id, add validation parity
mxmzb May 19, 2026
6fb417a
style: auto-fix formatting
github-actions[bot] May 19, 2026
9cb8996
fix(sdk): harden validation, error types, and dispatch safety across …
mxmzb May 19, 2026
d5cc135
style: auto-fix formatting
github-actions[bot] May 19, 2026
be9b60c
fix(sdk): harden AG-UI dispatch, add exception hierarchy, fix docstrings
mxmzb May 19, 2026
1df8a80
style: auto-fix formatting
github-actions[bot] May 19, 2026
62a3164
fix(sdk): add @returns JSDoc and compensating-END test coverage
mxmzb May 19, 2026
4a6d364
fix(sdk): harden error handling, revert args strictness, shield sleep
mxmzb May 19, 2026
e1da79a
fix(sdk): fix CancelledError swallow, dispatched_end ordering, add JS…
mxmzb May 19, 2026
4f81dc3
style: auto-fix formatting
github-actions[bot] May 19, 2026
d395501
fix(sdk): enrich JS dispatch errors, shield emit_message, standardize…
mxmzb May 19, 2026
40a4fe6
style: auto-fix formatting
github-actions[bot] May 19, 2026
90de3ce
fix(sdk): wrap dispatch errors instead of mutating, fix duplicate-END…
mxmzb May 19, 2026
f434418
fix(sdk): restore batched queue_put for atomicity, fix end_dispatched…
mxmzb May 19, 2026
570bcf4
fix(sdk): remove dead try/except in emit_message, drop unused imports…
mxmzb May 19, 2026
2169cc9
fix(sdk): widen AG-UI dispatcher args validation to match emitter con…
mxmzb May 19, 2026
c4ccf02
style: auto-fix formatting
github-actions[bot] May 19, 2026
cc69fd0
fix(sdk): add pytest-asyncio dev dependency for async test support
mxmzb May 21, 2026
c1874ed
Refresh shell docs homepage and search modal
tylerslaton May 27, 2026
dff634d
Merge branch 'main' into feature/emit-tool-call-optional-id
mxmzb May 27, 2026
ca7ed42
feat(sdk): add optional id parameter to copilotkit_emit_tool_call (#4…
mxmzb May 27, 2026
6b1747e
docs(shell-docs): add CopilotKit Skills section to build-with-agents …
onsclom May 27, 2026
3f8d2de
docs(shell-docs): remove skills table, replace with prose + link
onsclom May 27, 2026
b9514a9
fix(skills): mark showcase-demo-debugging as internal
onsclom May 27, 2026
7b14a8c
fix(shell-docs): fix build-with-agents on built-in-agent (authored mode)
onsclom May 27, 2026
94f473b
refactor(shell-docs): single source of truth for build-with-agents co…
onsclom May 27, 2026
bfbd074
fix(skills): mark showcase-demo-debugging as internal (#5040)
onsclom May 27, 2026
d0d3b64
docs(shell-docs): add CopilotKit Skills section to build-with-agents …
onsclom May 27, 2026
ac25cc3
Merge branch 'main' into tyler/showcase-improve-docs-design
tylerslaton May 27, 2026
d1506ec
fix(react-core): propagate threadId prop from CopilotKit to agent (#5…
BenTaylorDev May 27, 2026
eddff6d
test(react-core): move threadId-propagation test out of the hooks dir
BenTaylorDev May 27, 2026
f02b83c
fix(react-core): propagate threadId prop from CopilotKit to agent (#5…
BenTaylorDev May 27, 2026
5e4af9e
feat(docs): improve search design and sidebar layout (#5034)
tylerslaton May 27, 2026
7cf05df
fix(ci): exclude node_modules from prerelease workspace artifact
BenTaylorDev May 27, 2026
ee6b2c3
fix(ci): exclude node_modules from prerelease workspace artifact (#5044)
BenTaylorDev May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/showcase-demo-debugging/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
name: showcase-demo-debugging
description: Build, debug, and regression-test CopilotKit showcase demos. Use when working under showcase/, creating new showcase demos/items/cells/pills, implementing showcase features, investigating showcase demo bugs, D5/e2e-deep failures, aimock fixture gaps, direct-LLM versus aimock behavior, recording fixtures, testing every demo pill/cell, verifying repeated and interleaved suggestion-pill fixture stability, enforcing langgraph-python 1:1 parity, or migrating a demo to the v2 CopilotKit showcase flow.
metadata:
internal: true
---

# Showcase Demo Work
Expand Down
23 changes: 22 additions & 1 deletion .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,24 @@ jobs:
- name: Run tests
run: pnpm run test

# Exclude node_modules and build caches from the artifact. Including
# them produces ~6M files for this monorepo, which OOMs
# upload-artifact's Node process at its 4GB heap limit during
# enumeration (each pnpm symlink resolves to a separate entry, and
# the action holds the full manifest in memory before streaming).
# The publish job runs `pnpm install --frozen-lockfile` after
# download to restore node_modules from pnpm-lock.yaml — mirrors
# publish-release.yml.
- name: Upload workspace
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: workspace
path: .
path: |
.
!**/node_modules/**
!**/.next/**
!**/.turbo/**
!**/.nx/**
include-hidden-files: true
retention-days: 1

Expand Down Expand Up @@ -107,6 +120,14 @@ jobs:
node-version: 22.x
registry-url: https://registry.npmjs.org

# Restore node_modules — the build job excludes them from the
# uploaded workspace artifact (see "Upload workspace" above). Uses
# pnpm-lock.yaml from the artifact, so this is a deterministic
# restore of exactly what the build job ran with. `pnpm tsx` in the
# publish step below requires tsx to be installed locally.
- name: Install Dependencies
run: pnpm install --frozen-lockfile

- name: Publish prerelease
env:
NODE_AUTH_TOKEN: ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React from "react";
import { render, act } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { AbstractAgent } from "@ag-ui/client";
import { useCopilotKit } from "../v2/context";
import { useAgent } from "../v2/hooks/use-agent";
import { CopilotChatConfigurationProvider } from "../v2/providers/CopilotChatConfigurationProvider";
import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";

vi.mock("../v2/context", () => ({
useCopilotKit: vi.fn(),
}));

const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;

/**
* Contract-level regression coverage for the threadId-propagation invariant.
*
* Lives at the package root (src/__tests__) rather than next to any one
* implementation site on purpose: the previous regression (issue #5041, root
* cause shared with #4739) slipped through because the original coverage
* (use-agent-thread-isolation.test.tsx, 333 lines) lived next to the
* per-thread-cloning feature and was deleted alongside it in May 2026 when
* cloning was reverted. The threadId-propagation invariant survived the
* feature change but its tests didn't.
*
* The invariant under test: a caller-supplied threadId (via <CopilotKit>,
* <ThreadsProvider>, or <CopilotChatConfigurationProvider>) MUST end up on
* the underlying agent's `threadId` field, because ProxiedCopilotRuntimeAgent
* uses that field to address /agent/run, /agent/connect, /agent/stop. Without
* it the agent ships its own auto-minted UUID and the backend sees a different
* thread than the app code reads via useThreads/useCopilotChatConfiguration.
*
* This file should outlive any specific implementation strategy (cloning,
* effect-based sync, prop drilling, context bridge). If you change the
* mechanism, keep this contract.
*/
describe("useAgent → agent.threadId sync from chat configuration", () => {
let mockCopilotkit: {
getAgent: ReturnType<typeof vi.fn>;
runtimeUrl: string | undefined;
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
runtimeTransport: string;
headers: Record<string, string>;
agents: Record<string, AbstractAgent>;
subscribeToAgentWithOptions: (
agent: AbstractAgent,
subscriber: any,
) => { unsubscribe: () => void };
};

beforeEach(() => {
mockCopilotkit = {
getAgent: vi.fn(() => undefined),
runtimeUrl: "http://localhost:3000/api/copilotkit",
runtimeConnectionStatus:
CopilotKitCoreRuntimeConnectionStatus.Disconnected,
runtimeTransport: "rest",
headers: {},
agents: {},
subscribeToAgentWithOptions: (agent, subscriber) =>
agent.subscribe(subscriber),
};

mockUseCopilotKit.mockReturnValue({
copilotkit: mockCopilotkit,
executingToolCallIds: new Set(),
});
});

afterEach(() => {
vi.restoreAllMocks();
});

it("sets agent.threadId from CopilotChatConfigurationProvider when explicit", () => {
// The agent's threadId is mutable state outside React's render tracking,
// so we assert against the captured reference rather than rendered DOM —
// mutating agent.threadId after commit doesn't trigger a re-render, but
// downstream consumers (HTTP run/connect/stop, syncDelegate) read it
// imperatively at call time, which is the actual contract the fix repairs.
const callerThreadId = "caller-supplied-thread-id";
let capturedAgent: AbstractAgent | null = null;

function Probe() {
const { agent } = useAgent({ agentId: "test-agent" });
capturedAgent = agent;
return null;
}

render(
<CopilotChatConfigurationProvider threadId={callerThreadId}>
<Probe />
</CopilotChatConfigurationProvider>,
);

expect(capturedAgent).not.toBeNull();
expect(capturedAgent!.threadId).toBe(callerThreadId);
});

it("does NOT overwrite the auto-minted threadId when hasExplicitThreadId is false", () => {
// Mirrors the v1 CopilotKit bridge: ThreadsProvider auto-mints a UUID, the
// bridge forwards it but marks it non-explicit so downstream consumers
// don't treat the placeholder as a real backend thread.
const placeholderThreadId = "placeholder-from-threads-provider";
let capturedAgent: AbstractAgent | null = null;

function Probe() {
const { agent } = useAgent({ agentId: "test-agent" });
capturedAgent = agent;
return null;
}

render(
<CopilotChatConfigurationProvider
threadId={placeholderThreadId}
hasExplicitThreadId={false}
>
<Probe />
</CopilotChatConfigurationProvider>,
);

expect(capturedAgent).not.toBeNull();
expect(capturedAgent!.threadId).not.toBe(placeholderThreadId);
// AbstractAgent's constructor auto-mints a UUID; just confirm it's a
// non-empty string distinct from the placeholder.
expect(capturedAgent!.threadId).toBeTruthy();
});

it("re-syncs agent.threadId when the configuration's threadId changes", () => {
let capturedAgent: AbstractAgent | null = null;

function Probe() {
const { agent } = useAgent({ agentId: "test-agent" });
capturedAgent = agent;
return null;
}

const { rerender } = render(
<CopilotChatConfigurationProvider threadId="first-thread">
<Probe />
</CopilotChatConfigurationProvider>,
);

expect(capturedAgent!.threadId).toBe("first-thread");

act(() => {
rerender(
<CopilotChatConfigurationProvider threadId="second-thread">
<Probe />
</CopilotChatConfigurationProvider>,
);
});

// Same agent instance (provisional cache keeps reference stable), updated threadId
expect(capturedAgent!.threadId).toBe("second-thread");
});

it("is a no-op when no CopilotChatConfigurationProvider is in scope", () => {
// Headless / pure-v2 use without any configuration provider — useAgent
// should leave the agent's auto-minted threadId alone instead of throwing.
let capturedAgent: AbstractAgent | null = null;

function Probe() {
const { agent } = useAgent({ agentId: "test-agent" });
capturedAgent = agent;
return null;
}

expect(() => render(<Probe />)).not.toThrow();
expect(capturedAgent).not.toBeNull();
expect(capturedAgent!.threadId).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { render, screen, act } from "@testing-library/react";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { CopilotKit } from "../copilotkit";
import { useCopilotContext } from "../../../context/copilot-context";
import { useCopilotChatConfiguration } from "../../../v2";
import { useCopilotChatConfiguration, useAgent } from "../../../v2";
import type { CopilotKitProps } from "../copilotkit-props";
import type { AbstractAgent } from "@ag-ui/client";

/**
* Probe that reads hasExplicitThreadId from the CopilotChatConfigurationProvider
Expand Down Expand Up @@ -105,3 +106,66 @@ describe("v1 <CopilotKit> bridge → hasExplicitThreadId", () => {
expect(screen.getByTestId("explicit").textContent).toBe("true");
});
});

/**
* Regression coverage for issue #5041 (and #4739) at the customer-facing
* surface: a threadId prop on <CopilotKit> must end up on the underlying
* agent so that ProxiedCopilotRuntimeAgent ships it in /agent/run,
* /agent/connect, /agent/stop. The cloning-revert in May 2026 broke this
* for headless useAgent and V1 chat; CopilotChatConfigurationProvider got
* the value but useAgent never copied it down onto agent.threadId.
*/
describe("v1 <CopilotKit> → useAgent → agent.threadId", () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
warnSpy.mockRestore();
});

function AgentProbe({
onCapture,
}: {
onCapture: (agent: AbstractAgent) => void;
}) {
const { agent } = useAgent({ agentId: "default" });
React.useEffect(() => {
onCapture(agent);
});
return null;
}

it("sets agent.threadId to the explicit threadId from the <CopilotKit> prop", () => {
let captured: AbstractAgent | null = null;
render(
<CopilotKitAny publicApiKey="test-key" threadId="customer-thread-id">
<AgentProbe onCapture={(agent) => (captured = agent)} />
</CopilotKitAny>,
);

expect(captured).not.toBeNull();
expect((captured as unknown as AbstractAgent).threadId).toBe(
"customer-thread-id",
);
});

it("leaves the agent's auto-minted threadId untouched when no threadId prop is supplied", () => {
// Without an explicit threadId, the ThreadsProvider mints a placeholder
// UUID that's marked non-explicit. The agent should keep its own
// constructor-generated UUID rather than adopt the placeholder, so the
// backend isn't asked to look up a thread it never created.
let captured: AbstractAgent | null = null;
render(
<CopilotKitAny publicApiKey="test-key">
<AgentProbe onCapture={(agent) => (captured = agent)} />
</CopilotKitAny>,
);

expect(captured).not.toBeNull();
expect((captured as unknown as AbstractAgent).threadId).toBeTruthy();
expect((captured as unknown as AbstractAgent).threadId).not.toBe(
"mock-thread-id",
);
});
});
22 changes: 20 additions & 2 deletions packages/react-core/src/v2/hooks/use-agent.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useCopilotKit } from "../context";
import { useMemo, useEffect, useReducer, useRef } from "react";
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
import type { AbstractAgent } from "@ag-ui/client";
import { HttpAgent } from "@ag-ui/client";
import {
ProxiedCopilotRuntimeAgent,
CopilotKitCoreRuntimeConnectionStatus,
type SubscribeToAgentSubscriber,
} from "@copilotkit/core";
import type { SubscribeToAgentSubscriber } from "@copilotkit/core";
import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";

export enum UseAgentUpdate {
OnMessagesChanged = "OnMessagesChanged",
Expand Down Expand Up @@ -221,6 +223,22 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent, JSON.stringify(copilotkit.headers)]);

// Propagate the caller-supplied threadId from the chat configuration onto
// the agent. AbstractAgent's constructor auto-mints a UUID when no threadId
// is passed, so without this sync the agent ships its own random UUID in
// /agent/run, /agent/connect, /agent/stop — diverging from the threadId the
// app code reads via useThreads/useCopilotChatConfiguration. Gated on
// hasExplicitThreadId so a ThreadsProvider-minted placeholder UUID doesn't
// overwrite the auto-minted agent UUID (both are random and useless to the
// backend; the explicit gate keeps the agent's UUID stable across renders).
const chatConfig = useCopilotChatConfiguration();
const configThreadId = chatConfig?.threadId;
const configHasExplicitThreadId = chatConfig?.hasExplicitThreadId;
useEffect(() => {
if (!configHasExplicitThreadId || !configThreadId) return;
agent.threadId = configThreadId;
}, [agent, configThreadId, configHasExplicitThreadId]);

return {
agent,
};
Expand Down
Loading
Loading