@copilotkit/react-core 1.55.3-canary.1776260990 → 1.55.3-canary.1776979102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/{copilotkit-Dcf7Lkjb.d.mts → copilotkit-3mXoM0Hd.d.mts} +9 -36
  2. package/dist/copilotkit-3mXoM0Hd.d.mts.map +1 -0
  3. package/dist/{copilotkit-9_JxoZgF.d.cts → copilotkit-BDDjvB-p.d.cts} +9 -36
  4. package/dist/copilotkit-BDDjvB-p.d.cts.map +1 -0
  5. package/dist/{copilotkit-9l47K1Ot.cjs → copilotkit-BkcqmpWt.cjs} +163 -282
  6. package/dist/copilotkit-BkcqmpWt.cjs.map +1 -0
  7. package/dist/{copilotkit-a2pL_KeT.mjs → copilotkit-C7n8Umv9.mjs} +165 -278
  8. package/dist/copilotkit-C7n8Umv9.mjs.map +1 -0
  9. package/dist/index.cjs +4 -9
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.mts +1 -1
  13. package/dist/index.mjs +4 -9
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/index.umd.js +144 -233
  16. package/dist/index.umd.js.map +1 -1
  17. package/dist/v2/index.cjs +1 -2
  18. package/dist/v2/index.d.cts +2 -2
  19. package/dist/v2/index.d.mts +2 -2
  20. package/dist/v2/index.mjs +2 -2
  21. package/dist/v2/index.umd.js +166 -282
  22. package/dist/v2/index.umd.js.map +1 -1
  23. package/package.json +6 -6
  24. package/src/components/copilot-provider/copilot-messages.tsx +24 -39
  25. package/src/components/copilot-provider/copilotkit-props.tsx +5 -9
  26. package/src/components/copilot-provider/copilotkit.tsx +1 -4
  27. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +16 -27
  28. package/src/hooks/use-copilot-chat_internal.ts +4 -15
  29. package/src/v2/__tests__/utils/test-helpers.tsx +7 -40
  30. package/src/v2/components/chat/CopilotChat.tsx +1 -1
  31. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +15 -18
  32. package/src/v2/components/chat/CopilotChatMessageView.tsx +2 -7
  33. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +4 -17
  34. package/src/v2/components/chat/CopilotChatUserMessage.tsx +10 -13
  35. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +5 -131
  36. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +0 -60
  37. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +1 -1
  38. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +2 -5
  39. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2 -5
  40. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1 -55
  41. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +0 -8
  42. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +10 -10
  43. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +2 -13
  44. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +4 -23
  45. package/src/v2/hooks/index.ts +0 -1
  46. package/src/v2/hooks/use-agent.tsx +10 -157
  47. package/src/v2/hooks/use-interrupt.tsx +1 -10
  48. package/src/v2/hooks/use-render-activity-message.tsx +3 -9
  49. package/src/v2/hooks/use-render-custom-messages.tsx +1 -6
  50. package/src/v2/providers/CopilotKitProvider.tsx +2 -6
  51. package/dist/copilotkit-9_JxoZgF.d.cts.map +0 -1
  52. package/dist/copilotkit-9l47K1Ot.cjs.map +0 -1
  53. package/dist/copilotkit-Dcf7Lkjb.d.mts.map +0 -1
  54. package/dist/copilotkit-a2pL_KeT.mjs.map +0 -1
  55. package/src/components/copilot-provider/__tests__/error-visibility-prod.test.tsx +0 -70
  56. package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +0 -241
  57. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +0 -327
  58. package/src/v2/hooks/__tests__/use-capabilities.test.tsx +0 -76
  59. package/src/v2/hooks/use-capabilities.tsx +0 -25
