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