Files
sim/apps/sim/executor/handlers/condition/condition-handler.ts
Siddharth Ganesan 3bf00cbd2a improvement(executor): redesign executor + add start block (#1790)
* fix(billing): should allow restoring subscription (#1728)

* fix(already-cancelled-sub): UI should allow restoring subscription

* restore functionality fixed

* fix

* improvement(start): revert to start block

* make it work with start block

* fix start block persistence

* cleanup triggers

* debounce status checks

* update docs

* improvement(start): revert to start block

* make it work with start block

* fix start block persistence

* cleanup triggers

* debounce status checks

* update docs

* SSE v0.1

* v0.2

* v0.3

* v0.4

* v0.5

* v0.6

* broken checkpoint

* Executor progress - everything preliminarily tested except while loops and triggers

* Executor fixes

* Fix var typing

* Implement while loop execution

* Loop and parallel result agg

* Refactor v1 - loops work

* Fix var resolution in for each loop

* Fix while loop condition and variable resolution

* Fix loop iteration counts

* Fix loop badges

* Clean logs

* Fix variable references from start block

* Fix condition block

* Fix conditional convergence

* Dont execute orphaned nodse

* Code cleanup 1 and error surfacing

* compile time try catch

* Some fixes

* Fix error throwing

* Sentinels v1

* Fix multiple start and end nodes in loop

* Edge restoration

* Fix reachable nodes execution

* Parallel subflows

* Fix loop/parallel sentinel convergence

* Loops and parallels orchestrator

* Split executor

* Variable resolution split

* Dag phase

* Refactor

* Refactor

* Refactor 3

* Lint + refactor

* Lint + cleanup + refactor

* Readability

* Initial logs

* Fix trace spans

* Console pills for iters

* Add input/output pills

* Checkpoint

* remove unused code

* THIS IS THE COMMIT THAT CAN BREAK A LOT OF THINGS

* ANOTHER BIG REFACTOR

* Lint + fix tests

* Fix webhook

* Remove comment

* Merge stash

* Fix triggers?

* Stuff

* Fix error port

* Lint

* Consolidate state

* Clean up some var resolution

* Remove some var resolution logs

* Fix chat

* Fix chat triggers

* Fix chat trigger fully

* Snapshot refactor

* Fix mcp and custom tools

* Lint

* Fix parallel default count and trace span overlay

* Agent purple

* Fix test

* Fix test

---------

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2025-11-02 12:21:16 -08:00

227 lines
7.8 KiB
TypeScript

import { createLogger } from '@/lib/logs/console/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType, CONDITION, DEFAULTS, EDGE } from '@/executor/consts'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
const logger = createLogger('ConditionBlockHandler')
/**
* Evaluates a single condition expression with variable/block reference resolution
* Returns true if condition is met, false otherwise
*/
export async function evaluateConditionExpression(
ctx: ExecutionContext,
conditionExpression: string,
block: SerializedBlock,
resolver: any,
providedEvalContext?: Record<string, any>
): Promise<boolean> {
const evalContext = providedEvalContext || {
...(ctx.loopItems.get(block.id) || {}),
}
let resolvedConditionValue = conditionExpression
try {
if (resolver) {
const resolvedVars = resolver.resolveVariableReferences(conditionExpression, block)
const resolvedRefs = resolver.resolveBlockReferences(resolvedVars, ctx, block)
resolvedConditionValue = resolver.resolveEnvVariables(resolvedRefs)
logger.info(
`Resolved condition: from "${conditionExpression}" to "${resolvedConditionValue}"`
)
}
} catch (resolveError: any) {
logger.error(`Failed to resolve references in condition: ${resolveError.message}`, {
conditionExpression,
resolveError,
})
throw new Error(`Failed to resolve references in condition: ${resolveError.message}`)
}
try {
logger.info(`Evaluating resolved condition: "${resolvedConditionValue}"`, { evalContext })
const conditionMet = new Function(
'context',
`with(context) { return ${resolvedConditionValue} }`
)(evalContext)
logger.info(`Condition evaluated to: ${conditionMet}`)
return Boolean(conditionMet)
} catch (evalError: any) {
logger.error(`Failed to evaluate condition: ${evalError.message}`, {
originalCondition: conditionExpression,
resolvedCondition: resolvedConditionValue,
evalContext,
evalError,
})
throw new Error(
`Evaluation error in condition: ${evalError.message}. (Resolved: ${resolvedConditionValue})`
)
}
}
/**
* Handler for Condition blocks that evaluate expressions to determine execution paths.
*/
export class ConditionBlockHandler implements BlockHandler {
constructor(
private pathTracker?: any,
private resolver?: any
) {}
canHandle(block: SerializedBlock): boolean {
return block.metadata?.id === BlockType.CONDITION
}
async execute(
ctx: ExecutionContext,
block: SerializedBlock,
inputs: Record<string, any>
): Promise<BlockOutput> {
logger.info(`Executing condition block: ${block.id}`, {
rawConditionsInput: inputs.conditions,
})
const conditions = this.parseConditions(inputs.conditions)
const sourceBlockId = ctx.workflow?.connections.find((conn) => conn.target === block.id)?.source
const evalContext = this.buildEvaluationContext(ctx, block.id, sourceBlockId)
const sourceOutput = sourceBlockId ? ctx.blockStates.get(sourceBlockId)?.output : null
const outgoingConnections = ctx.workflow?.connections.filter((conn) => conn.source === block.id)
const { selectedConnection, selectedCondition } = await this.evaluateConditions(
conditions,
outgoingConnections || [],
evalContext,
ctx,
block
)
const targetBlock = ctx.workflow?.blocks.find((b) => b.id === selectedConnection?.target)
if (!targetBlock) {
throw new Error(`Target block ${selectedConnection?.target} not found`)
}
logger.info(
`Condition block ${block.id} selected path: ${selectedCondition.title} (${selectedCondition.id}) -> ${targetBlock.metadata?.name || targetBlock.id}`
)
const decisionKey = ctx.currentVirtualBlockId || block.id
ctx.decisions.condition.set(decisionKey, selectedCondition.id)
return {
...((sourceOutput as any) || {}),
conditionResult: true,
selectedPath: {
blockId: targetBlock.id,
blockType: targetBlock.metadata?.id || DEFAULTS.BLOCK_TYPE,
blockTitle: targetBlock.metadata?.name || DEFAULTS.BLOCK_TITLE,
},
selectedOption: selectedCondition.id,
selectedConditionId: selectedCondition.id,
}
}
private parseConditions(input: any): Array<{ id: string; title: string; value: string }> {
try {
const conditions = Array.isArray(input) ? input : JSON.parse(input || '[]')
logger.info('Parsed conditions:', conditions)
return conditions
} catch (error: any) {
logger.error('Failed to parse conditions:', { input, error })
throw new Error(`Invalid conditions format: ${error.message}`)
}
}
private buildEvaluationContext(
ctx: ExecutionContext,
blockId: string,
sourceBlockId?: string
): Record<string, any> {
let evalContext: Record<string, any> = {
...(ctx.loopItems.get(blockId) || {}),
}
if (sourceBlockId) {
const sourceOutput = ctx.blockStates.get(sourceBlockId)?.output
if (sourceOutput && typeof sourceOutput === 'object' && sourceOutput !== null) {
evalContext = {
...evalContext,
...sourceOutput,
}
}
}
logger.info('Base eval context:', evalContext)
return evalContext
}
private async evaluateConditions(
conditions: Array<{ id: string; title: string; value: string }>,
outgoingConnections: Array<{ source: string; target: string; sourceHandle?: string }>,
evalContext: Record<string, any>,
ctx: ExecutionContext,
block: SerializedBlock
): Promise<{
selectedConnection: { target: string; sourceHandle?: string }
selectedCondition: { id: string; title: string; value: string }
}> {
for (const condition of conditions) {
if (condition.title === CONDITION.ELSE_TITLE) {
const connection = this.findConnectionForCondition(outgoingConnections, condition.id)
if (connection) {
return { selectedConnection: connection, selectedCondition: condition }
}
continue
}
const conditionValueString = String(condition.value || '')
try {
const conditionMet = await evaluateConditionExpression(
ctx,
conditionValueString,
block,
this.resolver,
evalContext
)
logger.info(`Condition "${condition.title}" (${condition.id}) met: ${conditionMet}`)
const connection = this.findConnectionForCondition(outgoingConnections, condition.id)
if (connection && conditionMet) {
return { selectedConnection: connection, selectedCondition: condition }
}
} catch (error: any) {
logger.error(`Failed to evaluate condition "${condition.title}": ${error.message}`)
throw new Error(`Evaluation error in condition "${condition.title}": ${error.message}`)
}
}
const elseCondition = conditions.find((c) => c.title === CONDITION.ELSE_TITLE)
if (elseCondition) {
logger.warn(`No condition met, selecting 'else' path`, { blockId: block.id })
const elseConnection = this.findConnectionForCondition(outgoingConnections, elseCondition.id)
if (elseConnection) {
return { selectedConnection: elseConnection, selectedCondition: elseCondition }
}
throw new Error(
`No path found for condition block "${block.metadata?.name}", and 'else' connection missing.`
)
}
throw new Error(
`No matching path found for condition block "${block.metadata?.name}", and no 'else' block exists.`
)
}
private findConnectionForCondition(
connections: Array<{ source: string; target: string; sourceHandle?: string }>,
conditionId: string
): { target: string; sourceHandle?: string } | undefined {
return connections.find(
(conn) => conn.sourceHandle === `${EDGE.CONDITION_PREFIX}${conditionId}`
)
}
}