@copilotkit/react-core 1.4.8-no-pino-redact.1 → 1.5.0-coagents-v0-3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/dist/{chunk-JHEAUB3Z.mjs → chunk-35EN6BG4.mjs} +2 -2
  3. package/dist/{chunk-JHEAUB3Z.mjs.map → chunk-35EN6BG4.mjs.map} +1 -1
  4. package/dist/{chunk-O22KGHOQ.mjs → chunk-42N5VKIX.mjs} +38 -16
  5. package/dist/chunk-42N5VKIX.mjs.map +1 -0
  6. package/dist/{chunk-AG7FH7OD.mjs → chunk-5FYKUKG3.mjs} +2 -2
  7. package/dist/{chunk-ODN4H66E.mjs → chunk-7LRDVJH5.mjs} +6 -2
  8. package/dist/chunk-7LRDVJH5.mjs.map +1 -0
  9. package/dist/{chunk-Y7MI4PBB.mjs → chunk-ALR5W5JK.mjs} +22 -10
  10. package/dist/chunk-ALR5W5JK.mjs.map +1 -0
  11. package/dist/{chunk-XBVKTDXP.mjs → chunk-BT6WK2JZ.mjs} +57 -17
  12. package/dist/chunk-BT6WK2JZ.mjs.map +1 -0
  13. package/dist/chunk-QCUP6HLK.mjs +37 -0
  14. package/dist/chunk-QCUP6HLK.mjs.map +1 -0
  15. package/dist/chunk-QTDCEDOC.mjs +392 -0
  16. package/dist/chunk-QTDCEDOC.mjs.map +1 -0
  17. package/dist/{chunk-OT67R4NB.mjs → chunk-QX6V774L.mjs} +6 -8
  18. package/dist/chunk-QX6V774L.mjs.map +1 -0
  19. package/dist/{chunk-2KCEHGSI.mjs → chunk-SFPANIOY.mjs} +99 -49
  20. package/dist/chunk-SFPANIOY.mjs.map +1 -0
  21. package/dist/{chunk-XQFVXX6R.mjs → chunk-TQN3EZWQ.mjs} +10 -2
  22. package/dist/chunk-TQN3EZWQ.mjs.map +1 -0
  23. package/dist/{chunk-UOVONDR6.mjs → chunk-V3PFWGIY.mjs} +2 -2
  24. package/dist/{chunk-DHGXL5PC.mjs → chunk-VMP6JWBB.mjs} +18 -6
  25. package/dist/chunk-VMP6JWBB.mjs.map +1 -0
  26. package/dist/chunk-XERJQUHA.mjs +31 -0
  27. package/dist/chunk-XERJQUHA.mjs.map +1 -0
  28. package/dist/components/copilot-provider/copilotkit.js +175 -93
  29. package/dist/components/copilot-provider/copilotkit.js.map +1 -1
  30. package/dist/components/copilot-provider/copilotkit.mjs +5 -4
  31. package/dist/components/copilot-provider/index.js +175 -93
  32. package/dist/components/copilot-provider/index.js.map +1 -1
  33. package/dist/components/copilot-provider/index.mjs +5 -4
  34. package/dist/components/error-boundary/error-boundary.d.ts +22 -0
  35. package/dist/components/error-boundary/error-boundary.js +183 -0
  36. package/dist/components/error-boundary/error-boundary.js.map +1 -0
  37. package/dist/components/error-boundary/error-boundary.mjs +12 -0
  38. package/dist/components/error-boundary/error-boundary.mjs.map +1 -0
  39. package/dist/components/error-boundary/error-utils.d.ts +11 -0
  40. package/dist/components/error-boundary/error-utils.js +177 -0
  41. package/dist/components/error-boundary/error-utils.js.map +1 -0
  42. package/dist/components/error-boundary/error-utils.mjs +13 -0
  43. package/dist/components/error-boundary/error-utils.mjs.map +1 -0
  44. package/dist/components/index.js +175 -93
  45. package/dist/components/index.js.map +1 -1
  46. package/dist/components/index.mjs +5 -4
  47. package/dist/components/toast/toast-provider.d.ts +2 -1
  48. package/dist/components/toast/toast-provider.js +76 -62
  49. package/dist/components/toast/toast-provider.js.map +1 -1
  50. package/dist/components/toast/toast-provider.mjs +1 -1
  51. package/dist/context/copilot-context.d.ts +8 -2
  52. package/dist/context/copilot-context.js +9 -1
  53. package/dist/context/copilot-context.js.map +1 -1
  54. package/dist/context/copilot-context.mjs +1 -1
  55. package/dist/context/index.d.ts +1 -1
  56. package/dist/context/index.js +9 -1
  57. package/dist/context/index.js.map +1 -1
  58. package/dist/context/index.mjs +1 -1
  59. package/dist/hooks/index.d.ts +2 -1
  60. package/dist/hooks/index.js +584 -270
  61. package/dist/hooks/index.js.map +1 -1
  62. package/dist/hooks/index.mjs +19 -11
  63. package/dist/hooks/use-chat.d.ts +20 -0
  64. package/dist/hooks/use-chat.js +438 -200
  65. package/dist/hooks/use-chat.js.map +1 -1
  66. package/dist/hooks/use-chat.mjs +4 -3
  67. package/dist/hooks/use-coagent-state-render.d.ts +2 -2
  68. package/dist/hooks/use-coagent-state-render.js +9 -1
  69. package/dist/hooks/use-coagent-state-render.js.map +1 -1
  70. package/dist/hooks/use-coagent-state-render.mjs +2 -2
  71. package/dist/hooks/use-coagent.d.ts +14 -1
  72. package/dist/hooks/use-coagent.js +547 -242
  73. package/dist/hooks/use-coagent.js.map +1 -1
  74. package/dist/hooks/use-coagent.mjs +15 -7
  75. package/dist/hooks/use-copilot-action.d.ts +12 -2
  76. package/dist/hooks/use-copilot-action.js +156 -14
  77. package/dist/hooks/use-copilot-action.js.map +1 -1
  78. package/dist/hooks/use-copilot-action.mjs +4 -2
  79. package/dist/hooks/use-copilot-chat.d.ts +1 -0
  80. package/dist/hooks/use-copilot-chat.js +514 -231
  81. package/dist/hooks/use-copilot-chat.js.map +1 -1
  82. package/dist/hooks/use-copilot-chat.mjs +8 -6
  83. package/dist/hooks/use-copilot-readable.js +9 -1
  84. package/dist/hooks/use-copilot-readable.js.map +1 -1
  85. package/dist/hooks/use-copilot-readable.mjs +2 -2
  86. package/dist/hooks/use-copilot-runtime-client.js +110 -4
  87. package/dist/hooks/use-copilot-runtime-client.js.map +1 -1
  88. package/dist/hooks/use-copilot-runtime-client.mjs +2 -2
  89. package/dist/hooks/use-make-copilot-document-readable.js +9 -1
  90. package/dist/hooks/use-make-copilot-document-readable.js.map +1 -1
  91. package/dist/hooks/use-make-copilot-document-readable.mjs +2 -2
  92. package/dist/index.d.ts +2 -2
  93. package/dist/index.js +642 -363
  94. package/dist/index.js.map +1 -1
  95. package/dist/index.mjs +20 -12
  96. package/dist/lib/copilot-task.d.ts +1 -1
  97. package/dist/lib/copilot-task.js +33 -13
  98. package/dist/lib/copilot-task.js.map +1 -1
  99. package/dist/lib/copilot-task.mjs +7 -5
  100. package/dist/lib/index.d.ts +1 -1
  101. package/dist/lib/index.js +33 -13
  102. package/dist/lib/index.js.map +1 -1
  103. package/dist/lib/index.mjs +7 -5
  104. package/dist/types/frontend-action.d.ts +21 -2
  105. package/dist/types/frontend-action.js +34 -0
  106. package/dist/types/frontend-action.js.map +1 -1
  107. package/dist/types/frontend-action.mjs +7 -0
  108. package/dist/types/index.d.ts +2 -1
  109. package/dist/types/index.js.map +1 -1
  110. package/dist/utils/extract.js.map +1 -1
  111. package/dist/utils/extract.mjs +5 -4
  112. package/dist/utils/index.js.map +1 -1
  113. package/dist/utils/index.mjs +5 -4
  114. package/package.json +3 -3
  115. package/src/components/copilot-provider/copilotkit.tsx +14 -1
  116. package/src/components/error-boundary/error-boundary.tsx +42 -0
  117. package/src/components/error-boundary/error-utils.tsx +95 -0
  118. package/src/components/toast/toast-provider.tsx +10 -49
  119. package/src/context/copilot-context.tsx +30 -2
  120. package/src/hooks/index.ts +1 -1
  121. package/src/hooks/use-chat.ts +391 -256
  122. package/src/hooks/use-coagent-state-render.ts +2 -2
  123. package/src/hooks/use-coagent.ts +35 -15
  124. package/src/hooks/use-copilot-action.ts +41 -12
  125. package/src/hooks/use-copilot-chat.ts +52 -11
  126. package/src/hooks/use-copilot-runtime-client.ts +4 -0
  127. package/src/lib/copilot-task.ts +2 -8
  128. package/src/types/frontend-action.ts +55 -2
  129. package/src/types/index.ts +5 -1
  130. package/dist/chunk-2KCEHGSI.mjs.map +0 -1
  131. package/dist/chunk-DHGXL5PC.mjs.map +0 -1
  132. package/dist/chunk-EUIBVFV6.mjs +0 -294
  133. package/dist/chunk-EUIBVFV6.mjs.map +0 -1
  134. package/dist/chunk-O22KGHOQ.mjs.map +0 -1
  135. package/dist/chunk-ODN4H66E.mjs.map +0 -1
  136. package/dist/chunk-OT67R4NB.mjs.map +0 -1
  137. package/dist/chunk-XBVKTDXP.mjs.map +0 -1
  138. package/dist/chunk-XQFVXX6R.mjs.map +0 -1
  139. package/dist/chunk-Y7MI4PBB.mjs.map +0 -1
  140. /package/dist/{chunk-AG7FH7OD.mjs.map → chunk-5FYKUKG3.mjs.map} +0 -0
  141. /package/dist/{chunk-UOVONDR6.mjs.map → chunk-V3PFWGIY.mjs.map} +0 -0
