@copilotkit/react-core 1.9.2-next.8 → 1.9.2

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 (171) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/{chunk-ERXWDCY6.mjs → chunk-36MGCCPZ.mjs} +2 -2
  3. package/dist/{chunk-CCESTGAM.mjs → chunk-3OQM3NEK.mjs} +2 -2
  4. package/dist/{chunk-7G6RR4HE.mjs → chunk-3Q4F7RF2.mjs} +2 -2
  5. package/dist/chunk-57K2ZJ5F.mjs +348 -0
  6. package/dist/chunk-57K2ZJ5F.mjs.map +1 -0
  7. package/dist/{chunk-UBNRUXEK.mjs → chunk-5BSUSFHM.mjs} +2 -2
  8. package/dist/{chunk-RN3ZRHI7.mjs → chunk-AD7DWJNW.mjs} +66 -25
  9. package/dist/chunk-AD7DWJNW.mjs.map +1 -0
  10. package/dist/{chunk-JPMIAGI6.mjs → chunk-BVK7PLK6.mjs} +2 -2
  11. package/dist/{chunk-VJCHRQ7Q.mjs → chunk-DGON3GZX.mjs} +39 -6
  12. package/dist/chunk-DGON3GZX.mjs.map +1 -0
  13. package/dist/{chunk-XFOTNHYA.mjs → chunk-DKZTPL66.mjs} +2 -2
  14. package/dist/{chunk-XFOTNHYA.mjs.map → chunk-DKZTPL66.mjs.map} +1 -1
  15. package/dist/{chunk-S4BOATBG.mjs → chunk-FN3UA2ZE.mjs} +3 -3
  16. package/dist/{chunk-ISYBUDL4.mjs → chunk-JWAXDYOW.mjs} +11 -12
  17. package/dist/chunk-JWAXDYOW.mjs.map +1 -0
  18. package/dist/{chunk-T4ZKC4X4.mjs → chunk-KIXKBJUV.mjs} +3 -3
  19. package/dist/{chunk-I4JPQECN.mjs → chunk-LFAZTKBK.mjs} +5 -5
  20. package/dist/{chunk-JHIZ5HAI.mjs → chunk-NJA5ZLAZ.mjs} +29 -10
  21. package/dist/chunk-NJA5ZLAZ.mjs.map +1 -0
  22. package/dist/{chunk-ZHEEHGLS.mjs → chunk-QGT4JO7R.mjs} +35 -6
  23. package/dist/chunk-QGT4JO7R.mjs.map +1 -0
  24. package/dist/{chunk-JXF732XG.mjs → chunk-S5QUEHJC.mjs} +195 -77
  25. package/dist/chunk-S5QUEHJC.mjs.map +1 -0
  26. package/dist/{chunk-QQZLIEXK.mjs → chunk-SJJNFYGQ.mjs} +3 -3
  27. package/dist/{chunk-CMQV4XNY.mjs → chunk-VDADWRS3.mjs} +2 -2
  28. package/dist/chunk-YAF2LATQ.mjs +310 -0
  29. package/dist/chunk-YAF2LATQ.mjs.map +1 -0
  30. package/dist/{chunk-VF6UPRKM.mjs → chunk-ZGMZ5WJI.mjs} +4 -4
  31. package/dist/components/copilot-provider/copilot-messages.js +37 -4
  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 +14 -9
  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 +354 -258
  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 +354 -258
  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 +354 -258
  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 +1 -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 +1 -1
  65. package/dist/context/index.js.map +1 -1
  66. package/dist/context/index.mjs +1 -1
  67. package/dist/{copilot-context-3da805ab.d.ts → copilot-context-3ab4fdf5.d.ts} +3 -3
  68. package/dist/hooks/index.d.ts +1 -1
  69. package/dist/hooks/index.js +249 -88
  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 +287 -174
  74. package/dist/hooks/use-chat.js.map +1 -1
  75. package/dist/hooks/use-chat.mjs +5 -5
  76. package/dist/hooks/use-coagent-state-render.js +1 -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 +224 -82
  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 +26 -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 +1 -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 +26 -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 +195 -82
  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 +1 -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 -2
  100. package/dist/hooks/use-copilot-runtime-client.js +8 -8
  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 +1 -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 +195 -82
  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 +1 -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 +593 -336
  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/setupTests.d.ts +2 -0
  124. package/dist/setupTests.js +26 -0
  125. package/dist/setupTests.js.map +1 -0
  126. package/dist/setupTests.mjs +24 -0
  127. package/dist/setupTests.mjs.map +1 -0
  128. package/dist/types/interrupt-action.d.ts +1 -1
  129. package/dist/utils/extract.d.ts +1 -1
  130. package/dist/utils/extract.js.map +1 -1
  131. package/dist/utils/extract.mjs +10 -10
  132. package/dist/utils/index.d.ts +1 -1
  133. package/dist/utils/index.js.map +1 -1
  134. package/dist/utils/index.mjs +10 -10
  135. package/jest.config.js +4 -0
  136. package/package.json +6 -3
  137. package/src/components/copilot-provider/__tests__/{copilotkit-trace.test.tsx → copilotkit-error.test.tsx} +17 -17
  138. package/src/components/copilot-provider/copilot-messages.tsx +43 -4
  139. package/src/components/copilot-provider/copilotkit-props.tsx +13 -8
  140. package/src/components/copilot-provider/copilotkit.tsx +61 -19
  141. package/src/components/toast/toast-provider.tsx +49 -24
  142. package/src/components/usage-banner.tsx +144 -147
  143. package/src/context/copilot-context.tsx +4 -4
  144. package/src/hooks/__tests__/use-coagent-config.test.ts +284 -0
  145. package/src/hooks/use-chat.ts +249 -61
  146. package/src/hooks/use-coagent.ts +41 -0
  147. package/src/hooks/use-copilot-action.ts +51 -9
  148. package/src/hooks/use-copilot-runtime-client.ts +12 -50
  149. package/src/setupTests.ts +26 -0
  150. package/tsconfig.json +5 -2
  151. package/dist/chunk-HD2GE3DK.mjs +0 -359
  152. package/dist/chunk-HD2GE3DK.mjs.map +0 -1
  153. package/dist/chunk-ISYBUDL4.mjs.map +0 -1
  154. package/dist/chunk-JHIZ5HAI.mjs.map +0 -1
  155. package/dist/chunk-JXF732XG.mjs.map +0 -1
  156. package/dist/chunk-RN3ZRHI7.mjs.map +0 -1
  157. package/dist/chunk-VJCHRQ7Q.mjs.map +0 -1
  158. package/dist/chunk-VRXANACV.mjs +0 -277
  159. package/dist/chunk-VRXANACV.mjs.map +0 -1
  160. package/dist/chunk-ZHEEHGLS.mjs.map +0 -1
  161. /package/dist/{chunk-ERXWDCY6.mjs.map → chunk-36MGCCPZ.mjs.map} +0 -0
  162. /package/dist/{chunk-CCESTGAM.mjs.map → chunk-3OQM3NEK.mjs.map} +0 -0
  163. /package/dist/{chunk-7G6RR4HE.mjs.map → chunk-3Q4F7RF2.mjs.map} +0 -0
  164. /package/dist/{chunk-UBNRUXEK.mjs.map → chunk-5BSUSFHM.mjs.map} +0 -0
  165. /package/dist/{chunk-JPMIAGI6.mjs.map → chunk-BVK7PLK6.mjs.map} +0 -0
  166. /package/dist/{chunk-S4BOATBG.mjs.map → chunk-FN3UA2ZE.mjs.map} +0 -0
  167. /package/dist/{chunk-T4ZKC4X4.mjs.map → chunk-KIXKBJUV.mjs.map} +0 -0
  168. /package/dist/{chunk-I4JPQECN.mjs.map → chunk-LFAZTKBK.mjs.map} +0 -0
  169. /package/dist/{chunk-QQZLIEXK.mjs.map → chunk-SJJNFYGQ.mjs.map} +0 -0
  170. /package/dist/{chunk-CMQV4XNY.mjs.map → chunk-VDADWRS3.mjs.map} +0 -0
  171. /package/dist/{chunk-VF6UPRKM.mjs.map → chunk-ZGMZ5WJI.mjs.map} +0 -0
