@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
@@ -2,27 +2,9 @@ import { ReactCustomMessageRendererPosition, useAgent } from "@copilotkitnext/re
2
2
  import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import type { AgentSubscriber } from "@ag-ui/client";
4
4
  import { useCoAgentStateRenders } from "../context";
5
- import { dataToUUID, parseJson } from "@copilotkit/shared";
6
-
7
- function getStateWithoutConstantKeys(state: any) {
8
- if (!state) return {};
9
- const { messages, tools, copilotkit, ...stateWithoutConstantKeys } = state;
10
- return stateWithoutConstantKeys;
11
- }
12
-
13
- // Function that compares states, without the constant keys
14
- function areStatesEquals(a: any, b: any) {
15
- if ((a && !b) || (!a && b)) return false;
16
- const { messages, tools, copilotkit, ...aWithoutConstantKeys } = a;
17
- const {
18
- messages: bMessages,
19
- tools: bTools,
20
- copilotkit: bCopilotkit,
21
- ...bWithoutConstantKeys
22
- } = b;
23
-
24
- return JSON.stringify(aWithoutConstantKeys) === JSON.stringify(bWithoutConstantKeys);
25
- }
5
+ import { parseJson } from "@copilotkit/shared";
6
+ import { RenderStatus } from "./use-coagent-state-render-bridge.helpers";
7
+ import { useStateRenderRegistry } from "./use-coagent-state-render-registry";
26
8
 
