@copilotkit/react-core 1.55.0-next.8 → 1.55.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/CHANGELOG.md +48 -5
- package/dist/{copilotkit-DNYSFuz5.mjs → copilotkit-BY5S1-0P.mjs} +2772 -858
- package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
- package/dist/{copilotkit-Dy5w3qEV.d.mts → copilotkit-BuhSUZHb.d.mts} +230 -17
- package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
- package/dist/{copilotkit-B3Mb1yVE.cjs → copilotkit-Bz5-ImDl.cjs} +2776 -832
- package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
- package/dist/{copilotkit-DBzgOMby.d.cts → copilotkit-dwDWYpya.d.cts} +230 -17
- package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
- package/dist/index.cjs +9 -4
- 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 +9 -4
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1624 -396
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +13 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +3 -3
- package/dist/v2/index.d.mts +3 -3
- package/dist/v2/index.mjs +3 -2
- package/dist/v2/index.umd.js +2746 -790
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +62 -54
- package/scripts/scope-preflight.mjs +1 -2
- package/src/components/CopilotListeners.tsx +41 -8
- package/src/components/copilot-provider/__tests__/copilot-messages-key.test.tsx +92 -0
- package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
- package/src/components/copilot-provider/copilotkit.tsx +3 -3
- package/src/components/toast/toast-provider.tsx +269 -194
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +27 -16
- package/src/hooks/use-copilot-chat_internal.ts +15 -4
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
- package/src/v2/__tests__/utils/test-helpers.tsx +107 -7
- package/src/v2/a2ui/A2UICatalogContext.tsx +79 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +125 -37
- package/src/v2/a2ui/A2UIToolCallRenderer.tsx +290 -0
- package/src/v2/components/CopilotKitInspector.tsx +2 -0
- package/src/v2/components/OpenGenerativeUIRenderer.tsx +598 -0
- package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +665 -0
- package/src/v2/components/chat/CopilotChat.tsx +197 -52
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +17 -2
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +481 -0
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +139 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +146 -77
- package/src/v2/components/chat/CopilotChatMessageView.tsx +260 -151
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +1 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +54 -0
- package/src/v2/components/chat/CopilotChatView.tsx +179 -66
- package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +168 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +63 -2
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +544 -1
- package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +268 -0
- package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +249 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +5 -2
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +5 -2
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +60 -3
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +138 -0
- package/src/v2/components/chat/index.ts +9 -0
- package/src/v2/components/chat/scroll-element-context.ts +13 -0
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +8 -0
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +327 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +13 -2
- package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +23 -4
- package/src/v2/hooks/__tests__/use-threads.test.tsx +54 -0
- package/src/v2/hooks/index.ts +5 -0
- package/src/v2/hooks/use-agent.tsx +220 -15
- package/src/v2/hooks/use-attachments.tsx +269 -0
- package/src/v2/hooks/use-frontend-tool.tsx +5 -2
- package/src/v2/hooks/use-render-activity-message.tsx +9 -2
- package/src/v2/hooks/use-render-custom-messages.tsx +6 -1
- package/src/v2/hooks/use-threads.tsx +35 -15
- package/src/v2/index.ts +5 -1
- package/src/v2/lib/__tests__/processPartialHtml.test.ts +112 -0
- package/src/v2/lib/__tests__/slots.test.ts +56 -0
- package/src/v2/lib/processPartialHtml.ts +45 -0
- package/src/v2/lib/slots.tsx +42 -1
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +9 -3
- package/src/v2/providers/CopilotKitProvider.tsx +268 -32
- package/src/v2/providers/SandboxFunctionsContext.ts +10 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +198 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +71 -0
- package/src/v2/providers/index.ts +7 -0
- package/src/v2/styles/globals.css +2 -1
- package/src/v2/types/index.ts +1 -0
- package/src/v2/types/sandbox-function.ts +11 -0
- package/dist/copilotkit-B3Mb1yVE.cjs.map +0 -1
- package/dist/copilotkit-DBzgOMby.d.cts.map +0 -1
- package/dist/copilotkit-DNYSFuz5.mjs.map +0 -1
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +0 -1
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +0 -46
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
4
|
+
import { CopilotChat } from "../CopilotChat";
|
|
5
|
+
import { useAgent } from "../../../hooks/use-agent";
|
|
6
|
+
import { useCopilotKit } from "../../../providers/CopilotKitProvider";
|
|
7
|
+
import { useCopilotChatConfiguration } from "../../../providers/CopilotChatConfigurationProvider";
|
|
8
|
+
import { MockStepwiseAgent } from "../../../__tests__/utils/test-helpers";
|
|
9
|
+
import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
|
|
10
|
+
|
|
11
|
+
// Mock useAgent to inspect the props it receives
|
|
12
|
+
vi.mock("../../../hooks/use-agent", () => ({
|
|
13
|
+
useAgent: vi.fn(() => ({
|
|
14
|
+
agent: new MockStepwiseAgent(),
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("../../../providers/CopilotKitProvider", () => ({
|
|
19
|
+
useCopilotKit: vi.fn(),
|
|
20
|
+
useLicenseContext: vi.fn(() => ({
|
|
21
|
+
checkFeature: () => true,
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock(
|
|
26
|
+
"../../../providers/CopilotChatConfigurationProvider",
|
|
27
|
+
async (importOriginal) => {
|
|
28
|
+
const actual =
|
|
29
|
+
await importOriginal<
|
|
30
|
+
typeof import("../../../providers/CopilotChatConfigurationProvider")
|
|
31
|
+
>();
|
|
32
|
+
return {
|
|
33
|
+
...actual,
|
|
34
|
+
useCopilotChatConfiguration: vi.fn(() => undefined),
|
|
35
|
+
CopilotChatConfigurationProvider: ({
|
|
36
|
+
children,
|
|
37
|
+
}: {
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
}) => <>{children}</>,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Mock suggestions hook
|
|
45
|
+
vi.mock("../../../hooks/use-suggestions", () => ({
|
|
46
|
+
useSuggestions: vi.fn(() => ({ suggestions: [] })),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// Mock attachments hook
|
|
50
|
+
vi.mock("../../../hooks/use-attachments", () => ({
|
|
51
|
+
useAttachments: vi.fn(() => ({
|
|
52
|
+
attachments: [],
|
|
53
|
+
enabled: false,
|
|
54
|
+
dragOver: false,
|
|
55
|
+
fileInputRef: { current: null },
|
|
56
|
+
containerRef: { current: null },
|
|
57
|
+
handleFileUpload: vi.fn(),
|
|
58
|
+
handleDragOver: vi.fn(),
|
|
59
|
+
handleDragLeave: vi.fn(),
|
|
60
|
+
handleDrop: vi.fn(),
|
|
61
|
+
removeAttachment: vi.fn(),
|
|
62
|
+
consumeAttachments: vi.fn(() => []),
|
|
63
|
+
})),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const mockUseAgent = useAgent as ReturnType<typeof vi.fn>;
|
|
67
|
+
const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
|
|
68
|
+
|
|
69
|
+
/** Factory for the mock return value of useCopilotKit in CopilotChat tests */
|
|
70
|
+
function createMockChatContext(agent: MockStepwiseAgent) {
|
|
71
|
+
return {
|
|
72
|
+
copilotkit: {
|
|
73
|
+
getAgent: () => agent,
|
|
74
|
+
runtimeUrl: "http://localhost:3000/api/copilot",
|
|
75
|
+
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
76
|
+
runtimeTransport: "rest",
|
|
77
|
+
headers: {},
|
|
78
|
+
agents: { [String(agent.agentId)]: agent },
|
|
79
|
+
connectAgent: vi.fn().mockResolvedValue(undefined),
|
|
80
|
+
subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })),
|
|
81
|
+
audioFileTranscriptionEnabled: false,
|
|
82
|
+
},
|
|
83
|
+
executingToolCallIds: new Set(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("CopilotChat throttleMs prop", () => {
|
|
88
|
+
let mockAgent: MockStepwiseAgent;
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
mockAgent = new MockStepwiseAgent();
|
|
92
|
+
mockAgent.agentId = "default";
|
|
93
|
+
|
|
94
|
+
mockUseAgent.mockReturnValue({ agent: mockAgent });
|
|
95
|
+
mockUseCopilotKit.mockReturnValue(createMockChatContext(mockAgent));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
vi.restoreAllMocks();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("passes throttleMs prop to useAgent", () => {
|
|
103
|
+
render(<CopilotChat throttleMs={500} />);
|
|
104
|
+
|
|
105
|
+
expect(mockUseAgent).toHaveBeenCalledWith(
|
|
106
|
+
expect.objectContaining({
|
|
107
|
+
throttleMs: 500,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("passes undefined throttleMs when prop is not set", () => {
|
|
113
|
+
render(<CopilotChat />);
|
|
114
|
+
|
|
115
|
+
expect(mockUseAgent).toHaveBeenCalledWith(
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
throttleMs: undefined,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("throttleMs type inheritance", () => {
|
|
124
|
+
it("CopilotSidebarProps includes throttleMs via CopilotChatProps", () => {
|
|
125
|
+
// Type-level assertion — if this compiles, the type includes throttleMs.
|
|
126
|
+
const sidebarProps: import("../CopilotSidebar").CopilotSidebarProps = {
|
|
127
|
+
throttleMs: 1000,
|
|
128
|
+
};
|
|
129
|
+
expect(sidebarProps.throttleMs).toBe(1000);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("CopilotPopupProps includes throttleMs via CopilotChatProps", () => {
|
|
133
|
+
const popupProps: import("../CopilotPopup").CopilotPopupProps = {
|
|
134
|
+
throttleMs: 2000,
|
|
135
|
+
};
|
|
136
|
+
expect(popupProps.throttleMs).toBe(2000);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -77,3 +77,12 @@ export {
|
|
|
77
77
|
export { CopilotSidebar, type CopilotSidebarProps } from "./CopilotSidebar";
|
|
78
78
|
|
|
79
79
|
export { CopilotPopup, type CopilotPopupProps } from "./CopilotPopup";
|
|
80
|
+
|
|
81
|
+
export { CopilotChatAttachmentQueue } from "./CopilotChatAttachmentQueue";
|
|
82
|
+
export { CopilotChatAttachmentRenderer } from "./CopilotChatAttachmentRenderer";
|
|
83
|
+
|
|
84
|
+
export type {
|
|
85
|
+
Attachment,
|
|
86
|
+
AttachmentsConfig,
|
|
87
|
+
AttachmentModality,
|
|
88
|
+
} from "@copilotkit/shared";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Provides the scroll container element to child components that need it for
|
|
5
|
+
* virtualization. Set by CopilotChatView.ScrollView; consumed by
|
|
6
|
+
* CopilotChatMessageView to feed useVirtualizer's getScrollElement.
|
|
7
|
+
*
|
|
8
|
+
* Carries the element itself (not a ref) so that context consumers re-render
|
|
9
|
+
* reactively when the scroll container is first mounted.
|
|
10
|
+
*/
|
|
11
|
+
export const ScrollElementContext = React.createContext<HTMLElement | null>(
|
|
12
|
+
null,
|
|
13
|
+
);
|
|
@@ -42,8 +42,16 @@ describe("useAgentContext timing - follow-up run sees updated context", () => {
|
|
|
42
42
|
* with no new messages — which is fine; we only need to capture context.
|
|
43
43
|
*/
|
|
44
44
|
class ContextCapturingAgent extends MockStepwiseAgent {
|
|
45
|
+
// Shared so the clone and original both see the captured contexts
|
|
45
46
|
public contextPerRun: Context[][] = [];
|
|
46
47
|
|
|
48
|
+
clone(): this {
|
|
49
|
+
const cloned = super.clone();
|
|
50
|
+
(cloned as unknown as ContextCapturingAgent).contextPerRun =
|
|
51
|
+
this.contextPerRun;
|
|
52
|
+
return cloned;
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
async runAgent(
|
|
48
56
|
parameters?: RunAgentParameters,
|
|
49
57
|
subscriber?: AgentSubscriber,
|
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
});
|