agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
117 lines (116 loc) • 6.33 kB
TypeScript
/**
* exportBugReport / describeBugReport — a bug report IS the evidence.
*
* The usual bug report is a person's memory of a run: "it said the wrong
* thing, I think it called the search tool twice". The run itself — the
* timeline, the state, the chart, the narrative — is sitting right there in
* the process and never leaves it. This turns that around: the report is the
* run, packaged, with the prose attached.
*
* ## Two calls, because consent needs two
*
* 1. {@link describeBugReport} — "here is what would be sent." A manifest of
* SELECTABLE UNITS: each conversation with its event and turn counts and
* its size, each derived file, the redacted keys by name, the total, and
* a loud warning with trim hints if it is too big.
* 2. {@link exportBugReport} — "send exactly these." The reporter's ticked
* unit ids come back as `include`, and the bundle carries only those.
*
* One call does work for a server-side reporter that has no human in front of
* it; the two-call shape is what makes a browser consent dialog possible at
* all, because a dialog cannot ask about a blob it has not measured.
*
* ## What is in the bundle (layout 2 — `manifest.manifestVersion`)
*
* | file | what it is |
* |---|---|
* | `manifest.json` | this manifest — always present, never a selectable unit |
* | `envelope.json` | the run as a `RecordingEnvelope`: the archive contract, with the canon `{ snapshot, events, structure }` under `recording` |
* | `recording.json` | the bare recording — ONLY when the envelope's run facts were not available, with the manifest naming the missing one |
* | `conversations/<id>.json` | one file per conversation, when there is more than one run |
* | `conversation.json` | the readable transcript, derived from the events |
* | `narrative.txt` | the narrative recorder's lines, when one was attached |
* | `environment.json` | the HOST that ran it + the reporter's prose |
*
* The evidence file is named for what it is, and there is never more than one
* of it: a bundle carrying both an envelope and a copy of the recording it
* already contains would double the biggest file in the archive — and the
* archive is store-only, so doubling is real bytes, against a ceiling a
* reporter has to fit under.
*
* `environment.json` is the host and NOTHING that identifies a machine: Node
* version, platform, architecture. **No username, no hostname, no working
* directory, no environment variables, no file paths.** A bug report should not
* be the way an internal directory layout leaves a company. The producer
* versions it used to repeat now live where the archive contract stamps them,
* in `envelope.json`'s `producer` — one fact, one place. (The manifest's own
* `environment` block still prints both for the human reading the issue; it is
* a summary, not a second archive.)
*
* ## Redaction is already done, and the manifest proves it
*
* The recording arrives ALREADY redacted when its run had a policy (an
* agent's `Agent.create({ redact })`): footprintjs scrubs state at commit
* time, the snapshot a recording carries is the redacted view
* (`getLastSnapshot()`), and every event was served under the same rule
* before anything recorded it — so a redacted value was never in what this
* reads. Nothing here scrubs anything — it would be too late to matter and a
* second policy could only disagree with the first. What this
* does do is LIST the redacted keys by name, derived from the placeholders
* actually present in the evidence, so a human consenting to the bundle can
* see which secrets were protected. A key that is not on that list was not
* redacted, and the honest reading of an empty list is "no policy was set" —
* which the manifest says in a note.
*
* @example The consent flow
* ```ts
* const manifest = describeBugReport(recording);
* // …show manifest.units to the human; they tick some…
* const report = exportBugReport(recording, {
* include: ['conv-1', 'file-narrative', 'file-environment'],
* title: 'Agent answered with a stale price',
* stepsToReproduce: '1. ask for the price\n2. update it\n3. ask again',
* expected: 'the new price',
* actual: 'the old one',
* });
* fs.writeFileSync(report.filename, report.zip);
* ```
*/
import type { BugReport, BugReportInput, BugReportManifest, DescribeBugReportOptions, ExportBugReportOptions } from './types.js';
/**
* A size a human reads. KB under a megabyte, MB above it — a ceiling reported
* as "0.0 MB" teaches nothing, and these strings are the whole content of a
* refusal.
*/
export declare function formatBytes(bytes: number): string;
/**
* Measure a bug report before anything leaves — the consent step.
*
* Every unit is "selected" in the manifest this returns, because nothing has
* been chosen yet: it is the offer, with sizes and counts attached, for a human
* (or a policy) to narrow. Show `units` to the reporter; pass the ids they keep
* to {@link exportBugReport} as `include`.
*
* Cheap enough to call on every dialog open: it serializes the files to measure
* them, and throws them away.
*
* @param input a recording, a `recordRun` handle, a runner, or an array.
* @param options size ceiling and a fixed timestamp.
*/
export declare function describeBugReport(input: BugReportInput, options?: DescribeBugReportOptions): BugReportManifest;
/**
* Build the bundle: the manifest, the named files, and a real zip of them.
*
* @param input a recording, a `recordRun` handle, a runner, or an array.
* @param options the reporter's prose, plus `include` — the ids from
* {@link describeBugReport} that the reporter consented to.
*
* @throws TypeError naming the unknown id when `include` names a unit that
* does not exist, and naming the available conversations when the
* selection would carry no evidence at all.
*/
export declare function exportBugReport(input: BugReportInput, options: ExportBugReportOptions): BugReport;
/** `2026-08-11-agent-answered-with-a-stale-price.zip`. */
export declare function bundleFilename(title: string, createdAt: Date): string;
/** Lower-case, ASCII, hyphenated, bounded — a filename, not a sentence. */
export declare function slugify(title: string): string;