agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
103 lines (102 loc) • 5.03 kB
TypeScript
/**
* routeRecorder — records the skill-graph route a run actually took.
*
* A passive observer that reconstructs, hop by hop, which skill the agent was in,
* where it went next, and WHY — by COMPOSING already-shipped events (no engine
* change): `agentfootprint.context.evaluated` (its `routing[]` carries via/from/
* label per active skill-graph injection) + `agentfootprint.skill.rejected` (an
* out-of-reach read_skill) + `stream.tool_start` (the tool that drove a hop).
*
* Also folds in the GREY-AREA GOVERNORS (observability tier): it detects
* oscillation (A→B→A→B within `pingPongWindow`) and a run of consecutive rejected
* `read_skill` jumps (`maxRejectedRetries`), reported via `getTrips()`. These LABEL
* the trace (`onTrip:'stay'` semantics) — the hard "always stops" guarantee remains
* the agent's iteration cap; a runtime force-stop is a deferred follow-on.
*
* Pattern: CombinedRecorder (Convention 1 — single purpose: route evidence). Owns a
* `SequenceStore<RouteHop>`. Convention 4: resets on a new `runId`.
* Role: Tier-3 /observe recorder — `Agent.create(...).watch(routeRecorder())`.
* Powers the lens, the "Why this skill?" panel, and paper route figures.
*/
import type { EmitEvent } from 'footprintjs';
interface RunBoundaryEvent {
readonly traversalContext?: {
readonly runId?: string;
};
}
/**
* How the graph arrived at a skill on a hop.
*
* `'model-pick'` joined the union in 8.5.0 — a `read_skill` the gate accepted, which
* moved the cursor because no declared edge fired. It used to be recorded as a
* `'route'`, borrowing the label of whatever edge happened to point at the same
* skill, so the trace asserted that an edge fired when it had not. The cause now
* comes from the graph's own resolver (`cursorMove.by`), not from the drawn
* build-time provenance.
*
* `'intent'` and `'continuity'` joined in 9.17.0 (SG-C): the turn-start
* cascade's tier-2 scorer decisively routed the turn, and the inherited
* conversation cursor held it, respectively — both reported by the resolver
* on iteration 1, numbers on `agentfootprint.skill.turn_routed`.
*
* `'tool-proposal'` and `'decider'` joined in 9.19.0: an accepted
* `propose-transition` tool effect moved the cursor (deterministic tool
* evidence — outranked only by a declared edge), and the configured tier-3
* decider resolved an outstanding turn-start menu out-of-band, respectively.
*/
export type RouteOutcome = 'entry' | 'route' | 'model-pick' | 'tool-proposal' | 'intent' | 'continuity' | 'decider' | 'stay' | 'rejected';
/** One hop of the route — the skill the graph was in at one iteration + how. */
export interface RouteHop {
/** runtimeStageId of the iteration (the SequenceStore key). */
readonly runtimeStageId: string;
readonly iteration: number;
/** The skill before this hop (undefined at cold start). */
readonly fromSkill?: string;
/** The skill after this hop (undefined for a pure rejection). */
readonly toSkill?: string;
readonly outcome: RouteOutcome;
/** A human-readable reason for this hop (see `formatRouteHop`). */
readonly why: string;
/** The route edge's caption, when one drove the hop. */
readonly edgeLabel?: string;
/** The tool whose result drove the hop (most recent tool_start). */
readonly lastTool?: string;
/** Rejection only — the skill the model tried to jump to. */
readonly requestedSkill?: string;
/** Rejection only — the reachable set it was bounded to. */
readonly reachable?: readonly string[];
}
/** A governor trip — the route is misbehaving. */
export type RouteTripKind = 'ping-pong' | 'rejected-cap';
export interface RouteTrip {
readonly kind: RouteTripKind;
readonly iteration: number;
readonly skills: readonly string[];
readonly detail: string;
}
export interface RouteRecorderOptions {
readonly id?: string;
/** Window for oscillation detection (a [X,Y,X,Y] pattern trips). Default 4. */
readonly pingPongWindow?: number;
/** Consecutive rejected read_skill jumps before a `rejected-cap` trip. Default 3. */
readonly maxRejectedRetries?: number;
}
export interface RouteRecorderHandle {
readonly id: string;
/** The distinct skill sequence the run moved through (the "route"). */
getPath(): readonly string[];
/** Every hop, in order. */
getHops(): readonly RouteHop[];
/** The rejected read_skill attempts (out-of-reach jumps). */
getRejections(): readonly RouteHop[];
/** Governor trips (oscillation / rejected-retry cap). */
getTrips(): readonly RouteTrip[];
clear(): void;
onEmit(event: EmitEvent): void;
onRunStart(event: RunBoundaryEvent): void;
}
/** A human-readable one-line reason for a hop. Exported (pure). */
export declare function formatRouteHop(hop: RouteHop): string;
/** Build the route recorder. */
export declare function routeRecorder(options?: RouteRecorderOptions): RouteRecorderHandle;
export {};