@@ -1,70 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { ErrorVisibility } from "@copilotkit/shared";
3
- import { getErrorSuppression } from "../copilot-messages";
4
-
5
- /**
6
- * Regression tests for #2431: error visibility when showDevConsole=false.
7
- *
8
- * The bug: `routeError` returned early for ALL errors when `isDev` was false,
9
- * suppressing TOAST and BANNER errors that should always reach the user.
10
- *
11
- * The fix: only SILENT and DEV_ONLY errors are suppressed in production;
12
- * TOAST, BANNER, and untagged errors are always surfaced.
13
- */
14
- describe("getErrorSuppression — error visibility routing (#2431)", () => {
15
- // --- Production (isDev = false) ---
16
-
17
- it("surfaces TOAST errors in production", () => {
18
- expect(getErrorSuppression(ErrorVisibility.TOAST, false)).toBeNull();
19
- });
20
-
21
- it("surfaces BANNER errors in production", () => {
22
- expect(getErrorSuppression(ErrorVisibility.BANNER, false)).toBeNull();
23
- });
24
-
25
- it("suppresses DEV_ONLY errors in production", () => {
26
- expect(getErrorSuppression(ErrorVisibility.DEV_ONLY, false)).not.toBeNull();
27
- });
28
-
29
- it("suppresses SILENT errors in production", () => {
30
- expect(getErrorSuppression(ErrorVisibility.SILENT, false)).not.toBeNull();
31
- });
32
-
33
- it("surfaces errors with no visibility tag in production", () => {
34
- expect(getErrorSuppression(undefined, false)).toBeNull();
35
- });
36
-
37
- // --- Development (isDev = true) ---
38
-
39
- it("surfaces TOAST errors in development", () => {
40
- expect(getErrorSuppression(ErrorVisibility.TOAST, true)).toBeNull();
41
- });
42
-
43
- it("surfaces BANNER errors in development", () => {
44
- expect(getErrorSuppression(ErrorVisibility.BANNER, true)).toBeNull();
45
- });
46
-
47
- it("surfaces DEV_ONLY errors in development", () => {
48
- expect(getErrorSuppression(ErrorVisibility.DEV_ONLY, true)).toBeNull();
49
- });
50
-
51
- it("suppresses SILENT errors in development", () => {
52
- expect(getErrorSuppression(ErrorVisibility.SILENT, true)).not.toBeNull();
53
- });
54
-
55
- it("surfaces errors with no visibility tag in development", () => {
56
- expect(getErrorSuppression(undefined, true)).toBeNull();
57
- });
58
-
59
- // --- Log prefix strings ---
60
-
61
- it("returns a 'Silent Error' prefix for SILENT visibility", () => {
62
- const prefix = getErrorSuppression(ErrorVisibility.SILENT, false);
63
- expect(prefix).toContain("Silent");
64
- });
65
-
66
- it("returns a 'hidden in production' prefix for DEV_ONLY visibility", () => {
67
- const prefix = getErrorSuppression(ErrorVisibility.DEV_ONLY, false);
68
- expect(prefix).toContain("hidden in production");
69
- });
70
- });
@@ -1,241 +0,0 @@
1
- import React from "react";
2
- import { render, screen, fireEvent, waitFor } from "@testing-library/react";
3
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
4
- import { CopilotChatAssistantMessage } from "../CopilotChatAssistantMessage";
5
- import { CopilotChatUserMessage } from "../CopilotChatUserMessage";
6
- import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
7
- import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
8
- import { AssistantMessage, UserMessage } from "@ag-ui/core";
9
-
10
- const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
11
- <CopilotKitProvider>
12
- <CopilotChatConfigurationProvider threadId="test-thread">
13
- {children}
14
- </CopilotChatConfigurationProvider>
15
- </CopilotKitProvider>
16
- );
17
-
18
- const createAssistantMessage = (content: string): AssistantMessage => ({
19
- id: "msg-assistant-1",
20
- role: "assistant",
21
- content,
22
- });
23
-
24
- const createUserMessage = (content: string): UserMessage => ({
25
- id: "msg-user-1",
26
- role: "user",
27
- content,
28
- });
29
-
30
- describe("CopyButton clipboard behavior", () => {
31
- let originalClipboard: Clipboard;
32
-
33
- beforeEach(() => {
34
- originalClipboard = navigator.clipboard;
35
- });
36
-
37
- afterEach(() => {
38
- Object.defineProperty(navigator, "clipboard", {
39
- value: originalClipboard,
40
- writable: true,
41
- configurable: true,
42
- });
43
- });
44
-
45
- describe("AssistantMessage CopyButton", () => {
46
- it("shows copied state only after successful clipboard write", async () => {
47
- const writeTextMock = vi.fn().mockResolvedValue(undefined);
48
- Object.defineProperty(navigator, "clipboard", {
49
- value: { writeText: writeTextMock },
50
- writable: true,
51
- configurable: true,
52
- });
53
-
54
- const message = createAssistantMessage("Hello assistant");
55
- render(
56
- <TestWrapper>
57
- <CopilotChatAssistantMessage message={message} />
58
- </TestWrapper>,
59
- );
60
-
61
- const copyButton = screen.getByTestId("copilot-copy-button");
62
- fireEvent.click(copyButton);
63
-
64
- await waitFor(() => {
65
- expect(writeTextMock).toHaveBeenCalledWith("Hello assistant");
66
- });
67
-
68
- // After successful write, should show check icon
69
- await waitFor(() => {
70
- const checkIcon = copyButton.querySelector(".lucide-check");
71
- expect(checkIcon).not.toBeNull();
72
- });
73
- });
74
-
75
- it("does NOT show copied state when clipboard API is unavailable", async () => {
76
- Object.defineProperty(navigator, "clipboard", {
77
- value: undefined,
78
- writable: true,
79
- configurable: true,
80
- });
81
-
82
- const message = createAssistantMessage("Hello assistant");
83
- render(
84
- <TestWrapper>
85
- <CopilotChatAssistantMessage message={message} />
86
- </TestWrapper>,
87
- );
88
-
89
- const copyButton = screen.getByTestId("copilot-copy-button");
90
- fireEvent.click(copyButton);
91
-
92
- // Wait a tick for any async handlers
93
- await new Promise((r) => setTimeout(r, 50));
94
-
95
- // Should still show copy icon (not check icon)
96
- const checkIcon = copyButton.querySelector(".lucide-check");
97
- expect(checkIcon).toBeNull();
98
- const copyIcon = copyButton.querySelector(".lucide-copy");
99
- expect(copyIcon).not.toBeNull();
100
- });
101
-
102
- it("logs error when clipboard write rejects", async () => {
103
- const writeTextMock = vi
104
- .fn()
105
- .mockRejectedValue(new Error("Permission denied"));
106
- Object.defineProperty(navigator, "clipboard", {
107
- value: { writeText: writeTextMock },
108
- writable: true,
109
- configurable: true,
110
- });
111
-
112
- const consoleSpy = vi
113
- .spyOn(console, "error")
114
- .mockImplementation(() => {});
115
-
116
- const message = createAssistantMessage("Hello assistant");
117
- render(
118
- <TestWrapper>
119
- <CopilotChatAssistantMessage message={message} />
120
- </TestWrapper>,
121
- );
122
-
123
- const copyButton = screen.getByTestId("copilot-copy-button");
124
- fireEvent.click(copyButton);
125
-
126
- await waitFor(() => {
127
- expect(writeTextMock).toHaveBeenCalled();
128
- });
129
-
130
- await waitFor(() => {
131
- expect(consoleSpy).toHaveBeenCalledWith(
132
- "Failed to copy to clipboard:",
133
- expect.any(Error),
134
- );
135
- });
136
-
137
- // Should NOT show copied state when write failed
138
- const checkIcon = copyButton.querySelector(".lucide-check");
139
- expect(checkIcon).toBeNull();
140
-
141
- consoleSpy.mockRestore();
142
- });
143
- });
144
-
145
- describe("UserMessage CopyButton", () => {
146
- it("shows copied state only after successful clipboard write", async () => {
147
- const writeTextMock = vi.fn().mockResolvedValue(undefined);
148
- Object.defineProperty(navigator, "clipboard", {
149
- value: { writeText: writeTextMock },
150
- writable: true,
151
- configurable: true,
152
- });
153
-
154
- const message = createUserMessage("Hello user");
155
- render(
156
- <TestWrapper>
157
- <CopilotChatUserMessage message={message} />
158
- </TestWrapper>,
159
- );
160
-
161
- const copyButton = screen.getByTestId("copilot-user-copy-button");
162
- fireEvent.click(copyButton);
163
-
164
- await waitFor(() => {
165
- expect(writeTextMock).toHaveBeenCalledWith("Hello user");
166
- });
167
-
168
- await waitFor(() => {
169
- const checkIcon = copyButton.querySelector(".lucide-check");
170
- expect(checkIcon).not.toBeNull();
171
- });
172
- });
173
-
174
- it("does NOT show copied state when clipboard API is unavailable", async () => {
175
- Object.defineProperty(navigator, "clipboard", {
176
- value: undefined,
177
- writable: true,
178
- configurable: true,
179
- });
180
-
181
- const message = createUserMessage("Hello user");
182
- render(
183
- <TestWrapper>
184
- <CopilotChatUserMessage message={message} />
185
- </TestWrapper>,
186
- );
187
-
188
- const copyButton = screen.getByTestId("copilot-user-copy-button");
189
- fireEvent.click(copyButton);
190
-
191
- await new Promise((r) => setTimeout(r, 50));
192
-
193
- const checkIcon = copyButton.querySelector(".lucide-check");
194
- expect(checkIcon).toBeNull();
195
- const copyIcon = copyButton.querySelector(".lucide-copy");
196
- expect(copyIcon).not.toBeNull();
197
- });
198
-
199
- it("logs error when clipboard write rejects", async () => {
200
- const writeTextMock = vi
201
- .fn()
202
- .mockRejectedValue(new Error("Permission denied"));
203
- Object.defineProperty(navigator, "clipboard", {
204
- value: { writeText: writeTextMock },
205
- writable: true,
206
- configurable: true,
207
- });
208
-
209
- const consoleSpy = vi
210
- .spyOn(console, "error")
211
- .mockImplementation(() => {});
212
-
213
- const message = createUserMessage("Hello user");
214
- render(
215
- <TestWrapper>
216
- <CopilotChatUserMessage message={message} />
217
- </TestWrapper>,
218
- );
219
-
220
- const copyButton = screen.getByTestId("copilot-user-copy-button");
221
- fireEvent.click(copyButton);
222
-
223
- await waitFor(() => {
224
- expect(writeTextMock).toHaveBeenCalled();
225
- });
226
-
227
- await waitFor(() => {
228
- expect(consoleSpy).toHaveBeenCalledWith(
229
- "Failed to copy to clipboard:",
230
- expect.any(Error),
231
- );
232
- });
233
-
234
- // Should NOT show copied state when write failed
235
- const checkIcon = copyButton.querySelector(".lucide-check");
236
- expect(checkIcon).toBeNull();
237
-
238
- consoleSpy.mockRestore();
239
- });
240
- });
241
- });
@@ -1,327 +0,0 @@
1
- import React from "react";
2
- import { render } from "@testing-library/react";
3
- import { renderHook } from "@testing-library/react";
4
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
- import {
6
- AbstractAgent,
7
- type BaseEvent,
8
- type RunAgentInput,
9
- } from "@ag-ui/client";
10
- import { useCopilotKit } from "../../providers/CopilotKitProvider";
11
- import { useAgent } from "../use-agent";
12
- import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
13
- import { Observable } from "rxjs";
14
-
15
- vi.mock("../../providers/CopilotKitProvider", () => ({
16
- useCopilotKit: vi.fn(),
17
- }));
18
-
19
- const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
20
-
21
- /**
22
- * A minimal mock agent whose clone() returns a NEW instance and copies
23
- * messages from the source. This is essential for testing per-thread
24
- * isolation — each clone must be a distinct object that starts with the
25
- * source's state so that cloneForThread's setMessages([]) / setState({})
26
- * calls are meaningful (not vacuously true on an already-empty clone).
27
- */
28
- class CloneableAgent extends AbstractAgent {
29
- clone(): CloneableAgent {
30
- const cloned = new CloneableAgent();
31
- cloned.agentId = this.agentId;
32
- // Copy messages so cloneForThread's setMessages([]) actually clears state
33
- cloned.setMessages([...this.messages]);
34
- return cloned;
35
- }
36
-
37
- run(_input: RunAgentInput): Observable<BaseEvent> {
38
- return new Observable();
39
- }
40
- }
41
-
42
- describe("useAgent thread isolation", () => {
43
- let mockCopilotkit: {
44
- getAgent: ReturnType<typeof vi.fn>;
45
- runtimeUrl: string | undefined;
46
- runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
47
- runtimeTransport: string;
48
- headers: Record<string, string>;
49
- agents: Record<string, AbstractAgent>;
50
- };
51
-
52
- let registeredAgent: CloneableAgent;
53
-
54
- beforeEach(() => {
55
- registeredAgent = new CloneableAgent();
56
- registeredAgent.agentId = "my-agent";
57
-
58
- mockCopilotkit = {
59
- getAgent: vi.fn((id: string) =>
60
- id === "my-agent" ? registeredAgent : undefined,
61
- ),
62
- runtimeUrl: "http://localhost:3000/api/copilotkit",
63
- runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
64
- runtimeTransport: "rest",
65
- headers: {},
66
- agents: { "my-agent": registeredAgent },
67
- };
68
-
69
- mockUseCopilotKit.mockReturnValue({
70
- copilotkit: mockCopilotkit,
71
- executingToolCallIds: new Set(),
72
- });
73
- });
74
-
75
- afterEach(() => {
76
- vi.restoreAllMocks();
77
- });
78
-
79
- it("returns different agent instances for different threadIds with the same agentId", () => {
80
- const agents: Record<string, AbstractAgent> = {};
81
-
82
- function TrackerA() {
83
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
84
- agents["a"] = agent;
85
- return null;
86
- }
87
-
88
- function TrackerB() {
89
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
90
- agents["b"] = agent;
91
- return null;
92
- }
93
-
94
- render(
95
- <>
96
- <TrackerA />
97
- <TrackerB />
98
- </>,
99
- );
100
-
101
- expect(agents["a"]).toBeDefined();
102
- expect(agents["b"]).toBeDefined();
103
- expect(agents["a"]).not.toBe(agents["b"]);
104
- });
105
-
106
- it("returns the same cached instance for the same (agentId, threadId) across re-renders", () => {
107
- const instances: AbstractAgent[] = [];
108
-
109
- function Tracker() {
110
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-x" });
111
- instances.push(agent);
112
- return null;
113
- }
114
-
115
- const { rerender } = render(<Tracker />);
116
- rerender(<Tracker />);
117
-
118
- expect(instances.length).toBe(2);
119
- expect(instances[0]).toBe(instances[1]);
120
- });
121
-
122
- it("returns the shared registry agent when no threadId is provided (backward compat)", () => {
123
- let captured: AbstractAgent | undefined;
124
-
125
- function Tracker() {
126
- const { agent } = useAgent({ agentId: "my-agent" });
127
- captured = agent;
128
- return null;
129
- }
130
-
131
- render(<Tracker />);
132
- expect(captured).toBe(registeredAgent);
133
- });
134
-
135
- it("isolates messages between thread-specific agents", () => {
136
- // Pre-populate the source agent so CloneableAgent.clone() copies the
137
- // message into each clone — this makes cloneForThread's setMessages([])
138
- // meaningful rather than vacuously true on an already-empty clone.
139
- registeredAgent.addMessage({
140
- id: "source-msg",
141
- role: "user",
142
- content: "pre-existing on source",
143
- });
144
-
145
- const agents: Record<string, AbstractAgent> = {};
146
-
147
- function TrackerA() {
148
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
149
- agents["a"] = agent;
150
- return null;
151
- }
152
-
153
- function TrackerB() {
154
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
155
- agents["b"] = agent;
156
- return null;
157
- }
158
-
159
- render(
160
- <>
161
- <TrackerA />
162
- <TrackerB />
163
- </>,
164
- );
165
-
166
- // Both clones should start empty even though the source had a message —
167
- // cloneForThread must have called setMessages([]) on each clone.
168
- expect(agents["a"]!.messages).toHaveLength(0);
169
- expect(agents["b"]!.messages).toHaveLength(0);
170
-
171
- // Adding a message to thread A must not affect thread B
172
- agents["a"]!.addMessage({
173
- id: "msg-1",
174
- role: "user",
175
- content: "hello from thread A",
176
- });
177
-
178
- expect(agents["a"]!.messages).toHaveLength(1);
179
- expect(agents["b"]!.messages).toHaveLength(0);
180
- });
181
-
182
- it("sets threadId on cloned agents", () => {
183
- const agents: Record<string, AbstractAgent> = {};
184
-
185
- function TrackerA() {
186
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
187
- agents["a"] = agent;
188
- return null;
189
- }
190
-
191
- function TrackerB() {
192
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
193
- agents["b"] = agent;
194
- return null;
195
- }
196
-
197
- render(
198
- <>
199
- <TrackerA />
200
- <TrackerB />
201
- </>,
202
- );
203
-
204
- expect(agents["a"]!.threadId).toBe("thread-a");
205
- expect(agents["b"]!.threadId).toBe("thread-b");
206
- });
207
-
208
- it("invalidates stale clone when the registry agent is replaced", () => {
209
- // Simulates reconnect / hot-reload: copilotkit.agents holds a new object.
210
- const { result, rerender } = renderHook(
211
- ({ tid }: { tid: string }) =>
212
- useAgent({ agentId: "my-agent", threadId: tid }),
213
- { initialProps: { tid: "thread-a" } },
214
- );
215
-
216
- const firstClone = result.current.agent;
217
- expect(firstClone).not.toBe(registeredAgent); // it's a clone
218
-
219
- // Replace the registry agent
220
- const replacementAgent = new CloneableAgent();
221
- replacementAgent.agentId = "my-agent";
222
-
223
- mockCopilotkit.agents = { "my-agent": replacementAgent };
224
- mockCopilotkit.getAgent.mockImplementation((id: string) =>
225
- id === "my-agent" ? replacementAgent : undefined,
226
- );
227
- mockUseCopilotKit.mockReturnValue({
228
- copilotkit: { ...mockCopilotkit },
229
- executingToolCallIds: new Set(),
230
- });
231
-
232
- rerender({ tid: "thread-a" });
233
-
234
- const secondClone = result.current.agent;
235
- expect(secondClone).not.toBe(firstClone); // stale clone was invalidated
236
- expect(secondClone).not.toBe(replacementAgent); // still a clone, not the source
237
- });
238
-
239
- it("switching threadId returns a fresh clone; switching back returns the cached one", () => {
240
- const { result, rerender } = renderHook(
241
- ({ tid }: { tid: string }) =>
242
- useAgent({ agentId: "my-agent", threadId: tid }),
243
- { initialProps: { tid: "thread-a" } },
244
- );
245
-
246
- const cloneA = result.current.agent;
247
-
248
- rerender({ tid: "thread-b" });
249
- const cloneB = result.current.agent;
250
- expect(cloneB).not.toBe(cloneA);
251
-
252
- // Switching back to thread-a should return the originally cached clone
253
- rerender({ tid: "thread-a" });
254
- expect(result.current.agent).toBe(cloneA);
255
- });
256
-
257
- it("uses a fresh clone with correct threadId when provisional transitions to real agent", () => {
258
- // Start in Disconnected state — a provisional is created
259
- mockCopilotkit.runtimeConnectionStatus =
260
- CopilotKitCoreRuntimeConnectionStatus.Disconnected;
261
- mockCopilotkit.getAgent.mockReturnValue(undefined);
262
- mockCopilotkit.agents = {};
263
- mockUseCopilotKit.mockReturnValue({
264
- copilotkit: { ...mockCopilotkit },
265
- executingToolCallIds: new Set(),
266
- });
267
-
268
- const { result, rerender } = renderHook(() =>
269
- useAgent({ agentId: "my-agent", threadId: "thread-a" }),
270
- );
271
-
272
- const provisional = result.current.agent;
273
- expect(provisional.threadId).toBe("thread-a");
274
-
275
- // Real agent appears (runtime connected and agent registered)
276
- mockCopilotkit.runtimeConnectionStatus =
277
- CopilotKitCoreRuntimeConnectionStatus.Connected;
278
- mockCopilotkit.getAgent.mockImplementation((id: string) =>
279
- id === "my-agent" ? registeredAgent : undefined,
280
- );
281
- mockCopilotkit.agents = { "my-agent": registeredAgent };
282
- mockUseCopilotKit.mockReturnValue({
283
- copilotkit: { ...mockCopilotkit },
284
- executingToolCallIds: new Set(),
285
- });
286
-
287
- rerender();
288
-
289
- const realClone = result.current.agent;
290
- expect(realClone).not.toBe(provisional); // provisional replaced by real clone
291
- expect(realClone).not.toBe(registeredAgent); // it's a clone, not the source
292
- expect(realClone.threadId).toBe("thread-a");
293
- });
294
-
295
- it("uses composite key for provisional agents when threadId is provided", () => {
296
- // Put runtime in Disconnected state so provisionals are created
297
- mockCopilotkit.runtimeConnectionStatus =
298
- CopilotKitCoreRuntimeConnectionStatus.Disconnected;
299
- mockCopilotkit.getAgent.mockReturnValue(undefined);
300
- mockCopilotkit.agents = {};
301
-
302
- const agents: Record<string, AbstractAgent> = {};
303
-
304
- function TrackerA() {
305
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
306
- agents["a"] = agent;
307
- return null;
308
- }
309
-
310
- function TrackerB() {
311
- const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
312
- agents["b"] = agent;
313
- return null;
314
- }
315
-
316
- render(
317
- <>
318
- <TrackerA />
319
- <TrackerB />
320
- </>,
321
- );
322
-
323
- expect(agents["a"]).not.toBe(agents["b"]);
324
- expect(agents["a"]!.threadId).toBe("thread-a");
325
- expect(agents["b"]!.threadId).toBe("thread-b");
326
- });
327
- });