Skip to content

Commit 76509e1

Browse files
committed
progress
1 parent 628b452 commit 76509e1

21 files changed

Lines changed: 985 additions & 68 deletions

apps/docs/content/docs/en/blocks/function.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ const file = <readfile.file>;
198198
const base64 = await sim.files.readBase64(file);
199199
```
200200

201-
`sim.files.readBase64(file)`, `sim.files.readText(file)`, `sim.files.readBase64Chunk(file, { offset, length })`, and `sim.files.readTextChunk(file, { offset, length })` read from server-side execution storage under memory caps. `sim.values.read(ref)` can explicitly read a large execution value reference. These helpers are available only in JavaScript functions without imports. JavaScript with imports, Python, and shell do not support these lazy helpers yet.
201+
`sim.files.readBase64(file)`, `sim.files.readText(file)`, `sim.files.readBase64Chunk(file, { offset, length })`, and `sim.files.readTextChunk(file, { offset, length })` read from server-side execution storage under memory caps. `sim.values.read(ref)` explicitly reads a large execution value reference, and `sim.values.readArray(ref)` reads a manifest-backed large array. These helpers are available only in JavaScript functions without imports. JavaScript with imports, Python, and shell do not support these lazy helpers yet.
202202

203203
Very large full reads can still fail by design; use chunk helpers or return a file when you need to handle more data.
204204

@@ -228,7 +228,7 @@ return { name: file.name, chunk: firstMegabyteBase64 };
228228

229229
Chunk `offset` and `length` are byte-based. For Unicode text, a chunk can split a multi-byte character at the boundary; use text chunks for approximate text processing and prefer smaller structured references when exact parsing matters.
230230

231-
Avoid passing a full large object into a Function block when you only need one field. For example, prefer `<api.data.customerId>` over `<api.data>` when the API response is large. If a JavaScript Function without imports references a large execution value, Sim automatically reads it through `sim.values.read(...)` at runtime under memory caps.
231+
Avoid passing a full large object into a Function block when you only need one field. For example, prefer `<api.data.customerId>` over `<api.data>` when the API response is large. If a JavaScript Function without imports references a whole large execution value, Sim automatically rewrites it to `sim.values.read(...)` at runtime under memory caps. If the value is a manifest-backed array, Sim rewrites it to `sim.values.readArray(...)` so array variables can stay compact between blocks.
232232

233233
For large generated data, write the result to a file or table with `outputPath`, `outputSandboxPath`, or `outputTable` instead of returning the entire payload inline.
234234

apps/docs/content/docs/en/execution/api-deployment.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ Workflow execution responses are capped by platform request and response limits.
232232
}
233233
```
234234

235-
The `version` field is part of the external API contract. Treat the reference as an opaque placeholder for a value that could not be safely embedded in the response. `id`, `key`, and `executionId` are not fetch URLs; `key` points to execution-scoped server storage. Use `selectedOutputs` to request a smaller nested field, reduce the data passed between blocks, or return the data from a Response block when your workflow intentionally owns the HTTP response body. File outputs are metadata-first; request `.base64` only when you need inline file content. JavaScript Function blocks can explicitly read large files or value refs with the `sim.files` and `sim.values` helpers under memory caps.
235+
The `version` field is part of the external API contract. Treat the reference as an opaque placeholder for a value that could not be safely embedded in the response. `id`, `key`, and `executionId` are not fetch URLs; `key` points to execution-scoped server storage. Use `selectedOutputs` to request a smaller nested field, reduce the data passed between blocks, or return the data from a Response block when your workflow intentionally owns the HTTP response body. File outputs are metadata-first; request `.base64` only when you need inline file content. JavaScript Function blocks can explicitly read large files, value refs, and manifest-backed arrays with the `sim.files` and `sim.values` helpers under memory caps.
236236

