/** * IPC-mode interrupt handling for subprocess execution. * When AGENCY_IPC=1 is set, interrupts are sent to the parent process * over Node's built-in IPC channel instead of being returned as Interrupt[]. */ import { fork } from "child_process"; import type { ForkOptions } from "child_process"; import type { AgencyConfig } from "../config/config.js"; import { type TrustedModules } from "../compiler/trustedModules.js"; import type { RuntimeContext } from "./state/context.js"; import { type Run } from "./asyncContext.js"; import type { State } from "./state/stateStack.js"; import { getSubprocessRunInfo, setSubprocessRunInfo, isIpcMode, type SubprocessRunInfo } from "./subprocessRunInfo.js"; import { type IpcInvocationUsageMessage } from "./costTelemetry.js"; import { type IpcCallbackMessage } from "./callbackForwarding.js"; export { ipcLog, ipcRole, sendInterruptToParent, sendLockAcquireToParent, serializeInterruptsForIpc, setSubprocessIpcPayloadLimit, } from "./ipcChild.js"; export type { IpcDecisionMessage, IpcErrorMessage, IpcInterruptMessage, IpcInterruptedMessage, IpcLockAcquireMessage, IpcLockGrantedMessage, IpcLockReleaseMessage, IpcResultMessage, ParentToSubprocess, SerializedInterrupt, SubprocessToParent, } from "./ipcChild.js"; import { type IpcInterruptedMessage, type SerializedInterrupt } from "./ipcChild.js"; export { isIpcMode }; export { getSubprocessRunInfo, setSubprocessRunInfo, type SubprocessRunInfo }; import { type LockRelease } from "./lock.js"; import { type ResultFailure } from "./result.js"; /** Resolved filesystem path to the subprocess bootstrap script. */ export declare const subprocessBootstrapPath: string; export declare const LIMIT_CEILINGS: { readonly wallClock: number; readonly memory: number; readonly ipcPayload: number; readonly stdout: number; }; export declare const DEFAULT_MAX_SUBPROCESS_DEPTH = 5; export declare const SUBPROCESS_DEPTH_CEILING = 10; /** The effective depth cap for a run() call: the caller's maxDepth param, * tightened by the tightest ancestor cap (inherited via the run/resume * instruction) and the hard ceiling. */ export declare function resolveDepthCap(paramMaxDepth: number): number; export type RunLimits = { wallClock: number; memory: number; ipcPayload: number; stdout: number; }; export declare function clampLimits(input: RunLimits): RunLimits; /** * Build a structured limit-exceeded Result.failure that the Agency-side * `try _run(...)` will pass through (without converting to an Error string). * `limit` is the canonical name (e.g. "wall_clock", "memory", "ipc_payload", * "stdout"). `threshold` is the configured cap; `value` is what was observed * when the violation was detected. `extras` carries optional fields like * `samplePrefix` for IPC payload violations. */ export declare function makeLimitFailure(limit: string, threshold: number, value: number, extras?: Record): ResultFailure; export declare function registerSessionLock(ctx: { lockReleasers: Record; }, sessionId: string, releaserKey: string, release: LockRelease): void; export declare function cleanupSessionLocks(ctx: { lockReleasers: Record; }, sessionId: string): void; /** * Mutable per-call state shared by all the subprocess event handlers. * Bundled into one object so helpers can be extracted to module scope * without each one needing 8 closure parameters. */ export type RunSession = { sessionId: string; child: ReturnType; limits: RunLimits; ctx: any; stateStack: any; /** The run of the code that called run(). Forwarded callbacks * (handleCallbackMessage) fire from the event-loop message handler; they * run under a child of this run, so an AgencyFunction callback body sees * the parent's real globals and threads, exactly as an in-process callback * would. */ parentStore: Run; /** The run of the branch the subprocess was started in. The listener for * the child's interrupts runs the parent's handler chain under it, so * the chain sees the handlers that were executing, the globals, and the * logger of the code that started the subprocess. */ branchRun: Run; resolvePromise: (v: SessionOutcome) => void; rejectPromise: (v: any) => void; settled: boolean; startedAt: number; wallClockTimer: NodeJS.Timeout | null; stdoutBytes: number; stoppedForwarding: boolean; /** Detaches the abort listener from the (possibly long-lived) parent * signal. Run at settle: a composed AbortSignal pins its unremoved * listeners — and everything they close over (session, ctx, child) — * for the lifetime of the parent signal, leaking once per execution * segment under fork/race or a TimeGuard. */ detachAbortListener: (() => void) | null; }; /** How one subprocess execution segment ended: a value (including limit * failures, which are ordinary Result failures), or a self-checkpointed * pause. Session errors reject instead. */ type SessionOutcome = { type: "result"; value: any; } | { type: "interrupted"; msg: IpcInterruptedMessage; }; /** Everything `_run` persists across a pause so the replayed call can * re-fork and resume: the child's checkpoint (OPAQUE — its frames belong * to another process and must never be spliced into the parent's replay), * the surfaced interrupts in order (their ids key the user's responses), * and the node to resume. */ export type SubprocessResumePayload = { childCheckpoint: any; interrupts: SerializedInterrupt[]; node: string; subprocessSessionId: string; }; export declare function loadSubprocessPayload(frame: State): SubprocessResumePayload | undefined; /** Identity the child adopts, carried on both startup instructions: the * parent's runId (one trace across processes and pause/resume cycles), a * stable per-logical-child session id, the parent's subprocessRun span id * for span nesting, the child's nesting depth, the tightest ancestor * depth cap, and the trusted modules the child may compile against. */ type SubprocessIdentity = { runId?: string; subprocessSessionId?: string; spanContext?: string; depth?: number; maxDepth?: number; /** This process's own list when the child's program loads a trusted * module, and empty otherwise (`trustedModulesForChild`); a child never * widens it. Rebuilt from the parent on every segment, run and resume * alike, never read from a checkpoint. See docs/dev/runtime/trusted-modules.md. */ trustedModules?: TrustedModules[]; }; export type RunInstruction = SubprocessIdentity & { type: "run"; scriptPath: string; node: string; args: Record; /** Present on every eval run; absent on all other runs. Delivered as the * node's single positional argument (eval entry nodes take exactly one * parameter, whatever it is named — the sender does not know the agent's * parameter names, by design). Mutually exclusive with a non-empty * `args`. */ input?: string | Record; ipcPayload?: number; configOverrides?: Partial; }; /** * Decide a run instruction's positional call arguments, given the node's * parameter names (the compiled module's `__NodeParams`). Two modes: * * Eval runs (`input` present) deliver the test's input positionally — the * sender does not know the agent's parameter names, by design (tests * describe the task, not the agent). Exactly one parameter, checked loudly * before the node runs; a mismatch must never become a silent undefined. * * Everything else maps the named `args` record through the parameter list. * Lives here (not in the bootstrap, which has import-time side effects) so * tests can exercise it; the bootstrap turns an error into an IPC reply. */ export declare function resolveNodeCallArgs(msg: { node: string; args: Record; input?: string | Record; }, paramNames: string[]): { args: unknown[]; } | { error: string; }; export declare function buildRunInstruction(args: { scriptPath: string; node: string; args?: Record; input?: string | Record; limits: RunLimits; configOverrides?: Partial; identity?: SubprocessIdentity; }): RunInstruction; /** Startup instruction for resuming a previously-paused subprocess: the * child restores the checkpoint, pairs `interrupts` with `responses` * positionally (via the compiled module's own respondToInterrupts export), * and continues from exactly where it left off. */ export type ResumeInstruction = SubprocessIdentity & { type: "resume"; scriptPath: string; node: string; checkpoint: any; interrupts: SerializedInterrupt[]; responses: any[]; ipcPayload?: number; configOverrides?: Partial; }; export declare function buildResumeInstruction(args: { scriptPath: string; saved: SubprocessResumePayload; responses: any[]; limits: RunLimits; configOverrides?: Partial; identity?: SubprocessIdentity; }): ResumeInstruction; /** Pull the user's responses for this subprocess's pending interrupts, in * the exact order of the saved interrupts array (the child pairs them * positionally). Note: `ctx.getInterruptResponse` returns the response * ALREADY UNWRAPPED (context.ts does `?.response` internally). */ export declare function collectSubprocessResponses(ctx: any, saved: SubprocessResumePayload): any[]; export declare function buildForkOptions(args: { limits: RunLimits; cwd?: string; }): ForkOptions; /** Write the compiled JS to a fresh .agency-tmp// dir (under the * agency-lang package root so Node resolves agency-lang package imports via * the project's node_modules) * and return the script path. Called at every fork — initial run and resume * alike — and paired with cleanupTempDir when the session settles. */ export declare function materializeCompiledScript(compiled: { moduleId: string; code: string; modules?: Record; entryPath?: string; trustedModules?: string[]; }): string; /** Full per-invocation usage delta from a child. Recovered field-by-field from * untrusted input by `normalizeIpcUsageDelta`: independently-valid money and * tokens survive a malformed sibling, an unusable attribution entry is omitted * and degrades `complete`, and only a wholly non-object message is * dropped. The recovered delta (including its `attributionLost`) then flows * through the same accounting sink as an in-process charge. */ export declare function handleInvocationUsageMessage(s: RunSession, msg: IpcInvocationUsageMessage): void; /** A child reported that its (or a descendant's) usage telemetry may be * incomplete. Mark this invocation's usage a lower bound and relay once. */ export declare function handleInvocationUsageIncompleteMessage(s: RunSession): void; /** * Fire the PARENT's registered callbacks for an event forwarded from a child. * * MUST STAY SYNCHRONOUS (like handleTelemetryMessage): handleChildMessage * void-invokes its async dispatch, so FIFO arrival-order processing holds only * while this path has no awaits. Parent callbacks fire fire-and-forget (void * invokeCallbacks) so a slow or throwing parent callback cannot wedge the pump. * * invokeCallbacks re-emits upward when THIS process is itself a subprocess, so a * grandchild's event relays to the root with no explicit relay code. * * Fires on the parent's FULL stack (ctx.stateStack), NOT the run() call-site * slice s.stateStack: a parent callback registered on an ancestor frame (e.g. a * node-level `callback("onNodeStart")` above the run() call) is only reachable by * walking the full stack, exactly as an in-process event does via callHook (which * omits stateStack and defaults to ctx.stateStack). Passing s.stateStack here * would silently miss those ancestor-frame callbacks. */ export declare function handleCallbackMessage(s: RunSession, msg: IpcCallbackMessage): void; export declare function handleChildMessage(s: RunSession, msg: any): Promise; /** * Wire up all event handlers on the child process and kick off execution by * sending the (prebuilt) startup instruction over IPC. * Exported for unit tests: the per-segment wall-clock property (timer armed * per session, cleared at settle, never firing across a pause) is pinned * with fake timers here rather than by a timing-arithmetic execution test. */ export declare function attachSessionHandlers(s: RunSession, instruction: RunInstruction | ResumeInstruction): void; /** * Fork a compiled Agency program as a subprocess and manage the IPC protocol. * A runBatch adopter with a single child: relays subprocess interrupts * through the parent's handler chain, and — when the child pauses itself — * surfaces the child's interrupts to this process's caller with a * parent-side shared checkpoint stamped by runBatch. */ export declare function _run(compiled: { moduleId: string; code: string; }, node: string, args: Record, wallClock: number, memory: number, ipcPayload: number, stdout: number, configOverrides?: Partial, cwd?: string, maxDepth?: number): Promise; /** `_run` for a run the caller already holds. Generated code wraps this one * as an AgencyFunction, whose body is handed the run first. */ /** The list a child may compile against: this process's whole list when * every file the child's program is marked as loading sits under a trusted * compiled directory, and none otherwise. A program that loads host code is * a host program, and a trusted tool inside it may run a nested workflow * over tools from any entry. A program compiled without the list, such as * one a model wrote and ran with `runCode`, carries an empty mark, so * neither it nor any program it runs can reach a trusted directory. The * mark is a field on a record the calling program holds (issue #1366); a * mark naming a file outside every entry passes down nothing, and one * naming a real host file hands down a list the child cannot use without a * branded tool value. Throws when a config entry does not resolve. */ export declare function trustedModulesForChild(ctx: RuntimeContext, compiled: { trustedModules?: string[]; }): TrustedModules[]; export declare function _runFor(store: Run, compiled: { moduleId: string; code: string; trustedModules?: string[]; }, node: string, args: Record, wallClock: number, memory: number, ipcPayload: number, stdout: number, configOverrides?: Partial, cwd?: string, maxDepth?: number): Promise; /** * Forward the parent's statelog sink to a subprocess so child events land in * the SAME log the parent writes — nested under the parent's subprocessRun * span via the inherited runId + adopted span root. Without this, a child * compiled at runtime has observability baked OFF and its execution is * invisible in the parent's logs. * * Precedence: an explicit child logFile from the caller (run(logFile:)) * always wins; a parent with observability disabled forwards nothing. The * parent's logFile is absolutized against the parent's cwd so a child * launched with a different `cwd` still appends to the same file. */ export declare function withParentStatelog(overrides: Partial | undefined, parentConfig: { observability?: boolean; host?: string; apiKey?: string; projectId?: string; requestTimeoutMs?: number; metadata?: NonNullable["metadata"]; logFile?: string; }): Partial | undefined; /** The parent context fields a child's settings come from. */ export type ParentSettings = Pick, "smoltalkDefaults" | "maxToolResultChars" | "maxToolSchemaChars" | "maxCallDepth" | "failurePropagation" | "maxRestores">; /** * The parent's settings as the config a child starts with. A child compiled * in the sandbox baked the built-in model and provider, so it is handed the * parent's smoltalk defaults, tool caps, and limits. They are read from the * parent context's fields, which hold the baked config with the environment, * IPC, and per-invocation overrides merged in, so a grandchild gets the * root's values through a child compiled the same way. A parent with no * `provider` sends none, and the child's merge then clears its own. Only the * API keys and base URLs the parent has a value for are sent. `log`, * `trace*`, `budget`, `memory`, and `client.providerModules` are not sent; * see "What a child inherits" in docs/dev/runtime/subprocess-ipc.md. */ export declare function parentConfigOverrides(parent: ParentSettings): Partial; /** The parent's config under the caller's explicit overrides, key by key, * except that a model and its provider go together: an override that names * `client.defaultModel` without `client.defaultProvider` drops the parent's * provider, so smoltalk picks one from the model name. This is the rule * `applyCliFlags` applies to a bare `--model`. */ export declare function withParentConfig(overrides: Partial | undefined, parentConfig: Partial): Partial; /** * Merge the parent's provider-module paths into the config overrides sent to a * subprocess, resolving relative paths to absolute against the parent's cwd so * they still resolve in a child launched with a different `cwd`. Returns the * overrides unchanged when the parent has no configured provider modules. */ export declare function withParentProviderModules(overrides: Partial | undefined, parentModules: string[]): Partial | undefined;