@copilotkit/react-core 1.57.3 → 1.58.0-canary.thread-id-propagation

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 (292) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{copilotkit-CtXcs1ea.cjs → copilotkit-B4ouY7qC.cjs} +14 -3
  3. package/dist/copilotkit-B4ouY7qC.cjs.map +1 -0
  4. package/dist/copilotkit-BK9CVq9A.d.cts.map +1 -1
  5. package/dist/{copilotkit-CC8DjOiC.mjs → copilotkit-L4mM_JqG.mjs} +14 -3
  6. package/dist/copilotkit-L4mM_JqG.mjs.map +1 -0
  7. package/dist/copilotkit-WlmeVijs.d.mts.map +1 -1
  8. package/dist/index.cjs +3 -77
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +2 -2
  11. package/dist/index.d.mts +2 -2
  12. package/dist/index.mjs +3 -77
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/index.umd.js +15 -78
  15. package/dist/index.umd.js.map +1 -1
  16. package/dist/v2/headless.cjs +11 -0
  17. package/dist/v2/headless.cjs.map +1 -1
  18. package/dist/v2/headless.d.cts.map +1 -1
  19. package/dist/v2/headless.d.mts.map +1 -1
  20. package/dist/v2/headless.mjs +11 -0
  21. package/dist/v2/headless.mjs.map +1 -1
  22. package/dist/v2/index.cjs +1 -1
  23. package/dist/v2/index.mjs +1 -1
  24. package/dist/v2/index.umd.js +13 -2
  25. package/dist/v2/index.umd.js.map +1 -1
  26. package/package.json +12 -13
  27. package/skills/react-core/SKILL.md +108 -0
  28. package/skills/react-core/references/agent-access.md +288 -0
  29. package/skills/react-core/references/attachments.md +291 -0
  30. package/skills/react-core/references/capabilities.md +138 -0
  31. package/skills/react-core/references/chat-components.md +221 -0
  32. package/skills/react-core/references/client-side-tools.md +358 -0
  33. package/skills/react-core/references/custom-message-renderers.md +226 -0
  34. package/skills/react-core/references/debug-mode.md +153 -0
  35. package/skills/react-core/references/human-in-the-loop.md +312 -0
  36. package/skills/react-core/references/provider-setup.md +326 -0
  37. package/skills/react-core/references/rendering-activity-messages.md +207 -0
  38. package/skills/react-core/references/rendering-tool-calls.md +319 -0
  39. package/skills/react-core/references/suggestions.md +211 -0
  40. package/skills/react-core/references/switching-agents-recipes.md +160 -0
  41. package/skills/react-core/references/switching-agents.md +231 -0
  42. package/skills/react-core/references/threads.md +226 -0
  43. package/.attw.json +0 -3
  44. package/CHANGELOG.md +0 -5043
  45. package/dist/copilotkit-CC8DjOiC.mjs.map +0 -1
  46. package/dist/copilotkit-CtXcs1ea.cjs.map +0 -1
  47. package/scripts/scope-preflight.mjs +0 -100
  48. package/src/components/CopilotListeners.tsx +0 -137
  49. package/src/components/__tests__/CopilotListeners.test.tsx +0 -38
  50. package/src/components/copilot-provider/__tests__/copilot-messages-key.test.tsx +0 -92
  51. package/src/components/copilot-provider/__tests__/copilotkit-error.test.tsx +0 -77
  52. package/src/components/copilot-provider/__tests__/error-visibility-prod.test.tsx +0 -70
  53. package/src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx +0 -107
  54. package/src/components/copilot-provider/copilot-messages.tsx +0 -314
  55. package/src/components/copilot-provider/copilotkit-props.tsx +0 -214
  56. package/src/components/copilot-provider/copilotkit.tsx +0 -853
  57. package/src/components/copilot-provider/index.ts +0 -3
  58. package/src/components/dev-console/console-trigger.tsx +0 -283
  59. package/src/components/dev-console/developer-console-modal.tsx +0 -1016
  60. package/src/components/dev-console/icons.tsx +0 -106
  61. package/src/components/error-boundary/error-boundary.tsx +0 -99
  62. package/src/components/error-boundary/error-utils.tsx +0 -105
  63. package/src/components/index.ts +0 -1
  64. package/src/components/toast/exclamation-mark-icon.tsx +0 -27
  65. package/src/components/toast/toast-provider.tsx +0 -448
  66. package/src/components/usage-banner.tsx +0 -266
  67. package/src/context/__tests__/threads-context.test.tsx +0 -141
  68. package/src/context/coagent-state-renders-context.tsx +0 -89
  69. package/src/context/copilot-context.tsx +0 -365
  70. package/src/context/copilot-messages-context.tsx +0 -35
  71. package/src/context/index.ts +0 -22
  72. package/src/context/threads-context.tsx +0 -69
  73. package/src/hooks/__tests__/use-coagent-config.test.ts +0 -352
  74. package/src/hooks/__tests__/use-coagent-state-render-bridge.helpers.test.ts +0 -107
  75. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +0 -1209
  76. package/src/hooks/__tests__/use-coagent-state-render.test.tsx +0 -356
  77. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +0 -241
  78. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -72
  79. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +0 -102
  80. package/src/hooks/index.ts +0 -33
  81. package/src/hooks/use-agent-nodename.ts +0 -33
  82. package/src/hooks/use-coagent-state-render-bridge.helpers.ts +0 -345
  83. package/src/hooks/use-coagent-state-render-bridge.tsx +0 -222
  84. package/src/hooks/use-coagent-state-render-registry.ts +0 -230
  85. package/src/hooks/use-coagent-state-render.ts +0 -163
  86. package/src/hooks/use-coagent.ts +0 -377
  87. package/src/hooks/use-configure-chat-suggestions.tsx +0 -96
  88. package/src/hooks/use-copilot-action.ts +0 -245
  89. package/src/hooks/use-copilot-additional-instructions.ts +0 -98
  90. package/src/hooks/use-copilot-authenticated-action.ts +0 -73
  91. package/src/hooks/use-copilot-chat-headless_c.ts +0 -264
  92. package/src/hooks/use-copilot-chat-suggestions.tsx +0 -134
  93. package/src/hooks/use-copilot-chat.ts +0 -132
  94. package/src/hooks/use-copilot-chat_internal.ts +0 -875
  95. package/src/hooks/use-copilot-readable.ts +0 -135
  96. package/src/hooks/use-copilot-runtime-client.ts +0 -178
  97. package/src/hooks/use-default-tool.ts +0 -13
  98. package/src/hooks/use-flat-category-store.ts +0 -109
  99. package/src/hooks/use-frontend-tool.ts +0 -113
  100. package/src/hooks/use-human-in-the-loop.ts +0 -138
  101. package/src/hooks/use-langgraph-interrupt.ts +0 -103
  102. package/src/hooks/use-lazy-tool-renderer.tsx +0 -30
  103. package/src/hooks/use-make-copilot-document-readable.ts +0 -30
  104. package/src/hooks/use-render-tool-call.ts +0 -89
  105. package/src/hooks/use-tree.ts +0 -222
  106. package/src/index.tsx +0 -7
  107. package/src/lib/copilot-task.ts +0 -215
  108. package/src/lib/index.ts +0 -1
  109. package/src/lib/status-checker.ts +0 -67
  110. package/src/setupTests.ts +0 -37
  111. package/src/test-helpers/copilot-context.ts +0 -91
  112. package/src/types/chat-suggestion-configuration.ts +0 -23
  113. package/src/types/coagent-action.ts +0 -35
  114. package/src/types/coagent-state.ts +0 -13
  115. package/src/types/crew.ts +0 -89
  116. package/src/types/document-pointer.ts +0 -7
  117. package/src/types/frontend-action.ts +0 -213
  118. package/src/types/index.ts +0 -17
  119. package/src/types/interrupt-action.ts +0 -58
  120. package/src/types/system-message.ts +0 -4
  121. package/src/utils/dev-console.ts +0 -19
  122. package/src/utils/index.ts +0 -2
  123. package/src/utils/suggestions-constants.ts +0 -8
  124. package/src/utils/utils.test.ts +0 -7
  125. package/src/utils/utils.ts +0 -6
  126. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +0 -240
  127. package/src/v2/__tests__/globalSetup.ts +0 -14
  128. package/src/v2/__tests__/setup.ts +0 -93
  129. package/src/v2/__tests__/utils/test-helpers.tsx +0 -570
  130. package/src/v2/a2ui/A2UICatalogContext.tsx +0 -79
  131. package/src/v2/a2ui/A2UIMessageRenderer.tsx +0 -294
  132. package/src/v2/a2ui/A2UIToolCallRenderer.tsx +0 -290
  133. package/src/v2/components/CopilotKitInspector.tsx +0 -52
  134. package/src/v2/components/MCPAppsActivityRenderer.tsx +0 -815
  135. package/src/v2/components/OpenGenerativeUIRenderer.tsx +0 -598
  136. package/src/v2/components/WildcardToolCallRender.tsx +0 -86
  137. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +0 -665
  138. package/src/v2/components/chat/CopilotChat.tsx +0 -664
  139. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +0 -393
  140. package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +0 -374
  141. package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +0 -159
  142. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +0 -350
  143. package/src/v2/components/chat/CopilotChatInput.tsx +0 -1412
  144. package/src/v2/components/chat/CopilotChatMessageView.tsx +0 -716
  145. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +0 -265
  146. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +0 -59
  147. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +0 -134
  148. package/src/v2/components/chat/CopilotChatToggleButton.tsx +0 -171
  149. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +0 -40
  150. package/src/v2/components/chat/CopilotChatUserMessage.tsx +0 -445
  151. package/src/v2/components/chat/CopilotChatView.tsx +0 -890
  152. package/src/v2/components/chat/CopilotModalHeader.tsx +0 -129
  153. package/src/v2/components/chat/CopilotPopup.tsx +0 -81
  154. package/src/v2/components/chat/CopilotPopupView.tsx +0 -317
  155. package/src/v2/components/chat/CopilotSidebar.tsx +0 -80
  156. package/src/v2/components/chat/CopilotSidebarView.tsx +0 -269
  157. package/src/v2/components/chat/Lightbox.tsx +0 -103
  158. package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +0 -66
  159. package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +0 -168
  160. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +0 -1239
  161. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +0 -73
  162. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +0 -432
  163. package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +0 -183
  164. package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +0 -184
  165. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +0 -649
  166. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +0 -624
  167. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +0 -702
  168. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.thumbs.test.tsx +0 -72
  169. package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +0 -241
  170. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +0 -107
  171. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +0 -929
  172. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +0 -1567
  173. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +0 -1004
  174. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +0 -279
  175. package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +0 -336
  176. package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +0 -249
  177. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +0 -530
  178. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +0 -785
  179. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +0 -2416
  180. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +0 -621
  181. package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +0 -56
  182. package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +0 -264
  183. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +0 -853
  184. package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +0 -94
  185. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +0 -1050
  186. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +0 -484
  187. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +0 -612
  188. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.tsx +0 -159
  189. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +0 -502
  190. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +0 -1068
  191. package/src/v2/components/chat/__tests__/MCPAppsProxy.e2e.test.tsx +0 -589
  192. package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +0 -403
  193. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -137
  194. package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +0 -37
  195. package/src/v2/components/chat/__tests__/setup.ts +0 -1
  196. package/src/v2/components/chat/index.ts +0 -90
  197. package/src/v2/components/chat/last-user-message-context.ts +0 -21
  198. package/src/v2/components/chat/normalize-auto-scroll.ts +0 -17
  199. package/src/v2/components/chat/scroll-element-context.ts +0 -13
  200. package/src/v2/components/index.ts +0 -8
  201. package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -286
  202. package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -464
  203. package/src/v2/components/intelligence-indicator/index.ts +0 -2
  204. package/src/v2/components/license-warning-banner.tsx +0 -217
  205. package/src/v2/components/ui/button.tsx +0 -124
  206. package/src/v2/components/ui/dropdown-menu.tsx +0 -258
  207. package/src/v2/components/ui/tooltip.tsx +0 -60
  208. package/src/v2/context.ts +0 -62
  209. package/src/v2/headless.ts +0 -64
  210. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +0 -152
  211. package/src/v2/hooks/__tests__/standard-schema.test.tsx +0 -282
  212. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +0 -140
  213. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +0 -401
  214. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +0 -44
  215. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +0 -211
  216. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +0 -1029
  217. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +0 -159
  218. package/src/v2/hooks/__tests__/use-attachments.test.tsx +0 -169
  219. package/src/v2/hooks/__tests__/use-capabilities.test.tsx +0 -76
  220. package/src/v2/hooks/__tests__/use-component.test.tsx +0 -126
  221. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +0 -696
  222. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +0 -153
  223. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -167
  224. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +0 -2148
  225. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +0 -1261
  226. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +0 -397
  227. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +0 -56
  228. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +0 -192
  229. package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +0 -219
  230. package/src/v2/hooks/__tests__/use-render-custom-messages.test.tsx +0 -55
  231. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +0 -259
  232. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +0 -524
  233. package/src/v2/hooks/__tests__/use-threads.test.tsx +0 -757
  234. package/src/v2/hooks/__tests__/zod-regression.test.tsx +0 -311
  235. package/src/v2/hooks/index.ts +0 -24
  236. package/src/v2/hooks/use-agent-context.tsx +0 -45
  237. package/src/v2/hooks/use-agent.tsx +0 -227
  238. package/src/v2/hooks/use-attachments.tsx +0 -269
  239. package/src/v2/hooks/use-capabilities.tsx +0 -25
  240. package/src/v2/hooks/use-component.tsx +0 -91
  241. package/src/v2/hooks/use-configure-suggestions.tsx +0 -236
  242. package/src/v2/hooks/use-default-render-tool.tsx +0 -271
  243. package/src/v2/hooks/use-frontend-tool.tsx +0 -46
  244. package/src/v2/hooks/use-human-in-the-loop.tsx +0 -81
  245. package/src/v2/hooks/use-interrupt.tsx +0 -305
  246. package/src/v2/hooks/use-keyboard-height.tsx +0 -67
  247. package/src/v2/hooks/use-pin-to-send.ts +0 -94
  248. package/src/v2/hooks/use-render-activity-message.tsx +0 -72
  249. package/src/v2/hooks/use-render-custom-messages.tsx +0 -93
  250. package/src/v2/hooks/use-render-tool-call.tsx +0 -208
  251. package/src/v2/hooks/use-render-tool.tsx +0 -184
  252. package/src/v2/hooks/use-suggestions.tsx +0 -91
  253. package/src/v2/hooks/use-threads.tsx +0 -325
  254. package/src/v2/hooks/useKatexStyles.ts +0 -27
  255. package/src/v2/index.css +0 -1
  256. package/src/v2/index.ts +0 -27
  257. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +0 -495
  258. package/src/v2/lib/__tests__/processPartialHtml.test.ts +0 -112
  259. package/src/v2/lib/__tests__/renderSlot.test.tsx +0 -588
  260. package/src/v2/lib/__tests__/slots.test.ts +0 -56
  261. package/src/v2/lib/processPartialHtml.ts +0 -45
  262. package/src/v2/lib/react-core.ts +0 -156
  263. package/src/v2/lib/slots.tsx +0 -184
  264. package/src/v2/lib/transcription-client.ts +0 -184
  265. package/src/v2/lib/utils.ts +0 -8
  266. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +0 -196
  267. package/src/v2/providers/CopilotKitProvider.tsx +0 -800
  268. package/src/v2/providers/SandboxFunctionsContext.ts +0 -10
  269. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +0 -652
  270. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +0 -101
  271. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +0 -69
  272. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +0 -881
  273. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +0 -198
  274. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +0 -740
  275. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +0 -713
  276. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +0 -294
  277. package/src/v2/providers/index.ts +0 -21
  278. package/src/v2/styles/globals.css +0 -349
  279. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +0 -525
  280. package/src/v2/types/defineToolCallRenderer.ts +0 -68
  281. package/src/v2/types/frontend-tool.ts +0 -8
  282. package/src/v2/types/human-in-the-loop.ts +0 -33
  283. package/src/v2/types/index.ts +0 -8
  284. package/src/v2/types/interrupt.ts +0 -15
  285. package/src/v2/types/react-activity-message-renderer.ts +0 -27
  286. package/src/v2/types/react-custom-message-renderer.ts +0 -17
  287. package/src/v2/types/react-tool-call-renderer.ts +0 -35
  288. package/src/v2/types/sandbox-function.ts +0 -11
  289. package/tsconfig.json +0 -8
  290. package/tsdown.config.ts +0 -193
  291. package/typedoc.json +0 -4
  292. package/vitest.config.mjs +0 -31
