diff --git a/apps/sim/app/api/copilot/credentials/route.ts b/apps/sim/app/api/copilot/credentials/route.ts index acc99958f..2f764429d 100644 --- a/apps/sim/app/api/copilot/credentials/route.ts +++ b/apps/sim/app/api/copilot/credentials/route.ts @@ -18,9 +18,11 @@ export async function GET(_req: NextRequest) { return NextResponse.json({ success: true, result }) } catch (error) { return NextResponse.json( - { success: false, error: error instanceof Error ? error.message : 'Failed to load credentials' }, + { + success: false, + error: error instanceof Error ? error.message : 'Failed to load credentials', + }, { status: 500 } ) } } - diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 6f36be94d..9f8a0cf13 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -355,13 +355,23 @@ async function handleBuildToolCall( const { model } = getCopilotModel('chat') const workflowId = args.workflowId as string | undefined - const resolved = workflowId - ? { workflowId } - : await resolveWorkflowIdForUser(userId) + const resolved = workflowId ? { workflowId } : await resolveWorkflowIdForUser(userId) if (!resolved?.workflowId) { const response: CallToolResult = { - content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'workflowId is required for build. Call create_workflow first.' }, null, 2) }], + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: false, + error: 'workflowId is required for build. Call create_workflow first.', + }, + null, + 2 + ), + }, + ], isError: true, } return NextResponse.json(createResponse(id, response)) @@ -410,10 +420,9 @@ async function handleBuildToolCall( return NextResponse.json(createResponse(id, response)) } catch (error) { logger.error('Build tool call failed', { error }) - return NextResponse.json( - createError(id, ErrorCode.InternalError, `Build failed: ${error}`), - { status: 500 } - ) + return NextResponse.json(createError(id, ErrorCode.InternalError, `Build failed: ${error}`), { + status: 500, + }) } } diff --git a/apps/sim/components/ui/tool-call.tsx b/apps/sim/components/ui/tool-call.tsx index 0d7d2ece2..bc523894f 100644 --- a/apps/sim/components/ui/tool-call.tsx +++ b/apps/sim/components/ui/tool-call.tsx @@ -5,10 +5,43 @@ import { CheckCircle, ChevronDown, ChevronRight, Loader2, Settings, XCircle } fr import { Badge } from '@/components/emcn' import { Button } from '@/components/ui/button' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible' -import type { ToolCallGroup, ToolCallState } from '@/lib/copilot/types' import { cn } from '@/lib/core/utils/cn' import { formatDuration } from '@/lib/core/utils/formatting' +interface ToolCallState { + id: string + name: string + displayName?: string + parameters?: Record + state: + | 'detecting' + | 'pending' + | 'executing' + | 'completed' + | 'error' + | 'rejected' + | 'applied' + | 'ready_for_review' + | 'aborted' + | 'skipped' + | 'background' + startTime?: number + endTime?: number + duration?: number + result?: unknown + error?: string + progress?: string +} + +interface ToolCallGroup { + id: string + toolCalls: ToolCallState[] + status: 'pending' | 'in_progress' | 'completed' | 'error' + startTime?: number + endTime?: number + summary?: string +} + interface ToolCallProps { toolCall: ToolCallState isCompact?: boolean diff --git a/apps/sim/lib/copilot/constants.ts b/apps/sim/lib/copilot/constants.ts index 7a45127eb..21e29cdbc 100644 --- a/apps/sim/lib/copilot/constants.ts +++ b/apps/sim/lib/copilot/constants.ts @@ -4,4 +4,8 @@ export const SIM_AGENT_API_URL_DEFAULT = 'https://copilot.sim.ai' export const SIM_AGENT_VERSION = '1.0.3' /** Resolved copilot backend URL — reads from env with fallback to default. */ -export const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT +const rawAgentUrl = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT +export const SIM_AGENT_API_URL = + rawAgentUrl.startsWith('http://') || rawAgentUrl.startsWith('https://') + ? rawAgentUrl + : SIM_AGENT_API_URL_DEFAULT diff --git a/apps/sim/lib/copilot/orchestrator/index.ts b/apps/sim/lib/copilot/orchestrator/index.ts index 1f3a54ee9..a909fa294 100644 --- a/apps/sim/lib/copilot/orchestrator/index.ts +++ b/apps/sim/lib/copilot/orchestrator/index.ts @@ -1,21 +1,10 @@ import { createLogger } from '@sim/logger' import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' -import { handleSubagentRouting, sseHandlers, subAgentHandlers } from '@/lib/copilot/orchestrator/sse-handlers' -import { env } from '@/lib/core/config/env' -import { - normalizeSseEvent, - shouldSkipToolCallEvent, - shouldSkipToolResultEvent, -} from '@/lib/copilot/orchestrator/sse-utils' -import { parseSSEStream } from '@/lib/copilot/orchestrator/sse-parser' import { prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor' -import type { - OrchestratorOptions, - OrchestratorResult, - SSEEvent, - StreamingContext, - ToolCallSummary, -} from '@/lib/copilot/orchestrator/types' +import type { OrchestratorOptions, OrchestratorResult } from '@/lib/copilot/orchestrator/types' +import { env } from '@/lib/core/config/env' +import { buildToolCallSummaries, createStreamingContext, runStreamLoop } from './stream-core' + const logger = createLogger('CopilotOrchestrator') export interface OrchestrateStreamOptions extends OrchestratorOptions { @@ -24,118 +13,43 @@ export interface OrchestrateStreamOptions extends OrchestratorOptions { chatId?: string } -/** - * Orchestrate a copilot SSE stream and execute tool calls server-side. - */ export async function orchestrateCopilotStream( requestPayload: Record, options: OrchestrateStreamOptions ): Promise { - const { userId, workflowId, chatId, timeout = 300000, abortSignal } = options + const { userId, workflowId, chatId } = options const execContext = await prepareExecutionContext(userId, workflowId) - const context: StreamingContext = { + const context = createStreamingContext({ chatId, - conversationId: undefined, messageId: requestPayload?.messageId || crypto.randomUUID(), - accumulatedContent: '', - contentBlocks: [], - toolCalls: new Map(), - currentThinkingBlock: null, - isInThinkingBlock: false, - subAgentParentToolCallId: undefined, - subAgentContent: {}, - subAgentToolCalls: {}, - pendingContent: '', - streamComplete: false, - wasAborted: false, - errors: [], - } + }) try { - const response = await fetch(`${SIM_AGENT_API_URL}/api/chat-completion-streaming`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), + await runStreamLoop( + `${SIM_AGENT_API_URL}/api/chat-completion-streaming`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), + }, + body: JSON.stringify(requestPayload), }, - body: JSON.stringify(requestPayload), - signal: abortSignal, - }) + context, + execContext, + options + ) - if (!response.ok) { - const errorText = await response.text().catch(() => '') - throw new Error( - `Copilot backend error (${response.status}): ${errorText || response.statusText}` - ) + const result: OrchestratorResult = { + success: context.errors.length === 0, + content: context.accumulatedContent, + contentBlocks: context.contentBlocks, + toolCalls: buildToolCallSummaries(context), + chatId: context.chatId, + conversationId: context.conversationId, + errors: context.errors.length ? context.errors : undefined, } - - if (!response.body) { - throw new Error('Copilot backend response missing body') - } - - const reader = response.body.getReader() - const decoder = new TextDecoder() - - const timeoutId = setTimeout(() => { - context.errors.push('Request timed out') - context.streamComplete = true - reader.cancel().catch(() => {}) - }, timeout) - - try { - for await (const event of parseSSEStream(reader, decoder, abortSignal)) { - if (abortSignal?.aborted) { - context.wasAborted = true - break - } - - const normalizedEvent = normalizeSseEvent(event) - - // Skip duplicate tool events to prevent state regressions. - const shouldSkipToolCall = shouldSkipToolCallEvent(normalizedEvent) - const shouldSkipToolResult = shouldSkipToolResultEvent(normalizedEvent) - - if (!shouldSkipToolCall && !shouldSkipToolResult) { - await forwardEvent(normalizedEvent, options) - } - - if (normalizedEvent.type === 'subagent_start') { - const eventData = normalizedEvent.data as Record | undefined - const toolCallId = eventData?.tool_call_id as string | undefined - if (toolCallId) { - context.subAgentParentToolCallId = toolCallId - context.subAgentContent[toolCallId] = '' - context.subAgentToolCalls[toolCallId] = [] - } - continue - } - - if (normalizedEvent.type === 'subagent_end') { - context.subAgentParentToolCallId = undefined - continue - } - - if (handleSubagentRouting(normalizedEvent, context)) { - const handler = subAgentHandlers[normalizedEvent.type] - if (handler) { - await handler(normalizedEvent, context, execContext, options) - } - if (context.streamComplete) break - continue - } - - const handler = sseHandlers[normalizedEvent.type] - if (handler) { - await handler(normalizedEvent, context, execContext, options) - } - if (context.streamComplete) break - } - } finally { - clearTimeout(timeoutId) - } - - const result = buildResult(context) await options.onComplete?.(result) return result } catch (error) { @@ -153,37 +67,3 @@ export async function orchestrateCopilotStream( } } } - -async function forwardEvent(event: SSEEvent, options: OrchestratorOptions): Promise { - try { - await options.onEvent?.(event) - } catch (error) { - logger.warn('Failed to forward SSE event', { - type: event.type, - error: error instanceof Error ? error.message : String(error), - }) - } -} - -function buildResult(context: StreamingContext): OrchestratorResult { - const toolCalls: ToolCallSummary[] = Array.from(context.toolCalls.values()).map((toolCall) => ({ - id: toolCall.id, - name: toolCall.name, - status: toolCall.status, - params: toolCall.params, - result: toolCall.result?.output, - error: toolCall.error, - durationMs: - toolCall.endTime && toolCall.startTime ? toolCall.endTime - toolCall.startTime : undefined, - })) - - return { - success: context.errors.length === 0, - content: context.accumulatedContent, - contentBlocks: context.contentBlocks, - toolCalls, - chatId: context.chatId, - conversationId: context.conversationId, - errors: context.errors.length ? context.errors : undefined, - } -} diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers.test.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers.test.ts index f9368ec69..cc2586b2c 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers.test.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers.test.ts @@ -1,8 +1,9 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' + import { loggerMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@sim/logger', () => loggerMock) @@ -92,4 +93,3 @@ describe('sse-handlers tool lifecycle', () => { expect(markToolComplete).toHaveBeenCalledTimes(1) }) }) - diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts similarity index 79% rename from apps/sim/lib/copilot/orchestrator/sse-handlers.ts rename to apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts index 597de39b4..abbd2c32c 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts @@ -1,17 +1,12 @@ import { createLogger } from '@sim/logger' -import { - INTERRUPT_TOOL_SET, - RESPOND_TOOL_SET, - SUBAGENT_TOOL_SET, -} from '@/lib/copilot/orchestrator/config' -import { getToolConfirmation } from '@/lib/copilot/orchestrator/persistence' +import { RESPOND_TOOL_SET, SUBAGENT_TOOL_SET } from '@/lib/copilot/orchestrator/config' import { asRecord, getEventData, markToolResultSeen, wasToolResultSeen, } from '@/lib/copilot/orchestrator/sse-utils' -import { executeToolServerSide, markToolComplete } from '@/lib/copilot/orchestrator/tool-executor' +import { markToolComplete } from '@/lib/copilot/orchestrator/tool-executor' import type { ContentBlock, ExecutionContext, @@ -20,6 +15,7 @@ import type { StreamingContext, ToolCallState, } from '@/lib/copilot/orchestrator/types' +import { executeToolAndReport, isInterruptToolName, waitForToolDecision } from './tool-execution' const logger = createLogger('CopilotSseHandlers') @@ -39,100 +35,6 @@ function addContentBlock(context: StreamingContext, block: Omit { - const toolCall = context.toolCalls.get(toolCallId) - if (!toolCall) return - - if (toolCall.status === 'executing') return - if (wasToolResultSeen(toolCall.id)) return - - toolCall.status = 'executing' - try { - const result = await executeToolServerSide(toolCall, execContext) - toolCall.status = result.success ? 'success' : 'error' - toolCall.result = result - toolCall.error = result.error - toolCall.endTime = Date.now() - - // If create_workflow was successful, update the execution context with the new workflowId - // This ensures subsequent tools in the same stream have access to the workflowId - const output = asRecord(result.output) - if ( - toolCall.name === 'create_workflow' && - result.success && - output.workflowId && - !execContext.workflowId - ) { - execContext.workflowId = output.workflowId as string - if (output.workspaceId) { - execContext.workspaceId = output.workspaceId as string - } - } - - markToolResultSeen(toolCall.id) - - await markToolComplete( - toolCall.id, - toolCall.name, - result.success ? 200 : 500, - result.error || (result.success ? 'Tool completed' : 'Tool failed'), - result.output - ) - - await options?.onEvent?.({ - type: 'tool_result', - toolCallId: toolCall.id, - toolName: toolCall.name, - success: result.success, - result: result.output, - data: { - id: toolCall.id, - name: toolCall.name, - success: result.success, - result: result.output, - }, - }) - } catch (error) { - toolCall.status = 'error' - toolCall.error = error instanceof Error ? error.message : String(error) - toolCall.endTime = Date.now() - - markToolResultSeen(toolCall.id) - - await markToolComplete(toolCall.id, toolCall.name, 500, toolCall.error) - - await options?.onEvent?.({ - type: 'tool_error', - toolCallId: toolCall.id, - data: { - id: toolCall.id, - name: toolCall.name, - error: toolCall.error, - }, - }) - } -} - -async function waitForToolDecision( - toolCallId: string, - timeoutMs: number -): Promise<{ status: string; message?: string } | null> { - const start = Date.now() - while (Date.now() - start < timeoutMs) { - const decision = await getToolConfirmation(toolCallId) - if (decision?.status) { - return decision - } - await new Promise((resolve) => setTimeout(resolve, 100)) - } - return null -} - export const sseHandlers: Record = { chat_id: (event, context) => { context.chatId = asRecord(event.data).chatId @@ -145,13 +47,13 @@ export const sseHandlers: Record = { const current = context.toolCalls.get(toolCallId) if (!current) return - // Determine success: explicit success field, or if there's result data without explicit failure + // Determine success: explicit success field, or if there's result data without explicit failure. const hasExplicitSuccess = data?.success !== undefined || data?.result?.success !== undefined const explicitSuccess = data?.success ?? data?.result?.success const hasResultData = data?.result !== undefined || data?.data !== undefined const hasError = !!data?.error || !!data?.result?.error - // If explicitly set, use that; otherwise infer from data presence + // If explicitly set, use that; otherwise infer from data presence. const success = hasExplicitSuccess ? !!explicitSuccess : hasResultData && !hasError current.status = success ? 'success' : 'error' @@ -232,13 +134,13 @@ export const sseHandlers: Record = { const toolCall = context.toolCalls.get(toolCallId) if (!toolCall) return - // Subagent tools are executed by the copilot backend, not sim side + // Subagent tools are executed by the copilot backend, not sim side. if (SUBAGENT_TOOL_SET.has(toolName)) { return } - // Respond tools are internal to copilot's subagent system - skip execution - // The copilot backend handles these internally to signal subagent completion + // Respond tools are internal to copilot's subagent system - skip execution. + // The copilot backend handles these internally to signal subagent completion. if (RESPOND_TOOL_SET.has(toolName)) { toolCall.status = 'success' toolCall.endTime = Date.now() @@ -249,7 +151,7 @@ export const sseHandlers: Record = { return } - const isInterruptTool = INTERRUPT_TOOL_SET.has(toolName) + const isInterruptTool = isInterruptToolName(toolName) const isInteractive = options.interactive === true if (isInterruptTool && isInteractive) { @@ -358,8 +260,7 @@ export const sseHandlers: Record = { }, error: (event, context) => { const d = asRecord(event.data) - const message = - d.message || d.error || (typeof event.data === 'string' ? event.data : null) + const message = d.message || d.error || (typeof event.data === 'string' ? event.data : null) if (message) { context.errors.push(message) } @@ -388,7 +289,7 @@ export const subAgentHandlers: Record = { const args = toolData.arguments || toolData.input || asRecord(event.data).input const existing = context.toolCalls.get(toolCallId) - // Ignore late/duplicate tool_call events once we already have a result + // Ignore late/duplicate tool_call events once we already have a result. if (wasToolResultSeen(toolCallId) || existing?.endTime) { return } @@ -401,7 +302,7 @@ export const subAgentHandlers: Record = { startTime: Date.now(), } - // Store in both places - but do NOT overwrite existing tool call state for the same id + // Store in both places - but do NOT overwrite existing tool call state for the same id. if (!context.subAgentToolCalls[parentToolCallId]) { context.subAgentToolCalls[parentToolCallId] = [] } @@ -414,7 +315,7 @@ export const subAgentHandlers: Record = { if (isPartial) return - // Respond tools are internal to copilot's subagent system - skip execution + // Respond tools are internal to copilot's subagent system - skip execution. if (RESPOND_TOOL_SET.has(toolName)) { toolCall.status = 'success' toolCall.endTime = Date.now() @@ -436,14 +337,14 @@ export const subAgentHandlers: Record = { const toolCallId = event.toolCallId || data?.id if (!toolCallId) return - // Update in subAgentToolCalls + // Update in subAgentToolCalls. const toolCalls = context.subAgentToolCalls[parentToolCallId] || [] const subAgentToolCall = toolCalls.find((tc) => tc.id === toolCallId) - // Also update in main toolCalls (where we added it for execution) + // Also update in main toolCalls (where we added it for execution). const mainToolCall = context.toolCalls.get(toolCallId) - // Use same success inference logic as main handler + // Use same success inference logic as main handler. const hasExplicitSuccess = data?.success !== undefined || data?.result?.success !== undefined const explicitSuccess = data?.success ?? data?.result?.success const hasResultData = data?.result !== undefined || data?.data !== undefined diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/index.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/index.ts new file mode 100644 index 000000000..d0d6b14b5 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/index.ts @@ -0,0 +1,2 @@ +export type { SSEHandler } from './handlers' +export { handleSubagentRouting, sseHandlers, subAgentHandlers } from './handlers' diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts new file mode 100644 index 000000000..99eb593e5 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts @@ -0,0 +1,117 @@ +import { createLogger } from '@sim/logger' +import { INTERRUPT_TOOL_SET } from '@/lib/copilot/orchestrator/config' +import { getToolConfirmation } from '@/lib/copilot/orchestrator/persistence' +import { + asRecord, + markToolResultSeen, + wasToolResultSeen, +} from '@/lib/copilot/orchestrator/sse-utils' +import { executeToolServerSide, markToolComplete } from '@/lib/copilot/orchestrator/tool-executor' +import type { + ExecutionContext, + OrchestratorOptions, + SSEEvent, + StreamingContext, +} from '@/lib/copilot/orchestrator/types' + +const logger = createLogger('CopilotSseToolExecution') + +export function isInterruptToolName(toolName: string): boolean { + return INTERRUPT_TOOL_SET.has(toolName) +} + +export async function executeToolAndReport( + toolCallId: string, + context: StreamingContext, + execContext: ExecutionContext, + options?: OrchestratorOptions +): Promise { + const toolCall = context.toolCalls.get(toolCallId) + if (!toolCall) return + + if (toolCall.status === 'executing') return + if (wasToolResultSeen(toolCall.id)) return + + toolCall.status = 'executing' + try { + const result = await executeToolServerSide(toolCall, execContext) + toolCall.status = result.success ? 'success' : 'error' + toolCall.result = result + toolCall.error = result.error + toolCall.endTime = Date.now() + + // If create_workflow was successful, update the execution context with the new workflowId. + // This ensures subsequent tools in the same stream have access to the workflowId. + const output = asRecord(result.output) + if ( + toolCall.name === 'create_workflow' && + result.success && + output.workflowId && + !execContext.workflowId + ) { + execContext.workflowId = output.workflowId as string + if (output.workspaceId) { + execContext.workspaceId = output.workspaceId as string + } + } + + markToolResultSeen(toolCall.id) + + await markToolComplete( + toolCall.id, + toolCall.name, + result.success ? 200 : 500, + result.error || (result.success ? 'Tool completed' : 'Tool failed'), + result.output + ) + + const resultEvent: SSEEvent = { + type: 'tool_result', + toolCallId: toolCall.id, + toolName: toolCall.name, + success: result.success, + result: result.output, + data: { + id: toolCall.id, + name: toolCall.name, + success: result.success, + result: result.output, + }, + } + await options?.onEvent?.(resultEvent) + } catch (error) { + toolCall.status = 'error' + toolCall.error = error instanceof Error ? error.message : String(error) + toolCall.endTime = Date.now() + + markToolResultSeen(toolCall.id) + + await markToolComplete(toolCall.id, toolCall.name, 500, toolCall.error) + + const errorEvent: SSEEvent = { + type: 'tool_error', + toolCallId: toolCall.id, + data: { + id: toolCall.id, + name: toolCall.name, + error: toolCall.error, + }, + } + await options?.onEvent?.(errorEvent) + } +} + +export async function waitForToolDecision( + toolCallId: string, + timeoutMs: number +): Promise<{ status: string; message?: string } | null> { + const start = Date.now() + while (Date.now() - start < timeoutMs) { + const decision = await getToolConfirmation(toolCallId) + if (decision?.status) { + return decision + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + return null +} diff --git a/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts b/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts index 37b748a7f..ce41e3270 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts @@ -40,4 +40,3 @@ describe('sse-utils', () => { expect(shouldSkipToolResultEvent(event as any)).toBe(true) }) }) - diff --git a/apps/sim/lib/copilot/orchestrator/sse-utils.ts b/apps/sim/lib/copilot/orchestrator/sse-utils.ts index 0dd805dec..26d5a94bd 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-utils.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-utils.ts @@ -120,4 +120,3 @@ export function shouldSkipToolResultEvent(event: SSEEvent): boolean { markToolResultSeen(toolCallId) return false } - diff --git a/apps/sim/lib/copilot/orchestrator/stream-buffer.test.ts b/apps/sim/lib/copilot/orchestrator/stream-buffer.test.ts index 6e834c629..94458e452 100644 --- a/apps/sim/lib/copilot/orchestrator/stream-buffer.test.ts +++ b/apps/sim/lib/copilot/orchestrator/stream-buffer.test.ts @@ -1,8 +1,9 @@ /** * @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from 'vitest' + import { loggerMock } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@sim/logger', () => loggerMock) @@ -25,27 +26,29 @@ const createRedisStub = () => { hset: vi.fn().mockResolvedValue(1), hgetall: vi.fn().mockResolvedValue({}), expire: vi.fn().mockResolvedValue(1), - eval: vi.fn().mockImplementation( - ( - _lua: string, - _keysCount: number, - seqKey: string, - eventsKey: string, - _ttl: number, - _limit: number, - streamId: string, - eventJson: string - ) => { - const current = counters.get(seqKey) || 0 - const next = current + 1 - counters.set(seqKey, next) - const entry = JSON.stringify({ eventId: next, streamId, event: JSON.parse(eventJson) }) - const list = events.get(eventsKey) || [] - list.push({ score: next, value: entry }) - events.set(eventsKey, list) - return next - } - ), + eval: vi + .fn() + .mockImplementation( + ( + _lua: string, + _keysCount: number, + seqKey: string, + eventsKey: string, + _ttl: number, + _limit: number, + streamId: string, + eventJson: string + ) => { + const current = counters.get(seqKey) || 0 + const next = current + 1 + counters.set(seqKey, next) + const entry = JSON.stringify({ eventId: next, streamId, event: JSON.parse(eventJson) }) + const list = events.get(eventsKey) || [] + list.push({ score: next, value: entry }) + events.set(eventsKey, list) + return next + } + ), incrby: vi.fn().mockImplementation((key: string, amount: number) => { const current = counters.get(key) || 0 const next = current + amount @@ -58,19 +61,18 @@ const createRedisStub = () => { return Promise.resolve(readEntries(key, minVal, maxVal)) }), pipeline: vi.fn().mockImplementation(() => { - const api = { - zadd: vi.fn().mockImplementation((key: string, ...args: Array) => { - const list = events.get(key) || [] - for (let i = 0; i < args.length; i += 2) { - list.push({ score: Number(args[i]), value: String(args[i + 1]) }) - } - events.set(key, list) - return api - }), - expire: vi.fn().mockReturnValue(api), - zremrangebyrank: vi.fn().mockReturnValue(api), - exec: vi.fn().mockResolvedValue([]), - } + const api: Record = {} + api.zadd = vi.fn().mockImplementation((key: string, ...args: Array) => { + const list = events.get(key) || [] + for (let i = 0; i < args.length; i += 2) { + list.push({ score: Number(args[i]), value: String(args[i + 1]) }) + } + events.set(key, list) + return api + }) + api.expire = vi.fn().mockReturnValue(api) + api.zremrangebyrank = vi.fn().mockReturnValue(api) + api.exec = vi.fn().mockResolvedValue([]) return api }), } @@ -115,4 +117,3 @@ describe('stream-buffer', () => { expect(events.map((entry) => entry.event.data)).toEqual(['a', 'b']) }) }) - diff --git a/apps/sim/lib/copilot/orchestrator/stream-buffer.ts b/apps/sim/lib/copilot/orchestrator/stream-buffer.ts index 29fd8f55b..abf70aa2c 100644 --- a/apps/sim/lib/copilot/orchestrator/stream-buffer.ts +++ b/apps/sim/lib/copilot/orchestrator/stream-buffer.ts @@ -31,7 +31,10 @@ export function getStreamBufferConfig(): StreamBufferConfig { ttlSeconds: parseNumber(env.COPILOT_STREAM_TTL_SECONDS, STREAM_DEFAULTS.ttlSeconds), eventLimit: parseNumber(env.COPILOT_STREAM_EVENT_LIMIT, STREAM_DEFAULTS.eventLimit), reserveBatch: parseNumber(env.COPILOT_STREAM_RESERVE_BATCH, STREAM_DEFAULTS.reserveBatch), - flushIntervalMs: parseNumber(env.COPILOT_STREAM_FLUSH_INTERVAL_MS, STREAM_DEFAULTS.flushIntervalMs), + flushIntervalMs: parseNumber( + env.COPILOT_STREAM_FLUSH_INTERVAL_MS, + STREAM_DEFAULTS.flushIntervalMs + ), flushMaxBatch: parseNumber(env.COPILOT_STREAM_FLUSH_MAX_BATCH, STREAM_DEFAULTS.flushMaxBatch), } } @@ -190,8 +193,6 @@ export function createStreamEventWriter(streamId: string): StreamEventWriter { let nextEventId = 0 let maxReservedId = 0 let flushTimer: ReturnType | null = null - let isFlushing = false - const scheduleFlush = () => { if (flushTimer) return flushTimer = setTimeout(() => { @@ -210,9 +211,11 @@ export function createStreamEventWriter(streamId: string): StreamEventWriter { } } - const flush = async () => { - if (isFlushing || pending.length === 0) return - isFlushing = true + let flushPromise: Promise | null = null + let closed = false + + const doFlush = async () => { + if (pending.length === 0) return const batch = pending pending = [] try { @@ -233,13 +236,25 @@ export function createStreamEventWriter(streamId: string): StreamEventWriter { error: error instanceof Error ? error.message : String(error), }) pending = batch.concat(pending) + } + } + + const flush = async () => { + if (flushPromise) { + await flushPromise + return + } + flushPromise = doFlush() + try { + await flushPromise } finally { - isFlushing = false + flushPromise = null if (pending.length > 0) scheduleFlush() } } const write = async (event: Record) => { + if (closed) return { eventId: 0, streamId, event } if (nextEventId === 0 || nextEventId > maxReservedId) { await reserveIds(1) } @@ -255,6 +270,7 @@ export function createStreamEventWriter(streamId: string): StreamEventWriter { } const close = async () => { + closed = true if (flushTimer) { clearTimeout(flushTimer) flushTimer = null diff --git a/apps/sim/lib/copilot/orchestrator/stream-core.ts b/apps/sim/lib/copilot/orchestrator/stream-core.ts new file mode 100644 index 000000000..5f5af90b4 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/stream-core.ts @@ -0,0 +1,179 @@ +import { createLogger } from '@sim/logger' +import { + handleSubagentRouting, + sseHandlers, + subAgentHandlers, +} from '@/lib/copilot/orchestrator/sse-handlers' +import { parseSSEStream } from '@/lib/copilot/orchestrator/sse-parser' +import { + normalizeSseEvent, + shouldSkipToolCallEvent, + shouldSkipToolResultEvent, +} from '@/lib/copilot/orchestrator/sse-utils' +import type { + ExecutionContext, + OrchestratorOptions, + SSEEvent, + StreamingContext, + ToolCallSummary, +} from '@/lib/copilot/orchestrator/types' + +const logger = createLogger('CopilotStreamCore') + +/** + * Options for the shared stream processing loop. + */ +export interface StreamLoopOptions extends OrchestratorOptions { + /** + * Called for each normalized event BEFORE standard handler dispatch. + * Return true to skip the default handler for this event. + */ + onBeforeDispatch?: (event: SSEEvent, context: StreamingContext) => boolean | void +} + +/** + * Create a fresh StreamingContext. + */ +export function createStreamingContext(overrides?: Partial): StreamingContext { + return { + chatId: undefined, + conversationId: undefined, + messageId: crypto.randomUUID(), + accumulatedContent: '', + contentBlocks: [], + toolCalls: new Map(), + currentThinkingBlock: null, + isInThinkingBlock: false, + subAgentParentToolCallId: undefined, + subAgentContent: {}, + subAgentToolCalls: {}, + pendingContent: '', + streamComplete: false, + wasAborted: false, + errors: [], + ...overrides, + } +} + +/** + * Run the SSE stream processing loop. + * + * Handles: fetch -> parse -> normalize -> dedupe -> subagent routing -> handler dispatch. + * Callers provide the fetch URL/options and can intercept events via onBeforeDispatch. + */ +export async function runStreamLoop( + fetchUrl: string, + fetchOptions: RequestInit, + context: StreamingContext, + execContext: ExecutionContext, + options: StreamLoopOptions +): Promise { + const { timeout = 300000, abortSignal } = options + + const response = await fetch(fetchUrl, { + ...fetchOptions, + signal: abortSignal, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + throw new Error(`Copilot backend error (${response.status}): ${errorText || response.statusText}`) + } + + if (!response.body) { + throw new Error('Copilot backend response missing body') + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + + const timeoutId = setTimeout(() => { + context.errors.push('Request timed out') + context.streamComplete = true + reader.cancel().catch(() => {}) + }, timeout) + + try { + for await (const event of parseSSEStream(reader, decoder, abortSignal)) { + if (abortSignal?.aborted) { + context.wasAborted = true + break + } + + const normalizedEvent = normalizeSseEvent(event) + + // Skip duplicate tool events. + const shouldSkipToolCall = shouldSkipToolCallEvent(normalizedEvent) + const shouldSkipToolResult = shouldSkipToolResultEvent(normalizedEvent) + + if (!shouldSkipToolCall && !shouldSkipToolResult) { + try { + await options.onEvent?.(normalizedEvent) + } catch (error) { + logger.warn('Failed to forward SSE event', { + type: normalizedEvent.type, + error: error instanceof Error ? error.message : String(error), + }) + } + } + + // Let the caller intercept before standard dispatch. + if (options.onBeforeDispatch?.(normalizedEvent, context)) { + if (context.streamComplete) break + continue + } + + // Standard subagent start/end handling. + if (normalizedEvent.type === 'subagent_start') { + const eventData = normalizedEvent.data as Record | undefined + const toolCallId = eventData?.tool_call_id as string | undefined + if (toolCallId) { + context.subAgentParentToolCallId = toolCallId + context.subAgentContent[toolCallId] = '' + context.subAgentToolCalls[toolCallId] = [] + } + continue + } + + if (normalizedEvent.type === 'subagent_end') { + context.subAgentParentToolCallId = undefined + continue + } + + // Subagent event routing. + if (handleSubagentRouting(normalizedEvent, context)) { + const handler = subAgentHandlers[normalizedEvent.type] + if (handler) { + await handler(normalizedEvent, context, execContext, options) + } + if (context.streamComplete) break + continue + } + + // Main event handler dispatch. + const handler = sseHandlers[normalizedEvent.type] + if (handler) { + await handler(normalizedEvent, context, execContext, options) + } + if (context.streamComplete) break + } + } finally { + clearTimeout(timeoutId) + } +} + +/** + * Build a ToolCallSummary array from the streaming context. + */ +export function buildToolCallSummaries(context: StreamingContext): ToolCallSummary[] { + return Array.from(context.toolCalls.values()).map((toolCall) => ({ + id: toolCall.id, + name: toolCall.name, + status: toolCall.status, + params: toolCall.params, + result: toolCall.result?.output, + error: toolCall.error, + durationMs: + toolCall.endTime && toolCall.startTime ? toolCall.endTime - toolCall.startTime : undefined, + })) +} diff --git a/apps/sim/lib/copilot/orchestrator/subagent.ts b/apps/sim/lib/copilot/orchestrator/subagent.ts index 80e71d672..9788a686a 100644 --- a/apps/sim/lib/copilot/orchestrator/subagent.ts +++ b/apps/sim/lib/copilot/orchestrator/subagent.ts @@ -1,13 +1,5 @@ import { createLogger } from '@sim/logger' import { SIM_AGENT_API_URL } from '@/lib/copilot/constants' -import { handleSubagentRouting, sseHandlers, subAgentHandlers } from '@/lib/copilot/orchestrator/sse-handlers' -import { env } from '@/lib/core/config/env' -import { - normalizeSseEvent, - shouldSkipToolCallEvent, - shouldSkipToolResultEvent, -} from '@/lib/copilot/orchestrator/sse-utils' -import { parseSSEStream } from '@/lib/copilot/orchestrator/sse-parser' import { prepareExecutionContext } from '@/lib/copilot/orchestrator/tool-executor' import type { ExecutionContext, @@ -16,7 +8,9 @@ import type { StreamingContext, ToolCallSummary, } from '@/lib/copilot/orchestrator/types' +import { env } from '@/lib/core/config/env' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' +import { buildToolCallSummaries, createStreamingContext, runStreamLoop } from './stream-core' const logger = createLogger('CopilotSubagentOrchestrator') @@ -46,131 +40,58 @@ export async function orchestrateSubagentStream( requestPayload: Record, options: SubagentOrchestratorOptions ): Promise { - const { userId, workflowId, workspaceId, timeout = 300000, abortSignal } = options + const { userId, workflowId, workspaceId } = options const execContext = await buildExecutionContext(userId, workflowId, workspaceId) - const context: StreamingContext = { - chatId: undefined, - conversationId: undefined, + const context = createStreamingContext({ messageId: requestPayload?.messageId || crypto.randomUUID(), - accumulatedContent: '', - contentBlocks: [], - toolCalls: new Map(), - currentThinkingBlock: null, - isInThinkingBlock: false, - subAgentParentToolCallId: undefined, - subAgentContent: {}, - subAgentToolCalls: {}, - pendingContent: '', - streamComplete: false, - wasAborted: false, - errors: [], - } + }) let structuredResult: SubagentOrchestratorResult['structuredResult'] try { - const response = await fetch(`${SIM_AGENT_API_URL}/api/subagent/${agentId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), + await runStreamLoop( + `${SIM_AGENT_API_URL}/api/subagent/${agentId}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}), + }, + body: JSON.stringify({ ...requestPayload, stream: true }), }, - body: JSON.stringify({ ...requestPayload, stream: true }), - signal: abortSignal, - }) - - if (!response.ok) { - const errorText = await response.text().catch(() => '') - throw new Error( - `Copilot backend error (${response.status}): ${errorText || response.statusText}` - ) - } - - if (!response.body) { - throw new Error('Copilot backend response missing body') - } - - const reader = response.body.getReader() - const decoder = new TextDecoder() - - const timeoutId = setTimeout(() => { - context.errors.push('Request timed out') - context.streamComplete = true - reader.cancel().catch(() => {}) - }, timeout) - - try { - for await (const event of parseSSEStream(reader, decoder, abortSignal)) { - if (abortSignal?.aborted) { - context.wasAborted = true - break - } - - const normalizedEvent = normalizeSseEvent(event) - - // Skip duplicate tool events to prevent state regressions. - const shouldSkipToolCall = shouldSkipToolCallEvent(normalizedEvent) - const shouldSkipToolResult = shouldSkipToolResultEvent(normalizedEvent) - - if (!shouldSkipToolCall && !shouldSkipToolResult) { - await forwardEvent(normalizedEvent, options) - } - - if ( - normalizedEvent.type === 'structured_result' || - normalizedEvent.type === 'subagent_result' - ) { - structuredResult = normalizeStructuredResult(normalizedEvent.data) - context.streamComplete = true - continue - } - - // Handle subagent_start/subagent_end events to track nested subagent calls - if (normalizedEvent.type === 'subagent_start') { - const eventData = normalizedEvent.data as Record | undefined - const toolCallId = eventData?.tool_call_id as string | undefined - if (toolCallId) { - context.subAgentParentToolCallId = toolCallId - context.subAgentContent[toolCallId] = '' - context.subAgentToolCalls[toolCallId] = [] + context, + execContext, + { + ...options, + onBeforeDispatch: (event: SSEEvent, ctx: StreamingContext) => { + // Handle structured_result / subagent_result - subagent-specific. + if (event.type === 'structured_result' || event.type === 'subagent_result') { + structuredResult = normalizeStructuredResult(event.data) + ctx.streamComplete = true + return true // skip default dispatch } - continue - } - if (normalizedEvent.type === 'subagent_end') { - context.subAgentParentToolCallId = undefined - continue - } - - // For direct subagent calls, events may have the subagent field set (e.g., subagent: "discovery") - // but no subagent_start event because this IS the top-level agent. Skip subagent routing - // for events where the subagent field matches the current agentId - these are top-level events. - const isTopLevelSubagentEvent = - normalizedEvent.subagent === agentId && !context.subAgentParentToolCallId - - // Only route to subagent handlers for nested subagent events (not matching current agentId) - if (!isTopLevelSubagentEvent && handleSubagentRouting(normalizedEvent, context)) { - const handler = subAgentHandlers[normalizedEvent.type] - if (handler) { - await handler(normalizedEvent, context, execContext, options) + // For direct subagent calls, events may have the subagent field set + // but no subagent_start because this IS the top-level agent. + // Skip subagent routing for events where the subagent field matches + // the current agentId - these are top-level events. + if (event.subagent === agentId && !ctx.subAgentParentToolCallId) { + return false // let default dispatch handle it } - if (context.streamComplete) break - continue - } - // Process as a regular SSE event (including top-level subagent events) - const handler = sseHandlers[normalizedEvent.type] - if (handler) { - await handler(normalizedEvent, context, execContext, options) - } - if (context.streamComplete) break + return false // let default dispatch handle it + }, } - } finally { - clearTimeout(timeoutId) - } + ) - const result = buildResult(context, structuredResult) + const result: SubagentOrchestratorResult = { + success: context.errors.length === 0 && !context.wasAborted, + content: context.accumulatedContent, + toolCalls: buildToolCallSummaries(context), + structuredResult, + errors: context.errors.length ? context.errors : undefined, + } await options.onComplete?.(result) return result } catch (error) { @@ -186,26 +107,14 @@ export async function orchestrateSubagentStream( } } -async function forwardEvent(event: SSEEvent, options: OrchestratorOptions): Promise { - try { - await options.onEvent?.(event) - } catch (error) { - logger.warn('Failed to forward SSE event', { - type: event.type, - error: error instanceof Error ? error.message : String(error), - }) - } -} - -function normalizeStructuredResult(data: any): SubagentOrchestratorResult['structuredResult'] { - if (!data || typeof data !== 'object') { - return undefined - } +function normalizeStructuredResult(data: unknown): SubagentOrchestratorResult['structuredResult'] { + if (!data || typeof data !== 'object') return undefined + const d = data as Record return { - type: data.result_type || data.type, - summary: data.summary, - data: data.data ?? data, - success: data.success, + type: d.result_type || d.type, + summary: d.summary, + data: d.data ?? d, + success: d.success, } } @@ -217,7 +126,6 @@ async function buildExecutionContext( if (workflowId) { return prepareExecutionContext(userId, workflowId) } - const decryptedEnvVars = await getEffectiveDecryptedEnv(userId, workspaceId) return { userId, @@ -226,27 +134,3 @@ async function buildExecutionContext( decryptedEnvVars, } } - -function buildResult( - context: StreamingContext, - structuredResult?: SubagentOrchestratorResult['structuredResult'] -): SubagentOrchestratorResult { - const toolCalls: ToolCallSummary[] = Array.from(context.toolCalls.values()).map((toolCall) => ({ - id: toolCall.id, - name: toolCall.name, - status: toolCall.status, - params: toolCall.params, - result: toolCall.result?.output, - error: toolCall.error, - durationMs: - toolCall.endTime && toolCall.startTime ? toolCall.endTime - toolCall.startTime : undefined, - })) - - return { - success: context.errors.length === 0 && !context.wasAborted, - content: context.accumulatedContent, - toolCalls, - structuredResult, - errors: context.errors.length ? context.errors : undefined, - } -} diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/access.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/access.ts index 0f3f32492..b19459afa 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/access.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/access.ts @@ -127,4 +127,3 @@ export async function getAccessibleWorkflowsForUser( .where(or(...workflowConditions)) .orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)) } - diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts index aad2ed214..e876ed19d 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/deploy.ts @@ -1,15 +1,16 @@ import crypto from 'crypto' import { db } from '@sim/db' -import { chat, workflow, workflowMcpTool } from '@sim/db/schema' +import { chat, workflowMcpTool } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types' import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema' import { deployWorkflow, undeployWorkflow } from '@/lib/workflows/persistence/utils' import { checkChatAccess, checkWorkflowAccessForChatCreation } from '@/app/api/chat/utils' import { ensureWorkflowAccess } from '../access' +import type { DeployApiParams, DeployChatParams, DeployMcpParams } from '../param-types' export async function executeDeployApi( - params: Record, + params: DeployApiParams, context: ExecutionContext ): Promise { try { @@ -52,7 +53,7 @@ export async function executeDeployApi( } export async function executeDeployChat( - params: Record, + params: DeployChatParams, context: ExecutionContext ): Promise { try { @@ -126,10 +127,12 @@ export async function executeDeployChat( description: String(params.description || existingDeployment?.description || ''), customizations: { primaryColor: - params.customizations?.primaryColor || existingCustomizations.primaryColor || + params.customizations?.primaryColor || + existingCustomizations.primaryColor || 'var(--brand-primary-hover-hex)', welcomeMessage: - params.customizations?.welcomeMessage || existingCustomizations.welcomeMessage || + params.customizations?.welcomeMessage || + existingCustomizations.welcomeMessage || 'Hi there! How can I help you today?', }, authType: params.authType || existingDeployment?.authType || 'public', @@ -184,7 +187,7 @@ export async function executeDeployChat( } export async function executeDeployMcp( - params: Record, + params: DeployMcpParams, context: ExecutionContext ): Promise { try { @@ -217,14 +220,18 @@ export async function executeDeployMcp( const existingTool = await db .select() .from(workflowMcpTool) - .where(and(eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.workflowId, workflowId))) + .where( + and(eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.workflowId, workflowId)) + ) .limit(1) const toolName = sanitizeToolName( params.toolName || workflowRecord.name || `workflow_${workflowId}` ) const toolDescription = - params.toolDescription || workflowRecord.description || `Execute ${workflowRecord.name} workflow` + params.toolDescription || + workflowRecord.description || + `Execute ${workflowRecord.name} workflow` const parameterSchema = params.parameterSchema || {} if (existingTool.length > 0) { diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts index 4e6db4af3..555552693 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/deployment-tools/manage.ts @@ -6,9 +6,14 @@ import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrato import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema' import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server' import { ensureWorkflowAccess } from '../access' +import type { + CheckDeploymentStatusParams, + CreateWorkspaceMcpServerParams, + ListWorkspaceMcpServersParams, +} from '../param-types' export async function executeCheckDeploymentStatus( - params: Record, + params: CheckDeploymentStatusParams, context: ExecutionContext ): Promise { try { @@ -85,7 +90,7 @@ export async function executeCheckDeploymentStatus( } export async function executeListWorkspaceMcpServers( - params: Record, + params: ListWorkspaceMcpServersParams, context: ExecutionContext ): Promise { try { @@ -141,7 +146,7 @@ export async function executeListWorkspaceMcpServers( } export async function executeCreateWorkspaceMcpServer( - params: Record, + params: CreateWorkspaceMcpServerParams, context: ExecutionContext ): Promise { try { diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index fc839f612..41610306a 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -11,21 +11,7 @@ import type { import { routeExecution } from '@/lib/copilot/tools/server/router' import { env } from '@/lib/core/config/env' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' -import { executeIntegrationToolDirect } from './integration-tools' -import { - executeGetBlockOutputs, - executeGetBlockUpstreamReferences, - executeGetUserWorkflow, - executeGetWorkflowData, - executeGetWorkflowFromName, - executeListFolders, - executeListUserWorkflows, - executeListUserWorkspaces, - executeCreateWorkflow, - executeCreateFolder, - executeRunWorkflow, - executeSetGlobalWorkflowVariables, -} from './workflow-tools' +import { getTool, resolveToolId } from '@/tools/utils' import { executeCheckDeploymentStatus, executeCreateWorkspaceMcpServer, @@ -35,7 +21,40 @@ import { executeListWorkspaceMcpServers, executeRedeploy, } from './deployment-tools' -import { getTool, resolveToolId } from '@/tools/utils' +import { executeIntegrationToolDirect } from './integration-tools' +import type { + CheckDeploymentStatusParams, + CreateFolderParams, + CreateWorkflowParams, + CreateWorkspaceMcpServerParams, + DeployApiParams, + DeployChatParams, + DeployMcpParams, + GetBlockOutputsParams, + GetBlockUpstreamReferencesParams, + GetUserWorkflowParams, + GetWorkflowDataParams, + GetWorkflowFromNameParams, + ListFoldersParams, + ListUserWorkflowsParams, + ListWorkspaceMcpServersParams, + RunWorkflowParams, + SetGlobalWorkflowVariablesParams, +} from './param-types' +import { + executeCreateFolder, + executeCreateWorkflow, + executeGetBlockOutputs, + executeGetBlockUpstreamReferences, + executeGetUserWorkflow, + executeGetWorkflowData, + executeGetWorkflowFromName, + executeListFolders, + executeListUserWorkflows, + executeListUserWorkspaces, + executeRunWorkflow, + executeSetGlobalWorkflowVariables, +} from './workflow-tools' const logger = createLogger('CopilotToolExecutor') @@ -144,43 +163,43 @@ async function executeSimWorkflowTool( ): Promise { switch (toolName) { case 'get_user_workflow': - return executeGetUserWorkflow(params, context) + return executeGetUserWorkflow(params as GetUserWorkflowParams, context) case 'get_workflow_from_name': - return executeGetWorkflowFromName(params, context) + return executeGetWorkflowFromName(params as GetWorkflowFromNameParams, context) case 'list_user_workflows': - return executeListUserWorkflows(params, context) + return executeListUserWorkflows(params as ListUserWorkflowsParams, context) case 'list_user_workspaces': return executeListUserWorkspaces(context) case 'list_folders': - return executeListFolders(params, context) + return executeListFolders(params as ListFoldersParams, context) case 'create_workflow': - return executeCreateWorkflow(params, context) + return executeCreateWorkflow(params as CreateWorkflowParams, context) case 'create_folder': - return executeCreateFolder(params, context) + return executeCreateFolder(params as CreateFolderParams, context) case 'get_workflow_data': - return executeGetWorkflowData(params, context) + return executeGetWorkflowData(params as GetWorkflowDataParams, context) case 'get_block_outputs': - return executeGetBlockOutputs(params, context) + return executeGetBlockOutputs(params as GetBlockOutputsParams, context) case 'get_block_upstream_references': - return executeGetBlockUpstreamReferences(params, context) + return executeGetBlockUpstreamReferences(params as GetBlockUpstreamReferencesParams, context) case 'run_workflow': - return executeRunWorkflow(params, context) + return executeRunWorkflow(params as RunWorkflowParams, context) case 'set_global_workflow_variables': - return executeSetGlobalWorkflowVariables(params, context) + return executeSetGlobalWorkflowVariables(params as SetGlobalWorkflowVariablesParams, context) case 'deploy_api': - return executeDeployApi(params, context) + return executeDeployApi(params as DeployApiParams, context) case 'deploy_chat': - return executeDeployChat(params, context) + return executeDeployChat(params as DeployChatParams, context) case 'deploy_mcp': - return executeDeployMcp(params, context) + return executeDeployMcp(params as DeployMcpParams, context) case 'redeploy': return executeRedeploy(context) case 'check_deployment_status': - return executeCheckDeploymentStatus(params, context) + return executeCheckDeploymentStatus(params as CheckDeploymentStatusParams, context) case 'list_workspace_mcp_servers': - return executeListWorkspaceMcpServers(params, context) + return executeListWorkspaceMcpServers(params as ListWorkspaceMcpServersParams, context) case 'create_workspace_mcp_server': - return executeCreateWorkspaceMcpServer(params, context) + return executeCreateWorkspaceMcpServer(params as CreateWorkspaceMcpServerParams, context) default: return { success: false, error: `Unsupported workflow tool: ${toolName}` } } diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/integration-tools.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/integration-tools.ts index 44a10d7af..f70444acd 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/integration-tools.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/integration-tools.ts @@ -15,7 +15,10 @@ import { resolveToolId } from '@/tools/utils' export async function executeIntegrationToolDirect( toolCall: ToolCallState, - toolConfig: any, + toolConfig: { + oauth?: { required?: boolean; provider?: string } + params?: { apiKey?: { required?: boolean } } + }, context: ExecutionContext ): Promise { const { userId, workflowId } = context @@ -35,6 +38,9 @@ export async function executeIntegrationToolDirect( const decryptedEnvVars = context.decryptedEnvVars || (await getEffectiveDecryptedEnv(userId, workspaceId)) + // Deep resolution walks nested objects to replace {{ENV_VAR}} references. + // Safe because tool arguments originate from the LLM (not direct user input) + // and env vars belong to the user themselves. const executionParams: Record = resolveEnvVarReferences(toolArgs, decryptedEnvVars, { deep: true, }) as Record @@ -97,4 +103,3 @@ export async function executeIntegrationToolDirect( error: result.error, } } - diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts new file mode 100644 index 000000000..30d519087 --- /dev/null +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts @@ -0,0 +1,127 @@ +/** + * Typed parameter interfaces for tool executor functions. + * Replaces Record with specific shapes based on actual property access. + */ + +// === Workflow Query Params === + +export interface GetUserWorkflowParams { + workflowId?: string +} + +export interface GetWorkflowFromNameParams { + workflow_name?: string +} + +export interface ListUserWorkflowsParams { + workspaceId?: string + folderId?: string +} + +export interface GetWorkflowDataParams { + workflowId?: string + data_type?: string + dataType?: string +} + +export interface GetBlockOutputsParams { + workflowId?: string + blockIds?: string[] +} + +export interface GetBlockUpstreamReferencesParams { + workflowId?: string + blockIds: string[] +} + +export interface ListFoldersParams { + workspaceId?: string +} + +// === Workflow Mutation Params === + +export interface CreateWorkflowParams { + name?: string + workspaceId?: string + folderId?: string + description?: string +} + +export interface CreateFolderParams { + name?: string + workspaceId?: string + parentId?: string +} + +export interface RunWorkflowParams { + workflowId?: string + workflow_input?: unknown + input?: unknown +} + +export interface VariableOperation { + name: string + operation: 'add' | 'edit' | 'delete' + value?: unknown + type?: string +} + +export interface SetGlobalWorkflowVariablesParams { + workflowId?: string + operations?: VariableOperation[] +} + +// === Deployment Params === + +export interface DeployApiParams { + workflowId?: string + action?: 'deploy' | 'undeploy' +} + +export interface DeployChatParams { + workflowId?: string + action?: 'deploy' | 'undeploy' | 'update' + identifier?: string + title?: string + description?: string + customizations?: { + primaryColor?: string + secondaryColor?: string + welcomeMessage?: string + iconUrl?: string + } + authType?: 'none' | 'password' | 'public' | 'email' | 'sso' + password?: string + subdomain?: string + allowedEmails?: string[] + outputConfigs?: unknown[] +} + +export interface DeployMcpParams { + workflowId?: string + action?: 'deploy' | 'undeploy' + toolName?: string + toolDescription?: string + serverId?: string + parameterSchema?: Record +} + +export interface CheckDeploymentStatusParams { + workflowId?: string +} + +export interface ListWorkspaceMcpServersParams { + workspaceId?: string + workflowId?: string +} + +export interface CreateWorkspaceMcpServerParams { + workflowId?: string + name?: string + description?: string + toolName?: string + toolDescription?: string + serverName?: string + isPublic?: boolean + workflowIds?: string[] +} diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts index 938d84e7b..b908b0710 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/index.ts @@ -1,2 +1,2 @@ -export * from './queries' export * from './mutations' +export * from './queries' diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts index ed4b51cc0..12158fc74 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts @@ -8,9 +8,16 @@ import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' import { ensureWorkflowAccess, ensureWorkspaceAccess, getDefaultWorkspaceId } from '../access' +import type { + CreateFolderParams, + CreateWorkflowParams, + RunWorkflowParams, + SetGlobalWorkflowVariablesParams, + VariableOperation, +} from '../param-types' export async function executeCreateWorkflow( - params: Record, + params: CreateWorkflowParams, context: ExecutionContext ): Promise { try { @@ -18,10 +25,16 @@ export async function executeCreateWorkflow( if (!name) { return { success: false, error: 'name is required' } } + if (name.length > 200) { + return { success: false, error: 'Workflow name must be 200 characters or less' } + } + const description = typeof params?.description === 'string' ? params.description : null + if (description && description.length > 2000) { + return { success: false, error: 'Description must be 2000 characters or less' } + } const workspaceId = params?.workspaceId || (await getDefaultWorkspaceId(context.userId)) const folderId = params?.folderId || null - const description = typeof params?.description === 'string' ? params.description : null await ensureWorkspaceAccess(workspaceId, context.userId, true) @@ -73,7 +86,7 @@ export async function executeCreateWorkflow( } export async function executeCreateFolder( - params: Record, + params: CreateFolderParams, context: ExecutionContext ): Promise { try { @@ -81,6 +94,9 @@ export async function executeCreateFolder( if (!name) { return { success: false, error: 'name is required' } } + if (name.length > 200) { + return { success: false, error: 'Folder name must be 200 characters or less' } + } const workspaceId = params?.workspaceId || (await getDefaultWorkspaceId(context.userId)) const parentId = params?.parentId || null @@ -117,7 +133,7 @@ export async function executeCreateFolder( } export async function executeRunWorkflow( - params: Record, + params: RunWorkflowParams, context: ExecutionContext ): Promise { try { @@ -156,7 +172,7 @@ export async function executeRunWorkflow( } export async function executeSetGlobalWorkflowVariables( - params: Record, + params: SetGlobalWorkflowVariablesParams, context: ExecutionContext ): Promise { try { @@ -164,7 +180,9 @@ export async function executeSetGlobalWorkflowVariables( if (!workflowId) { return { success: false, error: 'workflowId is required' } } - const operations = Array.isArray(params.operations) ? params.operations : [] + const operations: VariableOperation[] = Array.isArray(params.operations) + ? params.operations + : [] const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) const currentVarsRecord = (workflowRecord.variables as Record) || {} diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts index 7bbb8bd38..5bcca2e0d 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts @@ -13,16 +13,25 @@ import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs' import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' +import { normalizeName } from '@/executor/constants' import { ensureWorkflowAccess, ensureWorkspaceAccess, getAccessibleWorkflowsForUser, getDefaultWorkspaceId, } from '../access' -import { normalizeName } from '@/executor/constants' +import type { + GetBlockOutputsParams, + GetBlockUpstreamReferencesParams, + GetUserWorkflowParams, + GetWorkflowDataParams, + GetWorkflowFromNameParams, + ListFoldersParams, + ListUserWorkflowsParams, +} from '../param-types' export async function executeGetUserWorkflow( - params: Record, + params: GetUserWorkflowParams, context: ExecutionContext ): Promise { try { @@ -57,7 +66,7 @@ export async function executeGetUserWorkflow( } export async function executeGetWorkflowFromName( - params: Record, + params: GetWorkflowFromNameParams, context: ExecutionContext ): Promise { try { @@ -95,7 +104,7 @@ export async function executeGetWorkflowFromName( } export async function executeListUserWorkflows( - params: Record, + params: ListUserWorkflowsParams, context: ExecutionContext ): Promise { try { @@ -119,7 +128,9 @@ export async function executeListUserWorkflows( } } -export async function executeListUserWorkspaces(context: ExecutionContext): Promise { +export async function executeListUserWorkspaces( + context: ExecutionContext +): Promise { try { const workspaces = await db .select({ @@ -146,7 +157,7 @@ export async function executeListUserWorkspaces(context: ExecutionContext): Prom } export async function executeListFolders( - params: Record, + params: ListFoldersParams, context: ExecutionContext ): Promise { try { @@ -179,7 +190,7 @@ export async function executeListFolders( } export async function executeGetWorkflowData( - params: Record, + params: GetWorkflowDataParams, context: ExecutionContext ): Promise { try { @@ -271,7 +282,7 @@ export async function executeGetWorkflowData( } export async function executeGetBlockOutputs( - params: Record, + params: GetBlockOutputsParams, context: ExecutionContext ): Promise { try { @@ -343,7 +354,7 @@ export async function executeGetBlockOutputs( } export async function executeGetBlockUpstreamReferences( - params: Record, + params: GetBlockUpstreamReferencesParams, context: ExecutionContext ): Promise { try { @@ -524,4 +535,3 @@ function formatOutputsWithPrefix(paths: string[], blockName: string): string[] { const normalizedName = normalizeName(blockName) return paths.map((path) => `${normalizedName}.${path}`) } - diff --git a/apps/sim/lib/copilot/tools/client/base-tool.ts b/apps/sim/lib/copilot/tools/client/base-tool.ts index 73a562aa1..32060e87a 100644 --- a/apps/sim/lib/copilot/tools/client/base-tool.ts +++ b/apps/sim/lib/copilot/tools/client/base-tool.ts @@ -20,7 +20,10 @@ export interface ClientToolDisplay { export interface BaseClientToolMetadata { displayNames: Partial> uiConfig?: Record - getDynamicText?: (params: Record, state: ClientToolCallState) => string | undefined + getDynamicText?: ( + params: Record, + state: ClientToolCallState + ) => string | undefined } export type DynamicTextFormatter = ( diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts index f7242d12c..42633ab37 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -1,5 +1,53 @@ +// @ts-nocheck import type { LucideIcon } from 'lucide-react' -import { Blocks, BookOpen, Bug, Check, CheckCircle, CheckCircle2, ClipboardCheck, Compass, Database, FileCode, FileText, FlaskConical, GitBranch, Globe, Globe2, Grid2x2, Grid2x2Check, Grid2x2X, Info, Key, KeyRound, ListChecks, ListFilter, ListTodo, Loader2, MessageSquare, MinusCircle, Moon, Navigation, Pencil, PencilLine, Play, PlugZap, Plus, Rocket, Search, Server, Settings2, Sparkles, Tag, TerminalSquare, WorkflowIcon, Wrench, X, XCircle, Zap } from 'lucide-react' +import { + Blocks, + BookOpen, + Bug, + Check, + CheckCircle, + CheckCircle2, + ClipboardCheck, + Compass, + Database, + FileCode, + FileText, + FlaskConical, + GitBranch, + Globe, + Globe2, + Grid2x2, + Grid2x2Check, + Grid2x2X, + Info, + Key, + KeyRound, + ListChecks, + ListFilter, + ListTodo, + Loader2, + MessageSquare, + MinusCircle, + Moon, + Navigation, + Pencil, + PencilLine, + Play, + PlugZap, + Plus, + Rocket, + Search, + Server, + Settings2, + Sparkles, + Tag, + TerminalSquare, + WorkflowIcon, + Wrench, + X, + XCircle, + Zap, +} from 'lucide-react' import { getLatestBlock } from '@/blocks/registry' import { getCustomTool } from '@/hooks/queries/custom-tools' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -102,893 +150,777 @@ function toToolDisplayEntry(metadata?: ToolMetadata): ToolDisplayEntry { } const META_auth: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Authenticating', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Authenticating', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Authenticating', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Authenticated', icon: KeyRound }, - [ClientToolCallState.error]: { text: 'Failed to authenticate', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped auth', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted auth', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Authenticating', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Authenticating', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Authenticating', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Authenticated', icon: KeyRound }, + [ClientToolCallState.error]: { text: 'Failed to authenticate', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped auth', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted auth', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Authenticating', + completedLabel: 'Authenticated', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Authenticating', - completedLabel: 'Authenticated', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_check_deployment_status: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Checking deployment status', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Checking deployment status', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Checking deployment status', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Checked deployment status', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to check deployment status', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted checking deployment status', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped checking deployment status', - icon: XCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Checking deployment status', + icon: Loader2, }, - interrupt: undefined, - } + [ClientToolCallState.pending]: { text: 'Checking deployment status', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Checking deployment status', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Checked deployment status', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to check deployment status', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted checking deployment status', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped checking deployment status', + icon: XCircle, + }, + }, + interrupt: undefined, +} const META_checkoff_todo: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Marking todo', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Marking todo', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Marked todo complete', icon: Check }, - [ClientToolCallState.error]: { text: 'Failed to mark todo', icon: XCircle }, - }, - } + displayNames: { + [ClientToolCallState.generating]: { text: 'Marking todo', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Marking todo', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Marked todo complete', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed to mark todo', icon: XCircle }, + }, +} const META_crawl_website: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Crawling website', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Crawling website', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Crawling website', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Crawled website', icon: Globe }, - [ClientToolCallState.error]: { text: 'Failed to crawl website', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted crawling website', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped crawling website', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.url && typeof params.url === 'string') { - const url = params.url + displayNames: { + [ClientToolCallState.generating]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Crawled website', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to crawl website', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted crawling website', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped crawling website', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const url = params.url - switch (state) { - case ClientToolCallState.success: - return `Crawled ${url}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Crawling ${url}` - case ClientToolCallState.error: - return `Failed to crawl ${url}` - case ClientToolCallState.aborted: - return `Aborted crawling ${url}` - case ClientToolCallState.rejected: - return `Skipped crawling ${url}` - } - } - return undefined - }, - } - -const META_create_workspace_mcp_server: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to create MCP server', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Create MCP server?', icon: Server }, - [ClientToolCallState.executing]: { text: 'Creating MCP server', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Created MCP server', icon: Server }, - [ClientToolCallState.error]: { text: 'Failed to create MCP server', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted creating MCP server', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped creating MCP server', icon: XCircle }, - }, - interrupt: { - accept: { text: 'Create', icon: Plus }, - reject: { text: 'Skip', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const name = params?.name || 'MCP server' switch (state) { case ClientToolCallState.success: - return `Created MCP server "${name}"` + return `Crawled ${url}` case ClientToolCallState.executing: - return `Creating MCP server "${name}"` case ClientToolCallState.generating: - return `Preparing to create "${name}"` case ClientToolCallState.pending: - return `Create MCP server "${name}"?` + return `Crawling ${url}` case ClientToolCallState.error: - return `Failed to create "${name}"` + return `Failed to crawl ${url}` + case ClientToolCallState.aborted: + return `Aborted crawling ${url}` + case ClientToolCallState.rejected: + return `Skipped crawling ${url}` } - return undefined + } + return undefined + }, +} + +const META_create_workspace_mcp_server: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to create MCP server', + icon: Loader2, }, - } + [ClientToolCallState.pending]: { text: 'Create MCP server?', icon: Server }, + [ClientToolCallState.executing]: { text: 'Creating MCP server', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Created MCP server', icon: Server }, + [ClientToolCallState.error]: { text: 'Failed to create MCP server', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted creating MCP server', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped creating MCP server', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Create', icon: Plus }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const name = params?.name || 'MCP server' + switch (state) { + case ClientToolCallState.success: + return `Created MCP server "${name}"` + case ClientToolCallState.executing: + return `Creating MCP server "${name}"` + case ClientToolCallState.generating: + return `Preparing to create "${name}"` + case ClientToolCallState.pending: + return `Create MCP server "${name}"?` + case ClientToolCallState.error: + return `Failed to create "${name}"` + } + return undefined + }, +} const META_custom_tool: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Wrench }, - [ClientToolCallState.error]: { text: 'Failed custom tool', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped custom tool', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted custom tool', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Wrench }, + [ClientToolCallState.error]: { text: 'Failed custom tool', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped custom tool', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted custom tool', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Managing custom tool', + completedLabel: 'Custom tool managed', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Managing custom tool', - completedLabel: 'Custom tool managed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_debug: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, - [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped debug', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted debug', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, + [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped debug', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted debug', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Debugging', + completedLabel: 'Debugged', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Debugging', - completedLabel: 'Debugged', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_deploy: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Deploying', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Deploying', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Deploying', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to deploy', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped deploy', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted deploy', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Deploying', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Deploying', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Deploying', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to deploy', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped deploy', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted deploy', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Deploying', + completedLabel: 'Deployed', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Deploying', - completedLabel: 'Deployed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_deploy_api: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to deploy API', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Deploy as API?', icon: Rocket }, - [ClientToolCallState.executing]: { text: 'Deploying API', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed API', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to deploy API', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted deploying API', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped deploying API', - icon: XCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to deploy API', + icon: Loader2, }, + [ClientToolCallState.pending]: { text: 'Deploy as API?', icon: Rocket }, + [ClientToolCallState.executing]: { text: 'Deploying API', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed API', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to deploy API', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted deploying API', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped deploying API', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Deploy', icon: Rocket }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, interrupt: { accept: { text: 'Deploy', icon: Rocket }, reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Deploy', icon: Rocket }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - }, - getDynamicText: (params, state) => { - const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' + }, + getDynamicText: (params, state) => { + const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' - const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId - const isAlreadyDeployed = workflowId - ? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed - : false + const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId + const isAlreadyDeployed = workflowId + ? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed + : false - let actionText = action - let actionTextIng = action === 'undeploy' ? 'undeploying' : 'deploying' - const actionTextPast = action === 'undeploy' ? 'undeployed' : 'deployed' + let actionText = action + let actionTextIng = action === 'undeploy' ? 'undeploying' : 'deploying' + const actionTextPast = action === 'undeploy' ? 'undeployed' : 'deployed' - if (action === 'deploy' && isAlreadyDeployed) { - actionText = 'redeploy' - actionTextIng = 'redeploying' - } + if (action === 'deploy' && isAlreadyDeployed) { + actionText = 'redeploy' + actionTextIng = 'redeploying' + } - const actionCapitalized = actionText.charAt(0).toUpperCase() + actionText.slice(1) + const actionCapitalized = actionText.charAt(0).toUpperCase() + actionText.slice(1) - switch (state) { - case ClientToolCallState.success: - return `API ${actionTextPast}` - case ClientToolCallState.executing: - return `${actionCapitalized}ing API` - case ClientToolCallState.generating: - return `Preparing to ${actionText} API` - case ClientToolCallState.pending: - return `${actionCapitalized} API?` - case ClientToolCallState.error: - return `Failed to ${actionText} API` - case ClientToolCallState.aborted: - return `Aborted ${actionTextIng} API` - case ClientToolCallState.rejected: - return `Skipped ${actionTextIng} API` - } - return undefined - }, - } + switch (state) { + case ClientToolCallState.success: + return `API ${actionTextPast}` + case ClientToolCallState.executing: + return `${actionCapitalized}ing API` + case ClientToolCallState.generating: + return `Preparing to ${actionText} API` + case ClientToolCallState.pending: + return `${actionCapitalized} API?` + case ClientToolCallState.error: + return `Failed to ${actionText} API` + case ClientToolCallState.aborted: + return `Aborted ${actionTextIng} API` + case ClientToolCallState.rejected: + return `Skipped ${actionTextIng} API` + } + return undefined + }, +} const META_deploy_chat: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to deploy chat', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Deploy as chat?', icon: MessageSquare }, - [ClientToolCallState.executing]: { text: 'Deploying chat', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed chat', icon: MessageSquare }, - [ClientToolCallState.error]: { text: 'Failed to deploy chat', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted deploying chat', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped deploying chat', - icon: XCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to deploy chat', + icon: Loader2, }, + [ClientToolCallState.pending]: { text: 'Deploy as chat?', icon: MessageSquare }, + [ClientToolCallState.executing]: { text: 'Deploying chat', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed chat', icon: MessageSquare }, + [ClientToolCallState.error]: { text: 'Failed to deploy chat', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted deploying chat', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped deploying chat', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Deploy Chat', icon: MessageSquare }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, interrupt: { accept: { text: 'Deploy Chat', icon: MessageSquare }, reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Deploy Chat', icon: MessageSquare }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - }, - getDynamicText: (params, state) => { - const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' + }, + getDynamicText: (params, state) => { + const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' - switch (state) { - case ClientToolCallState.success: - return action === 'undeploy' ? 'Chat undeployed' : 'Chat deployed' - case ClientToolCallState.executing: - return action === 'undeploy' ? 'Undeploying chat' : 'Deploying chat' - case ClientToolCallState.generating: - return `Preparing to ${action} chat` - case ClientToolCallState.pending: - return action === 'undeploy' ? 'Undeploy chat?' : 'Deploy as chat?' - case ClientToolCallState.error: - return `Failed to ${action} chat` - case ClientToolCallState.aborted: - return action === 'undeploy' ? 'Aborted undeploying chat' : 'Aborted deploying chat' - case ClientToolCallState.rejected: - return action === 'undeploy' ? 'Skipped undeploying chat' : 'Skipped deploying chat' - } - return undefined - }, - } + switch (state) { + case ClientToolCallState.success: + return action === 'undeploy' ? 'Chat undeployed' : 'Chat deployed' + case ClientToolCallState.executing: + return action === 'undeploy' ? 'Undeploying chat' : 'Deploying chat' + case ClientToolCallState.generating: + return `Preparing to ${action} chat` + case ClientToolCallState.pending: + return action === 'undeploy' ? 'Undeploy chat?' : 'Deploy as chat?' + case ClientToolCallState.error: + return `Failed to ${action} chat` + case ClientToolCallState.aborted: + return action === 'undeploy' ? 'Aborted undeploying chat' : 'Aborted deploying chat' + case ClientToolCallState.rejected: + return action === 'undeploy' ? 'Skipped undeploying chat' : 'Skipped deploying chat' + } + return undefined + }, +} const META_deploy_mcp: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to deploy to MCP', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Deploy to MCP server?', icon: Server }, - [ClientToolCallState.executing]: { text: 'Deploying to MCP', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed to MCP', icon: Server }, - [ClientToolCallState.error]: { text: 'Failed to deploy to MCP', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted MCP deployment', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped MCP deployment', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to deploy to MCP', + icon: Loader2, }, + [ClientToolCallState.pending]: { text: 'Deploy to MCP server?', icon: Server }, + [ClientToolCallState.executing]: { text: 'Deploying to MCP', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed to MCP', icon: Server }, + [ClientToolCallState.error]: { text: 'Failed to deploy to MCP', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted MCP deployment', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped MCP deployment', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Deploy', icon: Server }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, interrupt: { accept: { text: 'Deploy', icon: Server }, reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Deploy', icon: Server }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - }, - getDynamicText: (params, state) => { - const toolName = params?.toolName || 'workflow' - switch (state) { - case ClientToolCallState.success: - return `Deployed "${toolName}" to MCP` - case ClientToolCallState.executing: - return `Deploying "${toolName}" to MCP` - case ClientToolCallState.generating: - return `Preparing to deploy to MCP` - case ClientToolCallState.pending: - return `Deploy "${toolName}" to MCP?` - case ClientToolCallState.error: - return `Failed to deploy to MCP` - } - return undefined - }, - } + }, + getDynamicText: (params, state) => { + const toolName = params?.toolName || 'workflow' + switch (state) { + case ClientToolCallState.success: + return `Deployed "${toolName}" to MCP` + case ClientToolCallState.executing: + return `Deploying "${toolName}" to MCP` + case ClientToolCallState.generating: + return `Preparing to deploy to MCP` + case ClientToolCallState.pending: + return `Deploy "${toolName}" to MCP?` + case ClientToolCallState.error: + return `Failed to deploy to MCP` + } + return undefined + }, +} const META_edit: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Editing', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Editing', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Editing', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Edited', icon: Pencil }, - [ClientToolCallState.error]: { text: 'Failed to apply edit', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped edit', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted edit', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Editing', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Editing', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Editing', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Edited', icon: Pencil }, + [ClientToolCallState.error]: { text: 'Failed to apply edit', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped edit', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted edit', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, + subagent: { + streamingLabel: 'Editing', + completedLabel: 'Edited', + shouldCollapse: false, // Edit subagent stays expanded + outputArtifacts: ['edit_summary'], + hideThinkingText: true, // We show WorkflowEditSummary instead }, - uiConfig: { - isSpecial: true, - subagent: { - streamingLabel: 'Editing', - completedLabel: 'Edited', - shouldCollapse: false, // Edit subagent stays expanded - outputArtifacts: ['edit_summary'], - hideThinkingText: true, // We show WorkflowEditSummary instead - }, - }, - } + }, +} const META_edit_workflow: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Editing your workflow', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Editing your workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Edited your workflow', icon: Grid2x2Check }, - [ClientToolCallState.error]: { text: 'Failed to edit your workflow', icon: XCircle }, - [ClientToolCallState.review]: { text: 'Review your workflow changes', icon: Grid2x2 }, - [ClientToolCallState.rejected]: { text: 'Rejected workflow changes', icon: Grid2x2X }, - [ClientToolCallState.aborted]: { text: 'Aborted editing your workflow', icon: MinusCircle }, - [ClientToolCallState.pending]: { text: 'Editing your workflow', icon: Loader2 }, - }, - uiConfig: { - isSpecial: true, - customRenderer: 'edit_summary', - }, - } + displayNames: { + [ClientToolCallState.generating]: { text: 'Editing your workflow', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Editing your workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Edited your workflow', icon: Grid2x2Check }, + [ClientToolCallState.error]: { text: 'Failed to edit your workflow', icon: XCircle }, + [ClientToolCallState.review]: { text: 'Review your workflow changes', icon: Grid2x2 }, + [ClientToolCallState.rejected]: { text: 'Rejected workflow changes', icon: Grid2x2X }, + [ClientToolCallState.aborted]: { text: 'Aborted editing your workflow', icon: MinusCircle }, + [ClientToolCallState.pending]: { text: 'Editing your workflow', icon: Loader2 }, + }, + uiConfig: { + isSpecial: true, + customRenderer: 'edit_summary', + }, +} const META_evaluate: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Evaluating', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Evaluating', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Evaluating', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Evaluated', icon: ClipboardCheck }, - [ClientToolCallState.error]: { text: 'Failed to evaluate', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped evaluation', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted evaluation', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Evaluating', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Evaluating', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Evaluating', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Evaluated', icon: ClipboardCheck }, + [ClientToolCallState.error]: { text: 'Failed to evaluate', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped evaluation', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted evaluation', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Evaluating', + completedLabel: 'Evaluated', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Evaluating', - completedLabel: 'Evaluated', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_get_block_config: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting block config', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting block config', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting block config', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved block config', icon: FileCode }, - [ClientToolCallState.error]: { text: 'Failed to get block config', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting block config', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped getting block config', - icon: MinusCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting block config', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting block config', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting block config', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved block config', icon: FileCode }, + [ClientToolCallState.error]: { text: 'Failed to get block config', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting block config', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped getting block config', + icon: MinusCircle, }, - getDynamicText: (params, state) => { - if (params?.blockType && typeof params.blockType === 'string') { - const blockConfig = getLatestBlock(params.blockType) - const blockName = (blockConfig?.name ?? params.blockType.replace(/_/g, ' ')).toLowerCase() - const opSuffix = params.operation ? ` (${params.operation})` : '' - - switch (state) { - case ClientToolCallState.success: - return `Retrieved ${blockName}${opSuffix} config` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Retrieving ${blockName}${opSuffix} config` - case ClientToolCallState.error: - return `Failed to retrieve ${blockName}${opSuffix} config` - case ClientToolCallState.aborted: - return `Aborted retrieving ${blockName}${opSuffix} config` - case ClientToolCallState.rejected: - return `Skipped retrieving ${blockName}${opSuffix} config` - } - } - return undefined - }, - } - -const META_get_block_options: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting block operations', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting block operations', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting block operations', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved block operations', icon: ListFilter }, - [ClientToolCallState.error]: { text: 'Failed to get block operations', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting block operations', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped getting block operations', - icon: MinusCircle, - }, - }, - getDynamicText: (params, state) => { - const blockId = - (params as any)?.blockId || - (params as any)?.blockType || - (params as any)?.block_id || - (params as any)?.block_type - if (typeof blockId === 'string') { - const blockConfig = getLatestBlock(blockId) - const blockName = (blockConfig?.name ?? blockId.replace(/_/g, ' ')).toLowerCase() - - switch (state) { - case ClientToolCallState.success: - return `Retrieved ${blockName} operations` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Retrieving ${blockName} operations` - case ClientToolCallState.error: - return `Failed to retrieve ${blockName} operations` - case ClientToolCallState.aborted: - return `Aborted retrieving ${blockName} operations` - case ClientToolCallState.rejected: - return `Skipped retrieving ${blockName} operations` - } - } - return undefined - }, - } - -const META_get_block_outputs: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting block outputs', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting block outputs', icon: Tag }, - [ClientToolCallState.executing]: { text: 'Getting block outputs', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted getting outputs', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Retrieved block outputs', icon: Tag }, - [ClientToolCallState.error]: { text: 'Failed to get outputs', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped getting outputs', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const blockIds = params?.blockIds - if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { - const count = blockIds.length - switch (state) { - case ClientToolCallState.success: - return `Retrieved outputs for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Getting outputs for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.error: - return `Failed to get outputs for ${count} block${count > 1 ? 's' : ''}` - } - } - return undefined - }, - } - -const META_get_block_upstream_references: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting upstream references', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting upstream references', icon: GitBranch }, - [ClientToolCallState.executing]: { text: 'Getting upstream references', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted getting references', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Retrieved upstream references', icon: GitBranch }, - [ClientToolCallState.error]: { text: 'Failed to get references', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped getting references', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const blockIds = params?.blockIds - if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { - const count = blockIds.length - switch (state) { - case ClientToolCallState.success: - return `Retrieved references for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Getting references for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.error: - return `Failed to get references for ${count} block${count > 1 ? 's' : ''}` - } - } - return undefined - }, - } - -const META_get_blocks_and_tools: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Exploring available options', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Exploring available options', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Exploring available options', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Explored available options', icon: Blocks }, - [ClientToolCallState.error]: { text: 'Failed to explore options', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted exploring options', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped exploring options', icon: MinusCircle }, - }, - interrupt: undefined, - } - -const META_get_blocks_metadata: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching block choices', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching block choices', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching block choices', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Searched block choices', icon: ListFilter }, - [ClientToolCallState.error]: { text: 'Failed to search block choices', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted searching block choices', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped searching block choices', - icon: MinusCircle, - }, - }, - getDynamicText: (params, state) => { - if (params?.blockIds && Array.isArray(params.blockIds) && params.blockIds.length > 0) { - const blockList = params.blockIds - .slice(0, 3) - .map((blockId) => blockId.replace(/_/g, ' ')) - .join(', ') - const more = params.blockIds.length > 3 ? '...' : '' - const blocks = `${blockList}${more}` - - switch (state) { - case ClientToolCallState.success: - return `Searched ${blocks}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching ${blocks}` - case ClientToolCallState.error: - return `Failed to search ${blocks}` - case ClientToolCallState.aborted: - return `Aborted searching ${blocks}` - case ClientToolCallState.rejected: - return `Skipped searching ${blocks}` - } - } - return undefined - }, - } - -const META_get_credentials: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching connected integrations', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Fetching connected integrations', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Fetching connected integrations', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Fetched connected integrations', icon: Key }, - [ClientToolCallState.error]: { - text: 'Failed to fetch connected integrations', - icon: XCircle, - }, - [ClientToolCallState.aborted]: { - text: 'Aborted fetching connected integrations', - icon: MinusCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped fetching connected integrations', - icon: MinusCircle, - }, - }, - } - -const META_get_examples_rag: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching examples', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Fetching examples', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Fetching examples', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Fetched examples', icon: Search }, - [ClientToolCallState.error]: { text: 'Failed to fetch examples', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting examples', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped getting examples', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Found examples for ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching examples for ${query}` - case ClientToolCallState.error: - return `Failed to find examples for ${query}` - case ClientToolCallState.aborted: - return `Aborted searching examples for ${query}` - case ClientToolCallState.rejected: - return `Skipped searching examples for ${query}` - } - } - return undefined - }, - } - -const META_get_operations_examples: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Designing workflow component', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Designing workflow component', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Designing workflow component', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Designed workflow component', icon: Zap }, - [ClientToolCallState.error]: { text: 'Failed to design workflow component', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted designing workflow component', - icon: MinusCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped designing workflow component', - icon: MinusCircle, - }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Designed ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Designing ${query}` - case ClientToolCallState.error: - return `Failed to design ${query}` - case ClientToolCallState.aborted: - return `Aborted designing ${query}` - case ClientToolCallState.rejected: - return `Skipped designing ${query}` - } - } - return undefined - }, - } - -const META_get_page_contents: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting page contents', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting page contents', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting page contents', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved page contents', icon: FileText }, - [ClientToolCallState.error]: { text: 'Failed to get page contents', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting page contents', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped getting page contents', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.urls && Array.isArray(params.urls) && params.urls.length > 0) { - const firstUrl = String(params.urls[0]) - const count = params.urls.length - - switch (state) { - case ClientToolCallState.success: - return count > 1 ? `Retrieved ${count} pages` : `Retrieved ${firstUrl}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return count > 1 ? `Getting ${count} pages` : `Getting ${firstUrl}` - case ClientToolCallState.error: - return count > 1 ? `Failed to get ${count} pages` : `Failed to get ${firstUrl}` - case ClientToolCallState.aborted: - return count > 1 ? `Aborted getting ${count} pages` : `Aborted getting ${firstUrl}` - case ClientToolCallState.rejected: - return count > 1 ? `Skipped getting ${count} pages` : `Skipped getting ${firstUrl}` - } - } - return undefined - }, - } - -const META_get_trigger_blocks: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Finding trigger blocks', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Finding trigger blocks', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Finding trigger blocks', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Found trigger blocks', icon: ListFilter }, - [ClientToolCallState.error]: { text: 'Failed to find trigger blocks', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted finding trigger blocks', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped finding trigger blocks', icon: MinusCircle }, - }, - interrupt: undefined, - } - -const META_get_trigger_examples: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Selecting a trigger', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Selecting a trigger', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Selecting a trigger', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Selected a trigger', icon: Zap }, - [ClientToolCallState.error]: { text: 'Failed to select a trigger', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted selecting a trigger', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped selecting a trigger', icon: MinusCircle }, - }, - interrupt: undefined, - } - -const META_get_user_workflow: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Reading your workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Reading your workflow', icon: WorkflowIcon }, - [ClientToolCallState.executing]: { text: 'Reading your workflow', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted reading your workflow', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Read your workflow', icon: WorkflowIcon }, - [ClientToolCallState.error]: { text: 'Failed to read your workflow', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped reading your workflow', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId - if (workflowId) { - const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name - if (workflowName) { - switch (state) { - case ClientToolCallState.success: - return `Read ${workflowName}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Reading ${workflowName}` - case ClientToolCallState.error: - return `Failed to read ${workflowName}` - case ClientToolCallState.aborted: - return `Aborted reading ${workflowName}` - case ClientToolCallState.rejected: - return `Skipped reading ${workflowName}` - } - } - } - return undefined - }, - } - -const META_get_workflow_console: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching execution logs', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Fetching execution logs', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Fetched execution logs', icon: TerminalSquare }, - [ClientToolCallState.error]: { text: 'Failed to fetch execution logs', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped fetching execution logs', - icon: MinusCircle, - }, - [ClientToolCallState.aborted]: { - text: 'Aborted fetching execution logs', - icon: MinusCircle, - }, - [ClientToolCallState.pending]: { text: 'Fetching execution logs', icon: Loader2 }, - }, - getDynamicText: (params, state) => { - const limit = params?.limit - if (limit && typeof limit === 'number') { - const logText = limit === 1 ? 'execution log' : 'execution logs' - - switch (state) { - case ClientToolCallState.success: - return `Fetched last ${limit} ${logText}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Fetching last ${limit} ${logText}` - case ClientToolCallState.error: - return `Failed to fetch last ${limit} ${logText}` - case ClientToolCallState.rejected: - return `Skipped fetching last ${limit} ${logText}` - case ClientToolCallState.aborted: - return `Aborted fetching last ${limit} ${logText}` - } - } - return undefined - }, - } - -const META_get_workflow_data: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching workflow data', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Fetching workflow data', icon: Database }, - [ClientToolCallState.executing]: { text: 'Fetching workflow data', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted fetching data', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Retrieved workflow data', icon: Database }, - [ClientToolCallState.error]: { text: 'Failed to fetch data', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped fetching data', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const dataType = params?.data_type as WorkflowDataType | undefined - if (!dataType) return undefined - - const typeLabels: Record = { - global_variables: 'variables', - custom_tools: 'custom tools', - mcp_tools: 'MCP tools', - files: 'files', - } - - const label = typeLabels[dataType] || dataType + }, + getDynamicText: (params, state) => { + if (params?.blockType && typeof params.blockType === 'string') { + const blockConfig = getLatestBlock(params.blockType) + const blockName = (blockConfig?.name ?? params.blockType.replace(/_/g, ' ')).toLowerCase() + const opSuffix = params.operation ? ` (${params.operation})` : '' switch (state) { case ClientToolCallState.success: - return `Retrieved ${label}` + return `Retrieved ${blockName}${opSuffix} config` case ClientToolCallState.executing: case ClientToolCallState.generating: - return `Fetching ${label}` case ClientToolCallState.pending: - return `Fetch ${label}?` + return `Retrieving ${blockName}${opSuffix} config` case ClientToolCallState.error: - return `Failed to fetch ${label}` + return `Failed to retrieve ${blockName}${opSuffix} config` case ClientToolCallState.aborted: - return `Aborted fetching ${label}` + return `Aborted retrieving ${blockName}${opSuffix} config` case ClientToolCallState.rejected: - return `Skipped fetching ${label}` + return `Skipped retrieving ${blockName}${opSuffix} config` } - return undefined - }, - } + } + return undefined + }, +} -const META_get_workflow_from_name: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Reading workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Reading workflow', icon: FileText }, - [ClientToolCallState.executing]: { text: 'Reading workflow', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted reading workflow', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Read workflow', icon: FileText }, - [ClientToolCallState.error]: { text: 'Failed to read workflow', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped reading workflow', icon: XCircle }, +const META_get_block_options: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting block operations', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting block operations', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting block operations', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved block operations', icon: ListFilter }, + [ClientToolCallState.error]: { text: 'Failed to get block operations', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting block operations', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped getting block operations', + icon: MinusCircle, }, - getDynamicText: (params, state) => { - if (params?.workflow_name && typeof params.workflow_name === 'string') { - const workflowName = params.workflow_name + }, + getDynamicText: (params, state) => { + const blockId = + (params as any)?.blockId || + (params as any)?.blockType || + (params as any)?.block_id || + (params as any)?.block_type + if (typeof blockId === 'string') { + const blockConfig = getLatestBlock(blockId) + const blockName = (blockConfig?.name ?? blockId.replace(/_/g, ' ')).toLowerCase() + switch (state) { + case ClientToolCallState.success: + return `Retrieved ${blockName} operations` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Retrieving ${blockName} operations` + case ClientToolCallState.error: + return `Failed to retrieve ${blockName} operations` + case ClientToolCallState.aborted: + return `Aborted retrieving ${blockName} operations` + case ClientToolCallState.rejected: + return `Skipped retrieving ${blockName} operations` + } + } + return undefined + }, +} + +const META_get_block_outputs: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting block outputs', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting block outputs', icon: Tag }, + [ClientToolCallState.executing]: { text: 'Getting block outputs', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted getting outputs', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Retrieved block outputs', icon: Tag }, + [ClientToolCallState.error]: { text: 'Failed to get outputs', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped getting outputs', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const blockIds = params?.blockIds + if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { + const count = blockIds.length + switch (state) { + case ClientToolCallState.success: + return `Retrieved outputs for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Getting outputs for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.error: + return `Failed to get outputs for ${count} block${count > 1 ? 's' : ''}` + } + } + return undefined + }, +} + +const META_get_block_upstream_references: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting upstream references', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting upstream references', icon: GitBranch }, + [ClientToolCallState.executing]: { text: 'Getting upstream references', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted getting references', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Retrieved upstream references', icon: GitBranch }, + [ClientToolCallState.error]: { text: 'Failed to get references', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped getting references', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const blockIds = params?.blockIds + if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { + const count = blockIds.length + switch (state) { + case ClientToolCallState.success: + return `Retrieved references for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Getting references for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.error: + return `Failed to get references for ${count} block${count > 1 ? 's' : ''}` + } + } + return undefined + }, +} + +const META_get_blocks_and_tools: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Exploring available options', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Exploring available options', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Exploring available options', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Explored available options', icon: Blocks }, + [ClientToolCallState.error]: { text: 'Failed to explore options', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted exploring options', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped exploring options', icon: MinusCircle }, + }, + interrupt: undefined, +} + +const META_get_blocks_metadata: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching block choices', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching block choices', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching block choices', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Searched block choices', icon: ListFilter }, + [ClientToolCallState.error]: { text: 'Failed to search block choices', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted searching block choices', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped searching block choices', + icon: MinusCircle, + }, + }, + getDynamicText: (params, state) => { + if (params?.blockIds && Array.isArray(params.blockIds) && params.blockIds.length > 0) { + const blockList = params.blockIds + .slice(0, 3) + .map((blockId) => blockId.replace(/_/g, ' ')) + .join(', ') + const more = params.blockIds.length > 3 ? '...' : '' + const blocks = `${blockList}${more}` + + switch (state) { + case ClientToolCallState.success: + return `Searched ${blocks}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching ${blocks}` + case ClientToolCallState.error: + return `Failed to search ${blocks}` + case ClientToolCallState.aborted: + return `Aborted searching ${blocks}` + case ClientToolCallState.rejected: + return `Skipped searching ${blocks}` + } + } + return undefined + }, +} + +const META_get_credentials: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching connected integrations', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Fetching connected integrations', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Fetching connected integrations', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Fetched connected integrations', icon: Key }, + [ClientToolCallState.error]: { + text: 'Failed to fetch connected integrations', + icon: XCircle, + }, + [ClientToolCallState.aborted]: { + text: 'Aborted fetching connected integrations', + icon: MinusCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped fetching connected integrations', + icon: MinusCircle, + }, + }, +} + +const META_get_examples_rag: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Fetched examples', icon: Search }, + [ClientToolCallState.error]: { text: 'Failed to fetch examples', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting examples', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting examples', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query + + switch (state) { + case ClientToolCallState.success: + return `Found examples for ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching examples for ${query}` + case ClientToolCallState.error: + return `Failed to find examples for ${query}` + case ClientToolCallState.aborted: + return `Aborted searching examples for ${query}` + case ClientToolCallState.rejected: + return `Skipped searching examples for ${query}` + } + } + return undefined + }, +} + +const META_get_operations_examples: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Designing workflow component', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Designing workflow component', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Designing workflow component', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Designed workflow component', icon: Zap }, + [ClientToolCallState.error]: { text: 'Failed to design workflow component', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted designing workflow component', + icon: MinusCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped designing workflow component', + icon: MinusCircle, + }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query + + switch (state) { + case ClientToolCallState.success: + return `Designed ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Designing ${query}` + case ClientToolCallState.error: + return `Failed to design ${query}` + case ClientToolCallState.aborted: + return `Aborted designing ${query}` + case ClientToolCallState.rejected: + return `Skipped designing ${query}` + } + } + return undefined + }, +} + +const META_get_page_contents: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved page contents', icon: FileText }, + [ClientToolCallState.error]: { text: 'Failed to get page contents', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting page contents', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting page contents', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.urls && Array.isArray(params.urls) && params.urls.length > 0) { + const firstUrl = String(params.urls[0]) + const count = params.urls.length + + switch (state) { + case ClientToolCallState.success: + return count > 1 ? `Retrieved ${count} pages` : `Retrieved ${firstUrl}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return count > 1 ? `Getting ${count} pages` : `Getting ${firstUrl}` + case ClientToolCallState.error: + return count > 1 ? `Failed to get ${count} pages` : `Failed to get ${firstUrl}` + case ClientToolCallState.aborted: + return count > 1 ? `Aborted getting ${count} pages` : `Aborted getting ${firstUrl}` + case ClientToolCallState.rejected: + return count > 1 ? `Skipped getting ${count} pages` : `Skipped getting ${firstUrl}` + } + } + return undefined + }, +} + +const META_get_trigger_blocks: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Found trigger blocks', icon: ListFilter }, + [ClientToolCallState.error]: { text: 'Failed to find trigger blocks', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted finding trigger blocks', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped finding trigger blocks', icon: MinusCircle }, + }, + interrupt: undefined, +} + +const META_get_trigger_examples: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Selected a trigger', icon: Zap }, + [ClientToolCallState.error]: { text: 'Failed to select a trigger', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted selecting a trigger', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped selecting a trigger', icon: MinusCircle }, + }, + interrupt: undefined, +} + +const META_get_user_workflow: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Reading your workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Reading your workflow', icon: WorkflowIcon }, + [ClientToolCallState.executing]: { text: 'Reading your workflow', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted reading your workflow', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Read your workflow', icon: WorkflowIcon }, + [ClientToolCallState.error]: { text: 'Failed to read your workflow', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped reading your workflow', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId + if (workflowId) { + const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name + if (workflowName) { switch (state) { case ClientToolCallState.success: return `Read ${workflowName}` @@ -1004,1232 +936,1348 @@ const META_get_workflow_from_name: ToolMetadata = { return `Skipped reading ${workflowName}` } } - return undefined + } + return undefined + }, +} + +const META_get_workflow_console: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching execution logs', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Fetching execution logs', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Fetched execution logs', icon: TerminalSquare }, + [ClientToolCallState.error]: { text: 'Failed to fetch execution logs', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped fetching execution logs', + icon: MinusCircle, }, - } + [ClientToolCallState.aborted]: { + text: 'Aborted fetching execution logs', + icon: MinusCircle, + }, + [ClientToolCallState.pending]: { text: 'Fetching execution logs', icon: Loader2 }, + }, + getDynamicText: (params, state) => { + const limit = params?.limit + if (limit && typeof limit === 'number') { + const logText = limit === 1 ? 'execution log' : 'execution logs' + + switch (state) { + case ClientToolCallState.success: + return `Fetched last ${limit} ${logText}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Fetching last ${limit} ${logText}` + case ClientToolCallState.error: + return `Failed to fetch last ${limit} ${logText}` + case ClientToolCallState.rejected: + return `Skipped fetching last ${limit} ${logText}` + case ClientToolCallState.aborted: + return `Aborted fetching last ${limit} ${logText}` + } + } + return undefined + }, +} + +const META_get_workflow_data: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching workflow data', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Fetching workflow data', icon: Database }, + [ClientToolCallState.executing]: { text: 'Fetching workflow data', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted fetching data', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Retrieved workflow data', icon: Database }, + [ClientToolCallState.error]: { text: 'Failed to fetch data', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped fetching data', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const dataType = params?.data_type as WorkflowDataType | undefined + if (!dataType) return undefined + + const typeLabels: Record = { + global_variables: 'variables', + custom_tools: 'custom tools', + mcp_tools: 'MCP tools', + files: 'files', + } + + const label = typeLabels[dataType] || dataType + + switch (state) { + case ClientToolCallState.success: + return `Retrieved ${label}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + return `Fetching ${label}` + case ClientToolCallState.pending: + return `Fetch ${label}?` + case ClientToolCallState.error: + return `Failed to fetch ${label}` + case ClientToolCallState.aborted: + return `Aborted fetching ${label}` + case ClientToolCallState.rejected: + return `Skipped fetching ${label}` + } + return undefined + }, +} + +const META_get_workflow_from_name: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Reading workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Reading workflow', icon: FileText }, + [ClientToolCallState.executing]: { text: 'Reading workflow', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted reading workflow', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Read workflow', icon: FileText }, + [ClientToolCallState.error]: { text: 'Failed to read workflow', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped reading workflow', icon: XCircle }, + }, + getDynamicText: (params, state) => { + if (params?.workflow_name && typeof params.workflow_name === 'string') { + const workflowName = params.workflow_name + + switch (state) { + case ClientToolCallState.success: + return `Read ${workflowName}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Reading ${workflowName}` + case ClientToolCallState.error: + return `Failed to read ${workflowName}` + case ClientToolCallState.aborted: + return `Aborted reading ${workflowName}` + case ClientToolCallState.rejected: + return `Skipped reading ${workflowName}` + } + } + return undefined + }, +} const META_info: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting info', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting info', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting info', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved info', icon: Info }, - [ClientToolCallState.error]: { text: 'Failed to get info', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped info', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted info', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting info', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting info', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting info', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved info', icon: Info }, + [ClientToolCallState.error]: { text: 'Failed to get info', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped info', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted info', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Getting info', + completedLabel: 'Info retrieved', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Getting info', - completedLabel: 'Info retrieved', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_knowledge: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Managing knowledge', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Managing knowledge', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Managing knowledge', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed knowledge', icon: BookOpen }, - [ClientToolCallState.error]: { text: 'Failed to manage knowledge', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped knowledge', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted knowledge', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Managing knowledge', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Managing knowledge', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Managing knowledge', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed knowledge', icon: BookOpen }, + [ClientToolCallState.error]: { text: 'Failed to manage knowledge', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped knowledge', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted knowledge', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Managing knowledge', + completedLabel: 'Knowledge managed', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Managing knowledge', - completedLabel: 'Knowledge managed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_knowledge_base: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Accessing knowledge base', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Accessing knowledge base', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Accessing knowledge base', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Accessed knowledge base', icon: Database }, - [ClientToolCallState.error]: { text: 'Failed to access knowledge base', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted knowledge base access', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped knowledge base access', icon: MinusCircle }, - }, - getDynamicText: (params: Record, state: ClientToolCallState) => { - const operation = params?.operation as string | undefined - const name = params?.args?.name as string | undefined + displayNames: { + [ClientToolCallState.generating]: { text: 'Accessing knowledge base', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Accessing knowledge base', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Accessing knowledge base', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Accessed knowledge base', icon: Database }, + [ClientToolCallState.error]: { text: 'Failed to access knowledge base', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted knowledge base access', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped knowledge base access', icon: MinusCircle }, + }, + getDynamicText: (params: Record, state: ClientToolCallState) => { + const operation = params?.operation as string | undefined + const name = params?.args?.name as string | undefined - const opVerbs: Record = { - create: { - active: 'Creating knowledge base', - past: 'Created knowledge base', - pending: name ? `Create knowledge base "${name}"?` : 'Create knowledge base?', - }, - list: { active: 'Listing knowledge bases', past: 'Listed knowledge bases' }, - get: { active: 'Getting knowledge base', past: 'Retrieved knowledge base' }, - query: { active: 'Querying knowledge base', past: 'Queried knowledge base' }, - } - const defaultVerb: { active: string; past: string; pending?: string } = { - active: 'Accessing knowledge base', - past: 'Accessed knowledge base', - } - const verb = operation ? opVerbs[operation] || defaultVerb : defaultVerb + const opVerbs: Record = { + create: { + active: 'Creating knowledge base', + past: 'Created knowledge base', + pending: name ? `Create knowledge base "${name}"?` : 'Create knowledge base?', + }, + list: { active: 'Listing knowledge bases', past: 'Listed knowledge bases' }, + get: { active: 'Getting knowledge base', past: 'Retrieved knowledge base' }, + query: { active: 'Querying knowledge base', past: 'Queried knowledge base' }, + } + const defaultVerb: { active: string; past: string; pending?: string } = { + active: 'Accessing knowledge base', + past: 'Accessed knowledge base', + } + const verb = operation ? opVerbs[operation] || defaultVerb : defaultVerb - if (state === ClientToolCallState.success) { - return verb.past - } - if (state === ClientToolCallState.pending && verb.pending) { - return verb.pending - } - if ( - state === ClientToolCallState.generating || - state === ClientToolCallState.pending || - state === ClientToolCallState.executing - ) { - return verb.active - } - return undefined - }, - } + if (state === ClientToolCallState.success) { + return verb.past + } + if (state === ClientToolCallState.pending && verb.pending) { + return verb.pending + } + if ( + state === ClientToolCallState.generating || + state === ClientToolCallState.pending || + state === ClientToolCallState.executing + ) { + return verb.active + } + return undefined + }, +} const META_list_user_workflows: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Listing your workflows', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Listing your workflows', icon: ListChecks }, - [ClientToolCallState.executing]: { text: 'Listing your workflows', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted listing workflows', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Listed your workflows', icon: ListChecks }, - [ClientToolCallState.error]: { text: 'Failed to list workflows', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped listing workflows', icon: XCircle }, - }, - } + displayNames: { + [ClientToolCallState.generating]: { text: 'Listing your workflows', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Listing your workflows', icon: ListChecks }, + [ClientToolCallState.executing]: { text: 'Listing your workflows', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted listing workflows', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Listed your workflows', icon: ListChecks }, + [ClientToolCallState.error]: { text: 'Failed to list workflows', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped listing workflows', icon: XCircle }, + }, +} const META_list_workspace_mcp_servers: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Getting MCP servers', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Getting MCP servers', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting MCP servers', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved MCP servers', icon: Server }, - [ClientToolCallState.error]: { text: 'Failed to get MCP servers', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting MCP servers', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped getting MCP servers', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Getting MCP servers', + icon: Loader2, }, - interrupt: undefined, - } + [ClientToolCallState.pending]: { text: 'Getting MCP servers', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting MCP servers', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved MCP servers', icon: Server }, + [ClientToolCallState.error]: { text: 'Failed to get MCP servers', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting MCP servers', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting MCP servers', icon: XCircle }, + }, + interrupt: undefined, +} const META_make_api_request: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Preparing API request', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Review API request', icon: Globe2 }, - [ClientToolCallState.executing]: { text: 'Executing API request', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed API request', icon: Globe2 }, - [ClientToolCallState.error]: { text: 'Failed to execute API request', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped API request', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted API request', icon: XCircle }, - }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing API request', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Review API request', icon: Globe2 }, + [ClientToolCallState.executing]: { text: 'Executing API request', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed API request', icon: Globe2 }, + [ClientToolCallState.error]: { text: 'Failed to execute API request', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped API request', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted API request', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Execute', icon: Globe2 }, + reject: { text: 'Skip', icon: MinusCircle }, + }, + uiConfig: { interrupt: { accept: { text: 'Execute', icon: Globe2 }, reject: { text: 'Skip', icon: MinusCircle }, + showAllowOnce: true, + showAllowAlways: true, }, - uiConfig: { - interrupt: { - accept: { text: 'Execute', icon: Globe2 }, - reject: { text: 'Skip', icon: MinusCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - paramsTable: { - columns: [ - { key: 'method', label: 'Method', width: '26%', editable: true, mono: true }, - { key: 'url', label: 'Endpoint', width: '74%', editable: true, mono: true }, - ], - extractRows: (params) => { - return [['request', (params.method || 'GET').toUpperCase(), params.url || '']] - }, + paramsTable: { + columns: [ + { key: 'method', label: 'Method', width: '26%', editable: true, mono: true }, + { key: 'url', label: 'Endpoint', width: '74%', editable: true, mono: true }, + ], + extractRows: (params) => { + return [['request', (params.method || 'GET').toUpperCase(), params.url || '']] }, }, - getDynamicText: (params, state) => { - if (params?.url && typeof params.url === 'string') { - const method = params.method || 'GET' - let url = params.url + }, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const method = params.method || 'GET' + let url = params.url - // Extract domain from URL for cleaner display - try { - const urlObj = new URL(url) - url = urlObj.hostname + urlObj.pathname - } catch { - // Use URL as-is if parsing fails - } - - switch (state) { - case ClientToolCallState.success: - return `${method} ${url} complete` - case ClientToolCallState.executing: - return `${method} ${url}` - case ClientToolCallState.generating: - return `Preparing ${method} ${url}` - case ClientToolCallState.pending: - return `Review ${method} ${url}` - case ClientToolCallState.error: - return `Failed ${method} ${url}` - case ClientToolCallState.rejected: - return `Skipped ${method} ${url}` - case ClientToolCallState.aborted: - return `Aborted ${method} ${url}` - } + // Extract domain from URL for cleaner display + try { + const urlObj = new URL(url) + url = urlObj.hostname + urlObj.pathname + } catch { + // Use URL as-is if parsing fails } - return undefined - }, - } + + switch (state) { + case ClientToolCallState.success: + return `${method} ${url} complete` + case ClientToolCallState.executing: + return `${method} ${url}` + case ClientToolCallState.generating: + return `Preparing ${method} ${url}` + case ClientToolCallState.pending: + return `Review ${method} ${url}` + case ClientToolCallState.error: + return `Failed ${method} ${url}` + case ClientToolCallState.rejected: + return `Skipped ${method} ${url}` + case ClientToolCallState.aborted: + return `Aborted ${method} ${url}` + } + } + return undefined + }, +} const META_manage_custom_tool: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Managing custom tool', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Manage custom tool?', icon: Plus }, - [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Check }, - [ClientToolCallState.error]: { text: 'Failed to manage custom tool', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted managing custom tool', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped managing custom tool', - icon: XCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Managing custom tool', + icon: Loader2, }, - interrupt: { - accept: { text: 'Allow', icon: Check }, - reject: { text: 'Skip', icon: XCircle }, + [ClientToolCallState.pending]: { text: 'Manage custom tool?', icon: Plus }, + [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed to manage custom tool', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted managing custom tool', + icon: XCircle, }, - getDynamicText: (params, state) => { - const operation = params?.operation as 'add' | 'edit' | 'delete' | 'list' | undefined - - if (!operation) return undefined - - let toolName = params?.schema?.function?.name - if (!toolName && params?.toolId) { - try { - const tool = getCustomTool(params.toolId) - toolName = tool?.schema?.function?.name - } catch { - // Ignore errors accessing cache - } - } - - const getActionText = (verb: 'present' | 'past' | 'gerund') => { - switch (operation) { - case 'add': - return verb === 'present' ? 'Create' : verb === 'past' ? 'Created' : 'Creating' - case 'edit': - return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' - case 'delete': - return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' - case 'list': - return verb === 'present' ? 'List' : verb === 'past' ? 'Listed' : 'Listing' - default: - return verb === 'present' ? 'Manage' : verb === 'past' ? 'Managed' : 'Managing' - } - } - - // For add: only show tool name in past tense (success) - // For edit/delete: always show tool name - // For list: never show individual tool name, use plural - const shouldShowToolName = (currentState: ClientToolCallState) => { - if (operation === 'list') return false - if (operation === 'add') { - return currentState === ClientToolCallState.success - } - return true // edit and delete always show tool name - } - - const nameText = - operation === 'list' - ? ' custom tools' - : shouldShowToolName(state) && toolName - ? ` ${toolName}` - : ' custom tool' - - switch (state) { - case ClientToolCallState.success: - return `${getActionText('past')}${nameText}` - case ClientToolCallState.executing: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.generating: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.pending: - return `${getActionText('present')}${nameText}?` - case ClientToolCallState.error: - return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` - case ClientToolCallState.aborted: - return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` - case ClientToolCallState.rejected: - return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` - } - return undefined + [ClientToolCallState.rejected]: { + text: 'Skipped managing custom tool', + icon: XCircle, }, - } + }, + interrupt: { + accept: { text: 'Allow', icon: Check }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const operation = params?.operation as 'add' | 'edit' | 'delete' | 'list' | undefined + + if (!operation) return undefined + + let toolName = params?.schema?.function?.name + if (!toolName && params?.toolId) { + try { + const tool = getCustomTool(params.toolId) + toolName = tool?.schema?.function?.name + } catch { + // Ignore errors accessing cache + } + } + + const getActionText = (verb: 'present' | 'past' | 'gerund') => { + switch (operation) { + case 'add': + return verb === 'present' ? 'Create' : verb === 'past' ? 'Created' : 'Creating' + case 'edit': + return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' + case 'delete': + return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' + case 'list': + return verb === 'present' ? 'List' : verb === 'past' ? 'Listed' : 'Listing' + default: + return verb === 'present' ? 'Manage' : verb === 'past' ? 'Managed' : 'Managing' + } + } + + // For add: only show tool name in past tense (success) + // For edit/delete: always show tool name + // For list: never show individual tool name, use plural + const shouldShowToolName = (currentState: ClientToolCallState) => { + if (operation === 'list') return false + if (operation === 'add') { + return currentState === ClientToolCallState.success + } + return true // edit and delete always show tool name + } + + const nameText = + operation === 'list' + ? ' custom tools' + : shouldShowToolName(state) && toolName + ? ` ${toolName}` + : ' custom tool' + + switch (state) { + case ClientToolCallState.success: + return `${getActionText('past')}${nameText}` + case ClientToolCallState.executing: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.generating: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.pending: + return `${getActionText('present')}${nameText}?` + case ClientToolCallState.error: + return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` + case ClientToolCallState.aborted: + return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` + case ClientToolCallState.rejected: + return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` + } + return undefined + }, +} const META_manage_mcp_tool: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Managing MCP tool', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Manage MCP tool?', icon: Server }, - [ClientToolCallState.executing]: { text: 'Managing MCP tool', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed MCP tool', icon: Check }, - [ClientToolCallState.error]: { text: 'Failed to manage MCP tool', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted managing MCP tool', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped managing MCP tool', - icon: XCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Managing MCP tool', + icon: Loader2, }, - interrupt: { - accept: { text: 'Allow', icon: Check }, - reject: { text: 'Skip', icon: XCircle }, + [ClientToolCallState.pending]: { text: 'Manage MCP tool?', icon: Server }, + [ClientToolCallState.executing]: { text: 'Managing MCP tool', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed MCP tool', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed to manage MCP tool', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted managing MCP tool', + icon: XCircle, }, - getDynamicText: (params, state) => { - const operation = params?.operation as 'add' | 'edit' | 'delete' | undefined - - if (!operation) return undefined - - const serverName = params?.config?.name || params?.serverName - - const getActionText = (verb: 'present' | 'past' | 'gerund') => { - switch (operation) { - case 'add': - return verb === 'present' ? 'Add' : verb === 'past' ? 'Added' : 'Adding' - case 'edit': - return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' - case 'delete': - return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' - } - } - - const shouldShowServerName = (currentState: ClientToolCallState) => { - if (operation === 'add') { - return currentState === ClientToolCallState.success - } - return true - } - - const nameText = shouldShowServerName(state) && serverName ? ` ${serverName}` : ' MCP tool' - - switch (state) { - case ClientToolCallState.success: - return `${getActionText('past')}${nameText}` - case ClientToolCallState.executing: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.generating: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.pending: - return `${getActionText('present')}${nameText}?` - case ClientToolCallState.error: - return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` - case ClientToolCallState.aborted: - return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` - case ClientToolCallState.rejected: - return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` - } - return undefined + [ClientToolCallState.rejected]: { + text: 'Skipped managing MCP tool', + icon: XCircle, }, - } + }, + interrupt: { + accept: { text: 'Allow', icon: Check }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const operation = params?.operation as 'add' | 'edit' | 'delete' | undefined + + if (!operation) return undefined + + const serverName = params?.config?.name || params?.serverName + + const getActionText = (verb: 'present' | 'past' | 'gerund') => { + switch (operation) { + case 'add': + return verb === 'present' ? 'Add' : verb === 'past' ? 'Added' : 'Adding' + case 'edit': + return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' + case 'delete': + return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' + } + } + + const shouldShowServerName = (currentState: ClientToolCallState) => { + if (operation === 'add') { + return currentState === ClientToolCallState.success + } + return true + } + + const nameText = shouldShowServerName(state) && serverName ? ` ${serverName}` : ' MCP tool' + + switch (state) { + case ClientToolCallState.success: + return `${getActionText('past')}${nameText}` + case ClientToolCallState.executing: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.generating: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.pending: + return `${getActionText('present')}${nameText}?` + case ClientToolCallState.error: + return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` + case ClientToolCallState.aborted: + return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` + case ClientToolCallState.rejected: + return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` + } + return undefined + }, +} const META_mark_todo_in_progress: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Marking todo in progress', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Marking todo in progress', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Marking todo in progress', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Marked todo in progress', icon: Loader2 }, - [ClientToolCallState.error]: { text: 'Failed to mark in progress', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted marking in progress', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped marking in progress', icon: MinusCircle }, - }, - } + displayNames: { + [ClientToolCallState.generating]: { text: 'Marking todo in progress', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Marking todo in progress', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Marking todo in progress', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Marked todo in progress', icon: Loader2 }, + [ClientToolCallState.error]: { text: 'Failed to mark in progress', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted marking in progress', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped marking in progress', icon: MinusCircle }, + }, +} const META_navigate_ui: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to open', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Open?', icon: Navigation }, - [ClientToolCallState.executing]: { text: 'Opening', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Opened', icon: Navigation }, - [ClientToolCallState.error]: { text: 'Failed to open', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted opening', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped opening', - icon: XCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to open', + icon: Loader2, }, - interrupt: { - accept: { text: 'Open', icon: Navigation }, - reject: { text: 'Skip', icon: XCircle }, + [ClientToolCallState.pending]: { text: 'Open?', icon: Navigation }, + [ClientToolCallState.executing]: { text: 'Opening', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Opened', icon: Navigation }, + [ClientToolCallState.error]: { text: 'Failed to open', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted opening', + icon: XCircle, }, - getDynamicText: (params, state) => { - const destination = params?.destination as NavigationDestination | undefined - const workflowName = params?.workflowName - - const action = 'open' - const actionCapitalized = 'Open' - const actionPast = 'opened' - const actionIng = 'opening' - let target = '' - - if (destination === 'workflow' && workflowName) { - target = ` workflow "${workflowName}"` - } else if (destination === 'workflow') { - target = ' workflows' - } else if (destination === 'logs') { - target = ' logs' - } else if (destination === 'templates') { - target = ' templates' - } else if (destination === 'vector_db') { - target = ' vector database' - } else if (destination === 'settings') { - target = ' settings' - } - - const fullAction = `${action}${target}` - const fullActionCapitalized = `${actionCapitalized}${target}` - const fullActionPast = `${actionPast}${target}` - const fullActionIng = `${actionIng}${target}` - - switch (state) { - case ClientToolCallState.success: - return fullActionPast.charAt(0).toUpperCase() + fullActionPast.slice(1) - case ClientToolCallState.executing: - return fullActionIng.charAt(0).toUpperCase() + fullActionIng.slice(1) - case ClientToolCallState.generating: - return `Preparing to ${fullAction}` - case ClientToolCallState.pending: - return `${fullActionCapitalized}?` - case ClientToolCallState.error: - return `Failed to ${fullAction}` - case ClientToolCallState.aborted: - return `Aborted ${fullAction}` - case ClientToolCallState.rejected: - return `Skipped ${fullAction}` - } - return undefined + [ClientToolCallState.rejected]: { + text: 'Skipped opening', + icon: XCircle, }, - } + }, + interrupt: { + accept: { text: 'Open', icon: Navigation }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const destination = params?.destination as NavigationDestination | undefined + const workflowName = params?.workflowName + + const action = 'open' + const actionCapitalized = 'Open' + const actionPast = 'opened' + const actionIng = 'opening' + let target = '' + + if (destination === 'workflow' && workflowName) { + target = ` workflow "${workflowName}"` + } else if (destination === 'workflow') { + target = ' workflows' + } else if (destination === 'logs') { + target = ' logs' + } else if (destination === 'templates') { + target = ' templates' + } else if (destination === 'vector_db') { + target = ' vector database' + } else if (destination === 'settings') { + target = ' settings' + } + + const fullAction = `${action}${target}` + const fullActionCapitalized = `${actionCapitalized}${target}` + const fullActionPast = `${actionPast}${target}` + const fullActionIng = `${actionIng}${target}` + + switch (state) { + case ClientToolCallState.success: + return fullActionPast.charAt(0).toUpperCase() + fullActionPast.slice(1) + case ClientToolCallState.executing: + return fullActionIng.charAt(0).toUpperCase() + fullActionIng.slice(1) + case ClientToolCallState.generating: + return `Preparing to ${fullAction}` + case ClientToolCallState.pending: + return `${fullActionCapitalized}?` + case ClientToolCallState.error: + return `Failed to ${fullAction}` + case ClientToolCallState.aborted: + return `Aborted ${fullAction}` + case ClientToolCallState.rejected: + return `Skipped ${fullAction}` + } + return undefined + }, +} const META_oauth_request_access: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Requesting integration access', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Requesting integration access', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Requesting integration access', icon: Loader2 }, - [ClientToolCallState.rejected]: { text: 'Skipped integration access', icon: MinusCircle }, - [ClientToolCallState.success]: { text: 'Requested integration access', icon: CheckCircle }, - [ClientToolCallState.error]: { text: 'Failed to request integration access', icon: X }, - [ClientToolCallState.aborted]: { text: 'Aborted integration access request', icon: XCircle }, - }, - interrupt: { - accept: { text: 'Connect', icon: PlugZap }, - reject: { text: 'Skip', icon: MinusCircle }, - }, - getDynamicText: (params, state) => { - if (params.providerName) { - const name = params.providerName - switch (state) { - case ClientToolCallState.generating: - case ClientToolCallState.pending: - case ClientToolCallState.executing: - return `Requesting ${name} access` - case ClientToolCallState.rejected: - return `Skipped ${name} access` - case ClientToolCallState.success: - return `Requested ${name} access` - case ClientToolCallState.error: - return `Failed to request ${name} access` - case ClientToolCallState.aborted: - return `Aborted ${name} access request` - } + displayNames: { + [ClientToolCallState.generating]: { text: 'Requesting integration access', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Requesting integration access', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Requesting integration access', icon: Loader2 }, + [ClientToolCallState.rejected]: { text: 'Skipped integration access', icon: MinusCircle }, + [ClientToolCallState.success]: { text: 'Requested integration access', icon: CheckCircle }, + [ClientToolCallState.error]: { text: 'Failed to request integration access', icon: X }, + [ClientToolCallState.aborted]: { text: 'Aborted integration access request', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Connect', icon: PlugZap }, + reject: { text: 'Skip', icon: MinusCircle }, + }, + getDynamicText: (params, state) => { + if (params.providerName) { + const name = params.providerName + switch (state) { + case ClientToolCallState.generating: + case ClientToolCallState.pending: + case ClientToolCallState.executing: + return `Requesting ${name} access` + case ClientToolCallState.rejected: + return `Skipped ${name} access` + case ClientToolCallState.success: + return `Requested ${name} access` + case ClientToolCallState.error: + return `Failed to request ${name} access` + case ClientToolCallState.aborted: + return `Aborted ${name} access request` } - return undefined - }, - } + } + return undefined + }, +} const META_plan: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Planning', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Planning', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Planning', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Planned', icon: ListTodo }, - [ClientToolCallState.error]: { text: 'Failed to plan', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped plan', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted plan', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Planning', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Planning', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Planning', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Planned', icon: ListTodo }, + [ClientToolCallState.error]: { text: 'Failed to plan', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped plan', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted plan', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Planning', + completedLabel: 'Planned', + shouldCollapse: true, + outputArtifacts: ['plan'], }, - uiConfig: { - subagent: { - streamingLabel: 'Planning', - completedLabel: 'Planned', - shouldCollapse: true, - outputArtifacts: ['plan'], - }, - }, - } + }, +} const META_redeploy: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Redeploying workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Redeploy workflow', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Redeploying workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Redeployed workflow', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to redeploy workflow', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted redeploy', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped redeploy', icon: XCircle }, - }, - interrupt: undefined, - } + displayNames: { + [ClientToolCallState.generating]: { text: 'Redeploying workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Redeploy workflow', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Redeploying workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Redeployed workflow', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to redeploy workflow', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted redeploy', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped redeploy', icon: XCircle }, + }, + interrupt: undefined, +} const META_remember_debug: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Validated fix', icon: CheckCircle2 }, - [ClientToolCallState.error]: { text: 'Failed to validate', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted validation', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped validation', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - const operation = params?.operation + displayNames: { + [ClientToolCallState.generating]: { text: 'Validating fix', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Validating fix', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Validating fix', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Validated fix', icon: CheckCircle2 }, + [ClientToolCallState.error]: { text: 'Failed to validate', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted validation', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped validation', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + const operation = params?.operation - if (operation === 'add' || operation === 'edit') { - // For add/edit, show from problem or solution - const text = params?.problem || params?.solution - if (text && typeof text === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Validated fix ${text}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Validating fix ${text}` - case ClientToolCallState.error: - return `Failed to validate fix ${text}` - case ClientToolCallState.aborted: - return `Aborted validating fix ${text}` - case ClientToolCallState.rejected: - return `Skipped validating fix ${text}` - } - } - } else if (operation === 'delete') { - // For delete, show from problem or solution (or id as fallback) - const text = params?.problem || params?.solution || params?.id - if (text && typeof text === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Adjusted fix ${text}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Adjusting fix ${text}` - case ClientToolCallState.error: - return `Failed to adjust fix ${text}` - case ClientToolCallState.aborted: - return `Aborted adjusting fix ${text}` - case ClientToolCallState.rejected: - return `Skipped adjusting fix ${text}` - } + if (operation === 'add' || operation === 'edit') { + // For add/edit, show from problem or solution + const text = params?.problem || params?.solution + if (text && typeof text === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Validated fix ${text}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Validating fix ${text}` + case ClientToolCallState.error: + return `Failed to validate fix ${text}` + case ClientToolCallState.aborted: + return `Aborted validating fix ${text}` + case ClientToolCallState.rejected: + return `Skipped validating fix ${text}` } } + } else if (operation === 'delete') { + // For delete, show from problem or solution (or id as fallback) + const text = params?.problem || params?.solution || params?.id + if (text && typeof text === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Adjusted fix ${text}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Adjusting fix ${text}` + case ClientToolCallState.error: + return `Failed to adjust fix ${text}` + case ClientToolCallState.aborted: + return `Aborted adjusting fix ${text}` + case ClientToolCallState.rejected: + return `Skipped adjusting fix ${text}` + } + } + } - return undefined - }, - } + return undefined + }, +} const META_research: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Researching', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Researching', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Researching', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Researched', icon: Search }, - [ClientToolCallState.error]: { text: 'Failed to research', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped research', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted research', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Researching', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Researching', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Researching', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Researched', icon: Search }, + [ClientToolCallState.error]: { text: 'Failed to research', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped research', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted research', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Researching', + completedLabel: 'Researched', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Researching', - completedLabel: 'Researched', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_run_workflow: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Preparing to run your workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Run this workflow?', icon: Play }, - [ClientToolCallState.executing]: { text: 'Running your workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Executed workflow', icon: Play }, - [ClientToolCallState.error]: { text: 'Errored running workflow', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped workflow execution', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted workflow execution', icon: MinusCircle }, - [ClientToolCallState.background]: { text: 'Running in background', icon: Play }, - }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to run your workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Run this workflow?', icon: Play }, + [ClientToolCallState.executing]: { text: 'Running your workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Executed workflow', icon: Play }, + [ClientToolCallState.error]: { text: 'Errored running workflow', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped workflow execution', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted workflow execution', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Running in background', icon: Play }, + }, + interrupt: { + accept: { text: 'Run', icon: Play }, + reject: { text: 'Skip', icon: MinusCircle }, + }, + uiConfig: { + isSpecial: true, interrupt: { accept: { text: 'Run', icon: Play }, reject: { text: 'Skip', icon: MinusCircle }, + showAllowOnce: true, + showAllowAlways: true, }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Run', icon: Play }, - reject: { text: 'Skip', icon: MinusCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - secondaryAction: { - text: 'Move to Background', - title: 'Move to Background', - variant: 'tertiary', - showInStates: [ClientToolCallState.executing], - completionMessage: - 'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete', - targetState: ClientToolCallState.background, - }, - paramsTable: { - columns: [ - { key: 'input', label: 'Input', width: '36%' }, - { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, - ], - extractRows: (params) => { - let inputs = params.input || params.inputs || params.workflow_input - if (typeof inputs === 'string') { - try { - inputs = JSON.parse(inputs) - } catch { - inputs = {} - } - } - if (params.workflow_input && typeof params.workflow_input === 'object') { - inputs = params.workflow_input - } - if (!inputs || typeof inputs !== 'object') { - const { workflowId, workflow_input, ...rest } = params - inputs = rest - } - const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} - return Object.entries(safeInputs).map(([key, value]) => [key, key, String(value)]) - }, - }, + secondaryAction: { + text: 'Move to Background', + title: 'Move to Background', + variant: 'tertiary', + showInStates: [ClientToolCallState.executing], + completionMessage: + 'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete', + targetState: ClientToolCallState.background, }, - getDynamicText: (params, state) => { - const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId - if (workflowId) { - const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name - if (workflowName) { - switch (state) { - case ClientToolCallState.success: - return `Ran ${workflowName}` - case ClientToolCallState.executing: - return `Running ${workflowName}` - case ClientToolCallState.generating: - return `Preparing to run ${workflowName}` - case ClientToolCallState.pending: - return `Run ${workflowName}?` - case ClientToolCallState.error: - return `Failed to run ${workflowName}` - case ClientToolCallState.rejected: - return `Skipped running ${workflowName}` - case ClientToolCallState.aborted: - return `Aborted running ${workflowName}` - case ClientToolCallState.background: - return `Running ${workflowName} in background` + paramsTable: { + columns: [ + { key: 'input', label: 'Input', width: '36%' }, + { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, + ], + extractRows: (params) => { + let inputs = params.input || params.inputs || params.workflow_input + if (typeof inputs === 'string') { + try { + inputs = JSON.parse(inputs) + } catch { + inputs = {} } } - } - return undefined + if (params.workflow_input && typeof params.workflow_input === 'object') { + inputs = params.workflow_input + } + if (!inputs || typeof inputs !== 'object') { + const { workflowId, workflow_input, ...rest } = params + inputs = rest + } + const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} + return Object.entries(safeInputs).map(([key, value]) => [key, key, String(value)]) + }, }, - } + }, + getDynamicText: (params, state) => { + const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId + if (workflowId) { + const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name + if (workflowName) { + switch (state) { + case ClientToolCallState.success: + return `Ran ${workflowName}` + case ClientToolCallState.executing: + return `Running ${workflowName}` + case ClientToolCallState.generating: + return `Preparing to run ${workflowName}` + case ClientToolCallState.pending: + return `Run ${workflowName}?` + case ClientToolCallState.error: + return `Failed to run ${workflowName}` + case ClientToolCallState.rejected: + return `Skipped running ${workflowName}` + case ClientToolCallState.aborted: + return `Aborted running ${workflowName}` + case ClientToolCallState.background: + return `Running ${workflowName} in background` + } + } + } + return undefined + }, +} const META_scrape_page: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Scraping page', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Scraping page', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Scraping page', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Scraped page', icon: Globe }, - [ClientToolCallState.error]: { text: 'Failed to scrape page', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted scraping page', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped scraping page', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.url && typeof params.url === 'string') { - const url = params.url + displayNames: { + [ClientToolCallState.generating]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Scraped page', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to scrape page', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted scraping page', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped scraping page', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const url = params.url - switch (state) { - case ClientToolCallState.success: - return `Scraped ${url}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Scraping ${url}` - case ClientToolCallState.error: - return `Failed to scrape ${url}` - case ClientToolCallState.aborted: - return `Aborted scraping ${url}` - case ClientToolCallState.rejected: - return `Skipped scraping ${url}` - } + switch (state) { + case ClientToolCallState.success: + return `Scraped ${url}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Scraping ${url}` + case ClientToolCallState.error: + return `Failed to scrape ${url}` + case ClientToolCallState.aborted: + return `Aborted scraping ${url}` + case ClientToolCallState.rejected: + return `Skipped scraping ${url}` } - return undefined - }, - } + } + return undefined + }, +} const META_search_documentation: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching documentation', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching documentation', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching documentation', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed documentation search', icon: BookOpen }, - [ClientToolCallState.error]: { text: 'Failed to search docs', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted documentation search', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped documentation search', icon: MinusCircle }, - }, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching documentation', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching documentation', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching documentation', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed documentation search', icon: BookOpen }, + [ClientToolCallState.error]: { text: 'Failed to search docs', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted documentation search', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped documentation search', icon: MinusCircle }, + }, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query - switch (state) { - case ClientToolCallState.success: - return `Searched docs for ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching docs for ${query}` - case ClientToolCallState.error: - return `Failed to search docs for ${query}` - case ClientToolCallState.aborted: - return `Aborted searching docs for ${query}` - case ClientToolCallState.rejected: - return `Skipped searching docs for ${query}` - } + switch (state) { + case ClientToolCallState.success: + return `Searched docs for ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching docs for ${query}` + case ClientToolCallState.error: + return `Failed to search docs for ${query}` + case ClientToolCallState.aborted: + return `Aborted searching docs for ${query}` + case ClientToolCallState.rejected: + return `Skipped searching docs for ${query}` } - return undefined - }, - } + } + return undefined + }, +} const META_search_errors: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, - [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted debugging', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped debugging', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query + displayNames: { + [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, + [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted debugging', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped debugging', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query - switch (state) { - case ClientToolCallState.success: - return `Debugged ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Debugging ${query}` - case ClientToolCallState.error: - return `Failed to debug ${query}` - case ClientToolCallState.aborted: - return `Aborted debugging ${query}` - case ClientToolCallState.rejected: - return `Skipped debugging ${query}` - } + switch (state) { + case ClientToolCallState.success: + return `Debugged ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Debugging ${query}` + case ClientToolCallState.error: + return `Failed to debug ${query}` + case ClientToolCallState.aborted: + return `Aborted debugging ${query}` + case ClientToolCallState.rejected: + return `Skipped debugging ${query}` } - return undefined - }, - } + } + return undefined + }, +} const META_search_library_docs: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Reading docs', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Reading docs', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Reading docs', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Read docs', icon: BookOpen }, - [ClientToolCallState.error]: { text: 'Failed to read docs', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted reading docs', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped reading docs', icon: MinusCircle }, - }, - getDynamicText: (params, state) => { - const libraryName = params?.library_name - if (libraryName && typeof libraryName === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Read ${libraryName} docs` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Reading ${libraryName} docs` - case ClientToolCallState.error: - return `Failed to read ${libraryName} docs` - case ClientToolCallState.aborted: - return `Aborted reading ${libraryName} docs` - case ClientToolCallState.rejected: - return `Skipped reading ${libraryName} docs` - } + displayNames: { + [ClientToolCallState.generating]: { text: 'Reading docs', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Reading docs', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Reading docs', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Read docs', icon: BookOpen }, + [ClientToolCallState.error]: { text: 'Failed to read docs', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted reading docs', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped reading docs', icon: MinusCircle }, + }, + getDynamicText: (params, state) => { + const libraryName = params?.library_name + if (libraryName && typeof libraryName === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Read ${libraryName} docs` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Reading ${libraryName} docs` + case ClientToolCallState.error: + return `Failed to read ${libraryName} docs` + case ClientToolCallState.aborted: + return `Aborted reading ${libraryName} docs` + case ClientToolCallState.rejected: + return `Skipped reading ${libraryName} docs` } - return undefined - }, - } + } + return undefined + }, +} const META_search_online: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching online', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching online', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching online', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed online search', icon: Globe }, - [ClientToolCallState.error]: { text: 'Failed to search online', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped online search', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted online search', icon: XCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching online', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching online', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching online', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed online search', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to search online', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped online search', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted online search', icon: XCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query - switch (state) { - case ClientToolCallState.success: - return `Searched online for ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching online for ${query}` - case ClientToolCallState.error: - return `Failed to search online for ${query}` - case ClientToolCallState.aborted: - return `Aborted searching online for ${query}` - case ClientToolCallState.rejected: - return `Skipped searching online for ${query}` - } + switch (state) { + case ClientToolCallState.success: + return `Searched online for ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching online for ${query}` + case ClientToolCallState.error: + return `Failed to search online for ${query}` + case ClientToolCallState.aborted: + return `Aborted searching online for ${query}` + case ClientToolCallState.rejected: + return `Skipped searching online for ${query}` } - return undefined - }, - } + } + return undefined + }, +} const META_search_patterns: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching workflow patterns', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching workflow patterns', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching workflow patterns', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Found workflow patterns', icon: Search }, - [ClientToolCallState.error]: { text: 'Failed to search patterns', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted pattern search', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped pattern search', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.queries && Array.isArray(params.queries) && params.queries.length > 0) { - const firstQuery = String(params.queries[0]) + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching workflow patterns', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching workflow patterns', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching workflow patterns', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Found workflow patterns', icon: Search }, + [ClientToolCallState.error]: { text: 'Failed to search patterns', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted pattern search', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped pattern search', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.queries && Array.isArray(params.queries) && params.queries.length > 0) { + const firstQuery = String(params.queries[0]) - switch (state) { - case ClientToolCallState.success: - return `Searched ${firstQuery}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching ${firstQuery}` - case ClientToolCallState.error: - return `Failed to search ${firstQuery}` - case ClientToolCallState.aborted: - return `Aborted searching ${firstQuery}` - case ClientToolCallState.rejected: - return `Skipped searching ${firstQuery}` - } + switch (state) { + case ClientToolCallState.success: + return `Searched ${firstQuery}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching ${firstQuery}` + case ClientToolCallState.error: + return `Failed to search ${firstQuery}` + case ClientToolCallState.aborted: + return `Aborted searching ${firstQuery}` + case ClientToolCallState.rejected: + return `Skipped searching ${firstQuery}` } - return undefined - }, - } + } + return undefined + }, +} const META_set_environment_variables: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to set environment variables', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Set environment variables?', icon: Settings2 }, - [ClientToolCallState.executing]: { text: 'Setting environment variables', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Set environment variables', icon: Settings2 }, - [ClientToolCallState.error]: { text: 'Failed to set environment variables', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted setting environment variables', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped setting environment variables', - icon: XCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to set environment variables', + icon: Loader2, }, + [ClientToolCallState.pending]: { text: 'Set environment variables?', icon: Settings2 }, + [ClientToolCallState.executing]: { text: 'Setting environment variables', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Set environment variables', icon: Settings2 }, + [ClientToolCallState.error]: { text: 'Failed to set environment variables', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted setting environment variables', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped setting environment variables', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Apply', icon: Settings2 }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + alwaysExpanded: true, interrupt: { accept: { text: 'Apply', icon: Settings2 }, reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, }, - uiConfig: { - alwaysExpanded: true, - interrupt: { - accept: { text: 'Apply', icon: Settings2 }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - paramsTable: { - columns: [ - { key: 'name', label: 'Variable', width: '36%', editable: true }, - { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, - ], - extractRows: (params) => { - const variables = params.variables || {} - const entries = Array.isArray(variables) - ? variables.map((v: any, i: number) => [String(i), v.name || `var_${i}`, v.value || '']) - : Object.entries(variables).map(([key, val]) => { - if (typeof val === 'object' && val !== null && 'value' in (val as any)) { - return [key, key, (val as any).value] - } - return [key, key, val] - }) - return entries as Array<[string, ...any[]]> - }, + paramsTable: { + columns: [ + { key: 'name', label: 'Variable', width: '36%', editable: true }, + { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, + ], + extractRows: (params) => { + const variables = params.variables || {} + const entries = Array.isArray(variables) + ? variables.map((v: any, i: number) => [String(i), v.name || `var_${i}`, v.value || '']) + : Object.entries(variables).map(([key, val]) => { + if (typeof val === 'object' && val !== null && 'value' in (val as any)) { + return [key, key, (val as any).value] + } + return [key, key, val] + }) + return entries as Array<[string, ...any[]]> }, }, - getDynamicText: (params, state) => { - if (params?.variables && typeof params.variables === 'object') { - const count = Object.keys(params.variables).length - const varText = count === 1 ? 'variable' : 'variables' + }, + getDynamicText: (params, state) => { + if (params?.variables && typeof params.variables === 'object') { + const count = Object.keys(params.variables).length + const varText = count === 1 ? 'variable' : 'variables' - switch (state) { - case ClientToolCallState.success: - return `Set ${count} ${varText}` - case ClientToolCallState.executing: - return `Setting ${count} ${varText}` - case ClientToolCallState.generating: - return `Preparing to set ${count} ${varText}` - case ClientToolCallState.pending: - return `Set ${count} ${varText}?` - case ClientToolCallState.error: - return `Failed to set ${count} ${varText}` - case ClientToolCallState.aborted: - return `Aborted setting ${count} ${varText}` - case ClientToolCallState.rejected: - return `Skipped setting ${count} ${varText}` - } + switch (state) { + case ClientToolCallState.success: + return `Set ${count} ${varText}` + case ClientToolCallState.executing: + return `Setting ${count} ${varText}` + case ClientToolCallState.generating: + return `Preparing to set ${count} ${varText}` + case ClientToolCallState.pending: + return `Set ${count} ${varText}?` + case ClientToolCallState.error: + return `Failed to set ${count} ${varText}` + case ClientToolCallState.aborted: + return `Aborted setting ${count} ${varText}` + case ClientToolCallState.rejected: + return `Skipped setting ${count} ${varText}` } - return undefined - }, - } + } + return undefined + }, +} const META_set_global_workflow_variables: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to set workflow variables', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Set workflow variables?', icon: Settings2 }, - [ClientToolCallState.executing]: { text: 'Setting workflow variables', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Updated workflow variables', icon: Settings2 }, - [ClientToolCallState.error]: { text: 'Failed to set workflow variables', icon: X }, - [ClientToolCallState.aborted]: { text: 'Aborted setting variables', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped setting variables', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to set workflow variables', + icon: Loader2, }, + [ClientToolCallState.pending]: { text: 'Set workflow variables?', icon: Settings2 }, + [ClientToolCallState.executing]: { text: 'Setting workflow variables', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Updated workflow variables', icon: Settings2 }, + [ClientToolCallState.error]: { text: 'Failed to set workflow variables', icon: X }, + [ClientToolCallState.aborted]: { text: 'Aborted setting variables', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped setting variables', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Apply', icon: Settings2 }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { interrupt: { accept: { text: 'Apply', icon: Settings2 }, reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, }, - uiConfig: { - interrupt: { - accept: { text: 'Apply', icon: Settings2 }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - paramsTable: { - columns: [ - { key: 'name', label: 'Name', width: '40%', editable: true, mono: true }, - { key: 'value', label: 'Value', width: '60%', editable: true, mono: true }, - ], - extractRows: (params) => { - const operations = params.operations || [] - return operations.map((op: any, idx: number) => [ - String(idx), - op.name || '', - String(op.value ?? ''), - ]) - }, + paramsTable: { + columns: [ + { key: 'name', label: 'Name', width: '40%', editable: true, mono: true }, + { key: 'value', label: 'Value', width: '60%', editable: true, mono: true }, + ], + extractRows: (params) => { + const operations = params.operations || [] + return operations.map((op: any, idx: number) => [ + String(idx), + op.name || '', + String(op.value ?? ''), + ]) }, }, - getDynamicText: (params, state) => { - if (params?.operations && Array.isArray(params.operations)) { - const varNames = params.operations - .slice(0, 2) - .map((op: any) => op.name) - .filter(Boolean) + }, + getDynamicText: (params, state) => { + if (params?.operations && Array.isArray(params.operations)) { + const varNames = params.operations + .slice(0, 2) + .map((op: any) => op.name) + .filter(Boolean) - if (varNames.length > 0) { - const varList = varNames.join(', ') - const more = params.operations.length > 2 ? '...' : '' - const displayText = `${varList}${more}` + if (varNames.length > 0) { + const varList = varNames.join(', ') + const more = params.operations.length > 2 ? '...' : '' + const displayText = `${varList}${more}` - switch (state) { - case ClientToolCallState.success: - return `Set ${displayText}` - case ClientToolCallState.executing: - return `Setting ${displayText}` - case ClientToolCallState.generating: - return `Preparing to set ${displayText}` - case ClientToolCallState.pending: - return `Set ${displayText}?` - case ClientToolCallState.error: - return `Failed to set ${displayText}` - case ClientToolCallState.aborted: - return `Aborted setting ${displayText}` - case ClientToolCallState.rejected: - return `Skipped setting ${displayText}` - } - } - } - return undefined - }, - } - -const META_sleep: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Preparing to sleep', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Sleeping', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Sleeping', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Finished sleeping', icon: Moon }, - [ClientToolCallState.error]: { text: 'Interrupted sleep', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped sleep', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted sleep', icon: MinusCircle }, - [ClientToolCallState.background]: { text: 'Resumed', icon: Moon }, - }, - uiConfig: { - secondaryAction: { - text: 'Wake', - title: 'Wake', - variant: 'tertiary', - showInStates: [ClientToolCallState.executing], - targetState: ClientToolCallState.background, - }, - }, - // No interrupt - auto-execute immediately - getDynamicText: (params, state) => { - const seconds = params?.seconds - if (typeof seconds === 'number' && seconds > 0) { - const displayTime = formatDuration(seconds) switch (state) { case ClientToolCallState.success: - return `Slept for ${displayTime}` + return `Set ${displayText}` case ClientToolCallState.executing: - case ClientToolCallState.pending: - return `Sleeping for ${displayTime}` + return `Setting ${displayText}` case ClientToolCallState.generating: - return `Preparing to sleep for ${displayTime}` + return `Preparing to set ${displayText}` + case ClientToolCallState.pending: + return `Set ${displayText}?` case ClientToolCallState.error: - return `Failed to sleep for ${displayTime}` - case ClientToolCallState.rejected: - return `Skipped sleeping for ${displayTime}` + return `Failed to set ${displayText}` case ClientToolCallState.aborted: - return `Aborted sleeping for ${displayTime}` - case ClientToolCallState.background: { - // Calculate elapsed time from when sleep started - const elapsedSeconds = params?._elapsedSeconds - if (typeof elapsedSeconds === 'number' && elapsedSeconds > 0) { - return `Resumed after ${formatDuration(Math.round(elapsedSeconds))}` - } - return 'Resumed early' - } + return `Aborted setting ${displayText}` + case ClientToolCallState.rejected: + return `Skipped setting ${displayText}` } } - return undefined + } + return undefined + }, +} + +const META_sleep: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to sleep', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Sleeping', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Sleeping', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Finished sleeping', icon: Moon }, + [ClientToolCallState.error]: { text: 'Interrupted sleep', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped sleep', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted sleep', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Resumed', icon: Moon }, + }, + uiConfig: { + secondaryAction: { + text: 'Wake', + title: 'Wake', + variant: 'tertiary', + showInStates: [ClientToolCallState.executing], + targetState: ClientToolCallState.background, }, - } + }, + // No interrupt - auto-execute immediately + getDynamicText: (params, state) => { + const seconds = params?.seconds + if (typeof seconds === 'number' && seconds > 0) { + const displayTime = formatDuration(seconds) + switch (state) { + case ClientToolCallState.success: + return `Slept for ${displayTime}` + case ClientToolCallState.executing: + case ClientToolCallState.pending: + return `Sleeping for ${displayTime}` + case ClientToolCallState.generating: + return `Preparing to sleep for ${displayTime}` + case ClientToolCallState.error: + return `Failed to sleep for ${displayTime}` + case ClientToolCallState.rejected: + return `Skipped sleeping for ${displayTime}` + case ClientToolCallState.aborted: + return `Aborted sleeping for ${displayTime}` + case ClientToolCallState.background: { + // Calculate elapsed time from when sleep started + const elapsedSeconds = params?._elapsedSeconds + if (typeof elapsedSeconds === 'number' && elapsedSeconds > 0) { + return `Resumed after ${formatDuration(Math.round(elapsedSeconds))}` + } + return 'Resumed early' + } + } + } + return undefined + }, +} const META_summarize_conversation: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Summarizing conversation', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Summarizing conversation', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Summarizing conversation', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Summarized conversation', icon: PencilLine }, - [ClientToolCallState.error]: { text: 'Failed to summarize conversation', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted summarizing conversation', - icon: MinusCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped summarizing conversation', - icon: MinusCircle, - }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Summarizing conversation', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Summarizing conversation', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Summarizing conversation', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Summarized conversation', icon: PencilLine }, + [ClientToolCallState.error]: { text: 'Failed to summarize conversation', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted summarizing conversation', + icon: MinusCircle, }, - interrupt: undefined, - } + [ClientToolCallState.rejected]: { + text: 'Skipped summarizing conversation', + icon: MinusCircle, + }, + }, + interrupt: undefined, +} const META_superagent: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Superagent working', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Superagent working', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Superagent working', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Superagent completed', icon: Sparkles }, - [ClientToolCallState.error]: { text: 'Superagent failed', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Superagent skipped', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Superagent aborted', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Superagent completed', icon: Sparkles }, + [ClientToolCallState.error]: { text: 'Superagent failed', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Superagent skipped', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Superagent aborted', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Superagent working', + completedLabel: 'Superagent completed', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Superagent working', - completedLabel: 'Superagent completed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_test: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Testing', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Testing', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Testing', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Tested', icon: FlaskConical }, - [ClientToolCallState.error]: { text: 'Failed to test', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped test', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted test', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Testing', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Testing', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Testing', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Tested', icon: FlaskConical }, + [ClientToolCallState.error]: { text: 'Failed to test', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped test', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted test', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Testing', + completedLabel: 'Tested', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Testing', - completedLabel: 'Tested', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_tour: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Touring', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Touring', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Touring', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed tour', icon: Compass }, - [ClientToolCallState.error]: { text: 'Failed tour', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped tour', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted tour', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Touring', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Touring', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Touring', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed tour', icon: Compass }, + [ClientToolCallState.error]: { text: 'Failed tour', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped tour', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted tour', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Touring', + completedLabel: 'Tour complete', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Touring', - completedLabel: 'Tour complete', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const META_workflow: ToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Managing workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Managing workflow', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Managing workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed workflow', icon: GitBranch }, - [ClientToolCallState.error]: { text: 'Failed to manage workflow', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped workflow', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted workflow', icon: XCircle }, + displayNames: { + [ClientToolCallState.generating]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed workflow', icon: GitBranch }, + [ClientToolCallState.error]: { text: 'Failed to manage workflow', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped workflow', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted workflow', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Managing workflow', + completedLabel: 'Workflow managed', + shouldCollapse: true, + outputArtifacts: [], }, - uiConfig: { - subagent: { - streamingLabel: 'Managing workflow', - completedLabel: 'Workflow managed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } + }, +} const TOOL_METADATA_BY_ID: Record = { - 'auth': META_auth, - 'check_deployment_status': META_check_deployment_status, - 'checkoff_todo': META_checkoff_todo, - 'crawl_website': META_crawl_website, - 'create_workspace_mcp_server': META_create_workspace_mcp_server, - 'custom_tool': META_custom_tool, - 'debug': META_debug, - 'deploy': META_deploy, - 'deploy_api': META_deploy_api, - 'deploy_chat': META_deploy_chat, - 'deploy_mcp': META_deploy_mcp, - 'edit': META_edit, - 'edit_workflow': META_edit_workflow, - 'evaluate': META_evaluate, - 'get_block_config': META_get_block_config, - 'get_block_options': META_get_block_options, - 'get_block_outputs': META_get_block_outputs, - 'get_block_upstream_references': META_get_block_upstream_references, - 'get_blocks_and_tools': META_get_blocks_and_tools, - 'get_blocks_metadata': META_get_blocks_metadata, - 'get_credentials': META_get_credentials, - 'get_examples_rag': META_get_examples_rag, - 'get_operations_examples': META_get_operations_examples, - 'get_page_contents': META_get_page_contents, - 'get_trigger_blocks': META_get_trigger_blocks, - 'get_trigger_examples': META_get_trigger_examples, - 'get_user_workflow': META_get_user_workflow, - 'get_workflow_console': META_get_workflow_console, - 'get_workflow_data': META_get_workflow_data, - 'get_workflow_from_name': META_get_workflow_from_name, - 'info': META_info, - 'knowledge': META_knowledge, - 'knowledge_base': META_knowledge_base, - 'list_user_workflows': META_list_user_workflows, - 'list_workspace_mcp_servers': META_list_workspace_mcp_servers, - 'make_api_request': META_make_api_request, - 'manage_custom_tool': META_manage_custom_tool, - 'manage_mcp_tool': META_manage_mcp_tool, - 'mark_todo_in_progress': META_mark_todo_in_progress, - 'navigate_ui': META_navigate_ui, - 'oauth_request_access': META_oauth_request_access, - 'plan': META_plan, - 'redeploy': META_redeploy, - 'remember_debug': META_remember_debug, - 'research': META_research, - 'run_workflow': META_run_workflow, - 'scrape_page': META_scrape_page, - 'search_documentation': META_search_documentation, - 'search_errors': META_search_errors, - 'search_library_docs': META_search_library_docs, - 'search_online': META_search_online, - 'search_patterns': META_search_patterns, - 'set_environment_variables': META_set_environment_variables, - 'set_global_workflow_variables': META_set_global_workflow_variables, - 'sleep': META_sleep, - 'summarize_conversation': META_summarize_conversation, - 'superagent': META_superagent, - 'test': META_test, - 'tour': META_tour, - 'workflow': META_workflow, + auth: META_auth, + check_deployment_status: META_check_deployment_status, + checkoff_todo: META_checkoff_todo, + crawl_website: META_crawl_website, + create_workspace_mcp_server: META_create_workspace_mcp_server, + custom_tool: META_custom_tool, + debug: META_debug, + deploy: META_deploy, + deploy_api: META_deploy_api, + deploy_chat: META_deploy_chat, + deploy_mcp: META_deploy_mcp, + edit: META_edit, + edit_workflow: META_edit_workflow, + evaluate: META_evaluate, + get_block_config: META_get_block_config, + get_block_options: META_get_block_options, + get_block_outputs: META_get_block_outputs, + get_block_upstream_references: META_get_block_upstream_references, + get_blocks_and_tools: META_get_blocks_and_tools, + get_blocks_metadata: META_get_blocks_metadata, + get_credentials: META_get_credentials, + get_examples_rag: META_get_examples_rag, + get_operations_examples: META_get_operations_examples, + get_page_contents: META_get_page_contents, + get_trigger_blocks: META_get_trigger_blocks, + get_trigger_examples: META_get_trigger_examples, + get_user_workflow: META_get_user_workflow, + get_workflow_console: META_get_workflow_console, + get_workflow_data: META_get_workflow_data, + get_workflow_from_name: META_get_workflow_from_name, + info: META_info, + knowledge: META_knowledge, + knowledge_base: META_knowledge_base, + list_user_workflows: META_list_user_workflows, + list_workspace_mcp_servers: META_list_workspace_mcp_servers, + make_api_request: META_make_api_request, + manage_custom_tool: META_manage_custom_tool, + manage_mcp_tool: META_manage_mcp_tool, + mark_todo_in_progress: META_mark_todo_in_progress, + navigate_ui: META_navigate_ui, + oauth_request_access: META_oauth_request_access, + plan: META_plan, + redeploy: META_redeploy, + remember_debug: META_remember_debug, + research: META_research, + run_workflow: META_run_workflow, + scrape_page: META_scrape_page, + search_documentation: META_search_documentation, + search_errors: META_search_errors, + search_library_docs: META_search_library_docs, + search_online: META_search_online, + search_patterns: META_search_patterns, + set_environment_variables: META_set_environment_variables, + set_global_workflow_variables: META_set_global_workflow_variables, + sleep: META_sleep, + summarize_conversation: META_summarize_conversation, + superagent: META_superagent, + test: META_test, + tour: META_tour, + workflow: META_workflow, } export const TOOL_DISPLAY_REGISTRY: Record = Object.fromEntries( diff --git a/apps/sim/lib/copilot/types.ts b/apps/sim/lib/copilot/types.ts index 6ed813308..d549bb702 100644 --- a/apps/sim/lib/copilot/types.ts +++ b/apps/sim/lib/copilot/types.ts @@ -1,58 +1,4 @@ -/** - * Copilot Types - Consolidated from various locations - * This file contains all copilot-related type definitions - */ - -// Tool call state types (from apps/sim/types/tool-call.ts) -export interface ToolCallState { - id: string - name: string - displayName?: string - parameters?: Record - state: - | 'detecting' - | 'pending' - | 'executing' - | 'completed' - | 'error' - | 'rejected' - | 'applied' - | 'ready_for_review' - | 'aborted' - | 'skipped' - | 'background' - startTime?: number - endTime?: number - duration?: number - result?: any - error?: string - progress?: string -} - -export interface ToolCallGroup { - id: string - toolCalls: ToolCallState[] - status: 'pending' | 'in_progress' | 'completed' | 'error' - startTime?: number - endTime?: number - summary?: string -} - -export interface InlineContent { - type: 'text' | 'tool_call' - content: string - toolCall?: ToolCallState -} - -export interface ParsedMessageContent { - textContent: string - toolCalls: ToolCallState[] - toolGroups: ToolCallGroup[] - inlineContent?: InlineContent[] -} - import type { ProviderId } from '@/providers/types' -// Copilot Tools Type Definitions (from workspace copilot lib) import type { CopilotToolCall, ToolState } from '@/stores/panel' export type NotificationStatus = @@ -63,82 +9,10 @@ export type NotificationStatus = | 'rejected' | 'background' -// Export the consolidated types export type { CopilotToolCall, ToolState } -// Display configuration for different states -export interface StateDisplayConfig { - displayName: string - icon?: string - className?: string -} - -// Complete display configuration for a tool -export interface ToolDisplayConfig { - states: { - [K in ToolState]?: StateDisplayConfig - } - getDynamicDisplayName?: (state: ToolState, params: Record) => string | null -} - -// Schema for tool parameters (OpenAI function calling format) -export interface ToolSchema { - name: string - description: string - parameters?: { - type: 'object' - properties: Record - required?: string[] - } -} - -// Tool metadata - all the static configuration -export interface ToolMetadata { - id: string - displayConfig: ToolDisplayConfig - schema: ToolSchema - requiresInterrupt: boolean - allowBackgroundExecution?: boolean - stateMessages?: Partial> -} - -// Result from executing a tool -export interface ToolExecuteResult { - success: boolean - data?: any - error?: string -} - -// Response from the confirmation API -export interface ToolConfirmResponse { - success: boolean - message?: string -} - -// Options for tool execution -export interface ToolExecutionOptions { - onStateChange?: (state: ToolState) => void - beforeExecute?: () => Promise - afterExecute?: (result: ToolExecuteResult) => Promise - context?: Record -} - -// The main tool interface that all tools must implement -export interface Tool { - metadata: ToolMetadata - execute(toolCall: CopilotToolCall, options?: ToolExecutionOptions): Promise - getDisplayName(toolCall: CopilotToolCall): string - getIcon(toolCall: CopilotToolCall): string - handleUserAction( - toolCall: CopilotToolCall, - action: 'run' | 'skip' | 'background', - options?: ToolExecutionOptions - ): Promise - requiresConfirmation(toolCall: CopilotToolCall): boolean -} - -// Provider configuration for Sim Agent requests -// This type is only for the `provider` field in requests sent to the Sim Agent +// Provider configuration for Sim Agent requests. +// This type is only for the `provider` field in requests sent to the Sim Agent. export type CopilotProviderConfig = | { provider: 'azure-openai'