@copilotkit/react-core 1.57.3 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{copilotkit-CC8DjOiC.mjs → copilotkit-BIn7HE8f.mjs} +2 -2
  3. package/dist/{copilotkit-CC8DjOiC.mjs.map → copilotkit-BIn7HE8f.mjs.map} +1 -1
  4. package/dist/{copilotkit-CtXcs1ea.cjs → copilotkit-Drw-g6zA.cjs} +2 -2
  5. package/dist/{copilotkit-CtXcs1ea.cjs.map → copilotkit-Drw-g6zA.cjs.map} +1 -1
  6. package/dist/index.cjs +3 -77
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +2 -2
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.mjs +3 -77
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/index.umd.js +3 -77
  13. package/dist/index.umd.js.map +1 -1
  14. package/dist/v2/index.cjs +1 -1
  15. package/dist/v2/index.mjs +1 -1
  16. package/dist/v2/index.umd.js +1 -1
  17. package/dist/v2/index.umd.js.map +1 -1
  18. package/package.json +12 -13
  19. package/skills/react-core/SKILL.md +108 -0
  20. package/skills/react-core/references/agent-access.md +288 -0
  21. package/skills/react-core/references/attachments.md +291 -0
  22. package/skills/react-core/references/capabilities.md +138 -0
  23. package/skills/react-core/references/chat-components.md +221 -0
  24. package/skills/react-core/references/client-side-tools.md +358 -0
  25. package/skills/react-core/references/custom-message-renderers.md +226 -0
  26. package/skills/react-core/references/debug-mode.md +153 -0
  27. package/skills/react-core/references/human-in-the-loop.md +312 -0
  28. package/skills/react-core/references/provider-setup.md +326 -0
  29. package/skills/react-core/references/rendering-activity-messages.md +207 -0
  30. package/skills/react-core/references/rendering-tool-calls.md +319 -0
  31. package/skills/react-core/references/suggestions.md +211 -0
  32. package/skills/react-core/references/switching-agents-recipes.md +160 -0
  33. package/skills/react-core/references/switching-agents.md +231 -0
  34. package/skills/react-core/references/threads.md +226 -0
  35. package/.attw.json +0 -3
  36. package/CHANGELOG.md +0 -5043
  37. package/scripts/scope-preflight.mjs +0 -100
  38. package/src/components/CopilotListeners.tsx +0 -137
  39. package/src/components/__tests__/CopilotListeners.test.tsx +0 -38
  40. package/src/components/copilot-provider/__tests__/copilot-messages-key.test.tsx +0 -92
  41. package/src/components/copilot-provider/__tests__/copilotkit-error.test.tsx +0 -77
  42. package/src/components/copilot-provider/__tests__/error-visibility-prod.test.tsx +0 -70
  43. package/src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx +0 -107
  44. package/src/components/copilot-provider/copilot-messages.tsx +0 -314
  45. package/src/components/copilot-provider/copilotkit-props.tsx +0 -214
  46. package/src/components/copilot-provider/copilotkit.tsx +0 -853
  47. package/src/components/copilot-provider/index.ts +0 -3
  48. package/src/components/dev-console/console-trigger.tsx +0 -283
  49. package/src/components/dev-console/developer-console-modal.tsx +0 -1016
  50. package/src/components/dev-console/icons.tsx +0 -106
  51. package/src/components/error-boundary/error-boundary.tsx +0 -99
  52. package/src/components/error-boundary/error-utils.tsx +0 -105
  53. package/src/components/index.ts +0 -1
  54. package/src/components/toast/exclamation-mark-icon.tsx +0 -27
  55. package/src/components/toast/toast-provider.tsx +0 -448
  56. package/src/components/usage-banner.tsx +0 -266
  57. package/src/context/__tests__/threads-context.test.tsx +0 -141
  58. package/src/context/coagent-state-renders-context.tsx +0 -89
  59. package/src/context/copilot-context.tsx +0 -365
  60. package/src/context/copilot-messages-context.tsx +0 -35
  61. package/src/context/index.ts +0 -22
  62. package/src/context/threads-context.tsx +0 -69
  63. package/src/hooks/__tests__/use-coagent-config.test.ts +0 -352
  64. package/src/hooks/__tests__/use-coagent-state-render-bridge.helpers.test.ts +0 -107
  65. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +0 -1209
  66. package/src/hooks/__tests__/use-coagent-state-render.test.tsx +0 -356
  67. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +0 -241
  68. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -72
  69. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +0 -102
  70. package/src/hooks/index.ts +0 -33
  71. package/src/hooks/use-agent-nodename.ts +0 -33
  72. package/src/hooks/use-coagent-state-render-bridge.helpers.ts +0 -345
  73. package/src/hooks/use-coagent-state-render-bridge.tsx +0 -222
  74. package/src/hooks/use-coagent-state-render-registry.ts +0 -230
  75. package/src/hooks/use-coagent-state-render.ts +0 -163
  76. package/src/hooks/use-coagent.ts +0 -377
  77. package/src/hooks/use-configure-chat-suggestions.tsx +0 -96
  78. package/src/hooks/use-copilot-action.ts +0 -245
  79. package/src/hooks/use-copilot-additional-instructions.ts +0 -98
  80. package/src/hooks/use-copilot-authenticated-action.ts +0 -73
  81. package/src/hooks/use-copilot-chat-headless_c.ts +0 -264
  82. package/src/hooks/use-copilot-chat-suggestions.tsx +0 -134
  83. package/src/hooks/use-copilot-chat.ts +0 -132
  84. package/src/hooks/use-copilot-chat_internal.ts +0 -875
  85. package/src/hooks/use-copilot-readable.ts +0 -135
  86. package/src/hooks/use-copilot-runtime-client.ts +0 -178
  87. package/src/hooks/use-default-tool.ts +0 -13
  88. package/src/hooks/use-flat-category-store.ts +0 -109
  89. package/src/hooks/use-frontend-tool.ts +0 -113
  90. package/src/hooks/use-human-in-the-loop.ts +0 -138
  91. package/src/hooks/use-langgraph-interrupt.ts +0 -103
  92. package/src/hooks/use-lazy-tool-renderer.tsx +0 -30
  93. package/src/hooks/use-make-copilot-document-readable.ts +0 -30
  94. package/src/hooks/use-render-tool-call.ts +0 -89
  95. package/src/hooks/use-tree.ts +0 -222
  96. package/src/index.tsx +0 -7
  97. package/src/lib/copilot-task.ts +0 -215
  98. package/src/lib/index.ts +0 -1
  99. package/src/lib/status-checker.ts +0 -67
  100. package/src/setupTests.ts +0 -37
  101. package/src/test-helpers/copilot-context.ts +0 -91
  102. package/src/types/chat-suggestion-configuration.ts +0 -23
  103. package/src/types/coagent-action.ts +0 -35
  104. package/src/types/coagent-state.ts +0 -13
  105. package/src/types/crew.ts +0 -89
  106. package/src/types/document-pointer.ts +0 -7
  107. package/src/types/frontend-action.ts +0 -213
  108. package/src/types/index.ts +0 -17
  109. package/src/types/interrupt-action.ts +0 -58
  110. package/src/types/system-message.ts +0 -4
  111. package/src/utils/dev-console.ts +0 -19
  112. package/src/utils/index.ts +0 -2
  113. package/src/utils/suggestions-constants.ts +0 -8
  114. package/src/utils/utils.test.ts +0 -7
  115. package/src/utils/utils.ts +0 -6
  116. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +0 -240
  117. package/src/v2/__tests__/globalSetup.ts +0 -14
  118. package/src/v2/__tests__/setup.ts +0 -93
  119. package/src/v2/__tests__/utils/test-helpers.tsx +0 -570
  120. package/src/v2/a2ui/A2UICatalogContext.tsx +0 -79
  121. package/src/v2/a2ui/A2UIMessageRenderer.tsx +0 -294
  122. package/src/v2/a2ui/A2UIToolCallRenderer.tsx +0 -290
  123. package/src/v2/components/CopilotKitInspector.tsx +0 -52
  124. package/src/v2/components/MCPAppsActivityRenderer.tsx +0 -815
  125. package/src/v2/components/OpenGenerativeUIRenderer.tsx +0 -598
  126. package/src/v2/components/WildcardToolCallRender.tsx +0 -86
  127. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +0 -665
  128. package/src/v2/components/chat/CopilotChat.tsx +0 -664
  129. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +0 -393
  130. package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +0 -374
  131. package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +0 -159
  132. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +0 -350
  133. package/src/v2/components/chat/CopilotChatInput.tsx +0 -1412
  134. package/src/v2/components/chat/CopilotChatMessageView.tsx +0 -716
  135. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +0 -265
  136. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +0 -59
  137. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +0 -134
  138. package/src/v2/components/chat/CopilotChatToggleButton.tsx +0 -171
  139. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +0 -40
  140. package/src/v2/components/chat/CopilotChatUserMessage.tsx +0 -445
  141. package/src/v2/components/chat/CopilotChatView.tsx +0 -890
  142. package/src/v2/components/chat/CopilotModalHeader.tsx +0 -129
  143. package/src/v2/components/chat/CopilotPopup.tsx +0 -81
  144. package/src/v2/components/chat/CopilotPopupView.tsx +0 -317
  145. package/src/v2/components/chat/CopilotSidebar.tsx +0 -80
  146. package/src/v2/components/chat/CopilotSidebarView.tsx +0 -269
  147. package/src/v2/components/chat/Lightbox.tsx +0 -103
  148. package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +0 -66
  149. package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +0 -168
  150. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +0 -1239
  151. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +0 -73
  152. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +0 -432
  153. package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +0 -183
  154. package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +0 -184
  155. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +0 -649
  156. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +0 -624
  157. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +0 -702
  158. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.thumbs.test.tsx +0 -72
  159. package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +0 -241
  160. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +0 -107
  161. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +0 -929
  162. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +0 -1567
  163. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +0 -1004
  164. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +0 -279
  165. package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +0 -336
  166. package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +0 -249
  167. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +0 -530
  168. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +0 -785
  169. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +0 -2416
  170. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +0 -621
  171. package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +0 -56
  172. package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +0 -264
  173. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +0 -853
  174. package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +0 -94
  175. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +0 -1050
  176. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +0 -484
  177. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +0 -612
  178. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.tsx +0 -159
  179. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +0 -502
  180. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +0 -1068
  181. package/src/v2/components/chat/__tests__/MCPAppsProxy.e2e.test.tsx +0 -589
  182. package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +0 -403
  183. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -137
  184. package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +0 -37
  185. package/src/v2/components/chat/__tests__/setup.ts +0 -1
  186. package/src/v2/components/chat/index.ts +0 -90
  187. package/src/v2/components/chat/last-user-message-context.ts +0 -21
  188. package/src/v2/components/chat/normalize-auto-scroll.ts +0 -17
  189. package/src/v2/components/chat/scroll-element-context.ts +0 -13
  190. package/src/v2/components/index.ts +0 -8
  191. package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -286
  192. package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -464
  193. package/src/v2/components/intelligence-indicator/index.ts +0 -2
  194. package/src/v2/components/license-warning-banner.tsx +0 -217
  195. package/src/v2/components/ui/button.tsx +0 -124
  196. package/src/v2/components/ui/dropdown-menu.tsx +0 -258
  197. package/src/v2/components/ui/tooltip.tsx +0 -60
  198. package/src/v2/context.ts +0 -62
  199. package/src/v2/headless.ts +0 -64
  200. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +0 -152
  201. package/src/v2/hooks/__tests__/standard-schema.test.tsx +0 -282
  202. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +0 -140
  203. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +0 -401
  204. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +0 -44
  205. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +0 -211
  206. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +0 -1029
  207. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +0 -159
  208. package/src/v2/hooks/__tests__/use-attachments.test.tsx +0 -169
  209. package/src/v2/hooks/__tests__/use-capabilities.test.tsx +0 -76
  210. package/src/v2/hooks/__tests__/use-component.test.tsx +0 -126
  211. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +0 -696
  212. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +0 -153
  213. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -167
  214. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +0 -2148
  215. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +0 -1261
  216. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +0 -397
  217. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +0 -56
  218. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +0 -192
  219. package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +0 -219
  220. package/src/v2/hooks/__tests__/use-render-custom-messages.test.tsx +0 -55
  221. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +0 -259
  222. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +0 -524
  223. package/src/v2/hooks/__tests__/use-threads.test.tsx +0 -757
  224. package/src/v2/hooks/__tests__/zod-regression.test.tsx +0 -311
  225. package/src/v2/hooks/index.ts +0 -24
  226. package/src/v2/hooks/use-agent-context.tsx +0 -45
  227. package/src/v2/hooks/use-agent.tsx +0 -227
  228. package/src/v2/hooks/use-attachments.tsx +0 -269
  229. package/src/v2/hooks/use-capabilities.tsx +0 -25
  230. package/src/v2/hooks/use-component.tsx +0 -91
  231. package/src/v2/hooks/use-configure-suggestions.tsx +0 -236
  232. package/src/v2/hooks/use-default-render-tool.tsx +0 -271
  233. package/src/v2/hooks/use-frontend-tool.tsx +0 -46
  234. package/src/v2/hooks/use-human-in-the-loop.tsx +0 -81
  235. package/src/v2/hooks/use-interrupt.tsx +0 -305
  236. package/src/v2/hooks/use-keyboard-height.tsx +0 -67
  237. package/src/v2/hooks/use-pin-to-send.ts +0 -94
  238. package/src/v2/hooks/use-render-activity-message.tsx +0 -72
  239. package/src/v2/hooks/use-render-custom-messages.tsx +0 -93
  240. package/src/v2/hooks/use-render-tool-call.tsx +0 -208
  241. package/src/v2/hooks/use-render-tool.tsx +0 -184
  242. package/src/v2/hooks/use-suggestions.tsx +0 -91
  243. package/src/v2/hooks/use-threads.tsx +0 -325
  244. package/src/v2/hooks/useKatexStyles.ts +0 -27
  245. package/src/v2/index.css +0 -1
  246. package/src/v2/index.ts +0 -27
  247. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +0 -495
  248. package/src/v2/lib/__tests__/processPartialHtml.test.ts +0 -112
  249. package/src/v2/lib/__tests__/renderSlot.test.tsx +0 -588
  250. package/src/v2/lib/__tests__/slots.test.ts +0 -56
  251. package/src/v2/lib/processPartialHtml.ts +0 -45
  252. package/src/v2/lib/react-core.ts +0 -156
  253. package/src/v2/lib/slots.tsx +0 -184
  254. package/src/v2/lib/transcription-client.ts +0 -184
  255. package/src/v2/lib/utils.ts +0 -8
  256. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +0 -196
  257. package/src/v2/providers/CopilotKitProvider.tsx +0 -800
  258. package/src/v2/providers/SandboxFunctionsContext.ts +0 -10
  259. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +0 -652
  260. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +0 -101
  261. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +0 -69
  262. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +0 -881
  263. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +0 -198
  264. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +0 -740
  265. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +0 -713
  266. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +0 -294
  267. package/src/v2/providers/index.ts +0 -21
  268. package/src/v2/styles/globals.css +0 -349
  269. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +0 -525
  270. package/src/v2/types/defineToolCallRenderer.ts +0 -68
  271. package/src/v2/types/frontend-tool.ts +0 -8
  272. package/src/v2/types/human-in-the-loop.ts +0 -33
  273. package/src/v2/types/index.ts +0 -8
  274. package/src/v2/types/interrupt.ts +0 -15
  275. package/src/v2/types/react-activity-message-renderer.ts +0 -27
  276. package/src/v2/types/react-custom-message-renderer.ts +0 -17
  277. package/src/v2/types/react-tool-call-renderer.ts +0 -35
  278. package/src/v2/types/sandbox-function.ts +0 -11
  279. package/tsconfig.json +0 -8
  280. package/tsdown.config.ts +0 -193
  281. package/typedoc.json +0 -4
  282. 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
- });