UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

139 lines • 5.77 kB
"use strict"; /** * 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). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.toolLineageRecorder = void 0; const TOOL_START = 'agentfootprint.stream.tool_start'; const TOOL_END = 'agentfootprint.stream.tool_end'; const ITER_START = 'agentfootprint.agent.iteration_start'; function toolLineageRecorder(options = {}) { const minLen = options.minValueLength ?? 4; const matchNumbers = options.matchNumbers ?? false; const maxDepth = options.maxDepth ?? 6; let currentIteration = 0; let lastPipelineId; const nodes = []; const edges = []; /** distinctive value → the tool call that last produced it (in a result). */ const producers = new Map(); /** toolCallId → ref, so `tool_end` can attribute its result. */ const byId = new Map(); function collectValues(x, depth, out) { if (x == null || depth > maxDepth) return; if (typeof x === 'string') { if (x.length >= minLen) out.add(x); } else if (typeof x === 'number') { if (matchNumbers && Number.isFinite(x)) out.add(String(x)); } else if (Array.isArray(x)) { for (const v of x) collectValues(v, depth + 1, out); } else if (typeof x === 'object') { for (const v of Object.values(x)) collectValues(v, depth + 1, out); } } function reset() { currentIteration = 0; nodes.length = 0; edges.length = 0; producers.clear(); byId.clear(); } return { id: options.id ?? 'tool-lineage', onEmit(e) { // Run-scope: a changed pipelineId means a new run — drop prior state. if (lastPipelineId !== undefined && e.pipelineId !== lastPipelineId) reset(); lastPipelineId = e.pipelineId; if (e.name === ITER_START) { const it = e.payload?.iterIndex; if (typeof it === 'number') currentIteration = it; return; } if (e.name === TOOL_START) { const p = e.payload; if (!p?.toolCallId) return; const ref = { runtimeStageId: e.runtimeStageId, toolCallId: p.toolCallId, toolName: p.toolName ?? '(unknown)', iteration: currentIteration, }; nodes.push(ref); byId.set(ref.toolCallId, ref); // Link arg values to values produced by EARLIER iterations only. const argVals = new Set(); collectValues(p.args, 0, argVals); const emitted = new Set(); // dedupe (producer,value) edges for (const v of argVals) { const producer = producers.get(v); if (producer && producer.iteration < ref.iteration) { const key = `${producer.toolCallId}${v}`; if (!emitted.has(key)) { emitted.add(key); edges.push({ from: producer, to: ref, value: v }); } } } return; } if (e.name === TOOL_END) { const p = e.payload; if (!p?.toolCallId) return; const ref = byId.get(p.toolCallId); if (!ref) return; const resVals = new Set(); collectValues(p.result, 0, resVals); for (const v of resVals) producers.set(v, ref); // last producer of a value wins } }, getLineage() { return { nodes: [...nodes], edges: [...edges] }; }, clear() { reset(); lastPipelineId = undefined; }, }; } exports.toolLineageRecorder = toolLineageRecorder; //# sourceMappingURL=ToolLineageRecorder.js.map