/** * 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;