@copilotkit/react-core 1.9.2-next.2 → 1.9.2-next.21

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 (170) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/dist/{chunk-UHQMV2CE.mjs → chunk-36MGCCPZ.mjs} +2 -2
  3. package/dist/{chunk-CCESTGAM.mjs → chunk-3OQM3NEK.mjs} +2 -2
  4. package/dist/{chunk-OUSWPVDT.mjs → chunk-4URMLOBR.mjs} +4 -4
  5. package/dist/chunk-57K2ZJ5F.mjs +348 -0
  6. package/dist/chunk-57K2ZJ5F.mjs.map +1 -0
  7. package/dist/{chunk-LZDDYZEY.mjs → chunk-5BSUSFHM.mjs} +2 -2
  8. package/dist/{chunk-C6F6EQNA.mjs → chunk-BVK7PLK6.mjs} +2 -2
  9. package/dist/{chunk-CMKIDDQL.mjs → chunk-DDIBJUWK.mjs} +11 -8
  10. package/dist/{chunk-CMKIDDQL.mjs.map → chunk-DDIBJUWK.mjs.map} +1 -1
  11. package/dist/{chunk-NNSXCFQO.mjs → chunk-DGON3GZX.mjs} +42 -8
  12. package/dist/chunk-DGON3GZX.mjs.map +1 -0
  13. package/dist/{chunk-6KGEF242.mjs → chunk-DKZTPL66.mjs} +3 -2
  14. package/dist/chunk-DKZTPL66.mjs.map +1 -0
  15. package/dist/{chunk-LDACFA2B.mjs → chunk-FN3UA2ZE.mjs} +3 -3
  16. package/dist/{chunk-RUY6MLHA.mjs → chunk-JWAXDYOW.mjs} +36 -6
  17. package/dist/chunk-JWAXDYOW.mjs.map +1 -0
  18. package/dist/{chunk-2FW7HH6W.mjs → chunk-KIXKBJUV.mjs} +3 -3
  19. package/dist/{chunk-MGIXEJWG.mjs → chunk-MTAJI7HV.mjs} +181 -68
  20. package/dist/chunk-MTAJI7HV.mjs.map +1 -0
  21. package/dist/{chunk-L6QAOAE4.mjs → chunk-N2M65NJ2.mjs} +69 -25
  22. package/dist/chunk-N2M65NJ2.mjs.map +1 -0
  23. package/dist/{chunk-T42PN5VN.mjs → chunk-NJA5ZLAZ.mjs} +29 -10
  24. package/dist/chunk-NJA5ZLAZ.mjs.map +1 -0
  25. package/dist/{chunk-4I7PLQF7.mjs → chunk-QKEH3O4S.mjs} +5 -5
  26. package/dist/{chunk-FRZZPPIV.mjs → chunk-RAQK4M64.mjs} +2 -2
  27. package/dist/{chunk-QQZLIEXK.mjs → chunk-SJJNFYGQ.mjs} +3 -3
  28. package/dist/{chunk-Q5D5XQFA.mjs → chunk-VDADWRS3.mjs} +2 -2
  29. package/dist/chunk-YAF2LATQ.mjs +310 -0
  30. package/dist/chunk-YAF2LATQ.mjs.map +1 -0
  31. package/dist/components/copilot-provider/copilot-messages.js +40 -5
  32. package/dist/components/copilot-provider/copilot-messages.js.map +1 -1
  33. package/dist/components/copilot-provider/copilot-messages.mjs +3 -3
  34. package/dist/components/copilot-provider/copilotkit-props.d.ts +27 -3
  35. package/dist/components/copilot-provider/copilotkit-props.js.map +1 -1
  36. package/dist/components/copilot-provider/copilotkit.d.ts +1 -1
  37. package/dist/components/copilot-provider/copilotkit.js +385 -254
  38. package/dist/components/copilot-provider/copilotkit.js.map +1 -1
  39. package/dist/components/copilot-provider/copilotkit.mjs +10 -10
  40. package/dist/components/copilot-provider/index.d.ts +1 -1
  41. package/dist/components/copilot-provider/index.js +385 -254
  42. package/dist/components/copilot-provider/index.js.map +1 -1
  43. package/dist/components/copilot-provider/index.mjs +10 -10
  44. package/dist/components/error-boundary/error-boundary.js +135 -146
  45. package/dist/components/error-boundary/error-boundary.js.map +1 -1
  46. package/dist/components/error-boundary/error-boundary.mjs +4 -4
  47. package/dist/components/error-boundary/error-utils.js.map +1 -1
  48. package/dist/components/error-boundary/error-utils.mjs +2 -2
  49. package/dist/components/index.d.ts +1 -1
  50. package/dist/components/index.js +385 -254
  51. package/dist/components/index.js.map +1 -1
  52. package/dist/components/index.mjs +10 -10
  53. package/dist/components/toast/toast-provider.js +118 -85
  54. package/dist/components/toast/toast-provider.js.map +1 -1
  55. package/dist/components/toast/toast-provider.mjs +1 -1
  56. package/dist/components/usage-banner.js +135 -146
  57. package/dist/components/usage-banner.js.map +1 -1
  58. package/dist/components/usage-banner.mjs +1 -1
  59. package/dist/context/copilot-context.d.ts +1 -1
  60. package/dist/context/copilot-context.js +2 -1
  61. package/dist/context/copilot-context.js.map +1 -1
  62. package/dist/context/copilot-context.mjs +1 -1
  63. package/dist/context/index.d.ts +1 -1
  64. package/dist/context/index.js +2 -1
  65. package/dist/context/index.js.map +1 -1
  66. package/dist/context/index.mjs +1 -1
  67. package/dist/{copilot-context-f9b2b4c3.d.ts → copilot-context-3ab4fdf5.d.ts} +6 -2
  68. package/dist/hooks/index.d.ts +1 -1
  69. package/dist/hooks/index.js +232 -71
  70. package/dist/hooks/index.js.map +1 -1
  71. package/dist/hooks/index.mjs +32 -32
  72. package/dist/hooks/use-chat.d.ts +1 -1
  73. package/dist/hooks/use-chat.js +295 -156
  74. package/dist/hooks/use-chat.js.map +1 -1
  75. package/dist/hooks/use-chat.mjs +6 -6
  76. package/dist/hooks/use-coagent-state-render.js +2 -1
  77. package/dist/hooks/use-coagent-state-render.js.map +1 -1
  78. package/dist/hooks/use-coagent-state-render.mjs +3 -3
  79. package/dist/hooks/use-coagent.d.ts +1 -1
  80. package/dist/hooks/use-coagent.js +207 -65
  81. package/dist/hooks/use-coagent.js.map +1 -1
  82. package/dist/hooks/use-coagent.mjs +14 -14
  83. package/dist/hooks/use-copilot-action.js +27 -7
  84. package/dist/hooks/use-copilot-action.js.map +1 -1
  85. package/dist/hooks/use-copilot-action.mjs +4 -4
  86. package/dist/hooks/use-copilot-additional-instructions.js +2 -1
  87. package/dist/hooks/use-copilot-additional-instructions.js.map +1 -1
  88. package/dist/hooks/use-copilot-additional-instructions.mjs +2 -2
  89. package/dist/hooks/use-copilot-authenticated-action.js +27 -7
  90. package/dist/hooks/use-copilot-authenticated-action.js.map +1 -1
  91. package/dist/hooks/use-copilot-authenticated-action.mjs +5 -5
  92. package/dist/hooks/use-copilot-chat.d.ts +1 -1
  93. package/dist/hooks/use-copilot-chat.js +204 -65
  94. package/dist/hooks/use-copilot-chat.js.map +1 -1
  95. package/dist/hooks/use-copilot-chat.mjs +13 -13
  96. package/dist/hooks/use-copilot-readable.js +2 -1
  97. package/dist/hooks/use-copilot-readable.js.map +1 -1
  98. package/dist/hooks/use-copilot-readable.mjs +2 -2
  99. package/dist/hooks/use-copilot-runtime-client.d.ts +2 -0
  100. package/dist/hooks/use-copilot-runtime-client.js +52 -2
  101. package/dist/hooks/use-copilot-runtime-client.js.map +1 -1
  102. package/dist/hooks/use-copilot-runtime-client.mjs +2 -2
  103. package/dist/hooks/use-langgraph-interrupt-render.js +2 -1
  104. package/dist/hooks/use-langgraph-interrupt-render.js.map +1 -1
  105. package/dist/hooks/use-langgraph-interrupt-render.mjs +2 -2
  106. package/dist/hooks/use-langgraph-interrupt.d.ts +1 -1
  107. package/dist/hooks/use-langgraph-interrupt.js +204 -65
  108. package/dist/hooks/use-langgraph-interrupt.js.map +1 -1
  109. package/dist/hooks/use-langgraph-interrupt.mjs +14 -14
  110. package/dist/hooks/use-make-copilot-document-readable.js +2 -1
  111. package/dist/hooks/use-make-copilot-document-readable.js.map +1 -1
  112. package/dist/hooks/use-make-copilot-document-readable.mjs +2 -2
  113. package/dist/index.d.ts +1 -1
  114. package/dist/index.js +582 -321
  115. package/dist/index.js.map +1 -1
  116. package/dist/index.mjs +33 -33
  117. package/dist/lib/copilot-task.d.ts +1 -1
  118. package/dist/lib/copilot-task.js.map +1 -1
  119. package/dist/lib/copilot-task.mjs +11 -11
  120. package/dist/lib/index.d.ts +1 -1
  121. package/dist/lib/index.js.map +1 -1
  122. package/dist/lib/index.mjs +11 -11
  123. package/dist/types/interrupt-action.d.ts +1 -1
  124. package/dist/utils/extract.d.ts +1 -1
  125. package/dist/utils/extract.js.map +1 -1
  126. package/dist/utils/extract.mjs +10 -10
  127. package/dist/utils/index.d.ts +1 -1
  128. package/dist/utils/index.js.map +1 -1
  129. package/dist/utils/index.mjs +10 -10
  130. package/jest.config.js +7 -3
  131. package/package.json +4 -3
  132. package/src/components/copilot-provider/__tests__/copilotkit-error.test.tsx +75 -0
  133. package/src/components/copilot-provider/copilot-messages.tsx +47 -6
  134. package/src/components/copilot-provider/copilotkit-props.tsx +27 -1
  135. package/src/components/copilot-provider/copilotkit.tsx +64 -18
  136. package/src/components/toast/toast-provider.tsx +49 -24
  137. package/src/components/usage-banner.tsx +144 -147
  138. package/src/context/copilot-context.tsx +8 -2
  139. package/src/hooks/use-chat.ts +247 -61
  140. package/src/hooks/use-coagent.ts +5 -0
  141. package/src/hooks/use-copilot-action.ts +51 -9
  142. package/src/hooks/use-copilot-runtime-client.ts +41 -40
  143. package/tsconfig.json +4 -8
  144. package/tsup.config.ts +6 -6
  145. package/dist/chunk-6KGEF242.mjs.map +0 -1
  146. package/dist/chunk-HD2GE3DK.mjs +0 -359
  147. package/dist/chunk-HD2GE3DK.mjs.map +0 -1
  148. package/dist/chunk-L6QAOAE4.mjs.map +0 -1
  149. package/dist/chunk-MGIXEJWG.mjs.map +0 -1
  150. package/dist/chunk-NNSXCFQO.mjs.map +0 -1
  151. package/dist/chunk-RUY6MLHA.mjs.map +0 -1
  152. package/dist/chunk-T42PN5VN.mjs.map +0 -1
  153. package/dist/chunk-VRXANACV.mjs +0 -277
  154. package/dist/chunk-VRXANACV.mjs.map +0 -1
  155. package/dist/utils/utils.test.d.ts +0 -2
  156. package/dist/utils/utils.test.js +0 -9
  157. package/dist/utils/utils.test.js.map +0 -1
  158. package/dist/utils/utils.test.mjs +0 -7
  159. package/dist/utils/utils.test.mjs.map +0 -1
  160. /package/dist/{chunk-UHQMV2CE.mjs.map → chunk-36MGCCPZ.mjs.map} +0 -0
  161. /package/dist/{chunk-CCESTGAM.mjs.map → chunk-3OQM3NEK.mjs.map} +0 -0
  162. /package/dist/{chunk-OUSWPVDT.mjs.map → chunk-4URMLOBR.mjs.map} +0 -0
  163. /package/dist/{chunk-LZDDYZEY.mjs.map → chunk-5BSUSFHM.mjs.map} +0 -0
  164. /package/dist/{chunk-C6F6EQNA.mjs.map → chunk-BVK7PLK6.mjs.map} +0 -0
  165. /package/dist/{chunk-LDACFA2B.mjs.map → chunk-FN3UA2ZE.mjs.map} +0 -0
  166. /package/dist/{chunk-2FW7HH6W.mjs.map → chunk-KIXKBJUV.mjs.map} +0 -0
  167. /package/dist/{chunk-4I7PLQF7.mjs.map → chunk-QKEH3O4S.mjs.map} +0 -0
  168. /package/dist/{chunk-FRZZPPIV.mjs.map → chunk-RAQK4M64.mjs.map} +0 -0
  169. /package/dist/{chunk-QQZLIEXK.mjs.map → chunk-SJJNFYGQ.mjs.map} +0 -0
  170. /package/dist/{chunk-Q5D5XQFA.mjs.map → chunk-VDADWRS3.mjs.map} +0 -0
