agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
162 lines • 6.73 kB
JavaScript
/**
* sliceToBacktrackTrace — serialize a footprintjs VARIABLE SLICE
* (`sliceToJSON(sliceForKey(...))` from `foottrace`) into the
* BacktrackTrace shape agentThinkingUI's <BacktrackView>/<BacktrackOverlay>
* renders.
*
* The sibling of {@link toBacktrackTrace}: that one maps a LOCALIZER REPORT
* (scored suspects, ablation verdicts); this one maps a STRUCTURAL SLICE —
* the exact dependency chain behind one variable, with no influence claims
* at all. Same board, honestly weaker chips:
*
* - `mode` is ALWAYS 'correlational' — a slice is exact STRUCTURE, but it
* never tested influence; the default modeLabel says exactly that.
* - every card is `upperBound: true` (the UI's hatched meter + star): the
* score is 1/(1+depth) — hop PROXIMITY, a deterministic layout aid, not
* evidence. The honesty lines state the formula so nobody reads it as
* influence.
* - slice honesty rides along verbatim: reads-coverage warning
* (readTracking off), truncation, per-node incomplete-sources counts,
* and honest ABSENCE — a missing slice renders an empty board whose
* honesty lines say why ('never written' → initial state / frozen args /
* closure), never a fabricated suspect.
*
* Pure mapping, no UI dependency, framework-agnostic JSON on both sides —
* the human and the LLM triage the SAME artifact (the parity loop: an agent
* emits this, a person confirms or overrides on the board).
*/
import { HONESTY_CODES } from 'foottrace';
/** The structural-honesty lines every slice board carries. */
const SLICE_CLAIMS_LINES = [
'scores are hop proximity (1/(1+depth)) — a layout aid, not influence; every card is a path-only upper bound.',
'this board shows exact dependency STRUCTURE; only ablation verdicts make causal claims.',
];
/**
* Shortest hop chain root → node over the slice's id-referenced edges
* (BFS; edges point child→parent, i.e. away from the root).
*/
function pathTo(json, targetId) {
if (!json.edges || !json.writerId || targetId === json.writerId)
return undefined;
const prev = new Map();
const queue = [json.writerId];
const seen = new Set(queue);
while (queue.length > 0) {
const cur = queue.shift();
if (cur === targetId)
break;
for (const e of json.edges) {
if (e.from !== cur || seen.has(e.to))
continue;
seen.add(e.to);
prev.set(e.to, { from: e.from, key: e.key, kind: e.kind });
queue.push(e.to);
}
}
if (!prev.has(targetId))
return undefined;
const hops = [];
let cur = targetId;
while (cur !== json.writerId) {
const p = prev.get(cur);
hops.unshift({ key: p.key ?? '', kind: p.kind, via: `${p.from} ← ${cur}` });
cur = p.from;
}
return hops.length > 1 ? hops : undefined;
}
/**
* Serialize a variable slice for agentThinkingUI's BacktrackView. See the
* module doc — structural honesty is the whole design.
*/
export function sliceToBacktrackTrace(json, opts) {
const max = opts.maxSuspects ?? 6;
const claim = opts.claim ?? `Why is '${json.key}' what it is?`;
// Every sentence is footprintjs's own (`HONESTY_CODES`) — this module keeps no copy.
const honesty = [];
const say = (code, subject) => {
honesty.push(`⚠ ${subject !== undefined ? `${subject}: ` : ''}${HONESTY_CODES[code]}`);
};
if (json.missing !== undefined)
say(json.missing, `'${json.key}'`);
for (const note of json.notes ?? [])
say(note.code, `'${json.key}'`);
if (json.readsCoverage &&
json.readsCoverage.steps > 1 &&
json.readsCoverage.stepsWithReads === 0) {
say('reads-not-recorded');
}
if (json.truncated) {
say('truncated', `slice truncated (${[
json.truncated.byDepth && 'depth',
json.truncated.byNodes && 'node budget',
]
.filter(Boolean)
.join(' + ')})`);
}
// Honest absence: an empty board whose honesty says why.
if (!json.writerId || !json.nodes) {
return {
claim,
mode: 'correlational',
modeLabel: opts.modeLabel ?? 'no slice — see honesty',
agent: opts.agent,
model: opts.model,
answer: opts.answer,
decidedAt: {
id: json.key,
label: `'${json.key}' (no recorded writer)`,
kind: opts.decidedAtKind ?? 'rule',
},
suspects: [],
honesty: [...honesty, ...SLICE_CLAIMS_LINES],
};
}
const writer = json.nodes[json.writerId];
let incompleteCount = 0;
// Non-root nodes in their BFS insertion order (sliceToJSON preserves it).
const entries = Object.entries(json.nodes).filter(([id]) => id !== json.writerId);
const cards = entries.map(([id, node], i) => {
if (node.incompleteSources && node.incompleteSources.length > 0)
incompleteCount++;
const inboundEdge = json.edges?.find((e) => e.to === id);
return {
kind: 'stage',
name: node.stageName,
score: Number((1 / (1 + node.depth)).toFixed(2)),
rank: i + 1,
upperBound: true,
edge: inboundEdge
? { key: inboundEdge.key, weight: inboundEdge.weight, kind: inboundEdge.kind }
: undefined,
path: pathTo(json, id),
bornAt: { id, label: node.stageName, via: 'stage commit' },
};
});
if (incompleteCount > 0) {
honesty.push(`⚠ ${incompleteCount} step${incompleteCount === 1 ? '' : 's'} in the chain also consumed untracked inputs (args/env/silent reads) — the slice may be incomplete there.`);
}
const selected = cards.slice(0, max);
const dropped = cards.slice(max);
const folded = dropped.length > 0
? `${dropped.length} more step${dropped.length === 1 ? '' : 's'} folded — ` +
dropped.map((c) => `#${c.rank} ${c.bornAt.id}`).join(' · ') +
' — every id drillable with the trace toolpack'
: undefined;
return {
claim,
mode: 'correlational',
modeLabel: opts.modeLabel ?? 'exact dependency chain — structural, not ablation-tested',
agent: opts.agent,
model: opts.model,
answer: opts.answer,
decidedAt: {
id: json.writerId,
label: writer?.stageName,
kind: opts.decidedAtKind ?? 'rule',
},
suspects: selected,
folded,
honesty: [...honesty, ...SLICE_CLAIMS_LINES],
};
}
//# sourceMappingURL=sliceToBacktrackTrace.js.map