@copilotkit/react-core 1.51.3-next.6 → 1.51.3-next.7
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 +10 -0
- package/dist/chunk-77IVITG3.mjs +158 -0
- package/dist/chunk-77IVITG3.mjs.map +1 -0
- package/dist/chunk-BKMJ4LC7.mjs +119 -0
- package/dist/chunk-BKMJ4LC7.mjs.map +1 -0
- package/dist/chunk-C3YJYDK4.mjs +189 -0
- package/dist/chunk-C3YJYDK4.mjs.map +1 -0
- package/dist/{chunk-GIU66J37.mjs → chunk-DQXCQWSG.mjs} +47 -5
- package/dist/chunk-DQXCQWSG.mjs.map +1 -0
- package/dist/{chunk-HBMPXNW2.mjs → chunk-LO4RRITI.mjs} +71 -18
- package/dist/chunk-LO4RRITI.mjs.map +1 -0
- package/dist/{chunk-3G4VFRVV.mjs → chunk-NXHQDCZF.mjs} +2 -2
- package/dist/{chunk-FDOMAPJY.mjs → chunk-QD7EID4N.mjs} +1 -1
- package/dist/chunk-QD7EID4N.mjs.map +1 -0
- package/dist/{chunk-YTQHRJUA.mjs → chunk-VKNLTZJE.mjs} +2 -2
- package/dist/{chunk-4RRUJHCI.mjs → chunk-VP43SLSZ.mjs} +2 -2
- package/dist/{chunk-MF2ZSLBV.mjs → chunk-XZFIJ7XF.mjs} +2 -2
- package/dist/components/copilot-provider/copilotkit.js +437 -150
- package/dist/components/copilot-provider/copilotkit.js.map +1 -1
- package/dist/components/copilot-provider/copilotkit.mjs +5 -3
- package/dist/components/copilot-provider/index.js +437 -150
- package/dist/components/copilot-provider/index.js.map +1 -1
- package/dist/components/copilot-provider/index.mjs +5 -3
- package/dist/components/index.js +437 -150
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +5 -3
- package/dist/context/coagent-state-renders-context.d.ts +1 -0
- package/dist/context/coagent-state-renders-context.js.map +1 -1
- package/dist/context/coagent-state-renders-context.mjs +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/index.mjs +1 -1
- package/dist/hooks/index.js +512 -212
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +19 -17
- package/dist/hooks/use-coagent-state-render-bridge.helpers.d.ts +92 -0
- package/dist/hooks/use-coagent-state-render-bridge.helpers.js +231 -0
- package/dist/hooks/use-coagent-state-render-bridge.helpers.js.map +1 -0
- package/dist/hooks/use-coagent-state-render-bridge.helpers.mjs +24 -0
- package/dist/hooks/use-coagent-state-render-bridge.helpers.mjs.map +1 -0
- package/dist/hooks/use-coagent-state-render-bridge.js +334 -72
- package/dist/hooks/use-coagent-state-render-bridge.js.map +1 -1
- package/dist/hooks/use-coagent-state-render-bridge.mjs +4 -2
- package/dist/hooks/use-coagent-state-render-registry.d.ts +25 -0
- package/dist/hooks/use-coagent-state-render-registry.js +358 -0
- package/dist/hooks/use-coagent-state-render-registry.js.map +1 -0
- package/dist/hooks/use-coagent-state-render-registry.mjs +9 -0
- package/dist/hooks/use-coagent-state-render-registry.mjs.map +1 -0
- package/dist/hooks/use-coagent-state-render.js.map +1 -1
- package/dist/hooks/use-coagent-state-render.mjs +2 -2
- package/dist/hooks/use-copilot-chat-headless_c.js +414 -114
- package/dist/hooks/use-copilot-chat-headless_c.js.map +1 -1
- package/dist/hooks/use-copilot-chat-headless_c.mjs +7 -5
- package/dist/hooks/use-copilot-chat.js +406 -106
- package/dist/hooks/use-copilot-chat.js.map +1 -1
- package/dist/hooks/use-copilot-chat.mjs +7 -5
- package/dist/hooks/use-copilot-chat_internal.js +406 -106
- package/dist/hooks/use-copilot-chat_internal.js.map +1 -1
- package/dist/hooks/use-copilot-chat_internal.mjs +6 -4
- package/dist/hooks/use-langgraph-interrupt-render.mjs +1 -1
- package/dist/index.js +651 -311
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +22 -20
- package/dist/lib/copilot-task.js.map +1 -1
- package/dist/lib/copilot-task.mjs +6 -4
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/index.mjs +6 -4
- package/dist/setupTests.js +1 -0
- package/dist/setupTests.js.map +1 -1
- package/dist/setupTests.mjs +1 -0
- package/dist/setupTests.mjs.map +1 -1
- package/dist/test-helpers/copilot-context.d.ts +14 -0
- package/dist/test-helpers/copilot-context.js +128 -0
- package/dist/test-helpers/copilot-context.js.map +1 -0
- package/dist/test-helpers/copilot-context.mjs +74 -0
- package/dist/test-helpers/copilot-context.mjs.map +1 -0
- package/dist/types/index.mjs +1 -1
- package/package.json +5 -5
- package/src/components/copilot-provider/copilotkit.tsx +56 -0
- package/src/context/coagent-state-renders-context.tsx +1 -0
- package/src/hooks/__tests__/use-coagent-state-render-bridge.helpers.test.ts +100 -0
- package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +892 -37
- package/src/hooks/__tests__/use-coagent-state-render.test.tsx +334 -0
- package/src/hooks/use-coagent-state-render-bridge.helpers.ts +311 -0
- package/src/hooks/use-coagent-state-render-bridge.tsx +25 -120
- package/src/hooks/use-coagent-state-render-registry.ts +215 -0
- package/src/hooks/use-copilot-chat_internal.ts +93 -34
- package/src/setupTests.ts +1 -0
- package/src/test-helpers/copilot-context.ts +91 -0
- package/dist/chunk-3X3I7OJV.mjs +0 -172
- package/dist/chunk-3X3I7OJV.mjs.map +0 -1
- package/dist/chunk-FDOMAPJY.mjs.map +0 -1
- package/dist/chunk-GIU66J37.mjs.map +0 -1
- package/dist/chunk-HBMPXNW2.mjs.map +0 -1
- /package/dist/{chunk-3G4VFRVV.mjs.map → chunk-NXHQDCZF.mjs.map} +0 -0
- /package/dist/{chunk-YTQHRJUA.mjs.map → chunk-VKNLTZJE.mjs.map} +0 -0
- /package/dist/{chunk-4RRUJHCI.mjs.map → chunk-VP43SLSZ.mjs.map} +0 -0
- /package/dist/{chunk-MF2ZSLBV.mjs.map → chunk-XZFIJ7XF.mjs.map} +0 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import { render, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import { useCoAgentStateRender } from "../use-coagent-state-render";
|
|
4
|
+
import type { CoAgentStateRender } from "../../types/coagent-action";
|
|
5
|
+
import {
|
|
6
|
+
CoAgentStateRendersProvider,
|
|
7
|
+
CopilotContext,
|
|
8
|
+
useCoAgentStateRenders,
|
|
9
|
+
} from "../../context";
|
|
10
|
+
import { CopilotKitAgentDiscoveryError, randomId } from "@copilotkit/shared";
|
|
11
|
+
import { createTestCopilotContext } from "../../test-helpers/copilot-context";
|
|
12
|
+
|
|
13
|
+
const addToast = jest.fn();
|
|
14
|
+
const setBannerError = jest.fn();
|
|
15
|
+
|
|
16
|
+
jest.mock("../../components/toast/toast-provider", () => ({
|
|
17
|
+
useToast: () => ({
|
|
18
|
+
addToast,
|
|
19
|
+
setBannerError,
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
function createWrapper(copilotContextValue: ReturnType<typeof createTestCopilotContext>) {
|
|
24
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
25
|
+
return (
|
|
26
|
+
<CopilotContext.Provider value={copilotContextValue}>
|
|
27
|
+
<CoAgentStateRendersProvider>{children}</CoAgentStateRendersProvider>
|
|
28
|
+
</CopilotContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function useHarness<T>(action: Parameters<typeof useCoAgentStateRender<T>>[0], deps?: unknown[]) {
|
|
34
|
+
useCoAgentStateRender(action, deps);
|
|
35
|
+
return useCoAgentStateRenders();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function HookUser<T>({
|
|
39
|
+
action,
|
|
40
|
+
deps,
|
|
41
|
+
}: {
|
|
42
|
+
action: CoAgentStateRender<T>;
|
|
43
|
+
deps?: unknown[];
|
|
44
|
+
}) {
|
|
45
|
+
useCoAgentStateRender(action, deps);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getSingleEntry<T>(renders: Record<string, T>) {
|
|
50
|
+
const entries = Object.entries(renders);
|
|
51
|
+
expect(entries).toHaveLength(1);
|
|
52
|
+
return entries[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("useCoAgentStateRender (hook behaviors)", () => {
|
|
56
|
+
let idCounter = 0;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
jest.clearAllMocks();
|
|
60
|
+
idCounter = 0;
|
|
61
|
+
(randomId as jest.Mock).mockImplementation(() => `test-random-id-${++idCounter}`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("registers state render and writes to the render cache", async () => {
|
|
65
|
+
const chatComponentsCache = { current: { actions: {}, coAgentStateRenders: {} } };
|
|
66
|
+
const wrapper = createWrapper(
|
|
67
|
+
createTestCopilotContext({
|
|
68
|
+
chatComponentsCache,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const renderFn = jest.fn(() => null);
|
|
73
|
+
|
|
74
|
+
const { result } = renderHook(
|
|
75
|
+
() =>
|
|
76
|
+
useHarness({
|
|
77
|
+
name: "agent-a",
|
|
78
|
+
nodeName: "node-1",
|
|
79
|
+
render: renderFn,
|
|
80
|
+
}),
|
|
81
|
+
{ wrapper },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(Object.keys(result.current.coAgentStateRenders)).toHaveLength(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(chatComponentsCache.current.coAgentStateRenders["agent-a-node-1"]).toBe(renderFn);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("mutates handler + cache in place when dependencies are omitted", async () => {
|
|
92
|
+
const chatComponentsCache = { current: { actions: {}, coAgentStateRenders: {} } };
|
|
93
|
+
const wrapper = createWrapper(
|
|
94
|
+
createTestCopilotContext({
|
|
95
|
+
chatComponentsCache,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const handlerOne = jest.fn();
|
|
100
|
+
const handlerTwo = jest.fn();
|
|
101
|
+
const renderOne = jest.fn(() => null);
|
|
102
|
+
const renderTwo = jest.fn(() => null);
|
|
103
|
+
|
|
104
|
+
const { result, rerender } = renderHook(
|
|
105
|
+
({ handler, renderFn }) =>
|
|
106
|
+
useHarness({
|
|
107
|
+
name: "agent-b",
|
|
108
|
+
handler,
|
|
109
|
+
render: renderFn,
|
|
110
|
+
}),
|
|
111
|
+
{
|
|
112
|
+
wrapper,
|
|
113
|
+
initialProps: { handler: handlerOne, renderFn: renderOne },
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(Object.keys(result.current.coAgentStateRenders)).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const initialRenders = result.current.coAgentStateRenders;
|
|
122
|
+
const [id, initialRender] = getSingleEntry(initialRenders);
|
|
123
|
+
|
|
124
|
+
expect(initialRender.handler).toBe(handlerOne);
|
|
125
|
+
expect(chatComponentsCache.current.coAgentStateRenders["agent-b-global"]).toBe(renderOne);
|
|
126
|
+
|
|
127
|
+
rerender({ handler: handlerTwo, renderFn: renderTwo });
|
|
128
|
+
|
|
129
|
+
expect(result.current.coAgentStateRenders).toBe(initialRenders);
|
|
130
|
+
expect(result.current.coAgentStateRenders[id].handler).toBe(handlerTwo);
|
|
131
|
+
expect(chatComponentsCache.current.coAgentStateRenders["agent-b-global"]).toBe(renderTwo);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("re-registers when dependencies change", async () => {
|
|
135
|
+
const wrapper = createWrapper(createTestCopilotContext());
|
|
136
|
+
|
|
137
|
+
const handlerOne = jest.fn();
|
|
138
|
+
const handlerTwo = jest.fn();
|
|
139
|
+
|
|
140
|
+
const { result, rerender } = renderHook(
|
|
141
|
+
({ deps, handler }) =>
|
|
142
|
+
useHarness(
|
|
143
|
+
{
|
|
144
|
+
name: "agent-c",
|
|
145
|
+
handler,
|
|
146
|
+
},
|
|
147
|
+
deps,
|
|
148
|
+
),
|
|
149
|
+
{
|
|
150
|
+
wrapper,
|
|
151
|
+
initialProps: { deps: [0], handler: handlerOne },
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(Object.keys(result.current.coAgentStateRenders)).toHaveLength(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const initialRenders = result.current.coAgentStateRenders;
|
|
160
|
+
const [id] = Object.keys(initialRenders);
|
|
161
|
+
|
|
162
|
+
rerender({ deps: [1], handler: handlerTwo });
|
|
163
|
+
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(result.current.coAgentStateRenders).not.toBe(initialRenders);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.current.coAgentStateRenders[id].handler).toBe(handlerTwo);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("re-registers when string render changes", async () => {
|
|
172
|
+
const chatComponentsCache = { current: { actions: {}, coAgentStateRenders: {} } };
|
|
173
|
+
const wrapper = createWrapper(
|
|
174
|
+
createTestCopilotContext({
|
|
175
|
+
chatComponentsCache,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const { result, rerender } = renderHook(
|
|
180
|
+
({ renderValue }) =>
|
|
181
|
+
useHarness({
|
|
182
|
+
name: "agent-d",
|
|
183
|
+
render: renderValue,
|
|
184
|
+
}),
|
|
185
|
+
{
|
|
186
|
+
wrapper,
|
|
187
|
+
initialProps: { renderValue: "Step 1" },
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(Object.keys(result.current.coAgentStateRenders)).toHaveLength(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const initialRenders = result.current.coAgentStateRenders;
|
|
196
|
+
rerender({ renderValue: "Step 2" });
|
|
197
|
+
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(result.current.coAgentStateRenders).not.toBe(initialRenders);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(chatComponentsCache.current.coAgentStateRenders["agent-d-global"]).toBe("Step 2");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("warns when duplicate registrations target the same agent + node", async () => {
|
|
206
|
+
const copilotContextValue = createTestCopilotContext();
|
|
207
|
+
|
|
208
|
+
function DuplicateHarness() {
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
<HookUser
|
|
212
|
+
action={{
|
|
213
|
+
name: "agent-dup",
|
|
214
|
+
nodeName: "node-x",
|
|
215
|
+
handler: jest.fn(),
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
<HookUser
|
|
219
|
+
action={{
|
|
220
|
+
name: "agent-dup",
|
|
221
|
+
nodeName: "node-x",
|
|
222
|
+
handler: jest.fn(),
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
</>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
render(
|
|
230
|
+
<CopilotContext.Provider value={copilotContextValue}>
|
|
231
|
+
<CoAgentStateRendersProvider>
|
|
232
|
+
<DuplicateHarness />
|
|
233
|
+
</CoAgentStateRendersProvider>
|
|
234
|
+
</CopilotContext.Provider>,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(addToast).toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(addToast).toHaveBeenCalledWith(
|
|
242
|
+
expect.objectContaining({
|
|
243
|
+
type: "warning",
|
|
244
|
+
message:
|
|
245
|
+
"Found multiple state renders for agent agent-dup and node node-x. State renders might get overridden",
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("does not warn when duplicate agents target different nodes", async () => {
|
|
251
|
+
const copilotContextValue = createTestCopilotContext();
|
|
252
|
+
|
|
253
|
+
function NonDuplicateHarness() {
|
|
254
|
+
return (
|
|
255
|
+
<>
|
|
256
|
+
<HookUser
|
|
257
|
+
action={{
|
|
258
|
+
name: "agent-ok",
|
|
259
|
+
nodeName: "node-a",
|
|
260
|
+
handler: jest.fn(),
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
<HookUser
|
|
264
|
+
action={{
|
|
265
|
+
name: "agent-ok",
|
|
266
|
+
nodeName: "node-b",
|
|
267
|
+
handler: jest.fn(),
|
|
268
|
+
}}
|
|
269
|
+
/>
|
|
270
|
+
</>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
render(
|
|
275
|
+
<CopilotContext.Provider value={copilotContextValue}>
|
|
276
|
+
<CoAgentStateRendersProvider>
|
|
277
|
+
<NonDuplicateHarness />
|
|
278
|
+
</CoAgentStateRendersProvider>
|
|
279
|
+
</CopilotContext.Provider>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await waitFor(() => {
|
|
283
|
+
expect(addToast).not.toHaveBeenCalled();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("surfaces missing agents in the banner error state", async () => {
|
|
288
|
+
const availableAgents = [{ name: "known-agent", id: "agent-1" }];
|
|
289
|
+
const wrapper = createWrapper(
|
|
290
|
+
createTestCopilotContext({
|
|
291
|
+
availableAgents,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
renderHook(
|
|
296
|
+
() =>
|
|
297
|
+
useHarness({
|
|
298
|
+
name: "missing-agent",
|
|
299
|
+
handler: jest.fn(),
|
|
300
|
+
}),
|
|
301
|
+
{ wrapper },
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
await waitFor(() => {
|
|
305
|
+
expect(CopilotKitAgentDiscoveryError).toHaveBeenCalledWith({
|
|
306
|
+
agentName: "missing-agent",
|
|
307
|
+
availableAgents: [{ name: "known-agent", id: "agent-1" }],
|
|
308
|
+
});
|
|
309
|
+
expect(setBannerError).toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("does not surface banner errors when agent is available", async () => {
|
|
314
|
+
const availableAgents = [{ name: "agent-present", id: "agent-2" }];
|
|
315
|
+
const wrapper = createWrapper(
|
|
316
|
+
createTestCopilotContext({
|
|
317
|
+
availableAgents,
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
renderHook(
|
|
322
|
+
() =>
|
|
323
|
+
useHarness({
|
|
324
|
+
name: "agent-present",
|
|
325
|
+
handler: jest.fn(),
|
|
326
|
+
}),
|
|
327
|
+
{ wrapper },
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
expect(setBannerError).not.toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { dataToUUID, parseJson } from "@copilotkit/shared";
|
|
2
|
+
|
|
3
|
+
export enum RenderStatus {
|
|
4
|
+
InProgress = "inProgress",
|
|
5
|
+
Complete = "complete",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum ClaimAction {
|
|
9
|
+
Create = "create",
|
|
10
|
+
Override = "override",
|
|
11
|
+
Existing = "existing",
|
|
12
|
+
Block = "block",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StateRenderContext {
|
|
16
|
+
agentId: string;
|
|
17
|
+
stateRenderId: string;
|
|
18
|
+
messageId: string;
|
|
19
|
+
runId: string;
|
|
20
|
+
messageIndex?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Claim {
|
|
24
|
+
stateRenderId: string;
|
|
25
|
+
runId?: string;
|
|
26
|
+
stateSnapshot?: any;
|
|
27
|
+
locked?: boolean;
|
|
28
|
+
messageIndex?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ClaimsByMessageId = Record<string, Claim>;
|
|
32
|
+
|
|
33
|
+
export interface ClaimResolution {
|
|
34
|
+
canRender: boolean;
|
|
35
|
+
action: ClaimAction;
|
|
36
|
+
nextClaim?: Claim;
|
|
37
|
+
lockOthers?: boolean;
|
|
38
|
+
updateRunId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SnapshotCaches {
|
|
42
|
+
byStateRenderAndRun: Record<string, any>;
|
|
43
|
+
byMessageId: Record<string, any>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SnapshotSelectionInput {
|
|
47
|
+
messageId: string;
|
|
48
|
+
messageName?: string;
|
|
49
|
+
allowLiveState?: boolean;
|
|
50
|
+
skipLatestCache?: boolean;
|
|
51
|
+
stateRenderId?: string;
|
|
52
|
+
effectiveRunId: string;
|
|
53
|
+
stateSnapshotProp?: any;
|
|
54
|
+
agentState?: any;
|
|
55
|
+
agentMessages?: Array<{ id: string; role?: string }>;
|
|
56
|
+
existingClaim?: Claim;
|
|
57
|
+
caches: SnapshotCaches;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SnapshotSelectionResult {
|
|
61
|
+
snapshot?: any;
|
|
62
|
+
hasSnapshotKeys: boolean;
|
|
63
|
+
cachedSnapshot?: any;
|
|
64
|
+
allowEmptySnapshot?: boolean;
|
|
65
|
+
snapshotForClaim?: any;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getStateWithoutConstantKeys(state: any) {
|
|
69
|
+
if (!state) return {};
|
|
70
|
+
const { messages, tools, copilotkit, ...stateWithoutConstantKeys } = state;
|
|
71
|
+
return stateWithoutConstantKeys;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Function that compares states, without the constant keys
|
|
75
|
+
export function areStatesEquals(a: any, b: any) {
|
|
76
|
+
if ((a && !b) || (!a && b)) return false;
|
|
77
|
+
const { messages, tools, copilotkit, ...aWithoutConstantKeys } = a;
|
|
78
|
+
const {
|
|
79
|
+
messages: bMessages,
|
|
80
|
+
tools: bTools,
|
|
81
|
+
copilotkit: bCopilotkit,
|
|
82
|
+
...bWithoutConstantKeys
|
|
83
|
+
} = b;
|
|
84
|
+
|
|
85
|
+
return JSON.stringify(aWithoutConstantKeys) === JSON.stringify(bWithoutConstantKeys);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isPlaceholderMessageId(messageId: string | undefined) {
|
|
89
|
+
return !!messageId && messageId.startsWith("coagent-state-render-");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isPlaceholderMessageName(messageName: string | undefined) {
|
|
93
|
+
return messageName === "coagent-state-render";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readCachedMessageEntry(entry: any): { snapshot?: any; runId?: string } {
|
|
97
|
+
if (!entry || typeof entry !== "object") {
|
|
98
|
+
return { snapshot: entry, runId: undefined };
|
|
99
|
+
}
|
|
100
|
+
const snapshot = "snapshot" in entry ? entry.snapshot : entry;
|
|
101
|
+
const runId = "runId" in entry ? entry.runId : undefined;
|
|
102
|
+
return { snapshot, runId };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getEffectiveRunId({
|
|
106
|
+
existingClaimRunId,
|
|
107
|
+
cachedMessageRunId,
|
|
108
|
+
runId,
|
|
109
|
+
}: {
|
|
110
|
+
existingClaimRunId?: string;
|
|
111
|
+
cachedMessageRunId?: string;
|
|
112
|
+
runId?: string;
|
|
113
|
+
}) {
|
|
114
|
+
return existingClaimRunId || cachedMessageRunId || runId || "pending";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve whether a message can claim a render slot.
|
|
119
|
+
* This is a pure decision function; the caller applies claim mutations.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveClaim({
|
|
122
|
+
claims,
|
|
123
|
+
context,
|
|
124
|
+
stateSnapshot,
|
|
125
|
+
}: {
|
|
126
|
+
claims: ClaimsByMessageId;
|
|
127
|
+
context: StateRenderContext;
|
|
128
|
+
stateSnapshot?: any;
|
|
129
|
+
}): ClaimResolution {
|
|
130
|
+
const { messageId, stateRenderId, runId, messageIndex } = context;
|
|
131
|
+
const existing = claims[messageId];
|
|
132
|
+
|
|
133
|
+
if (existing) {
|
|
134
|
+
const canRender = existing.stateRenderId === stateRenderId;
|
|
135
|
+
const shouldUpdateRunId =
|
|
136
|
+
canRender && runId && (!existing.runId || existing.runId === "pending");
|
|
137
|
+
return {
|
|
138
|
+
canRender,
|
|
139
|
+
action: canRender ? ClaimAction.Existing : ClaimAction.Block,
|
|
140
|
+
updateRunId: shouldUpdateRunId ? runId : undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const normalizedRunId = runId ?? "pending";
|
|
145
|
+
const renderClaimedByOtherMessageEntry = Object.entries(claims).find(
|
|
146
|
+
([, claim]) =>
|
|
147
|
+
claim.stateRenderId === stateRenderId &&
|
|
148
|
+
(claim.runId ?? "pending") === normalizedRunId &&
|
|
149
|
+
dataToUUID(getStateWithoutConstantKeys(claim.stateSnapshot)) ===
|
|
150
|
+
dataToUUID(getStateWithoutConstantKeys(stateSnapshot)),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const renderClaimedByOtherMessage = renderClaimedByOtherMessageEntry?.[1];
|
|
154
|
+
const claimedMessageId = renderClaimedByOtherMessageEntry?.[0];
|
|
155
|
+
|
|
156
|
+
if (renderClaimedByOtherMessage) {
|
|
157
|
+
if (
|
|
158
|
+
messageIndex !== undefined &&
|
|
159
|
+
renderClaimedByOtherMessage.messageIndex !== undefined &&
|
|
160
|
+
messageIndex > renderClaimedByOtherMessage.messageIndex
|
|
161
|
+
) {
|
|
162
|
+
return {
|
|
163
|
+
canRender: true,
|
|
164
|
+
action: ClaimAction.Override,
|
|
165
|
+
nextClaim: { stateRenderId, runId, messageIndex },
|
|
166
|
+
lockOthers:
|
|
167
|
+
runId === renderClaimedByOtherMessage.runId || isPlaceholderMessageId(claimedMessageId),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (runId && renderClaimedByOtherMessage.runId && runId !== renderClaimedByOtherMessage.runId) {
|
|
172
|
+
return {
|
|
173
|
+
canRender: true,
|
|
174
|
+
action: ClaimAction.Override,
|
|
175
|
+
nextClaim: { stateRenderId, runId, messageIndex },
|
|
176
|
+
lockOthers: isPlaceholderMessageId(claimedMessageId),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isPlaceholderMessageId(claimedMessageId)) {
|
|
181
|
+
return {
|
|
182
|
+
canRender: true,
|
|
183
|
+
action: ClaimAction.Override,
|
|
184
|
+
nextClaim: { stateRenderId, runId, messageIndex },
|
|
185
|
+
lockOthers: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
stateSnapshot &&
|
|
191
|
+
renderClaimedByOtherMessage.stateSnapshot &&
|
|
192
|
+
!areStatesEquals(renderClaimedByOtherMessage.stateSnapshot, stateSnapshot)
|
|
193
|
+
) {
|
|
194
|
+
return {
|
|
195
|
+
canRender: true,
|
|
196
|
+
action: ClaimAction.Override,
|
|
197
|
+
nextClaim: { stateRenderId, runId },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { canRender: false, action: ClaimAction.Block };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!runId) {
|
|
205
|
+
return { canRender: false, action: ClaimAction.Block };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
canRender: true,
|
|
210
|
+
action: ClaimAction.Create,
|
|
211
|
+
nextClaim: { stateRenderId, runId, messageIndex },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Select the best snapshot to render for this message.
|
|
217
|
+
* Priority order is:
|
|
218
|
+
* 1) explicit message snapshot
|
|
219
|
+
* 2) live agent state (latest assistant only)
|
|
220
|
+
* 3) cached snapshot for message
|
|
221
|
+
* 4) cached snapshot for stateRenderId+runId
|
|
222
|
+
* 5) last cached snapshot for stateRenderId
|
|
223
|
+
*/
|
|
224
|
+
export function selectSnapshot({
|
|
225
|
+
messageId,
|
|
226
|
+
messageName,
|
|
227
|
+
allowLiveState,
|
|
228
|
+
skipLatestCache,
|
|
229
|
+
stateRenderId,
|
|
230
|
+
effectiveRunId,
|
|
231
|
+
stateSnapshotProp,
|
|
232
|
+
agentState,
|
|
233
|
+
agentMessages,
|
|
234
|
+
existingClaim,
|
|
235
|
+
caches,
|
|
236
|
+
}: SnapshotSelectionInput): SnapshotSelectionResult {
|
|
237
|
+
const lastAssistantId = agentMessages
|
|
238
|
+
? [...agentMessages].reverse().find((msg) => msg.role === "assistant")?.id
|
|
239
|
+
: undefined;
|
|
240
|
+
const latestSnapshot =
|
|
241
|
+
stateRenderId !== undefined ? caches.byStateRenderAndRun[`${stateRenderId}::latest`] : undefined;
|
|
242
|
+
const messageIndex = agentMessages
|
|
243
|
+
? agentMessages.findIndex((msg) => msg.id === messageId)
|
|
244
|
+
: -1;
|
|
245
|
+
const messageRole =
|
|
246
|
+
messageIndex >= 0 && agentMessages ? agentMessages[messageIndex]?.role : undefined;
|
|
247
|
+
let previousUserMessageId: string | undefined;
|
|
248
|
+
if (messageIndex > 0 && agentMessages) {
|
|
249
|
+
for (let i = messageIndex - 1; i >= 0; i -= 1) {
|
|
250
|
+
if (agentMessages[i]?.role === "user") {
|
|
251
|
+
previousUserMessageId = agentMessages[i]?.id;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const liveStateIsStale =
|
|
257
|
+
stateSnapshotProp === undefined &&
|
|
258
|
+
latestSnapshot !== undefined &&
|
|
259
|
+
agentState !== undefined &&
|
|
260
|
+
areStatesEquals(latestSnapshot, agentState);
|
|
261
|
+
const shouldUseLiveState =
|
|
262
|
+
(Boolean(allowLiveState) || !lastAssistantId || messageId === lastAssistantId) &&
|
|
263
|
+
!liveStateIsStale;
|
|
264
|
+
const snapshot = stateSnapshotProp
|
|
265
|
+
? parseJson(stateSnapshotProp, stateSnapshotProp)
|
|
266
|
+
: shouldUseLiveState
|
|
267
|
+
? agentState
|
|
268
|
+
: undefined;
|
|
269
|
+
const hasSnapshotKeys = !!(snapshot && Object.keys(snapshot).length > 0);
|
|
270
|
+
const allowEmptySnapshot =
|
|
271
|
+
snapshot !== undefined &&
|
|
272
|
+
!hasSnapshotKeys &&
|
|
273
|
+
(stateSnapshotProp !== undefined || shouldUseLiveState);
|
|
274
|
+
|
|
275
|
+
const messageCacheEntry = caches.byMessageId[messageId];
|
|
276
|
+
const cachedMessageSnapshot = readCachedMessageEntry(messageCacheEntry).snapshot;
|
|
277
|
+
const cacheKey =
|
|
278
|
+
stateRenderId !== undefined ? `${stateRenderId}::${effectiveRunId}` : undefined;
|
|
279
|
+
let cachedSnapshot = cachedMessageSnapshot ?? caches.byMessageId[messageId];
|
|
280
|
+
if (cachedSnapshot === undefined && cacheKey && caches.byStateRenderAndRun[cacheKey] !== undefined) {
|
|
281
|
+
cachedSnapshot = caches.byStateRenderAndRun[cacheKey];
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
cachedSnapshot === undefined &&
|
|
285
|
+
stateRenderId &&
|
|
286
|
+
previousUserMessageId &&
|
|
287
|
+
caches.byStateRenderAndRun[`${stateRenderId}::pending:${previousUserMessageId}`] !==
|
|
288
|
+
undefined
|
|
289
|
+
) {
|
|
290
|
+
cachedSnapshot =
|
|
291
|
+
caches.byStateRenderAndRun[`${stateRenderId}::pending:${previousUserMessageId}`];
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
cachedSnapshot === undefined &&
|
|
295
|
+
!skipLatestCache &&
|
|
296
|
+
stateRenderId &&
|
|
297
|
+
messageRole !== "assistant" &&
|
|
298
|
+
(stateSnapshotProp !== undefined ||
|
|
299
|
+
(agentState && Object.keys(agentState).length > 0))
|
|
300
|
+
) {
|
|
301
|
+
cachedSnapshot = caches.byStateRenderAndRun[`${stateRenderId}::latest`];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const snapshotForClaim = existingClaim?.locked
|
|
305
|
+
? existingClaim.stateSnapshot ?? cachedSnapshot
|
|
306
|
+
: hasSnapshotKeys
|
|
307
|
+
? snapshot
|
|
308
|
+
: existingClaim?.stateSnapshot ?? cachedSnapshot;
|
|
309
|
+
|
|
310
|
+
return { snapshot, hasSnapshotKeys, cachedSnapshot, allowEmptySnapshot, snapshotForClaim };
|
|
311
|
+
}
|