@copilotkit/react-core 1.56.5-canary.1777972218 → 1.57.0
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.
- package/dist/{copilotkit-BVK_b6St.mjs → copilotkit-CPe2-340.mjs} +177 -330
- package/dist/copilotkit-CPe2-340.mjs.map +1 -0
- package/dist/{copilotkit-DV9LwRgi.d.mts → copilotkit-DFaI4j2r.d.mts} +6 -52
- package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -0
- package/dist/{copilotkit-BGIsblrk.cjs → copilotkit-DGbvw8n2.cjs} +176 -335
- package/dist/copilotkit-DGbvw8n2.cjs.map +1 -0
- package/dist/{copilotkit-Bc7kZ72T.d.cts → copilotkit-Dg4r4Gi_.d.cts} +6 -52
- package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -0
- package/dist/index.cjs +5 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +5 -2
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +172 -117
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +1 -2
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +2 -2
- package/dist/v2/index.umd.js +182 -340
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +6 -5
- package/src/hooks/use-copilot-chat_internal.ts +1 -0
- package/src/v2/components/chat/CopilotChat.tsx +1 -2
- package/src/v2/components/chat/CopilotChatMessageView.tsx +13 -124
- package/src/v2/components/chat/CopilotChatView.tsx +2 -2
- package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +3 -1
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +25 -29
- package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +60 -5
- package/src/v2/components/index.ts +0 -1
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +333 -0
- package/src/v2/hooks/use-agent.tsx +116 -7
- package/src/v2/hooks/use-render-activity-message.tsx +11 -3
- package/src/v2/hooks/use-render-custom-messages.tsx +6 -1
- package/src/v2/styles/globals.css +0 -112
- package/dist/copilotkit-BGIsblrk.cjs.map +0 -1
- package/dist/copilotkit-BVK_b6St.mjs.map +0 -1
- package/dist/copilotkit-Bc7kZ72T.d.cts.map +0 -1
- package/dist/copilotkit-DV9LwRgi.d.mts.map +0 -1
- package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -265
- package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -362
- package/src/v2/components/intelligence-indicator/index.ts +0 -2
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
subscribeToAgentWithOptions: (
|
|
51
|
+
agent: AbstractAgent,
|
|
52
|
+
subscriber: any,
|
|
53
|
+
) => { unsubscribe: () => void };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let registeredAgent: CloneableAgent;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
registeredAgent = new CloneableAgent();
|
|
60
|
+
registeredAgent.agentId = "my-agent";
|
|
61
|
+
|
|
62
|
+
mockCopilotkit = {
|
|
63
|
+
getAgent: vi.fn((id: string) =>
|
|
64
|
+
id === "my-agent" ? registeredAgent : undefined,
|
|
65
|
+
),
|
|
66
|
+
runtimeUrl: "http://localhost:3000/api/copilotkit",
|
|
67
|
+
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
68
|
+
runtimeTransport: "rest",
|
|
69
|
+
headers: {},
|
|
70
|
+
agents: { "my-agent": registeredAgent },
|
|
71
|
+
subscribeToAgentWithOptions: (agent, subscriber) =>
|
|
72
|
+
agent.subscribe(subscriber),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
mockUseCopilotKit.mockReturnValue({
|
|
76
|
+
copilotkit: mockCopilotkit,
|
|
77
|
+
executingToolCallIds: new Set(),
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.restoreAllMocks();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns different agent instances for different threadIds with the same agentId", () => {
|
|
86
|
+
const agents: Record<string, AbstractAgent> = {};
|
|
87
|
+
|
|
88
|
+
function TrackerA() {
|
|
89
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
|
|
90
|
+
agents["a"] = agent;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function TrackerB() {
|
|
95
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
|
|
96
|
+
agents["b"] = agent;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
render(
|
|
101
|
+
<>
|
|
102
|
+
<TrackerA />
|
|
103
|
+
<TrackerB />
|
|
104
|
+
</>,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(agents["a"]).toBeDefined();
|
|
108
|
+
expect(agents["b"]).toBeDefined();
|
|
109
|
+
expect(agents["a"]).not.toBe(agents["b"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns the same cached instance for the same (agentId, threadId) across re-renders", () => {
|
|
113
|
+
const instances: AbstractAgent[] = [];
|
|
114
|
+
|
|
115
|
+
function Tracker() {
|
|
116
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-x" });
|
|
117
|
+
instances.push(agent);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { rerender } = render(<Tracker />);
|
|
122
|
+
rerender(<Tracker />);
|
|
123
|
+
|
|
124
|
+
expect(instances.length).toBe(2);
|
|
125
|
+
expect(instances[0]).toBe(instances[1]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns the shared registry agent when no threadId is provided (backward compat)", () => {
|
|
129
|
+
let captured: AbstractAgent | undefined;
|
|
130
|
+
|
|
131
|
+
function Tracker() {
|
|
132
|
+
const { agent } = useAgent({ agentId: "my-agent" });
|
|
133
|
+
captured = agent;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
render(<Tracker />);
|
|
138
|
+
expect(captured).toBe(registeredAgent);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("isolates messages between thread-specific agents", () => {
|
|
142
|
+
// Pre-populate the source agent so CloneableAgent.clone() copies the
|
|
143
|
+
// message into each clone — this makes cloneForThread's setMessages([])
|
|
144
|
+
// meaningful rather than vacuously true on an already-empty clone.
|
|
145
|
+
registeredAgent.addMessage({
|
|
146
|
+
id: "source-msg",
|
|
147
|
+
role: "user",
|
|
148
|
+
content: "pre-existing on source",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const agents: Record<string, AbstractAgent> = {};
|
|
152
|
+
|
|
153
|
+
function TrackerA() {
|
|
154
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
|
|
155
|
+
agents["a"] = agent;
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function TrackerB() {
|
|
160
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
|
|
161
|
+
agents["b"] = agent;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
render(
|
|
166
|
+
<>
|
|
167
|
+
<TrackerA />
|
|
168
|
+
<TrackerB />
|
|
169
|
+
</>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Both clones should start empty even though the source had a message —
|
|
173
|
+
// cloneForThread must have called setMessages([]) on each clone.
|
|
174
|
+
expect(agents["a"]!.messages).toHaveLength(0);
|
|
175
|
+
expect(agents["b"]!.messages).toHaveLength(0);
|
|
176
|
+
|
|
177
|
+
// Adding a message to thread A must not affect thread B
|
|
178
|
+
agents["a"]!.addMessage({
|
|
179
|
+
id: "msg-1",
|
|
180
|
+
role: "user",
|
|
181
|
+
content: "hello from thread A",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(agents["a"]!.messages).toHaveLength(1);
|
|
185
|
+
expect(agents["b"]!.messages).toHaveLength(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("sets threadId on cloned agents", () => {
|
|
189
|
+
const agents: Record<string, AbstractAgent> = {};
|
|
190
|
+
|
|
191
|
+
function TrackerA() {
|
|
192
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
|
|
193
|
+
agents["a"] = agent;
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function TrackerB() {
|
|
198
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
|
|
199
|
+
agents["b"] = agent;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
render(
|
|
204
|
+
<>
|
|
205
|
+
<TrackerA />
|
|
206
|
+
<TrackerB />
|
|
207
|
+
</>,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(agents["a"]!.threadId).toBe("thread-a");
|
|
211
|
+
expect(agents["b"]!.threadId).toBe("thread-b");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("invalidates stale clone when the registry agent is replaced", () => {
|
|
215
|
+
// Simulates reconnect / hot-reload: copilotkit.agents holds a new object.
|
|
216
|
+
const { result, rerender } = renderHook(
|
|
217
|
+
({ tid }: { tid: string }) =>
|
|
218
|
+
useAgent({ agentId: "my-agent", threadId: tid }),
|
|
219
|
+
{ initialProps: { tid: "thread-a" } },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const firstClone = result.current.agent;
|
|
223
|
+
expect(firstClone).not.toBe(registeredAgent); // it's a clone
|
|
224
|
+
|
|
225
|
+
// Replace the registry agent
|
|
226
|
+
const replacementAgent = new CloneableAgent();
|
|
227
|
+
replacementAgent.agentId = "my-agent";
|
|
228
|
+
|
|
229
|
+
mockCopilotkit.agents = { "my-agent": replacementAgent };
|
|
230
|
+
mockCopilotkit.getAgent.mockImplementation((id: string) =>
|
|
231
|
+
id === "my-agent" ? replacementAgent : undefined,
|
|
232
|
+
);
|
|
233
|
+
mockUseCopilotKit.mockReturnValue({
|
|
234
|
+
copilotkit: { ...mockCopilotkit },
|
|
235
|
+
executingToolCallIds: new Set(),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
rerender({ tid: "thread-a" });
|
|
239
|
+
|
|
240
|
+
const secondClone = result.current.agent;
|
|
241
|
+
expect(secondClone).not.toBe(firstClone); // stale clone was invalidated
|
|
242
|
+
expect(secondClone).not.toBe(replacementAgent); // still a clone, not the source
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("switching threadId returns a fresh clone; switching back returns the cached one", () => {
|
|
246
|
+
const { result, rerender } = renderHook(
|
|
247
|
+
({ tid }: { tid: string }) =>
|
|
248
|
+
useAgent({ agentId: "my-agent", threadId: tid }),
|
|
249
|
+
{ initialProps: { tid: "thread-a" } },
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const cloneA = result.current.agent;
|
|
253
|
+
|
|
254
|
+
rerender({ tid: "thread-b" });
|
|
255
|
+
const cloneB = result.current.agent;
|
|
256
|
+
expect(cloneB).not.toBe(cloneA);
|
|
257
|
+
|
|
258
|
+
// Switching back to thread-a should return the originally cached clone
|
|
259
|
+
rerender({ tid: "thread-a" });
|
|
260
|
+
expect(result.current.agent).toBe(cloneA);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("uses a fresh clone with correct threadId when provisional transitions to real agent", () => {
|
|
264
|
+
// Start in Disconnected state — a provisional is created
|
|
265
|
+
mockCopilotkit.runtimeConnectionStatus =
|
|
266
|
+
CopilotKitCoreRuntimeConnectionStatus.Disconnected;
|
|
267
|
+
mockCopilotkit.getAgent.mockReturnValue(undefined);
|
|
268
|
+
mockCopilotkit.agents = {};
|
|
269
|
+
mockUseCopilotKit.mockReturnValue({
|
|
270
|
+
copilotkit: { ...mockCopilotkit },
|
|
271
|
+
executingToolCallIds: new Set(),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const { result, rerender } = renderHook(() =>
|
|
275
|
+
useAgent({ agentId: "my-agent", threadId: "thread-a" }),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const provisional = result.current.agent;
|
|
279
|
+
expect(provisional.threadId).toBe("thread-a");
|
|
280
|
+
|
|
281
|
+
// Real agent appears (runtime connected and agent registered)
|
|
282
|
+
mockCopilotkit.runtimeConnectionStatus =
|
|
283
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected;
|
|
284
|
+
mockCopilotkit.getAgent.mockImplementation((id: string) =>
|
|
285
|
+
id === "my-agent" ? registeredAgent : undefined,
|
|
286
|
+
);
|
|
287
|
+
mockCopilotkit.agents = { "my-agent": registeredAgent };
|
|
288
|
+
mockUseCopilotKit.mockReturnValue({
|
|
289
|
+
copilotkit: { ...mockCopilotkit },
|
|
290
|
+
executingToolCallIds: new Set(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
rerender();
|
|
294
|
+
|
|
295
|
+
const realClone = result.current.agent;
|
|
296
|
+
expect(realClone).not.toBe(provisional); // provisional replaced by real clone
|
|
297
|
+
expect(realClone).not.toBe(registeredAgent); // it's a clone, not the source
|
|
298
|
+
expect(realClone.threadId).toBe("thread-a");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("uses composite key for provisional agents when threadId is provided", () => {
|
|
302
|
+
// Put runtime in Disconnected state so provisionals are created
|
|
303
|
+
mockCopilotkit.runtimeConnectionStatus =
|
|
304
|
+
CopilotKitCoreRuntimeConnectionStatus.Disconnected;
|
|
305
|
+
mockCopilotkit.getAgent.mockReturnValue(undefined);
|
|
306
|
+
mockCopilotkit.agents = {};
|
|
307
|
+
|
|
308
|
+
const agents: Record<string, AbstractAgent> = {};
|
|
309
|
+
|
|
310
|
+
function TrackerA() {
|
|
311
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
|
|
312
|
+
agents["a"] = agent;
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function TrackerB() {
|
|
317
|
+
const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
|
|
318
|
+
agents["b"] = agent;
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
render(
|
|
323
|
+
<>
|
|
324
|
+
<TrackerA />
|
|
325
|
+
<TrackerB />
|
|
326
|
+
</>,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(agents["a"]).not.toBe(agents["b"]);
|
|
330
|
+
expect(agents["a"]!.threadId).toBe("thread-a");
|
|
331
|
+
expect(agents["b"]!.threadId).toBe("thread-b");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
|
2
|
+
import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
|
|
2
3
|
import { useMemo, useEffect, useReducer, useRef } from "react";
|
|
3
4
|
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
|
4
5
|
import { AbstractAgent, HttpAgent } from "@ag-ui/client";
|
|
@@ -22,6 +23,7 @@ const ALL_UPDATES: UseAgentUpdate[] = [
|
|
|
22
23
|
|
|
23
24
|
export interface UseAgentProps {
|
|
24
25
|
agentId?: string;
|
|
26
|
+
threadId?: string;
|
|
25
27
|
updates?: UseAgentUpdate[];
|
|
26
28
|
/**
|
|
27
29
|
* Throttle interval (in milliseconds) for re-renders triggered by
|
|
@@ -48,7 +50,80 @@ export interface UseAgentProps {
|
|
|
48
50
|
throttleMs?: number;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Clone a registry agent for per-thread isolation.
|
|
55
|
+
* Copies agent configuration (transport, headers, etc.) but resets conversation
|
|
56
|
+
* state (messages, threadId, state) so each thread starts fresh.
|
|
57
|
+
*/
|
|
58
|
+
function cloneForThread(
|
|
59
|
+
source: AbstractAgent,
|
|
60
|
+
threadId: string,
|
|
61
|
+
headers: Record<string, string>,
|
|
62
|
+
): AbstractAgent {
|
|
63
|
+
const clone = source.clone();
|
|
64
|
+
if (clone === source) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`useAgent: ${source.constructor.name}.clone() returned the same instance. ` +
|
|
67
|
+
`clone() must return a new, independent object.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
clone.threadId = threadId;
|
|
71
|
+
clone.setMessages([]);
|
|
72
|
+
clone.setState({});
|
|
73
|
+
if (clone instanceof HttpAgent) {
|
|
74
|
+
clone.headers = { ...headers };
|
|
75
|
+
}
|
|
76
|
+
return clone;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Module-level WeakMap: registryAgent → (threadId → clone).
|
|
81
|
+
* Shared across all useAgent() calls so that every component using the same
|
|
82
|
+
* (agentId, threadId) pair receives the same agent instance. Using WeakMap
|
|
83
|
+
* ensures the clone map is garbage-collected when the registry agent is
|
|
84
|
+
* replaced (e.g. after reconnect or hot-reload).
|
|
85
|
+
*/
|
|
86
|
+
export const globalThreadCloneMap = new WeakMap<
|
|
87
|
+
AbstractAgent,
|
|
88
|
+
Map<string, AbstractAgent>
|
|
89
|
+
>();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Look up an existing per-thread clone without creating one.
|
|
93
|
+
* Returns undefined when no clone has been created yet for this pair.
|
|
94
|
+
*/
|
|
95
|
+
export function getThreadClone(
|
|
96
|
+
registryAgent: AbstractAgent | undefined | null,
|
|
97
|
+
threadId: string | undefined | null,
|
|
98
|
+
): AbstractAgent | undefined {
|
|
99
|
+
if (!registryAgent || !threadId) return undefined;
|
|
100
|
+
return globalThreadCloneMap.get(registryAgent)?.get(threadId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getOrCreateThreadClone(
|
|
104
|
+
existing: AbstractAgent,
|
|
105
|
+
threadId: string,
|
|
106
|
+
headers: Record<string, string>,
|
|
107
|
+
): AbstractAgent {
|
|
108
|
+
let byThread = globalThreadCloneMap.get(existing);
|
|
109
|
+
if (!byThread) {
|
|
110
|
+
byThread = new Map();
|
|
111
|
+
globalThreadCloneMap.set(existing, byThread);
|
|
112
|
+
}
|
|
113
|
+
const cached = byThread.get(threadId);
|
|
114
|
+
if (cached) return cached;
|
|
115
|
+
|
|
116
|
+
const clone = cloneForThread(existing, threadId, headers);
|
|
117
|
+
byThread.set(threadId, clone);
|
|
118
|
+
return clone;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useAgent({
|
|
122
|
+
agentId,
|
|
123
|
+
threadId,
|
|
124
|
+
updates,
|
|
125
|
+
throttleMs,
|
|
126
|
+
}: UseAgentProps = {}) {
|
|
52
127
|
agentId ??= DEFAULT_AGENT_ID;
|
|
53
128
|
|
|
54
129
|
const { copilotkit } = useCopilotKit();
|
|
@@ -56,6 +131,12 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
|
|
|
56
131
|
// subscribeToAgentWithOptions reads it from the core instance, but React needs the dep
|
|
57
132
|
// to know when to re-subscribe.
|
|
58
133
|
const providerThrottleMs = copilotkit.defaultThrottleMs;
|
|
134
|
+
// Fall back to the enclosing CopilotChatConfigurationProvider's threadId so
|
|
135
|
+
// that useAgent() called without explicit threadId (e.g. inside a custom
|
|
136
|
+
// message renderer) automatically uses the same per-thread clone as the
|
|
137
|
+
// CopilotChat component it lives within.
|
|
138
|
+
const chatConfig = useCopilotChatConfiguration();
|
|
139
|
+
threadId ??= chatConfig?.threadId;
|
|
59
140
|
|
|
60
141
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
61
142
|
|
|
@@ -72,11 +153,29 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
|
|
|
72
153
|
);
|
|
73
154
|
|
|
74
155
|
const agent: AbstractAgent = useMemo(() => {
|
|
156
|
+
// Use a composite key when threadId is provided so that different threads
|
|
157
|
+
// for the same agent get independent instances.
|
|
158
|
+
const cacheKey = threadId ? `${agentId}:${threadId}` : agentId;
|
|
159
|
+
|
|
75
160
|
const existing = copilotkit.getAgent(agentId);
|
|
76
161
|
if (existing) {
|
|
77
|
-
// Real agent found — clear any cached
|
|
162
|
+
// Real agent found — clear any cached provisionals for this key and the
|
|
163
|
+
// bare agentId key (handles the case where a provisional was created
|
|
164
|
+
// before threadId was available, then the component re-renders with one).
|
|
165
|
+
provisionalAgentCache.current.delete(cacheKey);
|
|
78
166
|
provisionalAgentCache.current.delete(agentId);
|
|
79
|
-
|
|
167
|
+
|
|
168
|
+
if (!threadId) {
|
|
169
|
+
// No threadId — return the shared registry agent (original behavior)
|
|
170
|
+
return existing;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// threadId provided — return the shared per-thread clone.
|
|
174
|
+
// The global WeakMap ensures all components using the same
|
|
175
|
+
// (registryAgent, threadId) pair receive the same instance, so state
|
|
176
|
+
// mutations (addMessage, setState) are visible everywhere. The WeakMap
|
|
177
|
+
// entry is GC-collected automatically when the registry agent is replaced.
|
|
178
|
+
return getOrCreateThreadClone(existing, threadId, copilotkit.headers);
|
|
80
179
|
}
|
|
81
180
|
|
|
82
181
|
const isRuntimeConfigured = copilotkit.runtimeUrl !== undefined;
|
|
@@ -89,7 +188,7 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
|
|
|
89
188
|
status === CopilotKitCoreRuntimeConnectionStatus.Connecting)
|
|
90
189
|
) {
|
|
91
190
|
// Return cached provisional if available (keeps reference stable)
|
|
92
|
-
const cached = provisionalAgentCache.current.get(
|
|
191
|
+
const cached = provisionalAgentCache.current.get(cacheKey);
|
|
93
192
|
if (cached) {
|
|
94
193
|
// Update headers on the cached agent in case they changed
|
|
95
194
|
cached.headers = { ...copilotkit.headers };
|
|
@@ -104,7 +203,10 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
|
|
|
104
203
|
});
|
|
105
204
|
// Apply current headers so runs/connects inherit them
|
|
106
205
|
provisional.headers = { ...copilotkit.headers };
|
|
107
|
-
|
|
206
|
+
if (threadId) {
|
|
207
|
+
provisional.threadId = threadId;
|
|
208
|
+
}
|
|
209
|
+
provisionalAgentCache.current.set(cacheKey, provisional);
|
|
108
210
|
return provisional;
|
|
109
211
|
}
|
|
110
212
|
|
|
@@ -117,7 +219,10 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
|
|
|
117
219
|
isRuntimeConfigured &&
|
|
118
220
|
status === CopilotKitCoreRuntimeConnectionStatus.Error
|
|
119
221
|
) {
|
|
120
|
-
|
|
222
|
+
// Cache the provisional so that dep changes while in Error state (e.g.
|
|
223
|
+
// headers update) return the same agent reference, matching the
|
|
224
|
+
// Disconnected/Connecting path and preventing spurious re-subscriptions.
|
|
225
|
+
const cached = provisionalAgentCache.current.get(cacheKey);
|
|
121
226
|
if (cached) {
|
|
122
227
|
cached.headers = { ...copilotkit.headers };
|
|
123
228
|
return cached;
|
|
@@ -129,7 +234,10 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
|
|
|
129
234
|
runtimeMode: "pending",
|
|
130
235
|
});
|
|
131
236
|
provisional.headers = { ...copilotkit.headers };
|
|
132
|
-
|
|
237
|
+
if (threadId) {
|
|
238
|
+
provisional.threadId = threadId;
|
|
239
|
+
}
|
|
240
|
+
provisionalAgentCache.current.set(cacheKey, provisional);
|
|
133
241
|
return provisional;
|
|
134
242
|
}
|
|
135
243
|
|
|
@@ -148,6 +256,7 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
|
|
|
148
256
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
149
257
|
}, [
|
|
150
258
|
agentId,
|
|
259
|
+
threadId,
|
|
151
260
|
copilotkit.agents,
|
|
152
261
|
copilotkit.runtimeConnectionStatus,
|
|
153
262
|
copilotkit.runtimeUrl,
|
|
@@ -3,10 +3,12 @@ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
|
|
3
3
|
import { useCopilotKit, useCopilotChatConfiguration } from "../providers";
|
|
4
4
|
import { useCallback, useMemo } from "react";
|
|
5
5
|
import { ReactActivityMessageRenderer } from "../types";
|
|
6
|
+
import { getThreadClone } from "./use-agent";
|
|
6
7
|
|
|
7
8
|
export function useRenderActivityMessage() {
|
|
8
9
|
const { copilotkit } = useCopilotKit();
|
|
9
|
-
const
|
|
10
|
+
const config = useCopilotChatConfiguration();
|
|
11
|
+
const agentId = config?.agentId ?? DEFAULT_AGENT_ID;
|
|
10
12
|
|
|
11
13
|
const renderers = copilotkit.renderActivityMessages;
|
|
12
14
|
|
|
@@ -50,7 +52,13 @@ export function useRenderActivityMessage() {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
const Component = renderer.render;
|
|
53
|
-
|
|
55
|
+
// Prefer the per-thread clone so that handleAction in ReactSurfaceHost
|
|
56
|
+
// calls runAgent on the same agent instance that CopilotChat renders from.
|
|
57
|
+
// Without this, button clicks accumulate messages on the registry agent
|
|
58
|
+
// while CopilotChat displays from the clone — responses appear to vanish.
|
|
59
|
+
const registryAgent = copilotkit.getAgent(agentId);
|
|
60
|
+
const agent =
|
|
61
|
+
getThreadClone(registryAgent, config?.threadId) ?? registryAgent;
|
|
54
62
|
|
|
55
63
|
return (
|
|
56
64
|
<Component
|
|
@@ -62,7 +70,7 @@ export function useRenderActivityMessage() {
|
|
|
62
70
|
/>
|
|
63
71
|
);
|
|
64
72
|
},
|
|
65
|
-
[agentId, copilotkit, findRenderer],
|
|
73
|
+
[agentId, config?.threadId, copilotkit, findRenderer],
|
|
66
74
|
);
|
|
67
75
|
|
|
68
76
|
return useMemo(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCopilotChatConfiguration, useCopilotKit } from "../providers";
|
|
2
|
+
import { getThreadClone } from "./use-agent";
|
|
2
3
|
import { ReactCustomMessageRendererPosition } from "../types/react-custom-message-renderer";
|
|
3
4
|
import { Message } from "@ag-ui/core";
|
|
4
5
|
|
|
@@ -38,7 +39,11 @@ export function useRenderCustomMessages() {
|
|
|
38
39
|
copilotkit.getRunIdForMessage(agentId, threadId, message.id) ??
|
|
39
40
|
copilotkit.getRunIdsForThread(agentId, threadId).slice(-1)[0];
|
|
40
41
|
const runId = resolvedRunId ?? `missing-run-id:${message.id}`;
|
|
41
|
-
|
|
42
|
+
// Prefer the per-thread clone so that agent.messages reflects the actual
|
|
43
|
+
// conversation state (messages live on the clone, not the registry agent).
|
|
44
|
+
// Fall back to the registry agent when no clone exists (no threadId).
|
|
45
|
+
const registryAgent = copilotkit.getAgent(agentId);
|
|
46
|
+
const agent = getThreadClone(registryAgent, threadId) ?? registryAgent;
|
|
42
47
|
if (!agent) {
|
|
43
48
|
return null;
|
|
44
49
|
}
|
|
@@ -229,115 +229,3 @@
|
|
|
229
229
|
background-repeat: no-repeat;
|
|
230
230
|
background-size: 100% 100%;
|
|
231
231
|
}
|
|
232
|
-
/*
|
|
233
|
-
* IntelligenceIndicator pill — visual styles ported from
|
|
234
|
-
* CopilotKit/Intelligence #155.
|
|
235
|
-
*
|
|
236
|
-
* Violet/indigo glassmorphism — text #5B21B6, icon #7C3AED, border
|
|
237
|
-
* #9599E0, gradient stop #EEE6FE, soft shadow #5E64AD.
|
|
238
|
-
*/
|
|
239
|
-
.cpk-intelligence-pill {
|
|
240
|
-
display: inline-flex;
|
|
241
|
-
align-items: center;
|
|
242
|
-
gap: 0.55rem;
|
|
243
|
-
margin: 0.4rem 0;
|
|
244
|
-
padding: 0.4rem 0.85rem;
|
|
245
|
-
border: 1px solid rgb(149 153 224 / 0.32);
|
|
246
|
-
border-radius: 999px;
|
|
247
|
-
background: linear-gradient(
|
|
248
|
-
135deg,
|
|
249
|
-
rgb(255 255 255 / 0.55) 0%,
|
|
250
|
-
rgb(238 230 254 / 0.55) 100%
|
|
251
|
-
);
|
|
252
|
-
-webkit-backdrop-filter: blur(14px) saturate(160%);
|
|
253
|
-
backdrop-filter: blur(14px) saturate(160%);
|
|
254
|
-
color: rgb(91 33 182 / 0.92);
|
|
255
|
-
font-size: 0.78rem;
|
|
256
|
-
font-weight: 500;
|
|
257
|
-
letter-spacing: 0.01em;
|
|
258
|
-
white-space: nowrap;
|
|
259
|
-
box-shadow:
|
|
260
|
-
0 1px 2px rgb(94 100 173 / 0.06),
|
|
261
|
-
0 8px 24px rgb(149 153 224 / 0.08),
|
|
262
|
-
inset 0 1px 0 rgb(255 255 255 / 0.6);
|
|
263
|
-
opacity: 0;
|
|
264
|
-
transform: translateY(2px);
|
|
265
|
-
animation: cpk-intelligence-pill-fade-in 280ms ease-out forwards;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
.cpk-intelligence-pill--fading {
|
|
269
|
-
/* Switching the `animation` property cancels the in-flight fade-in
|
|
270
|
-
and replaces the held final-value (opacity:1 from `forwards`) with
|
|
271
|
-
this fade-out keyframe set. Plain `opacity: 0` declarations would
|
|
272
|
-
otherwise lose to the animation's held value. */
|
|
273
|
-
animation: cpk-intelligence-pill-fade-out 480ms ease-out forwards;
|
|
274
|
-
pointer-events: none;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
.cpk-intelligence-pill__icon {
|
|
278
|
-
display: block;
|
|
279
|
-
flex-shrink: 0;
|
|
280
|
-
color: rgb(124 58 237);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
.cpk-intelligence-pill__ring {
|
|
284
|
-
stroke: currentColor;
|
|
285
|
-
stroke-dasharray: 40 16.55;
|
|
286
|
-
stroke-dashoffset: 0;
|
|
287
|
-
transform-origin: 12px 12px;
|
|
288
|
-
animation: cpk-intelligence-pill-spin 0.9s linear infinite;
|
|
289
|
-
transition: stroke-dasharray 320ms ease-out;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
.cpk-intelligence-pill__ring--done {
|
|
293
|
-
stroke-dasharray: 56.55 0;
|
|
294
|
-
animation: none;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
.cpk-intelligence-pill__check {
|
|
298
|
-
stroke: currentColor;
|
|
299
|
-
stroke-dasharray: 12;
|
|
300
|
-
stroke-dashoffset: 12;
|
|
301
|
-
/* Opacity zero hides the rounded `stroke-linecap` end-cap that would
|
|
302
|
-
otherwise leak through as a stationary dot at path end (16, 9.5)
|
|
303
|
-
during spinner phase. The `dasharray + dashoffset` zeroes the
|
|
304
|
-
visible stroke length, but the cap is drawn at the dash boundary
|
|
305
|
-
regardless, so we need opacity to fully hide it. */
|
|
306
|
-
opacity: 0;
|
|
307
|
-
transition:
|
|
308
|
-
stroke-dashoffset 320ms ease-out 200ms,
|
|
309
|
-
opacity 200ms ease-out 200ms;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
.cpk-intelligence-pill__check--shown {
|
|
313
|
-
stroke-dashoffset: 0;
|
|
314
|
-
opacity: 1;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
@keyframes cpk-intelligence-pill-fade-in {
|
|
318
|
-
from {
|
|
319
|
-
opacity: 0;
|
|
320
|
-
transform: translateY(2px);
|
|
321
|
-
}
|
|
322
|
-
to {
|
|
323
|
-
opacity: 1;
|
|
324
|
-
transform: translateY(0);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
@keyframes cpk-intelligence-pill-fade-out {
|
|
329
|
-
from {
|
|
330
|
-
opacity: 1;
|
|
331
|
-
transform: translateY(0);
|
|
332
|
-
}
|
|
333
|
-
to {
|
|
334
|
-
opacity: 0;
|
|
335
|
-
transform: translateY(-2px);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
@keyframes cpk-intelligence-pill-spin {
|
|
340
|
-
to {
|
|
341
|
-
transform: rotate(360deg);
|
|
342
|
-
}
|
|
343
|
-
}
|