-
Notifications
You must be signed in to change notification settings - Fork 3.5k
fix: use context variables for block outputs in function block code #4223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
0b9019d
a54dcbe
28af223
d889f32
316bc8c
3f508e4
d6ec115
d7da35b
cf233bb
f8f3758
3c8bb40
d33acf4
4f40c4c
cbfab1c
4309d06
8b57476
e3d0e74
0ac0539
3838b6e
fc07922
3a1b1a8
46ffc49
010435c
c0bc62c
387cc97
2dbc7fd
8a50f18
dcf3302
6734052
2783606
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,9 @@ import { | |
| import { WorkflowResolver } from '@/executor/variables/resolvers/workflow' | ||
| import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' | ||
|
|
||
| /** Key used to carry pre-resolved context variables through the inputs map. */ | ||
| export const FUNCTION_BLOCK_CONTEXT_VARS_KEY = '_runtimeContextVars' | ||
|
|
||
| const logger = createLogger('VariableResolver') | ||
|
|
||
| export class VariableResolver { | ||
|
|
@@ -36,6 +39,63 @@ export class VariableResolver { | |
| ] | ||
| } | ||
|
|
||
| /** | ||
| * Resolves inputs for function blocks. Block output references in the `code` field | ||
| * are stored as named context variables instead of being embedded as JavaScript | ||
| * literals, preventing large values from bloating the code string. | ||
| * | ||
| * Returns the resolved inputs and a `contextVariables` map. Callers should inject | ||
| * contextVariables into the function execution request body so the isolated VM can | ||
| * access them as global variables. | ||
| */ | ||
| resolveInputsForFunctionBlock( | ||
| ctx: ExecutionContext, | ||
| currentNodeId: string, | ||
| params: Record<string, any>, | ||
| block: SerializedBlock | ||
| ): { resolvedInputs: Record<string, any>; contextVariables: Record<string, unknown> } { | ||
| const contextVariables: Record<string, unknown> = {} | ||
| const resolved: Record<string, any> = {} | ||
|
|
||
| for (const [key, value] of Object.entries(params)) { | ||
| if (key === 'code') { | ||
| if (typeof value === 'string') { | ||
| resolved[key] = this.resolveCodeWithContextVars( | ||
| ctx, | ||
| currentNodeId, | ||
| value, | ||
| undefined, | ||
| block, | ||
| contextVariables | ||
| ) | ||
| } else if (Array.isArray(value)) { | ||
| resolved[key] = value.map((item: any) => { | ||
| if (item && typeof item === 'object' && typeof item.content === 'string') { | ||
| return { | ||
| ...item, | ||
| content: this.resolveCodeWithContextVars( | ||
| ctx, | ||
| currentNodeId, | ||
| item.content, | ||
| undefined, | ||
| block, | ||
| contextVariables | ||
| ), | ||
| } | ||
| } | ||
| return item | ||
| }) | ||
| } else { | ||
| resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) | ||
| } | ||
| } else { | ||
| resolved[key] = this.resolveValue(ctx, currentNodeId, value, undefined, block) | ||
| } | ||
| } | ||
|
|
||
| return { resolvedInputs: resolved, contextVariables } | ||
| } | ||
|
|
||
| resolveInputs( | ||
| ctx: ExecutionContext, | ||
| currentNodeId: string, | ||
|
|
@@ -149,6 +209,83 @@ export class VariableResolver { | |
| } | ||
| return value | ||
| } | ||
| /** | ||
| * Resolves a code template for a function block. Block output references are stored | ||
| * in `contextVarAccumulator` as named variables (e.g. `__blockRef_0`) and replaced | ||
| * with those variable names in the returned code string. Non-block references (loop | ||
| * items, workflow variables, env vars) are still inlined as literals so they remain | ||
| * available without any extra passing mechanism. | ||
| */ | ||
| private resolveCodeWithContextVars( | ||
| ctx: ExecutionContext, | ||
| currentNodeId: string, | ||
| template: string, | ||
| loopScope: LoopScope | undefined, | ||
| block: SerializedBlock, | ||
| contextVarAccumulator: Record<string, unknown> | ||
| ): string { | ||
| const resolutionContext: ResolutionContext = { | ||
| executionContext: ctx, | ||
| executionState: this.state, | ||
| currentNodeId, | ||
| loopScope, | ||
| } | ||
|
|
||
| const language = (block.config?.params as Record<string, unknown> | undefined)?.language as | ||
| | string | ||
| | undefined | ||
|
|
||
| let replacementError: Error | null = null | ||
|
|
||
| const blockRefByMatch = new Map<string, string>() | ||
|
|
||
| let result = replaceValidReferences(template, (match) => { | ||
| if (replacementError) return match | ||
|
|
||
| try { | ||
| if (this.blockResolver.canResolve(match)) { | ||
| // Deduplicate: identical references in the same template share a single | ||
| // accumulator slot so we do not duplicate large payloads. | ||
| const existing = blockRefByMatch.get(match) | ||
| if (existing !== undefined) return existing | ||
|
|
||
| const resolved = this.resolveReference(match, resolutionContext) | ||
| if (resolved === undefined) return match | ||
|
|
||
| const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved | ||
|
|
||
| // Block output: store in contextVarAccumulator, replace with variable name | ||
| const varName = `__blockRef_${Object.keys(contextVarAccumulator).length}` | ||
| contextVarAccumulator[varName] = effectiveValue | ||
| blockRefByMatch.set(match, varName) | ||
| return varName | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shell block references break: missing
|
||
| } | ||
|
|
||
| const resolved = this.resolveReference(match, resolutionContext) | ||
| if (resolved === undefined) return match | ||
|
|
||
| const effectiveValue = resolved === RESOLVED_EMPTY ? null : resolved | ||
|
|
||
| // Non-block reference (loop, parallel, workflow, env): embed as literal | ||
| return this.blockResolver.formatValueForBlock(effectiveValue, BlockType.FUNCTION, language) | ||
| } catch (error) { | ||
| replacementError = error instanceof Error ? error : new Error(String(error)) | ||
| return match | ||
| } | ||
| }) | ||
|
|
||
| if (replacementError !== null) { | ||
| throw replacementError | ||
| } | ||
|
|
||
| result = result.replace(createEnvVarPattern(), (match) => { | ||
| const resolved = this.resolveReference(match, resolutionContext) | ||
| return typeof resolved === 'string' ? resolved : match | ||
| }) | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| private resolveTemplate( | ||
| ctx: ExecutionContext, | ||
| currentNodeId: string, | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.