UNPKG

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
"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