237237
### Asynchronous
238238

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

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ describe('Function Execute API Route', () => {
245245
it('rejects large refs in runtimes without ref-native helpers', async () => {
246246
featureFlagsMock.isE2bEnabled = true
247247
const req = createMockRequest('POST', {
248-
code: 'echo "${__blockRef_0}"',
248+
code: 'echo "$__blockRef_0"',
249249
language: 'shell',
250250
contextVariables: {
251251
__blockRef_0: {
@@ -268,6 +268,46 @@ describe('Function Execute API Route', () => {
268268
'Large execution values require the JavaScript isolated-vm runtime'
269269
)
270270
})
271+
272+
it('registers manifest array read broker for isolated-vm execution', async () => {
273+
const req = createMockRequest('POST', {
274+
code: 'return await sim.values.readArray(__blockRef_0)',
275+
language: 'javascript',
276+
contextVariables: {
277+
__blockRef_0: {
278+
__simLargeArrayManifest: true,
279+
version: 2,
280+
kind: 'array',
281+
totalCount: 1,
282+
chunkCount: 1,
283+
byteSize: 16,
284+
chunks: [
285+
{
286+
ref: {
287+
__simLargeValueRef: true,
288+
version: 1,
289+
id: 'lv_ABCDEFGHIJKL',
290+
kind: 'array',
291+
size: 16,
292+
executionId: 'execution-1',
293+
},
294+
count: 1,
295+
byteSize: 16,
296+
},
297+
],
298+
preview: [{ id: 1 }],
299+
},
300+
},
301+
})
302+
303+
const response = await POST(req)
304+
const data = await response.json()
305+
const [, options] = mockExecuteInIsolatedVM.mock.calls.at(-1) ?? []
306+
307+
expect(response.status).toBe(200)
308+
expect(data.success).toBe(true)
309+
expect(options?.brokers).toHaveProperty('sim.values.readArray')
310+
})
271311
})
272312

273313
describe('Template Variable Resolution', () => {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1414
import { executeInE2B, executeShellInE2B } from '@/lib/execution/e2b'
1515
import { executeInIsolatedVM, type IsolatedVMBrokerHandler } from '@/lib/execution/isolated-vm'
1616
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
17+
import {
18+
isLargeArrayManifest,
19+
materializeLargeArrayManifest,
20+
} from '@/lib/execution/payloads/large-array-manifest'
1721
import { containsLargeValueRef, isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
1822
import {
1923
MAX_FUNCTION_INLINE_BYTES,
@@ -788,6 +792,21 @@ function createFunctionRuntimeBrokers(
788792
}
789793
return value
790794
},
795+
'sim.values.readArray': async (args) => {
796+
const record = asRecord(args)
797+
const options = asRecord(record.options)
798+
const manifest = record.ref
799+
if (!isLargeArrayManifest(manifest)) {
800+
throw new Error('Expected a large array manifest.')
801+
}
802+
if (!context.executionId) {
803+
throw new Error('Large array manifests require an execution context.')
804+
}
805+
return materializeLargeArrayManifest(manifest, {
806+
...base,
807+
maxBytes: clampInlineBytes(options.maxBytes, MAX_INLINE_MATERIALIZATION_BYTES),
808+
})
809+
},
791810
}
792811
}
793812

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

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
import { beforeEach, describe, expect, it, vi } from 'vitest'
55
import { clearLargeValueCacheForTests } from '@/lib/execution/payloads/cache'
6-
import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
6+
import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest'
77
import { BlockType } from '@/executor/constants'
88
import { VariablesBlockHandler } from '@/executor/handlers/variables/variables-handler'
99
import type { ExecutionContext } from '@/executor/types'
@@ -81,7 +81,7 @@ describe('VariablesBlockHandler', () => {
8181
expect(mockUploadFile).not.toHaveBeenCalled()
8282
})
8383

84-
it('stores oversized assignments as durable refs in variables and block output', async () => {
84+
it('stores oversized array assignments as durable manifests in variables and block output', async () => {
8585
const handler = new VariablesBlockHandler()
8686
const ctx = createContext()
8787
const value = Array.from({ length: 120_000 }, (_, index) => ({
@@ -101,13 +101,14 @@ describe('VariablesBlockHandler', () => {
101101
})
102102

103103
const storedValue = ctx.workflowVariables?.['var-1'].value
104-
expect(isLargeValueRef(storedValue)).toBe(true)
104+
expect(isLargeArrayManifest(storedValue)).toBe(true)
105105
expect(output.issues).toBe(storedValue)
106106
expect(storedValue).toMatchObject({
107-
__simLargeValueRef: true,
107+
__simLargeArrayManifest: true,
108108
kind: 'array',
109-
executionId: 'execution-1',
109+
totalCount: value.length,
110110
})
111+
expect(storedValue.chunkCount).toBeGreaterThan(1)
111112
expect(mockUploadFile).toHaveBeenCalledWith(
112113
expect.objectContaining({
113114
context: 'execution',
@@ -143,6 +144,60 @@ describe('VariablesBlockHandler', () => {
143144
expect(mockUploadFile).not.toHaveBeenCalled()
144145
})
145146

147+
it('preserves whole large refs before scalar type coercion', async () => {
148+
const handler = new VariablesBlockHandler()
149+
const ref = {
150+
__simLargeValueRef: true,
151+
version: 1,
152+
id: 'lv_ABCDEFGHIJKL',
153+
kind: 'object',
154+
size: 12 * 1024 * 1024,
155+
executionId: 'execution-1',
156+
}
157+
const ctx = createContext({
158+
workflowVariables: {
159+
stringVar: { id: 'stringVar', name: 'stringRef', type: 'string', value: '' },
160+
plainVar: { id: 'plainVar', name: 'plainRef', type: 'plain', value: '' },
161+
numberVar: { id: 'numberVar', name: 'numberRef', type: 'number', value: 0 },
162+
booleanVar: { id: 'booleanVar', name: 'booleanRef', type: 'boolean', value: false },
163+
},
164+
})
165+
166+
await handler.execute(ctx, createBlock(), {
167+
variables: [
168+
{
169+
variableId: 'stringVar',
170+
variableName: 'stringRef',
171+
type: 'string',
172+
value: JSON.stringify(ref),
173+
},
174+
{
175+
variableId: 'plainVar',
176+
variableName: 'plainRef',
177+
type: 'plain',
178+
value: JSON.stringify(ref),
179+
},
180+
{
181+
variableId: 'numberVar',
182+
variableName: 'numberRef',
183+
type: 'number',
184+
value: JSON.stringify(ref),
185+
},
186+
{
187+
variableId: 'booleanVar',
188+
variableName: 'booleanRef',
189+
type: 'boolean',
190+
value: JSON.stringify(ref),
191+
},
192+
],
193+
})
194+
195+
expect(ctx.workflowVariables?.stringVar.value).toEqual(ref)
196+
expect(ctx.workflowVariables?.plainVar.value).toEqual(ref)
197+
expect(ctx.workflowVariables?.numberVar.value).toEqual(ref)
198+
expect(ctx.workflowVariables?.booleanVar.value).toEqual(ref)
199+
})
200+
146201
it('preserves existing variable metadata when compacting reassignment', async () => {
147202
const handler = new VariablesBlockHandler()
148203
const ctx = createContext({

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { toError } from '@sim/utils/errors'
3+
import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest'
4+
import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
35
import { compactWorkflowVariableValue } from '@/lib/execution/payloads/serializer'
46
import type { BlockOutput } from '@/blocks/types'
57
import { BlockType } from '@/executor/constants'
@@ -96,6 +98,11 @@ export class VariablesBlockHandler implements BlockHandler {
9698
}
9799

98100
private parseValueByType(value: any, type: string, variableName?: string): any {
101+
const refValue = this.parseLargeExecutionValue(value)
102+
if (refValue !== undefined) {
103+
return refValue
104+
}
105+
99106
if (value === null || value === undefined || value === '') {
100107
if (type === 'number') return 0
101108
if (type === 'boolean') return false
@@ -167,4 +174,21 @@ export class VariablesBlockHandler implements BlockHandler {
167174

168175
return value
169176
}
177+
178+
private parseLargeExecutionValue(value: any): any | undefined {
179+
if (isLargeValueRef(value) || isLargeArrayManifest(value)) {
180+
return value
181+
}
182+
183+
if (typeof value !== 'string' || !value.trim()) {
184+
return undefined
185+
}
186+
187+
try {
188+
const parsed = JSON.parse(value)
189+
return isLargeValueRef(parsed) || isLargeArrayManifest(parsed) ? parsed : undefined
190+
} catch {
191+
return undefined
192+
}
193+
}
170194
}

apps/sim/executor/variables/resolver.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,106 @@ describe('VariableResolver function block inputs', () => {
9595
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
9696
})
9797

98+
it('allows Variables block assignments to receive whole large refs', async () => {
99+
const producer = createBlock('producer', 'Producer', BlockType.API)
100+
const variablesBlock = createBlock('variables', 'Variables', BlockType.VARIABLES, {
101+
variables: [
102+
{
103+
variableId: 'var-1',
104+
variableName: 'issues',
105+
type: 'array',
106+
value: '<Producer.result>',
107+
},
108+
],
109+
})
110+
const workflow: SerializedWorkflow = {
111+
version: '1',
112+
blocks: [producer, variablesBlock],
113+
connections: [],
114+
loops: {},
115+
parallels: {},
116+
}
117+
const state = new ExecutionState()
118+
const ref = {
119+
__simLargeValueRef: true,
120+
version: 1,
121+
id: 'lv_ABCDEFGHIJKL',
122+
kind: 'array',
123+
size: 12 * 1024 * 1024,
124+
executionId: 'execution-1',
125+
}
126+
state.setBlockOutput('producer', { result: ref })
127+
const ctx = {
128+
blockStates: state.getBlockStates(),
129+
blockLogs: [],
130+
environmentVariables: {},
131+
workflowVariables: {},
132+
decisions: { router: new Map(), condition: new Map() },
133+
loopExecutions: new Map(),
134+
executedBlocks: new Set(),
135+
activeExecutionPath: new Set(),
136+
completedLoops: new Set(),
137+
metadata: {},
138+
} as ExecutionContext
139+
140+
const resolver = new VariableResolver(workflow, {}, state)
141+
const result = await resolver.resolveInputs(
142+
ctx,
143+
'variables',
144+
variablesBlock.config.params,
145+
variablesBlock
146+
)
147+
148+
expect(JSON.parse(result.variables[0].value)).toEqual(ref)
149+
})
150+
151+
it('allows Response block data to preserve whole large refs', async () => {
152+
const producer = createBlock('producer', 'Producer', BlockType.API)
153+
const responseBlock = createBlock('response', 'Response', BlockType.RESPONSE, {
154+
dataMode: 'json',
155+
data: '<Producer.result>',
156+
})
157+
const workflow: SerializedWorkflow = {
158+
version: '1',
159+
blocks: [producer, responseBlock],
160+
connections: [],
161+
loops: {},
162+
parallels: {},
163+
}
164+
const state = new ExecutionState()
165+
const ref = {
166+
__simLargeValueRef: true,
167+
version: 1,
168+
id: 'lv_ZYXWVUTSRQPO',
169+
kind: 'array',
170+
size: 12 * 1024 * 1024,
171+
executionId: 'execution-1',
172+
}
173+
state.setBlockOutput('producer', { result: ref })
174+
const ctx = {
175+
blockStates: state.getBlockStates(),
176+
blockLogs: [],
177+
environmentVariables: {},
178+
workflowVariables: {},
179+
decisions: { router: new Map(), condition: new Map() },
180+
loopExecutions: new Map(),
181+
executedBlocks: new Set(),
182+
activeExecutionPath: new Set(),
183+
completedLoops: new Set(),
184+
metadata: {},
185+
} as ExecutionContext
186+
187+
const resolver = new VariableResolver(workflow, {}, state)
188+
const result = await resolver.resolveInputs(
189+
ctx,
190+
'response',
191+
responseBlock.config.params,
192+
responseBlock
193+
)
194+
195+
expect(JSON.parse(result.data)).toEqual(ref)
196+
})
197+
98198
it('resolves workflow variable object references through context variables', async () => {
99199
const { block, ctx, resolver } = createResolver('javascript')
100200
const issues = [{ key: 'SIM-1', summary: 'Small issue' }]

0 commit comments

Comments
 (0)