/** * FlowChartBuilder — Fluent API for constructing flowchart execution graphs. * * Builds StageNode trees and SerializedPipelineStructure (JSON) in tandem. * Zero dependencies on old code — only imports from local types. * * The builder creates two parallel structures: * 1. StageNode tree — runtime graph with embedded functions * 2. SerializedPipelineStructure — JSON-safe structure for visualization * * The execute() convenience method is intentionally omitted — * it belongs in the runner layer (Phase 5). */ import type { ScopeFactory } from '../engine/types.js'; import type { PausableHandler } from '../pause/types.js'; import type { TypedScope } from '../reactive/types.js'; import { type RunnableFlowChart } from '../runner/RunnableChart.js'; import type { StructureEdgeKind, StructureRecorder } from './structure/StructureRecorder.js'; import { StructureRecorderDispatcher } from './structure/StructureRecorderDispatcher.js'; import { type TypedStageFunction } from './typedFlowChart.js'; import type { FlowChart, FlowChartOptions, FlowChartSpec, ILogger, SerializedPipelineStructure, SimplifiedParallelSpec, StageFunction, StageNode, StreamLifecycleHandler, StreamTokenHandler, SubflowMountOptions } from './types.js'; /** * Fluent helper returned by addDeciderFunction to add branches. * `end()` sets `deciderFn = true` — the fn IS the decider. */ export declare class DeciderList { private readonly b; private readonly curNode; private readonly curSpec; private readonly branchIds; private defaultId?; private readonly parentDescriptionParts; private readonly parentStageDescriptions; private readonly reservedStepNumber; private readonly deciderDescription?; private readonly branchDescInfo; constructor(builder: FlowChartBuilder, curNode: StageNode, curSpec: SerializedPipelineStructure, parentDescriptionParts?: string[], parentStageDescriptions?: Map, reservedStepNumber?: number, deciderDescription?: string); addFunctionBranch(id: string, name: string, fn?: StageFunction, description?: string, /** `{ loopTo }` declares this branch loops back to an already-declared * stage — the loop is SOURCED FROM THIS BRANCH (not the decider). */ options?: { readonly loopTo?: string; }): DeciderList; /** * Add a pausable stage as a decider branch. * * When this branch is chosen, the handler's `execute` runs. If it returns * data, the pipeline pauses. On resume, `handler.resume` runs with the * human's input. If `execute` returns void, the stage continues normally * (conditional pause). */ addPausableFunctionBranch(id: string, name: string, handler: PausableHandler, description?: string, /** `{ loopTo }` declares this branch loops back to an already-declared * stage — the loop is SOURCED FROM THIS BRANCH (not the decider). */ options?: { readonly loopTo?: string; }): DeciderList; addSubFlowChartBranch(id: string, subflow: FlowChart, mountName?: string, options?: SubflowMountOptions): DeciderList; addLazySubFlowChartBranch(id: string, resolver: () => FlowChart, mountName?: string, options?: SubflowMountOptions): DeciderList; addBranchList(branches: Array<{ id: string; name: string; fn?: StageFunction; }>): DeciderList; setDefault(id: string): DeciderList; /** * Attach a loop-back edge to the LAST-added branch, so the loop is sourced * from THAT branch node (e.g. `'tool-calls' → loopTo('context')`) rather than * from the decider. The chart then reads honestly: the decider splits into a * looping branch `[ToolCalls → back to Context]` and a terminating branch * `[Final → end]`, instead of a single loop hanging off the decider. * * No engine change is needed: the runtime runs the chosen branch and then * follows that branch node's OWN `next` — and a `next` flagged `isLoopRef` * routes back to the target exactly like the decider's own loop does. This * method just lets the builder express what the engine already supports. * * Targets the branch added immediately before this call (chain it right after * the branch's `addFunctionBranch`/`addPausableFunctionBranch`/ * `addSubFlowChartBranch`). Mirrors `FlowChartBuilder.loopTo` validation. * * Works on a SUBFLOW branch too: the branch node carries both its subflow * resolver AND the loop-back `next` — they coexist safely (the runtime runs * the subflow, then follows the loop ref). The target must be a stage already * declared BEFORE the decider (e.g. an upstream `context`); branch ids and the * synthetic `'default'` clone are NOT valid loop targets. */ loopTo(stageId: string): DeciderList; /** * Decorate ONE branch node/spec with a loop-back edge to `stageId`. Shared by * the positional `loopTo()` (which targets the last-added branch) AND the * per-branch `{ loopTo }` option on `addFunctionBranch` / * `addPausableFunctionBranch` / `addSubFlowChartBranch`. Either way the loop * SOURCE is the branch — so visualizers read `tool-calls → context`, never * `Route → context`. Validates the target is a stage declared BEFORE the * decider (branch ids / the synthetic 'default' clone are not valid targets). */ private _applyBranchLoop; end(): FlowChartBuilder; } export declare class SelectorFnList { private readonly b; private readonly curNode; private readonly curSpec; private readonly branchIds; private readonly parentDescriptionParts; private readonly parentStageDescriptions; private readonly reservedStepNumber; private readonly selectorDescription?; private readonly branchDescInfo; constructor(builder: FlowChartBuilder, curNode: StageNode, curSpec: SerializedPipelineStructure, parentDescriptionParts?: string[], parentStageDescriptions?: Map, reservedStepNumber?: number, selectorDescription?: string); addFunctionBranch(id: string, name: string, fn?: StageFunction, description?: string): SelectorFnList; /** * Add a pausable stage as a selector branch. * * When this branch is selected, the handler's `execute` runs. If it returns * data, the pipeline pauses. On resume, `handler.resume` runs with the * human's input. If `execute` returns void, the stage continues normally. */ addPausableFunctionBranch(id: string, name: string, handler: PausableHandler, description?: string): SelectorFnList; addSubFlowChartBranch(id: string, subflow: FlowChart, mountName?: string, options?: SubflowMountOptions): SelectorFnList; addLazySubFlowChartBranch(id: string, resolver: () => FlowChart, mountName?: string, options?: SubflowMountOptions): SelectorFnList; addBranchList(branches: Array<{ id: string; name: string; fn?: StageFunction; }>): SelectorFnList; end(): FlowChartBuilder; } export declare class FlowChartBuilder { private _root?; private _rootSpec?; private _cursor?; private _cursorSpec?; private _stageMap; _subflowDefs: Map; }>; private _streamHandlers; /** * L7.3 — Build-time observer fan-out. Owned by the builder so every * `addX()` method can fire `StructureRecorder` events at the natural * moment of the corresponding mutation. Dispatcher is allocated * lazily on first attach to keep the zero-recorder path allocation- * free. */ private _structureDispatcher?; /** * L7.3 — Sealed-after-build flag (Panel 2 phase invariant). Flips * to `true` when `.build()` returns; subsequent `attachStructureRecorder` * throws. Prevents the footgun where a consumer attaches a recorder * mid-execution and gets partial structure data (missed every event * already fired during construction). */ private _sealed; private _enableNarrative; private _logger?; private _descriptionParts; private _stepCounter; private _stageDescriptions; private _stageStepMap; private _knownStageIds; private _inputSchema?; private _outputSchema?; private _outputMapper?; private _scopeFactory?; /** * Attach a `StructureRecorder` for build-phase observation. Multiple * recorders coexist (same id allowed; iteration order = attach * order). Throws if called after `.build()` — the chart is sealed at * that point and any recorder attached late would miss every event * fired during construction. * * **Seed replay**: when this is called AFTER `start()` has already * fired (i.e., after the `flowChart()` factory returns), the * just-attached recorder receives a one-time `onStageAdded` for the * root stage so it observes the seed. Only the new recorder sees * the replay; already-attached recorders are not re-fired. * * **Mid-chain attach caveat**: a recorder attached AFTER one or more * `addX()` calls receives the seed replay but MISSES every * intermediate event. Attach BEFORE the first `addX()` for complete * capture. * * Public for now to enable direct attach in tests + early consumers. * L7.4 will wire `flowChart(..., { structureRecorders: [...] })` as * an additional registration site; this method will remain. */ attachStructureRecorder(recorder: StructureRecorder): this; /** * Inspect accumulated `StructureBuildError`s. Returns empty array * when no recorders attached OR no errors occurred. Returns a * defensive copy — caller mutations do not affect subsequent calls. * * **Call on the BUILDER, not the chart returned by `.build()`.** * Capture the builder reference before `.build()` if you need * post-build access: * ```ts * const builder = flowChart(...).attachStructureRecorder(rec); * const chart = builder.build(); * const errors = builder.getStructureBuildErrors(); * ``` */ getStructureBuildErrors(): ReturnType; private _fireStageAdded; private _fireEdgeAdded; private _fireLoopEdgeAdded; /** * Fire the `next` edge(s) from a parent spec to a freshly-added * node — with convergence expansion when the parent is a * fork / decider / selector with branches. * * A fork at `parent` is semantically `parent ──fork-branch──► child[i]` * for each child, and the chained `.addFunction(X)` continues * AFTER the fork converges. The runtime semantics are that each * child INDEPENDENTLY feeds `X` (parallel completion → join). The * literal "edge from parent to X" would misrepresent this — * visualizers and topological algorithms would see one edge where * there should be N convergence edges. * * Fix: when `parentSpec` has branch children (fork or branched * decider/selector), fire one `next` edge from EACH child to the * target. Otherwise fire the single edge from `parentSpec` itself. * * Loop-reference children (synthetic spec nodes created by * `.loopTo()`) are excluded — they're back-edge markers, not * convergence sources. A branch that carries an OWN loop-back `next` * (a branch-sourced `loopTo`) is likewise skipped — it loops, it does * not converge at the linear next stage. * * A branch carrying `convergeAt` is REDIRECTED: its single convergence * edge fires to its named target instead of `targetId` — expressing an * unequal-depth merge (e.g. `tools → call-llm`, bypassing `message-api`). * The named target is a forward stage, so it is NOT validated here. * * Call ORDER constraint: must be called BEFORE the cursor advances * to the new target. The caller passes the PRE-ADVANCE parent spec. */ private _fireNextEdgeFromParent; private _fireDeciderComplete; private _fireSubflowMounted; /** Sub-builder access (`.b._fireXxx`) is needed by DeciderList / * SelectorFnList; expose the dispatcher through internal helpers * that go through the same no-op-when-absent guard. * * @internal — these methods are exposed because TypeScript `private` * doesn't traverse class boundaries. Consumer code MUST NOT call * them; calling them post-construction lets a hostile caller * fabricate structure events and corrupt downstream visualizations * or audit trails. The `_` prefix is intentional convention. */ _fireEdgeAddedFromSubBuilder(from: string, to: string, kind: StructureEdgeKind, label?: string): void; /** @internal — see `_fireEdgeAddedFromSubBuilder`. */ _fireStageAddedFromSubBuilder(spec: SerializedPipelineStructure): void; /** @internal — see `_fireEdgeAddedFromSubBuilder`. */ _fireDeciderCompleteFromSubBuilder(decider: string, type: 'decider' | 'selector', branchIds: string[], defaultBranch?: string): void; /** @internal — see `_fireEdgeAddedFromSubBuilder`. */ _fireSubflowMountedFromSubBuilder(subflowId: string, subflowName: string, rootStageId: string, isLazy?: boolean, subflowSpec?: SerializedPipelineStructure, subflowPath?: string): void; /** @internal — see `_fireEdgeAddedFromSubBuilder`. Used by `DeciderList.loopTo` * to validate a branch-sourced loop target against the known stage ids * (mirrors `FlowChartBuilder.loopTo`'s `_knownStageIds.has` guard). */ _knownStageIdsHas(id: string): boolean; /** @internal — see `_fireEdgeAddedFromSubBuilder`. Used by `DeciderList.loopTo` * to fire a loop back-edge SOURCED FROM A BRANCH node (not the decider). */ _fireLoopEdgeAddedFromSubBuilder(from: string, to: string): void; private _appendDescriptionLine; private _appendSubflowDescription; setLogger(logger: ILogger): this; /** * Declare the API contract — input validation, output shape, and output mapper. * Replaces setInputSchema() + setOutputSchema() + setOutputMapper() in a single call. * * If a contract with input schema is declared, chart.run() validates input automatically. * Contract data is used by chart.toOpenAPI() and chart.toMCPTool(). */ contract(opts: { input?: unknown; output?: unknown; mapper?: (finalScope: Record) => unknown; }): this; start(name: string, fn: StageFunction | PausableHandler, id: string, description?: string): this; /** * Start a chart whose ROOT stage IS a selector — it runs first (reading * args, seeding state, returning the chosen branch ids via `select()`), * and its branches attach directly to the root. Mirrors `start()` for the * root-node setup, then returns a `SelectorFnList` bound to the root so * `.addFunctionBranch()` / `.addSubFlowChartBranch()` / `.end()` work * exactly as they do after `addSelectorFunction()`. * * Use when the first thing a chart does is choose among branches — e.g. a * `Context` selector that inits + picks which context slots to engineer, * with no separate seed stage before it. */ startSelector(name: string, fn: StageFunction, id: string, description?: string, options?: { failFast?: boolean; }): SelectorFnList; addFunction(name: string, fn: StageFunction, id: string, description?: string): this; addStreamingFunction(name: string, fn: StageFunction, id: string, streamId?: string, description?: string): this; /** * Add a pausable stage — can pause execution and resume later with input. * * The handler has two phases: * - `execute`: runs first time. Return `{ pause: true }` to pause. * - `resume`: runs when the flowchart is resumed with input. * * @example * ```typescript * .addPausableFunction('ApproveOrder', { * execute: async (scope) => { * scope.orderId = '123'; * return { pause: true, data: { question: 'Approve?' } }; * }, * resume: async (scope, input) => { * scope.approved = input.approved; * }, * }, 'approve-order', 'Manager approval gate') * ``` */ addPausableFunction(name: string, handler: PausableHandler, id: string, description?: string): this; /** * Add a stage that fires a child flowchart on the given driver and * DISCARDS the handle. Pure fire-and-forget — useful for telemetry * exports, audit log shipping, cache warm-up. * * @param id Stable id for this stage (also the stageMap key). * @param child The child flowchart to detach. * @param options.driver The driver to schedule on (e.g. `microtaskBatchDriver`). * @param options.inputMapper Maps the parent's scope to the child's input. * Defaults to passing `undefined`. * @param options.mountName Display name; defaults to `id`. * @param options.description Stage description for narrative + tools. * * @example * ```ts * import { microtaskBatchDriver } from 'footprintjs/detach'; * * flowChart('process', processFn, 'process') * .addDetachAndForget('telemetry', telemetryChart, { * driver: microtaskBatchDriver, * inputMapper: (scope) => ({ event: 'processed', orderId: scope.orderId }), * }) * .addFunction('next', nextFn, 'next') * .build(); * ``` */ addDetachAndForget(id: string, child: import('./types.js').FlowChart, options: { driver: import('../detach/types.js').DetachDriver; inputMapper?: (scope: TScope) => unknown; mountName?: string; description?: string; }): this; /** * Add a stage that fires a child flowchart on the given driver and * delivers the resulting `DetachHandle` to a consumer-supplied * `onHandle` callback. The handle CANNOT be stored in shared state * — `StageContext.setValue` calls `structuredClone` which drops * class prototypes (and therefore the handle's `.wait()` method). * * The callback pattern is the explicit alternative: keep handles in * a closure-local array (or whatever shape suits) and have a * downstream stage `await Promise.all(...)` over them. * * @example * ```ts * import { microtaskBatchDriver } from 'footprintjs/detach'; * import type { DetachHandle } from 'footprintjs/detach'; * * const handles: DetachHandle[] = []; * * const chart = flowChart('seed', seedFn, 'seed') * .addDetachAndJoinLater('eval-a', evalChart, { * driver: microtaskBatchDriver, * inputMapper: (scope) => scope.configA, * onHandle: (h) => handles.push(h), * }) * .addDetachAndJoinLater('eval-b', evalChart, { * driver: microtaskBatchDriver, * inputMapper: (scope) => scope.configB, * onHandle: (h) => handles.push(h), * }) * .addFunction('join', async (scope) => { * const settled = await Promise.all(handles.map((h) => h.wait())); * scope.results = settled; * }, 'join') * .build(); * ``` * * Note: putting `handles` in a module-level closure is fine for * single-run scripts. For server code that runs the same chart * concurrently across requests, allocate a new closure per run * (e.g., wrap chart construction in a factory function) so handles * from different runs don't bleed into each other. */ addDetachAndJoinLater(id: string, child: import('./types.js').FlowChart, options: { driver: import('../detach/types.js').DetachDriver; onHandle: (handle: import('../detach/types.js').DetachHandle) => void; inputMapper?: (scope: TScope) => unknown; mountName?: string; description?: string; }): this; addDeciderFunction(name: string, fn: StageFunction, id: string, description?: string): DeciderList; addSelectorFunction(name: string, fn: StageFunction, id: string, description?: string, options?: { failFast?: boolean; }): SelectorFnList; addListOfFunction(children: SimplifiedParallelSpec[], options?: { failFast?: boolean; }): this; addSubFlowChart(id: string, subflow: FlowChart, mountName?: string, options?: SubflowMountOptions): this; addLazySubFlowChart(id: string, resolver: () => FlowChart, mountName?: string, options?: SubflowMountOptions): this; addLazySubFlowChartNext(id: string, resolver: () => FlowChart, mountName?: string, options?: SubflowMountOptions): this; addSubFlowChartNext(id: string, subflow: FlowChart, mountName?: string, options?: SubflowMountOptions): this; loopTo(stageId: string): this; onStream(handler: StreamTokenHandler): this; onStreamStart(handler: StreamLifecycleHandler): this; onStreamEnd(handler: StreamLifecycleHandler): this; build(): RunnableFlowChart; /** Override the scope factory. Rarely needed — auto-embeds TypedScope by default. */ setScopeFactory(factory: ScopeFactory): this; toSpec(): TResult; toMermaid(): string; private _needCursor; private _needCursorSpec; /** * Advance the spec cursor. Retained as a method so call sites stay * one-liners and future cursor-related side effects have a hook. */ private _advanceCursorSpec; _stageMapHas(key: string): boolean; _addToMap(id: string, fn: StageFunction): void; _mergeStageMap(other: Map>, prefix?: string): void; _prefixNodeTree(node: StageNode, prefix: string): StageNode; _mergeSubflows(subflows: Record; }> | undefined, prefix: string): void; } export declare function flowChart(name: string, fn: TypedStageFunction | PausableHandler>, id: string, options?: FlowChartOptions): FlowChartBuilder>; export declare function flowChart(name: string, fn: StageFunction | PausableHandler, id: string, options?: FlowChartOptions): FlowChartBuilder; /** * Like `flowChart()`, but the ROOT stage is a SELECTOR — it runs first and * its branches attach directly to it (no separate seed stage). Returns a * `SelectorFnList`; declare branches then call `.end()` to get the builder * back for any subsequent stages. * * @example * flowChartSelector('Context', contextSelectorFn, 'context') * .addSubFlowChartBranch('sf-system-prompt', sysSlot, 'System Prompt', {...}) * .addSubFlowChartBranch('sf-messages', msgSlot, 'Messages', {...}) * .end() * .addFunction('messageAPI', assembleFn, 'message-api') * .build(); */ export declare function flowChartSelector(name: string, fn: StageFunction, id: string, options?: FlowChartOptions): SelectorFnList; export declare function specToStageNode(spec: FlowChartSpec): StageNode;