From 76bd405293ce75438d139e930ad283dda903acc3 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 12 Feb 2026 10:22:52 -0800 Subject: [PATCH] Checkpoint --- .../diff-controls/diff-controls.tsx | 1 - .../components/tool-call/tool-call.tsx | 3 +- apps/sim/lib/copilot/client-sse/handlers.ts | 96 +++- .../copilot/client-sse/run-tool-execution.ts | 41 ++ .../copilot/client-sse/subagent-handlers.ts | 51 ++- .../orchestrator/sse-handlers/handlers.ts | 1 + .../copilot/orchestrator/sse-utils.test.ts | 4 +- .../tool-executor/deployment-tools/deploy.ts | 3 +- .../orchestrator/tool-executor/index.ts | 80 +++- .../orchestrator/tool-executor/param-types.ts | 45 ++ .../tools/client/tool-display-registry.ts | 20 +- apps/sim/lib/copilot/tools/mcp/definitions.ts | 54 ++- .../tools/server/blocks/get-block-config.ts | 2 +- apps/sim/lib/copilot/tools/server/router.ts | 2 - .../tools/server/workflow/change-store.ts | 13 + .../server/workflow/edit-workflow/index.ts | 8 +- .../tools/server/workflow/workflow-change.ts | 421 +++++++++++++++++- .../tools/server/workflow/workflow-context.ts | 16 +- apps/sim/stores/panel/copilot/store.ts | 1 - apps/sim/stores/workflow-diff/utils.ts | 3 +- 20 files changed, 783 insertions(+), 82 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx index 03ff15512..35024ec72 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx @@ -15,7 +15,6 @@ const NOTIFICATION_WIDTH = 240 const NOTIFICATION_GAP = 16 function isWorkflowEditToolCall(name?: string, params?: Record): boolean { - if (name === 'edit_workflow') return true if (name !== 'workflow_change') return false const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : '' 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 9587639e3..a606c23e8 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 @@ -65,7 +65,6 @@ function isWorkflowChangeApplyMode(toolCall?: CopilotToolCall): boolean { function isWorkflowEditSummaryTool(toolCall?: CopilotToolCall): boolean { if (!toolCall) return false - if (toolCall.name === 'edit_workflow') return true return isWorkflowChangeApplyMode(toolCall) } @@ -2209,7 +2208,7 @@ export function ToolCall({ ) : null} - {/* Workflow edit summary - shows block changes after edit_workflow/workflow_change(apply) */} + {/* Workflow edit summary - shows block changes after workflow_change(apply) */} {/* Render subagent content as thinking text */} diff --git a/apps/sim/lib/copilot/client-sse/handlers.ts b/apps/sim/lib/copilot/client-sse/handlers.ts index d7f956cc0..0df4660a1 100644 --- a/apps/sim/lib/copilot/client-sse/handlers.ts +++ b/apps/sim/lib/copilot/client-sse/handlers.ts @@ -3,6 +3,7 @@ import { STREAM_STORAGE_KEY } from '@/lib/copilot/constants' import { asRecord } from '@/lib/copilot/orchestrator/sse-utils' import type { SSEEvent } from '@/lib/copilot/orchestrator/types' import { + humanizedFallback, isBackgroundState, isRejectedState, isReviewState, @@ -27,7 +28,6 @@ const MIN_BATCH_INTERVAL = 16 const MAX_QUEUE_SIZE = 5 function isWorkflowEditToolCall(toolName?: string, params?: Record): boolean { - if (toolName === 'edit_workflow') return true if (toolName !== 'workflow_change') return false const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : '' @@ -109,6 +109,45 @@ function extractToolExecutionMetadata( } } +function displayVerb(state: ClientToolCallState): string { + switch (state) { + case ClientToolCallState.success: + return 'Completed' + case ClientToolCallState.error: + return 'Failed' + case ClientToolCallState.rejected: + return 'Skipped' + case ClientToolCallState.aborted: + return 'Aborted' + case ClientToolCallState.generating: + return 'Preparing' + case ClientToolCallState.pending: + return 'Waiting' + default: + return 'Running' + } +} + +function resolveDisplayFromServerUi( + toolName: string, + state: ClientToolCallState, + toolCallId: string, + params: Record | undefined, + ui?: CopilotToolCall['ui'] +) { + const fallback = + resolveToolDisplay(toolName, state, toolCallId, params) || + humanizedFallback(toolName, state) + if (!fallback) return undefined + if (ui?.phaseLabel) { + return { text: ui.phaseLabel, icon: fallback.icon } + } + if (ui?.title) { + return { text: `${displayVerb(state)} ${ui.title}`, icon: fallback.icon } + } + return fallback +} + function isWorkflowChangeApplyCall(toolName?: string, params?: Record): boolean { if (toolName !== 'workflow_change') return false const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : '' @@ -392,11 +431,12 @@ export const sseHandlers: Record = { execution: executionMetadata || current.execution, params: paramsForCurrentToolCall, state: targetState, - display: resolveToolDisplay( + display: resolveDisplayFromServerUi( current.name, targetState, current.id, - paramsForCurrentToolCall + paramsForCurrentToolCall, + uiMetadata || current.ui ), } set({ toolCallsById: updatedMap }) @@ -483,7 +523,8 @@ export const sseHandlers: Record = { (current.name === 'deploy_api' || current.name === 'deploy_chat' || current.name === 'deploy_mcp' || - current.name === 'redeploy') + current.name === 'redeploy' || + current.name === 'workflow_deploy') ) { try { const resultPayload = asRecord( @@ -494,7 +535,19 @@ export const sseHandlers: Record = { (resultPayload?.workflowId as string) || (input?.workflowId as string) || useWorkflowRegistry.getState().activeWorkflowId - const isDeployed = resultPayload?.isDeployed !== false + const deployMode = String(input?.mode || '') + const action = String(input?.action || 'deploy') + const isDeployed = (() => { + if (typeof resultPayload?.isDeployed === 'boolean') return resultPayload.isDeployed + if (current.name !== 'workflow_deploy') return true + if (deployMode === 'status') { + const statusIsDeployed = resultPayload?.isDeployed + return typeof statusIsDeployed === 'boolean' ? statusIsDeployed : true + } + if (deployMode === 'api' || deployMode === 'chat') return action !== 'undeploy' + if (deployMode === 'redeploy' || deployMode === 'mcp') return true + return true + })() if (workflowId) { useWorkflowRegistry .getState() @@ -608,11 +661,12 @@ export const sseHandlers: Record = { ui: uiMetadata || b.toolCall?.ui, execution: executionMetadata || b.toolCall?.execution, state: targetState, - display: resolveToolDisplay( + display: resolveDisplayFromServerUi( b.toolCall?.name, targetState, toolCallId, - paramsForBlock + paramsForBlock, + uiMetadata || b.toolCall?.ui ), }, } @@ -658,7 +712,13 @@ export const sseHandlers: Record = { ui: uiMetadata || current.ui, execution: executionMetadata || current.execution, state: targetState, - display: resolveToolDisplay(current.name, targetState, current.id, current.params), + display: resolveDisplayFromServerUi( + current.name, + targetState, + current.id, + current.params, + uiMetadata || current.ui + ), } set({ toolCallsById: updatedMap }) } @@ -685,11 +745,12 @@ export const sseHandlers: Record = { ui: uiMetadata || b.toolCall?.ui, execution: executionMetadata || b.toolCall?.execution, state: targetState, - display: resolveToolDisplay( + display: resolveDisplayFromServerUi( b.toolCall?.name, targetState, toolCallId, - b.toolCall?.params + b.toolCall?.params, + uiMetadata || b.toolCall?.ui ), }, } @@ -718,13 +779,14 @@ export const sseHandlers: Record = { if (!toolCallsById[toolCallId]) { const initialState = ClientToolCallState.generating + const uiMetadata = extractToolUiMetadata(eventData) const tc: CopilotToolCall = { id: toolCallId, name: toolName, state: initialState, - ui: extractToolUiMetadata(eventData), + ui: uiMetadata, execution: extractToolExecutionMetadata(eventData), - display: resolveToolDisplay(toolName, initialState, toolCallId), + display: resolveDisplayFromServerUi(toolName, initialState, toolCallId, undefined, uiMetadata), } const updated = { ...toolCallsById, [toolCallId]: tc } set({ toolCallsById: updated }) @@ -774,7 +836,13 @@ export const sseHandlers: Record = { ui: uiMetadata || existing.ui, execution: executionMetadata || existing.execution, ...(args ? { params: args } : {}), - display: resolveToolDisplay(toolName, initialState, id, args || existing.params), + display: resolveDisplayFromServerUi( + toolName, + initialState, + id, + args || existing.params, + uiMetadata || existing.ui + ), } : { id, @@ -783,7 +851,7 @@ export const sseHandlers: Record = { ui: uiMetadata, execution: executionMetadata, ...(args ? { params: args } : {}), - display: resolveToolDisplay(toolName, initialState, id, args), + display: resolveDisplayFromServerUi(toolName, initialState, id, args, uiMetadata), } const updated = { ...toolCallsById, [id]: next } set({ toolCallsById: updated }) diff --git a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts index 37ccd80e7..b7d3af0dd 100644 --- a/apps/sim/lib/copilot/client-sse/run-tool-execution.ts +++ b/apps/sim/lib/copilot/client-sse/run-tool-execution.ts @@ -15,6 +15,7 @@ const logger = createLogger('CopilotRunToolExecution') * (block pulsing, logs, stop button, etc.). */ export const CLIENT_EXECUTABLE_RUN_TOOLS = new Set([ + 'workflow_run', 'run_workflow', 'run_workflow_until_block', 'run_from_block', @@ -74,14 +75,54 @@ async function doExecuteRunTool( | Record | 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 === 'run_workflow_until_block') return params.stopAfterBlockId as string | undefined if (toolName === 'run_block') return params.blockId 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', + } + } if (toolName === 'run_from_block' && params.startBlockId) { return { startBlockId: params.startBlockId as string, diff --git a/apps/sim/lib/copilot/client-sse/subagent-handlers.ts b/apps/sim/lib/copilot/client-sse/subagent-handlers.ts index dc49739d9..7c52a2652 100644 --- a/apps/sim/lib/copilot/client-sse/subagent-handlers.ts +++ b/apps/sim/lib/copilot/client-sse/subagent-handlers.ts @@ -6,7 +6,7 @@ import { shouldSkipToolResultEvent, } from '@/lib/copilot/orchestrator/sse-utils' import type { SSEEvent } from '@/lib/copilot/orchestrator/types' -import { resolveToolDisplay } from '@/lib/copilot/store-utils' +import { humanizedFallback, resolveToolDisplay } from '@/lib/copilot/store-utils' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry' import type { CopilotStore, CopilotToolCall } from '@/stores/panel/copilot/types' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' @@ -92,6 +92,45 @@ function extractToolExecutionMetadata( } } +function displayVerb(state: ClientToolCallState): string { + switch (state) { + case ClientToolCallState.success: + return 'Completed' + case ClientToolCallState.error: + return 'Failed' + case ClientToolCallState.rejected: + return 'Skipped' + case ClientToolCallState.aborted: + return 'Aborted' + case ClientToolCallState.generating: + return 'Preparing' + case ClientToolCallState.pending: + return 'Waiting' + default: + return 'Running' + } +} + +function resolveDisplayFromServerUi( + toolName: string, + state: ClientToolCallState, + toolCallId: string, + params: Record | undefined, + ui?: CopilotToolCall['ui'] +) { + const fallback = + resolveToolDisplay(toolName, state, toolCallId, params) || + humanizedFallback(toolName, state) + if (!fallback) return undefined + if (ui?.phaseLabel) { + return { text: ui.phaseLabel, icon: fallback.icon } + } + if (ui?.title) { + return { text: `${displayVerb(state)} ${ui.title}`, icon: fallback.icon } + } + return fallback +} + function isClientRunCapability(toolCall: CopilotToolCall): boolean { if (toolCall.execution?.target === 'sim_client_capability') { return toolCall.execution.capabilityId === 'workflow.run' || !toolCall.execution.capabilityId @@ -329,7 +368,7 @@ export const subAgentSSEHandlers: Record = { ui: uiMetadata, execution: executionMetadata, ...(args ? { params: args } : {}), - display: resolveToolDisplay(name, initialState, id, args), + display: resolveDisplayFromServerUi(name, initialState, id, args, uiMetadata), } if (existingIndex >= 0) { @@ -421,7 +460,13 @@ export const subAgentSSEHandlers: Record = { ui: uiMetadata || existing.ui, execution: executionMetadata || existing.execution, state: targetState, - display: resolveToolDisplay(existing.name, targetState, toolCallId, nextParams), + display: resolveDisplayFromServerUi( + existing.name, + targetState, + toolCallId, + nextParams, + uiMetadata || existing.ui + ), } context.subAgentToolCalls[parentToolCallId][existingIndex] = updatedSubAgentToolCall diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts index 926e3a42f..ad8aaaf41 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts @@ -29,6 +29,7 @@ const logger = createLogger('CopilotSseHandlers') * execution to the browser client instead of running executeWorkflow directly. */ const CLIENT_EXECUTABLE_RUN_TOOLS = new Set([ + 'workflow_run', 'run_workflow', 'run_workflow_until_block', 'run_from_block', diff --git a/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts b/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts index ce41e3270..5a6796b2e 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-utils.test.ts @@ -14,7 +14,7 @@ describe('sse-utils', () => { type: 'tool_result', data: JSON.stringify({ id: 'tool_1', - name: 'edit_workflow', + name: 'workflow_change', success: true, result: { ok: true }, }), @@ -23,7 +23,7 @@ describe('sse-utils', () => { const normalized = normalizeSseEvent(event as any) expect(normalized.toolCallId).toBe('tool_1') - expect(normalized.toolName).toBe('edit_workflow') + expect(normalized.toolName).toBe('workflow_change') expect(normalized.success).toBe(true) expect(normalized.result).toEqual({ ok: true }) }) 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 7e1607f09..ecd1d4702 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 @@ -220,7 +220,8 @@ export async function executeDeployMcp( if (!workflowRecord.isDeployed) { return { success: false, - error: 'Workflow must be deployed before adding as an MCP tool. Use deploy_api first.', + error: + 'Workflow must be deployed before adding as an MCP tool. Use workflow_deploy(mode: "api") first.', } } diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index 5c426abe8..8116a27ed 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -50,6 +50,8 @@ import type { RunWorkflowParams, RunWorkflowUntilBlockParams, SetGlobalWorkflowVariablesParams, + WorkflowDeployParams, + WorkflowRunParams, } from './param-types' import { PLATFORM_ACTIONS_CONTENT } from './platform-actions' import { @@ -318,13 +320,87 @@ async function executeManageCustomTool( } } +async function executeWorkflowRunUnified( + rawParams: Record, + context: ExecutionContext +): Promise { + const params = rawParams as WorkflowRunParams + const mode = params.mode || 'full' + + switch (mode) { + case 'full': + return executeRunWorkflow(params as RunWorkflowParams, context) + case 'until_block': + if (!params.stopAfterBlockId) { + return { success: false, error: 'stopAfterBlockId is required for mode=until_block' } + } + return executeRunWorkflowUntilBlock(params as RunWorkflowUntilBlockParams, context) + case 'from_block': + if (!params.startBlockId) { + return { success: false, error: 'startBlockId is required for mode=from_block' } + } + return executeRunFromBlock(params as RunFromBlockParams, context) + case 'block': + if (!params.blockId) { + return { success: false, error: 'blockId is required for mode=block' } + } + return executeRunBlock(params as RunBlockParams, context) + default: + return { + success: false, + error: `Unsupported workflow_run mode: ${String(mode)}`, + } + } +} + +async function executeWorkflowDeployUnified( + rawParams: Record, + context: ExecutionContext +): Promise { + const params = rawParams as unknown as WorkflowDeployParams + const mode = params.mode + + if (!mode) { + return { success: false, error: 'mode is required for workflow_deploy' } + } + + const scopedContext = + params.workflowId && params.workflowId !== context.workflowId + ? { ...context, workflowId: params.workflowId } + : context + + switch (mode) { + case 'status': + return executeCheckDeploymentStatus(params as CheckDeploymentStatusParams, scopedContext) + case 'redeploy': + return executeRedeploy(scopedContext) + case 'api': + return executeDeployApi(params as DeployApiParams, scopedContext) + case 'chat': + return executeDeployChat(params as DeployChatParams, scopedContext) + case 'mcp': + return executeDeployMcp(params as DeployMcpParams, scopedContext) + case 'list_mcp_servers': + return executeListWorkspaceMcpServers(params as ListWorkspaceMcpServersParams, scopedContext) + case 'create_mcp_server': + return executeCreateWorkspaceMcpServer( + params as CreateWorkspaceMcpServerParams, + scopedContext + ) + default: + return { + success: false, + error: `Unsupported workflow_deploy mode: ${String(mode)}`, + } + } +} + const SERVER_TOOLS = new Set([ 'get_blocks_and_tools', 'get_blocks_metadata', 'get_block_options', 'get_block_config', 'get_trigger_blocks', - 'edit_workflow', 'workflow_context_get', 'workflow_context_expand', 'workflow_change', @@ -356,6 +432,7 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record< get_block_outputs: (p, c) => executeGetBlockOutputs(p as GetBlockOutputsParams, c), get_block_upstream_references: (p, c) => executeGetBlockUpstreamReferences(p as unknown as GetBlockUpstreamReferencesParams, c), + workflow_run: (p, c) => executeWorkflowRunUnified(p, c), run_workflow: (p, c) => executeRunWorkflow(p as RunWorkflowParams, c), run_workflow_until_block: (p, c) => executeRunWorkflowUntilBlock(p as unknown as RunWorkflowUntilBlockParams, c), @@ -371,6 +448,7 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record< }), set_global_workflow_variables: (p, c) => executeSetGlobalWorkflowVariables(p as SetGlobalWorkflowVariablesParams, c), + workflow_deploy: (p, c) => executeWorkflowDeployUnified(p, c), deploy_api: (p, c) => executeDeployApi(p as DeployApiParams, c), deploy_chat: (p, c) => executeDeployChat(p as DeployChatParams, c), deploy_mcp: (p, c) => executeDeployMcp(p as DeployMcpParams, c), diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts index 1f49ab616..1967d506a 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts @@ -93,6 +93,18 @@ export interface RunBlockParams { useDeployedState?: boolean } +export interface WorkflowRunParams { + mode?: 'full' | 'until_block' | 'from_block' | 'block' + workflowId?: string + workflow_input?: unknown + input?: unknown + useDeployedState?: boolean + stopAfterBlockId?: string + startBlockId?: string + blockId?: string + executionId?: string +} + export interface GetDeployedWorkflowStateParams { workflowId?: string } @@ -169,6 +181,39 @@ export interface CreateWorkspaceMcpServerParams { workflowIds?: string[] } +export interface WorkflowDeployParams { + mode: + | 'status' + | 'redeploy' + | 'api' + | 'chat' + | 'mcp' + | 'list_mcp_servers' + | 'create_mcp_server' + workflowId?: string + action?: 'deploy' | 'undeploy' + identifier?: string + title?: string + description?: string + customizations?: { + primaryColor?: string + secondaryColor?: string + welcomeMessage?: string + iconUrl?: string + } + authType?: 'none' | 'password' | 'public' | 'email' | 'sso' + password?: string + allowedEmails?: string[] + outputConfigs?: unknown[] + serverId?: string + toolName?: string + toolDescription?: string + parameterSchema?: Record + name?: string + isPublic?: boolean + workflowIds?: string[] +} + // === Workflow Organization Params === export interface RenameWorkflowParams { 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 574bb2693..9790e5e97 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -592,23 +592,6 @@ const META_edit: ToolMetadata = { }, } -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', - }, -} - const META_workflow_change: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Planning workflow changes', icon: Loader2 }, @@ -2618,11 +2601,12 @@ const TOOL_METADATA_BY_ID: Record = { deploy_chat: META_deploy_chat, deploy_mcp: META_deploy_mcp, edit: META_edit, - edit_workflow: META_edit_workflow, workflow_context_get: META_workflow_context_get, workflow_context_expand: META_workflow_context_expand, workflow_change: META_workflow_change, workflow_verify: META_workflow_verify, + workflow_run: META_run_workflow, + workflow_deploy: META_deploy_api, evaluate: META_evaluate, get_block_config: META_get_block_config, get_block_options: META_get_block_options, diff --git a/apps/sim/lib/copilot/tools/mcp/definitions.ts b/apps/sim/lib/copilot/tools/mcp/definitions.ts index 0dc26951b..ac118f8f0 100644 --- a/apps/sim/lib/copilot/tools/mcp/definitions.ts +++ b/apps/sim/lib/copilot/tools/mcp/definitions.ts @@ -190,6 +190,52 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ required: ['folderId'], }, }, + { + name: 'workflow_run', + toolId: 'workflow_run', + description: + 'Run a workflow using one unified interface. Supports full runs and partial execution modes.', + inputSchema: { + type: 'object', + properties: { + workflowId: { + type: 'string', + description: 'REQUIRED. The workflow ID to run.', + }, + mode: { + type: 'string', + description: 'Execution mode: full, until_block, from_block, or block. Default: full.', + enum: ['full', 'until_block', 'from_block', 'block'], + }, + workflow_input: { + type: 'object', + description: + 'JSON object with input values. Keys should match workflow start block input names.', + }, + stopAfterBlockId: { + type: 'string', + description: 'Required when mode is until_block.', + }, + startBlockId: { + type: 'string', + description: 'Required when mode is from_block.', + }, + blockId: { + type: 'string', + description: 'Required when mode is block.', + }, + executionId: { + type: 'string', + description: 'Optional execution snapshot ID for from_block or block modes.', + }, + useDeployedState: { + type: 'boolean', + description: 'When true, runs deployed state instead of draft. Default: false.', + }, + }, + required: ['workflowId'], + }, + }, { name: 'run_workflow', toolId: 'run_workflow', @@ -531,10 +577,10 @@ ALSO CAN: description: `Run a workflow and verify its outputs. Works on both deployed and undeployed (draft) workflows. Use after building to verify correctness. Supports full and partial execution: -- Full run with test inputs -- Stop after a specific block (run_workflow_until_block) -- Run a single block in isolation (run_block) -- Resume from a specific block (run_from_block)`, +- Full run with test inputs using workflow_run mode "full" +- Stop after a specific block using workflow_run mode "until_block" +- Run a single block in isolation using workflow_run mode "block" +- Resume from a specific block using workflow_run mode "from_block"`, inputSchema: { type: 'object', properties: { diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts b/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts index 64021e07c..e0d55ec3a 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts @@ -109,7 +109,7 @@ function resolveSubBlockOptions(sb: SubBlockConfig): string[] | undefined { return undefined } - // Return the actual option ID/value that edit_workflow expects, not the display label + // Return canonical option IDs/values expected by workflow_change compilation and apply return rawOptions .map((opt: any) => { if (!opt) return undefined diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index a3f3dc780..851ae2f4d 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -11,7 +11,6 @@ import { makeApiRequestServerTool } from '@/lib/copilot/tools/server/other/make- import { searchOnlineServerTool } from '@/lib/copilot/tools/server/other/search-online' import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials' import { setEnvironmentVariablesServerTool } from '@/lib/copilot/tools/server/user/set-environment-variables' -import { editWorkflowServerTool } from '@/lib/copilot/tools/server/workflow/edit-workflow' import { getWorkflowConsoleServerTool } from '@/lib/copilot/tools/server/workflow/get-workflow-console' import { workflowChangeServerTool } from '@/lib/copilot/tools/server/workflow/workflow-change' import { @@ -33,7 +32,6 @@ const serverToolRegistry: Record = { [getBlockOptionsServerTool.name]: getBlockOptionsServerTool, [getBlockConfigServerTool.name]: getBlockConfigServerTool, [getTriggerBlocksServerTool.name]: getTriggerBlocksServerTool, - [editWorkflowServerTool.name]: editWorkflowServerTool, [getWorkflowConsoleServerTool.name]: getWorkflowConsoleServerTool, [searchDocumentationServerTool.name]: searchDocumentationServerTool, [searchOnlineServerTool.name]: searchOnlineServerTool, diff --git a/apps/sim/lib/copilot/tools/server/workflow/change-store.ts b/apps/sim/lib/copilot/tools/server/workflow/change-store.ts index f3288a83e..dcb316f11 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/change-store.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/change-store.ts @@ -71,6 +71,19 @@ export type WorkflowChangeProposal = { warnings: string[] diagnostics: string[] touchedBlocks: string[] + acceptanceAssertions: string[] + postApply?: { + verify?: boolean + run?: Record + evaluator?: Record + } + handoff?: { + objective?: string + constraints?: string[] + resolvedIds?: Record + assumptions?: string[] + unresolvedRisks?: string[] + } } const contextPackStore = new TTLStore() diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts index 6b8592f79..0eeb6161a 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts @@ -68,8 +68,8 @@ async function getCurrentWorkflowStateFromDb( return { workflowState, subBlockValues } } -export const editWorkflowServerTool: BaseServerTool = { - name: 'edit_workflow', +export const applyWorkflowOperationsServerTool: BaseServerTool = { + name: '__internal_apply_workflow_operations', async execute(params: EditWorkflowParams, context?: { userId: string }): Promise { const logger = createLogger('EditWorkflowServerTool') const { operations, workflowId, currentUserWorkflow } = params @@ -90,7 +90,7 @@ export const editWorkflowServerTool: BaseServerTool throw new Error(authorization.message || 'Unauthorized workflow access') } - logger.info('Executing edit_workflow', { + logger.info('Executing internal workflow operation apply', { operationCount: operations.length, workflowId, hasCurrentUserWorkflow: !!currentUserWorkflow, @@ -210,7 +210,7 @@ export const editWorkflowServerTool: BaseServerTool logger.warn('No userId in context - skipping custom tools persistence', { workflowId }) } - logger.info('edit_workflow successfully applied operations', { + logger.info('Internal workflow operation apply succeeded', { operationCount: operations.length, blocksCount: Object.keys(modifiedWorkflowState.blocks).length, edgesCount: modifiedWorkflowState.edges.length, diff --git a/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts b/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts index 07d09b408..f27dbf92d 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts @@ -2,6 +2,13 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' import { z } from 'zod' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types' +import { + executeRunBlock, + executeRunFromBlock, + executeRunWorkflow, + executeRunWorkflowUntilBlock, +} from '@/lib/copilot/orchestrator/tool-executor/workflow-tools' import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { getBlock } from '@/blocks/registry' @@ -12,9 +19,10 @@ import { saveProposal, type WorkflowChangeProposal, } from './change-store' -import { editWorkflowServerTool } from './edit-workflow' +import { applyWorkflowOperationsServerTool } from './edit-workflow' import { applyOperationsToWorkflowState } from './edit-workflow/engine' import { preValidateCredentialInputs } from './edit-workflow/validation' +import { workflowVerifyServerTool } from './workflow-verify' import { hashWorkflowState, loadWorkflowStateFromDb } from './workflow-state' const logger = createLogger('WorkflowChangeServerTool') @@ -31,6 +39,9 @@ const TargetSchema = z .optional(), }) .strict() + .refine((target) => Boolean(target.blockId || target.alias || target.match), { + message: 'target must include blockId, alias, or match', + }) const CredentialSelectionSchema = z .object({ @@ -51,33 +62,63 @@ const ChangeOperationSchema = z }) .strict() -const MutationSchema = z +const EnsureBlockMutationSchema = z .object({ - action: z.enum([ - 'ensure_block', - 'patch_block', - 'remove_block', - 'connect', - 'disconnect', - 'ensure_variable', - 'set_variable', - ]), - target: TargetSchema.optional(), + action: z.literal('ensure_block'), + target: TargetSchema, type: z.string().optional(), name: z.string().optional(), inputs: z.record(z.any()).optional(), triggerMode: z.boolean().optional(), advancedMode: z.boolean().optional(), enabled: z.boolean().optional(), - changes: z.array(ChangeOperationSchema).optional(), - from: TargetSchema.optional(), - to: TargetSchema.optional(), + }) + .strict() + +const PatchBlockMutationSchema = z + .object({ + action: z.literal('patch_block'), + target: TargetSchema, + changes: z.array(ChangeOperationSchema).min(1), + }) + .strict() + +const RemoveBlockMutationSchema = z + .object({ + action: z.literal('remove_block'), + target: TargetSchema, + }) + .strict() + +const ConnectMutationSchema = z + .object({ + action: z.literal('connect'), + from: TargetSchema, + to: TargetSchema, handle: z.string().optional(), toHandle: z.string().optional(), mode: z.enum(['set', 'append', 'remove']).optional(), }) .strict() +const DisconnectMutationSchema = z + .object({ + action: z.literal('disconnect'), + from: TargetSchema, + to: TargetSchema, + handle: z.string().optional(), + toHandle: z.string().optional(), + }) + .strict() + +const MutationSchema = z.discriminatedUnion('action', [ + EnsureBlockMutationSchema, + PatchBlockMutationSchema, + RemoveBlockMutationSchema, + ConnectMutationSchema, + DisconnectMutationSchema, +]) + const LinkEndpointSchema = z .object({ blockId: z.string().optional(), @@ -100,16 +141,64 @@ const LinkSchema = z }) .strict() +const PostApplyRunSchema = z + .object({ + enabled: z.boolean().optional(), + mode: z.enum(['full', 'until_block', 'from_block', 'block']).optional(), + useDeployedState: z.boolean().optional(), + workflowInput: z.record(z.any()).optional(), + stopAfterBlockId: z.string().optional(), + startBlockId: z.string().optional(), + blockId: z.string().optional(), + }) + .strict() + +const PostApplyEvaluatorSchema = z + .object({ + enabled: z.boolean().optional(), + requireVerified: z.boolean().optional(), + maxWarnings: z.number().int().min(0).optional(), + maxDiagnostics: z.number().int().min(0).optional(), + requireRunSuccess: z.boolean().optional(), + }) + .strict() + +const PostApplySchema = z + .object({ + verify: z.boolean().optional(), + run: PostApplyRunSchema.optional(), + evaluator: PostApplyEvaluatorSchema.optional(), + }) + .strict() + +const AcceptanceItemSchema = z.union([ + z.string(), + z + .object({ + kind: z.string().optional(), + assert: z.string(), + }) + .strict(), +]) + const ChangeSpecSchema = z .object({ + version: z.literal('1').optional(), objective: z.string().optional(), - constraints: z.record(z.any()).optional(), + constraints: z.array(z.string()).optional(), + assumptions: z.array(z.string()).optional(), + unresolvedRisks: z.array(z.string()).optional(), + resolvedIds: z.record(z.string()).optional(), resources: z.record(z.any()).optional(), mutations: z.array(MutationSchema).optional(), links: z.array(LinkSchema).optional(), - acceptance: z.array(z.any()).optional(), + acceptance: z.array(AcceptanceItemSchema).optional(), + postApply: PostApplySchema.optional(), }) .strict() + .refine((spec) => Boolean((spec.mutations && spec.mutations.length > 0) || (spec.links && spec.links.length > 0)), { + message: 'changeSpec must include at least one mutation or link', + }) const WorkflowChangeInputSchema = z .object({ @@ -120,13 +209,69 @@ const WorkflowChangeInputSchema = z baseSnapshotHash: z.string().optional(), expectedSnapshotHash: z.string().optional(), changeSpec: ChangeSpecSchema.optional(), + postApply: PostApplySchema.optional(), }) .strict() + .superRefine((value, ctx) => { + if (value.mode === 'dry_run') { + if (!value.workflowId && !value.contextPackId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['workflowId'], + message: 'workflowId is required for dry_run when contextPackId is not provided', + }) + } + if (!value.changeSpec) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['changeSpec'], + message: 'changeSpec is required for dry_run', + }) + } + return + } + + if (!value.proposalId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['proposalId'], + message: 'proposalId is required for apply', + }) + } + if (!value.expectedSnapshotHash) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['expectedSnapshotHash'], + message: 'expectedSnapshotHash is required for apply', + }) + } + }) type WorkflowChangeParams = z.input type ChangeSpec = z.input type TargetRef = z.input type ChangeOperation = z.input +type PostApply = z.input + +type NormalizedPostApply = { + verify: boolean + run: { + enabled: boolean + mode: 'full' | 'until_block' | 'from_block' | 'block' + useDeployedState: boolean + workflowInput?: Record + stopAfterBlockId?: string + startBlockId?: string + blockId?: string + } + evaluator: { + enabled: boolean + requireVerified: boolean + maxWarnings: number + maxDiagnostics: number + requireRunSuccess: boolean + } +} type CredentialRecord = { id: string @@ -162,6 +307,173 @@ function stableUnique(values: string[]): string[] { return [...new Set(values.filter(Boolean))] } +function normalizeAcceptance(assertions: ChangeSpec['acceptance'] | undefined): string[] { + if (!Array.isArray(assertions)) return [] + return assertions + .map((item) => (typeof item === 'string' ? item : item?.assert)) + .filter((item): item is string => typeof item === 'string' && item.trim().length > 0) +} + +function normalizePostApply(postApply?: PostApply): NormalizedPostApply { + const run = postApply?.run + const evaluator = postApply?.evaluator + + return { + verify: postApply?.verify !== false, + run: { + enabled: run?.enabled === true, + mode: run?.mode || 'full', + useDeployedState: run?.useDeployedState === true, + workflowInput: + run?.workflowInput && typeof run.workflowInput === 'object' + ? (run.workflowInput as Record) + : undefined, + stopAfterBlockId: run?.stopAfterBlockId, + startBlockId: run?.startBlockId, + blockId: run?.blockId, + }, + evaluator: { + enabled: evaluator?.enabled !== false, + requireVerified: evaluator?.requireVerified !== false, + maxWarnings: + typeof evaluator?.maxWarnings === 'number' && evaluator.maxWarnings >= 0 + ? evaluator.maxWarnings + : 50, + maxDiagnostics: + typeof evaluator?.maxDiagnostics === 'number' && evaluator.maxDiagnostics >= 0 + ? evaluator.maxDiagnostics + : 0, + requireRunSuccess: evaluator?.requireRunSuccess === true, + }, + } +} + +async function executePostApplyRun(params: { + workflowId: string + userId: string + run: NormalizedPostApply['run'] +}): Promise { + const context: ExecutionContext = { + userId: params.userId, + workflowId: params.workflowId, + } + + switch (params.run.mode) { + case 'until_block': { + if (!params.run.stopAfterBlockId) { + return { + success: false, + error: 'postApply.run.stopAfterBlockId is required for mode "until_block"', + } + } + return executeRunWorkflowUntilBlock( + { + workflowId: params.workflowId, + stopAfterBlockId: params.run.stopAfterBlockId, + useDeployedState: params.run.useDeployedState, + workflow_input: params.run.workflowInput, + }, + context + ) + } + case 'from_block': { + if (!params.run.startBlockId) { + return { + success: false, + error: 'postApply.run.startBlockId is required for mode "from_block"', + } + } + return executeRunFromBlock( + { + workflowId: params.workflowId, + startBlockId: params.run.startBlockId, + useDeployedState: params.run.useDeployedState, + workflow_input: params.run.workflowInput, + }, + context + ) + } + case 'block': { + if (!params.run.blockId) { + return { + success: false, + error: 'postApply.run.blockId is required for mode "block"', + } + } + return executeRunBlock( + { + workflowId: params.workflowId, + blockId: params.run.blockId, + useDeployedState: params.run.useDeployedState, + workflow_input: params.run.workflowInput, + }, + context + ) + } + default: + return executeRunWorkflow( + { + workflowId: params.workflowId, + useDeployedState: params.run.useDeployedState, + workflow_input: params.run.workflowInput, + }, + context + ) + } +} + +function evaluatePostApplyGate(params: { + verifyEnabled: boolean + verifyResult: any | null + runEnabled: boolean + runResult: ToolCallResult | null + evaluator: NormalizedPostApply['evaluator'] + warnings: string[] + diagnostics: string[] +}): { + passed: boolean + reasons: string[] + summary: string +} { + if (!params.evaluator.enabled) { + return { + passed: true, + reasons: [], + summary: 'Evaluator gate disabled', + } + } + + const reasons: string[] = [] + + if (params.verifyEnabled && params.evaluator.requireVerified) { + const verified = params.verifyResult?.verified === true + if (!verified) { + reasons.push('verification_failed') + } + } + + if (params.warnings.length > params.evaluator.maxWarnings) { + reasons.push(`warnings_exceeded:${params.warnings.length}`) + } + + if (params.diagnostics.length > params.evaluator.maxDiagnostics) { + reasons.push(`diagnostics_exceeded:${params.diagnostics.length}`) + } + + if (params.runEnabled && params.evaluator.requireRunSuccess) { + if (!params.runResult || params.runResult.success !== true) { + reasons.push('run_failed') + } + } + + const passed = reasons.length === 0 + return { + passed, + reasons, + summary: passed ? 'Evaluator gate passed' : `Evaluator gate failed: ${reasons.join(', ')}`, + } +} + function buildConnectionState(workflowState: { edges: Array> }): ConnectionState { @@ -679,7 +991,8 @@ async function compileChangeSpec(params: { connectionState.set(from, sourceMap) } const existingTargets = sourceMap.get(sourceHandle) || [] - const mode = mutation.action === 'disconnect' ? 'remove' : mutation.mode || 'set' + const mode = + mutation.action === 'disconnect' ? 'remove' : ('mode' in mutation ? mutation.mode : undefined) || 'set' const nextTargets = ensureConnectionTarget( existingTargets, { block: to, handle: targetHandle }, @@ -895,6 +1208,10 @@ export const workflowChangeServerTool: BaseServerTool ) const diagnostics = [...compileResult.diagnostics, ...simulation.diagnostics] const warnings = [...compileResult.warnings, ...simulation.warnings] + const acceptanceAssertions = normalizeAcceptance(params.changeSpec.acceptance) + const normalizedPostApply = normalizePostApply( + (params.postApply as PostApply | undefined) || params.changeSpec.postApply + ) const proposal: WorkflowChangeProposal = { workflowId, @@ -904,6 +1221,15 @@ export const workflowChangeServerTool: BaseServerTool warnings, diagnostics, touchedBlocks: compileResult.touchedBlocks, + acceptanceAssertions, + postApply: normalizedPostApply, + handoff: { + objective: params.changeSpec.objective, + constraints: params.changeSpec.constraints, + resolvedIds: params.changeSpec.resolvedIds, + assumptions: params.changeSpec.assumptions, + unresolvedRisks: params.changeSpec.unresolvedRisks, + }, } const proposalId = saveProposal(proposal) @@ -913,6 +1239,7 @@ export const workflowChangeServerTool: BaseServerTool operationCount: proposal.compiledOperations.length, warningCount: warnings.length, diagnosticsCount: diagnostics.length, + acceptanceCount: acceptanceAssertions.length, }) return { @@ -926,6 +1253,9 @@ export const workflowChangeServerTool: BaseServerTool warnings, diagnostics, touchedBlocks: proposal.touchedBlocks, + acceptance: proposal.acceptanceAssertions, + postApply: normalizedPostApply, + handoff: proposal.handoff, } } @@ -951,12 +1281,17 @@ export const workflowChangeServerTool: BaseServerTool const { workflowState } = await loadWorkflowStateFromDb(proposal.workflowId) const currentHash = hashWorkflowState(workflowState as unknown as Record) - const expectedHash = params.expectedSnapshotHash || proposal.baseSnapshotHash - if (expectedHash && expectedHash !== currentHash) { + const expectedHash = params.expectedSnapshotHash + if (expectedHash !== proposal.baseSnapshotHash) { + throw new Error( + `snapshot_mismatch: expectedSnapshotHash ${expectedHash} does not match proposal base ${proposal.baseSnapshotHash}` + ) + } + if (expectedHash !== currentHash) { throw new Error(`snapshot_mismatch: expected ${expectedHash} but current is ${currentHash}`) } - const applyResult = await editWorkflowServerTool.execute( + const applyResult = await applyWorkflowOperationsServerTool.execute( { workflowId: proposal.workflowId, operations: proposal.compiledOperations as any, @@ -968,9 +1303,43 @@ export const workflowChangeServerTool: BaseServerTool const newSnapshotHash = appliedWorkflowState ? hashWorkflowState(appliedWorkflowState as Record) : null + const normalizedPostApply = normalizePostApply( + (params.postApply as PostApply | undefined) || (proposal.postApply as PostApply | undefined) + ) + + let verifyResult: any | null = null + if (normalizedPostApply.verify) { + verifyResult = await workflowVerifyServerTool.execute( + { + workflowId: proposal.workflowId, + baseSnapshotHash: newSnapshotHash || undefined, + acceptance: proposal.acceptanceAssertions, + }, + { userId: context.userId } + ) + } + + let runResult: ToolCallResult | null = null + if (normalizedPostApply.run.enabled) { + runResult = await executePostApplyRun({ + workflowId: proposal.workflowId, + userId: context.userId, + run: normalizedPostApply.run, + }) + } + + const evaluatorGate = evaluatePostApplyGate({ + verifyEnabled: normalizedPostApply.verify, + verifyResult, + runEnabled: normalizedPostApply.run.enabled, + runResult, + evaluator: normalizedPostApply.evaluator, + warnings: proposal.warnings, + diagnostics: proposal.diagnostics, + }) return { - success: true, + success: evaluatorGate.passed, mode: 'apply', workflowId: proposal.workflowId, proposalId, @@ -982,6 +1351,14 @@ export const workflowChangeServerTool: BaseServerTool warnings: proposal.warnings, diagnostics: proposal.diagnostics, editResult: applyResult, + postApply: { + ok: evaluatorGate.passed, + policy: normalizedPostApply, + verify: verifyResult, + run: runResult, + evaluator: evaluatorGate, + }, + handoff: proposal.handoff, } }, } diff --git a/apps/sim/lib/copilot/tools/server/workflow/workflow-context.ts b/apps/sim/lib/copilot/tools/server/workflow/workflow-context.ts index 8bcc3fc00..ce44d9024 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/workflow-context.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/workflow-context.ts @@ -18,6 +18,7 @@ const WorkflowContextGetInputSchema = z.object({ objective: z.string().optional(), includeBlockTypes: z.array(z.string()).optional(), includeAllSchemas: z.boolean().optional(), + schemaMode: z.enum(['minimal', 'workflow', 'all']).optional(), }) type WorkflowContextGetParams = z.infer @@ -71,11 +72,16 @@ export const workflowContextGetServerTool: BaseServerTool): boolean { - if (name === 'edit_workflow') return true if (name !== 'workflow_change') return false const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : '' diff --git a/apps/sim/stores/workflow-diff/utils.ts b/apps/sim/stores/workflow-diff/utils.ts index 15449381c..3c9ca9c76 100644 --- a/apps/sim/stores/workflow-diff/utils.ts +++ b/apps/sim/stores/workflow-diff/utils.ts @@ -130,13 +130,12 @@ export async function findLatestEditWorkflowToolCallId(): Promise): boolean { - if (name === 'edit_workflow') return true if (name !== 'workflow_change') return false const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : '' if (mode === 'apply') return true - // Be permissive for legacy/incomplete events: apply calls always include proposalId. + // Be permissive for incomplete events: apply calls always include proposalId. return typeof params?.proposalId === 'string' && params.proposalId.length > 0 }