@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
|
@@ -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 {
|
|
6
|
-
|
|
7
|
-
|
|
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,
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 ?
|
|
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
|
-
|
|
291
|
-
|
|
195
|
+
stateRender,
|
|
196
|
+
stateRenderId,
|
|
292
197
|
agent?.state,
|
|
293
198
|
agent?.isRunning,
|
|
294
199
|
nodeName,
|
|
295
|
-
effectiveRunId,
|
|
296
200
|
message.id,
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
message,
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|