diff --git a/executor/index.ts b/executor/index.ts index 0791e6a81a..1906b3bc25 100644 --- a/executor/index.ts +++ b/executor/index.ts @@ -1,19 +1,3 @@ -/** - * Executor for running agentic workflows in parallel. - * - * High-Level Overview: - * - This class is responsible for running workflows using a layered topological sort. - * - Blocks that have no unresolved dependencies are executed in parallel. - * - Depending on the block type (router, condition, agent, or regular tool), different execution - * logic is applied. For example, condition blocks evaluate multiple branches and record the - * chosen branch via its condition ID so that only that path is executed. - * - Each block's output is stored in the ExecutionContext so that subsequent blocks can reference them. - * - Detailed logs are collected for each block to assist with debugging. - * - * Error Handling: - * - If a block fails, an error is thrown, halting the workflow. - * - Meaningful error messages are provided. - */ import { getAllBlocks } from '@/blocks' import { generateEvaluatorPrompt } from '@/blocks/blocks/evaluator' import { generateRouterPrompt } from '@/blocks/blocks/router' @@ -25,6 +9,10 @@ import { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import { executeTool, getTool } from '@/tools' import { BlockLog, ExecutionContext, ExecutionResult, Tool } from './types' +/** + * Main executor class for running agentic workflows. + * Handles parallel execution, state management, and special block types. + */ export class Executor { constructor( private workflow: SerializedWorkflow, @@ -34,7 +22,11 @@ export class Executor { ) {} /** - * Main entry point that executes the entire workflow in layered parallel fashion. + * Main entry point for workflow execution. + * Initializes context, executes blocks, and returns results. + * + * @param workflowId - Unique identifier for the workflow + * @returns Promise - Execution results including success/failure, output, and logs */ async execute(workflowId: string): Promise { const startTime = new Date() @@ -83,12 +75,16 @@ export class Executor { } /** - * Executes workflow blocks layer-by-layer. Blocks with no dependencies are processed together. + * Executes workflow blocks layer-by-layer in parallel, handling loops and conditional paths. * - * Notes: - * - Maintains in-degrees and adjacency lists for blocks (i.e. dependencies). - * - Blocks with condition or router types update routing/conditional decisions. - * - Only the branch corresponding to the evaluated condition is executed. + * Key Features: + * - Executes blocks with no dependencies in parallel using topological sorting + * - Handles special blocks (router, evaluator, condition) and their path decisions + * - Manages feedback loops with iteration limits + * - Tracks and updates block states in the execution context + * + * @param context - The execution context containing block states and logs + * @returns Promise - The output of the last executed block */ private async executeInParallel(context: ExecutionContext): Promise { const { blocks, connections } = this.workflow @@ -348,10 +344,19 @@ export class Executor { } /** - * Executes a single block. Deduces the tool to call, validates parameters, - * makes the request, and transforms the response. + * Executes a single block with appropriate tool or provider. + * Handles different block types (router, evaluator, condition, agent). * - * The result is logged and returned. + * Process: + * 1. Validates block state and configuration + * 2. Executes based on block type + * 3. Logs execution details + * 4. Stores results in context + * + * @param block - Block to execute + * @param inputs - Resolved inputs for the block + * @param context - Current execution context + * @returns Promise - Block execution results */ private async executeBlock( block: SerializedBlock, @@ -550,9 +555,413 @@ export class Executor { } /** - * Resolves template references in a block's configuration (e.g., ""), - * as well as environment variables (format: "{{ENV_VAR}}"). - * The values are pulled from the context's blockStates and environmentVariables. + * Executes a router block to determine the next execution path. + * + * Process: + * 1. Resolves inputs and gets possible target blocks + * 2. Generates and sends routing prompt to the model + * 3. Processes response to determine chosen path + * 4. Validates and returns routing decision + * + * @param block - The router block to execute + * @param context - Current execution context + * @returns Promise with routing result including chosen path + */ + private async executeRouterBlock( + block: SerializedBlock, + context: ExecutionContext + ): Promise<{ + content: string + model: string + tokens: { + prompt: number + completion: number + total: number + } + selectedPath: { + blockId: string + blockType: string + blockTitle: string + } + }> { + // Resolve inputs for the router block. + const resolvedInputs = this.resolveInputs(block, context) + const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id) + const targetBlocks = outgoingConnections.map((conn) => { + const targetBlock = this.workflow.blocks.find((b) => b.id === conn.target) + if (!targetBlock) { + throw new Error(`Target block ${conn.target} not found`) + } + return { + id: targetBlock.id, + type: targetBlock.metadata?.type, + title: targetBlock.metadata?.title, + description: targetBlock.metadata?.description, + subBlocks: targetBlock.config.params, + currentState: context.blockStates.get(targetBlock.id), + } + }) + + const routerConfig = { + prompt: resolvedInputs.prompt, + model: resolvedInputs.model, + apiKey: resolvedInputs.apiKey, + temperature: resolvedInputs.temperature || 0, + } + + const model = routerConfig.model || 'gpt-4o' + const providerId = getProviderFromModel(model) + + // Generate and send the router prompt. + const response = await executeProviderRequest(providerId, { + model: routerConfig.model, + systemPrompt: generateRouterPrompt(routerConfig.prompt, targetBlocks), + messages: [{ role: 'user', content: routerConfig.prompt }], + temperature: routerConfig.temperature, + apiKey: routerConfig.apiKey, + }) + + const chosenBlockId = response.content.trim().toLowerCase() + const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId) + if (!chosenBlock) { + throw new Error(`Invalid routing decision: ${chosenBlockId}`) + } + + const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 } + return { + content: resolvedInputs.prompt, + model: response.model, + tokens: { + prompt: tokens.prompt || 0, + completion: tokens.completion || 0, + total: tokens.total || 0, + }, + selectedPath: { + blockId: chosenBlock.id, + blockType: chosenBlock.type || 'unknown', + blockTitle: chosenBlock.title || 'Untitled Block', + }, + } + } + + /** + * Executes an evaluator block which analyzes content against criteria and chooses a path. + * + * Process: + * 1. Resolves inputs and gets possible target blocks + * 2. Generates and sends evaluation prompt to the model + * 3. Processes response to determine chosen path + * 4. Stores evaluation result in context + * + * @param block - The evaluator block to execute + * @param context - Current execution context + * @returns Promise with evaluation result including chosen path + */ + private async executeEvaluatorBlock( + block: SerializedBlock, + context: ExecutionContext + ): Promise<{ + content: string + model: string + tokens: { + prompt: number + completion: number + total: number + } + selectedPath: { + blockId: string + blockType: string + blockTitle: string + } + }> { + // Resolve inputs for the evaluator block. + const resolvedInputs = this.resolveInputs(block, context) + + // Get all possible target blocks from outgoing connections + const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id) + + const targetBlocks = outgoingConnections.map((conn) => { + const targetBlock = this.workflow.blocks.find((b) => b.id === conn.target) + if (!targetBlock) { + throw new Error(`Target block ${conn.target} not found`) + } + return { + id: targetBlock.id, + type: targetBlock.metadata?.type, + title: targetBlock.metadata?.title, + description: targetBlock.metadata?.description, + subBlocks: targetBlock.config.params, + currentState: context.blockStates.get(targetBlock.id), + } + }) + + const evaluatorConfig = { + prompt: resolvedInputs.prompt, + content: resolvedInputs.content, + model: resolvedInputs.model, + apiKey: resolvedInputs.apiKey, + temperature: resolvedInputs.temperature || 0, + } + + const model = evaluatorConfig.model || 'gpt-4o' + const providerId = getProviderFromModel(model) + + // Generate and execute the evaluator prompt + const response = await executeProviderRequest(providerId, { + model: evaluatorConfig.model, + systemPrompt: generateEvaluatorPrompt( + evaluatorConfig.prompt, + evaluatorConfig.content, + targetBlocks + ), + messages: [{ role: 'user', content: evaluatorConfig.prompt }], + temperature: evaluatorConfig.temperature, + apiKey: evaluatorConfig.apiKey, + }) + + const chosenBlockId = response.content.trim().toLowerCase() + + const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId) + if (!chosenBlock) { + throw new Error(`Invalid evaluation decision: ${chosenBlockId}`) + } + + // Store the evaluation result in the context + const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 } + const result = { + content: evaluatorConfig.content, + model: response.model, + tokens: { + prompt: tokens.prompt || 0, + completion: tokens.completion || 0, + total: tokens.total || 0, + }, + selectedPath: { + blockId: chosenBlock.id, + blockType: chosenBlock.type || 'unknown', + blockTitle: chosenBlock.title || 'Untitled Block', + }, + } + + // ADDED: Explicitly store the evaluation decision in the context + context.blockStates.set(block.id, { + response: result, + }) + + return result + } + + /** + * Determines if a block is reachable along the chosen path from a decision block. + * + * Uses breadth-first search to: + * 1. Start from the chosen block + * 2. Follow valid connections + * 3. Skip paths from other routers/evaluators + * 4. Check if target block is reachable + * + * @param blockId - ID of block to check + * @param chosenBlockId - ID of the chosen target block + * @param decisionBlockId - ID of the router/evaluator making the decision + * @returns boolean - Whether the block is reachable + */ + private isInChosenPath(blockId: string, chosenBlockId: string, decisionBlockId: string): boolean { + const visited = new Set() + const queue = [chosenBlockId] + + // Add the decision block (router/evaluator) itself as valid + if (blockId === decisionBlockId) { + return true + } + + while (queue.length > 0) { + const currentId = queue.shift()! + if (visited.has(currentId)) continue + visited.add(currentId) + + // If we found the block we're looking for + if (currentId === blockId) { + return true + } + + // Get all outgoing connections from current block + const connections = this.workflow.connections.filter((conn) => conn.source === currentId) + for (const conn of connections) { + // Don't follow connections from other routers/evaluators + const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source) + if ( + sourceBlock?.metadata?.type !== 'router' && + sourceBlock?.metadata?.type !== 'evaluator' + ) { + queue.push(conn.target) + } + } + } + + return false + } + + /** + * Executes a condition block that evaluates logical conditions and selects a path. + * + * Process: + * 1. Parses and evaluates conditions in order (if/else-if/else) + * 2. Uses source block's output for evaluation context + * 3. Selects matching path based on condition result + * 4. Stores decision in context for downstream execution + * + * @param block - The condition block to execute + * @param context - Current execution context + * @returns Promise with condition result and selected path + */ + private async executeConditionalBlock( + block: SerializedBlock, + context: ExecutionContext + ): Promise<{ + content: string + condition: boolean + selectedConditionId: string + sourceOutput: BlockOutput + selectedPath: { + blockId: string + blockType: string + blockTitle: string + } + }> { + const conditions = JSON.parse(block.config.params.conditions) + + // Identify the source block that feeds into this condition block. + const sourceBlockId = this.workflow.connections.find((conn) => conn.target === block.id)?.source + + if (!sourceBlockId) { + throw new Error(`No source block found for condition block ${block.id}`) + } + + const sourceOutput = context.blockStates.get(sourceBlockId) + if (!sourceOutput) { + throw new Error(`No output found for source block ${sourceBlockId}`) + } + + const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id) + + let conditionMet = false + let selectedConnection: { target: string; sourceHandle?: string } | null = null + let selectedCondition: { id: string; title: string; value: string } | null = null + + // Evaluate conditions one by one. + for (const condition of conditions) { + try { + // Resolve the condition expression using the current context. + const resolvedCondition = this.resolveInputs( + { + id: block.id, + config: { params: { condition: condition.value }, tool: block.config.tool }, + metadata: block.metadata, + position: block.position, + inputs: block.inputs, + outputs: block.outputs, + enabled: block.enabled, + }, + context + ) + const evalContext = { + ...(typeof sourceOutput === 'object' && sourceOutput !== null ? sourceOutput : {}), + agent1: sourceOutput, + } + conditionMet = new Function( + 'context', + `with(context) { return ${resolvedCondition.condition} }` + )(evalContext) + + // Cast the connection so that TypeScript knows it has a target property. + const connection = outgoingConnections.find( + (conn) => conn.sourceHandle === `condition-${condition.id}` + ) as { target: string; sourceHandle?: string } | undefined + + if (connection) { + // For if/else-if, require conditionMet to be true. + // For else, unconditionally select it. + if ((condition.title === 'if' || condition.title === 'else if') && conditionMet) { + selectedConnection = connection + selectedCondition = condition + break + } else if (condition.title === 'else') { + selectedConnection = connection + selectedCondition = condition + break + } + } + } catch (error: any) { + console.error(`Failed to evaluate condition: ${error.message}`, { + condition, + error, + }) + throw new Error(`Failed to evaluate condition: ${error.message}`) + } + } + + if (!selectedConnection || !selectedCondition) { + throw new Error(`No matching path found for condition block ${block.id}`) + } + + // Identify the target block based on the selected connection. + const targetBlock = this.workflow.blocks.find((b) => b.id === selectedConnection!.target) + if (!targetBlock) { + throw new Error(`Target block ${selectedConnection!.target} not found`) + } + + // Get the raw output from the source block's state + const sourceBlockState = context.blockStates.get(sourceBlockId) + if (!sourceBlockState) { + throw new Error(`No state found for source block ${sourceBlockId}`) + } + + // Create the block output with the source output when condition is met + const blockOutput = { + response: { + result: conditionMet ? sourceBlockState : false, + content: `Condition '${selectedCondition.title}' evaluated to ${conditionMet}`, + condition: { + result: conditionMet, + selectedPath: { + blockId: targetBlock.id, + blockType: targetBlock.metadata?.type || '', + blockTitle: targetBlock.metadata?.title || '', + }, + selectedConditionId: selectedCondition.id, + }, + }, + } + + // Store the block output in the context + context.blockStates.set(block.id, blockOutput) + + return { + content: `Condition '${selectedCondition.title}' chosen`, + condition: conditionMet, + selectedConditionId: selectedCondition.id, + sourceOutput: sourceBlockState, + selectedPath: { + blockId: targetBlock.id, + blockType: targetBlock.metadata?.type || '', + blockTitle: targetBlock.metadata?.title || '', + }, + } + } + + /** + * Resolves block input values from context and environment. + * Handles template references and variable substitution. + * + * Features: + * - Resolves block references () + * - Resolves environment variables ({{ENV_VAR}}) + * - Handles special formatting for function blocks + * - Validates references and paths + * + * @param block - Block whose inputs need resolution + * @param context - Current execution context + * @returns Record - Resolved input values */ private resolveInputs(block: SerializedBlock, context: ExecutionContext): Record { const inputs = { ...block.config.params } @@ -688,377 +1097,4 @@ export class Executor { return resolvedInputs } - - /** - * Executes a router block which calculates branching decisions based on a prompt. - */ - private async executeRouterBlock( - block: SerializedBlock, - context: ExecutionContext - ): Promise<{ - content: string - model: string - tokens: { - prompt: number - completion: number - total: number - } - selectedPath: { - blockId: string - blockType: string - blockTitle: string - } - }> { - // Resolve inputs for the router block. - const resolvedInputs = this.resolveInputs(block, context) - const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id) - const targetBlocks = outgoingConnections.map((conn) => { - const targetBlock = this.workflow.blocks.find((b) => b.id === conn.target) - if (!targetBlock) { - throw new Error(`Target block ${conn.target} not found`) - } - return { - id: targetBlock.id, - type: targetBlock.metadata?.type, - title: targetBlock.metadata?.title, - description: targetBlock.metadata?.description, - subBlocks: targetBlock.config.params, - currentState: context.blockStates.get(targetBlock.id), - } - }) - - const routerConfig = { - prompt: resolvedInputs.prompt, - model: resolvedInputs.model, - apiKey: resolvedInputs.apiKey, - temperature: resolvedInputs.temperature || 0, - } - - const model = routerConfig.model || 'gpt-4o' - const providerId = getProviderFromModel(model) - - // Generate and send the router prompt. - const response = await executeProviderRequest(providerId, { - model: routerConfig.model, - systemPrompt: generateRouterPrompt(routerConfig.prompt, targetBlocks), - messages: [{ role: 'user', content: routerConfig.prompt }], - temperature: routerConfig.temperature, - apiKey: routerConfig.apiKey, - }) - - const chosenBlockId = response.content.trim().toLowerCase() - const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId) - if (!chosenBlock) { - throw new Error(`Invalid routing decision: ${chosenBlockId}`) - } - - const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 } - return { - content: resolvedInputs.prompt, - model: response.model, - tokens: { - prompt: tokens.prompt || 0, - completion: tokens.completion || 0, - total: tokens.total || 0, - }, - selectedPath: { - blockId: chosenBlock.id, - blockType: chosenBlock.type || 'unknown', - blockTitle: chosenBlock.title || 'Untitled Block', - }, - } - } - - /** - * Executes a router block which calculates branching decisions based on a prompt. - */ - private async executeEvaluatorBlock( - block: SerializedBlock, - context: ExecutionContext - ): Promise<{ - content: string - model: string - tokens: { - prompt: number - completion: number - total: number - } - selectedPath: { - blockId: string - blockType: string - blockTitle: string - } - }> { - // Resolve inputs for the evaluator block. - console.log('Evaluator: Resolving inputs for the evaluator block.') - const resolvedInputs = this.resolveInputs(block, context) - console.log('Evaluator: Resolved inputs:', resolvedInputs) - - // Get all possible target blocks from outgoing connections - const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id) - console.log('Evaluator: Outgoing connections:', outgoingConnections) - - const targetBlocks = outgoingConnections.map((conn) => { - const targetBlock = this.workflow.blocks.find((b) => b.id === conn.target) - if (!targetBlock) { - throw new Error(`Target block ${conn.target} not found`) - } - console.log('Evaluator: Found target block:', targetBlock) - return { - id: targetBlock.id, - type: targetBlock.metadata?.type, - title: targetBlock.metadata?.title, - description: targetBlock.metadata?.description, - subBlocks: targetBlock.config.params, - currentState: context.blockStates.get(targetBlock.id), - } - }) - console.log('Evaluator: Mapped target blocks:', targetBlocks) - - const evaluatorConfig = { - prompt: resolvedInputs.prompt, - content: resolvedInputs.content, - model: resolvedInputs.model, - apiKey: resolvedInputs.apiKey, - temperature: resolvedInputs.temperature || 0, - } - - const model = evaluatorConfig.model || 'gpt-4o' - const providerId = getProviderFromModel(model) - - // Generate and execute the evaluator prompt - console.log('Evaluator: Sending request with config:', evaluatorConfig) - const response = await executeProviderRequest(providerId, { - model: evaluatorConfig.model, - systemPrompt: generateEvaluatorPrompt( - evaluatorConfig.prompt, - evaluatorConfig.content, - targetBlocks - ), - messages: [{ role: 'user', content: evaluatorConfig.prompt }], - temperature: evaluatorConfig.temperature, - apiKey: evaluatorConfig.apiKey, - }) - - console.log('Evaluator: Raw response:', response) - const chosenBlockId = response.content.trim().toLowerCase() - console.log('Evaluator: Chosen block ID:', chosenBlockId) - - const chosenBlock = targetBlocks.find((b) => b.id === chosenBlockId) - if (!chosenBlock) { - throw new Error(`Invalid evaluation decision: ${chosenBlockId}`) - } - - // Store the evaluation result in the context - const tokens = response.tokens || { prompt: 0, completion: 0, total: 0 } - const result = { - content: evaluatorConfig.prompt, - model: response.model, - tokens: { - prompt: tokens.prompt || 0, - completion: tokens.completion || 0, - total: tokens.total || 0, - }, - selectedPath: { - blockId: chosenBlock.id, - blockType: chosenBlock.type || 'unknown', - blockTitle: chosenBlock.title || 'Untitled Block', - }, - } - - // ADDED: Explicitly store the evaluation decision in the context - context.blockStates.set(block.id, { - response: result, - }) - - return result - } - - /** - * Determines whether a block is reachable along the chosen router path. - * - * This uses a breadth-first search starting from the chosen block id. - */ - private isInChosenPath(blockId: string, chosenBlockId: string, decisionBlockId: string): boolean { - const visited = new Set() - const queue = [chosenBlockId] - - // Add the decision block (router/evaluator) itself as valid - if (blockId === decisionBlockId) { - return true - } - - while (queue.length > 0) { - const currentId = queue.shift()! - if (visited.has(currentId)) continue - visited.add(currentId) - - // If we found the block we're looking for - if (currentId === blockId) { - return true - } - - // Get all outgoing connections from current block - const connections = this.workflow.connections.filter((conn) => conn.source === currentId) - for (const conn of connections) { - // Don't follow connections from other routers/evaluators - const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source) - if ( - sourceBlock?.metadata?.type !== 'router' && - sourceBlock?.metadata?.type !== 'evaluator' - ) { - queue.push(conn.target) - } - } - } - - return false - } - - /** - * Executes a condition block that evaluates a set of conditions (if/else-if/else). - * - * The block: - * - Parses its conditions. - * - Uses the source block's output to evaluate each condition. - * - Selects the branch matching the evaluation (via sourceHandle in the connection). - * - Returns an output that includes the boolean result and the selected condition's ID. - */ - private async executeConditionalBlock( - block: SerializedBlock, - context: ExecutionContext - ): Promise<{ - content: string - condition: boolean - selectedConditionId: string - sourceOutput: BlockOutput - selectedPath: { - blockId: string - blockType: string - blockTitle: string - } - }> { - const conditions = JSON.parse(block.config.params.conditions) - console.log('Parsed conditions:', conditions) - - // Identify the source block that feeds into this condition block. - const sourceBlockId = this.workflow.connections.find((conn) => conn.target === block.id)?.source - - if (!sourceBlockId) { - throw new Error(`No source block found for condition block ${block.id}`) - } - - const sourceOutput = context.blockStates.get(sourceBlockId) - if (!sourceOutput) { - throw new Error(`No output found for source block ${sourceBlockId}`) - } - console.log('Source block output:', sourceOutput) - - const outgoingConnections = this.workflow.connections.filter((conn) => conn.source === block.id) - console.log('Outgoing connections:', outgoingConnections) - - let conditionMet = false - let selectedConnection: { target: string; sourceHandle?: string } | null = null - let selectedCondition: { id: string; title: string; value: string } | null = null - - // Evaluate conditions one by one. - for (const condition of conditions) { - try { - // Resolve the condition expression using the current context. - const resolvedCondition = this.resolveInputs( - { - id: block.id, - config: { params: { condition: condition.value }, tool: block.config.tool }, - metadata: block.metadata, - position: block.position, - inputs: block.inputs, - outputs: block.outputs, - enabled: block.enabled, - }, - context - ) - const evalContext = { - ...(typeof sourceOutput === 'object' && sourceOutput !== null ? sourceOutput : {}), - agent1: sourceOutput, - } - conditionMet = new Function( - 'context', - `with(context) { return ${resolvedCondition.condition} }` - )(evalContext) - - // Cast the connection so that TypeScript knows it has a target property. - const connection = outgoingConnections.find( - (conn) => conn.sourceHandle === `condition-${condition.id}` - ) as { target: string; sourceHandle?: string } | undefined - - if (connection) { - // For if/else-if, require conditionMet to be true. - // For else, unconditionally select it. - if ((condition.title === 'if' || condition.title === 'else if') && conditionMet) { - selectedConnection = connection - selectedCondition = condition - break - } else if (condition.title === 'else') { - selectedConnection = connection - selectedCondition = condition - break - } - } - } catch (error: any) { - console.error(`Failed to evaluate condition: ${error.message}`, { - condition, - error, - }) - throw new Error(`Failed to evaluate condition: ${error.message}`) - } - } - - if (!selectedConnection || !selectedCondition) { - throw new Error(`No matching path found for condition block ${block.id}`) - } - - // Identify the target block based on the selected connection. - const targetBlock = this.workflow.blocks.find((b) => b.id === selectedConnection!.target) - if (!targetBlock) { - throw new Error(`Target block ${selectedConnection!.target} not found`) - } - - // Get the raw output from the source block's state - const sourceBlockState = context.blockStates.get(sourceBlockId) - if (!sourceBlockState) { - throw new Error(`No state found for source block ${sourceBlockId}`) - } - - // Create the block output with the source output when condition is met - const blockOutput = { - response: { - result: conditionMet ? sourceBlockState : false, - content: `Condition '${selectedCondition.title}' evaluated to ${conditionMet}`, - condition: { - result: conditionMet, - selectedPath: { - blockId: targetBlock.id, - blockType: targetBlock.metadata?.type || '', - blockTitle: targetBlock.metadata?.title || '', - }, - selectedConditionId: selectedCondition.id, - }, - }, - } - - // Store the block output in the context - context.blockStates.set(block.id, blockOutput) - - return { - content: `Condition '${selectedCondition.title}' chosen`, - condition: conditionMet, - selectedConditionId: selectedCondition.id, - sourceOutput: sourceBlockState, - selectedPath: { - blockId: targetBlock.id, - blockType: targetBlock.metadata?.type || '', - blockTitle: targetBlock.metadata?.title || '', - }, - } - } }