@@ -1,4 +1,4 @@
1
- import { CopilotCloudConfig, FunctionCallHandler } from "@copilotkit/shared";
1
+ import { CopilotCloudConfig, FunctionCallHandler, CopilotErrorHandler } from "@copilotkit/shared";
2
2
  import {
3
3
  ActionRenderProps,
4
4
  CatchAllActionRenderProps,
@@ -195,7 +195,7 @@ export interface CopilotContextParams {
195
195
  /**
196
196
  * The forwarded parameters to use for the task.
197
197
  */
198
- forwardedParameters?: Pick<ForwardedParametersInput, "temperature">;
198
+ forwardedParameters?: Partial<Pick<ForwardedParametersInput, "temperature">>;
199
199
  availableAgents: Agent[];
200
200
 
201
201
  /**
@@ -218,6 +218,11 @@ export interface CopilotContextParams {
218
218
  langGraphInterruptAction: LangGraphInterruptAction | null;
219
219
  setLangGraphInterruptAction: LangGraphInterruptActionSetter;
220
220
  removeLangGraphInterruptAction: () => void;
221
+
222
+ /**
223
+ * Optional trace handler for comprehensive debugging and observability.
224
+ */
225
+ onError?: CopilotErrorHandler;
221
226
  }
222
227
 
223
228
  const emptyCopilotContext: CopilotContextParams = {
@@ -288,6 +293,7 @@ const emptyCopilotContext: CopilotContextParams = {
288
293
  langGraphInterruptAction: null,
289
294
  setLangGraphInterruptAction: () => null,
290
295
  removeLangGraphInterruptAction: () => null,
296
+ onError: undefined,
291
297
  };
292
298
 
293
299
  export const CopilotContext = React.createContext<CopilotContextParams>(emptyCopilotContext);
@@ -1,10 +1,13 @@
1
1
  import React, { useCallback, useEffect, useRef } from "react";
2
+ import { flushSync } from "react-dom";
2
3
  import {
3
4
  FunctionCallHandler,
4
5
  COPILOT_CLOUD_PUBLIC_API_KEY_HEADER,
5
6
  CoAgentStateRenderHandler,
6
7
  randomId,
7
8
  parseJson,
9
+ CopilotKitError,
10
+ CopilotKitErrorCode,
8
11
  } from "@copilotkit/shared";
9
12
  import {
10
13
  Message,
@@ -35,10 +38,10 @@ import {
35
38
  import { CopilotApiConfig } from "../context";
36
39
  import { FrontendAction, processActionsForRuntimeRequest } from "../types/frontend-action";
37
40
  import { CoagentState } from "../types/coagent-state";
38
- import { AgentSession } from "../context/copilot-context";
41
+ import { AgentSession, useCopilotContext } from "../context/copilot-context";
39
42
  import { useCopilotRuntimeClient } from "./use-copilot-runtime-client";
40
- import { useCopilotContext } from "../context/copilot-context";
41
43
  import { useAsyncCallback, useErrorToast } from "../components/error-boundary/error-utils";
44
+ import { useToast } from "../components/toast/toast-provider";
42
45
  import {
43
46
  LangGraphInterruptAction,
44
47
  LangGraphInterruptActionSetter,
@@ -218,6 +221,41 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
218
221
  } = options;
219
222
  const runChatCompletionRef = useRef<(previousMessages: Message[]) => Promise<Message[]>>();
220
223
  const addErrorToast = useErrorToast();
224
+ const { setBannerError } = useToast();
225
+
226
+ // Get onError from context since it's not part of copilotConfig
227
+ const { onError } = useCopilotContext();
228
+
229
+ // Add tracing functionality to use-chat
230
+ const traceUIError = async (error: CopilotKitError, originalError?: any) => {
231
+ // Just check if onError and publicApiKey are defined
232
+ if (!onError || !copilotConfig?.publicApiKey) return;
233
+
234
+ try {
235
+ const traceEvent = {
236
+ type: "error" as const,
237
+ timestamp: Date.now(),
238
+ context: {
239
+ source: "ui" as const,
240
+ request: {
241
+ operation: "useChatCompletion",
242
+ url: copilotConfig.chatApiEndpoint,
243
+ startTime: Date.now(),
244
+ },
245
+ technical: {
246
+ environment: "browser",
247
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
248
+ stackTrace: originalError instanceof Error ? originalError.stack : undefined,
249
+ },
250
+ },
251
+ error,
252
+ };
253
+
254
+ await onError(traceEvent);
255
+ } catch (traceError) {
256
+ console.error("Error in use-chat onError handler:", traceError);
257
+ }
258
+ };
221
259
  // We need to keep a ref of coagent states and session because of renderAndWait - making sure
222
260
  // the latest state is sent to the API
223
261
  // This is a workaround and needs to be addressed in the future
@@ -453,29 +491,91 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
453
491
  filterAdjacentAgentStateMessages(rawMessagesResponse),
454
492
  );
455
493
 
456
- if (messages.length === 0) {
457
- continue;
458
- }
459
-
460
494
  newMessages = [];
461
495
 
496
+ // Handle error statuses BEFORE checking if there are messages
497
+ // (errors can come in chunks with no messages)
498
+
462
499
  // request failed, display error message and quit
463
500
  if (
464
501
  value.generateCopilotResponse.status?.__typename === "FailedResponseStatus" &&
465
502
  value.generateCopilotResponse.status.reason === "GUARDRAILS_VALIDATION_FAILED"
466
503
  ) {
504
+ const guardrailsReason =
505
+ value.generateCopilotResponse.status.details?.guardrailsReason || "";
506
+
467
507
  newMessages = [
468
508
  new TextMessage({
469
509
  role: MessageRole.Assistant,
470
- content: value.generateCopilotResponse.status.details?.guardrailsReason || "",
510
+ content: guardrailsReason,
471
511
  }),
472
512
  ];
513
+
514
+ // Trace guardrails validation failure
515
+ const guardrailsError = new CopilotKitError({
516
+ message: `Guardrails validation failed: ${guardrailsReason}`,
517
+ code: CopilotKitErrorCode.MISUSE,
518
+ });
519
+ await traceUIError(guardrailsError, {
520
+ statusReason: value.generateCopilotResponse.status.reason,
521
+ statusDetails: value.generateCopilotResponse.status.details,
522
+ });
523
+
473
524
  setMessages([...previousMessages, ...newMessages]);
474
525
  break;
475
526
  }
476
527
 
528
+ // Handle UNKNOWN_ERROR failures (like authentication errors) by routing to banner error system
529
+ if (
530
+ value.generateCopilotResponse.status?.__typename === "FailedResponseStatus" &&
531
+ value.generateCopilotResponse.status.reason === "UNKNOWN_ERROR"
532
+ ) {
533
+ const errorMessage =
534
+ value.generateCopilotResponse.status.details?.description ||
535
+ "An unknown error occurred";
536
+
537
+ // Try to extract original error information from the response details
538
+ const statusDetails = value.generateCopilotResponse.status.details;
539
+ const originalError = statusDetails?.originalError || statusDetails?.error;
540
+
541
+ // Extract structured error information if available (prioritize top-level over extensions)
542
+ const originalCode = originalError?.code || originalError?.extensions?.code;
543
+ const originalSeverity = originalError?.severity || originalError?.extensions?.severity;
544
+ const originalVisibility =
545
+ originalError?.visibility || originalError?.extensions?.visibility;
546
+
547
+ // Use the original error code if available, otherwise default to NETWORK_ERROR
548
+ let errorCode = CopilotKitErrorCode.NETWORK_ERROR;
549
+ if (originalCode && Object.values(CopilotKitErrorCode).includes(originalCode)) {
550
+ errorCode = originalCode;
551
+ }
552
+
553
+ // Create a structured CopilotKitError preserving original error information
554
+ const structuredError = new CopilotKitError({
555
+ message: errorMessage,
556
+ code: errorCode,
557
+ severity: originalSeverity,
558
+ visibility: originalVisibility,
559
+ });
560
+
561
+ // Display the error in the banner
562
+ setBannerError(structuredError);
563
+
564
+ // Trace the error for debugging/observability
565
+ await traceUIError(structuredError, {
566
+ statusReason: value.generateCopilotResponse.status.reason,
567
+ statusDetails: value.generateCopilotResponse.status.details,
568
+ originalErrorCode: originalCode,
569
+ preservedStructure: !!originalCode,
570
+ });
571
+
572
+ // Stop processing and break from the loop
573
+ setIsLoading(false);
574
+ break;
575
+ }
576
+
477
577
  // add messages to the chat
478
- else {
578
+ else if (messages.length > 0) {
479
579
  newMessages = [...messages];
480
580
 
481
581
  for (const message of messages) {
@@ -558,6 +658,55 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
558
658
 
559
659
  let didExecuteAction = false;
560
660
 
661
+ // ----- Helper function to execute an action and manage its lifecycle -----
662
+ const executeActionFromMessage = async (
663
+ currentAction: FrontendAction<any>,
664
+ actionMessage: ActionExecutionMessage,
665
+ ) => {
666
+ const isInterruptAction = interruptMessages.find((m) => m.id === actionMessage.id);
667
+ // Determine follow-up behavior: use action's specific setting if defined, otherwise default based on interrupt status.
668
+ followUp = currentAction?.followUp ?? !isInterruptAction;
669
+
670
+ // Call _setActivatingMessageId before executing the action for HITL correlation
671
+ if ((currentAction as any)?._setActivatingMessageId) {
672
+ (currentAction as any)._setActivatingMessageId(actionMessage.id);
673
+ }
674
+
675
+ const resultMessage = await executeAction({
676
+ onFunctionCall: onFunctionCall!,
677
+ message: actionMessage,
678
+ chatAbortControllerRef,
679
+ onError: (error: Error) => {
680
+ addErrorToast([error]);
681
+ // console.error is kept here as it's a genuine error in action execution
682
+ console.error(`Failed to execute action ${actionMessage.name}: ${error}`);
683
+ },
684
+ setMessages,
685
+ getFinalMessages: () => finalMessages,
686
+ isRenderAndWait: (currentAction as any)?._isRenderAndWait || false,
687
+ });
688
+ didExecuteAction = true;
689
+ const messageIndex = finalMessages.findIndex((msg) => msg.id === actionMessage.id);
690
+ finalMessages.splice(messageIndex + 1, 0, resultMessage);
691
+
692
+ // If the executed action was a renderAndWaitForResponse type, update messages immediately
693
+ // to reflect its completion in the UI, making it interactive promptly.
694
+ if ((currentAction as any)?._isRenderAndWait) {
695
+ const messagesForImmediateUpdate = [...finalMessages];
696
+ flushSync(() => {
697
+ setMessages(messagesForImmediateUpdate);
698
+ });
699
+ }
700
+
701
+ // Clear _setActivatingMessageId after the action is done
702
+ if ((currentAction as any)?._setActivatingMessageId) {
703
+ (currentAction as any)._setActivatingMessageId(null);
704
+ }
705
+
706
+ return resultMessage;
707
+ };
708
+ // ----------------------------------------------------------------------
709
+
561
710
  // execute regular action executions that are specific to the frontend (last actions)
562
711
  if (onFunctionCall) {
563
712
  // Find consecutive action execution messages at the end
@@ -587,44 +736,38 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
587
736
  ? getPairedFeAction(actions, message)
588
737
  : null;
589
738
 
590
- const executeActionFromMessage = async (
591
- action: FrontendAction<any>,
592
- message: ActionExecutionMessage,
593
- ) => {
594
- const isInterruptAction = interruptMessages.find((m) => m.id === message.id);
595
- followUp = action?.followUp ?? !isInterruptAction;
596
- const resultMessage = await executeAction({
597
- onFunctionCall,
598
- previousMessages,
599
- message,
600
- chatAbortControllerRef,
601
- onError: (error: Error) => {
602
- addErrorToast([error]);
603
- console.error(`Failed to execute action ${message.name}: ${error}`);
604
- },
605
- });
606
- didExecuteAction = true;
607
- const messageIndex = finalMessages.findIndex((msg) => msg.id === message.id);
608
- finalMessages.splice(messageIndex + 1, 0, resultMessage);
609
-
610
- return resultMessage;
611
- };
612
-
613
739
  // execution message which has an action registered with the hook (remote availability):
614
740
  // execute that action first, and then the "paired FE action"
615
741
  if (action && message.isActionExecutionMessage()) {
616
- const resultMessage = await executeActionFromMessage(action, message);
617
- const pairedFeAction = getPairedFeAction(actions, resultMessage);
618
-
619
- if (pairedFeAction) {
620
- const newExecutionMessage = new ActionExecutionMessage({
621
- name: pairedFeAction.name,
622
- arguments: parseJson(resultMessage.result, resultMessage.result),
623
- status: message.status,
624
- createdAt: message.createdAt,
625
- parentMessageId: message.parentMessageId,
626
- });
627
- await executeActionFromMessage(pairedFeAction, newExecutionMessage);
742
+ // For HITL actions, check if they've already been processed to avoid redundant handler calls.
743
+ const isRenderAndWaitAction = (action as any)?._isRenderAndWait || false;
744
+ const alreadyProcessed =
745
+ isRenderAndWaitAction &&
746
+ finalMessages.some(
747
+ (fm) => fm.isResultMessage() && fm.actionExecutionId === message.id,
748
+ );
749
+
750
+ if (alreadyProcessed) {
751
+ // Skip re-execution if already processed
752
+ } else {
753
+ // Call the single, externally defined executeActionFromMessage
754
+ const resultMessage = await executeActionFromMessage(
755
+ action,
756
+ message as ActionExecutionMessage,
757
+ );
758
+ const pairedFeAction = getPairedFeAction(actions, resultMessage);
759
+
760
+ if (pairedFeAction) {
761
+ const newExecutionMessage = new ActionExecutionMessage({
762
+ name: pairedFeAction.name,
763
+ arguments: parseJson(resultMessage.result, resultMessage.result),
764
+ status: message.status,
765
+ createdAt: message.createdAt,
766
+ parentMessageId: message.parentMessageId,
767
+ });
768
+ // Call the single, externally defined executeActionFromMessage
769
+ await executeActionFromMessage(pairedFeAction, newExecutionMessage);
770
+ }
628
771
  }
629
772
  } else if (message.isResultMessage() && currentResultMessagePairedFeAction) {
630
773
  // Actions which are set up in runtime actions array: Grab the result, executed paired FE action with it as args.
@@ -635,6 +778,7 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
635
778
  createdAt: message.createdAt,
636
779
  });
637
780
  finalMessages.push(newExecutionMessage);
781
+ // Call the single, externally defined executeActionFromMessage
638
782
  await executeActionFromMessage(
639
783
  currentResultMessagePairedFeAction,
640
784
  newExecutionMessage,
@@ -645,10 +789,10 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
645
789
  setMessages(finalMessages);
646
790
  }
647
791
 
792
+ // Conditionally run chat completion again if followUp is not explicitly false
793
+ // and an action was executed or the last message is a server-side result (for non-agent runs).
648
794
  if (
649
- // if followUp is not explicitly false
650
795
  followUp !== false &&
651
- // and we executed an action
652
796
  (didExecuteAction ||
653
797
  // the last message is a server side result
654
798
  (!isAgentRun &&
@@ -792,25 +936,47 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
792
936
  );
793
937
 
794
938
  const reload = useAsyncCallback(
795
- async (messageId: string): Promise<void> => {
939
+ async (reloadMessageId: string): Promise<void> => {
796
940
  if (isLoading || messages.length === 0) {
797
941
  return;
798
942
  }
799
943
 
800
- const index = messages.findIndex((msg) => msg.id === messageId);
801
- if (index === -1) {
802
- console.warn(`Message with id ${messageId} not found`);
944
+ const reloadMessageIndex = messages.findIndex((msg) => msg.id === reloadMessageId);
945
+ if (reloadMessageIndex === -1) {
946
+ console.warn(`Message with id ${reloadMessageId} not found`);
803
947
  return;
804
948
  }
805
949
 
806
- let newMessages = messages.slice(0, index); // excludes the message with messageId
807
- if (newMessages.length > 0 && newMessages[newMessages.length - 1].isAgentStateMessage()) {
808
- newMessages = newMessages.slice(0, newMessages.length - 1); // remove last one too
950
+ // @ts-expect-error -- message has role
951
+ const reloadMessageRole = messages[reloadMessageIndex].role;
952
+ if (reloadMessageRole !== MessageRole.Assistant) {
953
+ console.warn(`Regenerate cannot be performed on ${reloadMessageRole} role`);
954
+ return;
809
955
  }
810
956
 
811
- setMessages(newMessages);
957
+ let historyCutoff: Message[] = [];
958
+ if (messages.length > 2) {
959
+ // message to regenerate from is now first.
960
+ // Work backwards to find the first the closest user message
961
+ const lastUserMessageBeforeRegenerate = messages
962
+ .slice(0, reloadMessageIndex)
963
+ .reverse()
964
+ .find(
965
+ (msg) =>
966
+ // @ts-expect-error -- message has role
967
+ msg.role === MessageRole.User,
968
+ );
969
+ const indexOfLastUserMessageBeforeRegenerate = messages.findIndex(
970
+ (msg) => msg.id === lastUserMessageBeforeRegenerate!.id,
971
+ );
812
972
 
813
- return runChatCompletionAndHandleFunctionCall(newMessages);
973
+ // Include the user message, remove everything after it
974
+ historyCutoff = messages.slice(0, indexOfLastUserMessageBeforeRegenerate + 1);
975
+ }
976
+
977
+ setMessages(historyCutoff);
978
+
979
+ return runChatCompletionAndHandleFunctionCall(historyCutoff);
814
980
  },
815
981
  [isLoading, messages, setMessages, runChatCompletionAndHandleFunctionCall],
816
982
  );
@@ -858,26 +1024,46 @@ function constructFinalMessages(
858
1024
 
859
1025
  async function executeAction({
860
1026
  onFunctionCall,
861
- previousMessages,
862
1027
  message,
863
1028
  chatAbortControllerRef,
864
1029
  onError,
1030
+ setMessages,
1031
+ getFinalMessages,
1032
+ isRenderAndWait,
865
1033
  }: {
866
1034
  onFunctionCall: FunctionCallHandler;
867
- previousMessages: Message[];
868
1035
  message: ActionExecutionMessage;
869
1036
  chatAbortControllerRef: React.MutableRefObject<AbortController | null>;
870
1037
  onError: (error: Error) => void;
1038
+ setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
1039
+ getFinalMessages: () => Message[];
1040
+ isRenderAndWait: boolean;
871
1041
  }) {
872
1042
  let result: any;
873
1043
  let error: Error | null = null;
1044
+
1045
+ const currentMessagesForHandler = getFinalMessages();
1046
+
1047
+ // The handler (onFunctionCall) runs its synchronous part here, potentially setting up
1048
+ // renderAndWaitRef.current for HITL actions via useCopilotAction's transformed handler.
1049
+ const handlerReturnedPromise = onFunctionCall({
1050
+ messages: currentMessagesForHandler,
1051
+ name: message.name,
1052
+ args: message.arguments,
1053
+ });
1054
+
1055
+ // For HITL actions, call flushSync immediately after their handler has set up the promise
1056
+ // and before awaiting the promise. This ensures the UI updates to an interactive state.
1057
+ if (isRenderAndWait) {
1058
+ const currentMessagesForRender = getFinalMessages();
1059
+ flushSync(() => {
1060
+ setMessages([...currentMessagesForRender]);
1061
+ });
1062
+ }
1063
+
874
1064
  try {
875
1065
  result = await Promise.race([
876
- onFunctionCall({
877
- messages: previousMessages,
878
- name: message.name,
879
- args: message.arguments,
880
- }),
1066
+ handlerReturnedPromise, // Await the promise returned by the handler
881
1067
  new Promise((resolve) =>
882
1068
  chatAbortControllerRef.current?.signal.addEventListener("abort", () =>
883
1069
  resolve("Operation was aborted by the user"),
@@ -278,6 +278,11 @@ export function useCoAgent<T = any>(options: UseCoagentOptions<T>): UseCoagentRe
278
278
  agentName: name,
279
279
  });
280
280
 
281
+ // Runtime client handles errors automatically via handleGQLErrors
282
+ if (result.error) {
283
+ return; // Don't process data on error
284
+ }
285
+
281
286
  const newState = result.data?.loadAgentState?.state;
282
287
  if (newState === lastLoadedState.current) return;
283
288
 
@@ -159,11 +159,14 @@ export function useCopilotAction<const T extends Parameter[] | [] = []>(
159
159
  const { setAction, removeAction, actions, chatComponentsCache } = useCopilotContext();
160
160
  const idRef = useRef<string>(randomId());
161
161
  const renderAndWaitRef = useRef<RenderAndWaitForResponse | null>(null);
162
+ const activatingMessageIdRef = useRef<string | null>(null);
162
163
  const { addToast } = useToast();
163
164
 
164
165
  // clone the action to avoid mutating the original object
165
166
  action = { ...action };
166
167
 
168
+ // const { currentlyActivatingHitlActionMessageIdRef } = useCopilotContext() as any; // <-- REMOVE THIS FOR NOW
169
+
167
170
  // If the developer provides a renderAndWaitForResponse function, we transform the action
168
171
  // to use a promise internally, so that we can treat it like a normal action.
169
172
  if (
@@ -172,12 +175,21 @@ export function useCopilotAction<const T extends Parameter[] | [] = []>(
172
175
  // check if renderAndWaitForResponse is set
173
176
  (action.renderAndWait || action.renderAndWaitForResponse)
174
177
  ) {
178
+ (action as any)._isRenderAndWait = true; // Internal flag to identify this action type later
175
179
  const renderAndWait = action.renderAndWait || action.renderAndWaitForResponse;
176
180
  // remove the renderAndWait function from the action
177
181
  action.renderAndWait = undefined;
178
182
  action.renderAndWaitForResponse = undefined;
183
+
184
+ // Add a method for use-chat.ts to set the activating message ID.
185
+ // This helps correlate the action instance with the message being processed by use-chat.
186
+ (action as any)._setActivatingMessageId = (id: string | null) => {
187
+ activatingMessageIdRef.current = id;
188
+ };
189
+
179
190
  // add a handler that will be called when the action is executed
180
191
  action.handler = useAsyncCallback(async () => {
192
+ const currentActivatingId = activatingMessageIdRef.current;
181
193
  // we create a new promise when the handler is called
182
194
  let resolve: (result: any) => void;
183
195
  let reject: (error: any) => void;
@@ -185,26 +197,55 @@ export function useCopilotAction<const T extends Parameter[] | [] = []>(
185
197
  resolve = resolvePromise;
186
198
  reject = rejectPromise;
187
199
  });
188
- renderAndWaitRef.current = { promise, resolve: resolve!, reject: reject! };
200
+ renderAndWaitRef.current = {
201
+ promise,
202
+ resolve: resolve!,
203
+ reject: reject!,
204
+ messageId: currentActivatingId,
205
+ };
189
206
  // then we await the promise (it will be resolved in the original renderAndWait function)
190
- return await promise;
207
+ const result = await promise;
208
+ return result;
191
209
  }, []) as any;
192
210
 
193
211
  // add a render function that will be called when the action is rendered
194
- action.render = ((props: ActionRenderProps<T>): React.ReactElement => {
195
- // Specifically for renderAndWaitForResponse the executing state is set too early, causing a race condition
196
- // To fit it: we will wait for the handler to be ready
212
+ action.render = ((props: ActionRenderProps<T> & { messageId?: string }): React.ReactElement => {
213
+ const currentRenderMessageId = props.messageId;
214
+ // For renderAndWaitForResponse, the 'executing' state might be set by use-chat before
215
+ // this specific action instance's handler (and thus its promise) is ready.
216
+ // This logic adjusts the status to 'inProgress' if the current render
217
+ // isn't for the actively processing HITL action, preventing premature interaction.
197
218
  let status = props.status;
198
- if (props.status === "executing" && !renderAndWaitRef.current) {
199
- status = "inProgress";
219
+ if (props.status === "executing") {
220
+ if (!renderAndWaitRef.current || !renderAndWaitRef.current.promise) {
221
+ status = "inProgress";
222
+ } else if (
223
+ renderAndWaitRef.current.messageId !== currentRenderMessageId &&
224
+ activatingMessageIdRef.current !== currentRenderMessageId
225
+ ) {
226
+ status = "inProgress";
227
+ }
228
+ // If conditions met, status remains 'executing'
200
229
  }
201
230
  // Create type safe waitProps based on whether T extends empty array or not
202
231
  const waitProps = {
203
232
  status,
204
233
  args: props.args,
205
234
  result: props.result,
206
- handler: status === "executing" ? renderAndWaitRef.current!.resolve : undefined,
207
- respond: status === "executing" ? renderAndWaitRef.current!.resolve : undefined,
235
+ // handler and respond should only be provided if this is the truly active instance
236
+ // and its promise infrastructure is ready.
237
+ handler:
238
+ status === "executing" &&
239
+ renderAndWaitRef.current &&
240
+ renderAndWaitRef.current.messageId === currentRenderMessageId
241
+ ? renderAndWaitRef.current!.resolve
242
+ : undefined,
243
+ respond:
244
+ status === "executing" &&
245
+ renderAndWaitRef.current &&
246
+ renderAndWaitRef.current.messageId === currentRenderMessageId
247
+ ? renderAndWaitRef.current!.resolve
248
+ : undefined,
208
249
  } as T extends [] ? ActionRenderPropsNoArgsWait<T> : ActionRenderPropsWait<T>;
209
250
 
210
251
  // Type guard to check if renderAndWait is for no args case
@@ -302,4 +343,5 @@ interface RenderAndWaitForResponse {
302
343
  promise: Promise<any>;
303
344
  resolve: (result: any) => void;
304
345
  reject: (error: any) => void;
346
+ messageId: string | null;
305
347
  }