@@ -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) {
@@ -523,6 +623,8 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
523
623
  threadId: lastAgentStateMessage.threadId,
524
624
  nodeName: lastAgentStateMessage.nodeName,
525
625
  runId: lastAgentStateMessage.runId,
626
+ // Preserve existing config from previous state
627
+ config: prevAgentStates[lastAgentStateMessage.agentName]?.config,
526
628
  },
527
629
  }));
528
630
  if (lastAgentStateMessage.running) {
@@ -558,6 +660,55 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
558
660
 
559
661
  let didExecuteAction = false;
560
662
 
663
+ // ----- Helper function to execute an action and manage its lifecycle -----
664
+ const executeActionFromMessage = async (
665
+ currentAction: FrontendAction<any>,
666
+ actionMessage: ActionExecutionMessage,
667
+ ) => {
668
+ const isInterruptAction = interruptMessages.find((m) => m.id === actionMessage.id);
669
+ // Determine follow-up behavior: use action's specific setting if defined, otherwise default based on interrupt status.
670
+ followUp = currentAction?.followUp ?? !isInterruptAction;
671
+
672
+ // Call _setActivatingMessageId before executing the action for HITL correlation
673
+ if ((currentAction as any)?._setActivatingMessageId) {
674
+ (currentAction as any)._setActivatingMessageId(actionMessage.id);
675
+ }
676
+
677
+ const resultMessage = await executeAction({
678
+ onFunctionCall: onFunctionCall!,
679
+ message: actionMessage,
680
+ chatAbortControllerRef,
681
+ onError: (error: Error) => {
682
+ addErrorToast([error]);
683
+ // console.error is kept here as it's a genuine error in action execution
684
+ console.error(`Failed to execute action ${actionMessage.name}: ${error}`);
685
+ },
686
+ setMessages,
687
+ getFinalMessages: () => finalMessages,
688
+ isRenderAndWait: (currentAction as any)?._isRenderAndWait || false,
689
+ });
690
+ didExecuteAction = true;
691
+ const messageIndex = finalMessages.findIndex((msg) => msg.id === actionMessage.id);
692
+ finalMessages.splice(messageIndex + 1, 0, resultMessage);
693
+
694
+ // If the executed action was a renderAndWaitForResponse type, update messages immediately
695
+ // to reflect its completion in the UI, making it interactive promptly.
696
+ if ((currentAction as any)?._isRenderAndWait) {
697
+ const messagesForImmediateUpdate = [...finalMessages];
698
+ flushSync(() => {
699
+ setMessages(messagesForImmediateUpdate);
700
+ });
701
+ }
702
+
703
+ // Clear _setActivatingMessageId after the action is done
704
+ if ((currentAction as any)?._setActivatingMessageId) {
705
+ (currentAction as any)._setActivatingMessageId(null);
706
+ }
707
+
708
+ return resultMessage;
709
+ };
710
+ // ----------------------------------------------------------------------
711
+
561
712
  // execute regular action executions that are specific to the frontend (last actions)
562
713
  if (onFunctionCall) {
563
714
  // Find consecutive action execution messages at the end
@@ -587,44 +738,38 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
587
738
  ? getPairedFeAction(actions, message)
588
739
  : null;
589
740
 
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
741
  // execution message which has an action registered with the hook (remote availability):
614
742
  // execute that action first, and then the "paired FE action"
615
743
  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);
744
+ // For HITL actions, check if they've already been processed to avoid redundant handler calls.
745
+ const isRenderAndWaitAction = (action as any)?._isRenderAndWait || false;
746
+ const alreadyProcessed =
747
+ isRenderAndWaitAction &&
748
+ finalMessages.some(
749
+ (fm) => fm.isResultMessage() && fm.actionExecutionId === message.id,
750
+ );
751
+
752
+ if (alreadyProcessed) {
753
+ // Skip re-execution if already processed
754
+ } else {
755
+ // Call the single, externally defined executeActionFromMessage
756
+ const resultMessage = await executeActionFromMessage(
757
+ action,
758
+ message as ActionExecutionMessage,
759
+ );
760
+ const pairedFeAction = getPairedFeAction(actions, resultMessage);
761
+
762
+ if (pairedFeAction) {
763
+ const newExecutionMessage = new ActionExecutionMessage({
764
+ name: pairedFeAction.name,
765
+ arguments: parseJson(resultMessage.result, resultMessage.result),
766
+ status: message.status,
767
+ createdAt: message.createdAt,
768
+ parentMessageId: message.parentMessageId,
769
+ });
770
+ // Call the single, externally defined executeActionFromMessage
771
+ await executeActionFromMessage(pairedFeAction, newExecutionMessage);
772
+ }
628
773
  }
629
774
  } else if (message.isResultMessage() && currentResultMessagePairedFeAction) {
630
775
  // Actions which are set up in runtime actions array: Grab the result, executed paired FE action with it as args.
@@ -635,6 +780,7 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
635
780
  createdAt: message.createdAt,
636
781
  });