@@ -1,2148 +0,0 @@
1
- import React, { useEffect, useState, useReducer } from "react";
2
- import { screen, fireEvent, waitFor } from "@testing-library/react";
3
- import { z } from "zod";
4
- import { useFrontendTool } from "../use-frontend-tool";
5
- import { ReactFrontendTool } from "../../types";
6
- import { CopilotChat } from "../../components/chat/CopilotChat";
7
- import CopilotChatToolCallsView from "../../components/chat/CopilotChatToolCallsView";
8
- import { AssistantMessage, Message } from "@ag-ui/core";
9
- import { ToolCallStatus } from "@copilotkit/core";
10
- import {
11
- AbstractAgent,
12
- EventType,
13
- type AgentSubscriber,
14
- type BaseEvent,
15
- type RunAgentInput,
16
- type RunAgentParameters,
17
- } from "@ag-ui/client";
18
- import { Observable } from "rxjs";
19
- import {
20
- MockStepwiseAgent,
21
- renderWithCopilotKit,
22
- runStartedEvent,
23
- runFinishedEvent,
24
- toolCallChunkEvent,
25
- toolCallResultEvent,
26
- textChunkEvent,
27
- testId,
28
- } from "../../__tests__/utils/test-helpers";
29
-
30
- describe("useFrontendTool E2E - Dynamic Registration", () => {
31
- describe("Minimal dynamic registration without chat run", () => {
32
- it("registers tool and renders tool call via ToolCallsView", async () => {
33
- // No agent run; we render ToolCallsView directly
34
- const DynamicToolComponent: React.FC = () => {
35
- const tool: ReactFrontendTool<{ message: string }> = {
36
- name: "dynamicTool",
37
- parameters: z.object({ message: z.string() }),
38
- render: ({ name, args }) => (
39
- <div data-testid="dynamic-tool-render">
40
- {name}: {args.message}
41
- </div>
42
- ),
43
- };
44
- useFrontendTool(tool);
45
- return null;
46
- };
47
-
48
- const toolCallId = testId("tc_dyn");
49
- const assistantMessage: AssistantMessage = {
50
- id: testId("a"),
51
- role: "assistant",
52
- content: "",
53
- toolCalls: [
54
- {
55
- id: toolCallId,
56
- type: "function",
57
- function: {
58
- name: "dynamicTool",
59
- arguments: JSON.stringify({ message: "hello" }),
60
- },
61
- } as any,
62
- ],
63
- } as any;
64
- const messages: Message[] = [];
65
-
66
- const ui = renderWithCopilotKit({
67
- children: (
68
- <>
69
- <DynamicToolComponent />
70
- <CopilotChatToolCallsView
71
- message={assistantMessage}
72
- messages={messages}
73
- />
74
- </>
75
- ),
76
- });
77
-
78
- await waitFor(() => {
79
- const el = screen.getByTestId("dynamic-tool-render");
80
- expect(el).toBeDefined();
81
- expect(el.textContent).toContain("dynamicTool");
82
- expect(el.textContent).toContain("hello");
83
- });
84
- // Explicitly unmount to avoid any lingering handles
85
- ui.unmount();
86
- });
87
- });
88
- describe("Register at runtime", () => {
89
- it("should register tool dynamically after provider is mounted", async () => {
90
- const agent = new MockStepwiseAgent();
91
-
92
- // Inner component that uses the hook
93
- const ToolUser: React.FC = () => {
94
- const tool: ReactFrontendTool<{ message: string }> = {
95
- name: "dynamicTool",
96
- parameters: z.object({ message: z.string() }),
97
- render: ({ name, args, result }) => (
98
- <div data-testid="dynamic-tool-render">
99
- {name}: {args.message} | Result:{" "}
100
- {result ? JSON.stringify(result) : "pending"}
101
- </div>
102
- ),
103
- handler: async (args) => {
104
- return { processed: args.message.toUpperCase() };
105
- },
106
- };
107
-
108
- useFrontendTool(tool);
109
- return null;
110
- };
111
-
112
- // Component that registers a tool after mount
113
- const DynamicToolComponent: React.FC = () => {
114
- const [isRegistered, setIsRegistered] = useState(false);
115
-
116
- useEffect(() => {
117
- // Register immediately after mount
118
- setIsRegistered(true);
119
- }, []);
120
-
121
- return (
122
- <>
123
- <div data-testid="dynamic-status">
124
- {isRegistered ? "Registered" : "Not registered"}
125
- </div>
126
- {isRegistered && <ToolUser />}
127
- </>
128
- );
129
- };
130
-
131
- renderWithCopilotKit({
132
- agent,
133
- children: (
134
- <>
135
- <DynamicToolComponent />
136
- <div style={{ height: 400 }}>
137
- <CopilotChat welcomeScreen={false} />
138
- </div>
139
- </>
140
- ),
141
- });
142
-
143
- // Wait for dynamic registration
144
- await waitFor(() => {
145
- expect(screen.getByTestId("dynamic-status").textContent).toBe(
146
- "Registered",
147
- );
148
- });
149
-
150
- // Submit a message that will trigger the dynamically registered tool
151
- const input = await screen.findByRole("textbox");
152
- fireEvent.change(input, { target: { value: "Use dynamic tool" } });
153
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
154
-
155
- // Wait for message to be processed
156
- await waitFor(() => {
157
- expect(screen.getByText("Use dynamic tool")).toBeDefined();
158
- });
159
-
160
- const messageId = testId("msg");
161
- const toolCallId = testId("tc");
162
-
163
- // Emit tool call for the dynamically registered tool
164
- agent.emit(runStartedEvent());
165
- agent.emit(
166
- toolCallChunkEvent({
167
- toolCallId,
168
- toolCallName: "dynamicTool",
169
- parentMessageId: messageId,
170
- delta: '{"message":"hello world"}',
171
- }),
172
- );
173
-
174
- // The dynamically registered renderer should appear
175
- await waitFor(() => {
176
- const toolRender = screen.getByTestId("dynamic-tool-render");
177
- expect(toolRender).toBeDefined();
178
- expect(toolRender.textContent).toContain("hello world");
179
- });
180
-
181
- // Send result
182
- agent.emit(
183
- toolCallResultEvent({
184
- toolCallId,
185
- messageId: `${messageId}_result`,
186
- content: JSON.stringify({ processed: "HELLO WORLD" }),
187
- }),
188
- );
189
-
190
- await waitFor(() => {
191
- const toolRender = screen.getByTestId("dynamic-tool-render");
192
- expect(toolRender.textContent).toContain("HELLO WORLD");
193
- });
194
-
195
- agent.emit(runFinishedEvent());
196
- agent.complete();
197
- });
198
- });
199
-
200
- describe("Streaming tool calls with incomplete JSON", () => {
201
- it("renders tool calls progressively as incomplete JSON chunks arrive", async () => {
202
- const agent = new MockStepwiseAgent();
203
-
204
- // Tool that renders the arguments it receives
205
- const StreamingTool: React.FC = () => {
206
- const tool: ReactFrontendTool<{
207
- name: string;
208
- items: string[];
209
- count: number;
210
- }> = {
211
- name: "streamingTool",
212
- parameters: z.object({
213
- name: z.string(),
214
- items: z.array(z.string()),
215
- count: z.number(),
216
- }),
217
- render: ({ args }) => (
218
- <div data-testid="streaming-tool-render">
219
- <div data-testid="tool-name">{args.name || "undefined"}</div>
220
- <div data-testid="tool-items">
221
- {args.items ? args.items.join(", ") : "undefined"}
222
- </div>
223
- <div data-testid="tool-count">
224
- {args.count !== undefined ? args.count : "undefined"}
225
- </div>
226
- </div>
227
- ),
228
- };
229
-
230
- useFrontendTool(tool);
231
- return null;
232
- };
233
-
234
- renderWithCopilotKit({
235
- agent,
236
- children: (
237
- <>
238
- <StreamingTool />
239
- <div style={{ height: 400 }}>
240
- <CopilotChat welcomeScreen={false} />
241
- </div>
242
- </>
243
- ),
244
- });
245
-
246
- // Submit a message to start the agent
247
- const input = await screen.findByRole("textbox");
248
- fireEvent.change(input, { target: { value: "Test streaming" } });
249
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
250
-
251
- // Wait for message to appear
252
- await waitFor(() => {
253
- expect(screen.getByText("Test streaming")).toBeDefined();
254
- });
255
-
256
- const messageId = testId("msg");
257
- const toolCallId = testId("tc");
258
-
259
- // Start the run
260
- agent.emit(runStartedEvent());
261
-
262
- // Stream incomplete JSON chunks
263
- // First chunk: just opening brace and part of first field
264
- agent.emit(
265
- toolCallChunkEvent({
266
- toolCallId,
267
- toolCallName: "streamingTool",
268
- parentMessageId: messageId,
269
- delta: '{"na',
270
- }),
271
- );
272
-
273
- // Check that tool is rendering (even with incomplete JSON)
274
- await waitFor(() => {
275
- expect(screen.getByTestId("streaming-tool-render")).toBeDefined();
276
- });
277
-
278
- // Second chunk: complete the name field
279
- agent.emit(
280
- toolCallChunkEvent({
281
- toolCallId,
282
- parentMessageId: messageId,
283
- delta: 'me":"Test Tool"',
284
- }),
285
- );
286
-
287
- // Check name is now rendered
288
- await waitFor(() => {
289
- expect(screen.getByTestId("tool-name").textContent).toBe("Test Tool");
290
- });
291
-
292
- // Third chunk: start items array
293
- agent.emit(
294
- toolCallChunkEvent({
295
- toolCallId,
296
- parentMessageId: messageId,
297
- delta: ',"items":["item1"',
298
- }),
299
- );
300
-
301
- // Check items array has first item
302
- await waitFor(() => {
303
- expect(screen.getByTestId("tool-items").textContent).toContain("item1");
304
- });
305
-
306
- // Fourth chunk: add more items and start count
307
- agent.emit(
308
- toolCallChunkEvent({
309
- toolCallId,
310
- parentMessageId: messageId,
311
- delta: ',"item2","item3"],"cou',
312
- }),
313
- );
314
-
315
- // Check items array is complete
316
- await waitFor(() => {
317
- expect(screen.getByTestId("tool-items").textContent).toBe(
318
- "item1, item2, item3",
319
- );
320
- });
321
-
322
- // Final chunk: complete the JSON
323
- agent.emit(
324
- toolCallChunkEvent({
325
- toolCallId,
326
- parentMessageId: messageId,
327
- delta: 'nt":42}',
328
- }),
329
- );
330
-
331
- // Check count is rendered
332
- await waitFor(() => {
333
- expect(screen.getByTestId("tool-count").textContent).toBe("42");
334
- });
335
-
336
- agent.emit(runFinishedEvent());
337
- agent.complete();
338
- });
339
- });
340
-
341
- describe("Tool followUp property behavior", () => {
342
- it("stops agent execution when followUp is false", async () => {
343
- const agent = new MockStepwiseAgent();
344
-
345
- const NoFollowUpTool: React.FC = () => {
346
- const tool: ReactFrontendTool<{ action: string }> = {
347
- name: "noFollowUpTool",
348
- parameters: z.object({ action: z.string() }),
349
- followUp: false, // This should stop execution after tool call
350
- render: ({ args, status }) => (
351
- <div data-testid="no-followup-tool">
352
- <div data-testid="tool-action">{args.action || "no action"}</div>
353
- <div data-testid="tool-status">{status}</div>
354
- </div>
355
- ),
356
- };
357
-
358
- useFrontendTool(tool);
359
- return null;
360
- };
361
-
362
- renderWithCopilotKit({
363
- agent,
364
- children: (
365
- <>
366
- <NoFollowUpTool />
367
- <div style={{ height: 400 }}>
368
- <CopilotChat welcomeScreen={false} />
369
- </div>
370
- </>
371
- ),
372
- });
373
-
374
- // Submit a message
375
- const input = await screen.findByRole("textbox");
376
- fireEvent.change(input, { target: { value: "Execute no followup" } });
377
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
378
-
379
- await waitFor(() => {
380
- expect(screen.getByText("Execute no followup")).toBeDefined();
381
- });
382
-
383
- const messageId = testId("msg");
384
- const toolCallId = testId("tc");
385
-
386
- // Start run and emit tool call
387
- agent.emit(runStartedEvent());
388
- agent.emit(
389
- toolCallChunkEvent({
390
- toolCallId,
391
- toolCallName: "noFollowUpTool",
392
- parentMessageId: messageId,
393
- delta: '{"action":"stop-after-this"}',
394
- }),
395
- );
396
-
397
- // Tool should render
398
- await waitFor(() => {
399
- expect(screen.getByTestId("no-followup-tool")).toBeDefined();
400
- expect(screen.getByTestId("tool-action").textContent).toBe(
401
- "stop-after-this",
402
- );
403
- });
404
-
405
- // The agent should NOT continue after this tool call
406
- // We can verify this by NOT emitting more events and checking the UI state
407
- // In a real scenario, the agent would stop sending events
408
-
409
- agent.emit(runFinishedEvent());
410
- agent.complete();
411
-
412
- // Verify execution stopped (no further messages)
413
- // The chat should only have the user message and tool call, no follow-up
414
- const messages = screen.queryAllByRole("article");
415
- expect(messages.length).toBeLessThanOrEqual(2); // User message + tool response
416
- });
417
-
418
- it("continues agent execution when followUp is true or undefined", async () => {
419
- const agent = new MockStepwiseAgent();
420
-
421
- const ContinueFollowUpTool: React.FC = () => {
422
- const tool: ReactFrontendTool<{ action: string }> = {
423
- name: "continueFollowUpTool",
424
- parameters: z.object({ action: z.string() }),
425
- // followUp is undefined (default) - should continue execution
426
- render: ({ args }) => (
427
- <div data-testid="continue-followup-tool">
428
- <div data-testid="tool-action">{args.action || "no action"}</div>
429
- </div>
430
- ),
431
- };
432
-
433
- useFrontendTool(tool);
434
- return null;
435
- };
436
-
437
- renderWithCopilotKit({
438
- agent,
439
- children: (
440
- <>
441
- <ContinueFollowUpTool />
442
- <div style={{ height: 400 }}>
443
- <CopilotChat welcomeScreen={false} />
444
- </div>
445
- </>
446
- ),
447
- });
448
-
449
- // Submit a message
450
- const input = await screen.findByRole("textbox");
451
- fireEvent.change(input, { target: { value: "Execute with followup" } });
452
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
453
-
454
- await waitFor(() => {
455
- expect(screen.getByText("Execute with followup")).toBeDefined();
456
- });
457
-
458
- const messageId = testId("msg");
459
- const toolCallId = testId("tc");
460
- const followUpMessageId = testId("followup");
461
-
462
- // Start run and emit tool call
463
- agent.emit(runStartedEvent());
464
- agent.emit(
465
- toolCallChunkEvent({
466
- toolCallId,
467
- toolCallName: "continueFollowUpTool",
468
- parentMessageId: messageId,
469
- delta: '{"action":"continue-after-this"}',
470
- }),
471
- );
472
-
473
- // Tool should render
474
- await waitFor(() => {
475
- expect(screen.getByTestId("continue-followup-tool")).toBeDefined();
476
- expect(screen.getByTestId("tool-action").textContent).toBe(
477
- "continue-after-this",
478
- );
479
- });
480
-
481
- // The agent SHOULD continue after this tool call
482
- // Emit a follow-up message to simulate continued execution
483
- agent.emit(
484
- textChunkEvent(
485
- followUpMessageId,
486
- "This is a follow-up message after tool execution",
487
- ),
488
- );
489
-
490
- // Verify the follow-up message appears
491
- await waitFor(() => {
492
- expect(
493
- screen.getByText("This is a follow-up message after tool execution"),
494
- ).toBeDefined();
495
- });
496
-
497
- agent.emit(runFinishedEvent());
498
- agent.complete();
499
- });
500
- });
501
-
502
- describe("Agent input plumbing", () => {
503
- it("forwards registered frontend tools to runAgent input", async () => {
504
- class InstrumentedMockAgent extends MockStepwiseAgent {
505
- // Shared so the clone and original both see the captured parameters
506
- private _capture: { lastRunParameters?: RunAgentParameters } = {};
507
-
508
- get lastRunParameters(): RunAgentParameters | undefined {
509
- return this._capture.lastRunParameters;
510
- }
511
-
512
- clone(): this {
513
- const cloned = super.clone();
514
- (cloned as unknown as InstrumentedMockAgent)._capture = this._capture;
515
- return cloned;
516
- }
517
-
518
- async runAgent(
519
- parameters?: RunAgentParameters,
520
- subscriber?: AgentSubscriber,
521
- ) {
522
- this._capture.lastRunParameters = parameters;
523
- return super.runAgent(parameters, subscriber);
524
- }
525
- }
526
-
527
- const agent = new InstrumentedMockAgent();
528
-
529
- const ToolRegistrar: React.FC = () => {
530
- const tool: ReactFrontendTool<{ query: string }> = {
531
- name: "inspectionTool",
532
- parameters: z.object({ query: z.string() }),
533
- handler: async ({ query }) => `handled ${query}`,
534
- };
535
-
536
- useFrontendTool(tool);
537
- return null;
538
- };
539
-
540
- renderWithCopilotKit({
541
- agent,
542
- children: (
543
- <>
544
- <ToolRegistrar />
545
- <div style={{ height: 400 }}>
546
- <CopilotChat welcomeScreen={false} />
547
- </div>
548
- </>
549
- ),
550
- });
551
-
552
- const input = await screen.findByRole("textbox");
553
- fireEvent.change(input, { target: { value: "Trigger inspection" } });
554
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
555
-
556
- await waitFor(() => {
557
- expect(agent.lastRunParameters).toBeDefined();
558
- });
559
-
560
- const messageId = testId("msg");
561
- agent.emit(runStartedEvent());
562
- agent.emit(
563
- toolCallResultEvent({
564
- toolCallId: testId("tc"),
565
- messageId: `${messageId}_result`,
566
- content: JSON.stringify({}),
567
- }),
568
- );
569
- agent.emit(runFinishedEvent());
570
- agent.complete();
571
-
572
- expect(agent.lastRunParameters?.tools).toBeDefined();
573
- });
574
- });
575
-
576
- describe("Unmount disables handler, render persists", () => {
577
- it("Tool is properly removed from copilotkit.tools after component unmounts", async () => {
578
- // A deterministic agent that emits a single tool call per run and finishes
579
- class OneShotToolCallAgent extends AbstractAgent {
580
- private runCount = 0;
581
- clone(): OneShotToolCallAgent {
582
- const cloned = new OneShotToolCallAgent();
583
- cloned.agentId = this.agentId;
584
- // Share runCount via reference so the second run emits different args
585
- Object.defineProperty(cloned, "runCount", {
586
- get: () => this.runCount,
587
- set: (v: number) => {
588
- this.runCount = v;
589
- },
590
- });
591
- return cloned;
592
- }
593
- async detachActiveRun(): Promise<void> {}
594
- run(_input: RunAgentInput): Observable<BaseEvent> {
595
- return new Observable<BaseEvent>((observer) => {
596
- const messageId = testId("m");
597
- const toolCallId = testId("tc");
598
- this.runCount += 1;
599
- const valueArg = this.runCount === 1 ? "first call" : "second call";
600
- observer.next({ type: EventType.RUN_STARTED } as BaseEvent);
601
- observer.next({
602
- type: EventType.TOOL_CALL_CHUNK,
603
- toolCallId,
604
- toolCallName: "temporaryTool",
605
- parentMessageId: messageId,
606
- delta: JSON.stringify({ value: valueArg }),
607
- } as BaseEvent);
608
- observer.next({ type: EventType.RUN_FINISHED } as BaseEvent);
609
- observer.complete();
610
- return () => {};
611
- });
612
- }
613
- }
614
-
615
- const agent = new OneShotToolCallAgent();
616
- let handlerCalls = 0;
617
-
618
- // Component that can be toggled on/off
619
- const ToggleableToolComponent: React.FC = () => {
620
- const tool: ReactFrontendTool<{ value: string }> = {
621
- name: "temporaryTool",
622
- parameters: z.object({ value: z.string() }),
623
- followUp: false,
624
- handler: async ({ value }) => {
625
- handlerCalls += 1;
626
- return `HANDLED ${value.toUpperCase()}`;
627
- },
628
- render: ({ name, args, result, status }) => (
629
- <div data-testid="temporary-tool">
630
- {name}: {args.value} | Status: {status} | Result:{" "}
631
- {String(result ?? "")}
632
- </div>
633
- ),
634
- };
635
- useFrontendTool(tool);
636
- return <div data-testid="tool-mounted">Tool is mounted</div>;
637
- };
638
-
639
- const TestWrapper: React.FC = () => {
640
- const [showTool, setShowTool] = useState(true);
641
- return (
642
- <>
643
- <button
644
- onClick={() => setShowTool(!showTool)}
645
- data-testid="toggle-button"
646
- >
647
- Toggle Tool
648
- </button>
649
- {showTool && <ToggleableToolComponent />}
650
- <div style={{ height: 400 }}>
651
- <CopilotChat welcomeScreen={false} />
652
- </div>
653
- </>
654
- );
655
- };
656
-
657
- renderWithCopilotKit({ agent, children: <TestWrapper /> });
658
-
659
- // Tool should be mounted initially
660
- expect(screen.getByTestId("tool-mounted")).toBeDefined();
661
-
662
- // Run 1: submit a message to trigger agent run with "first call"
663
- const input = await screen.findByRole("textbox");
664
- fireEvent.change(input, { target: { value: "Trigger 1" } });
665
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
666
-
667
- // The tool should render and handler should have produced a result
668
- await waitFor(() => {
669
- const toolRender = screen.getByTestId("temporary-tool");
670
- expect(toolRender.textContent).toContain("first call");
671
- expect(toolRender.textContent).toContain("HANDLED FIRST CALL");
672
- expect(handlerCalls).toBe(1);
673
- });
674
-
675
- // Unmount the tool component (removes handler but keeps renderer via hook policy)
676
- fireEvent.click(screen.getByTestId("toggle-button"));
677
- await waitFor(() => {
678
- expect(screen.queryByTestId("tool-mounted")).toBeNull();
679
- });
680
-
681
- // Run 2: trigger agent again with "second call"
682
- fireEvent.change(input, { target: { value: "Trigger 2" } });
683
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
684
-
685
- // The renderer should still render with new args, but no handler result should be produced
686
- await waitFor(() => {
687
- const toolRender = screen.getAllByTestId("temporary-tool");
688
- // There will be two renders in the chat history; check the last one
689
- const last = toolRender[toolRender.length - 1];
690
- expect(last?.textContent).toContain("second call");
691
- // The handler should not have been called a second time since tool was removed
692
- expect(handlerCalls).toBe(1);
693
- });
694
- });
695
- });
696
-
697
- describe("Override behavior", () => {
698
- it("should use latest registration when same tool name is registered multiple times", async () => {
699
- const agent = new MockStepwiseAgent();
700
-
701
- // First component with initial tool definition
702
- const FirstToolComponent: React.FC = () => {
703
- const tool: ReactFrontendTool<{ text: string }> = {
704
- name: "overridableTool",
705
- parameters: z.object({ text: z.string() }),
706
- render: ({ name, args }) => (
707
- <div data-testid="first-version">
708
- First Version: {args.text} ({name})
709
- </div>
710
- ),
711
- };
712
-
713
- useFrontendTool(tool);
714
- return null;
715
- };
716
-
717
- // Second component with override tool definition
718
- const SecondToolComponent: React.FC<{ isActive: boolean }> = ({
719
- isActive,
720
- }) => {
721
- if (!isActive) return null;
722
-
723
- const tool: ReactFrontendTool<{ text: string }> = {
724
- name: "overridableTool",
725
- parameters: z.object({ text: z.string() }),
726
- render: ({ name, args }) => (
727
- <div data-testid="second-version">
728
- Second Version (Override): {args.text} ({name})
729
- </div>
730
- ),
731
- };
732
-
733
- useFrontendTool(tool);
734
- return null;
735
- };
736
-
737
- const TestWrapper: React.FC = () => {
738
- const [showSecond, setShowSecond] = useState(false);
739
-
740
- return (
741
- <>
742
- <FirstToolComponent />
743
- <SecondToolComponent isActive={showSecond} />
744
- <button
745
- onClick={() => setShowSecond(true)}
746
- data-testid="activate-override"
747
- >
748
- Activate Override
749
- </button>
750
- <div style={{ height: 400 }}>
751
- <CopilotChat welcomeScreen={false} />
752
- </div>
753
- </>
754
- );
755
- };
756
-
757
- renderWithCopilotKit({
758
- agent,
759
- children: <TestWrapper />,
760
- });
761
-
762
- // Submit message before override
763
- const input = await screen.findByRole("textbox");
764
- fireEvent.change(input, { target: { value: "Test original" } });
765
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
766
-
767
- // Wait for message to be processed
768
- await waitFor(() => {
769
- expect(screen.getByText("Test original")).toBeDefined();
770
- });
771
-
772
- const messageId1 = testId("msg1");
773
- const toolCallId1 = testId("tc1");
774
-
775
- agent.emit(runStartedEvent());
776
- agent.emit(
777
- toolCallChunkEvent({
778
- toolCallId: toolCallId1,
779
- toolCallName: "overridableTool",
780
- parentMessageId: messageId1,
781
- delta: '{"text":"before override"}',
782
- }),
783
- );
784
-
785
- // First version should render
786
- await waitFor(() => {
787
- const firstVersion = screen.getByTestId("first-version");
788
- expect(firstVersion.textContent).toContain("before override");
789
- });
790
-
791
- agent.emit(runFinishedEvent());
792
-
793
- // Activate the override
794
- const overrideButton = screen.getByTestId("activate-override");
795
- fireEvent.click(overrideButton);
796
-
797
- // Submit another message after override
798
- fireEvent.change(input, { target: { value: "Test override" } });
799
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
800
-
801
- // Wait for message to be processed
802
- await waitFor(() => {
803
- expect(screen.getByText("Test override")).toBeDefined();
804
- });
805
-
806
- const messageId2 = testId("msg2");
807
- const toolCallId2 = testId("tc2");
808
-
809
- agent.emit(runStartedEvent());
810
- agent.emit(
811
- toolCallChunkEvent({
812
- toolCallId: toolCallId2,
813
- toolCallName: "overridableTool",
814
- parentMessageId: messageId2,
815
- delta: '{"text":"after override"}',
816
- }),
817
- );
818
-
819
- // Second version should render (override) - there might be multiple due to both tool calls
820
- await waitFor(() => {
821
- const secondVersions = screen.getAllByTestId("second-version");
822
- // Find the one with "after override"
823
- const afterOverride = secondVersions.find((el) =>
824
- el.textContent?.includes("after override"),
825
- );
826
- expect(afterOverride).toBeDefined();
827
- expect(afterOverride?.textContent).toContain("after override");
828
- });
829
-
830
- agent.emit(runFinishedEvent());
831
- agent.complete();
832
- });
833
- });
834
-
835
- describe("Integration with Chat UI", () => {
836
- it("should render tool output correctly in chat interface", async () => {
837
- const agent = new MockStepwiseAgent();
838
-
839
- const IntegratedToolComponent: React.FC = () => {
840
- const tool: ReactFrontendTool<{ action: string; target: string }> = {
841
- name: "chatIntegratedTool",
842
- parameters: z.object({
843
- action: z.string(),
844
- target: z.string(),
845
- }),
846
- render: ({ name, args, result, status }) => (
847
- <div data-testid="integrated-tool" className="tool-render">
848
- <div>Tool: {name}</div>
849
- <div>Action: {args.action}</div>
850
- <div>Target: {args.target}</div>
851
- <div>Status: {status}</div>
852
- {result && <div>Result: {JSON.stringify(result)}</div>}
853
- </div>
854
- ),
855
- handler: async (args) => {
856
- return {
857
- success: true,
858
- message: `${args.action} completed on ${args.target}`,
859
- };
860
- },
861
- };
862
-
863
- useFrontendTool(tool);
864
- return null;
865
- };
866
-
867
- renderWithCopilotKit({
868
- agent,
869
- children: (
870
- <>
871
- <IntegratedToolComponent />
872
- <div style={{ height: 400 }}>
873
- <CopilotChat welcomeScreen={false} />
874
- </div>
875
- </>
876
- ),
877
- });
878
-
879
- // Submit user message
880
- const input = await screen.findByRole("textbox");
881
- fireEvent.change(input, { target: { value: "Perform an action" } });
882
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
883
-
884
- // User message should appear in chat
885
- await waitFor(() => {
886
- expect(screen.getByText("Perform an action")).toBeDefined();
887
- });
888
-
889
- const messageId = testId("msg");
890
- const toolCallId = testId("tc");
891
-
892
- // Stream tool call
893
- agent.emit(runStartedEvent());
894
- agent.emit(
895
- toolCallChunkEvent({
896
- toolCallId,
897
- toolCallName: "chatIntegratedTool",
898
- parentMessageId: messageId,
899
- delta: '{"action":"process","target":"data"}',
900
- }),
901
- );
902
-
903
- // Tool should render in chat with proper styling
904
- await waitFor(() => {
905
- const toolRender = screen.getByTestId("integrated-tool");
906
- expect(toolRender).toBeDefined();
907
- expect(toolRender.textContent).toContain("Action: process");
908
- expect(toolRender.textContent).toContain("Target: data");
909
- expect(toolRender.classList.contains("tool-render")).toBe(true);
910
- });
911
-
912
- // Send result
913
- agent.emit(
914
- toolCallResultEvent({
915
- toolCallId,
916
- messageId: `${messageId}_result`,
917
- content: JSON.stringify({
918
- success: true,
919
- message: "process completed on data",
920
- }),
921
- }),
922
- );
923
-
924
- // Result should appear in the tool render
925
- await waitFor(() => {
926
- const toolRender = screen.getByTestId("integrated-tool");
927
- expect(toolRender.textContent).toContain("Result:");
928
- expect(toolRender.textContent).toContain("process completed on data");
929
- });
930
-
931
- agent.emit(runFinishedEvent());
932
- agent.complete();
933
- });
934
- });
935
-
936
- describe("Tool Executing State", () => {
937
- it("should be in executing state while handler is running", async () => {
938
- const statusHistory: ToolCallStatus[] = [];
939
- let handlerStarted = false;
940
- let handlerCompleted = false;
941
- let handlerResult: any = null;
942
-
943
- // We'll use a custom agent that tracks when tool handlers execute
944
- const agent = new MockStepwiseAgent();
945
-
946
- const ExecutingStateTool: React.FC = () => {
947
- const tool: ReactFrontendTool<{ value: string }> = {
948
- name: "executingStateTool",
949
- parameters: z.object({ value: z.string() }),
950
- render: ({ args, status, result }) => {
951
- // Track all status changes
952
- useEffect(() => {
953
- if (!statusHistory.includes(status)) {
954
- statusHistory.push(status);
955
- }
956
- }, [status]);
957
-
958
- return (
959
- <div data-testid="executing-tool">
960
- <div data-testid="tool-status">{status}</div>
961
- <div data-testid="tool-value">{args.value || "undefined"}</div>
962
- <div data-testid="tool-result">
963
- {result ? JSON.stringify(result) : "no-result"}
964
- </div>
965
- </div>
966
- );
967
- },
968
- handler: async (args) => {
969
- handlerStarted = true;
970
- // Simulate async work to allow React to re-render with Executing status
971
- await new Promise((resolve) => setTimeout(resolve, 50));
972
- handlerCompleted = true;
973
- handlerResult = { processed: args.value.toUpperCase() };
974
- return handlerResult;
975
- },
976
- };
977
-
978
- useFrontendTool(tool);
979
-
980
- // No need for subscription here - the hook already subscribes internally
981
-
982
- return null;
983
- };
984
-
985
- renderWithCopilotKit({
986
- agent,
987
- children: (
988
- <>
989
- <ExecutingStateTool />
990
- <div style={{ height: 400 }}>
991
- <CopilotChat welcomeScreen={false} />
992
- </div>
993
- </>
994
- ),
995
- });
996
-
997
- // Submit message to trigger agent.runAgent
998
- const input = await screen.findByRole("textbox");
999
- fireEvent.change(input, { target: { value: "Test executing state" } });
1000
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1001
-
1002
- // Wait for message to appear
1003
- await waitFor(() => {
1004
- expect(screen.getByText("Test executing state")).toBeDefined();
1005
- });
1006
-
1007
- // Emit tool call events from the agent
1008
- const messageId = testId("msg");
1009
- const toolCallId = testId("tc");
1010
-
1011
- agent.emit(runStartedEvent());
1012
- agent.emit(
1013
- toolCallChunkEvent({
1014
- toolCallId,
1015
- toolCallName: "executingStateTool",
1016
- parentMessageId: messageId,
1017
- delta: '{"value":"test"}',
1018
- }),
1019
- );
1020
-
1021
- // Wait for tool to render with InProgress status
1022
- await waitFor(() => {
1023
- const toolEl = screen.getByTestId("executing-tool");
1024
- expect(toolEl).toBeDefined();
1025
- expect(screen.getByTestId("tool-value").textContent).toBe("test");
1026
- expect(screen.getByTestId("tool-status").textContent).toBe(
1027
- ToolCallStatus.InProgress,
1028
- );
1029
- });
1030
-
1031
- agent.emit(runFinishedEvent());
1032
-
1033
- // Complete the agent to trigger handler execution
1034
- agent.complete();
1035
-
1036
- // Trigger another run to process the tool
1037
- await waitFor(
1038
- async () => {
1039
- // The handler should start executing
1040
- expect(handlerStarted).toBe(true);
1041
- },
1042
- { timeout: 3000 },
1043
- );
1044
- // Wait for handler to complete
1045
- await waitFor(
1046
- () => {
1047
- expect(handlerCompleted).toBe(true);
1048
- },
1049
- { timeout: 3000 },
1050
- );
1051
- // Verify the handler executed
1052
- expect(handlerStarted).toBe(true);
1053
- expect(handlerCompleted).toBe(true);
1054
- expect(handlerResult).toEqual({ processed: "TEST" });
1055
-
1056
- // Wait for status to transition to Complete (React re-render cycle)
1057
- await waitFor(
1058
- () => {
1059
- expect(statusHistory).toContain(ToolCallStatus.Complete);
1060
- },
1061
- { timeout: 3000 },
1062
- );
1063
-
1064
- // Verify we captured all three states in the correct order
1065
- expect(statusHistory).toContain(ToolCallStatus.InProgress);
1066
- expect(statusHistory).toContain(ToolCallStatus.Executing);
1067
-
1068
- // Verify the order is correct
1069
- const inProgressIndex = statusHistory.indexOf(ToolCallStatus.InProgress);
1070
- const executingIndex = statusHistory.indexOf(ToolCallStatus.Executing);
1071
- const completeIndex = statusHistory.indexOf(ToolCallStatus.Complete);
1072
-
1073
- expect(inProgressIndex).toBeGreaterThanOrEqual(0);
1074
- expect(executingIndex).toBeGreaterThan(inProgressIndex);
1075
- expect(completeIndex).toBeGreaterThan(executingIndex);
1076
- });
1077
- });
1078
-
1079
- describe("Agent Scoping", () => {
1080
- it("supports multiple tools with same name but different agentId", async () => {
1081
- // Track which handlers are called
1082
- let defaultAgentHandlerCalled = false;
1083
- let specificAgentHandlerCalled = false;
1084
- let wrongAgentHandlerCalled = false;
1085
-
1086
- // We'll test with the default agent
1087
- const agent = new MockStepwiseAgent();
1088
-
1089
- // Tool scoped to "wrongAgent" - should NOT execute
1090
- const WrongAgentTool: React.FC = () => {
1091
- const tool: ReactFrontendTool<{ message: string }> = {
1092
- name: "testTool", // Same name as other tools
1093
- parameters: z.object({ message: z.string() }),
1094
- agentId: "wrongAgent", // Different agent
1095
- render: ({ args }) => (
1096
- <div data-testid="wrong-agent-tool">
1097
- Wrong Agent Tool: {args.message}
1098
- </div>
1099
- ),
1100
- handler: async (args) => {
1101
- wrongAgentHandlerCalled = true;
1102
- return { result: `Wrong agent processed: ${args.message}` };
1103
- },
1104
- };
1105
- useFrontendTool(tool);
1106
- return null;
1107
- };
1108
-
1109
- // Tool scoped to "default" agent - SHOULD execute
1110
- const DefaultAgentTool: React.FC = () => {
1111
- const tool: ReactFrontendTool<{ message: string }> = {
1112
- name: "testTool", // Same name
1113
- parameters: z.object({ message: z.string() }),
1114
- agentId: "default", // Matches our test agent
1115
- render: ({ args, result }) => (
1116
- <div data-testid="default-agent-tool">
1117
- Default Agent Tool: {args.message}
1118
- {result && (
1119
- <div data-testid="default-result">{JSON.stringify(result)}</div>
1120
- )}
1121
- </div>
1122
- ),
1123
- handler: async (args) => {
1124
- defaultAgentHandlerCalled = true;
1125
- return { result: `Default agent processed: ${args.message}` };
1126
- },
1127
- };
1128
- useFrontendTool(tool);
1129
- return null;
1130
- };
1131
-
1132
- // Tool scoped to "specificAgent" - should NOT execute
1133
- const SpecificAgentTool: React.FC = () => {
1134
- const tool: ReactFrontendTool<{ message: string }> = {
1135
- name: "testTool", // Same name again
1136
- parameters: z.object({ message: z.string() }),
1137
- agentId: "specificAgent", // Different agent
1138
- render: ({ args }) => (
1139
- <div data-testid="specific-agent-tool">
1140
- Specific Agent Tool: {args.message}
1141
- </div>
1142
- ),
1143
- handler: async (args) => {
1144
- specificAgentHandlerCalled = true;
1145
- return { result: `Specific agent processed: ${args.message}` };
1146
- },
1147
- };
1148
- useFrontendTool(tool);
1149
- return null;
1150
- };
1151
-
1152
- renderWithCopilotKit({
1153
- agent,
1154
- children: (
1155
- <>
1156
- <WrongAgentTool />
1157
- <DefaultAgentTool />
1158
- <SpecificAgentTool />
1159
- <div style={{ height: 400 }}>
1160
- <CopilotChat agentId="default" />
1161
- </div>
1162
- </>
1163
- ),
1164
- });
1165
-
1166
- // Submit message to trigger tools
1167
- const input = await screen.findByRole("textbox");
1168
- fireEvent.change(input, { target: { value: "Test agent scoping" } });
1169
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1170
-
1171
- await waitFor(() => {
1172
- expect(screen.getByText("Test agent scoping")).toBeDefined();
1173
- });
1174
-
1175
- const messageId = testId("msg");
1176
- const toolCallId = testId("tc");
1177
-
1178
- // Call "testTool" - multiple tools have this name but only the one
1179
- // scoped to "default" agent should execute its handler
1180
- agent.emit(runStartedEvent());
1181
- agent.emit(
1182
- toolCallChunkEvent({
1183
- toolCallId,
1184
- toolCallName: "testTool",
1185
- parentMessageId: messageId,
1186
- delta: '{"message":"test message"}',
1187
- }),
1188
- );
1189
- agent.emit(runFinishedEvent());
1190
-
1191
- // Wait for tool to render - the correct renderer should be used
1192
- await waitFor(() => {
1193
- // The default agent tool should render (it's scoped to our agent)
1194
- const defaultTool = screen.queryByTestId("default-agent-tool");
1195
- expect(defaultTool).not.toBeNull();
1196
- expect(defaultTool!.textContent).toContain("test message");
1197
- });
1198
-
1199
- // Complete the agent to trigger handler execution
1200
- agent.complete();
1201
-
1202
- // Wait for handler execution
1203
- await waitFor(() => {
1204
- // Only the default agent handler should be called
1205
- expect(defaultAgentHandlerCalled).toBe(true);
1206
- });
1207
-
1208
- // Log which handlers were called
1209
- console.log("Handler calls:", {
1210
- defaultAgent: defaultAgentHandlerCalled,
1211
- wrongAgent: wrongAgentHandlerCalled,
1212
- specificAgent: specificAgentHandlerCalled,
1213
- });
1214
-
1215
- // Verify the correct handler was executed and others weren't
1216
- expect(defaultAgentHandlerCalled).toBe(true);
1217
- expect(wrongAgentHandlerCalled).toBe(false);
1218
- expect(specificAgentHandlerCalled).toBe(false);
1219
-
1220
- // Debug: Check what's actually rendered
1221
- const defaultTool = screen.queryByTestId("default-agent-tool");
1222
- const wrongTool = screen.queryByTestId("wrong-agent-tool");
1223
- const specificTool = screen.queryByTestId("specific-agent-tool");
1224
-
1225
- console.log("Tools rendered:", {
1226
- default: defaultTool ? "yes" : "no",
1227
- wrong: wrongTool ? "yes" : "no",
1228
- specific: specificTool ? "yes" : "no",
1229
- });
1230
-
1231
- // Check if result is displayed
1232
- const resultEl = screen.queryByTestId("default-result");
1233
- if (resultEl) {
1234
- console.log("Result element found:", resultEl.textContent);
1235
- } else {
1236
- console.log("No result element found");
1237
- }
1238
-
1239
- // The test reveals whether agent scoping works correctly
1240
- // If the wrong tool's handler is called, this is a bug in core
1241
- });
1242
-
1243
- it("demonstrates that agent scoping prevents execution of tools for wrong agents", async () => {
1244
- // This simpler test shows that agent scoping does work for preventing execution
1245
- let scopedHandlerCalled = false;
1246
- let globalHandlerCalled = false;
1247
-
1248
- const agent = new MockStepwiseAgent();
1249
-
1250
- // Tool scoped to a different agent - should NOT execute
1251
- const ScopedTool: React.FC = () => {
1252
- const tool: ReactFrontendTool<{ message: string }> = {
1253
- name: "scopedTool",
1254
- parameters: z.object({ message: z.string() }),
1255
- agentId: "differentAgent", // Different from default
1256
- render: ({ args, result }) => (
1257
- <div data-testid="scoped-tool">
1258
- Scoped Tool: {args.message}
1259
- {result && (
1260
- <div data-testid="scoped-result">{JSON.stringify(result)}</div>
1261
- )}
1262
- </div>
1263
- ),
1264
- handler: async (args) => {
1265
- scopedHandlerCalled = true;
1266
- return { result: `Scoped processed: ${args.message}` };
1267
- },
1268
- };
1269
- useFrontendTool(tool);
1270
- return null;
1271
- };
1272
-
1273
- // Global tool (no agentId) - SHOULD execute for any agent
1274
- const GlobalTool: React.FC = () => {
1275
- const tool: ReactFrontendTool<{ message: string }> = {
1276
- name: "globalTool",
1277
- parameters: z.object({ message: z.string() }),
1278
- // No agentId - available to all agents
1279
- render: ({ args, result }) => (
1280
- <div data-testid="global-tool">
1281
- Global Tool: {args.message}
1282
- {result && (
1283
- <div data-testid="global-result">{JSON.stringify(result)}</div>
1284
- )}
1285
- </div>
1286
- ),
1287
- handler: async (args) => {
1288
- globalHandlerCalled = true;
1289
- return { result: `Global processed: ${args.message}` };
1290
- },
1291
- };
1292
- useFrontendTool(tool);
1293
- return null;
1294
- };
1295
-
1296
- renderWithCopilotKit({
1297
- agent,
1298
- children: (
1299
- <>
1300
- <ScopedTool />
1301
- <GlobalTool />
1302
- <div style={{ height: 400 }}>
1303
- <CopilotChat agentId="default" />
1304
- </div>
1305
- </>
1306
- ),
1307
- });
1308
-
1309
- // Submit message
1310
- const input = await screen.findByRole("textbox");
1311
- fireEvent.change(input, { target: { value: "Test scoping" } });
1312
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1313
-
1314
- await waitFor(() => {
1315
- expect(screen.getByText("Test scoping")).toBeDefined();
1316
- });
1317
-
1318
- const messageId = testId("msg");
1319
-
1320
- // Try to call the scoped tool - handler should NOT execute
1321
- agent.emit(runStartedEvent());
1322
- agent.emit(
1323
- toolCallChunkEvent({
1324
- toolCallId: testId("tc1"),
1325
- toolCallName: "scopedTool",
1326
- parentMessageId: messageId,
1327
- delta: '{"message":"trying scoped"}',
1328
- }),
1329
- );
1330
-
1331
- // Tool should render (renderer is always shown)
1332
- await waitFor(() => {
1333
- expect(screen.getByTestId("scoped-tool")).toBeDefined();
1334
- });
1335
-
1336
- // Call the global tool - handler SHOULD execute
1337
- agent.emit(
1338
- toolCallChunkEvent({
1339
- toolCallId: testId("tc2"),
1340
- toolCallName: "globalTool",
1341
- parentMessageId: messageId,
1342
- delta: '{"message":"trying global"}',
1343
- }),
1344
- );
1345
-
1346
- await waitFor(() => {
1347
- expect(screen.getByTestId("global-tool")).toBeDefined();
1348
- });
1349
-
1350
- agent.emit(runFinishedEvent());
1351
- agent.complete();
1352
-
1353
- // Wait for the global handler to be called
1354
- await waitFor(() => {
1355
- expect(globalHandlerCalled).toBe(true);
1356
- });
1357
-
1358
- // Verify that only the global handler was called
1359
- expect(scopedHandlerCalled).toBe(false); // Should NOT be called (wrong agent)
1360
- expect(globalHandlerCalled).toBe(true); // Should be called (no agent restriction)
1361
-
1362
- // The scoped tool should render but have no result
1363
- const scopedResult = screen.queryByTestId("scoped-result");
1364
- expect(scopedResult).toBeNull();
1365
-
1366
- // The global tool should have a result
1367
- await waitFor(() => {
1368
- const globalResult = screen.getByTestId("global-result");
1369
- expect(globalResult.textContent).toContain(
1370
- "Global processed: trying global",
1371
- );
1372
- });
1373
- });
1374
- });
1375
-
1376
- describe("Nested Tool Calls", () => {
1377
- it("should enable tool calls that render other tools", async () => {
1378
- const agent = new MockStepwiseAgent();
1379
- let childToolRegistered = false;
1380
-
1381
- // Simple approach: both tools registered at top level
1382
- // but one triggers the other through tool calls
1383
- const ChildTool: React.FC = () => {
1384
- const tool: ReactFrontendTool<{ childValue: string }> = {
1385
- name: "childTool",
1386
- parameters: z.object({ childValue: z.string() }),
1387
- render: ({ args }) => (
1388
- <div data-testid="child-tool">Child: {args.childValue}</div>
1389
- ),
1390
- };
1391
-
1392
- useFrontendTool(tool);
1393
-
1394
- useEffect(() => {
1395
- childToolRegistered = true;
1396
- }, []);
1397
-
1398
- return null;
1399
- };
1400
-
1401
- const ParentTool: React.FC = () => {
1402
- const tool: ReactFrontendTool<{ parentValue: string }> = {
1403
- name: "parentTool",
1404
- parameters: z.object({ parentValue: z.string() }),
1405
- render: ({ args }) => (
1406
- <div data-testid="parent-tool">Parent: {args.parentValue}</div>
1407
- ),
1408
- };
1409
-
1410
- useFrontendTool(tool);
1411
- return null;
1412
- };
1413
-
1414
- renderWithCopilotKit({
1415
- agent,
1416
- children: (
1417
- <>
1418
- <ParentTool />
1419
- <ChildTool />
1420
- <div style={{ height: 400 }}>
1421
- <CopilotChat welcomeScreen={false} />
1422
- </div>
1423
- </>
1424
- ),
1425
- });
1426
-
1427
- // Verify both tools are registered
1428
- expect(childToolRegistered).toBe(true);
1429
-
1430
- // Submit message
1431
- const input = await screen.findByRole("textbox");
1432
- fireEvent.change(input, { target: { value: "Test nested tools" } });
1433
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1434
-
1435
- await waitFor(() => {
1436
- expect(screen.getByText("Test nested tools")).toBeDefined();
1437
- });
1438
-
1439
- const messageId = testId("msg");
1440
-
1441
- // Call parent tool
1442
- agent.emit(runStartedEvent());
1443
- agent.emit(
1444
- toolCallChunkEvent({
1445
- toolCallId: testId("parent-tc"),
1446
- toolCallName: "parentTool",
1447
- parentMessageId: messageId,
1448
- delta: '{"parentValue":"test parent"}',
1449
- }),
1450
- );
1451
-
1452
- // Parent tool should render
1453
- await waitFor(() => {
1454
- expect(screen.getByTestId("parent-tool")).toBeDefined();
1455
- });
1456
-
1457
- // Now call the child tool (simulating nested call)
1458
- agent.emit(
1459
- toolCallChunkEvent({
1460
- toolCallId: testId("child-tc"),
1461
- toolCallName: "childTool",
1462
- parentMessageId: messageId,
1463
- delta: '{"childValue":"test child"}',
1464
- }),
1465
- );
1466
-
1467
- // Child tool should render
1468
- await waitFor(() => {
1469
- expect(screen.getByTestId("child-tool")).toBeDefined();
1470
- expect(screen.getByTestId("child-tool").textContent).toContain(
1471
- "test child",
1472
- );
1473
- });
1474
-
1475
- agent.emit(runFinishedEvent());
1476
- agent.complete();
1477
- });
1478
- });
1479
-
1480
- describe("Tool Availability", () => {
1481
- it("should ensure tools are available when request is made", async () => {
1482
- const agent = new MockStepwiseAgent();
1483
-
1484
- const AvailabilityTestTool: React.FC<{ onRegistered?: () => void }> = ({
1485
- onRegistered,
1486
- }) => {
1487
- const tool: ReactFrontendTool<{ test: string }> = {
1488
- name: "availabilityTool",
1489
- parameters: z.object({ test: z.string() }),
1490
- render: ({ args }) => (
1491
- <div data-testid="availability-tool">{args.test}</div>
1492
- ),
1493
- handler: async (args) => ({ received: args.test }),
1494
- };
1495
-
1496
- useFrontendTool(tool);
1497
-
1498
- // Notify when registered
1499
- useEffect(() => {
1500
- onRegistered?.();
1501
- }, [onRegistered]);
1502
-
1503
- return null;
1504
- };
1505
-
1506
- let toolRegistered = false;
1507
- const onRegistered = () => {
1508
- toolRegistered = true;
1509
- };
1510
-
1511
- renderWithCopilotKit({
1512
- agent,
1513
- children: (
1514
- <>
1515
- <AvailabilityTestTool onRegistered={onRegistered} />
1516
- <div style={{ height: 400 }}>
1517
- <CopilotChat welcomeScreen={false} />
1518
- </div>
1519
- </>
1520
- ),
1521
- });
1522
-
1523
- // Tool should be available immediately after mounting
1524
- await waitFor(() => {
1525
- expect(toolRegistered).toBe(true);
1526
- });
1527
-
1528
- // Verify tool is in copilotkit.tools
1529
- // Note: We can't directly access copilotkit.tools from here,
1530
- // but we can verify it works by calling it
1531
- const input = await screen.findByRole("textbox");
1532
- fireEvent.change(input, { target: { value: "Test availability" } });
1533
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1534
-
1535
- await waitFor(() => {
1536
- expect(screen.getByText("Test availability")).toBeDefined();
1537
- });
1538
-
1539
- // Tool call should work immediately
1540
- agent.emit(runStartedEvent());
1541
- agent.emit(
1542
- toolCallChunkEvent({
1543
- toolCallId: testId("tc"),
1544
- toolCallName: "availabilityTool",
1545
- parentMessageId: testId("msg"),
1546
- delta: '{"test":"available"}',
1547
- }),
1548
- );
1549
-
1550
- // Tool should render successfully
1551
- await waitFor(() => {
1552
- expect(screen.getByTestId("availability-tool")).toBeDefined();
1553
- expect(screen.getByTestId("availability-tool").textContent).toBe(
1554
- "available",
1555
- );
1556
- });
1557
-
1558
- agent.emit(runFinishedEvent());
1559
- agent.complete();
1560
- });
1561
- });
1562
-
1563
- describe("Re-render Idempotence", () => {
1564
- it("should not create duplicates on re-render", async () => {
1565
- const agent = new MockStepwiseAgent();
1566
- let renderCount = 0;
1567
-
1568
- const IdempotentTool: React.FC = () => {
1569
- // Use state to trigger re-renders
1570
- const [counter, setCounter] = useState(0);
1571
-
1572
- const tool: ReactFrontendTool<{ value: string }> = {
1573
- name: "idempotentTool",
1574
- parameters: z.object({ value: z.string() }),
1575
- render: ({ args }) => {
1576
- renderCount++;
1577
- return (
1578
- <div data-testid="idempotent-tool">
1579
- Value: {args.value} | Renders: {renderCount}
1580
- </div>
1581
- );
1582
- },
1583
- };
1584
-
1585
- useFrontendTool(tool);
1586
-
1587
- return (
1588
- <div>
1589
- <button
1590
- data-testid="rerender-button"
1591
- onClick={() => setCounter((c) => c + 1)}
1592
- >
1593
- Re-render ({counter})
1594
- </button>
1595
- </div>
1596
- );
1597
- };
1598
-
1599
- renderWithCopilotKit({
1600
- agent,
1601
- children: (
1602
- <>
1603
- <IdempotentTool />
1604
- <div style={{ height: 400 }}>
1605
- <CopilotChat welcomeScreen={false} />
1606
- </div>
1607
- </>
1608
- ),
1609
- });
1610
-
1611
- // Submit message
1612
- const input = await screen.findByRole("textbox");
1613
- fireEvent.change(input, { target: { value: "Test idempotence" } });
1614
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1615
-
1616
- await waitFor(() => {
1617
- expect(screen.getByText("Test idempotence")).toBeDefined();
1618
- });
1619
-
1620
- // Emit tool call
1621
- agent.emit(runStartedEvent());
1622
- agent.emit(
1623
- toolCallChunkEvent({
1624
- toolCallId: testId("tc"),
1625
- toolCallName: "idempotentTool",
1626
- parentMessageId: testId("msg"),
1627
- delta: '{"value":"test"}',
1628
- }),
1629
- );
1630
-
1631
- // Tool should render once
1632
- await waitFor(() => {
1633
- const tools = screen.getAllByTestId("idempotent-tool");
1634
- expect(tools).toHaveLength(1);
1635
- expect(tools[0]?.textContent).toContain("Value: test");
1636
- });
1637
-
1638
- const initialRenderCount = renderCount;
1639
-
1640
- // Trigger re-render by clicking button
1641
- fireEvent.click(screen.getByTestId("rerender-button"));
1642
-
1643
- // Wait for re-render
1644
- await waitFor(() => {
1645
- const button = screen.getByTestId("rerender-button");
1646
- expect(button.textContent).toContain("1");
1647
- });
1648
-
1649
- // Tool should still render only once (no duplicate elements)
1650
- const toolsAfterRerender = screen.getAllByTestId("idempotent-tool");
1651
- expect(toolsAfterRerender).toHaveLength(1);
1652
-
1653
- // The render count should not have increased dramatically
1654
- // (may increase slightly due to React re-renders, but not duplicate the tool)
1655
- expect(renderCount).toBeLessThanOrEqual(initialRenderCount + 2);
1656
-
1657
- agent.emit(runFinishedEvent());
1658
- agent.complete();
1659
- });
1660
- });
1661
-
1662
- describe("useFrontendTool dependencies", () => {
1663
- it("updates tool renderer when optional deps change", async () => {
1664
- const DependencyDrivenTool: React.FC = () => {
1665
- const [version, setVersion] = useState(0);
1666
-
1667
- const tool: ReactFrontendTool<{ message: string }> = {
1668
- name: "dependencyTool",
1669
- parameters: z.object({ message: z.string() }),
1670
- render: ({ args }) => (
1671
- <div data-testid="dependency-tool-render">
1672
- {args.message} (v{version})
1673
- </div>
1674
- ),
1675
- };
1676
-
1677
- useFrontendTool(tool, [version]);
1678
-
1679
- const toolCallId = testId("dep_tc");
1680
- const assistantMessage: AssistantMessage = {
1681
- id: testId("dep_a"),
1682
- role: "assistant",
1683
- content: "",
1684
- toolCalls: [
1685
- {
1686
- id: toolCallId,
1687
- type: "function",
1688
- function: {
1689
- name: "dependencyTool",
1690
- arguments: JSON.stringify({ message: "hello" }),
1691
- },
1692
- } as any,
1693
- ],
1694
- } as any;
1695
- const messages: Message[] = [];
1696
-
1697
- return (
1698
- <>
1699
- <button
1700
- data-testid="bump-version"
1701
- type="button"
1702
- onClick={() => setVersion((v) => v + 1)}
1703
- >
1704
- Bump
1705
- </button>
1706
- <CopilotChatToolCallsView
1707
- message={assistantMessage}
1708
- messages={messages}
1709
- />
1710
- </>
1711
- );
1712
- };
1713
-
1714
- renderWithCopilotKit({
1715
- children: <DependencyDrivenTool />,
1716
- });
1717
-
1718
- await waitFor(() => {
1719
- const el = screen.getByTestId("dependency-tool-render");
1720
- expect(el).toBeDefined();
1721
- expect(el.textContent).toContain("hello");
1722
- expect(el.textContent).toContain("(v0)");
1723
- });
1724
-
1725
- fireEvent.click(screen.getByTestId("bump-version"));
1726
-
1727
- await waitFor(() => {
1728
- const el = screen.getByTestId("dependency-tool-render");
1729
- expect(el.textContent).toContain("(v1)");
1730
- });
1731
- });
1732
- });
1733
-
1734
- describe("Error Propagation", () => {
1735
- it("should propagate handler errors to renderer", async () => {
1736
- const agent = new MockStepwiseAgent();
1737
- let handlerCalled = false;
1738
- let errorThrown = false;
1739
-
1740
- const ErrorTool: React.FC = () => {
1741
- const tool: ReactFrontendTool<{
1742
- shouldError: boolean;
1743
- message: string;
1744
- }> = {
1745
- name: "errorTool",
1746
- parameters: z.object({
1747
- shouldError: z.boolean(),
1748
- message: z.string(),
1749
- }),
1750
- render: ({ args, status, result }) => (
1751
- <div data-testid="error-tool">
1752
- <div data-testid="error-status">{status}</div>
1753
- <div data-testid="error-message">{args.message}</div>
1754
- <div data-testid="error-result">
1755
- {result ? String(result) : "no-result"}
1756
- </div>
1757
- </div>
1758
- ),
1759
- handler: async (args) => {
1760
- handlerCalled = true;
1761
- if (args.shouldError) {
1762
- errorThrown = true;
1763
- throw new Error(`Handler error: ${args.message}`);
1764
- }
1765
- return { success: true, message: args.message };
1766
- },
1767
- };
1768
-
1769
- useFrontendTool(tool);
1770
- return null;
1771
- };
1772
-
1773
- renderWithCopilotKit({
1774
- agent,
1775
- children: (
1776
- <>
1777
- <ErrorTool />
1778
- <div style={{ height: 400 }}>
1779
- <CopilotChat welcomeScreen={false} />
1780
- </div>
1781
- </>
1782
- ),
1783
- });
1784
-
1785
- // Submit message
1786
- const input = await screen.findByRole("textbox");
1787
- fireEvent.change(input, { target: { value: "Test error" } });
1788
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1789
-
1790
- await waitFor(() => {
1791
- expect(screen.getByText("Test error")).toBeDefined();
1792
- });
1793
-
1794
- // Emit tool call that will error
1795
- const messageId = testId("msg");
1796
- const toolCallId = testId("tc");
1797
-
1798
- agent.emit(runStartedEvent());
1799
- agent.emit(
1800
- toolCallChunkEvent({
1801
- toolCallId,
1802
- toolCallName: "errorTool",
1803
- parentMessageId: messageId,
1804
- delta: '{"shouldError":true,"message":"test error"}',
1805
- }),
1806
- );
1807
- agent.emit(runFinishedEvent());
1808
-
1809
- // Wait for tool to render
1810
- await waitFor(() => {
1811
- expect(screen.getByTestId("error-tool")).toBeDefined();
1812
- });
1813
-
1814
- // Complete the agent to trigger handler execution
1815
- agent.complete();
1816
-
1817
- // Wait for handler to be called and error to be thrown
1818
- await waitFor(() => {
1819
- expect(handlerCalled).toBe(true);
1820
- expect(errorThrown).toBe(true);
1821
- });
1822
-
1823
- // Wait for the error result to be displayed in the renderer
1824
- await waitFor(() => {
1825
- const resultEl = screen.getByTestId("error-result");
1826
- const resultText = resultEl.textContent || "";
1827
- expect(resultText).not.toBe("no-result");
1828
- expect(resultText).toContain("Error:");
1829
- expect(resultText).toContain("Handler error: test error");
1830
- });
1831
-
1832
- // Status should be complete even with error
1833
- expect(screen.getByTestId("error-status").textContent).toBe(
1834
- ToolCallStatus.Complete,
1835
- );
1836
- });
1837
-
1838
- it("should handle async errors in handler", async () => {
1839
- const agent = new MockStepwiseAgent();
1840
-
1841
- const AsyncErrorTool: React.FC = () => {
1842
- const tool: ReactFrontendTool<{ delay: number; errorMessage: string }> =
1843
- {
1844
- name: "asyncErrorTool",
1845
- parameters: z.object({
1846
- delay: z.number(),
1847
- errorMessage: z.string(),
1848
- }),
1849
- render: ({ args, status, result }) => (
1850
- <div data-testid="async-error-tool">
1851
- <div data-testid="async-status">{status}</div>
1852
- <div data-testid="async-delay">Delay: {args.delay}ms</div>
1853
- <div data-testid="async-error-msg">{args.errorMessage}</div>
1854
- {result && <div data-testid="async-result">{result}</div>}
1855
- </div>
1856
- ),
1857
- handler: async (args) => {
1858
- // Simulate async operation
1859
- await new Promise((resolve) => setTimeout(resolve, args.delay));
1860
- // In test environment, throwing might not propagate as expected
1861
- throw new Error(args.errorMessage);
1862
- },
1863
- };
1864
-
1865
- useFrontendTool(tool);
1866
- return null;
1867
- };
1868
-
1869
- renderWithCopilotKit({
1870
- agent,
1871
- children: (
1872
- <>
1873
- <AsyncErrorTool />
1874
- <div style={{ height: 400 }}>
1875
- <CopilotChat welcomeScreen={false} />
1876
- </div>
1877
- </>
1878
- ),
1879
- });
1880
-
1881
- // Submit message
1882
- const input = await screen.findByRole("textbox");
1883
- fireEvent.change(input, { target: { value: "Test async error" } });
1884
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1885
-
1886
- await waitFor(() => {
1887
- expect(screen.getByText("Test async error")).toBeDefined();
1888
- });
1889
-
1890
- // Emit tool call that will error after delay
1891
- agent.emit(runStartedEvent());
1892
- agent.emit(
1893
- toolCallChunkEvent({
1894
- toolCallId: testId("tc"),
1895
- toolCallName: "asyncErrorTool",
1896
- parentMessageId: testId("msg"),
1897
- delta:
1898
- '{"delay":10,"errorMessage":"Async operation failed after delay"}',
1899
- }),
1900
- );
1901
-
1902
- // Tool should render immediately with args
1903
- await waitFor(() => {
1904
- expect(screen.getByTestId("async-error-tool")).toBeDefined();
1905
- expect(screen.getByTestId("async-delay").textContent).toContain("10ms");
1906
- expect(screen.getByTestId("async-error-msg").textContent).toContain(
1907
- "Async operation failed",
1908
- );
1909
- });
1910
-
1911
- // The test verifies that:
1912
- // 1. Async tools with delays can render immediately
1913
- // 2. Error messages are properly passed through args
1914
- // 3. The tool continues to function even with async handlers that may throw
1915
-
1916
- // In production, the error would be caught and sent as a result
1917
- // but in test environment, handler execution may not complete
1918
-
1919
- agent.emit(runFinishedEvent());
1920
- agent.complete();
1921
- });
1922
- });
1923
-
1924
- describe("Wildcard Handler", () => {
1925
- it("should handle unknown tools with wildcard", async () => {
1926
- const agent = new MockStepwiseAgent();
1927
- const wildcardHandlerCalls: { name: string; args: any }[] = [];
1928
-
1929
- // Note: Wildcard tools work as fallback renderers when no specific tool is found
1930
- // The wildcard renderer receives the original tool name and arguments
1931
- const WildcardTool: React.FC = () => {
1932
- const tool: ReactFrontendTool<any> = {
1933
- name: "*",
1934
- parameters: z.any(),
1935
- render: ({ name, args, status, result }) => (
1936
- <div data-testid={`wildcard-render-${name}`}>
1937
- <div data-testid="wildcard-tool-name">
1938
- Wildcard caught: {name}
1939
- </div>
1940
- <div data-testid="wildcard-args">
1941
- Args: {JSON.stringify(args)}
1942
- </div>
1943
- <div data-testid="wildcard-status">Status: {status}</div>
1944
- {result && (
1945
- <div data-testid="wildcard-result">Result: {result}</div>
1946
- )}
1947
- </div>
1948
- ),
1949
- handler: async (args: any) => {
1950
- // Track handler calls
1951
- wildcardHandlerCalls.push({ name: "wildcard", args });
1952
- return { handled: "by wildcard", receivedArgs: args };
1953
- },
1954
- };
1955
-
1956
- useFrontendTool(tool);
1957
- return null;
1958
- };
1959
-
1960
- renderWithCopilotKit({
1961
- agent,
1962
- children: (
1963
- <>
1964
- <WildcardTool />
1965
- <div style={{ height: 400 }}>
1966
- <CopilotChat welcomeScreen={false} />
1967
- </div>
1968
- </>
1969
- ),
1970
- });
1971
-
1972
- // Submit message
1973
- const input = await screen.findByRole("textbox");
1974
- fireEvent.change(input, { target: { value: "Test wildcard" } });
1975
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
1976
-
1977
- await waitFor(() => {
1978
- expect(screen.getByText("Test wildcard")).toBeDefined();
1979
- });
1980
-
1981
- agent.emit(runStartedEvent());
1982
-
1983
- // Test 1: Call first undefined tool
1984
- agent.emit(
1985
- toolCallChunkEvent({
1986
- toolCallId: testId("tc1"),
1987
- toolCallName: "undefinedTool",
1988
- parentMessageId: testId("msg"),
1989
- delta: '{"someParam":"value","anotherParam":123}',
1990
- }),
1991
- );
1992
-
1993
- // Wildcard should render the unknown tool with correct name and args
1994
- await waitFor(() => {
1995
- const nameEl = screen.getByTestId("wildcard-tool-name");
1996
- expect(nameEl.textContent).toContain("undefinedTool");
1997
- const argsEl = screen.getByTestId("wildcard-args");
1998
- expect(argsEl.textContent).toContain("someParam");
1999
- expect(argsEl.textContent).toContain("value");
2000
- expect(argsEl.textContent).toContain("123");
2001
- });
2002
-
2003
- // Check status is InProgress or Complete
2004
- await waitFor(() => {
2005
- const statusEl = screen.getByTestId("wildcard-status");
2006
- expect(statusEl.textContent).toMatch(/Status: (inProgress|complete)/);
2007
- });
2008
-
2009
- // Test 2: Call another undefined tool to verify wildcard catches multiple
2010
- agent.emit(
2011
- toolCallChunkEvent({
2012
- toolCallId: testId("tc2"),
2013
- toolCallName: "anotherUnknownTool",
2014
- parentMessageId: testId("msg"),
2015
- delta: '{"differentArg":"test"}',
2016
- }),
2017
- );
2018
-
2019
- // Should render both unknown tools
2020
- await waitFor(() => {
2021
- const tool1 = screen.getByTestId("wildcard-render-undefinedTool");
2022
- const tool2 = screen.getByTestId("wildcard-render-anotherUnknownTool");
2023
- expect(tool1).toBeDefined();
2024
- expect(tool2).toBeDefined();
2025
- });
2026
-
2027
- // Send result for first tool
2028
- agent.emit(
2029
- toolCallResultEvent({
2030
- toolCallId: testId("tc1"),
2031
- messageId: testId("msg_result"),
2032
- content: "Tool executed successfully",
2033
- }),
2034
- );
2035
-
2036
- // Check result is displayed
2037
- await waitFor(() => {
2038
- const resultEl = screen.queryByTestId("wildcard-result");
2039
- if (resultEl) {
2040
- expect(resultEl.textContent).toContain("Tool executed successfully");
2041
- }
2042
- });
2043
-
2044
- agent.emit(runFinishedEvent());
2045
- agent.complete();
2046
- });
2047
- });
2048
-
2049
- describe("Renderer Precedence", () => {
2050
- it("should use specific renderer over wildcard", async () => {
2051
- const agent = new MockStepwiseAgent();
2052
-
2053
- // Specific tool
2054
- const SpecificTool: React.FC = () => {
2055
- const tool: ReactFrontendTool<{ value: string }> = {
2056
- name: "specificTool",
2057
- parameters: z.object({ value: z.string() }),
2058
- render: ({ args }) => (
2059
- <div data-testid="specific-render">Specific: {args.value}</div>
2060
- ),
2061
- };
2062
- useFrontendTool(tool);
2063
- return null;
2064
- };
2065
-
2066
- // Wildcard tool - should only catch unknown tools
2067
- const WildcardTool: React.FC = () => {
2068
- const tool: ReactFrontendTool<any> = {
2069
- name: "*",
2070
- parameters: z.any(),
2071
- render: ({ name }) => (
2072
- <div data-testid="wildcard-render">Wildcard: {name}</div>
2073
- ),
2074
- };
2075
- useFrontendTool(tool);
2076
- return null;
2077
- };
2078
-
2079
- renderWithCopilotKit({
2080
- agent,
2081
- children: (
2082
- <>
2083
- <SpecificTool />
2084
- <WildcardTool />
2085
- <div style={{ height: 400 }}>
2086
- <CopilotChat welcomeScreen={false} />
2087
- </div>
2088
- </>
2089
- ),
2090
- });
2091
-
2092
- // Submit message
2093
- const input = await screen.findByRole("textbox");
2094
- fireEvent.change(input, { target: { value: "Test precedence" } });
2095
- fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
2096
-
2097
- await waitFor(() => {
2098
- expect(screen.getByText("Test precedence")).toBeDefined();
2099
- });
2100
-
2101
- agent.emit(runStartedEvent());
2102
-
2103
- // Call specific tool - should use specific renderer
2104
- agent.emit(
2105
- toolCallChunkEvent({
2106
- toolCallId: testId("tc1"),
2107
- toolCallName: "specificTool",
2108
- parentMessageId: testId("msg"),
2109
- delta: '{"value":"test specific"}',
2110
- }),
2111
- );
2112
-
2113
- // Should render with specific renderer, not wildcard
2114
- await waitFor(() => {
2115
- expect(screen.getByTestId("specific-render")).toBeDefined();
2116
- expect(screen.getByTestId("specific-render").textContent).toContain(
2117
- "test specific",
2118
- );
2119
- });
2120
-
2121
- // Call unknown tool - should use wildcard renderer
2122
- agent.emit(
2123
- toolCallChunkEvent({
2124
- toolCallId: testId("tc2"),
2125
- toolCallName: "unknownTool",
2126
- parentMessageId: testId("msg"),
2127
- delta: '{"someArg":"test wildcard"}',
2128
- }),
2129
- );
2130
-
2131
- // Should render with wildcard renderer
2132
- await waitFor(() => {
2133
- const wildcards = screen.getAllByTestId("wildcard-render");
2134
- expect(wildcards.length).toBeGreaterThan(0);
2135
- const unknownToolRender = wildcards.find((el) =>
2136
- el.textContent?.includes("unknownTool"),
2137
- );
2138
- expect(unknownToolRender).toBeDefined();
2139
- });
2140
-
2141
- // Verify specific tool still used its renderer (not replaced by wildcard)
2142
- expect(screen.getByTestId("specific-render")).toBeDefined();
2143
-
2144
- agent.emit(runFinishedEvent());
2145
- agent.complete();
2146
- });
2147
- });
2148
- });