agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
2,501 lines • 143 kB
JavaScript
"use strict";
/**
* traceToolpack — footprintjs trace evidence exposed as TOOLS an LLM calls
* (RFC-003 Part C: the introspection toolpack).
*
* "The framework's internal tool for itself": after a run completes, a
* debugging LLM (a cheap model in a SEPARATE session) navigates the run's
* evidence by ids instead of reading dumps — the same just-in-time,
* token-efficient loading pattern as `read_skill`. Feed the slice, not the
* trace; the LLM ranks by navigating, so no embedder is needed.
*
* Pattern: Factory over frozen artifacts. `traceToolpack(artifacts)` returns
* plain `Tool[]` — mount them on any Agent, or drive them scripted
* via `callTraceTool` (the offline auditor pattern, like
* examples/features/20). Nothing re-runs; every tool is a bounded
* read-only VIEW over a COMPLETED run's snapshot + commit log.
*
* The toolpack's three contracts (B13 posture):
*
* 1. BOUNDED BY DEFAULT — every output is capped (previews, slice
* depth/nodes, value chars, narrative lines). Per-call params raise
* the budget only up to hard caps the LLM cannot exceed.
* 2. HONEST — truncation and incompleteness are ALWAYS marked (⚠), never
* silent: truncated slices, untracked sources (args/env/silent reads),
* missing read tracking, missing control-dependence lookup, values the
* commit log cannot see (pre-run state, closures).
* 3. REDACTION-RESPECTING — the commit log already carries redacted
* payloads (footprintjs scrubs at commit time); the toolpack passes
* placeholders through verbatim and flags redacted keys. It never
* reconstructs around a redaction.
*
* Why ids: every view names steps by `runtimeStageId`
* (`stageId#executionIndex`) — the universal key linking the commit log,
* the execution tree, and recorder events. The LLM drills like a debugger:
* overview → slice → node → value, paying only for what it opens.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.callTraceTool = exports.TRACE_TOOL_NAMES = exports.traceToolpack = void 0;
const foottrace_1 = require("foottrace");
const foottrace_2 = require("foottrace");
const toolArgsValidation_js_1 = require("../tools/toolArgsValidation.js");
const tools_js_1 = require("../tools/tools.js");
const types_js_1 = require("../identity/types.js");
const capability_js_1 = require("../artifacts/capability.js");
const bounded_js_1 = require("./bounded.js");
const innerDescent_js_1 = require("./innerDescent.js");
const openRecording_js_1 = require("./openRecording.js");
const causeTools_js_1 = require("./causeTools.js");
const storedCauseBinding_js_1 = require("./storedCauseBinding.js");
const causeCapture_js_1 = require("./causeCapture.js");
const subflowRecords_js_1 = require("./subflowRecords.js");
const types_js_2 = require("./types.js");
// ── Display caps that are structural (not consumer-tunable) ───────────────
const OVERVIEW_STAGE_CAP = 40;
const OVERVIEW_KEY_CAP = 40;
const OVERVIEW_ERROR_CAP = 10;
const OVERVIEW_DESCRIPTION_CAP = 140;
const NODE_WRITE_CAP = 20;
const NODE_READ_CAP = 30;
const NARRATIVE_LINE_CHAR_CAP = 400;
const UNKNOWN_ID_SUGGESTION_CAP = 8;
const KEY_SUGGESTION_CAP = 12;
/** `find_in_trace` — default hits, query bounds, and per-hit context window. */
const FIND_HITS_DEFAULT = 10;
const FIND_QUERY_MIN_CHARS = 2;
const FIND_QUERY_MAX_CHARS = 200;
const FIND_CONTEXT_RADIUS = 55;
/**
* How many committed writes one search may open before it stops. A search
* is a pointer list, not a scan of the whole run — and a search that ran
* out of budget SAYS so rather than reporting "no match".
*/
const FIND_SCAN_BUDGET = 1500;
/** `inspect_tool_call` — how many real ids an unknown-id correction names. */
const TOOL_CALL_SUGGESTION_CAP = 12;
/** `inspect_tool_run` — how many RETAINED inner runs a correction names. */
const INNER_RUN_SUGGESTION_CAP = 12;
/** `find_context_errors` — findings per call by default, and the display caps. */
const FINDINGS_DEFAULT = 10;
const FINDING_MESSAGE_CHARS = 300;
const FINDING_SUBJECT_CAP = 6;
const FINDING_WITNESS_CAP = 6;
/** Disposition rows printed. Real packs register ~3; the cap is a fence. */
const DISPOSITION_ROW_CAP = 12;
/** Checks named in the partial-coverage headline before it says "and N more". */
const SILENT_CHECK_NAME_CAP = 4;
/** Result preview for `inspect_tool_call` (its own dial: results are the answer). */
const TOOL_RESULT_PREVIEW_CHARS = 400;
/** Schemas embed an `enum` of valid ids/keys only when the set is small —
* free #9 validation without bloating the tools block on long runs. */
const SCHEMA_ENUM_CAP = 48;
/**
* Artifact bags whose pack must not name `inspect_subflow` — the inner packs
* `inspect_tool_run` opens. Module-private on purpose: not an option a
* consumer sets, a fact about how the pack is reached.
*/
const WITHOUT_SUBFLOW_DOOR = new WeakSet();
function stagePartOf(runtimeStageId) {
const hash = runtimeStageId.lastIndexOf('#');
return hash > 0 ? runtimeStageId.slice(0, hash) : runtimeStageId;
}
function buildIndex(artifacts) {
const commitLog = artifacts.snapshot.commitLog ?? [];
const firstIdxOf = new Map();
const lastIdxOf = new Map();
const bundlesOf = new Map();
const knownPaths = new Set();
let untrackedStepCount = 0;
for (let i = 0; i < commitLog.length; i++) {
const bundle = commitLog[i];
const id = bundle.runtimeStageId;
if (!firstIdxOf.has(id))
firstIdxOf.set(id, i);
lastIdxOf.set(id, i);
const list = bundlesOf.get(id);
if (list)
list.push(bundle);
else {
bundlesOf.set(id, [bundle]);
if (bundle.untrackedSources && bundle.untrackedSources.length > 0)
untrackedStepCount++;
}
for (const entry of bundle.trace)
knownPaths.add(entry.path);
}
const subflowDoor = !WITHOUT_SUBFLOW_DOOR.has(artifacts);
const subflowResults = subflowDoor ? (0, subflowRecords_js_1.subflowResultsOf)(artifacts.snapshot) : undefined;
const homes = new Map();
// Walk the execution tree: node → children → next (≈ execution order).
const nodes = new Map();
const orderedIds = [];
const errorSteps = [];
let hasReadTracking = false;
const visit = (node) => {
if (!node)
return;
const id = node.runtimeStageId;
if (id && !nodes.has(id)) {
nodes.set(id, node);
orderedIds.push(id);
if (node.stageReads && Object.keys(node.stageReads).length > 0)
hasReadTracking = true;
const errorKeys = Object.keys(node.errors ?? {});
if (errorKeys.length > 0)
errorSteps.push({ id, keys: errorKeys });
}
for (const child of node.children ?? [])
visit(child);
visit(node.next);
};
visit(artifacts.snapshot.executionTree);
// Steps present in the commit log but missing from the tree. A subflow's own
// log opens with its MOUNT's seed commit, which its tree does not hold — so
// an id that committed before the tree's first step goes FIRST, not last.
const treeFirstIdx = firstIdxOf.get(orderedIds[0] ?? '') ?? Infinity;
const before = [];
for (const id of bundlesOf.keys()) {
if (nodes.has(id))
continue;
if ((firstIdxOf.get(id) ?? Infinity) < treeFirstIdx)
before.push(id);
else
orderedIds.push(id);
}
orderedIds.unshift(...before);
const idsByStagePart = new Map();
const groupsByStagePart = new Map();
const groups = [];
for (const id of orderedIds) {
const stagePart = stagePartOf(id);
const ids = idsByStagePart.get(stagePart);
if (ids)
ids.push(id);
else
idsByStagePart.set(stagePart, [id]);
let group = groupsByStagePart.get(stagePart);
if (!group) {
group = { stagePart, ids: [], isDecider: false, isSubflow: false, errorIds: [] };
groupsByStagePart.set(stagePart, group);
groups.push(group);
}
group.ids.push(id);
const node = nodes.get(id);
if (node) {
if (group.name === undefined && node.name !== undefined)
group.name = node.name;
if (group.description === undefined && node.description !== undefined) {
group.description = node.description;
}
if (node.isDecider)
group.isDecider = true;
if (node.subflowId !== undefined)
group.isSubflow = true;
if (Object.keys(node.errors ?? {}).length > 0)
group.errorIds.push(id);
}
else {
const bundle = bundlesOf.get(id)?.[0];
if (group.name === undefined && bundle)
group.name = bundle.stage;
}
}
return {
commitLog,
firstIdxOf,
lastIdxOf,
bundlesOf,
nodes,
orderedIds,
idsByStagePart,
knownPaths,
groups,
hasReadTracking,
errorSteps,
untrackedStepCount,
initialState: artifacts.snapshot.initialState,
explained: new Set(),
subflowResults,
subflowDoor,
homeMountOf: (id) => {
if (subflowResults === undefined)
return undefined;
if (!homes.has(id))
homes.set(id, (0, subflowRecords_js_1.homeMountOf)(subflowResults, id));
return homes.get(id);
},
};
}
// ── Shared message helpers ─────────────────────────────────────────────────
function unknownIdMessage(id, index) {
const home = index.homeMountOf(id);
if (home !== undefined)
return innerIdMessage(id, home);
const stagePart = stagePartOf(id);
const siblings = index.idsByStagePart.get(stagePart);
if (siblings && siblings.length > 0) {
return (`unknown runtimeStageId '${id}'. Stage '${stagePart}' has ${siblings.length} ` +
`execution(s): ${siblings.slice(0, UNKNOWN_ID_SUGGESTION_CAP).join(', ')}` +
(siblings.length > UNKNOWN_ID_SUGGESTION_CAP ? ', …' : '') +
`. Retry with one of those ids.`);
}
const sample = index.orderedIds.slice(0, UNKNOWN_ID_SUGGESTION_CAP).join(', ');
return (`unknown runtimeStageId '${id}'. Ids look like stageId#executionIndex` +
(sample
? ` — known steps include: ${sample}${index.orderedIds.length > UNKNOWN_ID_SUGGESTION_CAP ? ', …' : ''}`
: '') +
`. Call run_overview to list every stage.`);
}
/** An id from a subflow's OWN log, pasted into a tool over the log above it. */
function innerIdMessage(id, mount) {
return (`'${id}' is a step INSIDE subflow mount ${mount} — an id of that subflow's own log, not ` +
`of this one. Open it there: inspect_subflow({ mount: '${mount}', runtimeStageId: '${id}' }).`);
}
/** The one line a subflow mount carries: it can be opened, and how. */
function mountLine(index, id) {
return index.subflowResults !== undefined && id in index.subflowResults
? `inside: inspect_subflow({ mount: '${id}' }) opens this subflow's own log.`
: `inside: no own log kept for this mount — inspect_subflow({ mount: '${id}' }) says why.`;
}
function unknownKeySuffix(index) {
if (index.knownPaths.size === 0)
return '';
const sample = [...index.knownPaths].slice(0, KEY_SUGGESTION_CAP).map(bounded_js_1.displayKey).join(', ');
return ` Known keys include: ${sample}${index.knownPaths.size > KEY_SUGGESTION_CAP ? ', …' : ''}.`;
}
/** Is `path` redacted in any bundle of this step (or, keyless, of the writer)? */
function redactionNote(bundles, path) {
const redacted = (bundles ?? []).some((b) => b.redactedPaths.includes(path));
return redacted ? ' (redacted by policy)' : '';
}
/**
* A footprintjs reason code as the toolpack prints it. The FIRST time a code appears in this
* toolpack instance it is `⚠ <code>: <sentence>` — the sentence is footprintjs's own
* (`HONESTY_CODES` on `foottrace`), this pack keeps no copy — and every later time the
* bare `⚠ <code>`: the sentence is already in the model's context, and repeating it on every
* answer is pure token cost.
*/
function honestyLine(index, code) {
if (index.explained.has(code))
return `⚠ ${code}`;
index.explained.add(code);
return `⚠ ${code}: ${foottrace_1.HONESTY_CODES[code]}`;
}
/** One `honestyLine` per distinct code, each on its own line after `\n`; '' when exact. */
function basisLines(index, codes) {
return [...new Set(codes)].map((code) => `\n${honestyLine(index, code)}`).join('');
}
/** Union of untracked sources across a step's bundles. */
function untrackedSourcesOf(bundles) {
const set = new Set();
for (const bundle of bundles ?? []) {
for (const source of bundle.untrackedSources ?? [])
set.add(source);
}
return [...set];
}
function truncateText(text, cap) {
return text.length > cap ? `${text.slice(0, cap)}…` : text;
}
/**
* The final COMMITTED view of a key — reconstructed from the commit log,
* never read off `snapshot.sharedState`.
*
* That distinction is the whole redaction contract: `sharedState` is the
* run's LIVE state (unredacted by construction), while the commit log
* carries what footprintjs scrubbed at commit time. Everything in this
* file that serves a VALUE goes through here or through `commitValueAt`
* directly; `sharedState` is read for key NAMES only.
*/
function committedFinal(index, path) {
if (index.commitLog.length === 0)
return undefined;
return (0, foottrace_1.commitValueAt)(index.commitLog, index.commitLog.length - 1, path);
}
/** A bounded window of `text` around the first case-insensitive match. */
function matchWindow(text, needleLower, radius) {
const at = text.toLowerCase().indexOf(needleLower);
if (at < 0)
return undefined;
const start = Math.max(0, at - radius);
const end = Math.min(text.length, at + needleLower.length + radius);
return ((start > 0 ? '…' : '') +
text.slice(start, end).replace(/\s+/g, ' ') +
(end < text.length ? '…' : ''));
}
// ── The factory ────────────────────────────────────────────────────────────
// LENS · tool-result · persistent-history
// reads: the completed run ← footprintjs's own owners (causalChain, commitValueAt, findLastWriter, sliceForKey,
// arrayProvenance, elementProvenance) — re-derived nowhere; the disposition line ← the recorded event
// the bounds ← bounded.ts, through which every value this toolpack serves passes
// a never-dispatched call ← the bracket's typed `notDispatched`, else the history message's (`notDispatchedOf`); never `error`/`durationMs`
// law: may omit, never deny; every clause anchored to the call it was composed on.
/**
* Build the introspection toolpack over a COMPLETED run's artifacts.
*
* Returns plain `Tool[]`:
*
* | Tool | Question it answers |
* |---------------------|--------------------------------------------------------|
* | `run_overview` | What happened, broadly? (the entry point) |
* | `find_context_errors` | What did this run contradict itself about, and why? |
* | `find_in_trace` | WHERE in this run does "…" appear? (free text → ids) |
* | `trace_node` | What did step X read/write, and where did its inputs come from? |
* | `trace_slice` | Which chain of steps produced the data at X? (causal slice) |
* | `backtrack` | Why is VARIABLE K what it is? (+ `element`: who made K[i]?) — variable-first |
* | `who_wrote` | Which step last wrote key K? |
* | `get_value` | The full value of K as of step X (capped, truncation-marked) |
* | `inspect_tool_call` | One tool call end to end: proposed args → ran-with args → result → outcome |
* | `inspect_tool_run` | INSIDE one tool call — the chart it ran, when that tool kept its record |
* | `inspect_subflow` | INSIDE one subflow mount — the subflow's own log, one level at a time |
* | `read_narrative` | The human-readable story, paginated (only when narrative provided) |
*
* Mount on an Agent (`Agent.create({...}).tool(...tools)`) or drive scripted
* via {@link callTraceTool}. The tools NEVER throw on bad ids/keys — they
* return corrective, model-visible messages (the #9 philosophy), and their
* strict input schemas give Agent-dispatched calls free arg validation.
*
* Security note (B13 posture): trace content can carry adversarial text from
* the original run (tool results, user input). Serve these tools to a
* SEPARATE debugging session over completed runs — not to the production
* agent mid-run — and treat tool outputs as data, not instructions.
*/
function traceToolpack(artifacts, options) {
// Saved-only evidence has its own graph-only door. Inspect the claim as data
// before a snapshot getter, indexing or any graph code can run.
let savedClaim = true;
try {
const cause = Object.getOwnPropertyDescriptor(artifacts, 'cause');
savedClaim =
(0, storedCauseBinding_js_1.storedCauseAssessment)(artifacts).kind !== 'none' ||
(cause === undefined
? 'cause' in artifacts
: (0, storedCauseBinding_js_1.storedCauseAssessment)((0, causeCapture_js_1.captureOwn)(artifacts, 'cause')).kind !== 'none');
}
catch {
// An accessor or hostile object cannot supply evidence for a mixed bag.
}
if (savedClaim)
throw new TypeError('Stored capture evidence requires traceCauseTools.');
const opts = (0, types_js_2.resolveToolpackOptions)(options);
const index = buildIndex(artifacts);
// ONE reader over the scattered tool-call evidence, shared by the two
// tools that need it: the join at the boundary (`inspect_tool_call`) and
// the descent through it (`inspect_tool_run`). Its lookups are lazy and
// memoized, so a pack that opens neither pays nothing and a pack that
// opens both pays once.
const calls = buildToolCallReader(artifacts, index);
const tools = [
buildRunOverview(artifacts, index),
buildFindContextErrors(artifacts, index, calls, opts),
buildFindInTrace(artifacts, index),
buildTraceNode(artifacts, index, opts),
buildTraceSlice(artifacts, index, opts),
buildBacktrack(artifacts, index, opts),
buildWhoWrote(index, opts),
buildGetValue(index, opts),
buildInspectToolCall(artifacts, calls),
buildInspectToolRun(artifacts, calls, options),
buildInspectSubflow(artifacts, index, options),
...(0, causeTools_js_1.buildCauseTools)(artifacts, index.nodes, options?.cause),
];
if (artifacts.narrative !== undefined) {
tools.push(buildReadNarrative(artifacts.narrative));
}
return tools;
}
exports.traceToolpack = traceToolpack;
/**
* The tool names this pack can mount — declared in `traceToolNames.ts` so the
* Agent builder can reserve them WITHOUT loading this module (the pack is
* mounted lazily since 9.94.0); re-exported here so the list still reads as
* the pack's own.
*/
var traceToolNames_js_1 = require("../lib/traceToolNames.js");
Object.defineProperty(exports, "TRACE_TOOL_NAMES", { enumerable: true, get: function () { return traceToolNames_js_1.TRACE_TOOL_NAMES; } });
// ── Schema fragments ───────────────────────────────────────────────────────
function idProperty(index, description) {
const property = { type: 'string', description };
// No enum when subflows kept their own logs: the model may then hold an
// INNER id, and it is owed the correction toward `inspect_subflow` that only
// `execute` can give — a schema refusal would read as "no such step".
if (index.orderedIds.length > 0 &&
index.orderedIds.length <= SCHEMA_ENUM_CAP &&
!hasMountLogs(index)) {
property.enum = index.orderedIds;
}
return property;
}
/** True when at least one subflow mount kept its own log in this snapshot. */
function hasMountLogs(index) {
const results = index.subflowResults;
return (results !== undefined && Object.keys(results).some((key) => (0, subflowRecords_js_1.innerCommitCount)(results, key) > 0));
}
/**
* Key params get guidance (known keys in the description) but NO enum:
* unlike step ids — whose universe is complete — a key OUTSIDE the commit
* log is a legitimate question with an honest answer ("args/env/pre-run
* values never enter the commit log ⚠"), and an enum would block that path.
*/
function keyProperty(index, description) {
let hint = '';
if (index.knownPaths.size > 0 && index.knownPaths.size <= SCHEMA_ENUM_CAP) {
hint = ` Tracked keys: ${[...index.knownPaths].map(bounded_js_1.displayKey).join(', ')}.`;
}
return { type: 'string', description: `${description}${hint}` };
}
// ── run_overview ───────────────────────────────────────────────────────────
/**
* ONE line, only when the run mounted subflows: their inner steps are not in
* this log, and here is the door. By reference — nothing from inside a
* subflow is printed until the model asks for it.
*/
function overviewSubflowLine(index) {
if (!index.subflowDoor)
return undefined;
const mounts = index.groups.filter((group) => group.isSubflow).flatMap((group) => group.ids);
if (mounts.length === 0)
return undefined;
const results = index.subflowResults;
const openable = mounts.find((id) => results !== undefined && id in results);
return openable !== undefined
? `[subflow] steps ran their own log: inspect_subflow({ mount: '${openable}' }) opens one.`
: `[subflow] steps kept no own log here — inspect_subflow({ mount }) says why.`;
}
function buildRunOverview(artifacts, index) {
return (0, tools_js_1.defineTool)({
name: 'run_overview',
description: 'Start here. One bounded summary of the completed run: status, step counts, every ' +
'stage (id + name + description), loop counts, where errors appeared, and honesty notes. ' +
"Step ids look like 'stageId#executionIndex' (e.g. 'normalize#0'); every other trace " +
'tool accepts them.',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
execute: () => {
const lines = ['TRACE RUN OVERVIEW'];
lines.push(index.errorSteps.length > 0
? `status: ⚠ completed with ${index.errorSteps.length} step error(s) — see ERRORS below`
: 'status: no step errors recorded');
lines.push(`execution steps: ${index.orderedIds.length} · distinct stages: ${index.groups.length} · ` +
`commit-log mode: ${artifacts.snapshot.commitValues}`);
const example = index.orderedIds[0] ?? 'stageId#0';
lines.push(`step ids look like stageId#executionIndex (e.g. '${example}') — pass them to ` +
`trace_node / trace_slice / get_value.`);
lines.push('', `STAGES (${index.groups.length} distinct, first-execution order):`);
for (const group of index.groups.slice(0, OVERVIEW_STAGE_CAP)) {
const flags = [
group.isDecider ? ' [decision]' : '',
group.isSubflow ? ' [subflow]' : '',
].join('');
const name = group.name !== undefined ? ` — "${group.name}"` : '';
const description = group.description !== undefined
? `: ${truncateText(group.description, OVERVIEW_DESCRIPTION_CAP)}`
: '';
const errors = group.errorIds.length > 0
? ` ⚠ errors in ${group.errorIds.slice(0, 3).join(', ')}${group.errorIds.length > 3 ? ', …' : ''}`
: '';
lines.push(`- ${group.stagePart} ×${group.ids.length}${flags}${name}${description}${errors}`);
}
if (index.groups.length > OVERVIEW_STAGE_CAP) {
lines.push(`…and ${index.groups.length - OVERVIEW_STAGE_CAP} more stages (output capped)`);
}
const subflowHint = overviewSubflowLine(index);
if (subflowHint !== undefined)
lines.push(subflowHint);
const loops = index.groups.filter((g) => g.ids.length > 1);
if (loops.length > 0) {
const top = [...loops]
.sort((a, b) => b.ids.length - a.ids.length)
.slice(0, 5)
.map((g) => `${g.stagePart} ×${g.ids.length}`)
.join(' · ');
lines.push('', `LOOPS: ${top}`);
}
if (index.errorSteps.length > 0) {
lines.push('', 'ERRORS:');
for (const step of index.errorSteps.slice(0, OVERVIEW_ERROR_CAP)) {
lines.push(`- ⚠ ${step.id}: ${step.keys.join(', ')} (drill with trace_node('${step.id}'))`);
}
if (index.errorSteps.length > OVERVIEW_ERROR_CAP) {
lines.push(`…and ${index.errorSteps.length - OVERVIEW_ERROR_CAP} more error steps`);
}
}
if (index.untrackedStepCount > 0) {
lines.push('', `HONESTY: ⚠ ${index.untrackedStepCount} step(s) consumed untracked inputs (args/env/silent ` +
`reads) — causal slices through them may be incomplete; trace_node marks each one.`);
}
const stateKeys = Object.keys(artifacts.snapshot.sharedState ?? {});
const shown = stateKeys.slice(0, OVERVIEW_KEY_CAP).map(bounded_js_1.displayKey);
lines.push('', `SHARED STATE KEYS (${stateKeys.length}): ${shown.join(', ')}` +
(stateKeys.length > OVERVIEW_KEY_CAP
? `, … +${stateKeys.length - OVERVIEW_KEY_CAP} more`
: '') +
' — values via who_wrote / get_value.');
// COST — only when the run actually priced itself. `cumEstimatedUsd`
// is seeded to 0 on every agent run and only ever moves under a
// configured `pricingTable`, so a zero here means "this run was not
// priced", and printing "$0.00" for it would be a number that lies.
const spend = committedFinal(index, 'cumEstimatedUsd');
if (typeof spend === 'number' && spend > 0) {
const tokensIn = committedFinal(index, 'cumTokensInput');
const tokensOut = committedFinal(index, 'cumTokensOutput');
const writer = (0, foottrace_1.findLastWriter)(index.commitLog, 'cumEstimatedUsd');
lines.push('', `COST: ~$${spend.toFixed(6)} (in ${typeof tokensIn === 'number' ? tokensIn : '?'} / out ${typeof tokensOut === 'number' ? tokensOut : '?'} tokens) — estimated by the ` +
`run's pricing table, not a bill` +
(writer ? ` — via get_value('${writer.runtimeStageId}', 'cumEstimatedUsd').` : '.'));
}
if (artifacts.narrative !== undefined) {
lines.push(`NARRATIVE: ${artifacts.narrative.length} line(s) available via read_narrative.`);
}
lines.push(`SEARCH: find_in_trace('<text>') locates any phrase in this record and hands back ids.`);
return lines.join('\n');
},
});
}
// ── find_context_errors ────────────────────────────────────────────────────
/**
* The six ContextErrorKinds — each mapped to the check that can FILE it in
* this build, or `false` when no check ships for that class.
*
* Two jobs in one table:
*
* - the compile-time pin. The record's KEY type is `ContextErrorKind`, so
* a kind added to the finding type without joining this list fails to
* compile HERE, rather than becoming a value the tool's enum silently
* rejects at run time.
* - the honesty gate. `ContextErrorKind` names nine defect classes;
* `src/integrity/` ships eight checks. Offering the sixth in the
* schema enum invites a query whose only possible answer is a negative
* verdict about a class nothing in the library could have filed — the
* tool telling a model "no duplicate-execution errors" when nothing on
* earth was looking for one. Only filable kinds reach the enum; the
* description and the refusal are generated from this same table, so a
* check that lands is advertised everywhere by flipping one entry.
*/
const CONTEXT_ERROR_KIND_CHECKS = {
'invariant-violation': 'src/integrity/invariant-violation (write + wire)',
'unsupported-argument': 'src/integrity/unsupported-argument (choice)',
'dangling-reference': 'src/integrity/dangling-reference (compose)',
'duplicate-execution': false,
'unsupported-claim': 'src/integrity/unsupported-claim (claim)',
'empty-lookup': 'src/integrity/empty-lookup (write)',
'column-type-mismatch': 'src/integrity/column-types (write)',
'missing-column': 'src/integrity/column-types (write)',
'prior-turn-evidence': 'src/integrity/prior-turn-evidence (claim)',
};
const ALL_CONTEXT_ERROR_KINDS = Object.keys(CONTEXT_ERROR_KIND_CHECKS);
/** The kinds the tool OFFERS — the ones a shipped check can actually file. */
const CONTEXT_ERROR_KINDS = ALL_CONTEXT_ERROR_KINDS.filter((kind) => CONTEXT_ERROR_KIND_CHECKS[kind] !== false);
/** Named in the finding type, filable by nothing here. Said out loud, never offered. */
const UNFILABLE_CONTEXT_ERROR_KINDS = ALL_CONTEXT_ERROR_KINDS.filter((kind) => CONTEXT_ERROR_KIND_CHECKS[kind] === false);
/**
* The answer for a `kind` this build cannot report on.
*
* It runs BEFORE the evidence checks on purpose: whether a class is filable
* is a fact about the BUILD, not about the run, and it stays true however
* much evidence the tail carries. Args validation is a dial
* (`toolArgValidation: 'off' | 'warn'`), so the enum is a menu rather than a
* wall — `execute` really is reachable with a kind it never offered, and the
* one answer it must never give there is a negative verdict.
*/
function unanswerableKind(kind) {
const offered = CONTEXT_ERROR_KINDS.join(', ');
if (!Object.prototype.hasOwnProperty.call(CONTEXT_ERROR_KIND_CHECKS, kind)) {
return (`find_context_errors: ⚠ '${kind}' is not a defect class this library knows — it is not a ` +
`Context Integrity kind at all, so nothing was ever filed under that word and this answer ` +
`says nothing about the run. Classes this build can file: ${offered}.`);
}
if (CONTEXT_ERROR_KIND_CHECKS[kind] !== false)
return undefined;
return (`find_context_errors: ⚠ UNANSWERABLE — '${kind}' is a defect class the finding type names, ` +
`and no check in this build can file it. Nothing ever looked for one, so silence about it is ` +
`not evidence of its absence. Classes this build can file: ${offered} — call again with one ` +
`of those, or with no 'kind' at all.`);
}
const CONTEXT_ERROR_EVENT = 'agentfootprint.integrity.context_error';
const DISPOSITION_EVENT = 'agentfootprint.integrity.disposition';
/**
* An advisory is a filed finding the seam marked as NOT a defect (9.61.0 —
* today, an answer that DECLINES to claim a fact the run verified). Same
* quarantine rule as the dev canary: it is shown, and it is never counted
* as a context error. A reader that did not know the field would report
* doubt as a contradiction.
*/
function isAdvisory(filed) {
return filed.error.advisory === true;
}
/**
* The standing bound of the whole Context Integrity family, repeated
* wherever this tool reports — because the sentence it prevents ("nothing
* was found, so the run is fine") is the exact misreading that let two
* shipped checks in this codebase decay into decoration.
*/
const INTEGRITY_BOUND = '⚠ Green means no REGISTERED check was violated — it does not mean no context error exists. ' +
'A seam with no checker files nothing, and an encounter with no stamped identity is silent.';
/** Subjects, read defensively: an event tail may have been parsed from JSON. */
function subjectsOf(raw) {
if (!Array.isArray(raw))
return [];
const out = [];
for (const item of raw) {
if (item === null || typeof item !== 'object')
continue;
const subject = item;
out.push({
kind: typeof subject.kind === 'string' ? subject.kind : '?',
id: typeof subject.id === 'string' ? subject.id : (0, bounded_js_1.safeStringify)(subject.id),
});
}
return out;
}
/** The witness assertions' step ids — the two-or-more facts that disagreed. */
function witnessStepsOf(raw) {
if (!Array.isArray(raw))
return { ids: [], total: 0, unstamped: 0 };
const ids = [];
let unstamped = 0;
for (const item of raw) {
const stamp = item !== null && typeof item === 'object'
? item.runtimeStageId
: undefined;
if (typeof stamp === 'string' && stamp !== '') {
if (!ids.includes(stamp))
ids.push(stamp);
}
else
unstamped++;
}
return { ids, total: raw.length, unstamped };
}
/**
* The commit-log answer for ONE state key, in a sentence.
*
* It walks nothing new: `findLastWriter` + `commitValueAt` are the same
* pair `who_wrote` and `get_value` use, so there is no second reader of the
* commit log to drift — only a shorter sentence, and the exact follow-up
* calls that open the long one.
*/
function stateJoinFor(index, path, previewChars) {
const writer = (0, foottrace_1.findLastWriter)(index.commitLog, path);
if (!writer)
return undefined;
const preview = (0, bounded_js_1.boundedPreview)((0, foottrace_1.commitValueAt)(index.commitLog, index.commitLog.indexOf(writer), path), previewChars);
const dotted = (0, bounded_js_1.displayKey)(path);
return (`'${dotted}' = ${(0, bounded_js_1.displayText)((0, bounded_js_1.renderPreview)(preview))}${redactionNote([writer], path)} — last ` +
`written by ${writer.runtimeStageId} ("${writer.stage}") → who_wrote('${dotted}') · ` +
`backtrack({ variable: '${dotted}' })`);
}
/** Render ONE disposition row: did this check run, and what did it see? */
function dispositionLine(report) {
const check = typeof report.check === 'string' ? report.check : '?';
const head = `- ${check} @${report.seam ?? '?'}`;
const checked = typeof report.checked === 'number' ? report.checked : 0;
const notApplicable = typeof report.notApplicable === 'number' ? report.notApplicable : 0;
const unreachable = typeof report.unreachable === 'number' ? report.unreachable : 0;
const findings = typeof report.findings === 'number' ? report.findings : 0;
const synthetic = typeof report.synthetic === 'number' ? report.synthetic : 0;
if (checked + notApplicable + unreachable === 0) {
return (`${head}: ⚠ REGISTERED BUT NEVER RAN — zero encounters while this run did work. The check ` +
`is wired and nothing fed it (wiring rot); its silence is not health.`);
}
// `ran 0×` with encounters on the other counters is NOT "found nothing":
// every encounter was out of scope or unreachable, so this seam went
// unchecked. Saying it found nothing would read the count backwards.
const parts = [
checked > 0 ? `ran ${checked}×` : `⚠ ran 0× — nothing at this seam was actually checked`,
...(findings > 0
? [`${findings} finding(s)`]
: checked > 0
? ['0 findings — the checker ran and found nothing at this seam']
: []),
];
if (notApplicable > 0) {
parts.push(`not-applicable ${notApplicable} (out of scope by rule)`);
}
if (unreachable > 0) {
parts.push(`unreachable ${unreachable} ⚠ — no stamped identity edge, so those encounters were SILENT, ` +
`not clean`);
}
if (synthetic > 0)
parts.push(`synthetic canary ${synthetic} (quarantined from the real counts)`);
if (typeof report.lastFiredAt === 'number') {
parts.push(`last fired ${new Date(report.lastFiredAt).toISOString()}`);
}
return `${head}: ${parts.join(' · ')}`;
}
/**
* The Context Integrity findings of this run — what it contradicted itself
* about, and the join from there to the state behind it.
*
* This tool READS. Every finding was already detected, judged and
* deduplicated at filing time (`contextErrorIdentity`), so nothing here
* re-runs a check or re-decides one: it enumerates what was filed and adds
* the one thing a findings stream cannot carry — the POINTERS. Each finding
* names the step it was filed at, the steps its witnesses came from, and
* (when a subject is a tracked state key) the writer behind it, in the exact
* shapes `trace_node` / `trace_slice` / `who_wrote` / `backtrack` /
* `find_in_trace` accept. The model's next call is never an id it had to
* assemble by hand.
*
* HONEST ABSENCE is the reason the tool exists in this family at all, and
* its sentences must never collapse into one:
*
* - a kind no check can file → UNANSWERABLE, said before any evidence is
* read: which classes are filable is a fact
* about the build, not about the run.
* - no event tail at all → NO EVIDENCE. Findings ride the event
* stream; without it this run cannot be
* reported on either way.
* - a tail with no integrity → no checker was registered, or the tail
* events was capped before they landed.
* - rows, but zero `checked` → NOTHING WAS CHECKED. Every encounter was
* encounters unreachable, out of scope, or never met —
* silence, and never a clean bill.
* - rows with real `checked` → the checkers RAN and found nothing —
* encounters, no findings health, stated with the encounter count
* that earned it and the rows that prove it,
* and with the standing bound that a seam
* nobody checks files nothing.
*
* The green sentence is the one that must be EARNED: it is chosen by summing
* the rows' `checked` counts, never by the mere absence of a finding.
*/
function buildFindContextErrors(artifacts, index, reader, opts) {
const isKnownStep = (id) => id !== undefined && (index.firstIdxOf.has(id) || index.nodes.has(id));
const readFindings = () => reader.eventsOf(CONTEXT_ERROR_EVENT).map((event) => {
const error = event.payload;
const filedAt = event.meta.runtimeStageId;
const iteration = typeof error.iteration === 'number'
? error.iteration
: typeof event.meta.iterIndex === 'number'
? event.meta.iterIndex
: undefined;
return {
error,
...(typeof filedAt === 'string' && filedAt !== '' && { filedAt }),
...(iteration !== undefined && { iteration }),
};
});
const renderFinding = (filed, position) => {
const { error } = filed;
const kind = typeof error.kind === 'string' ? error.kind : '⚠ (kind not recorded)';
const seam = typeof error.seam === 'string' ? `@${error.seam}` : '@⚠ (seam not recorded)';
const stamps = [
typeof error.epoch === 'number' ? `epoch ${error.epoch}` : '',
filed.iteration !== undefined ? `iteration ${filed.iteration}` : '',
error.synthetic === true ? 'SYNTHETIC canary — not a real defect' : '',
isAdvisory(filed) ? 'ADVISORY — counted apart from real defects' : '',
].filter(Boolean);
const message = typeof error.message === 'string' && error.message !== ''
? truncateText((0, bounded_js_1.displayText)(error.message), FINDING_MESSAGE_CHARS)
: '⚠ no message recorded on this finding';
const lines = [
`[${position}] ${kind} ${seam}${stamps.length > 0 ? ` · ${stamps.join(' · ')}` : ''} — ${message}`,
];
// WHO it is about — plus the free-text bridge into ids for the first one.
const subjects = subjectsOf(error.subjects);
if (subjects.length === 0) {
lines.push(' about: ⚠ no subjects recorded — the finding names no stamped identity.');
}
else {
const shown = subjects.slice(0, FINDING_SUBJECT_CAP).map((s) => `${s.kind}:${s.id}`);
lines.push(` about: ${shown.join(', ')}${subjects.length > FINDING_SUBJECT_CAP
? `, … +${subjects.length - FINDING_SUBJECT_CAP} more`
: ''} → find_in_trace('${subjects[0].id}')`);
// THE STATE JOIN: when a subject names a key the commit log really
// wrote, answer the "and what was it?" question here rather than
// making the model guess which key to ask about.
for (const subject of subjects.slice(0, FINDING_SUBJECT_CAP)) {
const path = (0, bounded_js_1.normalizeKey)(subject.id, index.knownPaths);
if (!index.knownPaths.has(path))
continue;
const join = stateJoinFor(index, path, opts.previewChars);
if (join !== undefined) {
lines.push(` state: ${join}`);
break;
}
}
}
// WHERE it was filed — the step, when it is a step this run really ran.
if (filed.filedAt === undefined) {
lines.push(' filed at: ⚠ no step stamped on the event — it cannot be located in the trace.');
}
else if (isKnownStep(filed.filedAt)) {
lines.push(` filed at ${filed.filedAt} → trace_node('${filed.filedAt}') · ` +
`trace_slice({ runtimeStageId: '${filed.filedAt}' })`);
}
else {
lines.push(` filed at ${filed.filedAt} ⚠ — not a step in this run's execution tree (a run-boundary ` +
`marker, or evidence from another run), so the step drills cannot open it.`);
}
// THE TWO FACTS THAT DISAGREED — as ids, when the assertions were stamped.
const witnesses = witnessStepsOf(error.witnesses);
if (witnesses.total === 0) {
lines.push(' witnesses: ⚠ none recorded — the proof did not travel with the finding.');
}
else {
const known = witnesses.ids.filter(isKnownStep);
const shown = known.slice(0, FINDING_WITNESS_CAP);
lines.push(` witnesses (${witnesses.total}): ` +
(shown.length > 0
? `${shown.join(', ')} → trace_node(<id>)`
: '⚠ none carries a step id in this run') +
(witnesses.unstamped > 0
? ` · ⚠ ${witnesses.unstamped} assertion(s) carry no step stamp`
: ''));
}
return lines;
};
const renderCheckers = (rows, header) => {
if (rows === undefined) {
return [
`CHECKERS: ⚠ no disposition accounting in this run's event tail — nothing records WHICH ` +
`checks ran, so the absence of a finding at any other seam says nothing at all.`,
];
}
if (rows.length === 0) {
return [
`CHECKERS: ⚠ none registered — this agent's configuration made no Context Integrity check ` +
`applicable (a check registers only where its preconditions exist), so nothing was ` +
`checked and nothing could be found. That is honest absence, not health.`,
];
}
const lines = [header, ...rows.slice(0, DISPOSITION_ROW_CAP).map(dispositionLine)];
if (rows.length > DISPOSITION_ROW_CAP) {
lines.push(`…and ${rows.length - DISPOSITION_ROW_CAP} more check row(s) (output capped)`);
}
return lines;
};
return (0, tools_js_1.defineTool)({
name: 'find_context_errors',
description: 'What did this run CONTRADICT ITSELF about? Lists the Context Integrity findings the ' +
'library already caught — an invariant violated, an offered action whose inputs left ' +
'scope, an argument nothing served, a claim the record does not support, a lookup for a ' +
'value this run produced that came back empty — each with the ' +
'step it was filed at and the ' +
'exact drill that opens the state behind it. Call it EARLY when an answer looks wrong: a ' +
'contradiction the library already caught beats re-deriving one. Reads only; nothing is ' +
're-checked or re-judged. When no finding evidence was captured it says so plainly rather ' +
`than reporting a clean run. The finding type also names ${UNFILABLE_CONTEXT_ERROR_KINDS.map((kind) => `'${kind}'`).join(' and ')}, and no check in this build files ${UNFILABLE_CONTEXT_ERROR_KINDS.length === 1 ? 'it' : 'them'} — this tool cannot report on ` +
`that class${UNFILABLE_CONTEXT_ERROR_KINDS.length === 1 ? '' : 'es'} at all, so its ` +
'silence about it is not evidence. ' +
`Bounded by limit (default ${FINDINGS_DEFAULT}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.contextErrorsMax}).`,
inputSchema: {
type: 'object',
properties: {
kind: {
type: 'string',
enum: CONTEXT_ERROR_KINDS,
description: 'Optional: only findings of this defect class. Omit to see every finding. Only ' +
'classes a check in this build can file are listed here.',
},
limit: {
type: 'integer',
description: `Findings to list (default ${FINDINGS_DEFAULT}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.contextErrorsMax}).`,
},
},
additionalProperties: false,
},
execute: ({ kind, limit }) => {
// ── a question no build of this library can answer ──────────────────
// Ahead of every evidence check, because which classes are FILABLE is a
// property of the build, not of the run. The one exception is evidence
// itself: if the tail really does carry findings under that kind (a
// foreign filer, or a recording made by a build that shipped the
// check), listing them beats refusing them — the refusal claims nothing
// ever looked, and here something did.
if (kind !== undefined) {
const refusal = unanswerableKind(kind);
const carried = artifacts.events !== undefined && readFindings().some((f) => f.error.kind === kind);
if (refusal !== undefined && !carried)
return refusal;
}
// ── absence, stated in the sentence the situation deserves ──────────
if (artifacts.events === undefined) {
return (`find_context_errors: ⚠ NO FINDING EVIDENCE — these artifacts carry no event tail at ` +
`all. Context Integrity findings ride the event stream (${CONTEXT_ERROR_EVENT}), and ` +
`the per-run accounting of which checks ran rides ${DISPOSITION_EVENT}; with neither ` +
`retained, this run cannot be reported on either way. That is "no evidence", NOT "no ` +
`context errors". Capture the tail with recordRun(agent), or leave it on for a ` +
`self-explaining agent (selfExplain({ include: { events: true } }) — the default).`);
}
const filed = readFindings();
const dispositionEvent = reader.eventsOf(DISPOSITION_EVENT).at(-1);
const rows = dispositionEvent !== undefined && Array.isArray(dispositionEvent.payload.rows)
? dispositionEvent.payload.rows
: dispositionEvent !== undefined
? []
: undefined;
if (filed.length === 0 && dispositionEvent === undefined) {
return (`find_context_errors: ⚠ this run's event tail carries no Context Integrity events — ` +
`neither findings (${CONTEXT_ERROR_EVENT}) nor the per-run disposition accounting ` +
`(${DISPOSITION_EVENT}). Either no checker was registered for this agent's ` +
`configuration, or the tail was capped or filtered before they were captured ` +
`(${artifacts.events.length} event(s) retained). Read it as "no evidence", never as a ` +
`clean bill of health.`);
}
// Canary material is quarantined from the real counts, everywhere the
// family reports — a synthetic finding is proof a check is ALIVE, not
// proof the run went wrong. Advisories are shown but counted apart for
// the same reason: a seam that filed one said it is not a defect.
const nonSynthetic = filed.filter((f) => f.error.synthetic !== true);
const synthetic = filed.length - nonSynthetic.length;
const real = nonSynthetic.filter((f) => !isAdvisory(f));
const matching = kind === undefined ? nonSynthetic : nonSynthetic.filter((f) => f.error.kind === kind);
const posture = dispositionEvent !== undefined && typeof dispositionEvent.payload.posture === 'string'
? dispositionEvent.payload.posture
: 'unknown';
const workExisted = dispositionEvent?.payload.workExisted === true;
const header = `CHECKERS (posture: ${posture} · ${workExisted ? 'the run did work a checker could see' : '⚠ the run did no checkable work'}):`;
// The rows count ENCOUNTERS that filed; the event stream is
// deduplicated by identity. When the two disagree, say which is which —
// a reader who sees "6" in a row and one finding in the list would
// otherwise conclude the list dropped five.
const rowFindings = (rows ?? []).reduce((sum, report) => sum + (typeof report.findings === 'number' ? report.findings : 0), 0);
const matchingAdvisories = matching.filter(isAdvisory).length;
const matchingDefects = matching.length - matchingAdvisories;
// ── what the rows actually say a check DID ──────────────────────────
// The disposition rows are the only witness that a check ever looked,
// and a green headline is earned by `checked` encounters alone: an
// `unreachable` encounter is one the check could not see the evidence
// for, a `not-applicable` one is out of its scope by rule, and a
// registered check with zero encounters never met a subject. Summing
// them defensively — a tail parsed back from JSON can carry anything.
const sumRows = (pick) => (rows ?? []).reduce((sum, report) => {
const value = pick(report);
return sum + (typeof value === 'number' && Number.isFinite(value) ? value : 0);
}, 0);
const num = (value) => typeof value === 'number' && Number.isFinite(value) ? value : 0;
const totalChecked = sumRows((report) => report.checked);
const totalUnreachable = sumRows((report) => report.unreachable);
const totalNotApplicable = sumRows((report) => report.notApplicable);
const neverRanRows = (rows ?? []).filter((report) => num(report.checked) + num(report.notApplicable) + num(report.unreachable) === 0).length;
// Checks that CHECKED nothing — a superset of `neverRanRows`: a check
// whose every encounter was out of scope or unreachable also passed
// nothing. Named, because the headline must not average them away.
const silentChecks = (rows ?? [])
.filter((report) => num(report.checked) === 0)
.map((report) => `${typeof report.check === 'string' ? report.check : '?'} @${report.seam ?? '?'}`);
/** Nothing matched: WHICH absence is this? The rows decide, not the silence. */
const absenceHeadline = () => {
if (rows === undefined) {
return (`CONTEXT ERRORS — ⚠ NONE FILED, AND NOTHING RECORDS WHICH CHECKS RAN. This tail ` +
`carries no disposition accounting (${DISPOSITION_EVENT}), so no check is known to ` +
`have run and none is known to have passed. Read it as unmeasured, not as clean.`);
}
if (rows.length === 0) {
return (`CONTEXT ERRORS — ⚠ NONE FILED, AND NO CHECK WAS REGISTERED. This agent's ` +
`configuration made no Context Integrity check applicable, so nothing was checked ` +
`and nothing could be found. That is honest absence, not health.`);
}
if (totalChecked === 0) {
const why = [
totalUnreachable > 0
? `${totalUnreachable} unreachable (no stamped identity edge, so the check could ` +
`not see the evidence)`
: '',
totalNotApplicable > 0 ? `${totalNotApplicable} out of scope by rule` : '',
neverRanRows > 0 ? `${neverRanRows} check(s) never met a subject at all` : '',
].filter(Boolean);
return (`CONTEXT ERRORS — ⚠ NOTHING WAS CHECKED. The ${rows.length} registered check(s) ` +
`below reached zero checked encounter(s)${why.length > 0 ? `: ${why.join(' · ')}` : ''}` +
`. A check that never saw a subject cannot have passed one, so this is silence about ` +
`every seam, not a clean run.`);
}
// Green, but not evenly. A headline that sums `checked` across every
// row reads as coverage the rows do not support: one busy check can
// carry a total while another registered check looked at NOTHING.
// The per-check rows already say so; a reader who stops at the
// headline must not get the rosier story. Same law as the rows —
// "ran 0×" is silence about that seam, never a pass.
if (silentChecks.length > 0) {
const named = silentChecks.slice(0, SILENT_CHECK_NAME_CAP).join(', ');
const more = silentChecks.length > SILENT_CHECK_NAME_CAP
? `, and ${silentChecks.length - SILENT_CHECK_NAME_CAP} more`
: '';
return (`CONTEXT ERRORS — none filed, ⚠ AND COVERAGE IS PARTIAL. ` +
`${rows.length - silentChecks.length} of ${rows.length} registered check(s) ` +
`actually checked something (${totalChecked} checked encounter(s)) and found ` +
`nothing; ${silentChecks.length} checked NOTHING at all — silent about its seam, ` +
`not clean: ${named}${more}. This green covers only the checks that ran.`);
}
return (`CONTEXT ERRORS — none filed. All ${rows.length} registered check(s) below RAN ` +
`(${totalChecked} checked encounter(s)); nothing they cover was violated.`);
};
const lines = [];
if (matching.length === 0 && nonSynthetic.length === 0 && rowFindings > 0) {
lines.push(`CONTEXT ERRORS — ⚠ EVIDENCE MISSING. The checker rows below report ${rowFindings} ` +
`finding(s) filed by this run, and this event tail carries NONE of them — the tail ` +
`was capped or filtered. The contradictions happened; their detail did not survive. ` +
`This is the opposite of a clean run.`);
}
else if (matching.length === 0 && nonSynthetic.length > 0) {
const kinds = [...new Set(nonSynthetic.map((f) => String(f.error.kind ?? '?')))].join(', ');
lines.push(`CONTEXT ERRORS — no '${kind}' findings in this run. It filed ${nonSynthetic.length} ` +
`finding(s) of other kinds: ${kinds}. Call again without 'kind' to see them.`);
}
else if (matching.length === 0) {
lines.push(absenceHeadline());
}
else {
const cap = (0, bounded_js_1.clampParam)(limit, FINDINGS_DEFAULT, 1, types_js_2.TOOLPACK_HARD_CAPS.contextErrorsMax);
const shown = matching.slice(0, cap);
lines.push(`CONTEXT ERRORS — ${matchingDefects} finding(s) filed by this run's integrity checks` +
(matchingAdvisories > 0
? `, plus ${matchingAdvisories} advisory (counted apart — a seam filed them as not ` +
`a defect)`
: '') +
` (showing ${shown.length} of ${matching.length}). Each was detected and deduplicated ` +
`when it happened; nothing is re-judged here.`, '');
shown.forEach((entry, i) => lines.push(...renderFinding(entry, i + 1)));
if (shown.length < matching.length) {
lines.push(`…${matching.length - shown.length} more finding(s) — raise limit (hard cap ` +
`${types_js_2.TOOLPACK_HARD_CAPS.contextErrorsMax}) or filter with 'kind'.`);
}
}
if (synthetic > 0) {
lines.push(`(${synthetic} synthetic canary finding(s) filed in dev posture are quarantined from ` +
`the count above — they prove a check can still catch its own defect.)`);
}
if (real.length > 0 && rowFindings > real.length) {
lines.push(`note: the rows below count every ENCOUNTER that filed (${rowFindings}); the list above ` +
`is deduplicated by identity (${real.length} distinct defect(s)) — one defect ` +
`re-detected on a later iteration stays ONE finding.`);
}
lines.push('', ...renderCheckers(rows, header), '', INTEGRITY_BOUND);
lines.push(`Next: trace_node / trace_slice open the step a finding was filed at · who_wrote and ` +
`backtrack explain a state key · find_in_trace turns a subject's name into ids.`);
return lines.join('\n');
},
});
}
// ── find_in_trace ──────────────────────────────────────────────────────────
/**
* The missing FIRST move.
*
* Every other tool needs a name you already have: a step id, a state key,
* a variable. But a follow-up question arrives in the user's words — "why
* did you say order 7712 was out of warranty?" — and the model's only
* options were to guess a key or read the whole narrative. This is the
* bridge from free text to ids: search the record, hand back POINTERS.
*
* Three properties make it a search rather than a dump:
*
* - it serves a bounded WINDOW around each match, never the value;
* - every hit line ends with the exact next call to make, so a search
* result is a menu of drills rather than an answer to be believed;
* - it searches the REDACTED commit log, because that is the only copy
* that exists — a redacted value reads as its placeholder here for
* the same reason it does everywhere else.
*
* The zero-hit answer is the honest one: it names what never enters the
* record at all, so "not found" cannot be read as "did not happen".
*/
function buildFindInTrace(artifacts, index) {
return (0, tools_js_1.defineTool)({
name: 'find_in_trace',
description: 'Search this run\'s record for free text — "7712", "out of stock", a phrase from the ' +
'answer — and get back the step ids and keys where it appears. Searches stage names and ' +
'descriptions, state keys, every committed value, and the narrative. Case-insensitive ' +
`substring; returns up to maxHits pointers (default ${FIND_HITS_DEFAULT}, hard cap ` +
`${types_js_2.TOOLPACK_HARD_CAPS.findMaxHits}), each with the exact follow-up call to make. Use it ` +
'FIRST when the question names something the run mentioned but you have no id for.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'The text to look for (case-insensitive substring), e.g. an order id, a phrase ' +
'from the answer, or a tool name.',
},
maxHits: {
type: 'integer',
description: `Pointers to return (default ${FIND_HITS_DEFAULT}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.findMaxHits}).`,
},
},
required: ['query'],
additionalProperties: false,
},
execute: ({ query, maxHits }) => {
const trimmed = query.trim();
if (trimmed.length < FIND_QUERY_MIN_CHARS) {
return (`find_in_trace: '${trimmed}' is too short to search (minimum ${FIND_QUERY_MIN_CHARS} ` +
`characters) — a one-character query matches nearly every step, which is the same as ` +
`no answer. Search for an id, a value, a key, or a phrase from the answer.`);
}
const needle = trimmed.slice(0, FIND_QUERY_MAX_CHARS).toLowerCase();
const cap = (0, bounded_js_1.clampParam)(maxHits, FIND_HITS_DEFAULT, 1, types_js_2.TOOLPACK_HARD_CAPS.findMaxHits);
const hits = [];
let scans = 0;
let budgetHit = false;
/** Record a hit; true once the cap is reached and scanning should stop. */
const add = (line) => {
hits.push(line);
return hits.length >= cap;
};
// 1. STAGES — what the run's steps are called and what they claim to do.
let full = false;
for (const group of index.groups) {
const haystack = `${group.stagePart} ${group.name ?? ''} ${group.description ?? ''}`;
const window = matchWindow(haystack, needle, FIND_CONTEXT_RADIUS);
if (window === undefined)
continue;
if (add(`- stage: ${window} → trace_node('${group.ids[0]}')`)) {
full = true;
break;
}
}
// 2. STATE KEYS — the names, not the values (those are step 3).
if (!full) {
for (const path of index.knownPaths) {
const dotted = (0, bounded_js_1.displayKey)(path);
if (!dotted.toLowerCase().includes(needle))
continue;
if (add(`- state key '${dotted}' → who_wrote('${dotted}')`)) {
full = true;
break;
}
}
}
// 3. COMMITTED VALUES — what each step actually wrote. Serialized ONE
// (step, key) payload at a time and discarded: a search must never
// build a serialization of the whole log to look through.
if (!full) {
outer: for (const bundle of index.commitLog) {
const overwrite = bundle.overwrite;
const updates = bundle.updates;
for (const entry of bundle.trace) {
if (scans >= FIND_SCAN_BUDGET) {
budgetHit = true;
break outer;
}
scans++;
const payload = overwrite?.[entry.path] ?? updates?.[entry.path];
if (payload === undefined)
continue;
const window = matchWindow((0, bounded_js_1.safeStringify)(payload), needle, FIND_CONTEXT_RADIUS);
if (window === undefined)
continue;
const dotted = (0, bounded_js_1.displayKey)(entry.path);
if (add(`- ${bundle.runtimeStageId} wrote '${dotted}' (${entry.verb}): ${(0, bounded_js_1.displayText)(window)}${redactionNote([bundle], entry.path)} → get_value('${bundle.runtimeStageId}', '${dotted}')`)) {
full = true;
break outer;
}
}
}
}
// 4. NARRATIVE — the story, when the artifacts carry one.
const narrative = artifacts.narrative;
if (!full && narrative !== undefined) {
for (let i = 0; i < narrative.length; i++) {
const window = matchWindow(narrative[i], needle, FIND_CONTEXT_RADIUS);
if (window === undefined)
continue;
if (add(`- narrative line ${i}: ${(0, bounded_js_1.displayText)(window)} → read_narrative({ offset: ${i} })`)) {
full = true;
break;
}
}
}
const scope = `${index.groups.length} stage(s), ${index.knownPaths.size} state key(s), ` +
`${index.commitLog.length} committed step(s)` +
(narrative !== undefined ? `, ${narrative.length} narrative line(s)` : '');
if (hits.length === 0) {
return (`find_in_trace: no match for '${trimmed}' — searched ${scope}.\n` +
(budgetHit
? `⚠ the value scan stopped after ${FIND_SCAN_BUDGET} writes, so this is "not found ` +
`so far", not "not present" — narrow the query or drill with run_overview.\n`
: '') +
`⚠ some things NEVER enter the record and cannot be found here: run input (args), env, ` +
`state seeded before the run, values carried in closures, and anything redaction removed ` +
`(those read as their placeholder). Tool internals are not traced either — only the ` +
`arguments in and the result out.\n` +
`Try a shorter phrase, a key from run_overview, or read_narrative for the story.`);
}
const lines = [
`FOUND ${hits.length}${hits.length >= cap ? '+' : ''} match(es) for '${trimmed}' ` +
`in ${scope} — each line ends with the call that opens it:`,
...hits,
];
if (hits.length >= cap) {
lines.push(`⚠ stopped at maxHits ${cap} — more matches may exist. Narrow the query, or raise ` +
`maxHits (hard cap ${types_js_2.TOOLPACK_HARD_CAPS.findMaxHits}).`);
}
if (budgetHit) {
lines.push(`⚠ the value scan stopped after ${FIND_SCAN_BUDGET} writes — later steps were not ` +
`searched. Narrow the query or drill from run_overview.`);
}
return lines.join('\n');
},
});
}
// ── trace_node ─────────────────────────────────────────────────────────────
function buildTraceNode(artifacts, index, opts) {
return (0, tools_js_1.defineTool)({
name: 'trace_node',
description: 'Inspect ONE execution step by runtimeStageId: name + description, what it wrote ' +
'(bounded previews + true sizes), what it read, its parents (which step provided each ' +
'input, plus the decision that routed to it when known), errors, and ⚠ honesty markers. ' +
'The drill-down primitive — use ids from run_overview, trace_slice, or who_wrote.',
inputSchema: {
type: 'object',
properties: {
runtimeStageId: idProperty(index, "The step to inspect, e.g. 'normalize#0'."),
},
required: ['runtimeStageId'],
additionalProperties: false,
},
execute: ({ runtimeStageId }) => {
const node = index.nodes.get(runtimeStageId);
const bundles = index.bundlesOf.get(runtimeStageId);
if (!node && !bundles)
return unknownIdMessage(runtimeStageId, index);
const lines = [];
const flags = [
node?.isDecider ? ' [decision]' : '',
node?.subflowId ? ' [subflow]' : '',
].join('');
const name = node?.name ?? bundles?.[0]?.stage ?? stagePartOf(runtimeStageId);
lines.push(`STEP ${runtimeStageId} — "${name}"${flags}`);
if (node?.description)
lines.push(`description: ${node.description}`);
if (node?.subflowId && index.subflowDoor)
lines.push(mountLine(index, runtimeStageId));
// Writes — verb-aware values via commitValueAt (delta-mode safe).
const writes = new Map(); // path → verb (last wins)
for (const bundle of bundles ?? []) {
for (const entry of bundle.trace)
writes.set(entry.path, entry.verb);
}
if (writes.size === 0) {
lines.push('wrote: nothing committed');
}
else {
lines.push(`wrote (${writes.size}):`);
const lastIdx = index.lastIdxOf.get(runtimeStageId) ?? index.commitLog.length - 1;
let shown = 0;
for (const [path, verb] of writes) {
if (shown >= NODE_WRITE_CAP) {
lines.push(`…and ${writes.size - NODE_WRITE_CAP} more keys (output capped)`);
break;
}
const preview = (0, bounded_js_1.boundedPreview)((0, foottrace_1.commitValueAt)(index.commitLog, lastIdx, path), opts.previewChars);
const dotted = (0, bounded_js_1.displayKey)(path);
lines.push(`- ${dotted} (${verb}): ${(0, bounded_js_1.displayText)((0, bounded_js_1.renderPreview)(preview, `get_value('${runtimeStageId}', '${dotted}') for full`))}${redactionNote(bundles, path)}`);
shown++;
}
}
// Reads + parents.
const reads = node?.stageReads ? Object.keys(node.stageReads) : [];
if (reads.length === 0) {
lines.push(index.hasReadTracking
? 'read: no tracked reads recorded'
: 'read: no tracked reads recorded ⚠ (artifacts carry no read tracking)');
}
else {
const shownReads = reads.slice(0, NODE_READ_CAP).map(bounded_js_1.displayKey).join(', ');
lines.push(`read (${reads.length}): ${shownReads}${reads.length > NODE_READ_CAP ? ', … (output capped)' : ''}`);
}
const parentLines = [];
const parentCodes = [];
const anchorIdx = anchorIdxFor(runtimeStageId, index);
for (const key of reads.slice(0, NODE_READ_CAP)) {
const { writer, basis } = (0, foottrace_1.findLastWriterWithBasis)(index.commitLog, key, anchorIdx);
parentLines.push(writer
? `- data: ${(0, bounded_js_1.displayKey)(key)} ← ${writer.runtimeStageId} "${writer.stage}"` +
(basis.length > 0 ? ` ⚠ ${basis.join(', ')}` : '')
: `- data: ${(0, bounded_js_1.displayKey)(key)} ← (no tracked writer ⚠ ${basis.join(', ')})`);
parentCodes.push(...basis);
}
const controlDep = artifacts.controlDeps?.(runtimeStageId);
if (controlDep) {
parentLines.push(`- control: routed here by ${controlDep.deciderId}` +
(controlDep.label ? ` — rule "${controlDep.label}"` : ''));
}
if (parentLines.length > 0) {
lines.push('parents:');
lines.push(...parentLines);
for (const code of new Set(parentCodes))
lines.push(honestyLine(index, code));
}
if (!artifacts.controlDeps) {
lines.push('⚠ control-dependence lookup not provided — the decision that routed here is unknown.');
}
const untracked = untrackedSourcesOf(bundles);
if (untracked.length > 0) {
lines.push(`⚠ this step also consumed ${untracked.join('/')} — NOT in the parents list. ` +
honestyLine(index, 'incomplete-sources'));
}
// The tool boundary, named honestly: the agent chart's tool-execution
// stage runs CONSUMER code (DB calls, services). The trace records the
// envelope — args in, results out — never the internals. Gated on the
// agent-chart signature (a call-llm stage exists) so a generic chart
// with a coincidental 'tool-calls' stage gets no false marker.
const isAgentChart = [...index.idsByStagePart.keys()].some((part) => part.split('/').pop() === 'call-llm');
if (isAgentChart && stagePartOf(runtimeStageId).split('/').pop() === 'tool-calls') {
lines.push('⚠ boundary: tool execution happens in consumer systems — the trace records ' +
'arguments in / results out; tool internals are not traced unless the tool ' +
'returns its own diagnostic refs.');
}
const errorKeys = Object.keys(node?.errors ?? {});
if (errorKeys.length > 0) {
lines.push(`errors (${errorKeys.length}):`);
for (const key of errorKeys.slice(0, OVERVIEW_ERROR_CAP)) {
const preview = (0, bounded_js_1.boundedPreview)((node?.errors ?? {})[key], opts.previewChars);
lines.push(`- ⚠ ${key}: ${(0, bounded_js_1.displayText)((0, bounded_js_1.renderPreview)(preview))}`);
}
}
return lines.join('\n');
},
});
}
/**
* Commit-log anchor for "strictly before this step" lookups. Committed steps
* anchor at their own first bundle. A step with no commit (defensive — every
* executed stage normally records at least an empty bundle) anchors at the
* next committed step in execution order.
*/
function anchorIdxFor(runtimeStageId, index) {
const own = index.firstIdxOf.get(runtimeStageId);
if (own !== undefined)
return own;
const position = index.orderedIds.indexOf(runtimeStageId);
if (position >= 0) {
for (let i = position + 1; i < index.orderedIds.length; i++) {
const idx = index.firstIdxOf.get(index.orderedIds[i]);
if (idx !== undefined)
return idx;
}
}
return index.commitLog.length;
}
// ── trace_slice ────────────────────────────────────────────────────────────
function buildTraceSlice(artifacts, index, opts) {
return (0, tools_js_1.defineTool)({
name: 'trace_slice',
description: 'The causal chain: which earlier steps produced the data at a step (backward ' +
'read→write slice over the commit log, plus [control: rule] edges to the decisions ' +
'that routed execution when available). Returns a compact indented tree of step ids — ' +
'drill any (id) with trace_node, fetch values with get_value. Bounded by maxDepth ' +
`(default ${opts.sliceMaxDepth}, max ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxDepth}) and maxNodes ` +
`(default ${opts.sliceMaxNodes}, max ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxNodes}); truncation and ` +
"incomplete-slice ⚠ markers are always shown. Pass 'key' to slice one state key only.",
inputSchema: {
type: 'object',
properties: {
runtimeStageId: idProperty(index, "The step to slice back from, e.g. 'approve#0'."),
key: keyProperty(index, 'Optional: restrict the slice to the chain that produced THIS state key.'),
maxDepth: {
type: 'integer',
description: `Optional slice depth (default ${opts.sliceMaxDepth}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxDepth}).`,
},
maxNodes: {
type: 'integer',
description: `Optional node budget (default ${opts.sliceMaxNodes}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxNodes}).`,
},
},
required: ['runtimeStageId'],
additionalProperties: false,
},
execute: ({ runtimeStageId, key, maxDepth, maxNodes }) => {
if (!index.firstIdxOf.has(runtimeStageId)) {
if (index.nodes.has(runtimeStageId)) {
const reads = Object.keys(index.nodes.get(runtimeStageId)?.stageReads ?? {});
return (`step '${runtimeStageId}' committed nothing — the commit-log slice cannot root there. ` +
(reads.length > 0
? `It read: ${reads
.map(bounded_js_1.displayKey)
.join(', ')} — use who_wrote on each, or trace_slice from a downstream step.`
: 'Use trace_node for its details, or trace_slice from a downstream step.'));
}
return unknownIdMessage(runtimeStageId, index);
}
const depth = (0, bounded_js_1.clampParam)(maxDepth, opts.sliceMaxDepth, 1, types_js_2.TOOLPACK_HARD_CAPS.sliceMaxDepth);
const nodeBudget = (0, bounded_js_1.clampParam)(maxNodes, opts.sliceMaxNodes, 2, types_js_2.TOOLPACK_HARD_CAPS.sliceMaxNodes);
const keysReadOf = (id) => {
const node = index.nodes.get(id);
return node?.stageReads ? Object.keys(node.stageReads) : [];
};
const normalizedKey = key !== undefined ? (0, bounded_js_1.normalizeKey)(key, index.knownPaths) : undefined;
const keysFn = normalizedKey !== undefined
? (id) => (id === runtimeStageId ? [normalizedKey] : keysReadOf(id))
: keysReadOf;
const dag = (0, foottrace_1.causalChain)(index.commitLog, runtimeStageId, keysFn, {
maxDepth: depth,
maxNodes: nodeBudget,
...(artifacts.controlDeps ? { controlDeps: artifacts.controlDeps } : {}),
});
if (!dag)
return unknownIdMessage(runtimeStageId, index);
const lines = [
`CAUSAL SLICE from ${runtimeStageId}` +
(normalizedKey !== undefined ? ` for key '${(0, bounded_js_1.displayKey)(normalizedKey)}'` : '') +
` (maxDepth ${depth}, maxNodes ${nodeBudget})`,
'each line: Stage (stepId) ← via <the key it provided> [wrote: …]. Drill any (stepId) ' +
'with trace_node; fetch values with get_value.',
'',
(0, bounded_js_1.displayText)((0, foottrace_1.formatCausalChain)(dag)),
];
if (!artifacts.controlDeps) {
lines.push('⚠ control edges unavailable — artifacts carry no controlDeps lookup (attach ' +
'controlDepRecorder() to the original run); decisions that routed execution are not shown.');
}
if (!index.hasReadTracking) {
lines.push('⚠ artifacts carry no per-step read tracking — read→write edges cannot be followed; ' +
'the slice may show only the start step.');
}
return lines.join('\n');
},
});
}
// ── who_wrote ──────────────────────────────────────────────────────────────
// ── backtrack — variable-first triage (the follow-up question shape) ──────
/**
* `trace_slice` is STEP-addressed ("what fed step X?"). Follow-up questions
* arrive VARIABLE-addressed ("why did you say the quote is 11.88?" → "why is
* `quote` what it is?") — and for agent history, ELEMENT-addressed ("where
* did message 7 come from?"). This tool is that address translation, built
* on footprintjs' slice layer (`sliceForKey` / `elementProvenance`): anchor
* at the variable's last writer, walk backwards; or name one array element's
* birth step. Same honesty contracts as the rest of the pack — a missing
* slice states WHY (initial state / frozen args / a closure), element
* attribution carries its basis (append-verb exact vs prefix-inference),
* and reads-coverage warns when read tracking was off.
*/
function buildBacktrack(artifacts, index, opts) {
return (0, tools_js_1.defineTool)({
name: 'backtrack',
description: "Why is a state VARIABLE what it is? Anchors at the variable's last writer and walks the " +
'backward dependency chain (variable-first — you do not need a step id). For array ' +
"variables (e.g. 'history'), pass 'element' to ask which step produced element N instead. " +
"Optional 'before' (commit index) time-travels: the value as it stood before that point. " +
`Bounded by maxDepth (default ${opts.sliceMaxDepth}, max ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxDepth}) ` +
`and maxNodes (default ${opts.sliceMaxNodes}, max ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxNodes}).`,
inputSchema: {
type: 'object',
properties: {
variable: keyProperty(index, "The state key to explain, e.g. 'quote' or 'history'."),
element: {
type: 'integer',
description: 'Optional array index: ask which step PRODUCED this element (birth attribution) ' +
'instead of slicing the whole variable.',
},
before: {
type: 'integer',
description: 'Optional exclusive commit index — explain the value as it stood BEFORE this point ' +
"(chain from an element birth with: element_birth's commitIdx + 1).",
},
maxDepth: {
type: 'integer',
description: `Optional slice depth (default ${opts.sliceMaxDepth}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxDepth}).`,
},
maxNodes: {
type: 'integer',
description: `Optional node budget (default ${opts.sliceMaxNodes}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.sliceMaxNodes}).`,
},
},
required: ['variable'],
additionalProperties: false,
},
execute: ({ variable, element, before, maxDepth, maxNodes }) => {
const key = (0, bounded_js_1.normalizeKey)(variable, index.knownPaths);
// ── element mode: birth attribution ─────────────────────────────────
if (element !== undefined) {
const birth = (0, foottrace_2.elementProvenance)(index.commitLog, key, element, {
...(before !== undefined && { atIdx: before - 1 }),
});
if (!birth) {
const prov = (0, foottrace_2.arrayProvenance)(index.commitLog, key, {
...(before !== undefined && { atIdx: before - 1 }),
});
if (prov.missing === 'not-an-array') {
return (`'${(0, bounded_js_1.displayKey)(key)}': ${honestyLine(index, 'not-an-array')}` +
basisLines(index, prov.basis ?? []) +
`\nCall backtrack without 'element' to slice it.`);
}
if (prov.missing !== undefined) {
return `'${(0, bounded_js_1.displayKey)(key)}': ${honestyLine(index, prov.missing)}`;
}
return (`element ${element} is out of range for '${(0, bounded_js_1.displayKey)(key)}' ` +
`(length ${prov.length}). Valid indices: 0..${(prov.length ?? 1) - 1}.`);
}
return (`'${(0, bounded_js_1.displayKey)(key)}'[${birth.index}] = ${(0, bounded_js_1.renderPreview)((0, bounded_js_1.boundedPreview)(birth.value, opts.previewChars))} — ` +
`born at ${birth.runtimeStageId} ("${birth.stageName}", verb: ${birth.verb}, ` +
`attribution: ${birth.basis}${birth.basis === 'append-verb' ? ' — engine-recorded, exact' : ''}). ` +
`Drill the producer with trace_node('${birth.runtimeStageId}'), or ask why it wrote this ` +
`with backtrack({variable: '${(0, bounded_js_1.displayKey)(key)}', before: ${birth.commitIdx + 1}}).` +
// Only an INFERRED birth says why; an exact one keeps its bytes.
(birth.basis === 'prefix-inference' ? basisLines(index, [birth.basis]) : ''));
}
// ── slice mode: variable-first backward slice ────────────────────────
const depth = (0, bounded_js_1.clampParam)(maxDepth, opts.sliceMaxDepth, 1, types_js_2.TOOLPACK_HARD_CAPS.sliceMaxDepth);
const nodeBudget = (0, bounded_js_1.clampParam)(maxNodes, opts.sliceMaxNodes, 2, types_js_2.TOOLPACK_HARD_CAPS.sliceMaxNodes);
const keysReadOf = (id) => {
const node = index.nodes.get(id);
return node?.stageReads ? Object.keys(node.stageReads) : [];
};
const slice = (0, foottrace_2.sliceForKey)(index.commitLog, key, keysReadOf, {
maxDepth: depth,
maxNodes: nodeBudget,
...(before !== undefined && { before }),
...(artifacts.controlDeps ? { controlDeps: artifacts.controlDeps } : {}),
});
const lines = [
(0, bounded_js_1.displayText)((0, foottrace_2.formatSlice)(slice)),
'',
'drill any (stepId) with trace_node; fetch values with get_value; array variables take ' +
"'element' for per-element attribution.",
];
if (slice.root !== undefined && !artifacts.controlDeps) {
lines.push('⚠ control edges unavailable — artifacts carry no controlDeps lookup; decisions that ' +
'routed execution are not shown.');
}
if (slice.root !== undefined && !index.hasReadTracking) {
lines.push('⚠ artifacts carry no per-step read tracking — the slice may show only the writer step.');
}
return lines.join('\n');
},
});
}
function buildWhoWrote(index, opts) {
return (0, tools_js_1.defineTool)({
name: 'who_wrote',
description: 'Find the LAST step that wrote a state key — optionally before a given step ' +
'(beforeStageId), for "who set this value that step X then read?" questions. Returns ' +
'the writer step id + stage name + write verb + a bounded value preview.',
inputSchema: {
type: 'object',
properties: {
key: keyProperty(index, "The state key, e.g. 'dti' or 'customer.address.zip'."),
beforeStageId: idProperty(index, 'Optional: only consider writes strictly BEFORE this step.'),
},
required: ['key'],
additionalProperties: false,
},
execute: ({ key, beforeStageId }) => {
const path = (0, bounded_js_1.normalizeKey)(key, index.knownPaths);
let beforeIdx;
if (beforeStageId !== undefined) {
if (!index.firstIdxOf.has(beforeStageId) && !index.nodes.has(beforeStageId)) {
return unknownIdMessage(beforeStageId, index);
}
beforeIdx = anchorIdxFor(beforeStageId, index);
}
const { writer, basis } = (0, foottrace_1.findLastWriterWithBasis)(index.commitLog, path, beforeIdx);
if (!writer) {
return (`no tracked write to '${(0, bounded_js_1.displayKey)(path)}'` +
(beforeStageId !== undefined ? ` before ${beforeStageId}` : '') +
` in the commit log.` +
(index.knownPaths.has(path) ? '' : unknownKeySuffix(index)) +
basisLines(index, basis) +
writtenInsideLine(index, path, beforeIdx));
}
const writerIdx = index.commitLog.indexOf(writer);
const verb = writer.trace.find((entry) => entry.path === path)?.verb ?? 'set';
const read = (0, foottrace_1.commitValueAtWithBasis)(index.commitLog, writerIdx, path, {
initialState: index.initialState,
});
const preview = (0, bounded_js_1.boundedPreview)(read.value, opts.previewChars);
const untracked = untrackedSourcesOf([writer]);
return (`'${(0, bounded_js_1.displayKey)(path)}' was last written by ${writer.runtimeStageId} — "${writer.stage}" ` +
`(verb: ${verb}): ${(0, bounded_js_1.displayText)((0, bounded_js_1.renderPreview)(preview, `get_value('${writer.runtimeStageId}', '${(0, bounded_js_1.displayKey)(path)}') for full`))}${redactionNote([writer], path)}` +
basisLines(index, [...basis, ...read.basis]) +
(untracked.length > 0
? `\n⚠ that step also consumed ${untracked.join('/')}. ${honestyLine(index, 'incomplete-sources')}`
: '') +
mergeBackLine(index, writer.runtimeStageId, path));
},
});
}
/**
* A write by a subflow MOUNT is its merge-back: the value was made inside. One
* line names the question that reaches the inner writer; '' otherwise.
*/
function mergeBackLine(index, writerId, path) {
if (!index.subflowDoor || index.nodes.get(writerId)?.subflowId === undefined)
return '';
// The outputMapper may rename or compute the key: claim a writer inside only
// when the mount's own log wrote this very path. Otherwise name the mount.
if (!(0, subflowRecords_js_1.subflowWrote)(index.subflowResults?.[writerId], path)) {
return `\n↳ ${writerId} is a subflow mount — ${mountLine(index, writerId)}`;
}
return (`\n↳ ${writerId} is a subflow mount — this write is its merge-back. ` +
`inspect_subflow({ mount: '${writerId}', key: '${(0, bounded_js_1.displayKey)(path)}' }) names the writer ` +
`inside.`);
}
/**
* A key this log never wrote may still be written INSIDE a subflow mounted at
* this level — its own log never merged it back. One line naming the latest
* such mount; '' when none did (asked only on a miss).
*/
function writtenInsideLine(index, path, beforeIdx) {
const results = index.subflowResults;
if (!index.subflowDoor || results === undefined)
return '';
// Only mounts that ran BEFORE the anchor answer "who wrote it before X".
const mounts = [...index.orderedIds]
.reverse()
.filter((id) => index.nodes.get(id)?.subflowId !== undefined)
.filter((id) => beforeIdx === undefined || (index.lastIdxOf.get(id) ?? Infinity) < beforeIdx);
const inside = mounts.find((id) => (0, subflowRecords_js_1.subflowWrote)(results[id], path));
if (inside === undefined)
return '';
// "Not merged back" is a claim about THIS log: true only if it never wrote the key.
const where = index.knownPaths.has(path) ? '' : ' (not merged back here)';
return (`\n↳ but subflow ${inside} wrote '${(0, bounded_js_1.displayKey)(path)}' in its OWN log${where}: ` +
`inspect_subflow({ mount: '${inside}', key: '${(0, bounded_js_1.displayKey)(path)}' }) names the writer.`);
}
// ── get_value ──────────────────────────────────────────────────────────────
function buildGetValue(index, opts) {
return (0, tools_js_1.defineTool)({
name: 'get_value',
description: 'Fetch the FULL value of a state key as of a given step (verb-aware reconstruction — ' +
'works on delta commit logs). Output is capped at maxChars ' +
`(default ${opts.valueMaxChars}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.valueMaxChars}) with an ` +
'explicit truncation notice. Prefer fetching a narrower nested key over raising the cap.',
inputSchema: {
type: 'object',
properties: {
runtimeStageId: idProperty(index, 'The step whose post-commit view of the key you want.'),
key: keyProperty(index, "The state key, e.g. 'dti' or 'customer.address.zip'."),
maxChars: {
type: 'integer',
description: `Optional char budget (default ${opts.valueMaxChars}, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.valueMaxChars}).`,
},
},
required: ['runtimeStageId', 'key'],
additionalProperties: false,
},
execute: ({ runtimeStageId, key, maxChars }) => {
const known = index.firstIdxOf.has(runtimeStageId) || index.nodes.has(runtimeStageId);
if (!known)
return unknownIdMessage(runtimeStageId, index);
const path = (0, bounded_js_1.normalizeKey)(key, index.knownPaths);
const cap = (0, bounded_js_1.clampParam)(maxChars, opts.valueMaxChars, 50, types_js_2.TOOLPACK_HARD_CAPS.valueMaxChars);
const lastIdx = index.lastIdxOf.get(runtimeStageId);
const atIdx = lastIdx !== undefined ? lastIdx : anchorIdxFor(runtimeStageId, index) - 1;
// Nested rows count (footprintjs 9.33.0): a key written only through paths inside it —
// a subflow seed, a merge-back — is written, though no row names it exactly.
const lastWrite = (0, foottrace_1.findLastWriterWithBasis)(index.commitLog, path);
if (!lastWrite.writer) {
return (`no tracked write to '${(0, bounded_js_1.displayKey)(path)}' anywhere in the commit log.` +
(index.knownPaths.has(path) ? '' : unknownKeySuffix(index)) +
basisLines(index, lastWrite.basis));
}
const read = atIdx >= 0
? (0, foottrace_1.commitValueAtWithBasis)(index.commitLog, atIdx, path, {
initialState: index.initialState,
})
: undefined;
const value = read?.value;
if (value === undefined) {
return (`'${(0, bounded_js_1.displayKey)(path)}' has no value as of ${runtimeStageId}` +
` (last writer over the whole run: ${lastWrite.writer.runtimeStageId}).` +
basisLines(index, read?.basis ?? ['never-written']));
}
const serialized = (0, bounded_js_1.displayText)((0, bounded_js_1.safeStringify)(value));
const redacted = redactionNote(index.bundlesOf.get(runtimeStageId), path);
const header = `VALUE of '${(0, bounded_js_1.displayKey)(path)}' as of ${runtimeStageId}${redacted}:`;
const basis = basisLines(index, read?.basis ?? []);
if (serialized.length <= cap)
return `${header}\n${serialized}${basis}`;
return (`${header}\n${serialized.slice(0, cap)}\n` +
`⚠ truncated: served ${cap} of ${serialized.length} chars — raise maxChars ` +
`(hard cap ${types_js_2.TOOLPACK_HARD_CAPS.valueMaxChars}) or fetch a narrower nested key.` +
basis);
},
});
}
/** A recorded `notDispatched` value read as the marker — `undefined` unless it is an object. */
function markerOf(found) {
return typeof found === 'object' && found !== null ? found : undefined;
}
/**
* Resolve ONE tool call across the four places its evidence lives.
*
* A tool call is the most-asked-about thing in an agent run and the most
* scattered: the model's PROPOSED args are on an assistant turn, the args
* it actually RAN with are in the middleware ledger (and only when a rule
* changed them), the result is a `role:'tool'` turn, and the timing exists
* only in the event stream. Four lookups, one id — so this reader does the
* join rather than making the model do it in four calls.
*
* The proposed/ran-with split is the point. A governance rule that
* rewrites args is invisible in the conversation: the model reads its own
* proposal in history and the tool ran on something else. That difference
* is exactly what "why did it do that?" is often asking about.
*/
function buildToolCallReader(artifacts, index) {
// All four readers are lazy + memoized: a toolpack that never inspects a
// tool call pays nothing, and one that inspects five pays once.
let historyMemo;
const history = () => {
if (historyMemo === undefined) {
const value = committedFinal(index, 'history');
historyMemo = Array.isArray(value) ? value : [];
}
return historyMemo;
};
let ledgerMemo;
const ledger = () => {
if (ledgerMemo === undefined) {
const value = committedFinal(index, 'middlewareDecisions');
ledgerMemo = Array.isArray(value) ? value : [];
}
return ledgerMemo;
};
const eventsOf = (type) => (artifacts.events ?? [])
.filter((event) => event.type === type)
.map((event) => ({
payload: (event.payload ?? {}),
meta: (event.meta ?? {}),
}));
/**
* One call's brackets of one kind, those of a call that RAN first (9.113.0).
* A settled bracket (`notDispatched`) is no record of a run, so when a
* provider reused a settled call's id for a call that did run, the STEP is
* read off the call that ran — never off the settlement's bracket, which
* stands on the resume step. Tail order within each group, so a tail with
* no settled bracket reads exactly as it always did. (The outcome and the
* duration read the LAST `tool_end` for the id instead — see
* `buildInspectToolCall`.)
*/
const bracketsFor = (type, toolCallId) => {
const mine = eventsOf(type).filter((event) => event.payload.toolCallId === toolCallId);
const settled = (event) => markerOf(event.payload.notDispatched) !== undefined;
return [...mine.filter((event) => !settled(event)), ...mine.filter(settled)];
};
/** Every tool call id this run recorded, in call order, with its name. */
let idsMemo;
const knownCalls = () => {
if (idsMemo !== undefined)
return idsMemo;
const byId = new Map();
for (const start of eventsOf('agentfootprint.stream.tool_start')) {
const id = start.payload.toolCallId;
if (typeof id === 'string')
byId.set(id, String(start.payload.toolName ?? '?'));
}
for (const turn of history()) {
for (const call of turn.toolCalls ?? []) {
if (typeof call.id === 'string' && !byId.has(call.id)) {
byId.set(call.id, call.name ?? '?');
}
}
if (typeof turn.toolCallId === 'string' && !byId.has(turn.toolCallId)) {
byId.set(turn.toolCallId, turn.toolName ?? '?');
}
}
idsMemo = [...byId].map(([id, name]) => ({ id, name }));
return idsMemo;
};
/**
* Which STEP ran this call. The event tail knows exactly (every event is
* stamped with its `runtimeStageId`) — a call that RAN before a settlement
* under the same id (`bracketsFor`); without it, fall back to the first
* committed step whose `history` carries the call's result — that is the
* tool-execution step by construction.
*/
const stepFor = (toolCallId) => {
for (const start of bracketsFor('agentfootprint.stream.tool_start', toolCallId)) {
const id = start.meta.runtimeStageId;
if (typeof id === 'string')
return { id, inferred: false };
}
for (let i = 0; i < index.commitLog.length; i++) {
const bundle = index.commitLog[i];
if (!bundle.trace.some((entry) => entry.path === 'history'))
continue;
const value = (0, foottrace_1.commitValueAt)(index.commitLog, i, 'history');
if (!Array.isArray(value))
continue;
const landed = value.some((turn) => turn.role === 'tool' && turn.toolCallId === toolCallId);
if (landed)
return { id: bundle.runtimeStageId, inferred: true };
}
return { inferred: true };
};
/**
* The settlement marker on the LATEST `role: 'tool'` message for the id in
* `turns` — `undefined` when that message carries none — and whether any
* message answered the id at all. The latest decides, as it decides the
* result: a provider may reuse an id across turns.
*/
const settledIn = (turns, toolCallId) => {
let answered = false;
let marker;
for (const turn of turns) {
if (turn.role !== 'tool' || turn.toolCallId !== toolCallId)
continue;
answered = true;
marker = markerOf(turn.notDispatched);
}
return { answered, ...(marker !== undefined && { marker }) };
};
/**
* The marker on the LATEST bracket for the id on the event tail — the
* stream's own owner of the fact (`ToolStartPayload.notDispatched`,
* `ToolEndPayload.notDispatched`, 9.113.0). `undefined` when the tail holds
* no bracket for the id or the latest one carries no marker: the latest
* decides, as it does in history, because a provider may reuse an id.
*/
const settledOnStream = (toolCallId) => {
let marker;
for (const event of artifacts.events ?? []) {
if (event.type !== 'agentfootprint.stream.tool_start' &&
event.type !== 'agentfootprint.stream.tool_end') {
continue;
}
const payload = (event.payload ?? {});
if (payload.toolCallId !== toolCallId)
continue;
marker = markerOf(payload.notDispatched);
}
return marker;
};
/**
* The history's answer: the final history decides when it still holds a
* message for the id. When a window has evicted it, the history the call's
* OWN step committed still does — that step appended it — so that is where
* the marker is read; with no step to read, nothing is said.
*/
const settledInHistory = (toolCallId, stepId) => {
const final = settledIn(history(), toolCallId);
if (final.answered || stepId === undefined)
return final.marker;
const at = index.lastIdxOf.get(stepId);
if (at === undefined)
return undefined;
const committed = (0, foottrace_1.commitValueAt)(index.commitLog, at, 'history');
return Array.isArray(committed)
? settledIn(committed, toolCallId).marker
: undefined;
};
/**
* Was the call NEVER DISPATCHED (9.113.0)? Read off a marker, never the
* sentence and never the bracket's `durationMs: 0`. The
* bracket's own field first — the event tail answers alone, even for a
* history that no longer holds the message — and the history's marker when
* the tail is absent or its bracket carries none (a bracket without the
* field is silence, not a denial).
*/
const notDispatchedOf = (toolCallId, stepId) => settledOnStream(toolCallId) ?? settledInHistory(toolCallId, stepId);
const factsFor = (toolCallId) => {
let toolName;
let proposedArgs;
let result;
for (const turn of history()) {
for (const call of turn.toolCalls ?? []) {
if (call.id === toolCallId) {
toolName = call.name ?? toolName;
proposedArgs = call.args;
}
}
if (turn.role === 'tool' && turn.toolCallId === toolCallId) {
toolName = turn.toolName ?? toolName;
result = turn.content;
}
}
if (toolName === undefined) {
toolName = knownCalls().find((call) => call.id === toolCallId)?.name;
}
const step = stepFor(toolCallId);
const notDispatched = notDispatchedOf(toolCallId, step.id);
return {
...(toolName !== undefined && { toolName }),
...(proposedArgs !== undefined && { proposedArgs }),
...(result !== undefined && { result }),
...(step.id !== undefined && { runtimeStageId: step.id }),
...(notDispatched !== undefined && { notDispatched }),
};
};
return {
knownCalls,
factsFor,
history,
rowsFor: (toolCallId) => ledger().filter((row) => row.toolCallId === toolCallId),
eventsOf,
};
}
/**
* The tool over {@link buildToolCallReader}'s join.
*
* The `toolCallId` param deliberately carries NO schema enum (unlike step
* ids): building one would mean reconstructing the whole committed history
* at factory time for every toolpack, including the runs that never open
* this tool. The corrective message does the same job, on demand.
*/
function buildInspectToolCall(artifacts, reader) {
const { knownCalls, factsFor, history, rowsFor, eventsOf } = reader;
return (0, tools_js_1.defineTool)({
name: 'inspect_tool_call',
description: 'Resolve ONE tool call end to end: which tool, the arguments the model proposed, the ' +
'arguments it actually ran with (they differ when a governance rule rewrote them), the ' +
'bounded result, the outcome (ok / error / denied / paused), how long it took, and the ' +
'step id that ran it. Use it whenever the question is about something a tool did — it ' +
'joins four records that otherwise take four calls to read.',
inputSchema: {
type: 'object',
properties: {
toolCallId: {
type: 'string',
description: "The tool call's id, e.g. 'call_01'. Ids appear in trace_node on the tool step, " +
'in the narrative, and in find_in_trace hits.',
},
},
required: ['toolCallId'],
additionalProperties: false,
},
execute: ({ toolCallId }) => {
const calls = knownCalls();
if (!calls.some((call) => call.id === toolCallId)) {
if (calls.length === 0) {
return (`unknown toolCallId '${toolCallId}' — this run recorded NO tool calls at all. ` +
(artifacts.events === undefined && history().length === 0
? '⚠ it also carries no committed conversation history and no event tail, so a ' +
'tool call could not be seen even if one happened. '
: '') +
'Call run_overview to see what the run did do.');
}
const sample = calls
.slice(0, TOOL_CALL_SUGGESTION_CAP)
.map((call) => `${call.id} (${call.name})`)
.join(', ');
return (`unknown toolCallId '${toolCallId}'. This run recorded ${calls.length} tool call(s): ` +
`${sample}${calls.length > TOOL_CALL_SUGGESTION_CAP ? ', …' : ''}. Retry with one of ` +
`those ids.`);
}
const facts = factsFor(toolCallId);
const lines = [`TOOL CALL ${toolCallId} — ${facts.toolName ?? '(unnamed tool)'}`];
lines.push(facts.runtimeStageId !== undefined
? `step: ${facts.runtimeStageId} — drill with trace_node('${facts.runtimeStageId}')`
: `step: ⚠ not resolvable — no event tail, and no committed step carries the result ` +
`of call '${toolCallId}' (the turn may have ended before it landed).`);
lines.push(facts.proposedArgs !== undefined
? `proposed by the model: ${(0, bounded_js_1.displayText)((0, bounded_js_1.renderPreview)((0, bounded_js_1.boundedPreview)(facts.proposedArgs, TOOL_RESULT_PREVIEW_CHARS)))}`
: `proposed by the model: ⚠ not recorded — the assistant turn carrying call ` +
`'${toolCallId}' is not in the committed history (a window strategy may have ` +
`folded it away).`);
// A call the paused batch never dispatched (9.113.0) ran with nothing,
// has no outcome of its own, no duration and no inside. Read off the
// settlement marker (`notDispatchedOf`), each line a past fact anchored
// to the call by id. Its `tool_end` is the settlement's bracket
// (`durationMs: 0`, the sentence as `result`), never the record of a
// tool that ran — read as one, it would be an `ok` in 0ms.
const settled = facts.notDispatched;
// Ran-with args: the ledger is the ONLY record of a rule rewriting
// them. No ledger row means no rule changed anything — say that
// plainly rather than leaving the reader to assume it.
const rows = rowsFor(toolCallId);
const rewrite = [...rows]
.reverse()
.find((row) => row.changed === true && row.moment === 'before-tool');
if (settled !== undefined) {
lines.push(`ran with: nothing — call '${toolCallId}' was never executed.`);
}
else if (rewrite) {
lines.push(`ran with: ${(0, bounded_js_1.displayText)((0, bounded_js_1.renderPreview)((0, bounded_js_1.boundedPreview)(rewrite.after, TOOL_RESULT_PREVIEW_CHARS)))} — CHANGED at before-tool by '${rewrite.middleware ?? '?'}'` +
(rewrite.why ? `: "${rewrite.why}"` : '') +
'. The model proposed the line above; the tool ran on this one.');
}
else if (rows.length > 0) {
lines.push('ran with: the proposed arguments, unchanged (rules looked and allowed them).');
}
else {
lines.push(`ran with: the proposed arguments — no governance rule filed a row for call ` +
`'${toolCallId}'.`);
}
lines.push(facts.result !== undefined
? `result: ${(0, bounded_js_1.displayText)((0, bounded_js_1.renderPreview)((0, bounded_js_1.boundedPreview)(facts.result, TOOL_RESULT_PREVIEW_CHARS), `get_value('${facts.runtimeStageId ?? '<step>'}', 'history') for the full turn`))}`
: `result: ⚠ no result recorded for call '${toolCallId}' in the committed history — ` +
`it may have been denied before it ran, or the turn ended (paused/failed) before ` +
`it landed.`);
// Outcome + duration: the event tail is the only clock a run has. Read
// off the LAST `tool_end` for the id — the call the result line shows,
// since a provider may reuse an id and the latest message decides the
// result. After a settlement that is the call that ran under the id
// again; a settled call itself never reaches these lines (`settled`).
const denial = rows.find((row) => row.outcome === 'deny');
const end = eventsOf('agentfootprint.stream.tool_end')
.reverse()
.find((event) => event.payload.toolCallId === toolCallId);
const checkIn = eventsOf('agentfootprint.checkin.request').some((event) => event.payload.toolCallId === toolCallId);
const invalid = eventsOf('agentfootprint.validation.args_invalid').some((event) => event.payload.toolCallId === toolCallId);
let outcome;
if (settled !== undefined) {
outcome =
`not dispatched — the run paused on call '${settled.pausedCall?.toolCallId ?? '?'}' ` +
`to '${settled.pausedCall?.toolName ?? '?'}', earlier in the same batch, and resumed ` +
`without executing call '${toolCallId}'`;
}
else if (denial) {
outcome = `denied by '${denial.middleware ?? '?'}'${denial.why ? `: "${denial.why}"` : ''}`;
}
else if (end?.payload.error === true) {
outcome = 'error — the tool threw or returned a failure';
}
else if (checkIn && end === undefined) {
outcome = `paused — a check-in asked a human and call '${toolCallId}' has no recorded end`;
}
else if (facts.result !== undefined) {
outcome = 'ok';
}
else {
outcome = '⚠ unknown — no ledger row, no result, no end event';
}
lines.push(`outcome: ${outcome}`);
if (invalid) {
// Anchored to the call by ID, not by "this call" (9.86.0): the line
// lands in a tool result, and a model re-reading it three calls later
// resolves "this call" to whichever call it is answering then.
lines.push(`⚠ the arguments failed schema validation on call '${toolCallId}' — see the ` +
`validation event / the tool result, which carries the correction the model was ` +
`given.`);
}
if (settled !== undefined) {
lines.push(`duration: none — call '${toolCallId}' was never executed.`);
}
else if (artifacts.events === undefined) {
lines.push(`duration: ⚠ unavailable — these artifacts carry no event tail. The commit log records ` +
`what each step WROTE and has no clock; timings live only in the event stream.`);
}
else if (end !== undefined && typeof end.payload.durationMs === 'number') {
lines.push(`duration: ${end.payload.durationMs}ms`);
}
else {
lines.push(`duration: ⚠ no tool_end event for call '${toolCallId}' in the retained tail (it may ` +
`have been dropped by the tail cap, or the call never finished).`);
}
// THE DESCENT RUNG. The boundary marker below is honest for a tool
// that called someone else's system, and needlessly honest for one
// that kept its own record — so when a record exists the line that
// says "not traced" is replaced by the call that opens it. One line,
// and it teaches the id it takes: the SAME toolCallId, one level down.
const inner = artifacts.innerRuns?.get(toolCallId);
lines.push(settled !== undefined
? `inside: nothing — call '${toolCallId}' never reached the tool.`
: inner !== undefined
? `inside: this tool kept its own record of the run — ${inner.steps} step(s), ` +
`${inner.outcome}. Descend with inspect_tool_run({ toolCallId: '${toolCallId}' }).`
: '⚠ boundary: what happened INSIDE the tool is not traced — this is the envelope ' +
'(arguments in, result out) plus what the run itself decided about it.');
return lines.join('\n');
},
});
}
// ── inspect_tool_run ───────────────────────────────────────────────────────
/**
* THROUGH the boundary — the tool call's own run, opened.
*
* `inspect_tool_call` answers everything about a call except the question
* that usually follows it: *and then what did it DO?* For a tool that
* reaches into a payments API, "not traced" is the true answer. For a tool
* that is itself a footprintjs chart, the true answer was thrown away —
* the chart recorded every stage and nobody held the record.
*
* `flowchartAsTool({ keepRecord: true })` holds it. This tool opens it, and
* three properties keep the descent from becoming a second, worse trace API:
*
* - ONE VOCABULARY. The inner views are the pack's own tools, run over
* the inner artifact bag through `openRecording` — the same adapter a
* saved recording uses. There is no second implementation of "what did
* this step write", so there is no second implementation to drift.
* - TWO ID NAMESPACES, SAID OUT LOUD. Inner `runtimeStageId`s belong to
* the tool's chart, not the outer run. Every output says so, because a
* model that pastes an inner id into `trace_node` gets a correction
* that reads like a bug rather than like a boundary.
* - HONEST ABSENCE THAT NAMES THE SWITCH. `keepRecord` is off by default
* (retaining a snapshot per invocation is memory the caller has to
* agree to), so "nothing to open" is the COMMON case — and an answer
* that just says "nothing" teaches the model the tool is broken.
*/
function buildInspectToolRun(artifacts, reader, options) {
// One inner pack at a time, memoized by call id (`innerDescent`): three
// drills into one inner run build one index.
const slot = (0, innerDescent_js_1.innerPackSlot)((inner) => traceToolpack(inner, options));
const openRecord = (record) => {
if (record.recording === undefined) {
return (`tool call '${record.toolCallId}' ran '${record.toolName}', which keeps records — but ` +
`THIS one could not be captured: ${record.problem ?? 'reason not recorded'}. The call ` +
`itself was unaffected; inspect_tool_call('${record.toolCallId}') still has the ` +
`envelope (arguments in, result out).`);
}
try {
const inner = {
// `openRecording` is the adapter, not a copy of it: same lift, same
// two teaching refusals if the bag is malformed. The two fields it
// cannot lift from JSON — control edges, and a narrative nobody
// attached — are added here because this record is LIVE in process.
...(0, openRecording_js_1.openRecording)(record.recording),
...(record.controlDeps !== undefined && { controlDeps: record.controlDeps }),
...(record.narrative !== undefined && { narrative: record.narrative }),
};
WITHOUT_SUBFLOW_DOOR.add(inner);
return inner;
}
catch (e) {
return (`the retained record for '${record.toolCallId}' could not be opened: ` +
`${e instanceof Error ? e.message : String(e)}`);
}
};
return (0, tools_js_1.defineTool)({
name: 'inspect_tool_run',
description: 'Go INSIDE one tool call. Some tools are themselves recorded flowcharts and keep the ' +
'record of what they ran; this opens it. Called with just a toolCallId it returns a ' +
'bounded overview of the inner run (its stages, its errors, its state keys). Add ' +
"'find' to search that inner run in free text, 'variable' to ask why an inner value is " +
"what it is, 'key' alone for who last wrote an inner key, 'runtimeStageId' to open one " +
"inner step, or both 'runtimeStageId' and 'key' for an inner value in full. The ids it returns are the INNER chart's — the " +
'outer trace tools do not accept them. Reach for it when inspect_tool_call says a ' +
'record was kept and the question is about what the tool DID, not what it returned.',
inputSchema: {
type: 'object',
properties: {
toolCallId: {
type: 'string',
description: "The tool call to descend into — the same id inspect_tool_call takes, e.g. 'c1'.",
},
runtimeStageId: {
type: 'string',
description: "Optional: open ONE step of the inner run (an inner id, from this tool's own " +
'overview — not an outer step id).',
},
key: {
type: 'string',
description: "Optional: an inner state key, e.g. 'forecast' — alone, who last wrote it inside; " +
'with runtimeStageId, its value in full as of that step.',
},
variable: {
type: 'string',
description: 'Optional: an inner state key to explain — anchors at its last writer inside the ' +
'chart and walks back.',
},
find: {
type: 'string',
description: 'Optional: free text to locate inside the inner run (returns inner ids).',
},
cause: {
type: 'string',
enum: ['attention_list', 'root_cause', 'impact'],
description: 'Optional: ask a bounded cause tool inside this retained run.',
},
nodeId: {
type: 'string',
description: 'An opaque node id returned by attention_list inside this retained run.',
},
},
required: ['toolCallId'],
additionalProperties: false,
},
execute: async ({ toolCallId, runtimeStageId, key, variable, find, cause, nodeId }, ctx) => {
const lookup = artifacts.innerRuns;
if (lookup === undefined)
return noInnerRecordsMessage(toolCallId);
const record = lookup.get(toolCallId);
if (record === undefined)
return unknownInnerRunMessage(toolCallId, lookup, reader);
const inner = slot(record.toolCallId, () => openRecord(record));
if (typeof inner === 'string')
return inner;
const body = await (0, innerDescent_js_1.askInner)(inner, { runtimeStageId, key, variable, find, cause, nodeId }, callTraceTool, ctx);
return (0, innerDescent_js_1.framed)({
header: `INSIDE TOOL CALL ${toolCallId} — '${record.toolName}' ran a recorded flowchart ` +
`(${record.steps} committed step(s), ${record.outcome}).`,
next: `next inside call '${toolCallId}': inspect_tool_run({ toolCallId: '${toolCallId}', ` +
`runtimeStageId: '<inner id>' }) opens a step · add 'key' for a value in full · ` +
`'variable' asks why an inner value is what it is · 'find' searches this inner run.`,
namespace: `⚠ the ids above are INNER ids — they name steps of ${record.toolName}'s own chart, ` +
`not of the run that called it. trace_node / get_value / trace_slice do not accept ` +
`them; come back through inspect_tool_run.`,
}, body);
},
});
}
// ── inspect_subflow ────────────────────────────────────────────────────────
/** How many mounts a correction names. */
const MOUNT_SUGGESTION_CAP = 8;
/**
* INSIDE a subflow mount — the subflow's OWN log, opened.
*
* A subflow runs in an isolated runtime and commits to its own log, so this
* run's log holds only the mount boundary: what went in, what came back. The
* evidence of who wrote a value IN there was always recorded (footprintjs keeps
* it in `subflowResults`) — no tool served it. This one does, through the same
* descent `inspect_tool_run` uses (`innerDescent`): the inner views are the
* pack's own tools over the mount's own record, so the inner answers carry the
* same reason codes, folded from the SUBFLOW's own `initialState`.
*
* - BY REFERENCE. Nothing from inside a subflow is printed until the model
* asks; the outer views carry one line naming the door.
* - ONE LEVEL AT A TIME. A mount nested inside a subflow is opened through
* its parent's pack — this tool walks the chain (`mountChain`), each level
* answering only for its own log.
* - ITS OWN NAMESPACE. Inner ids are said to be inner on every answer, and
* an outer tool handed one corrects toward here (`innerIdMessage`).
*/
function buildInspectSubflow(artifacts, index, options) {
const slot = (0, innerDescent_js_1.innerPackSlot)((inner) => traceToolpack(inner, options));
const isStep = (id) => index.nodes.has(id) || index.firstIdxOf.has(id);
const mountIds = () => index.orderedIds.filter((id) => index.nodes.get(id)?.subflowId);
const notAMount = (mount) => {
if (isStep(mount)) {
return `'${mount}' is an outer step, not a subflow mount — trace_node('${mount}') opens it.`;
}
const mounts = mountIds();
const sameStage = mounts.filter((id) => stagePartOf(id) === mount);
if (sameStage.length > 0) {
return (`'${mount}' ran ${sameStage.length} time(s); a mount is ONE execution: ` +
`${sameStage.slice(0, MOUNT_SUGGESTION_CAP).join(', ')}` +
`${sameStage.length > MOUNT_SUGGESTION_CAP ? ', …' : ''}. Retry with one of those ids.`);
}
if (mounts.length === 0)
return `the traced run mounted no subflows — there is no inner log to open.`;
return (`unknown mount '${mount}'. Subflow mounts in the traced run: ` +
`${mounts.slice(0, MOUNT_SUGGESTION_CAP).join(', ')}` +
`${mounts.length > MOUNT_SUGGESTION_CAP ? ', …' : ''}.`);
};
return (0, tools_js_1.defineTool)({
name: 'inspect_subflow',
description: "Open a [subflow] step's OWN log (the run's log holds only its boundary). Mount id alone: " +
"inner overview. 'key': its last writer inside. 'variable': why it is what it is. " +
"'find': search. 'runtimeStageId' (+'key'): one inner step (value). Inner ids go back here.",
inputSchema: {
type: 'object',
properties: {
mount: { type: 'string', description: "The mount step, e.g. 'sf-tools#11'." },
runtimeStageId: { type: 'string', description: 'Optional: an inner step id.' },
key: { type: 'string', description: 'Optional: an inner state key.' },
variable: { type: 'string', description: 'Optional: an inner state key to explain.' },
find: { type: 'string', description: 'Optional: free text to locate inside.' },
cause: {
type: 'string',
enum: ['attention_list', 'root_cause', 'impact'],
description: 'Optional: ask a bounded cause tool inside this subflow.',
},
nodeId: {
type: 'string',
description: 'An opaque node id returned by attention_list inside this subflow.',
},
},
required: ['mount'],
additionalProperties: false,
},
execute: async ({ mount, runtimeStageId, key, variable, find, cause, nodeId }, ctx) => {
const question = { runtimeStageId, key, variable, find, cause, nodeId };
const results = index.subflowResults;
const isMount = index.nodes.get(mount)?.subflowId !== undefined;
// A mount nested below this level: open its parent here, ask there.
if (!isMount && !isStep(mount) && results !== undefined) {
const chain = (0, subflowRecords_js_1.mountChain)(results, mount, isStep);
if (chain !== undefined && chain.length > 1) {
const parent = chain[0];
const pack = slot(parent, () => openSubflowArtifacts(artifacts, parent));
if (typeof pack === 'string')
return pack;
return callTraceTool(pack, 'inspect_subflow', { mount, ...definedOf(question) }, ctx);
}
}
if (!isMount)
return notAMount(mount);
if (runtimeStageId !== undefined &&
runtimeStageId !== mount &&
isStep(runtimeStageId) &&
index.homeMountOf(runtimeStageId) !== mount) {
return (`'${runtimeStageId}' is a step of the run ABOVE subflow ${mount}, not of its own log — ` +
`trace_node('${runtimeStageId}') opens it there.`);
}
const inner = slot(mount, () => openSubflowArtifacts(artifacts, mount));
if (typeof inner === 'string')
return inner;
const body = await (0, innerDescent_js_1.askInner)(inner, question, callTraceTool, ctx);
const name = index.nodes.get(mount)?.name;
const parent = index.homeMountOf(mount);
const commits = (0, subflowRecords_js_1.innerCommitCount)(index.subflowResults, mount);
return (0, innerDescent_js_1.framed)({
header: `INSIDE SUBFLOW ${mount}${name !== undefined ? ` — "${name}"` : ''}` +
`${parent !== undefined ? ` (nested in ${parent})` : ''}: its own log, ` +
`${commits} commit(s)${(0, subflowRecords_js_1.opensWithSeed)(index.subflowResults, mount) ? ", the mount's seed first" : ''}.`,
next: `next inside '${mount}': inspect_subflow({ mount: '${mount}', runtimeStageId: ` +
`'<inner id>' }) opens a step · 'key' alone names its last writer inside · 'variable' ` +
`asks why an inner value is what it is · 'find' searches this inner log.`,
namespace: `⚠ the ids above are INNER ids — steps of subflow ${mount}'s own log, not of the run ` +
`that mounted it. trace_node / get_value / trace_slice correct them back here.`,
}, body);
},
});
}
/**
* The run's own control-dependence lookup, keyed by every subflow bag opened
* below it — so each level filters the RUN's edges to its own log
* (`openSubflowArtifacts`), at any depth.
*/
const RAW_CONTROL_DEPS = new WeakMap();
/** The mount's record as an artifact bag, or why there is none. */
function openSubflowArtifacts(artifacts, mount) {
const opened = (0, subflowRecords_js_1.openSubflow)(artifacts.snapshot, mount);
if (typeof opened === 'string')
return `inspect_subflow: ${opened}`;
// Step ids are unique across a run, so the RUN's lookup answers inner ids —
// but a decider OUTSIDE this log is not an inner step: keep only edges from
// a decider in the mount's own log, so no outer id is served as an inner one.
// Always filter the run's RAW lookup, never this level's filtered one: a
// nested mount's deciders are not in its parent's log, so filtering the
// parent's view would drop every edge one level further down.
const raw = RAW_CONTROL_DEPS.get(artifacts) ?? artifacts.controlDeps;
const innerIds = new Set(opened.commitLog.map((bundle) => bundle.runtimeStageId));
const bag = {
snapshot: opened,
...(raw !== undefined && {
controlDeps: (id) => {
const dep = raw(id);
return dep !== undefined && innerIds.has(dep.deciderId) ? dep : undefined;
},
}),
};
if (raw !== undefined)
RAW_CONTROL_DEPS.set(bag, raw);
return bag;
}
/** The question's params that were actually given (strict schemas refuse `undefined`). */
function definedOf(question) {
return Object.fromEntries(Object.entries(question).filter((entry) => entry[1] !== undefined));
}
/** No tool in this run keeps a record — name the switch, not the emptiness. */
function noInnerRecordsMessage(toolCallId) {
return (`inspect_tool_run: nothing in this run kept a record below the tool boundary, so there is ` +
`no inner run for '${toolCallId}' — or for any other call. A tool keeps one only when it ` +
`was built to: flowchartAsTool({ name, flowchart, keepRecord: true }). That switch is OFF ` +
`by default because retaining a run's snapshot per invocation is memory the caller has to ` +
`agree to. Without it the trace holds the envelope — arguments in, result out — which is ` +
`what inspect_tool_call('${toolCallId}') serves.`);
}
/** A record was expected and is not here: evicted, or never kept for THIS tool. */
function unknownInnerRunMessage(toolCallId, lookup, reader) {
const kept = lookup.list();
const lines = [
`inspect_tool_run: no retained inner run for tool call '${toolCallId}'.`,
];
// Did the OUTER run make this call at all? Three different situations
// wear the same "not found", and only one of them is the consumer's to fix.
const outer = reader.knownCalls().find((call) => call.id === toolCallId);
if (outer === undefined) {
lines.push(`⚠ the outer run does not record a call with that id either — check ` +
`inspect_tool_call('${toolCallId}') first; it names every id the outer run recorded.`);
}
else {
lines.push(`That call ran '${outer.name}', which either does not keep records (only ` +
`flowchartAsTool({ keepRecord: true }) does) or had this one dropped.`);
}
if (kept.length === 0) {
// Past tense, anchored to the lookup that was answered (9.86.0). "Right
// now" pointed at the moment of READING, and this string is a tool result
// the model re-reads on every later call of the debugging turn.
lines.push(`No inner run was held when inspect_tool_run('${toolCallId}') was answered. A tool that ` +
`keeps records had not been called in the turn this trace covers.`);
}
else {
const sample = kept
.slice(0, INNER_RUN_SUGGESTION_CAP)
.map((row) => `${row.toolCallId} (${row.toolName}, ${row.steps} step(s), ${row.outcome})`)
.join(', ');
lines.push(`Inner runs you CAN open (${kept.length}): ${sample}` +
(kept.length > INNER_RUN_SUGGESTION_CAP ? ', …' : '') +
'.');
}
if (lookup.dropped > 0) {
lines.push(
// Named by id, not "this call" (9.86.1): the same repair the
// schema-validation line above got in 9.86.0, on the arm the checker
// could not see because its rule only knew `on this call`.
`⚠ ${lookup.dropped} older record(s) were dropped to stay under the retention cap of ` +
`${lookup.limit} — call '${toolCallId}' may be one of them. Raise it with ` +
`keepRecordLimit, or ask sooner after the call.`);
}
return lines.join('\n');
}
// ── read_narrative ─────────────────────────────────────────────────────────
function buildReadNarrative(narrative) {
return (0, tools_js_1.defineTool)({
name: 'read_narrative',
description: "Read the run's human-readable narrative, paginated: offset (0-based line index) + " +
`maxLines (default 40, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.narrativeMaxLines}). Long lines are ` +
'capped. Use AFTER the structured tools when you need the story around a step.',
inputSchema: {
type: 'object',
properties: {
offset: { type: 'integer', description: '0-based first line to read (default 0).' },
maxLines: {
type: 'integer',
description: `Lines to read (default 40, hard cap ${types_js_2.TOOLPACK_HARD_CAPS.narrativeMaxLines}).`,
},
},
additionalProperties: false,
},
execute: ({ offset, maxLines }) => {
const total = narrative.length;
if (total === 0)
return 'NARRATIVE: empty (0 lines).';
const start = (0, bounded_js_1.clampParam)(offset, 0, 0, Math.max(0, total - 1));
const count = (0, bounded_js_1.clampParam)(maxLines, 40, 1, types_js_2.TOOLPACK_HARD_CAPS.narrativeMaxLines);
const slice = narrative.slice(start, start + count);
const lines = [
`NARRATIVE lines ${start}–${start + slice.length - 1} of ${total}:`,
...slice.map((line) => truncateText((0, bounded_js_1.displayText)(line), NARRATIVE_LINE_CHAR_CAP)),
];
const remaining = total - (start + slice.length);
if (remaining > 0) {
lines.push(`…${remaining} more line(s) — call read_narrative({ offset: ${start + slice.length} }).`);
}
return lines.join('\n');
},
});
}
// ── Scripted/offline invocation (the auditor pattern) ─────────────────────
/**
* Minimal offline ToolExecutionContext — trace tools never use credentials.
*
* `teardownScopes: []` is a STATEMENT, not an omission (9.7.0): nothing fires
* here, ever. There is no run, no session and no dispatch loop to settle a call
* against — this context exists so a toolpack tool can be invoked from a script.
* A tool that reads the empty list knows not to open anything it would need
* closed; a tool that calls `onTeardown` anyway is refused by name rather than
* having its cleanup silently filed where nothing will run it.
*/
const OFFLINE_CONTEXT = {
toolCallId: 'trace-toolpack-offline',
iteration: 0,
credentials: (0, types_js_1.unconfiguredCredentialProvider)(),
hasCredentials: false,
// Offline has no run to scope refs to; the capability teaches, never lies.
artifacts: (0, capability_js_1.unconfiguredArtifacts)(),
hasArtifacts: false,
// Offline has no stage to emit from and no dispatcher to reach (9.52.0);
// a progress report is dropped rather than refused, because losing telemetry
// is not a reason to fail a call that is doing its work.
progress: () => {
/* nowhere to file it offline — see the note above */
},
teardownScopes: [],
onTeardown: () => {
throw new Error('callTraceTool: onTeardown is not honoured offline — `ctx.teardownScopes` is empty, ' +
'so no scope would ever fire. Run this tool inside an Agent if it holds a session.');
},
};
/**
* Invoke a toolpack tool OUTSIDE an Agent (scripted debug sessions, tests,
* offline auditors). Mirrors the Agent's #9 boundary: args are validated
* against the tool's inputSchema first, and an invalid call returns the same
* model-visible correction string instead of executing.
*/
async function callTraceTool(tools, name, args = {}, context = OFFLINE_CONTEXT) {
const tool = tools.find((candidate) => candidate.schema.name === name);
if (!tool) {
const available = tools.map((candidate) => candidate.schema.name).join(', ');
throw new Error(`callTraceTool: no tool named '${name}'. Available: ${available}`);
}
const verdict = (0, toolArgsValidation_js_1.validateToolArgs)(args, tool.schema.inputSchema);
if (!verdict.ok)
return (0, toolArgsValidation_js_1.formatToolArgIssues)(name, verdict.issues);
return String(await tool.execute(args, context));
}
exports.callTraceTool = callTraceTool;
//# sourceMappingURL=traceToolpack.js.map