agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
170 lines (169 loc) • 7.08 kB
TypeScript
/**
* 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 StructureRecorder } from 'footprintjs';
import type { RunOptions } from '../core/runner.js';
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';
import type { MemoryIdentity } from '../memory/identity/types.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;
/**
* WHO this run is for — the same tuple as `AgentInput.identity`. Every
* member runs for it: its memory, its tools' `ctx.identity`. Omitted, every
* member runs under this run's own conversation (`{ conversationId: runId }`),
* never a namespace another run shares (`core/memberRun.ts`).
*/
readonly identity?: MemoryIdentity;
}
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<T>` 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<LoopInput, LoopOutput> {
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 | string, options?: RunOptions): Promise<LoopOutput | RunnerPauseOutcome>;
resume(checkpoint: FlowchartCheckpoint, input?: unknown, options?: RunOptions): Promise<LoopOutput | RunnerPauseOutcome>;
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 {};