From 7153141ee4c6ccaf23fdc04bb4c47e637662b0ec Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 9 Feb 2026 16:29:00 -0800 Subject: [PATCH] Fix run workflow --- .../app/api/workflows/[id]/execute/route.ts | 54 +++-- .../components/tool-call/tool-call.tsx | 12 + .../utils/workflow-execution-utils.ts | 15 ++ apps/sim/lib/copilot/client-sse/handlers.ts | 48 ++++ .../copilot/client-sse/run-tool-execution.ts | 220 +++++++++++++++++ .../copilot/client-sse/subagent-handlers.ts | 11 + .../orchestrator/sse-handlers/handlers.ts | 72 +++++- .../sse-handlers/tool-execution.ts | 31 +++ .../tools/client/tool-display-registry.ts | 223 ++++++++++++++++++ 9 files changed, 670 insertions(+), 16 deletions(-) create mode 100644 apps/sim/lib/copilot/client-sse/run-tool-execution.ts diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 06984a3e2..4564ff8be 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -62,20 +62,23 @@ const ExecuteWorkflowSchema = z.object({ runFromBlock: z .object({ startBlockId: z.string().min(1, 'Start block ID is required'), - sourceSnapshot: z.object({ - blockStates: z.record(z.any()), - executedBlocks: z.array(z.string()), - blockLogs: z.array(z.any()), - decisions: z.object({ - router: z.record(z.string()), - condition: z.record(z.string()), - }), - completedLoops: z.array(z.string()), - loopExecutions: z.record(z.any()).optional(), - parallelExecutions: z.record(z.any()).optional(), - parallelBlockMapping: z.record(z.any()).optional(), - activeExecutionPath: z.array(z.string()), - }), + sourceSnapshot: z + .object({ + blockStates: z.record(z.any()), + executedBlocks: z.array(z.string()), + blockLogs: z.array(z.any()), + decisions: z.object({ + router: z.record(z.string()), + condition: z.record(z.string()), + }), + completedLoops: z.array(z.string()), + loopExecutions: z.record(z.any()).optional(), + parallelExecutions: z.record(z.any()).optional(), + parallelBlockMapping: z.record(z.any()).optional(), + activeExecutionPath: z.array(z.string()), + }) + .optional(), + executionId: z.string().optional(), }) .optional(), }) @@ -269,9 +272,30 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: base64MaxBytes, workflowStateOverride, stopAfterBlockId, - runFromBlock, + runFromBlock: rawRunFromBlock, } = validation.data + // Resolve runFromBlock snapshot from executionId if needed + let runFromBlock = rawRunFromBlock + if (runFromBlock && !runFromBlock.sourceSnapshot && runFromBlock.executionId) { + const { + getExecutionState, + getLatestExecutionState, + } = await import('@/lib/workflows/executor/execution-state') + const snapshot = runFromBlock.executionId === 'latest' + ? await getLatestExecutionState(id) + : await getExecutionState(runFromBlock.executionId) + if (!snapshot) { + return NextResponse.json( + { + error: `No execution state found for ${runFromBlock.executionId === 'latest' ? 'workflow' : `execution ${runFromBlock.executionId}`}. Run the full workflow first.`, + }, + { status: 400 } + ) + } + runFromBlock = { startBlockId: runFromBlock.startBlockId, sourceSnapshot: snapshot } + } + // For API key and internal JWT auth, the entire body is the input (except for our control fields) // For session auth, the input is explicitly provided in the input field const input = diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 98fd19a7e..54fe62fc4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -18,6 +18,10 @@ import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/component import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { getBlock } from '@/blocks/registry' +import { + CLIENT_EXECUTABLE_RUN_TOOLS, + executeRunToolOnClient, +} from '@/lib/copilot/client-sse/run-tool-execution' import type { CopilotToolCall } from '@/stores/panel' import { useCopilotStore } from '@/stores/panel' import type { SubAgentContentBlock } from '@/stores/panel/copilot/types' @@ -1277,6 +1281,14 @@ async function handleRun( setToolCallState(toolCall, 'executing', editedParams ? { params: editedParams } : undefined) onStateChange?.('executing') await sendToolDecision(toolCall.id, 'accepted') + + // Client-executable run tools: execute on the client for real-time feedback + // (block pulsing, console logs, stop button). The server defers execution + // for these tools; the client reports back via mark-complete. + if (CLIENT_EXECUTABLE_RUN_TOOLS.has(toolCall.name)) { + const params = editedParams || toolCall.params || {} + executeRunToolOnClient(toolCall.id, toolCall.name, params) + } } async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts index 0d0597f9a..03eb068b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts @@ -11,6 +11,12 @@ export interface WorkflowExecutionOptions { executionId?: string onBlockComplete?: (blockId: string, output: any) => Promise overrideTriggerType?: 'chat' | 'manual' | 'api' + stopAfterBlockId?: string + /** For run_from_block / run_block: start from a specific block using cached state */ + runFromBlock?: { + startBlockId: string + executionId?: string + } } /** @@ -39,6 +45,15 @@ export async function executeWorkflowWithFullLogging( triggerType: options.overrideTriggerType || 'manual', useDraftState: true, isClientSession: true, + ...(options.stopAfterBlockId ? { stopAfterBlockId: options.stopAfterBlockId } : {}), + ...(options.runFromBlock + ? { + runFromBlock: { + startBlockId: options.runFromBlock.startBlockId, + executionId: options.runFromBlock.executionId || 'latest', + }, + } + : {}), } const response = await fetch(`/api/workflows/${activeWorkflowId}/execute`, { diff --git a/apps/sim/lib/copilot/client-sse/handlers.ts b/apps/sim/lib/copilot/client-sse/handlers.ts index cc294fb60..adbde8f6e 100644 --- a/apps/sim/lib/copilot/client-sse/handlers.ts +++ b/apps/sim/lib/copilot/client-sse/handlers.ts @@ -16,10 +16,15 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { appendTextBlock, beginThinkingBlock, finalizeThinkingBlock } from './content-blocks' +import { + CLIENT_EXECUTABLE_RUN_TOOLS, + executeRunToolOnClient, +} from './run-tool-execution' import type { ClientContentBlock, ClientStreamingContext } from './types' const logger = createLogger('CopilotClientSseHandlers') const TEXT_BLOCK_TYPE = 'text' + const MAX_BATCH_INTERVAL = 50 const MIN_BATCH_INTERVAL = 16 const MAX_QUEUE_SIZE = 5 @@ -408,6 +413,39 @@ export const sseHandlers: Record = { }) } } + + // Generate API key: update deployment status with the new key + if (targetState === ClientToolCallState.success && current.name === 'generate_api_key') { + try { + const resultPayload = asRecord( + data?.result || eventData.result || eventData.data || data?.data + ) + const input = asRecord(current.params) + const workflowId = + (input?.workflowId as string) || useWorkflowRegistry.getState().activeWorkflowId + const apiKey = (resultPayload?.apiKey || resultPayload?.key) as string | undefined + if (workflowId) { + const existingStatus = + useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId) + useWorkflowRegistry + .getState() + .setDeploymentStatus( + workflowId, + existingStatus?.isDeployed ?? false, + existingStatus?.deployedAt, + apiKey + ) + logger.info('[SSE] Updated deployment status with API key', { + workflowId, + hasKey: !!apiKey, + }) + } + } catch (err) { + logger.warn('[SSE] Failed to hydrate API key status', { + error: err instanceof Error ? err.message : String(err), + }) + } + } } for (let i = 0; i < context.contentBlocks.length; i++) { @@ -588,6 +626,16 @@ export const sseHandlers: Record = { sendAutoAcceptConfirmation(id) } + // Client-executable run tools: execute on the client for real-time feedback + // (block pulsing, console logs, stop button). The server defers execution + // for these tools in interactive mode; the client reports back via mark-complete. + if ( + CLIENT_EXECUTABLE_RUN_TOOLS.has(toolName) && + initialState === ClientToolCallState.executing + ) { + executeRunToolOnClient(id, toolName, args || existing?.params || {}) + } + // OAuth: dispatch event to open the OAuth connect modal if (toolName === 'oauth_request_access' && args && typeof window !== 'undefined') { try { diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts new file mode 100644 index 000000000..3836eecac --- /dev/null +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts @@ -0,0 +1,220 @@ +import { createLogger } from '@sim/logger' +import { v4 as uuidv4 } from 'uuid' +import { executeWorkflowWithFullLogging } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils' +import { COPILOT_CONFIRM_API_PATH } from '@/lib/copilot/constants' +import { resolveToolDisplay } from '@/lib/copilot/store-utils' +import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry' +import { useExecutionStore } from '@/stores/execution/store' +import { useCopilotStore } from '@/stores/panel/copilot/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +const logger = createLogger('CopilotRunToolExecution') + +/** + * Run tools that execute client-side for real-time feedback + * (block pulsing, logs, stop button, etc.). + */ +export const CLIENT_EXECUTABLE_RUN_TOOLS = new Set([ + 'run_workflow', + 'run_workflow_until_block', + 'run_from_block', + 'run_block', +]) + +/** + * Execute a run tool on the client side using the streaming execute endpoint. + * This gives full interactive feedback: block pulsing, console logs, stop button. + * + * Mirrors staging's RunWorkflowClientTool.handleAccept(): + * 1. Execute via executeWorkflowWithFullLogging + * 2. Update client tool state directly (success/error) + * 3. Report completion to server via /api/copilot/confirm (Redis), + * where the server-side handler picks it up and tells Go + */ +export function executeRunToolOnClient( + toolCallId: string, + toolName: string, + params: Record +): void { + doExecuteRunTool(toolCallId, toolName, params).catch((err) => { + logger.error('[RunTool] Unhandled error in client-side run tool execution', { + toolCallId, + toolName, + error: err instanceof Error ? err.message : String(err), + }) + }) +} + +async function doExecuteRunTool( + toolCallId: string, + toolName: string, + params: Record +): Promise { + const { isExecuting, setIsExecuting } = useExecutionStore.getState() + + if (isExecuting) { + logger.warn('[RunTool] Execution prevented: already executing', { toolCallId, toolName }) + setToolState(toolCallId, ClientToolCallState.error) + await reportCompletion(toolCallId, false, 'Workflow is already executing. Try again later') + return + } + + const { activeWorkflowId } = useWorkflowRegistry.getState() + if (!activeWorkflowId) { + logger.warn('[RunTool] Execution prevented: no active workflow', { toolCallId, toolName }) + setToolState(toolCallId, ClientToolCallState.error) + await reportCompletion(toolCallId, false, 'No active workflow found') + return + } + + // Extract params for all tool types + const workflowInput = (params.workflow_input || params.input || undefined) as + | Record + | undefined + + const stopAfterBlockId = (() => { + if (toolName === 'run_workflow_until_block') return params.stopAfterBlockId as string | undefined + if (toolName === 'run_block') return params.blockId as string | undefined + return undefined + })() + + const runFromBlock = (() => { + if (toolName === 'run_from_block' && params.startBlockId) { + return { + startBlockId: params.startBlockId as string, + executionId: (params.executionId as string | undefined) || 'latest', + } + } + if (toolName === 'run_block' && params.blockId) { + return { + startBlockId: params.blockId as string, + executionId: (params.executionId as string | undefined) || 'latest', + } + } + return undefined + })() + + setIsExecuting(true) + const executionId = uuidv4() + const executionStartTime = new Date().toISOString() + + logger.info('[RunTool] Starting client-side workflow execution', { + toolCallId, + toolName, + executionId, + activeWorkflowId, + hasInput: !!workflowInput, + stopAfterBlockId, + runFromBlock: runFromBlock ? { startBlockId: runFromBlock.startBlockId } : undefined, + }) + + try { + const result = await executeWorkflowWithFullLogging({ + workflowInput, + executionId, + stopAfterBlockId, + runFromBlock, + }) + + // Determine success (same logic as staging's RunWorkflowClientTool) + let succeeded = true + let errorMessage: string | undefined + try { + if (result && typeof result === 'object' && 'success' in (result as any)) { + succeeded = Boolean((result as any).success) + if (!succeeded) { + errorMessage = (result as any)?.error || (result as any)?.output?.error + } + } else if ( + result && + typeof result === 'object' && + 'execution' in (result as any) && + (result as any).execution + ) { + succeeded = Boolean((result as any).execution.success) + if (!succeeded) { + errorMessage = + (result as any).execution?.error || (result as any).execution?.output?.error + } + } + } catch {} + + if (succeeded) { + logger.info('[RunTool] Workflow execution succeeded', { toolCallId, toolName }) + setToolState(toolCallId, ClientToolCallState.success) + await reportCompletion( + toolCallId, + true, + `Workflow execution completed. Started at: ${executionStartTime}` + ) + } else { + const msg = errorMessage || 'Workflow execution failed' + logger.error('[RunTool] Workflow execution failed', { toolCallId, toolName, error: msg }) + setToolState(toolCallId, ClientToolCallState.error) + await reportCompletion(toolCallId, false, msg) + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + logger.error('[RunTool] Workflow execution threw', { toolCallId, toolName, error: msg }) + setToolState(toolCallId, ClientToolCallState.error) + await reportCompletion(toolCallId, false, msg) + } finally { + setIsExecuting(false) + } +} + +/** Update the tool call state directly in the copilot store (like staging's setState). */ +function setToolState(toolCallId: string, state: ClientToolCallState): void { + try { + const store = useCopilotStore.getState() + const current = store.toolCallsById[toolCallId] + if (!current) return + const updated = { + ...store.toolCallsById, + [toolCallId]: { + ...current, + state, + display: resolveToolDisplay(current.name, state, toolCallId, current.params), + }, + } + useCopilotStore.setState({ toolCallsById: updated }) + } catch (err) { + logger.warn('[RunTool] Failed to update tool state', { + toolCallId, + state, + error: err instanceof Error ? err.message : String(err), + }) + } +} + +/** + * Report tool completion to the server via the existing /api/copilot/confirm endpoint. + * This writes {status: 'success'|'error', message} to Redis. The server-side handler + * is polling Redis via waitForToolCompletion() and will pick this up, then fire-and-forget + * markToolComplete to the Go backend. + */ +async function reportCompletion( + toolCallId: string, + success: boolean, + message?: string +): Promise { + try { + const res = await fetch(COPILOT_CONFIRM_API_PATH, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + toolCallId, + status: success ? 'success' : 'error', + message: message || (success ? 'Tool completed' : 'Tool failed'), + }), + }) + if (!res.ok) { + logger.warn('[RunTool] reportCompletion failed', { toolCallId, status: res.status }) + } + } catch (err) { + logger.error('[RunTool] reportCompletion error', { + toolCallId, + error: err instanceof Error ? err.message : String(err), + }) + } +} diff --git a/apps/sim/lib/copilot/client-sse/subagent-handlers.ts b/apps/sim/lib/copilot/client-sse/subagent-handlers.ts index aa07b21d3..394c11f6d 100644 --- a/apps/sim/lib/copilot/client-sse/subagent-handlers.ts +++ b/apps/sim/lib/copilot/client-sse/subagent-handlers.ts @@ -15,6 +15,10 @@ import { sseHandlers, updateStreamingMessage, } from './handlers' +import { + CLIENT_EXECUTABLE_RUN_TOOLS, + executeRunToolOnClient, +} from './run-tool-execution' import type { ClientStreamingContext } from './types' const logger = createLogger('CopilotClientSubagentHandlers') @@ -245,6 +249,13 @@ export const subAgentSSEHandlers: Record = { if (isAutoAllowed) { sendAutoAcceptConfirmation(id) } + + // Client-executable run tools: execute on the client for real-time feedback. + // The server defers execution in interactive mode; we execute here and + // report back via mark-complete. + if (CLIENT_EXECUTABLE_RUN_TOOLS.has(name)) { + executeRunToolOnClient(id, name, args || {}) + } }, tool_result: (data, context, get, set) => { diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts index 111fe2047..6e0b28cfc 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts @@ -20,10 +20,27 @@ import type { StreamingContext, ToolCallState, } from '@/lib/copilot/orchestrator/types' -import { executeToolAndReport, isInterruptToolName, waitForToolDecision } from './tool-execution' +import { + executeToolAndReport, + isInterruptToolName, + waitForToolCompletion, + waitForToolDecision, +} from './tool-execution' const logger = createLogger('CopilotSseHandlers') +/** + * Run tools that can be executed client-side for real-time feedback + * (block pulsing, logs, stop button). When interactive, the server defers + * execution to the browser client instead of running executeWorkflow directly. + */ +const CLIENT_EXECUTABLE_RUN_TOOLS = new Set([ + 'run_workflow', + 'run_workflow_until_block', + 'run_from_block', + 'run_block', +]) + // Normalization + dedupe helpers live in sse-utils to keep server/client in sync. function inferToolSuccess(data: Record | undefined): { @@ -182,6 +199,35 @@ export const sseHandlers: Record = { options.abortSignal ) if (decision?.status === 'accepted' || decision?.status === 'success') { + // Client-executable run tools: defer execution to the browser client. + // The client calls executeWorkflowWithFullLogging for real-time feedback + // (block pulsing, logs, stop button) and reports completion via + // /api/copilot/confirm with status success/error. We poll Redis for + // that completion signal, then fire-and-forget markToolComplete to Go. + if (CLIENT_EXECUTABLE_RUN_TOOLS.has(toolName)) { + toolCall.status = 'executing' + const completion = await waitForToolCompletion( + toolCallId, + options.timeout || STREAM_TIMEOUT_MS, + options.abortSignal + ) + const success = completion?.status === 'success' + toolCall.status = success ? 'success' : 'error' + toolCall.endTime = Date.now() + const msg = + completion?.message || (success ? 'Tool completed' : 'Tool failed or timed out') + // Fire-and-forget: tell Go backend the tool is done + // (must NOT await — see deadlock note in executeToolAndReport) + markToolComplete(toolCall.id, toolCall.name, success ? 200 : 500, msg).catch((err) => { + logger.error('markToolComplete fire-and-forget failed (run tool)', { + toolCallId: toolCall.id, + toolName: toolCall.name, + error: err instanceof Error ? err.message : String(err), + }) + }) + markToolResultSeen(toolCallId) + return + } await executeToolAndReport(toolCallId, context, execContext, options) return } @@ -435,6 +481,30 @@ export const subAgentHandlers: Record = { } } + // Client-executable run tools in interactive mode: defer to client. + // Same pattern as main handler: wait for client completion, then tell Go. + if (options.interactive === true && CLIENT_EXECUTABLE_RUN_TOOLS.has(toolName)) { + toolCall.status = 'executing' + const completion = await waitForToolCompletion( + toolCallId, + options.timeout || STREAM_TIMEOUT_MS, + options.abortSignal + ) + const success = completion?.status === 'success' + toolCall.status = success ? 'success' : 'error' + toolCall.endTime = Date.now() + const msg = completion?.message || (success ? 'Tool completed' : 'Tool failed or timed out') + markToolComplete(toolCall.id, toolCall.name, success ? 200 : 500, msg).catch((err) => { + logger.error('markToolComplete fire-and-forget failed (subagent run tool)', { + toolCallId: toolCall.id, + toolName: toolCall.name, + error: err instanceof Error ? err.message : String(err), + }) + }) + markToolResultSeen(toolCallId) + return + } + if (options.autoExecuteTools !== false) { await executeToolAndReport(toolCallId, context, execContext, options) } diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts index 80c4c6036..8c48405ad 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts @@ -144,3 +144,34 @@ export async function waitForToolDecision( } return null } + +/** + * Wait for a tool completion signal (success/error) from the client. + * Unlike waitForToolDecision which returns on any status, this ignores + * intermediate statuses like 'accepted'/'rejected'/'background' and only + * returns when the client reports final completion via success/error. + * + * Used for client-executable run tools: the client executes the workflow + * and posts success/error to /api/copilot/confirm when done. The server + * polls here until that completion signal arrives. + */ +export async function waitForToolCompletion( + toolCallId: string, + timeoutMs: number, + abortSignal?: AbortSignal +): Promise<{ status: string; message?: string } | null> { + const start = Date.now() + let interval = TOOL_DECISION_INITIAL_POLL_MS + const maxInterval = TOOL_DECISION_MAX_POLL_MS + while (Date.now() - start < timeoutMs) { + if (abortSignal?.aborted) return null + const decision = await getToolConfirmation(toolCallId) + // Only return on completion statuses, not accept/reject decisions + if (decision?.status === 'success' || decision?.status === 'error') { + return decision + } + await new Promise((resolve) => setTimeout(resolve, interval)) + interval = Math.min(interval * TOOL_DECISION_POLL_BACKOFF, maxInterval) + } + return null +} 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 539414e96..9cfa68075 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -1697,6 +1697,225 @@ const META_research: ToolMetadata = { }, } +const META_generate_api_key: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to generate API key', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Generate API key?', icon: KeyRound }, + [ClientToolCallState.executing]: { text: 'Generating API key', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Generated API key', icon: KeyRound }, + [ClientToolCallState.error]: { text: 'Failed to generate API key', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped generating API key', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted generating API key', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Generate', icon: KeyRound }, + reject: { text: 'Skip', icon: MinusCircle }, + }, + uiConfig: { + interrupt: { + accept: { text: 'Generate', icon: KeyRound }, + reject: { text: 'Skip', icon: MinusCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + }, + getDynamicText: (params, state) => { + const name = params?.name + if (name && typeof name === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Generated API key "${name}"` + case ClientToolCallState.executing: + return `Generating API key "${name}"` + case ClientToolCallState.generating: + return `Preparing to generate "${name}"` + case ClientToolCallState.pending: + return `Generate API key "${name}"?` + case ClientToolCallState.error: + return `Failed to generate "${name}"` + } + } + return undefined + }, +} + +const META_run_block: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to run block', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Run this block?', icon: Play }, + [ClientToolCallState.executing]: { text: 'Running block', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Executed block', icon: Play }, + [ClientToolCallState.error]: { text: 'Failed to run block', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped block execution', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted block execution', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Running block 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, + }, + secondaryAction: { + text: 'Move to Background', + title: 'Move to Background', + variant: 'tertiary', + showInStates: [ClientToolCallState.executing], + completionMessage: + 'The user has chosen to move the block execution to the background. Check back with them later to know when the block execution is complete', + targetState: ClientToolCallState.background, + }, + }, + getDynamicText: (params, state) => { + const blockId = params?.blockId || params?.block_id + if (blockId && typeof blockId === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Executed block ${blockId}` + case ClientToolCallState.executing: + return `Running block ${blockId}` + case ClientToolCallState.generating: + return `Preparing to run block ${blockId}` + case ClientToolCallState.pending: + return `Run block ${blockId}?` + case ClientToolCallState.error: + return `Failed to run block ${blockId}` + case ClientToolCallState.rejected: + return `Skipped running block ${blockId}` + case ClientToolCallState.aborted: + return `Aborted running block ${blockId}` + case ClientToolCallState.background: + return `Running block ${blockId} in background` + } + } + return undefined + }, +} + +const META_run_from_block: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to run from block', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Run from this block?', icon: Play }, + [ClientToolCallState.executing]: { text: 'Running from block', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Executed from block', icon: Play }, + [ClientToolCallState.error]: { text: 'Failed to run from block', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped run from block', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted run from block', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Running from block 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, + }, + 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 blockId = params?.startBlockId || params?.start_block_id + if (blockId && typeof blockId === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Executed from block ${blockId}` + case ClientToolCallState.executing: + return `Running from block ${blockId}` + case ClientToolCallState.generating: + return `Preparing to run from block ${blockId}` + case ClientToolCallState.pending: + return `Run from block ${blockId}?` + case ClientToolCallState.error: + return `Failed to run from block ${blockId}` + case ClientToolCallState.rejected: + return `Skipped running from block ${blockId}` + case ClientToolCallState.aborted: + return `Aborted running from block ${blockId}` + case ClientToolCallState.background: + return `Running from block ${blockId} in background` + } + } + return undefined + }, +} + +const META_run_workflow_until_block: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to run until block', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Run until this block?', icon: Play }, + [ClientToolCallState.executing]: { text: 'Running until block', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Executed until block', icon: Play }, + [ClientToolCallState.error]: { text: 'Failed to run until block', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped run until block', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted run until block', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Running until block 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, + }, + 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 blockId = params?.stopAfterBlockId || params?.stop_after_block_id + if (blockId && typeof blockId === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Executed until block ${blockId}` + case ClientToolCallState.executing: + return `Running until block ${blockId}` + case ClientToolCallState.generating: + return `Preparing to run until block ${blockId}` + case ClientToolCallState.pending: + return `Run until block ${blockId}?` + case ClientToolCallState.error: + return `Failed to run until block ${blockId}` + case ClientToolCallState.rejected: + return `Skipped running until block ${blockId}` + case ClientToolCallState.aborted: + return `Aborted running until block ${blockId}` + case ClientToolCallState.background: + return `Running until block ${blockId} in background` + } + } + return undefined + }, +} + const META_run_workflow: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Preparing to run your workflow', icon: Loader2 }, @@ -2310,6 +2529,7 @@ const TOOL_METADATA_BY_ID: Record = { get_blocks_and_tools: META_get_blocks_and_tools, get_blocks_metadata: META_get_blocks_metadata, get_credentials: META_get_credentials, + generate_api_key: META_generate_api_key, get_examples_rag: META_get_examples_rag, get_operations_examples: META_get_operations_examples, get_page_contents: META_get_page_contents, @@ -2335,7 +2555,10 @@ const TOOL_METADATA_BY_ID: Record = { redeploy: META_redeploy, remember_debug: META_remember_debug, research: META_research, + run_block: META_run_block, + run_from_block: META_run_from_block, run_workflow: META_run_workflow, + run_workflow_until_block: META_run_workflow_until_block, scrape_page: META_scrape_page, search_documentation: META_search_documentation, search_errors: META_search_errors,