import { AgencyProgram } from "../types.js"; import { AgencyConfig } from "../config/config.js"; import type { TsNode } from "../ir/tsIR.js"; import { type CompilationUnit } from "../compilationUnit.js"; export declare class TypeScriptBuilder { private generatedStatements; /** One `__registerAlwaysScope(...)` per effect declaration with an * `@always` tag. */ private alwaysScopeRegistrations; private generatedTypeAliases; /** * TypeAlias AST nodes whose declaration has been hoisted to the * containing function/node's outer scope (so it's visible to every * runner.step closure). When processNode sees one of these inside a * body, it returns ts.empty() to avoid a redeclaration. */ private hoistedTypeAliasNodes; private importStatements; private toolRegistrations; private functionsUsed; private scopes; private steps; private names; private tracking; private pipes; private assigns; private finalize; private agencyConfig; private adjacentNodes; private currentAdjacentNodes; private isInsideGraphNode; private loopVars; private insideHandlerBody; private handlerFuncResultCounter; private insideGlobalInit; private plainForLoopCounter; private insidePlainMatchExpr; private _asyncBranchCheckNeeded; private _sourceMapBuilder; private compilationUnit; private moduleId; private outputFile; /** * Optional per-module init plan + cross-module alias resolver produced * by `compileClosure`. When present, drives: * - the order of local static/global assignments (topsort-ordered * rather than source-ordered), and * - the per-phase `await __awaitStaticInit(...)` / * `__awaitGlobalsInit(...)` prelude emitted in this module's * init functions, and * - the PR-1 trap message: `__readStatic` is called with the * SOURCE moduleId of an imported binding, not the placeholder * "" used before. * * When absent (callers that haven't migrated to compileClosure, or * single-file unit tests), codegen falls back to source order and * omits the cross-module awaits — the lazy per-function * `isInitialized` guards plus the runtime trap remain the safety net. */ private initPlan?; /** * @param config - Agency compiler configuration (model defaults, logging, etc.) * @param info - Pre-collected program metadata (function definitions, graph nodes, imports, type hints) * @param moduleId - Unique identifier for this module (e.g., "foo.agency"), used to * namespace global variables in the GlobalStore so that different modules' globals * don't collide. Must be consistent between the defining module and any importers. * @param outputFile - Absolute path of the file the generated code is * written to and loaded from. The module records its source file as a * path from here. Leave it out for code that loads from somewhere else, * which then records none. It is also the base for import paths when * the unit has no `fromFile`. * @param initPlan - Optional per-module init plan + resolver from * `compileClosure`. See the field docstring above. */ constructor(config: AgencyConfig | undefined, info: CompilationUnit, moduleId: string, outputFile?: string, initPlan?: TypeScriptBuilder["initPlan"]); private configDefaults; /** Convert a TsNode to string (for use in template-based methods) */ private str; /** Returns the template opts for checkpoint creation (moduleId, scopeName, stepPath as JSON-quoted strings). */ private checkpointOpts; private forkBranchSetup; /** Generate a TsNode for `hasInterrupts(x)` */ private interruptCheck; /** Generate a raw string for `hasInterrupts(x)` */ private interruptCheckRaw; private agencyFileToDefaultImportName; private needsParensLeft; private needsParensRight; build(program: AgencyProgram, moduleFingerprint?: { moduleId: string; hash: string; }): TsNode; /** Process a function call argument, unwrapping NamedArgument and SplatExpression. */ private processCallArg; /** Process resolved arguments into TsNodes, tracking function usage. */ private processResolvedArgs; /** * Adjust call-site arguments to match the function's parameter list: * 1. Pad omitted optional args (those with defaults) with null * 2. Wrap extra args into an array for variadic params */ private processNode; private processKeyword; /** * Walk a function/node body and collect every typeAlias declaration * that belongs to this body's scope. Used to hoist body-local type * aliases up to the enclosing function/node's outer scope so the * generated zod schemas are visible to every runner.step closure. * * Delegates body recursion to `walkNodes` so any new body-bearing * AST node (thread, parallelBlock, seqBlock, …) is automatically * handled. Aliases nested inside a function/graphNode/class method * are skipped — those defs hoist their own aliases when their bodies * are built. */ private collectBodyTypeAliases; /** * Hoist body-local type aliases. Returns the generated TS declarations * (to be inserted at the top of the enclosing function/node body) and * marks each AST node so processNode skips its in-body emission. * * Coalesces duplicates by alias name: if the same name appears in * multiple branches/blocks, only the first declaration is emitted to * avoid redeclaration errors at the function scope. (The Agency * typechecker is responsible for diagnosing genuine name collisions.) */ private hoistBodyTypeAliases; private processTypeAlias; /** * Build a zod validation schema string for a VariableType taken from user * source. Deep-resolves user-defined generic aliases first so that uses * like `Container` become a concrete object/array/etc. before the * (alias-unaware) zod mapper runs. */ private zodSchemaFor; /** See build() — plain top-level aliases in source order. */ private aliasEmissionOrder; /** * Aliases whose schema const is NOT yet initialized when module-load * code originating at source offset `start` runs: every plain top-level * alias declared at-or-after that offset. A PURE derivation from the * program captured in build() — no mutable emitted-set to keep in sync. * Generic/value-param aliases never emit consts (inlined / hoisted * factory functions) and function-body aliases initialize at call time, * so neither is listed. `start` undefined treats ALL listed aliases as * pending — z.lazy is always safe; over-inclusion only costs * indirection. */ private pendingAliasesFor; /** * Build the validation expression for a `!` site. If the resolved type * carries no `@validate(...)` tag anywhere, return the existing * `__validateType(value, schema)` call (zero behavior change). Otherwise * return `await __validateChainRecursive(__run, value, )`, * which runs Zod parse + the validator chain at each level. */ private validateExpr; /** `__schemaCheck(() => )` or `__descriptorCheck(() => )` * for a `!` parameter of type `t`: the same choice `validateExpr` makes, * as a check the parameter table keeps. */ private paramCheckValidate; /** The one decision behind every `!` site, whether a checked cast or a * parameter: a type that reaches no `@validate(...)` tag is checked by * a zod schema alone, and a type that does is walked with a descriptor * that runs zod and the validator chain at each level. The two emitters * above only differ in how they wrap the result. */ private validationFor; private processComment; private processDecoratorCallField; private processAgencyObject; private processAgencyArray; private generateLiteral; private generateStringLiteralNode; private processValueAccess; /** Emit an assignment/update *target*. A `valueAccess` target must stay a * raw lvalue (never `__nn`-wrapped); anything else goes through the normal * value path. Used by `++`/`--` and compound-assignment (`+=` etc.). */ private processAssignTarget; private awaitChainCall; private processBinOpExpression; private processRegexMatchExpression; private processCatchExpression; private processPipeExpression; private processTryExpression; private processNewExpression; private processSchemaExpression; /** * A type pattern's runtime test (`x is T`, arm `p: T`). Tier 1 (coarse * primitives and `any[]`) compiles to `__coarseTypeTest(v, kind)`; every * other type reuses the bang's validation path via `validateExpr`, wrapped * in `isSuccess(...)` — shape AND `@validate` validators decide the match. */ /** An unchecked cast exists only for the type checker. A checked cast is * the same validation a `!` declaration runs. */ private processCastExpression; private processTypeTestExpression; /** * After the ALS migration AND the trailing-`state`-arg drop, every * Agency call site reads `ctx` / `stack` / `threads` / `callsite` * from the active `agencyStore` frame. `__call` / `__callMethod` no * longer accept a state-extras object, and the codegen never emits * one. This helper is kept as a stable seam in case future codegen * needs to attach per-call metadata, but for now it always returns * `undefined` — callers should simply omit the third positional. * * Async-fork sites that need to override the active branch stack * wrap their `__call(...)` invocation in an `agencyStore.run` * frame in codegen (see `emitRuntimeDispatchCall`) — not via this * helper. */ private buildStateConfig; private processIfElseWithSteps; private processForLoopWithSteps; private processWhileLoopWithSteps; private processMatchBlockWithSteps; /** Lowered `return` inside a match arm used as an expression. Same * halt+return shape as `processReturnStatement`, but the value is stored as * the match result via `runner.exitMatch` and control unwinds to the owning * ifElse rather than the enclosing function. Never produced by the parser — * only by pattern lowering (Task 6). */ /** A graph-node call is a control-flow transition, not a value, so it cannot * be the result of a match/if arm. `processMatchYield` catches the common * case where the arm value is yielded directly; but a single-expression arm * whose value may interrupt is hoisted to a temp binding first (#430), which * hides the call from that check — so `_processAssignmentInner` re-runs this * guard on the marked binding. Shared here to keep one error message. */ private assertMatchArmValueNotGraphNode; private processMatchYield; /** * Plain-mode (handler body) routing for the two lowered shapes of a branching * construct: a match expression (literal arms → `matchBlock`, pattern arms → * `ifElse`, both carrying `matchExprId`) becomes a self-contained IIFE; a plain * statement match / if compiles as ordinary JS. Single source of truth so the * `matchBlock` and `ifElse` dispatch cases can't drift apart — a mismatch would * silently emit a statement match whose consumer reads an unassigned * `__matchval_N` as `undefined`. */ private processPlainBranching; /** * Compile an expression-position `match` inside a handler body (plain mode). * * Stepped code unwinds a match arm's value via `runner.exitMatch` + the owning * `runner.ifElse` clearing `_matchExit`. A handler body compiles to plain JS * with no owning `runner.ifElse`, so that protocol would leak the flag and * silently skip the rest of the enclosing node. Instead we emit the arm * dispatch as an async IIFE whose arms `return` their value, and store the * result in the `__matchval_` frame local the consumer reads: * * __stack.locals.__matchval_ = await (async () => { })(); * * `node` is either the pass-through `matchBlock` (literal arms) or the lowered * `ifElse` chain (pattern arms) — both carry `matchExprId`. `processBlockPlain` * builds the plain if/else for either; `insidePlainMatchExpr` makes the arms' * `matchYield` nodes emit real returns out of the IIFE. */ private processMatchExpressionPlain; /** * The frame a `__matchval_` temp lives in: `__bstack` inside a block, * `__stack` in a node or function. The stepped writer (`runner.exitMatch`) * uses the running runner's frame, which is the same choice. Every read * and the plain-mode write go through here so they cannot drift (#928). */ private matchValFrameScope; private processImportStatement; private processImportNodeStatement; /** * Process a block argument into a wrapped AgencyFunction TsNode. * Shared by generateFunctionCallExpression and buildCallDescriptor. */ /** * Compile a block body in stepped mode even when the block appears inside * a handler body. A block gets its own frame and Runner, so its `return` * must be `runner.halt(...)`, not the handler's plain `return` (which would * exit a step callback and drop the value). Resets both plain-mode flags * for the duration of `compile` and restores them after. */ private stepped; private processBlockArgument; /** * Compile a BlockArgument that appears as a standalone expression * (e.g. as a named arg value in .partial(), or assigned to a variable). * Uses type annotations on the block params if present, falls back to 'any'. */ private processBlockAsExpression; /** * Classify a parameter's contribution to a tool's JSON schema. * * Single source of truth for "what does this param contribute?" — consumed * by `buildToolDefinition` (and only by it). Three outcomes: * * - `drop` : function-typed, function-union, or variadic-of-function. * Omitted from the schema entirely. The required-vs-optional * distinction is enforced separately by the tool-binding * validator (lib/typeChecker/toolBlockBinding.ts). * - `scalar` : a regular field — type derived from the declared hint * (`string` is the historical default for untyped params). * Optional params get `.nullable().describe("Default: ...")` * appended so the LLM understands the value can be omitted. * - `array` : a variadic param whose element type is non-function. The * emitted schema is `z.array()` so the LLM supplies * the whole spread as a single array via the named-array * calling convention (`foo(rest: [1,2,3])`). */ private paramSchemaContribution; /** * A thunk for the zod schema of the value a call to `node` returns, or * null when there is none to give: the def is not exported (only an * exported def can be a workflow tool), declares no return type, or its * type holds a function or a `regex` anywhere, which JSON Schema cannot * express, directly or through an alias. A `Result`, written directly * or through an alias, gives `T`'s schema: a caller reads the value, and * a workflow step's reference goes through `.value`, as `describe`'s * `returnsResult` says. A thunk, so no schema is built when the module * loads, only when `catalog` asks; by then every alias const exists. */ private returnSchemaFor; /** `type` as written and as `resolveTypeDeep` inlines it (a generic * alias use such as `Box` is not a name, so only the resolved * form shows its body), and the body of every alias either names, at * any depth. */ private aliasClosure; /** * An alias the schema for `type` would print by name without a binding, * or null. The schema prints a plain alias by name and leaves its body to * the module that declares it, so only the names it prints need a * binding: those left after `resolveTypeDeep` inlines generic aliases, * plus the body of a value-parameterized alias, which the schema inlines * at the use site. An alias that reached the type table through an * imported signature or another alias's body (`pullTransitiveAliases`) * has no const here and no JavaScript import, so naming it would fail at * module load (#1371). A parameter schema has the same gap; this keeps a * return schema from widening it. */ private unboundAliasIn; /** * Build a tool definition TsNode for an Agency function. * Returns ts.id("null") if the function has no parameters (no schema needed for tools). * * Every "should this param appear in the schema?" decision is funneled * through `paramSchemaContribution` — the single classifier that knows * which params are dropped (function-typed, function-union, variadic of * function), which become scalar, and which become array (variadic). * No ad-hoc filter or inline predicate is allowed in this function; the * spec's discipline rule (§4.6 rule #3) is enforced by code review. */ private buildToolDefinition; /** * Returns true if any function or graph node in the compilation unit * has a doc string with at least one interpolation segment. */ private hasDocStringInterpolation; /** * Generate __toolRegistry as an empty object. AgencyFunction.create() calls * register local functions into it. Imported and builtin tools are registered * here directly. The reviver is bound at the end. */ private generateToolRegistry; /** * For Result-returning functions: emit a pinned checkpoint at function entry * and a preamble that applies arg overrides on restore (for result.retry()). */ /** `paramStore`, skipped when the frame already holds the slot. A fresh * frame is empty, so the first entry stores the argument as given. A * resume re-enters with the saved frame, whose slot may hold something * else that the argument as given must not overwrite: for a `!` * parameter, the checked value step 0 wrote (a zod check drops unknown * keys, so the two can differ); for every parameter of a decorated * function, the value its decorator rebound with `block(record)` * (docs/dev/language/decorators.md). */ private firstEntryParamStore; /** `__stack.args["name"] = name`, with the default applied when the * argument was left unset. */ private paramStore; /** A function's `!` parameter checks, emitted once at module level as * `const __name_paramChecks = { name: { typeText, validate: __schemaCheck(() => ...) } }`. * `validate` is `__schemaCheck` for a type that runs no validator and * `__descriptorCheck` for one that reaches a `@validate` tag, the same * split `validateExpr` makes for a checked cast. Both build the schema or * descriptor on the first check and keep it. Empty when the function has * no `!` parameter. */ private paramChecksDecl; private paramChecksTable; /** The module-level name of a function's `!` parameter checks table. */ private paramChecksName; /** The rebind statement a decorated body's block starts with: one * `__rebindParameters` call over a table of the function's parameters, * each default as a thunk compiled here so it sees the function's scope, * then the halt check every parameter check ends with. The function's * block-typed parameters are passed by name as values to keep. */ private processRebindParameters; /** `await __checkParams(__run, runner, __stack, "name", __name_paramChecks, names)` * and the halted return, over the `!` parameters; nothing when none is * checked. Functions and nodes both start with it (see the "step 0" * section of docs/dev/language/validation-annotations.md). */ private checkParamsCall; private buildResultCheckpointSetup; /** * The argument overrides a resume can carry (`agency resume --arg` or * `restore(cp, { args })`). `__takeArgOverrides` writes the ones that * name this frame into the argument slots and returns their names, or * `undefined` when none were pending. The parameter locals are then * re-read from the slots, since `onFunctionStart` reports them. Emitted * in the preamble, after the stores and before step 0, in every * function, so overrides addressed to a function with no parameters are * still consumed. An override is checked only when step 0 runs on this * entry; see "Overridden arguments" in * docs/dev/language/validation-annotations.md. */ private takeArgOverrides; /** * Build the body statements for an Agency function or class method. * Includes setup, runner, result checkpoint, try/catch/finally, hooks. * Shared between processFunctionDefinition and class method compilation. */ private buildFunctionBody; private processFunctionDefinition; private processStatement; private interruptTemplateArgs; private buildInterruptReturnStructured; private extractInterruptFields; private isInterruptExpression; private processInterruptStatement; private processFunctionCallAsStatement; private processFunctionCall; private generateFunctionCallExpression; private emitRuntimeDispatchCall; /** * Emit a plain direct function call: `f(arg1, arg2, blockArg?)`. * Context-injected builtins reuse this with `prependArgs = * [__ctx]`; the registry lookup at the call site is what marks the * intent, no separate method needed. */ private emitDirectFunctionCall; /** * Build a CallType descriptor TsNode for an Agency function call. * Determines whether to emit positional or named call type based on arguments. */ private buildCallDescriptor; private processForkCall; private generateNodeCallExpression; private processGraphNode; /** Substep 0 of a node: the `!` parameter checks, as in a function's * step 0, then the onNodeStart hook. A refused argument halts the node * before the hook fires. */ private nodeStartHook; /** If the enclosing function/node has returnTypeValidated, wrap value in __validateType */ private maybeWrapReturnValidation; private processGotoStatement; private processReturnStatement; /** In a finalize-bearing scope, a direct-call return must stop at the * finalize instead of passing an aborted result through (pass-through * would silently skip the finalize). AG6036 guarantees a direct call * is the only call-bearing return shape that reaches codegen here; * `return llm(...)` hoists through its own path and cannot produce an * AbortedResult. */ private isFinalizeInterceptedReturn; private processAssignment; /** True when an assignment RHS can evaluate to a bubbled Interrupt[] * without any halt check of its own: a tryExpression anywhere in a binOp * tree. `__tryCall` and `__catchResult` both forward a non-Result value * unchanged, and `||`/`??` pass a (truthy, non-null) array through — so * `try f()`, `try f() catch v`, and `try f() || x` all deliver the raw * batch to the assignment. Plain functionCall RHS is handled by its own * richer branch (async fork setup etc.), not this predicate. */ private rhsMayBubbleInterrupts; /** The propagation guards emitted after a sync assignment whose RHS is * a call (or a `try` expression that may deliver a callee's raw * result). Two markers can come back instead of a normal value, and * both must stop this scope: * * - an Interrupt[] — the callee paused; halt so the batch propagates * (awaitAll first), or throw inside a handler body, where interrupts * are not allowed. * - an AbortedResult — the callee was aborted; this scope stops too and * returns ITS OWN saved draft via carryThrough (the callee's partial * is dropped: salvage is opt-in per level). Nodes and handler bodies * rebuild the exception instead, so everything above compiled code * (the graph engine, the CLI entry) sees aborts exactly as before. * * Returns null only at global scope for the interrupt half; the aborted * check still applies there (an abort during module init should crash * init, not become data in a global). */ private assignmentInterruptGuard; /** The aborted-result half of the post-call guard. Function and block * scopes halt with their own AbortedResult (carryThrough applies the * salvage rule); handler bodies and every other scope rebuild the * exception — handlers run outside the runner-step machinery (there is * no runner to halt), and nothing above compiled code consumes aborted * values. * * In a finalize-bearing scope, the stop runs the finalize — and when * the guarded value is a real local (`bindOnAborted`), the callee's * partial is first bound into it via partialValueOrNull(), so the * finalize reads it like any other local. Bare-call temps have nothing * to bind. */ private assignmentAbortedGuard; private _processAssignmentInner; /** * Process an llm() function call. Generates a direct runPrompt() call * (no inner function wrapper). Handles async/sync, interrupt checking, * response format from type hints, and tools from config object. */ private processLlmCall; private processDebuggerStatement; private processMessageThread; private processBlockPlain; /** * Emit a plain-JS for-loop (used inside handler bodies) that iterates arrays * by (element, index) and records/plain objects by (key, value) — the same * array-vs-record normalization `Runner.loop` applies in stepped mode. The * old emission looped `i < iterable.length` over `iterable[i]`, which over a * record iterated zero times (`.length` is undefined) and made `for (k in * record)` a `for...of` over a non-iterable object (a TypeError). All three * for-loop implementations (this, `Runner.loop`, the type checker) now agree. * * const __src = ; * const __isArr = Array.isArray(__src); * const __keys = __isArr ? __src * : (__src != null && typeof __src === "object" ? Object.keys(__src) : []); * for (let __i = 0; __i < __keys.length; __i++) { * const = __keys[__i]; // element | key * const = __isArr ? __i : __src[__keys[__i]]; // index | value * ...body * } */ private processPlainForLoop; /** * Compile the synthetic `__objectRest(source, ["a", "b", ...])` call emitted * by the pattern lowering pass for `let { a, b, ...rest } = obj` into a * native-JS IIFE. No runtime helper required. * * __objectRest(source, ["a", "b"]) * → (({ a: __k0, b: __k1, ...__r }) => __r)() * * For `let { ...rest } = obj` (no excluded keys), emits * (({ ...__r }) => __r)() */ private buildObjectRestIIFE; private processNodeInGlobalInit; private buildHandlerArrow; private processHandleBlockWithSteps; private processWithModifier; /** In debugger mode, insert debuggerStatement nodes before each * step-triggering statement so that debugStep() is called at every * substep boundary, not just top-level steps. */ private insertDebugSteps; /** `__activation.__removedTools`: the tool loop's list of tools removed from * the conversation, kept on the enclosing function's frame so a block * argument that runs llm() more than once sees the same list each time. */ private removedToolsSlot; private processBodyAsParts; /** One statement of plain-mode code, with a destructive pre-flip in * front when it owns a destructive call. Every plain statement list * compiles its statements through this, as every stepped one goes * through `processBodyAsParts`. See DestructiveTracking.plainFlip. */ private withPlainFlip; private generateBuiltins; /** Build the baked `smoltalkDefaults` object: nested per-provider `apiKey` * and `baseUrl` maps (each key/URL falling back to its conventional env * var), plus model/logLevel/statelog and an optional default provider. * `ollama` is intentionally omitted from `apiKey` — it uses OLLAMA_HOST. */ private buildSmoltalkDefaults; private generateImports; /** The module's source file, as code: the path from the compiled file's * folder to its `.agency` file, joined to `__dirname` when the module * loads. Null when the output file or the source file is unknown. */ private sourceFileCode; private preprocess; private postprocess; }