@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/chunk-77IVITG3.mjs +158 -0
  3. package/dist/chunk-77IVITG3.mjs.map +1 -0
  4. package/dist/chunk-BKMJ4LC7.mjs +119 -0
  5. package/dist/chunk-BKMJ4LC7.mjs.map +1 -0
  6. package/dist/chunk-C3YJYDK4.mjs +189 -0
  7. package/dist/chunk-C3YJYDK4.mjs.map +1 -0
  8. package/dist/{chunk-GIU66J37.mjs → chunk-DQXCQWSG.mjs} +47 -5
  9. package/dist/chunk-DQXCQWSG.mjs.map +1 -0
  10. package/dist/{chunk-HBMPXNW2.mjs → chunk-LO4RRITI.mjs} +71 -18
  11. package/dist/chunk-LO4RRITI.mjs.map +1 -0
  12. package/dist/{chunk-3G4VFRVV.mjs → chunk-NXHQDCZF.mjs} +2 -2
  13. package/dist/{chunk-FDOMAPJY.mjs → chunk-QD7EID4N.mjs} +1 -1
  14. package/dist/chunk-QD7EID4N.mjs.map +1 -0
  15. package/dist/{chunk-YTQHRJUA.mjs → chunk-VKNLTZJE.mjs} +2 -2
  16. package/dist/{chunk-4RRUJHCI.mjs → chunk-VP43SLSZ.mjs} +2 -2
  17. package/dist/{chunk-MF2ZSLBV.mjs → chunk-XZFIJ7XF.mjs} +2 -2
  18. package/dist/components/copilot-provider/copilotkit.js +437 -150
  19. package/dist/components/copilot-provider/copilotkit.js.map +1 -1
  20. package/dist/components/copilot-provider/copilotkit.mjs +5 -3
  21. package/dist/components/copilot-provider/index.js +437 -150
  22. package/dist/components/copilot-provider/index.js.map +1 -1
  23. package/dist/components/copilot-provider/index.mjs +5 -3
  24. package/dist/components/index.js +437 -150
  25. package/dist/components/index.js.map +1 -1
  26. package/dist/components/index.mjs +5 -3
  27. package/dist/context/coagent-state-renders-context.d.ts +1 -0
  28. package/dist/context/coagent-state-renders-context.js.map +1 -1
  29. package/dist/context/coagent-state-renders-context.mjs +1 -1
  30. package/dist/context/index.js.map +1 -1
  31. package/dist/context/index.mjs +1 -1
  32. package/dist/hooks/index.js +512 -212
  33. package/dist/hooks/index.js.map +1 -1
  34. package/dist/hooks/index.mjs +19 -17
  35. package/dist/hooks/use-coagent-state-render-bridge.helpers.d.ts +92 -0
  36. package/dist/hooks/use-coagent-state-render-bridge.helpers.js +231 -0
  37. package/dist/hooks/use-coagent-state-render-bridge.helpers.js.map +1 -0
  38. package/dist/hooks/use-coagent-state-render-bridge.helpers.mjs +24 -0
  39. package/dist/hooks/use-coagent-state-render-bridge.helpers.mjs.map +1 -0
  40. package/dist/hooks/use-coagent-state-render-bridge.js +334 -72
  41. package/dist/hooks/use-coagent-state-render-bridge.js.map +1 -1
  42. package/dist/hooks/use-coagent-state-render-bridge.mjs +4 -2
  43. package/dist/hooks/use-coagent-state-render-registry.d.ts +25 -0
  44. package/dist/hooks/use-coagent-state-render-registry.js +358 -0
  45. package/dist/hooks/use-coagent-state-render-registry.js.map +1 -0
  46. package/dist/hooks/use-coagent-state-render-registry.mjs +9 -0
  47. package/dist/hooks/use-coagent-state-render-registry.mjs.map +1 -0
  48. package/dist/hooks/use-coagent-state-render.js.map +1 -1
  49. package/dist/hooks/use-coagent-state-render.mjs +2 -2
  50. package/dist/hooks/use-copilot-chat-headless_c.js +414 -114
  51. package/dist/hooks/use-copilot-chat-headless_c.js.map +1 -1
  52. package/dist/hooks/use-copilot-chat-headless_c.mjs +7 -5
  53. package/dist/hooks/use-copilot-chat.js +406 -106
  54. package/dist/hooks/use-copilot-chat.js.map +1 -1
  55. package/dist/hooks/use-copilot-chat.mjs +7 -5
  56. package/dist/hooks/use-copilot-chat_internal.js +406 -106
  57. package/dist/hooks/use-copilot-chat_internal.js.map +1 -1
  58. package/dist/hooks/use-copilot-chat_internal.mjs +6 -4
  59. package/dist/hooks/use-langgraph-interrupt-render.mjs +1 -1
  60. package/dist/index.js +651 -311
  61. package/dist/index.js.map +1 -1
  62. package/dist/index.mjs +22 -20
  63. package/dist/lib/copilot-task.js.map +1 -1
  64. package/dist/lib/copilot-task.mjs +6 -4
  65. package/dist/lib/index.js.map +1 -1
  66. package/dist/lib/index.mjs +6 -4
  67. package/dist/setupTests.js +1 -0
  68. package/dist/setupTests.js.map +1 -1
  69. package/dist/setupTests.mjs +1 -0
  70. package/dist/setupTests.mjs.map +1 -1
  71. package/dist/test-helpers/copilot-context.d.ts +14 -0
  72. package/dist/test-helpers/copilot-context.js +128 -0
  73. package/dist/test-helpers/copilot-context.js.map +1 -0
  74. package/dist/test-helpers/copilot-context.mjs +74 -0
  75. package/dist/test-helpers/copilot-context.mjs.map +1 -0
  76. package/dist/types/index.mjs +1 -1
  77. package/package.json +5 -5
  78. package/src/components/copilot-provider/copilotkit.tsx +56 -0
  79. package/src/context/coagent-state-renders-context.tsx +1 -0
  80. package/src/hooks/__tests__/use-coagent-state-render-bridge.helpers.test.ts +100 -0
  81. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +892 -37
  82. package/src/hooks/__tests__/use-coagent-state-render.test.tsx +334 -0
  83. package/src/hooks/use-coagent-state-render-bridge.helpers.ts +311 -0
  84. package/src/hooks/use-coagent-state-render-bridge.tsx +25 -120
  85. package/src/hooks/use-coagent-state-render-registry.ts +215 -0
  86. package/src/hooks/use-copilot-chat_internal.ts +93 -34
  87. package/src/setupTests.ts +1 -0
  88. package/src/test-helpers/copilot-context.ts +91 -0
  89. package/dist/chunk-3X3I7OJV.mjs +0 -172
  90. package/dist/chunk-3X3I7OJV.mjs.map +0 -1
  91. package/dist/chunk-FDOMAPJY.mjs.map +0 -1
  92. package/dist/chunk-GIU66J37.mjs.map +0 -1
  93. package/dist/chunk-HBMPXNW2.mjs.map +0 -1
  94. /package/dist/{chunk-3G4VFRVV.mjs.map → chunk-NXHQDCZF.mjs.map} +0 -0
  95. /package/dist/{chunk-YTQHRJUA.mjs.map → chunk-VKNLTZJE.mjs.map} +0 -0
  96. /package/dist/{chunk-4RRUJHCI.mjs.map → chunk-VP43SLSZ.mjs.map} +0 -0
  97. /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
+ }