Skip to content

Commit fcd0240

Browse files
fix(resolver): consolidate reference resolution (#2941)
* fix(resolver): consolidate code to resolve references * fix edge cases * use already formatted error * fix multi index * fix backwards compat reachability * handle backwards compatibility accurately * use shared constant correctly
1 parent 4e41497 commit fcd0240

File tree

15 files changed

+671
-232
lines changed

15 files changed

+671
-232
lines changed

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ describe('Function Execute API Route', () => {
313313
'block-2': 'world',
314314
},
315315
blockNameMapping: {
316-
validVar: 'block-1',
316+
validvar: 'block-1',
317317
another_valid: 'block-2',
318318
},
319319
})
@@ -539,7 +539,7 @@ describe('Function Execute API Route', () => {
539539
'block-complex': complexData,
540540
},
541541
blockNameMapping: {
542-
complexData: 'block-complex',
542+
complexdata: 'block-complex',
543543
},
544544
})
545545

apps/sim/app/api/function/execute/route.ts

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { executeInE2B } from '@/lib/execution/e2b'
66
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
77
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
88
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
9+
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
910
import {
1011
createEnvVarPattern,
1112
createWorkflowVariablePattern,
1213
} from '@/executor/utils/reference-validation'
13-
import { navigatePath } from '@/executor/variables/resolvers/reference'
1414
export const dynamic = 'force-dynamic'
1515
export const runtime = 'nodejs'
1616