@@ -18,14 +18,16 @@ import {
18
18
  Role,
19
19
  CopilotRequestType,
20
20
  ActionInputAvailability,
21
+ loadMessagesFromJsonRepresentation,
21
22
  } from "@copilotkit/runtime-client-gql";
22
23
 
23
24
  import { CopilotApiConfig } from "../context";
24
- import { FrontendAction } from "../types/frontend-action";
25
+ import { FrontendAction, processActionsForRuntimeRequest } from "../types/frontend-action";
25
26
  import { CoagentState } from "../types/coagent-state";
26
27
  import { AgentSession } from "../context/copilot-context";
27
28
  import { useToast } from "../components/toast/toast-provider";
28
29
  import { useCopilotRuntimeClient } from "./use-copilot-runtime-client";
30
+ import { useAsyncCallback } from "../components/error-boundary/error-utils";
29
31
 
30
32
  export type UseChatOptions = {
31
33
  /**
@@ -97,6 +99,27 @@ export type UseChatOptions = {
97
99
  * setState-powered method to update the agent session
98
100
  */
99
101
  setAgentSession: React.Dispatch<React.SetStateAction<AgentSession | null>>;
102
+
103
+ /**
104
+ * The current thread ID.
105
+ */
106
+ threadId: string | null;
107
+ /**
108
+ * set the current thread ID
109
+ */
110
+ setThreadId: (threadId: string | null) => void;
111
+ /**
112
+ * The current run ID.
113
+ */
114
+ runId: string | null;
115
+ /**
116
+ * set the current run ID
117
+ */
118
+ setRunId: (runId: string | null) => void;
119
+ /**
120
+ * The global chat abort controller.
121
+ */
122
+ chatAbortControllerRef: React.MutableRefObject<AbortController | null>;
100
123
  };
