Fix run workflow

This commit is contained in:
Siddharth Ganesan
2026-02-09 16:29:00 -08:00
parent 48c9a3a33e
commit 7153141ee4
9 changed files with 670 additions and 16 deletions

View File

@@ -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 =

View File

@@ -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) {

View File

@@ -11,6 +11,12 @@ export interface WorkflowExecutionOptions {
executionId?: string
onBlockComplete?: (blockId: string, output: any) => Promise<void>
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`, {

View File

@@ -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<string, SSEHandler> = {
})
}
}
// 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<string, SSEHandler> = {
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 {

View File

@@ -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<string, unknown>
): 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<string, unknown>
): Promise<void> {
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<string, unknown>
| 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<void> {
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),
})
}
}

View File

@@ -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<string, SSEHandler> = {
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) => {

View File

@@ -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<string, unknown> | undefined): {
@@ -182,6 +199,35 @@ export const sseHandlers: Record<string, SSEHandler> = {
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<string, SSEHandler> = {
}
}
// 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)
}

View File

@@ -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
}

View File

@@ -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<string, ToolMetadata> = {
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<string, ToolMetadata> = {
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,