/** * Workflow runtime sandbox. * * This module is intentionally independent from AgentManager. Integration can map * WorkflowAgentRunner to real subagent launches later; tests and callers can use * a fake runner today. * * Concepts adapted from michaelliv/pi-dynamic-workflows (MIT): phase/log helpers, * parallel/pipeline control-flow primitives, and budgeted workflow execution. */ import { type ChildProcess, fork } from "node:child_process"; import { createHash } from "node:crypto"; import { chmodSync, closeSync, constants, lstatSync, mkdtempSync, openSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { parseWorkflowScript } from "./workflow-parser.js"; import type { ParsedWorkflow, WorkflowAgentRequest, WorkflowAgentRunner, WorkflowBudgetOptions, WorkflowBudgetState, WorkflowProgressEvent, WorkflowRunResult, } from "./workflow-types.js"; export interface WorkflowRuntimeOptions { args?: Record; cwd?: string; budget?: WorkflowBudgetOptions; agentRunner?: WorkflowAgentRunner; filename?: string; signal?: AbortSignal; timeoutMs?: number; onProgress?: (event: WorkflowProgressEvent) => void; } const DEFAULT_TIMEOUT_MS = 30_000; const WORKFLOW_STARTUP_TIMEOUT_MS = 15_000; const WORKFLOW_CHILD_MAX_OLD_SPACE_MB = 256; const WORKFLOW_AGENT_RUNNER_ERROR = "Workflow agent() was called, but no agentRunner was provided."; const WORKFLOW_CHILD_MODULE_DIR_PREFIX = join( tmpdir(), "pi-subagents-workflow-runtime-" ); function noop(): undefined { return undefined; } let workflowChildModuleDir: string | undefined; let workflowChildModulePath: string | undefined; interface WorkflowChildRequest { type: "start"; workflow: ParsedWorkflow; args: Record; cwd: string; budgetOptions: WorkflowBudgetOptions; filename: string; } type WorkflowChildMessage = | { type: "progress"; event: WorkflowProgressEvent } | { type: "pong" } | { type: "agentAwait"; id: number } | { type: "agent"; id: number; request: WorkflowAgentRequest; phase?: string; budget: SerializableWorkflowBudgetState; } | { type: "result"; result: WorkflowRunResult } | { type: "error"; message: string }; type WorkflowParentMessage = | WorkflowChildRequest | { type: "ping" } | { type: "agentResult"; id: number; result: unknown } | { type: "agentError"; id: number; message: string }; interface SerializableWorkflowBudgetState extends WorkflowBudgetOptions { agentCalls: number; logEntries: number; remainingAgentCalls?: number; } export async function runWorkflowScript( source: string, options: WorkflowRuntimeOptions = {} ): Promise { return await runWorkflow(parseWorkflowScript(source), options); } export async function runWorkflow( workflow: ParsedWorkflow, options: WorkflowRuntimeOptions = {} ): Promise { if (options.signal?.aborted) { throw new Error("Workflow cancelled."); } const args = options.args ?? {}; const cwd = options.cwd ?? globalThis.process.cwd(); const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; const runtimeAbort = new AbortController(); const removeAbortForwarder = forwardAbortSignal(options.signal, runtimeAbort); const child = spawnWorkflowChild(); let stderr = ""; let settled = false; let timeout: NodeJS.Timeout | undefined; let agentWatchdogTimeout: NodeJS.Timeout | undefined; let agentWatchdogInterval: NodeJS.Timeout | undefined; const inFlightAgentCallIds = new Set(); const awaitedAgentCallIds = new Set(); child.stderr?.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); }); const stopChild = () => { if (!child.killed) { child.kill("SIGKILL"); } }; try { return await new Promise((resolve, reject) => { const settle = (handler: () => void) => { if (settled) { return; } settled = true; clearRuntimeTimeout(); clearAgentWatchdog(); cleanup(); handler(); }; const clearRuntimeTimeout = () => { if (timeout) { clearTimeout(timeout); timeout = undefined; } }; const clearAgentWatchdog = () => { if (agentWatchdogTimeout) { clearTimeout(agentWatchdogTimeout); agentWatchdogTimeout = undefined; } if (agentWatchdogInterval) { clearInterval(agentWatchdogInterval); agentWatchdogInterval = undefined; } }; const rejectAndStop = (error: Error) => { runtimeAbort.abort(); stopChild(); settle(() => reject(error)); }; const armRuntimeTimeout = () => { clearRuntimeTimeout(); clearAgentWatchdog(); timeout = setTimeout(() => { rejectAndStop(new Error(`Workflow timed out after ${timeoutMs}ms.`)); }, timeoutMs); }; const armAgentWatchdog = () => { clearRuntimeTimeout(); clearAgentWatchdog(); const pingChild = () => { sendWorkflowMessage(child, { type: "ping" }); }; pingChild(); agentWatchdogTimeout = setTimeout(() => { rejectAndStop(new Error(`Workflow timed out after ${timeoutMs}ms.`)); }, timeoutMs); agentWatchdogInterval = setInterval( pingChild, Math.max(10, Math.min(100, Math.floor(timeoutMs / 4))) ); }; const armStartupTimeout = () => { clearRuntimeTimeout(); timeout = setTimeout(() => { rejectAndStop( new Error( `Workflow runtime failed to start after ${WORKFLOW_STARTUP_TIMEOUT_MS}ms.` ) ); }, WORKFLOW_STARTUP_TIMEOUT_MS); }; const onAbort = () => { rejectAndStop(new Error("Workflow cancelled.")); }; const onError = (error: Error) => { runtimeAbort.abort(); settle(() => reject(error)); }; const onExit = (code: number | null, signal: NodeJS.Signals | null) => { if (settled) { return; } runtimeAbort.abort(); const details = stderr.trim(); const suffix = details ? `: ${details}` : ""; settle(() => reject( new Error( `Workflow runtime exited before completion (code ${String( code )}, signal ${String(signal)})${suffix}` ) ) ); }; const onMessage = (message: WorkflowChildMessage) => { if (message.type === "pong") { if (awaitedAgentCallIds.size > 0) { armAgentWatchdog(); } return; } if (message.type === "agentAwait") { if (inFlightAgentCallIds.has(message.id)) { awaitedAgentCallIds.add(message.id); armAgentWatchdog(); } return; } if (message.type === "progress" && message.event.kind === "started") { armRuntimeTimeout(); } else if (message.type === "agent") { inFlightAgentCallIds.add(message.id); } else if (message.type === "result" && inFlightAgentCallIds.size > 0) { rejectAndStop( createPendingAgentCallsError(inFlightAgentCallIds.size) ); return; } handleWorkflowChildMessage({ message, options, args, cwd, signal: runtimeAbort.signal, afterAgent: (id) => { inFlightAgentCallIds.delete(id); awaitedAgentCallIds.delete(id); if (!settled && awaitedAgentCallIds.size === 0) { armRuntimeTimeout(); } }, resolve: (result) => settle(() => resolve(result)), reject: rejectAndStop, send: (response) => { sendWorkflowMessage(child, response); }, }).catch((error: unknown) => { rejectAndStop( error instanceof Error ? error : new Error(String(error)) ); }); }; const cleanup = () => { options.signal?.removeEventListener("abort", onAbort); child.off("error", onError); child.off("exit", onExit); child.off("message", onMessage); }; options.signal?.addEventListener("abort", onAbort, { once: true }); child.on("error", onError); child.on("exit", onExit); child.on("message", onMessage); armStartupTimeout(); try { sendWorkflowMessage(child, { type: "start", workflow, args, cwd, budgetOptions: options.budget ?? {}, filename: options.filename ?? "workflow.js", }); } catch (error) { rejectAndStop( error instanceof Error ? error : new Error(String(error)) ); } }); } finally { removeAbortForwarder(); } } function spawnWorkflowChild(): ChildProcess { return fork(getWorkflowChildModulePath(), [], { execArgv: [`--max-old-space-size=${WORKFLOW_CHILD_MAX_OLD_SPACE_MB}`], stdio: ["ignore", "ignore", "pipe", "ipc"], serialization: "advanced", }); } function getWorkflowChildModulePath(): string { if (workflowChildModulePath) { return workflowChildModulePath; } const hash = createHash("sha256") .update(WORKFLOW_CHILD_SOURCE) .digest("hex") .slice(0, 16); const moduleDir = getWorkflowChildModuleDir(); const modulePath = join( moduleDir, `workflow-child-${hash}-${globalThis.process.pid}.mjs` ); const moduleFile = openSync( modulePath, constants.O_WRONLY + constants.O_CREAT + constants.O_EXCL + (constants.O_NOFOLLOW ?? 0), 0o600 ); try { writeFileSync(moduleFile, WORKFLOW_CHILD_SOURCE); } finally { closeSync(moduleFile); } assertPrivateFile(modulePath, "workflow child module"); workflowChildModulePath = modulePath; return workflowChildModulePath; } function getWorkflowChildModuleDir(): string { if (workflowChildModuleDir) { return workflowChildModuleDir; } const moduleDir = mkdtempSync(WORKFLOW_CHILD_MODULE_DIR_PREFIX); chmodSync(moduleDir, 0o700); assertPrivateDirectory(moduleDir, "workflow child module dir"); workflowChildModuleDir = moduleDir; return workflowChildModuleDir; } function assertPrivateDirectory(path: string, description: string): void { const stats = lstatSync(path); if (!stats.isDirectory()) { throw new Error(`${description} is not a directory.`); } assertOwnedByCurrentUser(stats.uid, description); if (stats.mode % 0o100 !== 0) { throw new Error(`${description} has unsafe permissions.`); } } function assertPrivateFile(path: string, description: string): void { const stats = lstatSync(path); if (!stats.isFile()) { throw new Error(`${description} is not a regular file.`); } assertOwnedByCurrentUser(stats.uid, description); if (stats.mode % 0o100 !== 0) { throw new Error(`${description} has unsafe permissions.`); } } function assertOwnedByCurrentUser(uid: number, description: string): void { const currentUid = getCurrentUid(); if (currentUid !== undefined && uid !== currentUid) { throw new Error(`${description} is not owned by the current user.`); } } function getCurrentUid(): number | undefined { return typeof globalThis.process.getuid === "function" ? globalThis.process.getuid() : undefined; } function forwardAbortSignal( source: AbortSignal | undefined, target: AbortController ): () => void { if (!source) { return noop; } if (source.aborted) { target.abort(); return noop; } const onAbort = () => target.abort(); source.addEventListener("abort", onAbort, { once: true }); return () => source.removeEventListener("abort", onAbort); } function sendWorkflowMessage( child: ChildProcess, message: WorkflowParentMessage ): void { if (child.connected) { try { child.send(message); } catch (error) { if (!isClosedIpcError(error)) { throw error; } } } } function isClosedIpcError(error: unknown): boolean { return ( typeof error === "object" && error !== null && "code" in error && (error.code === "EPIPE" || error.code === "ENOTCONN" || error.code === "ERR_IPC_CHANNEL_CLOSED") ); } function createPendingAgentCallsError(count: number): Error { return new Error( `Workflow completed with ${count} pending agent call${count === 1 ? "" : "s"}. Await every agent() call before returning.` ); } async function handleWorkflowChildMessage(options: { message: WorkflowChildMessage; options: WorkflowRuntimeOptions; args: Record; cwd: string; signal: AbortSignal; afterAgent: (id: number) => void; resolve: (result: WorkflowRunResult) => void; reject: (error: Error) => void; send: (response: WorkflowParentMessage) => void; }): Promise { const message = options.message; switch (message.type) { case "progress": options.options.onProgress?.(message.event); return; case "result": options.resolve(message.result); return; case "error": options.reject(new Error(message.message)); return; case "agent": await runWorkflowChildAgent(options, message); return; } } async function runWorkflowChildAgent( options: { options: WorkflowRuntimeOptions; args: Record; cwd: string; signal: AbortSignal; afterAgent: (id: number) => void; send: (response: WorkflowParentMessage) => void; }, message: Extract ): Promise { try { if (!options.options.agentRunner) { throw new Error(WORKFLOW_AGENT_RUNNER_ERROR); } const result = await options.options.agentRunner(message.request, { args: options.args, cwd: options.cwd, phase: message.phase, budget: createSerializableBudgetState(message.budget), signal: options.signal, }); if (options.signal.aborted) { return; } options.afterAgent(message.id); options.send({ type: "agentResult", id: message.id, result }); } catch (error) { if (options.signal.aborted) { return; } options.afterAgent(message.id); options.send({ type: "agentError", id: message.id, message: error instanceof Error ? error.message : String(error), }); } } function createSerializableBudgetState( budget: SerializableWorkflowBudgetState ): WorkflowBudgetState { return Object.freeze({ maxAgentCalls: budget.maxAgentCalls, maxLogEntries: budget.maxLogEntries, maxLogDataBytes: budget.maxLogDataBytes, maxResultBytes: budget.maxResultBytes, get agentCalls() { return budget.agentCalls; }, get logEntries() { return budget.logEntries; }, get remainingAgentCalls() { return budget.remainingAgentCalls; }, }); } const WORKFLOW_CHILD_SOURCE = String.raw` import { AsyncLocalStorage } from "node:async_hooks"; import { types as utilTypes } from "node:util"; import { createContext, Script } from "node:vm"; const pendingAgents = new Map(); const unawaitedAgents = new Set(); const phaseContext = new AsyncLocalStorage(); const workflowArrayPrototypeSnapshots = new WeakMap(); const DEFAULT_WORKFLOW_MAX_RESULT_BYTES = 262_144; const WORKFLOW_PROGRESS_AGENT_CALL_RESULT_MAX_BYTES = 4_096; const WORKFLOW_AGENT_REQUEST_RESULT_MAX_BYTES = 512; const WORKFLOW_AGENT_CALL_LABEL_MAX_BYTES = 120; const WORKFLOW_CURRENT_TASK_MAX_BYTES = 160; let nextAgentRequestId = 1; process.on("message", (message) => { if (!message || typeof message !== "object") { return; } if (message.type === "ping") { send({ type: "pong" }); return; } if (message.type === "start") { runWorkflow(message).catch((error) => { send({ type: "error", message: toErrorMessage(error) }, disconnect); }); return; } if (message.type === "agentResult" || message.type === "agentError") { const pending = pendingAgents.get(message.id); if (!pending) { return; } pendingAgents.delete(message.id); if (message.type === "agentResult") { pending.resolve(message.result); } else { pending.reject(new Error(message.message)); } } }); async function runWorkflow(message) { const workflow = message.workflow; const args = message.args ?? {}; const cwd = message.cwd; const budgetOptions = message.budgetOptions ?? {}; const budgetCounters = { agentCalls: 0, logEntries: 0 }; const budget = createBudgetState(budgetOptions, budgetCounters); const logs = []; const phases = []; const activeCallbackPhases = []; const agentCalls = []; const agentChildren = []; let defaultPhase; let currentTask; const rootPhaseContext = { name: undefined, defaultPhaseEntry: undefined }; const getCurrentPhase = () => phaseContext.getStore()?.name ?? defaultPhase; const emitProgress = (kind, phaseName = getCurrentPhase(), progressLogs = []) => { send({ type: "progress", event: { kind, meta: workflow.meta, currentPhase: phaseName, currentTask, phases: [...phases], logs: progressLogs, agents: agentChildren.map((child) => ({ ...child })), agentCalls: agentCalls.map((call) => copyWorkflowAgentCallForProgress(call) ), }, }); }; const completeDefaultPhase = (context, completedAt = Date.now()) => { if (context.defaultPhaseEntry) { context.defaultPhaseEntry.completedAt = completedAt; } context.defaultPhaseEntry = undefined; }; const runInPhaseContext = (name, run) => { const context = { name, defaultPhaseEntry: undefined }; return phaseContext .run(context, run) .finally(() => completeDefaultPhase(context)); }; const phase = (name, run) => { if (!name) { throw new Error("phase() requires a name."); } const startedAt = Date.now(); const currentPhaseContext = phaseContext.getStore() ?? rootPhaseContext; if (!run) { completeDefaultPhase(currentPhaseContext, startedAt); } const entry = { name, index: phases.length, startedAt }; phases.push(entry); currentTask = getWorkflowCurrentTask("phase", name); emitProgress("phase", name); if (!run) { currentPhaseContext.name = name; currentPhaseContext.defaultPhaseEntry = entry; if (currentPhaseContext === rootPhaseContext) { defaultPhase = name; } return undefined; } activeCallbackPhases.push(entry); const callbackPhaseContext = { name, defaultPhaseEntry: undefined, }; return phaseContext .run(callbackPhaseContext, () => Promise.resolve().then(run)) .finally(() => { completeDefaultPhase(callbackPhaseContext); entry.completedAt = Date.now(); const activeIndex = activeCallbackPhases.indexOf(entry); if (activeIndex !== -1) { activeCallbackPhases.splice(activeIndex, 1); } currentTask = undefined; emitProgress( "phase", activeCallbackPhases.at(-1)?.name ?? defaultPhase ); }); }; const log = (message, data) => { const maxLogEntries = budgetOptions.maxLogEntries; if ( maxLogEntries !== undefined && budgetCounters.logEntries >= maxLogEntries ) { throw new Error("Workflow log budget exceeded (" + maxLogEntries + ")."); } const phaseName = getCurrentPhase(); const entry = { phase: phaseName, message: String(message), index: logs.length, }; if (data !== undefined) { entry.data = truncateWorkflowLogData( data, budgetOptions.maxLogDataBytes ); } logs.push(entry); budgetCounters.logEntries += 1; currentTask = getWorkflowCurrentTask("log", entry.message); emitProgress("log", phaseName, [entry]); }; const agent = (requestOrType, prompt) => { const maxAgentCalls = budgetOptions.maxAgentCalls; if ( maxAgentCalls !== undefined && budgetCounters.agentCalls >= maxAgentCalls ) { throw new Error("Workflow agent budget exceeded (" + maxAgentCalls + ")."); } const request = normalizeAgentRequest(requestOrType, prompt); const phaseName = getCurrentPhase(); const callLabel = getAgentCallLabel(request, agentCalls.length); const call = { request: truncateWorkflowRequestValue( request, getWorkflowMaxResultBytes(budgetOptions) ), phase: phaseName, index: agentCalls.length, startedAt: Date.now(), label: callLabel.label, labelSource: callLabel.source, status: "running", }; agentCalls.push(call); budgetCounters.agentCalls += 1; currentTask = getWorkflowCurrentTask("agent", call.label); emitProgress("agent_start", phaseName); const agentRequest = requestAgent(request, phaseName, budget); unawaitedAgents.add(agentRequest.id); const resultPromise = agentRequest.promise.then((result) => { const agentResult = getWorkflowAgentResult(result); if (agentResult) { call.agentId = agentResult.id; agentChildren.push({ id: agentResult.id, type: agentResult.type, status: agentResult.status, description: typeof request.description === "string" ? request.description : "", }); } const callResult = truncateWorkflowResultValue( result, getWorkflowMaxResultBytes(budgetOptions) ); const completedStatus = agentResult?.status ?? "done"; if (getStructuredOutputSchema(request) !== undefined) { const structuredOutput = getStructuredOutputValue(result); if (structuredOutput === undefined) { call.status = "error"; call.result = callResult; call.error = "Workflow agent structured output was not captured."; call.completedAt = Date.now(); emitProgress("agent_error", phaseName); throw new Error(call.error); } call.status = completedStatus; call.result = callResult; call.completedAt = Date.now(); emitProgress("agent_end", phaseName); return structuredOutput; } call.status = completedStatus; call.result = callResult; call.completedAt = Date.now(); emitProgress("agent_end", phaseName); return result; }, (error) => { call.status = "error"; call.error = toErrorMessage(error); call.completedAt = Date.now(); emitProgress("agent_error", phaseName); throw error; }); return createAwaitTrackedPromise(agentRequest.id, resultPromise); }; const parallel = (steps) => { const stepList = copyWorkflowArray( steps, "parallel() requires an array of functions." ); const promises = new Array(stepList.length); const inheritedPhase = getCurrentPhase(); for (let index = 0; index < stepList.length; index += 1) { const step = stepList[index]; if (typeof step !== "function") { throw new Error("parallel() only accepts functions/thunks."); } promises[index] = runInPhaseContext(inheritedPhase, () => Promise.resolve().then(() => step()) ); } return Promise.all(promises); }; const pipeline = (items, ...stages) => { const itemList = copyWorkflowArray( items, "pipeline() requires an array of items." ); for (let index = 0; index < stages.length; index += 1) { const stage = stages[index]; if (typeof stage !== "function") { throw new Error("pipeline() stages must be functions."); } } const promises = new Array(itemList.length); const inheritedPhase = getCurrentPhase(); for (let index = 0; index < itemList.length; index += 1) { const item = itemList[index]; promises[index] = runInPhaseContext(inheritedPhase, async () => { let value = item; for (let stageIndex = 0; stageIndex < stages.length; stageIndex += 1) { const stage = stages[stageIndex]; value = await stage(value, item, index); } return value; }); } return Promise.all(promises); }; emitProgress("started"); const context = createWorkflowContext({ args, cwd, phase, log, agent, parallel, pipeline, budget, }); const script = new Script( '"use strict";\n(async () => {\n' + workflow.body + "\n})()", { filename: message.filename ?? "workflow.js" } ); const value = await phaseContext.run(rootPhaseContext, () => script.runInContext(context) ); if (pendingAgents.size > 0) { throw createPendingAgentCallsError(pendingAgents.size); } if (unawaitedAgents.size > 0) { throw createUnawaitedAgentCallsError(unawaitedAgents.size); } completeDefaultPhase(rootPhaseContext); emitProgress("completed"); send( { type: "result", result: { meta: workflow.meta, value: truncateWorkflowResultValue( value, getWorkflowMaxResultBytes(budgetOptions) ), logs, phases, agentCalls, agentChildren, budget, }, }, disconnect ); } function copyWorkflowArray(value, notArrayMessage) { if (!Array.isArray(value)) { throw new Error(notArrayMessage); } if (utilTypes.isProxy(value)) { throw new Error("Workflow helper arrays must not be proxies."); } assertWorkflowArrayPrototype(value); const propertyNames = Object.getOwnPropertyNames(value); for (let index = 0; index < propertyNames.length; index += 1) { const name = propertyNames[index]; if (name !== "length" && !isArrayIndexPropertyName(name)) { throw new Error("Workflow helper arrays must not define custom properties."); } } if (Object.getOwnPropertySymbols(value).length > 0) { throw new Error("Workflow helper arrays must not define custom properties."); } const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length"); if (!lengthDescriptor || !("value" in lengthDescriptor)) { throw new Error("Workflow helper arrays must use data properties."); } const length = lengthDescriptor.value; const copy = new Array(length); for (let index = 0; index < length; index += 1) { const descriptor = Object.getOwnPropertyDescriptor(value, index); if (!descriptor) { throw new Error("Workflow helper arrays must not be sparse."); } if (!("value" in descriptor)) { throw new Error("Workflow helper arrays must not use accessors."); } copy[index] = descriptor.value; } return copy; } function registerWorkflowArrayPrototype(prototype) { workflowArrayPrototypeSnapshots.set(prototype, snapshotObjectDescriptors(prototype)); } function assertWorkflowArrayPrototype(value) { const prototype = Object.getPrototypeOf(value); const snapshot = workflowArrayPrototypeSnapshots.get(prototype); if (!snapshot || !objectDescriptorsMatchSnapshot(prototype, snapshot)) { throw new Error("Workflow helper arrays must not use custom array methods."); } } function snapshotObjectDescriptors(object) { const names = Object.getOwnPropertyNames(object); const symbols = Object.getOwnPropertySymbols(object); const descriptors = new Map(); for (let index = 0; index < names.length; index += 1) { const name = names[index]; descriptors.set(name, Object.getOwnPropertyDescriptor(object, name)); } for (let index = 0; index < symbols.length; index += 1) { const symbol = symbols[index]; descriptors.set(symbol, Object.getOwnPropertyDescriptor(object, symbol)); } return { names, symbols, descriptors }; } function objectDescriptorsMatchSnapshot(object, snapshot) { const names = Object.getOwnPropertyNames(object); const symbols = Object.getOwnPropertySymbols(object); if ( !propertyKeysMatch(names, snapshot.names) || !propertyKeysMatch(symbols, snapshot.symbols) ) { return false; } for (const name of names) { if ( !propertyDescriptorsMatch( Object.getOwnPropertyDescriptor(object, name), snapshot.descriptors.get(name) ) ) { return false; } } for (const symbol of symbols) { if ( !propertyDescriptorsMatch( Object.getOwnPropertyDescriptor(object, symbol), snapshot.descriptors.get(symbol) ) ) { return false; } } return true; } function propertyKeysMatch(actual, expected) { if (actual.length !== expected.length) { return false; } for (let index = 0; index < actual.length; index += 1) { if (actual[index] !== expected[index]) { return false; } } return true; } function propertyDescriptorsMatch(actual, expected) { if (!actual || !expected) { return false; } if ( actual.configurable !== expected.configurable || actual.enumerable !== expected.enumerable ) { return false; } if ("value" in actual || "value" in expected) { return ( "value" in actual && "value" in expected && actual.value === expected.value && actual.writable === expected.writable ); } return actual.get === expected.get && actual.set === expected.set; } function isArrayIndexPropertyName(name) { const index = Number(name); return ( Number.isInteger(index) && index >= 0 && index < 4294967295 && String(index) === name ); } function truncateWorkflowLogData(data, maxBytes) { return truncateWorkflowJsonData( data, maxBytes, "Workflow log data was not JSON-serializable." ); } function truncateWorkflowResultValue(value, maxBytes) { if (value === undefined) { return undefined; } return truncateWorkflowJsonData( value, maxBytes, "Workflow result was not JSON-serializable." ); } function copyWorkflowAgentCallForProgress(call) { const progressCall = { ...call }; if (Object.hasOwn(progressCall, "result")) { progressCall.result = truncateWorkflowResultValue( progressCall.result, WORKFLOW_PROGRESS_AGENT_CALL_RESULT_MAX_BYTES ); } return progressCall; } function truncateWorkflowRequestValue(value, maxBytes) { const requestMaxBytes = Math.min( maxBytes ?? WORKFLOW_AGENT_REQUEST_RESULT_MAX_BYTES, WORKFLOW_AGENT_REQUEST_RESULT_MAX_BYTES ); let serialized; let jsonRequest; try { serialized = JSON.stringify(value); if (serialized !== undefined) { jsonRequest = JSON.parse(serialized); } } catch (_error) { return createTruncatedWorkflowRequest( value, requestMaxBytes, undefined, "Workflow agent request was not JSON-serializable." ); } if (serialized === undefined || jsonRequest === undefined) { return createTruncatedWorkflowRequest( value, requestMaxBytes, undefined, "Workflow agent request was not JSON-serializable." ); } const byteLength = Buffer.byteLength(serialized, "utf8"); if (byteLength <= requestMaxBytes) { return jsonRequest; } return createTruncatedWorkflowRequest( jsonRequest, requestMaxBytes, byteLength ); } function createTruncatedWorkflowRequest(value, maxBytes, bytes, reason) { const request = { prompt: typeof value?.prompt === "string" ? value.prompt : "", truncated: true, maxBytes, }; copyWorkflowRequestStringField(value, request, "agent"); copyWorkflowRequestStringField(value, request, "type"); copyWorkflowRequestStringField(value, request, "subagent_type"); copyWorkflowRequestStringField(value, request, "description"); copyWorkflowRequestStringField(value, request, "context"); copyWorkflowRequestStringField(value, request, "isolation"); if (bytes !== undefined) { request.bytes = bytes; } if (reason !== undefined) { request.reason = reason; } return fitWorkflowRequestPrompt(request, maxBytes); } function copyWorkflowRequestStringField(source, target, key) { if (typeof source !== "object" || source === null) { return; } const value = source[key]; if (typeof value === "string") { target[key] = value; } } function fitWorkflowRequestPrompt(request, maxBytes) { let serialized = JSON.stringify(request); if (Buffer.byteLength(serialized, "utf8") <= maxBytes) { return request; } const prompt = request.prompt; let low = 0; let high = prompt.length; while (low < high) { const mid = Math.floor((low + high + 1) / 2); const candidate = { ...request, prompt: prompt.slice(0, mid) }; serialized = JSON.stringify(candidate); if (Buffer.byteLength(serialized, "utf8") <= maxBytes) { low = mid; } else { high = mid - 1; } } const truncatedRequest = { ...request, prompt: prompt.slice(0, low) }; serialized = JSON.stringify(truncatedRequest); if (Buffer.byteLength(serialized, "utf8") <= maxBytes) { return truncatedRequest; } return { prompt: "", truncated: true, maxBytes }; } function truncateWorkflowJsonData(value, maxBytes, unserializableReason) { let serialized; try { serialized = JSON.stringify(value); } catch (_error) { return createTruncatedWorkflowJsonData(unserializableReason, maxBytes); } if (serialized === undefined) { return createTruncatedWorkflowJsonData(unserializableReason, maxBytes); } if (maxBytes === undefined) { return JSON.parse(serialized); } const byteLength = Buffer.byteLength(serialized, "utf8"); if (byteLength <= maxBytes) { return JSON.parse(serialized); } return { truncated: true, bytes: byteLength, maxBytes, preview: Buffer.from(serialized, "utf8").subarray(0, maxBytes).toString("utf8"), }; } function createTruncatedWorkflowJsonData(reason, maxBytes) { return { truncated: true, reason, maxBytes, }; } function getWorkflowMaxResultBytes(options) { return options.maxResultBytes ?? DEFAULT_WORKFLOW_MAX_RESULT_BYTES; } function createWorkflowContext(api) { const argsJson = JSON.stringify(api.args); if (argsJson === undefined) { throw new Error("Workflow args must be JSON-serializable."); } const context = createContext( { __workflowHost: Object.freeze({ argsJson, cwd: api.cwd, phase: api.phase, log: api.log, agent: api.agent, parallel: api.parallel, pipeline: api.pipeline, budget: api.budget, registerArrayPrototype: registerWorkflowArrayPrototype, }), }, { codeGeneration: { strings: false, wasm: false } } ); new Script("(" + installWorkflowGlobals.toString() + ")()", { filename: "workflow-bootstrap.js", }).runInContext(context); return context; } function installWorkflowGlobals() { const host = globalThis.__workflowHost; host.registerArrayPrototype(Array.prototype); const args = JSON.parse(host.argsJson); const cwd = host.cwd; const toSandboxError = (error) => { let message = "Workflow helper failed."; try { if (error !== null && typeof error === "object") { const descriptor = Object.getOwnPropertyDescriptor(error, "message"); if (descriptor && "value" in descriptor) { const value = descriptor.value; if ( value === undefined || value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ) { message = String(value); } } } else if (typeof error !== "symbol") { message = String(error); } } catch (_messageError) { message = "Workflow helper failed."; } return new Error(message); }; const callHost = (callback) => { try { return callback(); } catch (error) { throw toSandboxError(error); } }; const toSandboxValue = (value, seen = new WeakMap()) => { if (value === null || typeof value !== "object") { if (typeof value === "function" || typeof value === "symbol") { throw new Error("Workflow helper result was not serializable."); } return value; } const existing = seen.get(value); if (existing) { return existing; } if (Array.isArray(value)) { const copy = []; seen.set(value, copy); for (let index = 0; index < value.length; index += 1) { const descriptor = Object.getOwnPropertyDescriptor(value, index); if (!descriptor) { continue; } if (!("value" in descriptor)) { throw new Error("Workflow helper result was not serializable."); } copy[index] = toSandboxValue(descriptor.value, seen); } return copy; } const copy = {}; seen.set(value, copy); for (const key of Object.keys(value)) { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor || !("value" in descriptor)) { throw new Error("Workflow helper result was not serializable."); } Object.defineProperty(copy, key, { value: toSandboxValue(descriptor.value, seen), writable: true, configurable: true, enumerable: true, }); } return copy; }; const toSandboxPromise = (value) => new Promise((resolve, reject) => { try { Promise.resolve(value).then( (result) => { try { resolve(toSandboxValue(result)); } catch (error) { reject(toSandboxError(error)); } }, (error) => { reject(toSandboxError(error)); } ); } catch (error) { reject(toSandboxError(error)); } }); const toAwaitTrackedSandboxPromise = (hostThenable) => { let promise; const getPromise = () => { if (!promise) { promise = toSandboxPromise(hostThenable); } return promise; }; return Object.freeze({ then(onFulfilled, onRejected) { return getPromise().then(onFulfilled, onRejected); }, catch(onRejected) { return getPromise().catch(onRejected); }, finally(onFinally) { return getPromise().finally(onFinally); }, }); }; const phase = (name, run) => { const value = callHost(() => host.phase(name, run)); return value === undefined ? undefined : toSandboxPromise(value); }; const log = (message, data) => callHost(() => host.log(message, data)); const agent = (...input) => toAwaitTrackedSandboxPromise(callHost(() => host.agent(...input))); const parallel = (steps) => toSandboxPromise(callHost(() => host.parallel(steps))); const pipeline = (items, ...stages) => toSandboxPromise(callHost(() => host.pipeline(items, ...stages))); const budget = Object.freeze({ maxAgentCalls: host.budget.maxAgentCalls, maxLogEntries: host.budget.maxLogEntries, maxLogDataBytes: host.budget.maxLogDataBytes, maxResultBytes: host.budget.maxResultBytes, get agentCalls() { return host.budget.agentCalls; }, get logEntries() { return host.budget.logEntries; }, get remainingAgentCalls() { return host.budget.remainingAgentCalls; }, }); const workflowProcess = Object.freeze({ cwd: () => cwd }); const safeMath = {}; for (const name of Object.getOwnPropertyNames(globalThis.Math)) { if (name === "random") { continue; } const descriptor = Object.getOwnPropertyDescriptor(globalThis.Math, name); if (descriptor) { Object.defineProperty(safeMath, name, descriptor); } } Object.defineProperty(safeMath, "random", { value: undefined, writable: false, configurable: false, enumerable: false, }); const safeIntl = {}; for (const name of Object.getOwnPropertyNames(globalThis.Intl)) { if (name === "DateTimeFormat") { continue; } const descriptor = Object.getOwnPropertyDescriptor(globalThis.Intl, name); if (descriptor) { Object.defineProperty(safeIntl, name, descriptor); } } Object.defineProperty(safeIntl, "DateTimeFormat", { value: undefined, writable: false, configurable: false, enumerable: false, }); Object.defineProperties(globalThis, { args: { value: args, writable: false, configurable: false }, cwd: { value: cwd, writable: false, configurable: false }, phase: { value: phase, writable: false, configurable: false }, log: { value: log, writable: false, configurable: false }, agent: { value: agent, writable: false, configurable: false }, parallel: { value: parallel, writable: false, configurable: false }, pipeline: { value: pipeline, writable: false, configurable: false }, budget: { value: budget, writable: false, configurable: false }, process: { value: workflowProcess, writable: false, configurable: false }, constructor: { value: undefined, writable: false, configurable: false }, Math: { value: Object.freeze(safeMath), writable: false, configurable: false, }, Date: { value: undefined, writable: false, configurable: false }, Intl: { value: Object.freeze(safeIntl), writable: false, configurable: false, }, fetch: { value: undefined, writable: false, configurable: false }, WebSocket: { value: undefined, writable: false, configurable: false }, XMLHttpRequest: { value: undefined, writable: false, configurable: false }, require: { value: undefined, writable: false, configurable: false }, importScripts: { value: undefined, writable: false, configurable: false }, eval: { value: undefined, writable: false, configurable: false }, Function: { value: undefined, writable: false, configurable: false }, console: { value: undefined, writable: false, configurable: false }, }); delete globalThis.__workflowHost; } function requestAgent(request, phase, budget) { const id = nextAgentRequestId++; return { id, promise: new Promise((resolve, reject) => { pendingAgents.set(id, { resolve, reject }); send({ type: "agent", id, request, phase, budget: snapshotBudget(budget), }); }), }; } function createAwaitTrackedPromise(id, promise) { let observed = false; const observe = () => { if (observed) { return; } observed = true; unawaitedAgents.delete(id); send({ type: "agentAwait", id }); }; return Object.freeze({ then(onFulfilled, onRejected) { observe(); return promise.then(onFulfilled, onRejected); }, catch(onRejected) { observe(); return promise.catch(onRejected); }, finally(onFinally) { observe(); return promise.finally(onFinally); }, }); } function createPendingAgentCallsError(count) { return new Error( "Workflow completed with " + count + " pending agent call" + (count === 1 ? "" : "s") + ". Await every agent() call before returning." ); } function createUnawaitedAgentCallsError(count) { return new Error( "Workflow completed with " + count + " unawaited agent call" + (count === 1 ? "" : "s") + ". Await every agent() call before returning." ); } function normalizeAgentRequest(requestOrType, prompt) { if (typeof requestOrType === "string") { if (typeof prompt === "string") { return { type: requestOrType, prompt }; } return { ...(prompt ?? {}), prompt: requestOrType }; } if (typeof requestOrType !== "object" || requestOrType === null) { throw new Error("agent() requires a request object or type/prompt pair."); } if (typeof requestOrType.prompt !== "string") { throw new Error("agent() request.prompt must be a string."); } return { ...requestOrType }; } function getAgentCallLabel(request, index) { if (typeof request.description === "string" && request.description.trim()) { return { label: truncateWorkflowTextByBytes( request.description.trim(), WORKFLOW_AGENT_CALL_LABEL_MAX_BYTES ), source: "description", }; } const promptLine = request.prompt.split("\n")[0]?.trim(); return { label: promptLine ? truncateWorkflowTextByBytes(promptLine, WORKFLOW_AGENT_CALL_LABEL_MAX_BYTES) : String(index + 1), source: "prompt", }; } function getWorkflowCurrentTask(kind, label) { return truncateWorkflowTextByBytes( kind + ":" + label, WORKFLOW_CURRENT_TASK_MAX_BYTES ); } function truncateWorkflowTextByBytes(value, maxBytes) { if (Buffer.byteLength(value, "utf8") <= maxBytes) { return value; } let truncated = Buffer.from(value, "utf8").subarray(0, maxBytes).toString("utf8"); while (Buffer.byteLength(truncated, "utf8") > maxBytes) { truncated = truncated.slice(0, -1); } return truncated; } function getWorkflowAgentResult(value) { if (typeof value !== "object" || value === null) { return undefined; } if (typeof value.id !== "string" || typeof value.status !== "string") { return undefined; } return value; } function getStructuredOutputSchema(request) { return request.schema === undefined ? request.output : request.schema; } function getStructuredOutputValue(value) { if (typeof value !== "object" || value === null) { return undefined; } return value.structuredOutput; } function createBudgetState(options, counters) { return Object.freeze({ maxAgentCalls: options.maxAgentCalls, maxLogEntries: options.maxLogEntries, maxLogDataBytes: options.maxLogDataBytes, maxResultBytes: getWorkflowMaxResultBytes(options), get agentCalls() { return counters.agentCalls; }, get logEntries() { return counters.logEntries; }, get remainingAgentCalls() { if (options.maxAgentCalls === undefined) { return undefined; } return Math.max(0, options.maxAgentCalls - counters.agentCalls); }, }); } function snapshotBudget(budget) { const snapshot = { maxAgentCalls: budget.maxAgentCalls, maxLogEntries: budget.maxLogEntries, maxLogDataBytes: budget.maxLogDataBytes, maxResultBytes: budget.maxResultBytes, agentCalls: budget.agentCalls, logEntries: budget.logEntries, }; if (budget.remainingAgentCalls !== undefined) { snapshot.remainingAgentCalls = budget.remainingAgentCalls; } return snapshot; } function send(message, callback) { if (typeof process.send !== "function") { throw new Error("Workflow runtime IPC is unavailable."); } try { process.send(message, callback); } catch (error) { if (message.type === "error") { throw error; } process.send({ type: "error", message: toErrorMessage(error) }, callback); } } function disconnect() { if (typeof process.disconnect === "function") { process.disconnect(); } } function toErrorMessage(error) { return error instanceof Error ? error.message : String(error); } `;