101
124
 
102
125
  export type UseChatHelpers = {
@@ -139,19 +162,23 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
139
162
  coagentStatesRef,
140
163
  agentSession,
141
164
  setAgentSession,
165
+ threadId,
166
+ setThreadId,
167
+ runId,
168
+ setRunId,
169
+ chatAbortControllerRef,
142
170
  } = options;
143
-
144
- const abortControllerRef = useRef<AbortController>();
145
- const threadIdRef = useRef<string | null>(null);
146
- const runIdRef = useRef<string | null>(null);
147
171
  const { addGraphQLErrorsToast } = useToast();
148
-
149
172
  const runChatCompletionRef = useRef<(previousMessages: Message[]) => Promise<Message[]>>();
150
173
  // We need to keep a ref of coagent states and session because of renderAndWait - making sure
151
174
  // the latest state is sent to the API
152
175
  // This is a workaround and needs to be addressed in the future
153
176
  const agentSessionRef = useRef<AgentSession | null>(agentSession);
154
177
  agentSessionRef.current = agentSession;
178
+ const threadIdRef = useRef<string | null>(threadId);
179
+ threadIdRef.current = threadId;
180
+ const runIdRef = useRef<string | null>(runId);
181
+ runIdRef.current = runId;
155
182
 
156
183
  const publicApiKey = copilotConfig.publicApiKey;
157
184
 
@@ -167,292 +194,371 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
167
194
  credentials: copilotConfig.credentials,
168
195
  });
169
196
 