@@ -470,14 +470,17 @@ function resolveEnvironmentVariables(
470470

471471
function resolveTagVariables(
472472
code: string,
473-
blockData: Record<string, any>,
473+
blockData: Record<string, unknown>,
474474
blockNameMapping: Record<string, string>,
475-
contextVariables: Record<string, any>
475+
blockOutputSchemas: Record<string, OutputSchema>,
476+
contextVariables: Record<string, unknown>,
477+
language = 'javascript'
476478
): string {
477479
let resolvedCode = code
480+
const undefinedLiteral = language === 'python' ? 'None' : 'undefined'
478481

479482
const tagPattern = new RegExp(
480-
`${REFERENCE.START}([a-zA-Z_][a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])${REFERENCE.END}`,
483+
`${REFERENCE.START}([a-zA-Z_](?:[a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])?)${REFERENCE.END}`,
481484
'g'
482485
)
483486
const tagMatches = resolvedCode.match(tagPattern) || []
@@ -486,41 +489,37 @@ function resolveTagVariables(
486489
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
487490
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
488491
const blockName = pathParts[0]
492+
const fieldPath = pathParts.slice(1)
489493

490-
const blockId = blockNameMapping[blockName]
491-
if (!blockId) {
492-
continue
493-
}
494+
const result = resolveBlockReference(blockName, fieldPath, {
495+
blockNameMapping,
496+
blockData,
497+
blockOutputSchemas,
498+
})
494499

495-
const blockOutput = blockData[blockId]
496-
if (blockOutput === undefined) {
500+
if (!result) {
497501
continue
498502
}
499503

500-
let tagValue: any
501-
if (pathParts.length === 1) {
502-
tagValue = blockOutput
503-
} else {
504-
tagValue = navigatePath(blockOutput, pathParts.slice(1))
505-
}
504+
let tagValue = result.value
506505

507506
if (tagValue === undefined) {
507+
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral)
508508
continue
509509
}
510510

511-
if (
512-
typeof tagValue === 'string' &&
513-
tagValue.length > 100 &&
514-
(tagValue.startsWith('{') || tagValue.startsWith('['))
515-
) {
516-
try {
517-
tagValue = JSON.parse(tagValue)
518-
} catch {
519-
// Keep as-is
511+
if (typeof tagValue === 'string') {
512+
const trimmed = tagValue.trimStart()
513+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
514+
try {
515+
tagValue = JSON.parse(tagValue)
516+
} catch {
517+
// Keep as string if not valid JSON
518+
}
520519
}
521520
}
522521

523-
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
522+
const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}`
524523
contextVariables[safeVarName] = tagValue
525524
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
526525
}
@@ -537,18 +536,27 @@ function resolveTagVariables(
537536
*/
538537
function resolveCodeVariables(
539538
code: string,
540-
params: Record<string, any>,
539+
params: Record<string, unknown>,
541540
envVars: Record<string, string> = {},
542-
blockData: Record<string, any> = {},
541+
blockData: Record<string, unknown> = {},
543542
blockNameMapping: Record<string, string> = {},
544-
workflowVariables: Record<string, any> = {}
545-
): { resolvedCode: string; contextVariables: Record<string, any> } {
543+
blockOutputSchemas: Record<string, OutputSchema> = {},
544+
workflowVariables: Record<string, unknown> = {},
545+
language = 'javascript'
546+
): { resolvedCode: string; contextVariables: Record<string, unknown> } {
546547
let resolvedCode = code
547-
const contextVariables: Record<string, any> = {}
548+
const contextVariables: Record<string, unknown> = {}
548549

549550
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
550551
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
551-
resolvedCode = resolveTagVariables(resolvedCode, blockData, blockNameMapping, contextVariables)
552+
resolvedCode = resolveTagVariables(
553+
resolvedCode,
554+
blockData,
555+
blockNameMapping,
556+
blockOutputSchemas,
557+
contextVariables,
558+
language
559+
)
552560

553561
return { resolvedCode, contextVariables }
554562
}
@@ -585,6 +593,7 @@ export async function POST(req: NextRequest) {
585593
envVars = {},
586594
blockData = {},
587595
blockNameMapping = {},
596+
blockOutputSchemas = {},
588597
workflowVariables = {},
589598
workflowId,
590599
isCustomTool = false,
@@ -601,20 +610,21 @@ export async function POST(req: NextRequest) {
601610
isCustomTool,
602611
})
603612

604-
// Resolve variables in the code with workflow environment variables
613+
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
614+
605615
const codeResolution = resolveCodeVariables(
606616
code,
607617
executionParams,
608618
envVars,
609619
blockData,
610620
blockNameMapping,
611-
workflowVariables
621+
blockOutputSchemas,
622+
workflowVariables,
623+
lang
612624
)
613625
resolvedCode = codeResolution.resolvedCode
614626
const contextVariables = codeResolution.contextVariables
615627

616-
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
617-
618628
let jsImports = ''
619629
let jsRemainingCode = resolvedCode
620630
let hasImports = false
@@ -670,7 +680,11 @@ export async function POST(req: NextRequest) {
670680
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
671681
prologueLineCount++
672682
for (const [k, v] of Object.entries(contextVariables)) {
673-
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
683+
if (v === undefined) {
684+
prologue += `const ${k} = undefined;\n`
685+
} else {
686+
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
687+
}
674688
prologueLineCount++
675689
}
676690

@@ -741,7 +755,11 @@ export async function POST(req: NextRequest) {
741755
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
742756
prologueLineCount++
743757
for (const [k, v] of Object.entries(contextVariables)) {
744-
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
758+
if (v === undefined) {
759+
prologue += `${k} = None\n`
760+
} else {
761+
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
762+
}
745763
prologueLineCount++
746764
}
747765
const wrapped = [

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ export class AgentBlockHandler implements BlockHandler {
305305
base.executeFunction = async (callParams: Record<string, any>) => {
306306
const mergedParams = mergeToolParameters(userProvidedParams, callParams)
307307

308-
const { blockData, blockNameMapping } = collectBlockData(ctx)
308+
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
309309

310310
const result = await executeTool(
311311
'function_execute',
@@ -317,6 +317,7 @@ export class AgentBlockHandler implements BlockHandler {
317317
workflowVariables: ctx.workflowVariables || {},
318318
blockData,
319319
blockNameMapping,
320+
blockOutputSchemas,
320321
isCustomTool: true,
321322
_context: {
322323
workflowId: ctx.workflowId,

apps/sim/executor/handlers/condition/condition-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export async function evaluateConditionExpression(
2626
const contextSetup = `const context = ${JSON.stringify(evalContext)};`
2727
const code = `${contextSetup}\nreturn Boolean(${conditionExpression})`
2828

29-
const { blockData, blockNameMapping } = collectBlockData(ctx)
29+
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
3030

3131
const result = await executeTool(
3232
'function_execute',
@@ -37,6 +37,7 @@ export async function evaluateConditionExpression(
3737
workflowVariables: ctx.workflowVariables || {},
3838
blockData,
3939
blockNameMapping,
40+
blockOutputSchemas,
4041
_context: {
4142
workflowId: ctx.workflowId,
4243
workspaceId: ctx.workspaceId,

apps/sim/executor/handlers/function/function-handler.test.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ describe('FunctionBlockHandler', () => {
7575
workflowVariables: {},
7676
blockData: {},
7777
blockNameMapping: {},
78-
_context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId },
78+
blockOutputSchemas: {},
79+
_context: {
80+
workflowId: mockContext.workflowId,
81+
workspaceId: mockContext.workspaceId,
82+
isDeployedContext: mockContext.isDeployedContext,
83+
},
7984
}
8085
const expectedOutput: any = { result: 'Success' }
8186

@@ -84,8 +89,8 @@ describe('FunctionBlockHandler', () => {
8489
expect(mockExecuteTool).toHaveBeenCalledWith(
8590
'function_execute',
8691
expectedToolParams,
87-
false, // skipPostProcess
88-
mockContext // execution context
92+
false,
93+
mockContext
8994
)
9095
expect(result).toEqual(expectedOutput)
9196
})
@@ -107,7 +112,12 @@ describe('FunctionBlockHandler', () => {
107112
workflowVariables: {},
108113
blockData: {},
109114
blockNameMapping: {},
110-
_context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId },
115+
blockOutputSchemas: {},
116+
_context: {
117+
workflowId: mockContext.workflowId,
118+
workspaceId: mockContext.workspaceId,
119+
isDeployedContext: mockContext.isDeployedContext,
120+
},
111121
}
112122
const expectedOutput: any = { result: 'Success' }
113123

@@ -116,8 +126,8 @@ describe('FunctionBlockHandler', () => {
116126
expect(mockExecuteTool).toHaveBeenCalledWith(
117127
'function_execute',
118128
expectedToolParams,
119-
false, // skipPostProcess
120-
mockContext // execution context
129+
false,
130+
mockContext
121131
)
122132
expect(result).toEqual(expectedOutput)
123133
})
@@ -132,7 +142,12 @@ describe('FunctionBlockHandler', () => {
132142
workflowVariables: {},
133143
blockData: {},
134144
blockNameMapping: {},
135-
_context: { workflowId: mockContext.workflowId, workspaceId: mockContext.workspaceId },
145+
blockOutputSchemas: {},
146+
_context: {
147+
workflowId: mockContext.workflowId,
148+
workspaceId: mockContext.workspaceId,
149+
isDeployedContext: mockContext.isDeployedContext,
150+
},
136151
}
137152

138153
await handler.execute(mockContext, mockBlock, inputs)

apps/sim/executor/handlers/function/function-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class FunctionBlockHandler implements BlockHandler {
2323
? inputs.code.map((c: { content: string }) => c.content).join('\n')
2424
: inputs.code
2525

26-
const { blockData, blockNameMapping } = collectBlockData(ctx)
26+
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
2727

2828
const result = await executeTool(
2929
'function_execute',
@@ -35,6 +35,7 @@ export class FunctionBlockHandler implements BlockHandler {
3535
workflowVariables: ctx.workflowVariables || {},
3636
blockData,
3737
blockNameMapping,
38+
blockOutputSchemas,
3839
_context: {
3940
workflowId: ctx.workflowId,
4041
workspaceId: ctx.workspaceId,
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
1+
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
12
import { normalizeName } from '@/executor/constants'
23
import type { ExecutionContext } from '@/executor/types'
4+
import type { OutputSchema } from '@/executor/utils/block-reference'
35

46
export interface BlockDataCollection {
5-
blockData: Record<string, any>
7+
blockData: Record<string, unknown>
68
blockNameMapping: Record<string, string>
9+
blockOutputSchemas: Record<string, OutputSchema>
710
}
811

912
export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
10-
const blockData: Record<string, any> = {}
13+
const blockData: Record<string, unknown> = {}
1114
const blockNameMapping: Record<string, string> = {}
15+
const blockOutputSchemas: Record<string, OutputSchema> = {}
1216

1317
for (const [id, state] of ctx.blockStates.entries()) {
1418
if (state.output !== undefined) {
1519
blockData[id] = state.output
16-
const workflowBlock = ctx.workflow?.blocks?.find((b) => b.id === id)
17-
if (workflowBlock?.metadata?.name) {
18-
blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id
20+
}
21+
22+
const workflowBlock = ctx.workflow?.blocks?.find((b) => b.id === id)
23+
if (!workflowBlock) continue
24+
25+
if (workflowBlock.metadata?.name) {
26+
blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id
27+
}
28+
29+
const blockType = workflowBlock.metadata?.id
30+
if (blockType) {
31+
const params = workflowBlock.config?.params as Record<string, unknown> | undefined
32+
const subBlocks = params
33+
? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, { value: v }]))
34+
: undefined
35+
const schema = getBlockOutputs(blockType, subBlocks)
36+
if (schema && Object.keys(schema).length > 0) {
37+
blockOutputSchemas[id] = schema
1938
}
2039
}
2140
}
2241

23-
return { blockData, blockNameMapping }
42+
return { blockData, blockNameMapping, blockOutputSchemas }
2443
}

0 commit comments

Comments
 (0)