637
782
  finalMessages.push(newExecutionMessage);
783
+ // Call the single, externally defined executeActionFromMessage
638
784
  await executeActionFromMessage(
639
785
  currentResultMessagePairedFeAction,
640
786
  newExecutionMessage,
@@ -645,10 +791,10 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
645
791
  setMessages(finalMessages);
646
792
  }
647
793
 
794
+ // Conditionally run chat completion again if followUp is not explicitly false
795
+ // and an action was executed or the last message is a server-side result (for non-agent runs).
648
796
  if (
649
- // if followUp is not explicitly false
650
797
  followUp !== false &&
651
- // and we executed an action
652
798
  (didExecuteAction ||
653
799
  // the last message is a server side result
654
800
  (!isAgentRun &&
@@ -792,25 +938,47 @@ export function useChat(options: UseChatOptions): UseChatHelpers {
792
938
  );
793
939
 
794
940
  const reload = useAsyncCallback(
795
- async (messageId: string): Promise<void> => {
941
+ async (reloadMessageId: string): Promise<void> => {
796
942
  if (isLoading || messages.length === 0) {
797
943
  return;
798
944
  }
799
945
 
800
- const index = messages.findIndex((msg) => msg.id === messageId);
801
- if (index === -1) {
802
- console.warn(`Message with id ${messageId} not found`);
946
+ const reloadMessageIndex = messages.findIndex((msg) => msg.id === reloadMessageId);
947
+ if (reloadMessageIndex === -1) {
948
+ console.warn(`Message with id ${reloadMessageId} not found`);
803
949
  return;
804
950
  }
805
951
 
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
952
+ // @ts-expect-error -- message has role
953
+ const reloadMessageRole = messages[reloadMessageIndex].role;
954
+ if (reloadMessageRole !== MessageRole.Assistant) {
955
+ console.warn(`Regenerate cannot be performed on ${reloadMessageRole} role`);
956
+ return;
809
957
  }
810
958
 
811
- setMessages(newMessages);
959
+ let historyCutoff: Message[] = [];
960
+ if (messages.length > 2) {
961
+ // message to regenerate from is now first.
962
+ // Work backwards to find the first the closest user message
963
+ const lastUserMessageBeforeRegenerate = messages
964
+ .slice(0, reloadMessageIndex)
965
+ .reverse()
966
+ .find(
967
+ (msg) =>
968
+ // @ts-expect-error -- message has role
969
+ msg.role === MessageRole.User,
970
+ );
971
+ const indexOfLastUserMessageBeforeRegenerate = messages.findIndex(
972
+ (msg) => msg.id === lastUserMessageBeforeRegenerate!.id,
973
+ );
812
974
 
813
- return runChatCompletionAndHandleFunctionCall(newMessages);
975
+ // Include the user message, remove everything after it
976
+ historyCutoff = messages.slice(0, indexOfLastUserMessageBeforeRegenerate + 1);
977
+ }
978
+
979
+ setMessages(historyCutoff);
980
+
981
+ return runChatCompletionAndHandleFunctionCall(historyCutoff);
814
982
  },
815
983
  [isLoading, messages, setMessages, runChatCompletionAndHandleFunctionCall],
816
984
  );
@@ -858,26 +1026,46 @@ function constructFinalMessages(
858
1026
 
859
1027
  async function executeAction({
860
1028
  onFunctionCall,
861
- previousMessages,
862
1029
  message,
863
1030
  chatAbortControllerRef,
864
1031
  onError,
1032
+ setMessages,
1033
+ getFinalMessages,
1034
+ isRenderAndWait,
865
1035
  }: {
866
1036
  onFunctionCall: FunctionCallHandler;
867
- previousMessages: Message[];
868
1037
  message: ActionExecutionMessage;
869
1038
  chatAbortControllerRef: React.MutableRefObject<AbortController | null>;
870
1039
  onError: (error: Error) => void;
1040
+ setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
1041
+ getFinalMessages: () => Message[];
1042
+ isRenderAndWait: boolean;
871
1043
  }) {
872
1044
  let result: any;
873
1045
  let error: Error | null = null;
1046
+
1047
+ const currentMessagesForHandler = getFinalMessages();
1048
+
1049
+ // The handler (onFunctionCall) runs its synchronous part here, potentially setting up
1050
+ // renderAndWaitRef.current for HITL actions via useCopilotAction's transformed handler.
1051
+ const handlerReturnedPromise = onFunctionCall({
1052
+ messages: currentMessagesForHandler,
1053
+ name: message.name,
1054
+ args: message.arguments,
1055
+ });
1056
+
1057
+ // For HITL actions, call flushSync immediately after their handler has set up the promise
1058
+ // and before awaiting the promise. This ensures the UI updates to an interactive state.
1059
+ if (isRenderAndWait) {
1060
+ const currentMessagesForRender = getFinalMessages();
1061
+ flushSync(() => {
1062
+ setMessages([...currentMessagesForRender]);
1063
+ });
1064
+ }
1065
+
874
1066
  try {
875
1067
  result = await Promise.race([
876
- onFunctionCall({
877
- messages: previousMessages,
878
- name: message.name,
879
- args: message.arguments,
880
- }),
1068
+ handlerReturnedPromise, // Await the promise returned by the handler
881
1069
  new Promise((resolve) =>
882
1070
  chatAbortControllerRef.current?.signal.addEventListener("abort", () =>
883
1071
  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
 
@@ -306,6 +311,42 @@ export function useCoAgent<T = any>(options: UseCoagentOptions<T>): UseCoagentRe
306
311
  coagentStates[name] === undefined,
307
312
  ]);
308
313
 
314
+ // Sync config when runtime configuration changes
315
+ useEffect(() => {
316
+ const newConfig = options.config
317
+ ? options.config
318
+ : options.configurable
319
+ ? { configurable: options.configurable }
320
+ : undefined;
321
+
322
+ if (newConfig === undefined) return;
323
+
324
+ setCoagentStatesWithRef((prev) => {
325
+ const existing = prev[name] ?? {
326
+ name,
327
+ state: isInternalStateManagementWithInitial(options) ? options.initialState : {},
328
+ config: {},
329
+ running: false,
330
+ active: false,
331
+ threadId: undefined,
332
+ nodeName: undefined,
333
+ runId: undefined,
334
+ };
335
+
336
+ if (JSON.stringify(existing.config) === JSON.stringify(newConfig)) {
337
+ return prev;
338
+ }
339
+
340
+ return {
341
+ ...prev,
342
+ [name]: {
343
+ ...existing,
344
+ config: newConfig,
345
+ },
346
+ };
347
+ });
348
+ }, [JSON.stringify(options.config), JSON.stringify(options.configurable)]);
349
+
309
350
  const runAgentCallback = useAsyncCallback(
310
351
  async (hint?: HintFunction) => {
311
352
  await runAgent(name, context, appendMessage, runChatCompletion, hint);
@@ -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
  }