Files
sim/apps/sim/lib/copilot/client-sse/run-tool-execution.ts
Siddharth Ganesan f733b8dd88 Checkpoint
2026-02-12 11:14:33 -08:00

245 lines
8.2 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { v4 as uuidv4 } from 'uuid'
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 { executeWorkflowWithFullLogging } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils'
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([
'workflow_run',
])
/**
* 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 { 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
}
const { getWorkflowExecution, setIsExecuting } = useExecutionStore.getState()
const { isExecuting } = getWorkflowExecution(activeWorkflowId)
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
}
// Extract params for all tool types
const workflowInput = (params.workflow_input || params.input || undefined) as
| Record<string, unknown>
| undefined
const runMode =
toolName === 'workflow_run' ? ((params.mode as string | undefined) || 'full').toLowerCase() : undefined
if (
toolName === 'workflow_run' &&
runMode !== 'full' &&
runMode !== 'until_block' &&
runMode !== 'from_block' &&
runMode !== 'block'
) {
const error = `Unsupported workflow_run mode: ${String(params.mode)}`
logger.warn('[RunTool] Execution prevented: unsupported workflow_run mode', {
toolCallId,
mode: params.mode,
})
setToolState(toolCallId, ClientToolCallState.error)
await reportCompletion(toolCallId, false, error)
return
}
const stopAfterBlockId = (() => {
if (toolName === 'workflow_run' && runMode === 'until_block') {
return params.stopAfterBlockId as string | undefined
}
if (toolName === 'workflow_run' && runMode === 'block') {
return params.blockId as string | undefined
}
return undefined
})()
const runFromBlock = (() => {
if (toolName === 'workflow_run' && runMode === 'from_block' && params.startBlockId) {
return {
startBlockId: params.startBlockId as string,
executionId: (params.executionId as string | undefined) || 'latest',
}
}
if (toolName === 'workflow_run' && runMode === 'block' && params.blockId) {
return {
startBlockId: params.blockId as string,
executionId: (params.executionId as string | undefined) || 'latest',
}
}
return undefined
})()
setIsExecuting(activeWorkflowId, 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,
overrideTriggerType: 'copilot',
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(activeWorkflowId, 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),
})
}
}