mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-12 15:34:58 -05:00
Checkpoint
This commit is contained in:
@@ -15,7 +15,6 @@ const NOTIFICATION_WIDTH = 240
|
||||
const NOTIFICATION_GAP = 16
|
||||
|
||||
function isWorkflowEditToolCall(name?: string, params?: Record<string, unknown>): boolean {
|
||||
if (name === 'edit_workflow') return true
|
||||
if (name !== 'workflow_change') return false
|
||||
|
||||
const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : ''
|
||||
|
||||
@@ -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({
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Workflow edit summary - shows block changes after edit_workflow/workflow_change(apply) */}
|
||||
{/* Workflow edit summary - shows block changes after workflow_change(apply) */}
|
||||
<WorkflowEditSummary toolCall={toolCall} />
|
||||
|
||||
{/* Render subagent content as thinking text */}
|
||||
|
||||
@@ -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<string, unknown>): 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<string, unknown> | 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<string, unknown>): boolean {
|
||||
if (toolName !== 'workflow_change') return false
|
||||
const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : ''
|
||||
@@ -392,11 +431,12 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
(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<string, SSEHandler> = {
|
||||
(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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
|
||||
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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
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 })
|
||||
|
||||
@@ -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<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 === '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,
|
||||
|
||||
@@ -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<string, unknown> | 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<string, SSEHandler> = {
|
||||
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<string, SSEHandler> = {
|
||||
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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
@@ -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.',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string, unknown>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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<string>([
|
||||
'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),
|
||||
|
||||
@@ -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<string, unknown>
|
||||
name?: string
|
||||
isPublic?: boolean
|
||||
workflowIds?: string[]
|
||||
}
|
||||
|
||||
// === Workflow Organization Params ===
|
||||
|
||||
export interface RenameWorkflowParams {
|
||||
|
||||
@@ -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<string, ToolMetadata> = {
|
||||
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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, BaseServerTool> = {
|
||||
[getBlockOptionsServerTool.name]: getBlockOptionsServerTool,
|
||||
[getBlockConfigServerTool.name]: getBlockConfigServerTool,
|
||||
[getTriggerBlocksServerTool.name]: getTriggerBlocksServerTool,
|
||||
[editWorkflowServerTool.name]: editWorkflowServerTool,
|
||||
[getWorkflowConsoleServerTool.name]: getWorkflowConsoleServerTool,
|
||||
[searchDocumentationServerTool.name]: searchDocumentationServerTool,
|
||||
[searchOnlineServerTool.name]: searchOnlineServerTool,
|
||||
|
||||
@@ -71,6 +71,19 @@ export type WorkflowChangeProposal = {
|
||||
warnings: string[]
|
||||
diagnostics: string[]
|
||||
touchedBlocks: string[]
|
||||
acceptanceAssertions: string[]
|
||||
postApply?: {
|
||||
verify?: boolean
|
||||
run?: Record<string, any>
|
||||
evaluator?: Record<string, any>
|
||||
}
|
||||
handoff?: {
|
||||
objective?: string
|
||||
constraints?: string[]
|
||||
resolvedIds?: Record<string, string>
|
||||
assumptions?: string[]
|
||||
unresolvedRisks?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
const contextPackStore = new TTLStore<WorkflowContextPack>()
|
||||
|
||||
@@ -68,8 +68,8 @@ async function getCurrentWorkflowStateFromDb(
|
||||
return { workflowState, subBlockValues }
|
||||
}
|
||||
|
||||
export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown> = {
|
||||
name: 'edit_workflow',
|
||||
export const applyWorkflowOperationsServerTool: BaseServerTool<EditWorkflowParams, unknown> = {
|
||||
name: '__internal_apply_workflow_operations',
|
||||
async execute(params: EditWorkflowParams, context?: { userId: string }): Promise<unknown> {
|
||||
const logger = createLogger('EditWorkflowServerTool')
|
||||
const { operations, workflowId, currentUserWorkflow } = params
|
||||
@@ -90,7 +90,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
|
||||
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<EditWorkflowParams, unknown>
|
||||
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,
|
||||
|
||||
@@ -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<typeof WorkflowChangeInputSchema>
|
||||
type ChangeSpec = z.input<typeof ChangeSpecSchema>
|
||||
type TargetRef = z.input<typeof TargetSchema>
|
||||
type ChangeOperation = z.input<typeof ChangeOperationSchema>
|
||||
type PostApply = z.input<typeof PostApplySchema>
|
||||
|
||||
type NormalizedPostApply = {
|
||||
verify: boolean
|
||||
run: {
|
||||
enabled: boolean
|
||||
mode: 'full' | 'until_block' | 'from_block' | 'block'
|
||||
useDeployedState: boolean
|
||||
workflowInput?: Record<string, any>
|
||||
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<string, any>)
|
||||
: 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<ToolCallResult> {
|
||||
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<Record<string, any>>
|
||||
}): 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<WorkflowChangeParams, any>
|
||||
)
|
||||
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<WorkflowChangeParams, any>
|
||||
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<WorkflowChangeParams, any>
|
||||
operationCount: proposal.compiledOperations.length,
|
||||
warningCount: warnings.length,
|
||||
diagnosticsCount: diagnostics.length,
|
||||
acceptanceCount: acceptanceAssertions.length,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -926,6 +1253,9 @@ export const workflowChangeServerTool: BaseServerTool<WorkflowChangeParams, any>
|
||||
warnings,
|
||||
diagnostics,
|
||||
touchedBlocks: proposal.touchedBlocks,
|
||||
acceptance: proposal.acceptanceAssertions,
|
||||
postApply: normalizedPostApply,
|
||||
handoff: proposal.handoff,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,12 +1281,17 @@ export const workflowChangeServerTool: BaseServerTool<WorkflowChangeParams, any>
|
||||
|
||||
const { workflowState } = await loadWorkflowStateFromDb(proposal.workflowId)
|
||||
const currentHash = hashWorkflowState(workflowState as unknown as Record<string, unknown>)
|
||||
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<WorkflowChangeParams, any>
|
||||
const newSnapshotHash = appliedWorkflowState
|
||||
? hashWorkflowState(appliedWorkflowState as Record<string, unknown>)
|
||||
: 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<WorkflowChangeParams, any>
|
||||
warnings: proposal.warnings,
|
||||
diagnostics: proposal.diagnostics,
|
||||
editResult: applyResult,
|
||||
postApply: {
|
||||
ok: evaluatorGate.passed,
|
||||
policy: normalizedPostApply,
|
||||
verify: verifyResult,
|
||||
run: runResult,
|
||||
evaluator: evaluatorGate,
|
||||
},
|
||||
handoff: proposal.handoff,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<typeof WorkflowContextGetInputSchema>
|
||||
@@ -71,11 +72,16 @@ export const workflowContextGetServerTool: BaseServerTool<WorkflowContextGetPara
|
||||
String(block?.type || '')
|
||||
)
|
||||
const requestedTypes = params.includeBlockTypes || []
|
||||
const includeAllSchemas = params.includeAllSchemas === true
|
||||
const candidateTypes = includeAllSchemas
|
||||
? getAllKnownBlockTypes()
|
||||
: [...blockTypesInWorkflow, ...requestedTypes]
|
||||
const schemaMode =
|
||||
params.includeAllSchemas === true ? 'all' : (params.schemaMode || 'minimal')
|
||||
const candidateTypes =
|
||||
schemaMode === 'all'
|
||||
? getAllKnownBlockTypes()
|
||||
: schemaMode === 'workflow'
|
||||
? [...blockTypesInWorkflow, ...requestedTypes]
|
||||
: [...requestedTypes]
|
||||
const { schemasByType, schemaRefsByType } = buildSchemasByType(candidateTypes)
|
||||
const suggestedSchemaTypes = [...new Set(blockTypesInWorkflow.filter(Boolean))]
|
||||
|
||||
const summary = summarizeWorkflowState(workflowState)
|
||||
const packId = saveContextPack({
|
||||
@@ -101,12 +107,14 @@ export const workflowContextGetServerTool: BaseServerTool<WorkflowContextGetPara
|
||||
contextPackId: packId,
|
||||
workflowId: params.workflowId,
|
||||
snapshotHash,
|
||||
schemaMode,
|
||||
summary: {
|
||||
...summary,
|
||||
objective: params.objective || null,
|
||||
},
|
||||
schemaRefsByType,
|
||||
availableBlockCatalog: buildAvailableBlockCatalog(schemaRefsByType),
|
||||
suggestedSchemaTypes,
|
||||
inScopeSchemas: schemasByType,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -84,7 +84,6 @@ function isPageUnloading(): boolean {
|
||||
}
|
||||
|
||||
function isWorkflowEditToolCall(name?: string, params?: Record<string, unknown>): boolean {
|
||||
if (name === 'edit_workflow') return true
|
||||
if (name !== 'workflow_change') return false
|
||||
|
||||
const mode = typeof params?.mode === 'string' ? params.mode.toLowerCase() : ''
|
||||
|
||||
@@ -130,13 +130,12 @@ export async function findLatestEditWorkflowToolCallId(): Promise<string | undef
|
||||
}
|
||||
|
||||
function isWorkflowEditToolCall(name?: string, params?: Record<string, unknown>): 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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user