agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
129 lines (128 loc) • 5.85 kB
TypeScript
/**
* Conditional — routing composition: evaluates branches in order, runs the first match.
*
* Pattern: Builder (GoF) + Adapter over footprintjs's `addDeciderFunction`.
* Role: core-flow/ layer. Picks exactly ONE branch based on a predicate
* (sync function of input) OR an LLM decision. Chosen branch
* receives `{ message }` and returns a string.
* Emits: agentfootprint.composition.enter / exit +
* composition.route_decided (via compositionRecorder).
*
* v1 of this primitive supports `.when(id, predicate, runner)` + `.otherwise(id, runner)`.
* LLM-gated routing (`.whenLLM(id, prompt, runner)`) lands in Phase 5.
*/
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 ConditionalOptions {
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 + Route decider + each branch
* mount + Finalize). 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 Conditional's `GroupMetadata` (kind `'Conditional'`,
* id, name, branches as members, plus `extra.fallbackId`).
* Returns `undefined` when omitted.
*/
readonly groupTranslator?: GroupTranslator;
}
export interface ConditionalInput {
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 ConditionalOutput = string;
type BranchChild = Runner<{
message: string;
}, string>;
export type Predicate = (input: ConditionalInput) => boolean;
interface BranchEntry {
readonly id: string;
readonly name: string;
readonly runner: BranchChild;
/** Undefined for the `otherwise` fallback. */
readonly predicate?: Predicate;
/** Optional per-method translator override for THIS branch only. */
readonly groupTranslator?: GroupTranslator;
}
/**
* Options bag accepted by `ConditionalBuilder.when()` and `.otherwise()`
* for per-method overrides. Backwards-compatible — when the trailing
* arg is a string, it's still treated as `name`.
*/
export interface ConditionalBranchOptions {
/** Human-friendly name for this branch. Default: the branch id. */
readonly name?: string;
/** Per-method translator override. See `BranchEntry.groupTranslator`. */
readonly groupTranslator?: GroupTranslator;
}
export declare class Conditional extends RunnerBase<ConditionalInput, ConditionalOutput> {
readonly name: string;
readonly id: string;
private readonly branches;
private readonly fallbackId;
private readonly opts;
private currentRunContext;
constructor(opts: ConditionalOptions, branches: readonly BranchEntry[], fallbackId: string);
static create(opts?: ConditionalOptions): ConditionalBuilder;
protected getGroupTranslator(): GroupTranslator | undefined;
/** Conditional: one member per branch (.when / .otherwise), plus
* `extra.fallbackId` marking the otherwise branch. Per-method
* overrides (L1c) take precedence over the branch runner's own
* translator. */
protected buildUIGroupMetadata(): GroupMetadata;
run(input: ConditionalInput | string, options?: RunOptions): Promise<ConditionalOutput | RunnerPauseOutcome>;
resume(checkpoint: FlowchartCheckpoint, input?: unknown, options?: RunOptions): Promise<ConditionalOutput | RunnerPauseOutcome>;
private createExecutor;
private finalizeResult;
private buildChart;
}
/**
* Fluent builder. Branches evaluate in registration order; first matching
* predicate wins. `.otherwise()` is the mandatory fallback.
*/
export declare class ConditionalBuilder {
private readonly opts;
private readonly branches;
private fallbackRegistered;
private fallbackId;
private readonly seenIds;
constructor(opts: ConditionalOptions);
/**
* Register a predicate-gated branch. `predicate` is a pure sync function
* of the Conditional's input; if it returns true, the corresponding
* runner executes. Branches evaluate in registration order.
*
* Fourth arg accepts EITHER a legacy bare `name` string OR a
* `ConditionalBranchOptions` bag containing `name` and/or a per-method
* `groupTranslator` override. The override applies ONLY to this
* branch's `member.uiGroup`.
*/
when(id: string, predicate: Predicate, runner: BranchChild, nameOrOpts?: string | ConditionalBranchOptions): this;
/**
* Register the fallback branch. Exactly ONE must be registered before build().
* Third arg accepts a legacy `name` string OR a `ConditionalBranchOptions`
* bag (same shape as `.when()`).
*/
otherwise(id: string, runner: BranchChild, nameOrOpts?: string | ConditionalBranchOptions): this;
build(): Conditional;
}
export {};