@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/react-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
"untruncate-json": "^0.0.1",
|
|
74
74
|
"use-stick-to-bottom": "^1.1.1",
|
|
75
75
|
"zod-to-json-schema": "^3.24.5",
|
|
76
|
-
"@copilotkit/core": "1.
|
|
77
|
-
"@copilotkit/
|
|
78
|
-
"@copilotkit/
|
|
79
|
-
"@copilotkit/
|
|
80
|
-
"@copilotkit/web-inspector": "1.
|
|
76
|
+
"@copilotkit/core": "1.57.0",
|
|
77
|
+
"@copilotkit/shared": "1.57.0",
|
|
78
|
+
"@copilotkit/a2ui-renderer": "1.57.0",
|
|
79
|
+
"@copilotkit/runtime-client-gql": "1.57.0",
|
|
80
|
+
"@copilotkit/web-inspector": "1.57.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@tailwindcss/cli": "^4.1.11",
|
|
@@ -179,9 +179,10 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
it("does not call connectAgent when threadId matches (same agent instance, no re-render)", async () => {
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
182
|
+
// useAgent now returns a per-thread clone, so the wrapper guards via
|
|
183
|
+
// lastConnectedAgentRef: connect fires once per agent instance, not once
|
|
184
|
+
// per render. After the first connect, further re-renders with the same
|
|
185
|
+
// agent do not trigger another connect.
|
|
185
186
|
mockRuntimeConnectionStatus =
|
|
186
187
|
CopilotKitCoreRuntimeConnectionStatus.Connected;
|
|
187
188
|
mockAgent.threadId = "config-thread-id";
|
|
@@ -229,13 +230,13 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
|
|
|
229
230
|
});
|
|
230
231
|
});
|
|
231
232
|
|
|
232
|
-
it("passes
|
|
233
|
+
it("passes config threadId to useAgent", () => {
|
|
233
234
|
applyMocks();
|
|
234
235
|
|
|
235
236
|
renderHook(() => useCopilotChatInternal(), { wrapper: createWrapper() });
|
|
236
237
|
|
|
237
238
|
expect(vi.mocked(useAgent)).toHaveBeenCalledWith(
|
|
238
|
-
expect.objectContaining({
|
|
239
|
+
expect.objectContaining({ threadId: "config-thread-id" }),
|
|
239
240
|
);
|
|
240
241
|
});
|
|
241
242
|
});
|
|
@@ -339,6 +339,7 @@ export function useCopilotChatInternal({
|
|
|
339
339
|
const resolvedAgentId = existingConfig?.agentId ?? "default";
|
|
340
340
|
const { agent } = useAgent({
|
|
341
341
|
agentId: resolvedAgentId,
|
|
342
|
+
threadId: existingConfig?.threadId,
|
|
342
343
|
});
|
|
343
344
|
|
|
344
345
|
// Track the last agent instance we called connect() on. Without this,
|
|
@@ -118,6 +118,7 @@ export function CopilotChat({
|
|
|
118
118
|
|
|
119
119
|
const { agent } = useAgent({
|
|
120
120
|
agentId: resolvedAgentId,
|
|
121
|
+
threadId: resolvedThreadId,
|
|
121
122
|
throttleMs,
|
|
122
123
|
});
|
|
123
124
|
const { copilotkit } = useCopilotKit();
|
|
@@ -234,8 +235,6 @@ export function CopilotChat({
|
|
|
234
235
|
agent.abortController = connectAbortController;
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
agent.threadId = resolvedThreadId;
|
|
238
|
-
|
|
239
238
|
const connect = async (agent: AbstractAgent) => {
|
|
240
239
|
try {
|
|
241
240
|
await copilotkit.connectAgent({ agent });
|
|
@@ -22,10 +22,9 @@ import {
|
|
|
22
22
|
} from "@ag-ui/core";
|
|
23
23
|
import { twMerge } from "tailwind-merge";
|
|
24
24
|
import { useRenderActivityMessage, useRenderCustomMessages } from "../../hooks";
|
|
25
|
+
import { getThreadClone } from "../../hooks/use-agent";
|
|
25
26
|
import { useCopilotKit } from "../../providers/CopilotKitProvider";
|
|
26
27
|
import { useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider";
|
|
27
|
-
import { IntelligenceIndicator } from "../intelligence-indicator";
|
|
28
|
-
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Resolves a slot value into a { Component, slotProps } pair, handling the three
|
|
@@ -283,63 +282,25 @@ const MemoizedCustomMessage = React.memo(
|
|
|
283
282
|
message: Message;
|
|
284
283
|
position: "before" | "after";
|
|
285
284
|
}) => React.ReactElement | null;
|
|
286
|
-
stateSnapshot
|
|
287
|
-
numberOfMessagesInRun: number | undefined;
|
|
288
|
-
isInLatestRun: boolean | undefined;
|
|
289
|
-
isRunning: boolean;
|
|
285
|
+
stateSnapshot?: unknown;
|
|
290
286
|
}) {
|
|
291
287
|
return renderCustomMessage({ message, position });
|
|
292
288
|
},
|
|
293
289
|
(prevProps, nextProps) => {
|
|
294
|
-
//
|
|
295
|
-
// observe has changed. Each branch returns false to invalidate.
|
|
290
|
+
// Only re-render if the message or position changed
|
|
296
291
|
if (prevProps.message.id !== nextProps.message.id) return false;
|
|
297
292
|
if (prevProps.position !== nextProps.position) return false;
|
|
293
|
+
// Compare message content - for assistant messages this is a string, for others may differ
|
|
298
294
|
if (prevProps.message.content !== nextProps.message.content) return false;
|
|
299
295
|
if (prevProps.message.role !== nextProps.message.role) return false;
|
|
300
|
-
//
|
|
301
|
-
// JSON.stringify (state-manager already deep-copies on each call, so
|
|
302
|
-
// reference equality would never hit anyway).
|
|
296
|
+
// Compare state snapshot - custom renderers may depend on state
|
|
303
297
|
if (
|
|
304
298
|
JSON.stringify(prevProps.stateSnapshot) !==
|
|
305
299
|
JSON.stringify(nextProps.stateSnapshot)
|
|
306
300
|
)
|
|
307
301
|
return false;
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
// revoked except via clearAgentState/clearThreadState which wipe
|
|
311
|
-
// wholesale), so in practice this fires only on growth as new messages
|
|
312
|
-
// stream into the run. Slots in completed runs are unaffected — their
|
|
313
|
-
// count is stable once the run has finished.
|
|
314
|
-
if (prevProps.numberOfMessagesInRun !== nextProps.numberOfMessagesInRun)
|
|
315
|
-
return false;
|
|
316
|
-
// Re-render when this message's run stops being the latest run on the
|
|
317
|
-
// thread — e.g. a new run starts and previously-completed messages need
|
|
318
|
-
// to drop "is this the latest activity?" indicators. Only the slots
|
|
319
|
-
// belonging to the run that just lost its "latest" status are
|
|
320
|
-
// invalidated; the new run's slots and any older runs are unaffected.
|
|
321
|
-
if (prevProps.isInLatestRun !== nextProps.isInLatestRun) return false;
|
|
322
|
-
// Re-render when isRunning flips, but only for slots known to belong
|
|
323
|
-
// to the latest run (`isInLatestRun === true`). Slots whose run is
|
|
324
|
-
// older or whose run-id hasn't been recorded yet (undefined) are not
|
|
325
|
-
// invalidated on isRunning changes — this preserves the perf guarantee
|
|
326
|
-
// that completed messages skip re-renders during streaming.
|
|
327
|
-
//
|
|
328
|
-
// Renderers that gate solely on `agent.isRunning` and attach to a
|
|
329
|
-
// message added before any run started (e.g. a user message inserted
|
|
330
|
-
// before runAgent fires) won't observe the false→true transition for
|
|
331
|
-
// the first run. In practice this is rare; the canonical pattern is
|
|
332
|
-
// to gate on the slot's own runId membership (see
|
|
333
|
-
// intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx
|
|
334
|
-
// for the recommended renderer shape).
|
|
335
|
-
if (
|
|
336
|
-
nextProps.isInLatestRun &&
|
|
337
|
-
prevProps.isRunning !== nextProps.isRunning
|
|
338
|
-
)
|
|
339
|
-
return false;
|
|
340
|
-
// Note: renderCustomMessage's reference is intentionally not compared —
|
|
341
|
-
// it's a closure that changes frequently. The inputs above are the only
|
|
342
|
-
// observable signals.
|
|
302
|
+
// Note: We don't compare renderCustomMessage function reference because it changes
|
|
303
|
+
// frequently. The message and state comparison is sufficient to determine if a re-render is needed.
|
|
343
304
|
return true;
|
|
344
305
|
},
|
|
345
306
|
);
|
|
@@ -431,14 +392,18 @@ export function CopilotChatMessageView({
|
|
|
431
392
|
// Subscribe to state changes so custom message renderers re-render when state updates.
|
|
432
393
|
useEffect(() => {
|
|
433
394
|
if (!config?.agentId) return;
|
|
434
|
-
const
|
|
395
|
+
const registryAgent = copilotkit.getAgent(config.agentId);
|
|
396
|
+
// Prefer the per-thread clone so that state changes from the running agent
|
|
397
|
+
// (which is the clone, not the registry) trigger re-renders.
|
|
398
|
+
const agent =
|
|
399
|
+
getThreadClone(registryAgent, config.threadId) ?? registryAgent;
|
|
435
400
|
if (!agent) return;
|
|
436
401
|
|
|
437
402
|
const subscription = agent.subscribe({
|
|
438
403
|
onStateChanged: forceUpdate,
|
|
439
404
|
});
|
|
440
405
|
return () => subscription.unsubscribe();
|
|
441
|
-
}, [config?.agentId, copilotkit, forceUpdate]);
|
|
406
|
+
}, [config?.agentId, config?.threadId, copilotkit, forceUpdate]);
|
|
442
407
|
|
|
443
408
|
// Subscribe to interrupt element changes for in-chat rendering.
|
|
444
409
|
const [interruptElement, setInterruptElement] =
|
|
@@ -453,48 +418,6 @@ export function CopilotChatMessageView({
|
|
|
453
418
|
return () => subscription.unsubscribe();
|
|
454
419
|
}, [copilotkit]);
|
|
455
420
|
|
|
456
|
-
// Build per-run metadata once per render, not once per message. The two
|
|
457
|
-
// memo inputs MemoizedCustomMessage cares about (numberOfMessagesInRun and
|
|
458
|
-
// isInLatestRun) are both derived from the same scan of `messages` →
|
|
459
|
-
// `getRunIdForMessage`, so consolidating here turns an O(n²) per-render
|
|
460
|
-
// pass (one O(n) helper call per message) into a single O(n) pass.
|
|
461
|
-
//
|
|
462
|
-
// Re-fires whenever messages, the active agent/thread, or copilotkit
|
|
463
|
-
// changes. The internal messageToRun map can update without these props
|
|
464
|
-
// changing (a runId association lands one tick after the message is
|
|
465
|
-
// appended) — in that case the next external trigger refreshes. Note
|
|
466
|
-
// the `MemoizedCustomMessage` gate at the `isInLatestRun &&` check is
|
|
467
|
-
// strict-truthy — slots whose runId is still undefined during that
|
|
468
|
-
// pre-association window will not re-render on isRunning flips. The
|
|
469
|
-
// canonical pattern (see the IntelligenceIndicator e2e test) is to gate
|
|
470
|
-
// the renderer on the slot's own runId membership rather than `isRunning`
|
|
471
|
-
// alone, so the limitation doesn't bite typical use.
|
|
472
|
-
const runMetadata = useMemo(() => {
|
|
473
|
-
if (!config) return null;
|
|
474
|
-
const runIdByMessageId = new Map<string, string>();
|
|
475
|
-
for (const msg of messages) {
|
|
476
|
-
const r = copilotkit.getRunIdForMessage(
|
|
477
|
-
config.agentId,
|
|
478
|
-
config.threadId,
|
|
479
|
-
msg.id,
|
|
480
|
-
);
|
|
481
|
-
if (r) runIdByMessageId.set(msg.id, r);
|
|
482
|
-
}
|
|
483
|
-
const countByRunId = new Map<string, number>();
|
|
484
|
-
for (const r of runIdByMessageId.values()) {
|
|
485
|
-
countByRunId.set(r, (countByRunId.get(r) ?? 0) + 1);
|
|
486
|
-
}
|
|
487
|
-
let latestRunId: string | undefined;
|
|
488
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
489
|
-
const r = runIdByMessageId.get(messages[i]!.id);
|
|
490
|
-
if (r) {
|
|
491
|
-
latestRunId = r;
|
|
492
|
-
break;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return { runIdByMessageId, countByRunId, latestRunId };
|
|
496
|
-
}, [messages, config?.agentId, config?.threadId, copilotkit]);
|
|
497
|
-
|
|
498
421
|
// Helper to get state snapshot for a message (used for memoization)
|
|
499
422
|
const getStateSnapshotForMessage = (messageId: string): unknown => {
|
|
500
423
|
if (!config) return undefined;
|
|
@@ -619,14 +542,6 @@ export function CopilotChatMessageView({
|
|
|
619
542
|
const renderMessageBlock = (message: Message): React.ReactElement[] => {
|
|
620
543
|
const elements: (React.ReactElement | null | undefined)[] = [];
|
|
621
544
|
const stateSnapshot = getStateSnapshotForMessage(message.id);
|
|
622
|
-
const messageRunId = runMetadata?.runIdByMessageId.get(message.id);
|
|
623
|
-
const numberOfMessagesInRun = messageRunId
|
|
624
|
-
? runMetadata?.countByRunId.get(messageRunId)
|
|
625
|
-
: undefined;
|
|
626
|
-
const isInLatestRun =
|
|
627
|
-
messageRunId === undefined
|
|
628
|
-
? undefined
|
|
629
|
-
: messageRunId === runMetadata?.latestRunId;
|
|
630
545
|
|
|
631
546
|
if (renderCustomMessage) {
|
|
632
547
|
elements.push(
|
|
@@ -636,9 +551,6 @@ export function CopilotChatMessageView({
|
|
|
636
551
|
position="before"
|
|
637
552
|
renderCustomMessage={renderCustomMessage}
|
|
638
553
|
stateSnapshot={stateSnapshot}
|
|
639
|
-
numberOfMessagesInRun={numberOfMessagesInRun}
|
|
640
|
-
isInLatestRun={isInLatestRun}
|
|
641
|
-
isRunning={isRunning}
|
|
642
554
|
/>,
|
|
643
555
|
);
|
|
644
556
|
}
|
|
@@ -692,29 +604,6 @@ export function CopilotChatMessageView({
|
|
|
692
604
|
position="after"
|
|
693
605
|
renderCustomMessage={renderCustomMessage}
|
|
694
606
|
stateSnapshot={stateSnapshot}
|
|
695
|
-
numberOfMessagesInRun={numberOfMessagesInRun}
|
|
696
|
-
isInLatestRun={isInLatestRun}
|
|
697
|
-
isRunning={isRunning}
|
|
698
|
-
/>,
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Auto-mount the IntelligenceIndicator on assistant message slots
|
|
703
|
-
// when the runtime is in intelligence mode. The component self-gates
|
|
704
|
-
// further (last in run, latest run, matching tool call) so only one
|
|
705
|
-
// pill renders at a time — but mounting it only for assistant
|
|
706
|
-
// messages avoids spinning up a `useAgent` subscription, a 200 ms
|
|
707
|
-
// poll interval, and four effects on every user/reasoning/activity
|
|
708
|
-
// slot just to have it return null at the role gate.
|
|
709
|
-
if (
|
|
710
|
-
copilotkit.intelligence !== undefined &&
|
|
711
|
-
message.role === "assistant"
|
|
712
|
-
) {
|
|
713
|
-
elements.push(
|
|
714
|
-
<IntelligenceIndicator
|
|
715
|
-
key={`${message.id}-intelligence`}
|
|
716
|
-
message={message}
|
|
717
|
-
agentId={config?.agentId ?? DEFAULT_AGENT_ID}
|
|
718
607
|
/>,
|
|
719
608
|
);
|
|
720
609
|
}
|
|
@@ -89,8 +89,8 @@ export type CopilotChatViewProps = WithSlots<
|
|
|
89
89
|
/**
|
|
90
90
|
* When `true`, suppresses the welcome screen while a thread's initial
|
|
91
91
|
* connect is in flight. Prevents the "How can I help you today?" flash
|
|
92
|
-
* that would otherwise appear between mounting an empty agent
|
|
93
|
-
*
|
|
92
|
+
* that would otherwise appear between mounting an empty cloned agent and
|
|
93
|
+
* the bootstrap messages arriving from /connect.
|
|
94
94
|
*/
|
|
95
95
|
isConnecting?: boolean;
|
|
96
96
|
/**
|
|
@@ -9,7 +9,8 @@ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Mock agent that records every connectAgent() invocation and resolves
|
|
12
|
-
* immediately with an empty run result.
|
|
12
|
+
* immediately with an empty run result. Tracking lives on the class so
|
|
13
|
+
* per-thread clones (from useAgent's WeakMap) share the counter.
|
|
13
14
|
*/
|
|
14
15
|
class TrackingAgent extends MockStepwiseAgent {
|
|
15
16
|
static connectCalls: Array<{
|
|
@@ -106,6 +107,7 @@ describe("CopilotChat welcome / connect integration", () => {
|
|
|
106
107
|
expect(TrackingAgent.connectCalls.length).toBeGreaterThan(0);
|
|
107
108
|
});
|
|
108
109
|
|
|
110
|
+
// The per-thread clone carries threadId; agentId is the default.
|
|
109
111
|
expect(
|
|
110
112
|
TrackingAgent.connectCalls.some((c) => c.threadId === "real-thread"),
|
|
111
113
|
).toBe(true);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import type { AbstractAgent } from "@ag-ui/client";
|
|
5
4
|
import { EventType } from "@ag-ui/client";
|
|
6
5
|
import {
|
|
7
6
|
MockReconnectableAgent,
|
|
@@ -18,7 +17,9 @@ import {
|
|
|
18
17
|
CopilotKitProvider,
|
|
19
18
|
useCopilotKit,
|
|
20
19
|
} from "../../../providers";
|
|
20
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
21
21
|
import { IntelligenceAgent } from "@copilotkit/core";
|
|
22
|
+
import { getThreadClone } from "../../../hooks/use-agent";
|
|
22
23
|
import { createA2UIMessageRenderer } from "../../../a2ui/A2UIMessageRenderer";
|
|
23
24
|
import type { Theme } from "@copilotkit/a2ui-renderer";
|
|
24
25
|
import { CopilotChat } from "..";
|
|
@@ -305,28 +306,18 @@ describe("CopilotChat activity message rendering", () => {
|
|
|
305
306
|
expect(capturedCopilotkit).toBeDefined();
|
|
306
307
|
});
|
|
307
308
|
|
|
308
|
-
it("
|
|
309
|
-
// Regression:
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
// assertion shape — "two agents in the registry, the renderer must
|
|
318
|
-
// receive the one matching config.agentId" — is generic to that bug
|
|
319
|
-
// class. The proxy-routing-specific behavior (registering a
|
|
320
|
-
// ProxiedCopilotRuntimeAgent with `remoteAgentId !== agentId` and
|
|
321
|
-
// verifying `getAgent(agentId)` returns the proxy, not the agent at
|
|
322
|
-
// remoteAgentId) is covered by `core-register-proxied-agent.test.ts`
|
|
323
|
-
// at the registry level.
|
|
324
|
-
const localAgent = new MockStepwiseAgent();
|
|
325
|
-
localAgent.agentId = "chat-1";
|
|
326
|
-
const otherAgent = new MockStepwiseAgent();
|
|
327
|
-
otherAgent.agentId = "default";
|
|
309
|
+
it("passes the per-thread clone (not the registry agent) to activity message renderers", async () => {
|
|
310
|
+
// Regression test for: A2UI button clicks firing runAgent on the registry
|
|
311
|
+
// agent instead of the per-thread clone that CopilotChat renders from.
|
|
312
|
+
// Caused by useRenderActivityMessage calling copilotkit.getAgent() directly
|
|
313
|
+
// instead of getThreadClone(registryAgent, threadId) ?? registryAgent.
|
|
314
|
+
const agent = new MockStepwiseAgent();
|
|
315
|
+
const agentId = "action-agent";
|
|
316
|
+
agent.agentId = agentId;
|
|
317
|
+
const threadId = "thread-for-action-test";
|
|
328
318
|
|
|
329
319
|
let capturedAgent: AbstractAgent | undefined;
|
|
320
|
+
|
|
330
321
|
const activityRenderer: ReactActivityMessageRenderer<{ label: string }> = {
|
|
331
322
|
activityType: "button-action",
|
|
332
323
|
content: z.object({ label: z.string() }),
|
|
@@ -337,9 +328,9 @@ describe("CopilotChat activity message rendering", () => {
|
|
|
337
328
|
};
|
|
338
329
|
|
|
339
330
|
renderWithCopilotKit({
|
|
340
|
-
agents: {
|
|
341
|
-
agentId
|
|
342
|
-
threadId
|
|
331
|
+
agents: { [agentId]: agent },
|
|
332
|
+
agentId,
|
|
333
|
+
threadId,
|
|
343
334
|
renderActivityMessages: [activityRenderer],
|
|
344
335
|
});
|
|
345
336
|
|
|
@@ -351,22 +342,27 @@ describe("CopilotChat activity message rendering", () => {
|
|
|
351
342
|
expect(screen.getByText("show me buttons")).toBeDefined();
|
|
352
343
|
});
|
|
353
344
|
|
|
354
|
-
|
|
355
|
-
|
|
345
|
+
agent.emit(runStartedEvent());
|
|
346
|
+
agent.emit(
|
|
356
347
|
activitySnapshotEvent({
|
|
357
348
|
messageId: testId("activity-action"),
|
|
358
349
|
activityType: "button-action",
|
|
359
350
|
content: { label: "Click Me" },
|
|
360
351
|
}),
|
|
361
352
|
);
|
|
362
|
-
|
|
353
|
+
agent.emit(runFinishedEvent());
|
|
363
354
|
|
|
364
355
|
await waitFor(() => {
|
|
365
356
|
expect(screen.getByTestId("action-button")).toBeDefined();
|
|
366
357
|
});
|
|
367
358
|
|
|
368
|
-
|
|
369
|
-
|
|
359
|
+
// CopilotChat creates a per-thread clone via useAgent. The activity renderer
|
|
360
|
+
// must receive that clone so that handleAction → runAgent targets the same
|
|
361
|
+
// instance chat is rendering from.
|
|
362
|
+
const clone = getThreadClone(agent, threadId);
|
|
363
|
+
expect(clone).toBeDefined();
|
|
364
|
+
expect(capturedAgent).toBe(clone);
|
|
365
|
+
expect(capturedAgent).not.toBe(agent); // must NOT be the registry agent
|
|
370
366
|
});
|
|
371
367
|
|
|
372
368
|
it("restores a completed A2UI surface after reconnect from an event-native baseline", async () => {
|
|
@@ -49,11 +49,6 @@ class MockMCPProxyAgent extends AbstractAgent {
|
|
|
49
49
|
this.runAgentResponses.set(method, response);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
addMessage(msg: Parameters<AbstractAgent["addMessage"]>[0]) {
|
|
53
|
-
this.addMessageCalls.push(msg as any);
|
|
54
|
-
return super.addMessage(msg);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
52
|
emit(event: BaseEvent) {
|
|
58
53
|
if (event.type === EventType.RUN_STARTED) {
|
|
59
54
|
this.isRunning = true;
|
|
@@ -75,6 +70,66 @@ class MockMCPProxyAgent extends AbstractAgent {
|
|
|
75
70
|
});
|
|
76
71
|
}
|
|
77
72
|
|
|
73
|
+
clone(): MockMCPProxyAgent {
|
|
74
|
+
const cloned = new MockMCPProxyAgent();
|
|
75
|
+
cloned.agentId = this.agentId;
|
|
76
|
+
type Internal = {
|
|
77
|
+
subject: Subject<BaseEvent>;
|
|
78
|
+
runAgentCalls: Array<{ input: Partial<RunAgentInput> }>;
|
|
79
|
+
addMessageCalls: Array<{ id: string; role: string; content: string }>;
|
|
80
|
+
runAgentResponses: Map<string, unknown>;
|
|
81
|
+
};
|
|
82
|
+
(cloned as unknown as Internal).subject = (
|
|
83
|
+
this as unknown as Internal
|
|
84
|
+
).subject;
|
|
85
|
+
(cloned as unknown as Internal).runAgentCalls = (
|
|
86
|
+
this as unknown as Internal
|
|
87
|
+
).runAgentCalls;
|
|
88
|
+
(cloned as unknown as Internal).addMessageCalls = (
|
|
89
|
+
this as unknown as Internal
|
|
90
|
+
).addMessageCalls;
|
|
91
|
+
(cloned as unknown as Internal).runAgentResponses = (
|
|
92
|
+
this as unknown as Internal
|
|
93
|
+
).runAgentResponses;
|
|
94
|
+
|
|
95
|
+
const registry = this;
|
|
96
|
+
Object.defineProperty(cloned, "isRunning", {
|
|
97
|
+
get() {
|
|
98
|
+
return registry.isRunning;
|
|
99
|
+
},
|
|
100
|
+
set(v: boolean) {
|
|
101
|
+
registry.isRunning = v;
|
|
102
|
+
},
|
|
103
|
+
configurable: true,
|
|
104
|
+
enumerable: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const proto = MockMCPProxyAgent.prototype;
|
|
108
|
+
cloned.runAgent = async function (
|
|
109
|
+
input?: Partial<RunAgentInput>,
|
|
110
|
+
): Promise<RunAgentResult> {
|
|
111
|
+
const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest;
|
|
112
|
+
if (proxiedRequest) {
|
|
113
|
+
return registry.runAgent(input);
|
|
114
|
+
}
|
|
115
|
+
return proto.runAgent.call(cloned, input);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Track addMessage calls on the clone (the component uses the clone)
|
|
119
|
+
const origAddMessage = cloned.addMessage.bind(cloned);
|
|
120
|
+
cloned.addMessage = function (msg: Parameters<typeof origAddMessage>[0]) {
|
|
121
|
+
registry.addMessageCalls.push(msg as any);
|
|
122
|
+
return origAddMessage(msg);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Proxy run() calls so spies on the registry's run() see clone invocations
|
|
126
|
+
cloned.run = function (input: RunAgentInput): Observable<BaseEvent> {
|
|
127
|
+
return registry.run(input);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return cloned;
|
|
131
|
+
}
|
|
132
|
+
|
|
78
133
|
async detachActiveRun(): Promise<void> {}
|
|
79
134
|
|
|
80
135
|
run(_input: RunAgentInput): Observable<BaseEvent> {
|