170
- const runChatCompletion = async (previousMessages: Message[]): Promise<Message[]> => {
171
- setIsLoading(true);
172
-
173
- // this message is just a placeholder. It will disappear once the first real message
174
- // is received
175
- let newMessages: Message[] = [
176
- new TextMessage({
177
- content: "",
178
- role: Role.Assistant,
179
- }),
180
- ];
181
- const abortController = new AbortController();
182
- abortControllerRef.current = abortController;
183
-
184
- setMessages([...previousMessages, ...newMessages]);
185
-
186
- const systemMessage = makeSystemMessageCallback();
187
-
188
- const messagesWithContext = [systemMessage, ...(initialMessages || []), ...previousMessages];
189
-
190
- const stream = runtimeClient.asStream(
191
- runtimeClient.generateCopilotResponse({
192
- data: {
193
- frontend: {
194
- actions: actions
195
- .filter(
196
- (action) =>
197
- action.available !== ActionInputAvailability.Disabled || !action.disabled,
198
- )
199
- .map((action) => {
200
- let available: ActionInputAvailability | undefined =
201
- ActionInputAvailability.Enabled;
202
- if (action.disabled) {
203
- available = ActionInputAvailability.Disabled;
204
- } else if (action.available === "disabled") {
205
- available = ActionInputAvailability.Disabled;
206
- } else if (action.available === "remote") {
207
- available = ActionInputAvailability.Remote;
208
- }
209
- return {
210
- name: action.name,
211
- description: action.description || "",
212
- jsonSchema: JSON.stringify(actionParametersToJsonSchema(action.parameters || [])),
213
- available,
214
- };
215
- }),
216
- url: window.location.href,
217
- },
218
- threadId: threadIdRef.current,
219
- runId: runIdRef.current,
220
- messages: convertMessagesToGqlInput(filterAgentStateMessages(messagesWithContext)),
221
- ...(copilotConfig.cloud
222
- ? {
223
- cloud: {
224
- ...(copilotConfig.cloud.guardrails?.input?.restrictToTopic?.enabled
225
- ? {
226
- guardrails: {
227
- inputValidationRules: {
228
- allowList:
229
- copilotConfig.cloud.guardrails.input.restrictToTopic.validTopics,
230
- denyList:
231
- copilotConfig.cloud.guardrails.input.restrictToTopic.invalidTopics,
197
+ const runChatCompletion = useAsyncCallback(
198
+ async (previousMessages: Message[]): Promise<Message[]> => {
199
+ setIsLoading(true);
200
+
201
+ // this message is just a placeholder. It will disappear once the first real message
202
+ // is received
203
+ let newMessages: Message[] = [
204
+ new TextMessage({
205
+ content: "",
206
+ role: Role.Assistant,
207
+ }),
208
+ ];
209
+
210
+ chatAbortControllerRef.current = new AbortController();
211
+
212
+ setMessages([...previousMessages, ...newMessages]);
213
+
214
+ const systemMessage = makeSystemMessageCallback();
215
+
216
+ const messagesWithContext = [systemMessage, ...(initialMessages || []), ...previousMessages];
217
+
218
+ const isAgentRun = agentSessionRef.current !== null;
219
+
220
+ const stream = runtimeClient.asStream(
221
+ runtimeClient.generateCopilotResponse({
222
+ data: {
223
+ frontend: {
224
+ actions: processActionsForRuntimeRequest(actions),
225
+ url: window.location.href,
226
+ },
227
+ threadId: threadIdRef.current,
228
+ runId: runIdRef.current,
229
+ messages: convertMessagesToGqlInput(filterAgentStateMessages(messagesWithContext)),
230
+ ...(copilotConfig.cloud
231
+ ? {
232
+ cloud: {
233
+ ...(copilotConfig.cloud.guardrails?.input?.restrictToTopic?.enabled
234
+ ? {
235
+ guardrails: {
236
+ inputValidationRules: {
237
+ allowList:
238
+ copilotConfig.cloud.guardrails.input.restrictToTopic.validTopics,
239
+ denyList:
240
+ copilotConfig.cloud.guardrails.input.restrictToTopic.invalidTopics,
241
+ },
232
242
  },
233
- },
234
- }
235
- : {}),
236
- },
237
- }
238
- : {}),
239
- metadata: {
240
- requestType: CopilotRequestType.Chat,
243
+ }
244
+ : {}),
245
+ },
246
+ }
247
+ : {}),
248
+ metadata: {
249
+ requestType: CopilotRequestType.Chat,
250
+ },
251
+ ...(agentSessionRef.current
252
+ ? {
253
+ agentSession: agentSessionRef.current,
254
+ }
255
+ : {}),
256
+ agentStates: Object.values(coagentStatesRef.current!).map((state) => ({
257
+ agentName: state.name,
258
+ state: JSON.stringify(state.state),
259
+ })),
241
260
  },
242
- ...(agentSessionRef.current
243
- ? {
244
- agentSession: agentSessionRef.current,
245
- }
246
- : {}),
247
- agentStates: Object.values(coagentStatesRef.current!).map((state) => ({
248
- agentName: state.name,
249
- state: JSON.stringify(state.state),
250
- })),
251
- },
252
- properties: copilotConfig.properties,
253
- signal: abortControllerRef.current?.signal,
254
- }),
255
- );
256
-
257
- const guardrailsEnabled =
258
- copilotConfig.cloud?.guardrails?.input?.restrictToTopic.enabled || false;
259
-
260
- const reader = stream.getReader();
261
-
262
- let actionResults: { [id: string]: string } = {};
263
- let executedCoAgentStateRenders: string[] = [];
264
- let followUp: FrontendAction["followUp"] = undefined;
265
-
266
- try {
267
- while (true) {
268
- let done, value;
269
-
270
- try {
271
- const readResult = await reader.read();
272
- done = readResult.done;
273
- value = readResult.value;
274
- } catch (readError) {
275
- break;
276
- }
261
+ properties: copilotConfig.properties,
262
+ signal: chatAbortControllerRef.current?.signal,
263
+ }),
264
+ );
277
265
 
278
- if (done) {
279
- break;
280
- }
266
+ const guardrailsEnabled =
267
+ copilotConfig.cloud?.guardrails?.input?.restrictToTopic.enabled || false;
281
268
 
282
- if (!value?.generateCopilotResponse) {
283
- continue;
284
- }
269
+ const reader = stream.getReader();
285
270
 
286
- threadIdRef.current = value.generateCopilotResponse.threadId || null;
287
- runIdRef.current = value.generateCopilotResponse.runId || null;
271
+ let executedCoAgentStateRenders: string[] = [];
272
+ let followUp: FrontendAction["followUp"] = undefined;
288
273
 
289
- const messages = convertGqlOutputToMessages(
290
- filterAdjacentAgentStateMessages(value.generateCopilotResponse.messages),
291
- );
274
+ let messages: Message[] = [];
275
+ let syncedMessages: Message[] = [];
292
276
 
293
- if (messages.length === 0) {
294
- continue;
295
- }
277
+ try {
278
+ while (true) {
279
+ let done, value;
296
280
 
297
- newMessages = [];
281
+ try {
282
+ const readResult = await reader.read();
283
+ done = readResult.done;
284
+ value = readResult.value;
285
+ } catch (readError) {
286
+ break;
287
+ }
298
288
 
299
- // request failed, display error message
300
- if (
301
- value.generateCopilotResponse.status?.__typename === "FailedResponseStatus" &&
302
- value.generateCopilotResponse.status.reason === "GUARDRAILS_VALIDATION_FAILED"
303
- ) {
304
- newMessages = [
305
- new TextMessage({
306
- role: MessageRole.Assistant,
307
- content: value.generateCopilotResponse.status.details?.guardrailsReason || "",
308
- }),
309
- ];
289
+ if (done) {
290
+ if (chatAbortControllerRef.current.signal.aborted) {
291
+ return [];
292
+ }
293
+ break;
294
+ }
295
+
296
+ if (!value?.generateCopilotResponse) {
297
+ continue;
298
+ }
299
+
300
+ threadIdRef.current = value.generateCopilotResponse.threadId || null;
301
+ runIdRef.current = value.generateCopilotResponse.runId || null;
302
+
303
+ setThreadId(threadIdRef.current);
304
+ setRunId(runIdRef.current);
305
+
306
+ messages = convertGqlOutputToMessages(
307
+ filterAdjacentAgentStateMessages(value.generateCopilotResponse.messages),
308
+ );
309
+
310
+ if (messages.length === 0) {
311
+ continue;
312
+ }
313
+
314
+ newMessages = [];
315
+
316
+ // request failed, display error message and quit
317
+ if (
318
+ value.generateCopilotResponse.status?.__typename === "FailedResponseStatus" &&
319
+ value.generateCopilotResponse.status.reason === "GUARDRAILS_VALIDATION_FAILED"
320
+ ) {
321
+ newMessages = [
322
+ new TextMessage({
323
+ role: MessageRole.Assistant,
324
+ content: value.generateCopilotResponse.status.details?.guardrailsReason || "",
325
+ }),
326
+ ];
327
+ setMessages([...previousMessages, ...newMessages]);
328
+ break;
329
+ }
330
+
331
+ // add messages to the chat
332
+ else {
333
+ newMessages = [...messages];
334
+
335
+ for (const message of messages) {
336
+ // execute onCoAgentStateRender handler
337
+ if (
338
+ message.isAgentStateMessage() &&
339
+ !message.active &&
340
+ !executedCoAgentStateRenders.includes(message.id) &&
341
+ onCoAgentStateRender
342
+ ) {
343
+ // Do not execute a coagent action if guardrails are enabled but the status is not known
344
+ if (guardrailsEnabled && value.generateCopilotResponse.status === undefined) {
345
+ break;
346
+ }
347
+ // execute coagent action
348
+ await onCoAgentStateRender({
349
+ name: message.agentName,
350
+ nodeName: message.nodeName,
351
+ state: message.state,
352
+ });
353
+ executedCoAgentStateRenders.push(message.id);
354
+ }
355
+ }
356
+
357
+ const lastAgentStateMessage = [...messages]
358
+ .reverse()
359
+ .find((message) => message.isAgentStateMessage());
360
+
361
+ if (lastAgentStateMessage) {
362
+ if (
363
+ lastAgentStateMessage.state.messages &&
364
+ lastAgentStateMessage.state.messages.length > 0
365
+ ) {
366
+ syncedMessages = loadMessagesFromJsonRepresentation(
367
+ lastAgentStateMessage.state.messages,
368
+ );
369
+ }
370
+ setCoagentStatesWithRef((prevAgentStates) => ({
371
+ ...prevAgentStates,
372
+ [lastAgentStateMessage.agentName]: {
373
+ name: lastAgentStateMessage.agentName,
374
+ state: lastAgentStateMessage.state,
375
+ running: lastAgentStateMessage.running,
376
+ active: lastAgentStateMessage.active,
377
+ threadId: lastAgentStateMessage.threadId,
378
+ nodeName: lastAgentStateMessage.nodeName,
379
+ runId: lastAgentStateMessage.runId,
380
+ },
381
+ }));
382
+ if (lastAgentStateMessage.running) {
383
+ setAgentSession({
384
+ threadId: lastAgentStateMessage.threadId,
385
+ agentName: lastAgentStateMessage.agentName,
386
+ nodeName: lastAgentStateMessage.nodeName,
387
+ });
388
+ } else {
389
+ setAgentSession(null);
390
+ }
391
+ }
392
+ }
393
+
394
+ if (newMessages.length > 0) {
395
+ // Update message state
396
+ setMessages([...previousMessages, ...newMessages]);
397
+ }
310
398
  }
399
+ const finalMessages = constructFinalMessages(syncedMessages, previousMessages, newMessages);
400
+
401
+ let didExecuteAction = false;
311
402
 
312
- // add messages to the chat
313
- else {
314
- for (const message of messages) {
315
- newMessages.push(message);
316
- // execute regular action executions
403
+ // execute regular action executions that are specific to the frontend (last actions)
404
+ if (onFunctionCall) {
405
+ // Find consecutive action execution messages at the end
406
+ const lastMessages = [];
407
+ for (let i = finalMessages.length - 1; i >= 0; i--) {
408
+ const message = finalMessages[i];
317
409
  if (
318
410
  message.isActionExecutionMessage() &&
319
- message.status.code !== MessageStatusCode.Pending &&
320
- message.scope === "client" &&
321
- onFunctionCall
411
+ message.status.code !== MessageStatusCode.Pending
322
412
  ) {
323
- if (!(message.id in actionResults)) {
324
- // Do not execute a function call if guardrails are enabled but the status is not known
325
- if (guardrailsEnabled && value.generateCopilotResponse.status === undefined) {
326
- break;
327
- }
328
- // execute action
329
- try {
330
- // We update the message state before calling the handler so that the render
331
- // function can be called with `executing` state
332
- setMessages([...previousMessages, ...newMessages]);
413
+ lastMessages.unshift(message);
414
+ } else {
415
+ break;
416
+ }
417
+ }
333
418
 
334
- const action = actions.find((action) => action.name === message.name);
419
+ for (const message of lastMessages) {
420
+ // We update the message state before calling the handler so that the render
421
+ // function can be called with `executing` state
422
+ setMessages(finalMessages);
335
423
 
336
- if (action) {
337
- followUp = action.followUp;
338
- }
424
+ const action = actions.find((action) => action.name === message.name);
339
425
 
340
- const result = await onFunctionCall({
426
+ if (action) {
427
+ followUp = action.followUp;
428
+ let result: any;
429
+ try {
430
+ result = await Promise.race([
431
+ onFunctionCall({
341
432
  messages: previousMessages,
342
433
  name: message.name,
343
434
  args: message.arguments,
344
- });
345
- actionResults[message.id] = result;
346
- } catch (e) {
347
- actionResults[message.id] = `Failed to execute action ${message.name}`;
348
- console.error(`Failed to execute action ${message.name}: ${e}`);
349
- }
435
+ }),
436
+ new Promise((resolve) =>
437
+ chatAbortControllerRef.current?.signal.addEventListener("abort", () =>
438
+ resolve("Operation was aborted by the user"),
439
+ ),
440
+ ),
441
+ // if the user stopped generation, we also abort consecutive actions
442
+ new Promise((resolve) => {
443
+ if (chatAbortControllerRef.current?.signal.aborted) {
444
+ resolve("Operation was aborted by the user");
445
+ }
446
+ }),
447
+ ]);
448
+ } catch (e) {
449
+ result = `Failed to execute action ${message.name}`;
450
+ console.error(`Failed to execute action ${message.name}: ${e}`);
350
451
  }
351
- // add the result message
352
- newMessages.push(
452
+ didExecuteAction = true;
453
+ const messageIndex = finalMessages.findIndex((msg) => msg.id === message.id);
454
+ finalMessages.splice(
455
+ messageIndex + 1,
456
+ 0,
353
457
  new ResultMessage({
354
- result: ResultMessage.encodeResult(actionResults[message.id]),
458
+ id: "result-" + message.id,
459
+ result: ResultMessage.encodeResult(result),
355
460
  actionExecutionId: message.id,
356
461
  actionName: message.name,
357
462
  }),
358
463
  );
359
464
  }
360
- // execute coagent actions
361
- if (
362
- message.isAgentStateMessage() &&
363
- !message.active &&
364
- !executedCoAgentStateRenders.includes(message.id) &&
365
- onCoAgentStateRender
366
- ) {
367
- // Do not execute a coagent action if guardrails are enabled but the status is not known
368
- if (guardrailsEnabled && value.generateCopilotResponse.status === undefined) {
369
- break;
370
- }
371
- // execute coagent action
372
- await onCoAgentStateRender({
373
- name: message.agentName,
374
- nodeName: message.nodeName,
375
- state: message.state,
376
- });
377
- executedCoAgentStateRenders.push(message.id);
378
- }
379
465
  }
380
466
 
381
- const lastAgentStateMessage = [...messages]
382
- .reverse()
383
- .find((message) => message.isAgentStateMessage());
384
-
385
- if (lastAgentStateMessage) {
386
- setCoagentStatesWithRef((prevAgentStates) => ({
387
- ...prevAgentStates,
388
- [lastAgentStateMessage.agentName]: {
389
- name: lastAgentStateMessage.agentName,
390
- state: lastAgentStateMessage.state,
391
- running: lastAgentStateMessage.running,
392
- active: lastAgentStateMessage.active,
393
- threadId: lastAgentStateMessage.threadId,
394
- nodeName: lastAgentStateMessage.nodeName,
395
- runId: lastAgentStateMessage.runId,
396
- },
397
- }));
398
- if (lastAgentStateMessage.running) {
399
- setAgentSession({
400
- threadId: lastAgentStateMessage.threadId,
401
- agentName: lastAgentStateMessage.agentName,
402
- nodeName: lastAgentStateMessage.nodeName,
403
- });
404
- } else {
405
- setAgentSession(null);
406
- }
407
- }
467
+ setMessages(finalMessages);
408
468
  }
409
469
 
410
- if (newMessages.length > 0) {
411
- // Update message state
412
- setMessages([...previousMessages, ...newMessages]);
470
+ if (
471
+ // if followUp is not explicitly false
472
+ followUp !== false &&
473
+ // and we executed an action
474
+ (didExecuteAction ||
475
+ // the last message is a server side result
476
+ (!isAgentRun &&
477
+ finalMessages.length &&
478
+ finalMessages[finalMessages.length - 1].isResultMessage())) &&
479
+ // the user did not stop generation
480
+ !chatAbortControllerRef.current?.signal.aborted
481
+ ) {
482
+ // run the completion again and return the result
483
+
484
+ // wait for next tick to make sure all the react state updates
485
+ // - tried using react-dom's flushSync, but it did not work
486
+ await new Promise((resolve) => setTimeout(resolve, 10));
487
+
488
+ return await runChatCompletionRef.current!(finalMessages);
489
+ } else if (chatAbortControllerRef.current?.signal.aborted) {
490
+ // filter out all the action execution messages that do not have a consecutive matching result message
491
+ const repairedMessages = finalMessages.filter((message, actionExecutionIndex) => {
492
+ if (message.isActionExecutionMessage()) {
493
+ return finalMessages.find(
494
+ (msg, resultIndex) =>
495
+ msg.isResultMessage() &&
496
+ msg.actionExecutionId === message.id &&
497
+ resultIndex === actionExecutionIndex + 1,
498
+ );
499
+ }
500
+ return true;
501
+ });
502
+ const repairedMessageIds = repairedMessages.map((message) => message.id);
503
+ setMessages(repairedMessages);
504
+
505
+ if (agentSessionRef.current?.nodeName) {
506
+ setAgentSession({
507
+ threadId: agentSessionRef.current.threadId,
508
+ agentName: agentSessionRef.current.agentName,
509
+ nodeName: "__end__",
510
+ });
511
+ }
512
+ // only return new messages that were not filtered out
513
+ return newMessages.filter((message) => repairedMessageIds.includes(message.id));
514
+ } else {
515
+ return newMessages.slice();
413
516
  }
517
+ } finally {
518
+ setIsLoading(false);
414
519
  }
415
-
416
- if (
417
- // if followUp is not explicitly false
418
- followUp !== false &&
419
- // if we have client side results
420
- (Object.values(actionResults).length ||
421
- // or the last message we received is a result
422
- (newMessages.length && newMessages[newMessages.length - 1].isResultMessage()))
423
- ) {
424
- // run the completion again and return the result
425
-
426
- // wait for next tick to make sure all the react state updates
427
- // - tried using react-dom's flushSync, but it did not work
428
- await new Promise((resolve) => setTimeout(resolve, 10));
429
-
430
- return await runChatCompletionRef.current!([...previousMessages, ...newMessages]);
431
- } else {
432
- return newMessages.slice();
433
- }
434
- } finally {
435
- setIsLoading(false);
436
- }
437
- };
520
+ },
521
+ [
522
+ messages,
523
+ setMessages,
524
+ makeSystemMessageCallback,
525
+ copilotConfig,
526
+ setIsLoading,
527
+ initialMessages,
528
+ isLoading,
529
+ actions,
530
+ onFunctionCall,
531
+ onCoAgentStateRender,
532
+ setCoagentStatesWithRef,
533
+ coagentStatesRef,
534
+ agentSession,
535
+ setAgentSession,
536
+ ],
537
+ );
438
538
 
439
539
  runChatCompletionRef.current = runChatCompletion;
440
540
 
441
- const runChatCompletionAndHandleFunctionCall = async (messages: Message[]): Promise<void> => {
442
- await runChatCompletionRef.current!(messages);
443
- };
541
+ const runChatCompletionAndHandleFunctionCall = useAsyncCallback(
542
+ async (messages: Message[]): Promise<void> => {
543
+ await runChatCompletionRef.current!(messages);
544
+ },
545
+ [messages],
546
+ );
547
+
548
+ const append = useAsyncCallback(
549
+ async (message: Message): Promise<void> => {
550
+ if (isLoading) {
551
+ return;
552
+ }
444
553
 
445
- const append = async (message: Message): Promise<void> => {
446
- if (isLoading) {
447
- return;
448
- }
554
+ const newMessages = [...messages, message];
555
+ setMessages(newMessages);
556
+ return runChatCompletionAndHandleFunctionCall(newMessages);
557
+ },
558
+ [isLoading, messages, setMessages, runChatCompletionAndHandleFunctionCall],
559
+ );
449
560
 
450
- const newMessages = [...messages, message];
451
- setMessages(newMessages);
452
- return runChatCompletionAndHandleFunctionCall(newMessages);
453
- };
454
-
455
- const reload = async (): Promise<void> => {
561
+ const reload = useAsyncCallback(async (): Promise<void> => {
456
562
  if (isLoading || messages.length === 0) {
457
563
  return;
458
564
  }
@@ -466,10 +572,10 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
466
572
  setMessages(newMessages);
467
573
 
468
574
  return runChatCompletionAndHandleFunctionCall(newMessages);
469
- };
575
+ }, [isLoading, messages, setMessages, runChatCompletionAndHandleFunctionCall]);
470
576
 
471
577
  const stop = (): void => {
472
- abortControllerRef.current?.abort();
578
+ chatAbortControllerRef.current?.abort("Stop was called");
473
579
  };
474
580
 
475
581
  return {
@@ -479,3 +585,32 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
479
585
  runChatCompletion: () => runChatCompletionRef.current!(messages),
480
586
  };
481
587
  }
588
+
589
+ function constructFinalMessages(
590
+ syncedMessages: Message[],
591
+ previousMessages: Message[],
592
+ newMessages: Message[],
593
+ ): Message[] {
594
+ const finalMessages =
595
+ syncedMessages.length > 0 ? [...syncedMessages] : [...previousMessages, ...newMessages];
596
+
597
+ if (syncedMessages.length > 0) {
598
+ const messagesWithAgentState = [...previousMessages, ...newMessages];
599
+
600
+ let previousMessageId: string | undefined = undefined;
601
+
602
+ for (const message of messagesWithAgentState) {
603
+ if (message.isAgentStateMessage()) {
604
+ // insert this message into finalMessages after the position of previousMessageId
605
+ const index = finalMessages.findIndex((msg) => msg.id === previousMessageId);
606
+ if (index !== -1) {
607
+ finalMessages.splice(index + 1, 0, message);
608
+ }
609
+ }
610
+
611
+ previousMessageId = message.id;
612
+ }
613
+ }
614
+
615
+ return finalMessages;
616
+ }