/** * Loop — iteration composition: runs a body runner repeatedly until exit. * * Pattern: Builder (GoF) + Adapter over footprintjs's `loopTo` + `$break`. * Role: core-flow/ layer. Enables Reflection, Self-Refine, Debate, * Reflexion, Constitutional AI, and any pattern needing * iterative refinement of a composition output. * Emits: agentfootprint.composition.enter / exit + * composition.iteration_start / iteration_exit * (via compositionRecorder). * * Budget guard is MANDATORY. You must set at least one of: * - maxIterations (default 10 if only .body() is set) * - maxWallclockMs * Hard ceiling of 500 iterations prevents runaway loops even if a guard * misfires; exceeding it throws. */ import { type FlowchartCheckpoint, type RunOptions, type StructureRecorder } from 'footprintjs'; import type { GroupMetadata, GroupTranslator } from '../core/translator.js'; import type { RunnerPauseOutcome } from '../core/pause.js'; import type { Runner } from '../core/runner.js'; import { RunnerBase } from '../core/RunnerBase.js'; export interface LoopOptions { readonly name?: string; readonly id?: string; /** * Optional build-time recorders passed through to footprintjs's * `flowChart()` factory. Each recorder observes per-node build * events (`onStageAdded` / `onSubflowMounted` / etc.) for this * composition's internal chart (Seed + IterationStart + body mount + * Guard). When omitted, no build-time observation is wired up. */ readonly structureRecorders?: readonly StructureRecorder[]; /** * Optional per-COMPOSITION translator (UI-agnostic). See * `core/translator.ts`. When attached, `runner.getUIGroup()` invokes * it with the Loop's `GroupMetadata` (kind `'Loop'`, id, name, body * as the single member, plus iteration budgets in `extra`). * Returns `undefined` when omitted. */ readonly groupTranslator?: GroupTranslator; } export interface LoopInput { readonly message: string; } export type LoopOutput = string; type BodyChild = Runner<{ message: string; }, string>; /** * Predicate evaluated AFTER each body iteration. Return true to exit the loop. * * `latestOutput` is the body's STRING output — by design (B15): the whole * core-flow layer composes `Runner<{ message: string }, string>`, and the * Loop chart's outputMapper coerces any non-string body output to `''`. * For structured exit conditions today, have the body emit JSON (e.g. an * Agent with `.outputSchema(...)`) and parse inside the guard: * * ```ts * .until(({ latestOutput }) => { * try { return (JSON.parse(latestOutput) as { done: boolean }).done; } * catch { return false; } // not JSON (yet) — keep looping * }) * ``` * * A typed guard (`Loop` with a structured body output) would require * genericizing the Runner output contract shared by Sequence / Parallel / * Conditional — tracked as a future enhancement, not in core today. */ export type UntilGuard = (ctx: { readonly iteration: number; readonly latestOutput: string; readonly startMs: number; }) => boolean; export declare class Loop extends RunnerBase { readonly name: string; readonly id: string; private readonly body; private readonly maxIterations; private readonly maxWallclockMs; private readonly until; private readonly opts; /** Per-method translator override on `.repeat()`, when set. Applies * to the body member's `uiGroup`. */ private readonly bodyTranslator; private currentRunContext; constructor(opts: LoopOptions, body: BodyChild, config: { maxIterations: number; maxWallclockMs?: number; until?: UntilGuard; bodyTranslator?: GroupTranslator; }); static create(opts?: LoopOptions): LoopBuilder; protected getGroupTranslator(): GroupTranslator | undefined; /** Loop has a single body member + iteration budgets in `extra`. * Per-method override (L1c) takes precedence over the body * runner's own translator. */ protected buildUIGroupMetadata(): GroupMetadata; run(input: LoopInput, options?: RunOptions): Promise; resume(checkpoint: FlowchartCheckpoint, input?: unknown, options?: RunOptions): Promise; private createExecutor; private finalizeResult; private buildChart; } /** * Fluent builder. Reads as natural English: * Loop.create().repeat(runner).times(10).forAtMost(30_000).until(fn).build() * → "Loop: repeat runner, up to 10 times, for at most 30 seconds, until fn." * * Enforces a body runner is supplied before .build(). Default budget is * 10 iterations (hard ceiling 500). Any of .times / .forAtMost / .until * can fire to exit the loop. */ /** * Options bag accepted by `LoopBuilder.repeat()` for per-method overrides. */ export interface LoopRepeatOptions { /** Per-method translator override for the body runner — overrides * the runner's own constructor-level translator for THIS loop only. */ readonly groupTranslator?: GroupTranslator; } export declare class LoopBuilder { private readonly opts; private _body; private _bodyTranslator; private _maxIterations; private _maxWallclockMs; private _until; constructor(opts: LoopOptions); /** * The runner that executes each iteration. Required. * Each iteration's output string becomes the next iteration's input `{ message }`. * * Optional second arg `opts.groupTranslator` overrides the body * runner's own translator for THIS loop only — only its * `member.uiGroup` flips to the override's output. */ repeat(runner: BodyChild, opts?: LoopRepeatOptions): this; /** * Maximum iteration count. Default 10 if only `.repeat()` is called. * Hard ceiling 500 — larger values are clamped. */ times(n: number): this; /** * Wall-clock time budget in milliseconds. The loop exits at the next * guard check after this elapses. */ forAtMost(ms: number): this; /** * Exit predicate evaluated after each iteration. Return `true` to exit. * Receives `{ iteration, latestOutput, startMs }`. * * `latestOutput` is the body's string output. For structured exit * conditions, emit JSON from the body and parse it inside the guard — * see the `UntilGuard` JSDoc for the pattern and the design rationale. */ until(guard: UntilGuard): this; build(): Loop; } export {};