27
9
  /**
28
10
  * Bridge hook that connects agent state renders to chat messages.
@@ -119,15 +101,12 @@ export interface CoAgentStateRenderBridgeProps {
119
101
  }
120
102
 
121
103
  export function useCoagentStateRenderBridge(agentId: string, props: CoAgentStateRenderBridgeProps) {
122
- const { stateSnapshot, messageIndexInRun, message } = props;
104
+ const { stateSnapshot, message } = props;
123
105
  const { coAgentStateRenders, claimsRef } = useCoAgentStateRenders();
124
106
  const { agent } = useAgent({ agentId });
125
107
  const [nodeName, setNodeName] = useState<string | undefined>(undefined);
126
108
  const [, forceUpdate] = useState(0);
127
109
 
128
- const runId = props.runId ?? message.runId;
129
- const effectiveRunId = runId || "pending";
130
-
131
110
  useEffect(() => {
132
111
  if (!agent) return;
133
112
  const subscriber: AgentSubscriber = {
@@ -166,107 +145,33 @@ export function useCoagentStateRenderBridge(agentId: string, props: CoAgentState
166
145
  },
167
146
  [coAgentStateRenders, nodeName, agentId],
168
147
  );
148
+ const stateRenderEntry = useMemo(() => getStateRender(message.id), [getStateRender, message.id]);
149
+ const stateRenderId = stateRenderEntry?.[0];
150
+ const stateRender = stateRenderEntry?.[1];
169
151
 
170
- // Message ID-based claim system - A state render can only be claimed by one message ID
171
- const handleRenderRequest = ({
172
- stateRenderId,
173
- messageId,
174
- runId,
175
- stateSnapshot: renderSnapshot,
176
- }: {
177
- stateRenderId: string;
178
- messageId: string;
179
- runId?: string;
180
- stateSnapshot?: any;
181
- }): boolean => {
182
- // Check if this message has already claimed this state render
183
- if (claimsRef.current[messageId]) {
184
- const canRender = claimsRef.current[messageId].stateRenderId === stateRenderId;
185
-
186
- // Update runId if it doesn't exist
187
- if (
188
- canRender &&
189
- runId &&
190
- (!claimsRef.current[messageId].runId || claimsRef.current[messageId].runId === "pending")
191
- ) {
192
- claimsRef.current[messageId].runId = runId;
193
- }
194
-
195
- return canRender;
196
- }
197
-
198
- // Do not allow render if any other message has claimed this state render
199
- const renderClaimedByOtherMessage = Object.values(claimsRef.current).find(
200
- (c) =>
201
- c.stateRenderId === stateRenderId &&
202
- dataToUUID(getStateWithoutConstantKeys(c.stateSnapshot)) ===
203
- dataToUUID(getStateWithoutConstantKeys(renderSnapshot)),
204
- );
205
- if (renderClaimedByOtherMessage) {
206
- // If:
207
- // - state render already claimed
208
- // - snapshot exists in the claiming object and is different from current,
209
- if (
210
- renderSnapshot &&
211
- renderClaimedByOtherMessage.stateSnapshot &&
212
- !areStatesEquals(renderClaimedByOtherMessage.stateSnapshot, renderSnapshot)
213
- ) {
214
- claimsRef.current[messageId] = { stateRenderId, runId };
215
- return true;
216
- }
217
- return false;
218
- }
219
-
220
- // No existing claim anywhere yet – allow this message to claim even if we already know the runId.
221
- if (!runId) {
222
- return false;
223
- }
224
-
225
- claimsRef.current[messageId] = { stateRenderId, runId };
226
- return true;
152
+ const registryMessage = {
153
+ ...message,
154
+ runId: props.runId ?? message.runId,
227
155
  };
156
+ const { canRender } = useStateRenderRegistry({
157
+ agentId,
158
+ stateRenderId,
159
+ message: registryMessage,
160
+ messageIndex: props.messageIndex,
161
+ stateSnapshot,
162
+ agentState: agent?.state,
163
+ agentMessages: agent?.messages,
164
+ claimsRef,
165
+ });
228
166
 
229
167
  return useMemo(() => {
230
- if (messageIndexInRun !== 0) {
231
- return null;
232
- }
233
-
234
- const [stateRenderId, stateRender] = getStateRender(message.id) ?? [];
235
-
236
168
  if (!stateRender || !stateRenderId) {
237
169
  return null;
238
170
  }
239
-
240
- // Is there any state we can use?
241
- const snapshot = stateSnapshot ? parseJson(stateSnapshot, stateSnapshot) : agent?.state;
242
-
243
- // Synchronously check/claim - returns true if this message can render
244
- const canRender = handleRenderRequest({
245
- stateRenderId,
246
- messageId: message.id,
247
- runId: effectiveRunId,
248
- stateSnapshot: snapshot,
249
- });
250
171
  if (!canRender) {
251
172
  return null;
252
173
  }
253
174
 
254
- // If we found state, and given that now there's a claim for the current message, let's save it in the claim
255
- if (snapshot) {
256
- const existingSnapshot = claimsRef.current[message.id].stateSnapshot;
257
- const snapshotChanged =
258
- stateSnapshot &&
259
- existingSnapshot !== undefined &&
260
- !areStatesEquals(existingSnapshot, snapshot);
261
-
262
- if (!claimsRef.current[message.id].locked || snapshotChanged) {
263
- claimsRef.current[message.id].stateSnapshot = snapshot;
264
- if (stateSnapshot) {
265
- claimsRef.current[message.id].locked = true;
266
- }
267
- }
268
- }
269
-
270
175
  if (stateRender.handler) {
271
176
  stateRender.handler({
272
177
  state: stateSnapshot ? parseJson(stateSnapshot, stateSnapshot) : (agent?.state ?? {}),
@@ -275,7 +180,7 @@ export function useCoagentStateRenderBridge(agentId: string, props: CoAgentState
275
180
  }
276
181
 
277
182
  if (stateRender.render) {
278
- const status = agent?.isRunning ? "inProgress" : "complete";
183
+ const status = agent?.isRunning ? RenderStatus.InProgress : RenderStatus.Complete;
279
184
 
280
185
  if (typeof stateRender.render === "string") return stateRender.render;
281
186
 
@@ -287,14 +192,14 @@ export function useCoagentStateRenderBridge(agentId: string, props: CoAgentState
287
192
  });
288
193
  }
289
194
  }, [
290
- getStateRender,
291
- stateSnapshot,
195
+ stateRender,
196
+ stateRenderId,
292
197
  agent?.state,
293
198
  agent?.isRunning,
294
199
  nodeName,
295
- effectiveRunId,
296
200
  message.id,
297
- messageIndexInRun,
201
+ stateSnapshot,
202
+ canRender,
298
203
  ]);
299
204
  }
300
205
 
@@ -0,0 +1,215 @@
1
+ import { useEffect } from "react";
2
+ import {
3
+ areStatesEquals,
4
+ ClaimAction,
5
+ getEffectiveRunId,
6
+ isPlaceholderMessageId,
7
+ isPlaceholderMessageName,
8
+ readCachedMessageEntry,
9
+ resolveClaim,
10
+ selectSnapshot,
11
+ type Claim,
12
+ type ClaimsByMessageId,
13
+ type SnapshotCaches,
14
+ type StateRenderContext,
15
+ } from "./use-coagent-state-render-bridge.helpers";
16
+
17
+ export interface StateRenderRegistryInput {
18
+ agentId: string;
19
+ stateRenderId?: string;
20
+ message: { id: string; runId?: string; name?: string };
21
+ messageIndex?: number;
22
+ stateSnapshot?: any;
23
+ agentState?: any;
24
+ agentMessages?: Array<{ id: string; role?: string }>;
25
+ claimsRef: React.MutableRefObject<Record<string, Claim>>;
26
+ }
27
+
28
+ export interface StateRenderRegistryResult {
29
+ canRender: boolean;
30
+ }
31
+
32
+ const LAST_SNAPSHOTS_BY_RENDER_AND_RUN = "__lastSnapshotsByStateRenderIdAndRun";
33
+ const LAST_SNAPSHOTS_BY_MESSAGE = "__lastSnapshotsByMessageId";
34
+
35
+ type SnapshotByMessageEntry = { snapshot: any; runId?: string } | any;
36
+ type ClaimsStore = Record<string, Claim> & {
37
+ [LAST_SNAPSHOTS_BY_RENDER_AND_RUN]?: Record<string, any>;
38
+ [LAST_SNAPSHOTS_BY_MESSAGE]?: Record<string, SnapshotByMessageEntry>;
39
+ };
40
+
41
+ function getClaimsStore(
42
+ claimsRef: React.MutableRefObject<Record<string, Claim>>,
43
+ ): ClaimsStore {
44
+ return claimsRef.current as ClaimsStore;
45
+ }
46
+
47
+ function getSnapshotCaches(claimsRef: React.MutableRefObject<Record<string, Claim>>): SnapshotCaches {
48
+ const store = getClaimsStore(claimsRef);
49
+ return {
50
+ byStateRenderAndRun: store[LAST_SNAPSHOTS_BY_RENDER_AND_RUN] ?? {},
51
+ byMessageId: store[LAST_SNAPSHOTS_BY_MESSAGE] ?? {},
52
+ };
53
+ }
54
+
55
+ export function useStateRenderRegistry({
56
+ agentId,
57
+ stateRenderId,
58
+ message,
59
+ messageIndex,
60
+ stateSnapshot,
61
+ agentState,
62
+ agentMessages,
63
+ claimsRef,
64
+ }: StateRenderRegistryInput): StateRenderRegistryResult {
65
+ const store = getClaimsStore(claimsRef);
66
+ const runId = message.runId;
67
+ const cachedMessageEntry = store[LAST_SNAPSHOTS_BY_MESSAGE]?.[message.id];
68
+ const { runId: cachedMessageRunId } = readCachedMessageEntry(cachedMessageEntry);
69
+ const existingClaimRunId = claimsRef.current[message.id]?.runId;
70
+ const effectiveRunId = getEffectiveRunId({
71
+ existingClaimRunId,
72
+ cachedMessageRunId,
73
+ runId,
74
+ });
75
+
76
+ useEffect(() => {
77
+ return () => {
78
+ const existingClaim = claimsRef.current[message.id];
79
+ if (
80
+ existingClaim?.stateSnapshot &&
81
+ Object.keys(existingClaim.stateSnapshot).length > 0
82
+ ) {
83
+ const snapshotCache = {
84
+ ...(store[LAST_SNAPSHOTS_BY_RENDER_AND_RUN] ?? {}),
85
+ };
86
+ const cacheKey = `${existingClaim.stateRenderId}::${existingClaim.runId ?? "pending"}`;
87
+ snapshotCache[cacheKey] = existingClaim.stateSnapshot;
88
+ snapshotCache[`${existingClaim.stateRenderId}::latest`] = existingClaim.stateSnapshot;
89
+ store[LAST_SNAPSHOTS_BY_RENDER_AND_RUN] = snapshotCache;
90
+
91
+ const messageCache = {
92
+ ...(store[LAST_SNAPSHOTS_BY_MESSAGE] ?? {}),
93
+ };
94
+ messageCache[message.id] = {
95
+ snapshot: existingClaim.stateSnapshot,
96
+ runId: existingClaim.runId ?? effectiveRunId,
97
+ };
98
+ store[LAST_SNAPSHOTS_BY_MESSAGE] = messageCache;
99
+ }
100
+ delete claimsRef.current[message.id];
101
+ };
102
+ }, [claimsRef, effectiveRunId, message.id]);
103
+
104
+ if (!stateRenderId) {
105
+ return { canRender: false };
106
+ }
107
+
108
+ const caches = getSnapshotCaches(claimsRef);
109
+ const existingClaim = claimsRef.current[message.id] as Claim | undefined;
110
+
111
+ const { snapshot, hasSnapshotKeys, allowEmptySnapshot, snapshotForClaim } = selectSnapshot({
112
+ messageId: message.id,
113
+ messageName: message.name,
114
+ allowLiveState:
115
+ isPlaceholderMessageName(message.name) || isPlaceholderMessageId(message.id),
116
+ skipLatestCache:
117
+ isPlaceholderMessageName(message.name) || isPlaceholderMessageId(message.id),
118
+ stateRenderId,
119
+ effectiveRunId,
120
+ stateSnapshotProp: stateSnapshot,
121
+ agentState,
122
+ agentMessages,
123
+ existingClaim,
124
+ caches,
125
+ });
126
+
127
+ const resolution = resolveClaim({
128
+ claims: claimsRef.current as ClaimsByMessageId,
129
+ context: {
130
+ agentId,
131
+ messageId: message.id,
132
+ stateRenderId,
133
+ runId: effectiveRunId,
134
+ messageIndex,
135
+ } satisfies StateRenderContext,
136
+ stateSnapshot: snapshotForClaim,
137
+ });
138
+
139
+ if (resolution.action === ClaimAction.Block) {
140
+ return { canRender: false };
141
+ }
142
+
143
+ if (resolution.updateRunId && claimsRef.current[message.id]) {
144
+ claimsRef.current[message.id].runId = resolution.updateRunId;
145
+ }
146
+
147
+ if (resolution.nextClaim) {
148
+ claimsRef.current[message.id] = resolution.nextClaim;
149
+ }
150
+
151
+ if (resolution.lockOthers) {
152
+ Object.entries(claimsRef.current).forEach(([id, claim]) => {
153
+ if (id !== message.id && claim.stateRenderId === stateRenderId) {
154
+ claim.locked = true;
155
+ }
156
+ });
157
+ }
158
+
159
+ if (existingClaim && !existingClaim.locked && agentMessages?.length) {
160
+ const indexInAgentMessages = agentMessages.findIndex((msg: any) => msg.id === message.id);
161
+ if (indexInAgentMessages >= 0 && indexInAgentMessages < agentMessages.length - 1) {
162
+ existingClaim.locked = true;
163
+ }
164
+ }
165
+
166
+ const existingSnapshot = claimsRef.current[message.id].stateSnapshot;
167
+ const snapshotChanged =
168
+ stateSnapshot &&
169
+ existingSnapshot !== undefined &&
170
+ !areStatesEquals(existingSnapshot, snapshot);
171
+
172
+ if (
173
+ snapshot &&
174
+ (stateSnapshot || hasSnapshotKeys || allowEmptySnapshot) &&
175
+ (!claimsRef.current[message.id].locked || snapshotChanged)
176
+ ) {
177
+ if (!claimsRef.current[message.id].locked || snapshotChanged) {
178
+ claimsRef.current[message.id].stateSnapshot = snapshot;
179
+ const snapshotCache = {
180
+ ...(store[LAST_SNAPSHOTS_BY_RENDER_AND_RUN] ?? {}),
181
+ };
182
+ const cacheKey = `${stateRenderId}::${effectiveRunId}`;
183
+ snapshotCache[cacheKey] = snapshot;
184
+ snapshotCache[`${stateRenderId}::latest`] = snapshot;
185
+ store[LAST_SNAPSHOTS_BY_RENDER_AND_RUN] = snapshotCache;
186
+ const messageCache = {
187
+ ...(store[LAST_SNAPSHOTS_BY_MESSAGE] ?? {}),
188
+ };
189
+ messageCache[message.id] = { snapshot, runId: effectiveRunId };
190
+ store[LAST_SNAPSHOTS_BY_MESSAGE] = messageCache;
191
+ if (stateSnapshot) {
192
+ claimsRef.current[message.id].locked = true;
193
+ }
194
+ }
195
+ } else if (snapshotForClaim) {
196
+ const existingSnapshot = claimsRef.current[message.id].stateSnapshot;
197
+ if (!existingSnapshot) {
198
+ claimsRef.current[message.id].stateSnapshot = snapshotForClaim;
199
+ const snapshotCache = {
200
+ ...(store[LAST_SNAPSHOTS_BY_RENDER_AND_RUN] ?? {}),
201
+ };
202
+ const cacheKey = `${stateRenderId}::${effectiveRunId}`;
203
+ snapshotCache[cacheKey] = snapshotForClaim;
204
+ snapshotCache[`${stateRenderId}::latest`] = snapshotForClaim;
205
+ store[LAST_SNAPSHOTS_BY_RENDER_AND_RUN] = snapshotCache;
206
+ const messageCache = {
207
+ ...(store[LAST_SNAPSHOTS_BY_MESSAGE] ?? {}),
208
+ };
209
+ messageCache[message.id] = { snapshot: snapshotForClaim, runId: effectiveRunId };
210
+ store[LAST_SNAPSHOTS_BY_MESSAGE] = messageCache;
211
+ }
212
+ }
213
+
214
+ return { canRender: true };
215
+ }
@@ -324,7 +324,8 @@ export function useCopilotChatInternal({
324
324
  if (error instanceof AGUIConnectNotImplementedError) {
325
325
  // connect not implemented, ignore
326
326
  } else {
327
- throw error;
327
+ console.error("CopilotChat: connectAgent failed", error);
328
+ // Error will be reported through subscription
328
329
  }
329
330
  }
330
331
  };
@@ -412,7 +413,12 @@ export function useCopilotChatInternal({
412
413
  agent?.setMessages(historyCutoff);
413
414
 
414
415
  if (agent) {
415
- copilotkit.runAgent({ agent });
416
+ try {
417
+ await copilotkit.runAgent({ agent });
418
+ } catch (error) {
419
+ console.error("CopilotChat: runAgent failed during reload", error);
420
+ // Error will be reported through subscription
421
+ }
416
422
  }
417
423
  return;
418
424
  },
@@ -451,6 +457,7 @@ export function useCopilotChatInternal({
451
457
  await copilotkit.runAgent({ agent });
452
458
  } catch (error) {
453
459
  console.error("CopilotChat: runAgent failed", error);
460
+ // Error will be reported through subscription
454
461
  }
455
462
  }
456
463
  },
@@ -526,46 +533,92 @@ export function useCopilotChatInternal({
526
533
  const bridgeRenderer =
527
534
  legacyCustomMessageRenderer || renderCustomMessage
528
535
  ? () => {
529
- const customRender = renderCustomMessage?.({
530
- message,
531
- position: "before",
532
- });
533
- if (customRender) {
534
- return customRender;
536
+ if (legacyCustomMessageRenderer) {
537
+ return legacyCustomMessageRenderer({ message, position: "before" });
538
+ }
539
+ try {
540
+ return renderCustomMessage?.({ message, position: "before" }) ?? null;
541
+ } catch (error) {
542
+ console.warn(
543
+ "[CopilotKit] renderCustomMessages failed, falling back to legacy renderer",
544
+ error,
545
+ );
546
+ return null;
535
547
  }
536
- return legacyCustomMessageRenderer?.({ message, position: "before" });
537
548
  }
538
549
  : null;
539
550
 
540
551
  if (bridgeRenderer) {
541
- return { ...message, generativeUI: bridgeRenderer };
552
+ // Attach a position so react-ui can render the custom UI above the assistant content.
553
+ return {
554
+ ...message,
555
+ generativeUI: bridgeRenderer,
556
+ generativeUIPosition: "before" as const,
557
+ };
542
558
  }
543
559
  return message;
544
560
  });
545
561
 
546
562
  const hasAssistantMessages = processedMessages.some((msg) => msg.role === "assistant");
547
-
548
- // TODO: what is this?
549
- // if (legacyCustomMessageRenderer && !hasAssistantMessages) {
550
- // const placeholderId = `coagent-state-render-${resolvedAgentId}`;
551
- // const placeholderMessage: Message = {
552
- // id: placeholderId,
553
- // role: "assistant",
554
- // content: "",
555
- // name: "coagent-state-render",
556
- // };
557
- // processedMessages = [
558
- // ...processedMessages,
559
- // {
560
- // ...placeholderMessage,
561
- // generativeUI: () =>
562
- // legacyCustomMessageRenderer({
563
- // message: placeholderMessage,
564
- // position: "before",
565
- // }),
566
- // } as Message,
567
- // ];
568
- // }
563
+ const canUseCustomRenderer = Boolean(
564
+ renderCustomMessage && copilotkit?.getAgent?.(resolvedAgentId),
565
+ );
566
+ const placeholderRenderer = legacyCustomMessageRenderer
567
+ ? legacyCustomMessageRenderer
568
+ : canUseCustomRenderer
569
+ ? renderCustomMessage
570
+ : null;
571
+
572
+ const shouldRenderPlaceholder =
573
+ Boolean(agent?.isRunning) || Boolean(agent?.state && Object.keys(agent.state).length);
574
+
575
+ const effectiveThreadId = threadId ?? agent?.threadId ?? "default";
576
+ let latestUserIndex = -1;
577
+ for (let i = processedMessages.length - 1; i >= 0; i -= 1) {
578
+ if (processedMessages[i].role === "user") {
579
+ latestUserIndex = i;
580
+ break;
581
+ }
582
+ }
583
+ const latestUserMessageId =
584
+ latestUserIndex >= 0 ? processedMessages[latestUserIndex].id : undefined;
585
+ const currentRunId = latestUserMessageId
586
+ ? copilotkit.getRunIdForMessage(resolvedAgentId, effectiveThreadId, latestUserMessageId) ||
587
+ `pending:${latestUserMessageId}`
588
+ : undefined;
589
+ const hasAssistantForCurrentRun =
590
+ latestUserIndex >= 0
591
+ ? processedMessages
592
+ .slice(latestUserIndex + 1)
593
+ .some((msg) => msg.role === "assistant")
594
+ : hasAssistantMessages;
595
+
596
+ // Insert a placeholder assistant message so state snapshots can render before any
597
+ // assistant text exists for the current run.
598
+ if (placeholderRenderer && shouldRenderPlaceholder && !hasAssistantForCurrentRun) {
599
+ const placeholderId = currentRunId
600
+ ? `coagent-state-render-${resolvedAgentId}-${currentRunId}`
601
+ : `coagent-state-render-${resolvedAgentId}`;
602
+ const placeholderMessage: Message = {
603
+ id: placeholderId,
604
+ role: "assistant",
605
+ content: "",
606
+ name: "coagent-state-render",
607
+ runId: currentRunId,
608
+ };
609
+ processedMessages = [
610
+ ...processedMessages,
611
+ {
612
+ ...placeholderMessage,
613
+ generativeUIPosition: "before" as const,
614
+ generativeUI: () =>
615
+ placeholderRenderer({
616
+ message: placeholderMessage,
617
+ position: "before",
618
+ }),
619
+ } as Message,
620
+ ];
621
+ }
569
622
 
570
623
  return processedMessages;
571
624
  }, [
@@ -573,8 +626,11 @@ export function useCopilotChatInternal({
573
626
  lazyToolRendered,
574
627
  allMessages,
575
628
  renderCustomMessage,
576
- // legacyCustomMessageRenderer,
629
+ legacyCustomMessageRenderer,
577
630
  resolvedAgentId,
631
+ copilotkit,
632
+ agent?.isRunning,
633
+ agent?.state,
578
634
  ]);
579
635
 
580
636
  const renderedSuggestions = useMemo(() => {
@@ -650,7 +706,10 @@ function useLegacyCoagentRenderer({
650
706
 
651
707
  return ({ message, position }: LegacyRenderParams) => {
652
708
  const effectiveThreadId = threadId ?? agent.threadId ?? "default";
653
- const existingRunId = copilotkit.getRunIdForMessage(agentId, effectiveThreadId, message.id);
709
+ const providedRunId = (message as any).runId as string | undefined;
710
+ const existingRunId = providedRunId
711
+ ? providedRunId
712
+ : copilotkit.getRunIdForMessage(agentId, effectiveThreadId, message.id);
654
713
  const runId = existingRunId || `pending:${message.id}`;
655
714
  const messageIndex = Math.max(
656
715
  agent.messages.findIndex((msg) => msg.id === message.id),
package/src/setupTests.ts CHANGED
@@ -17,6 +17,7 @@ jest.mock("@copilotkit/shared", () => ({
17
17
  return defaultValue;
18
18
  }
19
19
  }),
20
+ dataToUUID: jest.fn((data) => JSON.stringify(data)),
20
21
  randomId: jest.fn(() => "test-random-id"),
21
22
  CopilotKitAgentDiscoveryError: jest.fn(),
22
23
  randomUUID: jest.fn(() => "mock-thread-id"),
@@ -0,0 +1,91 @@
1
+ import type { CopilotContextParams, CopilotApiConfig } from "../context";
2
+
3
+ const noop = () => {};
4
+
5
+ const copilotApiConfig: CopilotApiConfig = {
6
+ chatApiEndpoint: "http://localhost",
7
+ headers: {},
8
+ };
9
+
10
+ export function createTestCopilotContext(
11
+ overrides: Partial<CopilotContextParams> = {},
12
+ ): CopilotContextParams {
13
+ return {
14
+ actions: {},
15
+ setAction: noop,
16
+ removeAction: noop,
17
+
18
+ setRegisteredActions: () => "action-id",
19
+ removeRegisteredAction: noop,
20
+
21
+ chatComponentsCache: { current: { actions: {}, coAgentStateRenders: {} } },
22
+ getFunctionCallHandler: () => async () => {},
23
+
24
+ addContext: () => "context-id",
25
+ removeContext: noop,
26
+ getAllContext: () => [],
27
+ getContextString: () => "",
28
+
29
+ addDocumentContext: () => "document-id",
30
+ removeDocumentContext: noop,
31
+ getDocumentsContext: () => [],
32
+
33
+ isLoading: false,
34
+ setIsLoading: noop,
35
+
36
+ chatSuggestionConfiguration: {},
37
+ addChatSuggestionConfiguration: noop,
38
+ removeChatSuggestionConfiguration: noop,
39
+
40
+ chatInstructions: "",
41
+ setChatInstructions: noop,
42
+
43
+ additionalInstructions: [],
44
+ setAdditionalInstructions: noop,
45
+
46
+ copilotApiConfig,
47
+
48
+ showDevConsole: false,
49
+
50
+ coagentStates: {},
51
+ setCoagentStates: noop,
52
+ coagentStatesRef: { current: {} },
53
+ setCoagentStatesWithRef: noop,
54
+
55
+ agentSession: null,
56
+ setAgentSession: noop,
57
+
58
+ agentLock: null,
59
+
60
+ threadId: "",
61
+ setThreadId: noop,
62
+
63
+ runId: null,
64
+ setRunId: noop,
65
+
66
+ chatAbortControllerRef: { current: null },
67
+
68
+ forwardedParameters: {},
69
+ availableAgents: [],
70
+
71
+ extensions: {},
72
+ setExtensions: noop,
73
+
74
+ interruptActions: {},
75
+ setInterruptAction: noop,
76
+ removeInterruptAction: noop,
77
+ interruptEventQueue: {},
78
+ addInterruptEvent: noop,
79
+ resolveInterruptEvent: noop,
80
+
81
+ onError: noop,
82
+
83
+ bannerError: null,
84
+ setBannerError: noop,
85
+ internalErrorHandlers: {},
86
+ setInternalErrorHandler: noop,
87
+ removeInternalErrorHandler: noop,
88
+
89
+ ...overrides,
90
+ };
91
+ }