agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
72 lines (71 loc) • 3.46 kB
TypeScript
/**
* ToolLineageRecorder — derives the tool→tool DATA-FLOW graph of a run.
*
* Why this exists: in a ReAct agent a tool's output flows back to the LLM as
* text, and the LLM decides the NEXT tool's arguments — so the data dependency
* between tools (e.g. FLOGI's FCID reused as io_profile's `initiator_id`) never
* touches footprintjs's shared scope, and `causalChain` can't reconstruct it.
* This recorder rebuilds that graph from the tool emit stream by VALUE
* PROVENANCE: when a tool call's argument value equals a distinctive value
* produced by an EARLIER iteration's tool result, it records an edge
* (producer → consumer).
*
* It is a HEURISTIC, not ground truth — it matches by value, so it is accurate
* for distinctive identifiers (FCIDs, WWPNs, ids, NAAs, hostnames) and
* deliberately conservative:
* - short / common values (below `minValueLength`) are ignored,
* - numbers are ignored by default (coincidental matches are likely),
* - same-iteration (parallel) tool calls never link to each other — the LLM
* chose all their args from the same prior context, before any of them ran.
*
* No core changes: it consumes the existing `agentfootprint.stream.tool_start`
* (args) and `agentfootprint.stream.tool_end` (result) emits. Attach via
* `.recorder(toolLineageRecorder())` and read `getLineage()` after the run; the
* lens (or any consumer) can render the returned graph.
*
* Convention 1 — one purpose: it derives ONE artifact (the lineage graph) from
* the emit channel. Convention 4 — run-scoped: it resets when a new run starts
* (the `pipelineId` changes).
*/
import type { EmitRecorder } from 'footprintjs';
/** A single tool invocation in the run. */
export interface ToolCallRef {
/** The execution-step id this tool call ran under. */
readonly runtimeStageId: string;
/** The provider tool-call id (stable per call). */
readonly toolCallId: string;
/** The tool's name. */
readonly toolName: string;
/** The ReAct iteration the call belongs to (0-based). */
readonly iteration: number;
}
/** A value-derived data dependency: `from`'s result value reappeared in `to`'s args. */
export interface ToolLineageEdge {
readonly from: ToolCallRef;
readonly to: ToolCallRef;
/** The distinctive value that links them. */
readonly value: string;
}
/** The tool→tool data-flow graph for one run. */
export interface ToolLineageGraph {
readonly nodes: readonly ToolCallRef[];
readonly edges: readonly ToolLineageEdge[];
}
export interface ToolLineageOptions {
/** Recorder id (idempotent-by-id when attached). Default `'tool-lineage'`. */
readonly id?: string;
/** Minimum string length for a value to be matchable. Default 4. */
readonly minValueLength?: number;
/** Also match numeric values. Default false (coincidental matches likely). */
readonly matchNumbers?: boolean;
/** Max recursion depth when flattening args/results. Default 6. */
readonly maxDepth?: number;
}
/** An EmitRecorder that also exposes the derived lineage graph. */
export interface ToolLineageRecorderHandle extends EmitRecorder {
/** The tool→tool data-flow graph derived so far. Safe to call during or after a run. */
getLineage(): ToolLineageGraph;
/** Reset all accumulated state. */
clear(): void;
}
export declare function toolLineageRecorder(options?: ToolLineageOptions): ToolLineageRecorderHandle;