UNPKG

agentfootprint

Version:

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

242 lines • 11.6 kB
"use strict"; /** * `recordingView` — ONE indexed pass over a recording, filtered to THIS run. * * Every reader of the account asks the view, never the raw `events` array, so * "which event is where" has one owner (and `failureCauses`, when it ships, * reuses it). * * Whose events: the rule `bridge/eventMeta.ts` · `eventBelongsToRun` owns (an * event that names a run belongs to that run; one that names none, to runs of * its session). The run is identified by the agentfootprint run id — NOT * `snapshot.runId`, which is footprintjs's executor id and a different id space * (filtering on it would drop every event). In order: * 1. `options.runId` — the stored artifact's `meta.origin.runId`; * 2. else the one run id on the recording's `agent.run_configured` events; * 3. with more than one (a shared-agent recording made before runs kept their * own events): the run that owns `turn_end` — never simply the first; * 4. with none of these, NOTHING is filtered and `scope` says `'unfiltered'`. * * Tolerance: an event that is not an object with a string `type` and an object * `payload` is passed over and counted in `unread`. Indices are the RECORDING's * indices, so a pointer lands on the event the reader can open. * * A REDACTED recording (a run under an agent's `redact`) holds a placeholder * where the policy selected a value: `[REDACTED]` for a payload field (or for a * whole payload, under `emitPatterns`), and `REDACTED` for a state key. Its * snapshot says it was served under a policy (`redaction/marker.ts`); in such * a record — and ONLY there; a tool can return the string `'REDACTED'` too — * a placeholder is NO value: a field or key that holds one reads as absent, * never as a result to judge or a pause to report — and it is NAMED * (`ViewEvent.redacted`, `RecordingView.stateKeptOut`), so a fact that would * otherwise say "not recorded" can say "kept out" (`keptOut`, * `RecordingView.isStateKeptOut`). An event whose whole payload is kept out * is still an event of the run: its type and meta are read, its payload is * empty. * * THE GUARD. A reader that touches a kept-out field without asking about it * would state a fact about a placeholder ("no tool ran" from a kept-out * `toolCallId`). So the view watches: every kept-out field or state key a * reader TOUCHES (reads, or tests with `in`) is noted, and so is every one it * ASKS about; `keptOutRead()` lists the touched ones never asked about, and * the account refuses to tell over them (`account.ts` · `notToldAccount`). * An unredacted recording is read through no guard at all. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.recordingView = exports.isKeptOut = exports.num = exports.str = exports.isRecord = exports.keptOut = void 0; const eventMeta_js_1 = require("../bridge/eventMeta.js"); const marker_js_1 = require("../redaction/marker.js"); /** * Whether the record keeps `field` of `event`'s payload out (see * `ViewEvent.redacted`) — and a reader that asks has handled it, so touching * the field afterwards is not a read of a placeholder (the view's guard). */ function keptOut(event, field) { const redacted = event?.redacted; if (event === undefined || redacted === undefined) return false; noteName(ASKED, event, field); return redacted === 'whole' || redacted.includes(field); } exports.keptOut = keptOut; /** Per event (or per state): the kept-out names a reader touched, and the ones it asked about. */ const TOUCHED = new WeakMap(); const ASKED = new WeakMap(); function noteName(map, owner, name) { const names = map.get(owner) ?? new Set(); names.add(name); map.set(owner, names); } /** `record` behind a guard that notes, on `owner`, every kept-out name a reader touches. */ function guarded(owner, record, isKept) { const touch = (key) => { if (typeof key === 'string' && isKept(key)) noteName(TOUCHED, owner, key); }; return new Proxy(record, { get: (target, key, receiver) => (touch(key), Reflect.get(target, key, receiver)), has: (target, key) => (touch(key), Reflect.has(target, key)), }); } /** The kept-out names `owner`'s readers touched and never asked about. */ function unasked(owner) { const asked = ASKED.get(owner); return [...(TOUCHED.get(owner) ?? [])].filter((name) => asked?.has(name) !== true); } /** An event as readers see it: a payload that keeps something out is read through the guard. */ function asViewEvent(event) { const redacted = event.redacted; if (redacted === undefined) return event; const isKept = redacted === 'whole' ? () => true : (name) => redacted.includes(name); const view = { ...event }; view.payload = guarded(view, event.payload, isKept); return view; } const PREFIX = 'agentfootprint.'; const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value); exports.isRecord = isRecord; const str = (value) => typeof value === 'string' ? value : undefined; exports.str = str; const num = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined; exports.num = num; /** * Whether `value` is a redaction placeholder — a value the record keeps out. * Asked only of a record SERVED under a policy (`redaction/marker.ts` · * `servedUnderPolicy`): anywhere else the same string is a value a tool or a * person really produced. */ exports.isKeptOut = marker_js_1.isPlaceholder; /** The names of `record`'s fields that hold a redaction placeholder. */ function placeholderFields(record) { return Object.keys(record).filter((key) => (0, exports.isKeptOut)(record[key])); } /** `record` without the fields that hold a redaction placeholder — the SAME * object when none does, so an unredacted recording is read as it always was. */ function withoutPlaceholders(record) { const masked = placeholderFields(record); if (masked.length === 0) return record; return Object.fromEntries(Object.entries(record).filter(([key]) => !masked.includes(key))); } /** * The run's events, narrowed. `served` — the record was served under a policy * (its snapshot carries the marker) — is the only case a placeholder is read * as kept out; otherwise every payload is read exactly as before. */ function narrow(events, served) { const raw = []; let unread = 0; if (!Array.isArray(events)) return { raw, unread }; events.forEach((event, index) => { if (!(0, exports.isRecord)(event) || typeof event.type !== 'string') { unread += 1; return; } const meta = (0, exports.isRecord)(event.meta) ? event.meta : {}; if ((0, exports.isRecord)(event.payload)) { const redacted = served ? placeholderFields(event.payload) : []; raw.push({ index, type: event.type, payload: redacted.length > 0 ? withoutPlaceholders(event.payload) : event.payload, meta, ...(redacted.length > 0 && { redacted }), }); } else if (served && (0, exports.isKeptOut)(event.payload)) { // The whole payload is kept out (`emitPatterns`): still an event of the run. raw.push({ index, type: event.type, payload: {}, meta, redacted: 'whole' }); } else { unread += 1; } }); return { raw, unread }; } const CONFIGURED = `${PREFIX}agent.run_configured`; const TURN_END = `${PREFIX}agent.turn_end`; /** Pick the run id by the documented order; `undefined` = none is known. */ function chooseRunId(raw, given) { if (given !== undefined && given.length > 0) return given; const configured = [ ...new Set(raw .filter((e) => e.type === CONFIGURED) .map((e) => (0, exports.str)(e.meta.runId)) .filter((id) => id !== undefined)), ]; if (configured.length === 1) return configured[0]; if (configured.length === 0) return undefined; const owner = (0, exports.str)(raw.find((e) => e.type === TURN_END)?.meta.runId); return owner !== undefined && configured.includes(owner) ? owner : undefined; } function recordingView(recording, options = {}) { const snapshot = (0, exports.isRecord)(recording.snapshot) ? recording.snapshot : undefined; // A placeholder is KEPT OUT only in a record a policy covered — the snapshot says so. const served = (0, marker_js_1.servedUnderPolicy)(snapshot); const { raw, unread } = narrow(recording.events, served); const runId = chooseRunId(raw, options.runId); const sessionId = runId === undefined ? undefined : (0, exports.str)(raw.find((e) => e.type === CONFIGURED && e.meta.runId === runId)?.meta.sessionId); const narrowed = runId === undefined ? raw : raw.filter((e) => (0, eventMeta_js_1.eventBelongsToRun)({ ...((0, exports.str)(e.meta.runId) !== undefined && { runId: (0, exports.str)(e.meta.runId) }), ...((0, exports.str)(e.meta.sessionId) !== undefined && { sessionId: (0, exports.str)(e.meta.sessionId) }), }, { runId, ...(sessionId !== undefined && { sessionId }) })); const own = narrowed.map(asViewEvent); const byType = new Map(); for (const event of own) { const list = byType.get(event.type) ?? []; list.push(event); byType.set(event.type, list); } const full = (type) => (type.startsWith(PREFIX) ? type : `${PREFIX}${type}`); const ofType = (type) => byType.get(full(type)) ?? []; const recorded = snapshot && (0, exports.isRecord)(snapshot.sharedState) ? snapshot.sharedState : undefined; const stateKeptOut = recorded !== undefined && served ? placeholderFields(recorded) : []; // The state's guard is noted on this object (`TOUCHED` / `ASKED`), as an event's is on the event. const stateOwner = {}; const stripped = recorded !== undefined && stateKeptOut.length > 0 ? withoutPlaceholders(recorded) : recorded; const state = stripped !== undefined && stateKeptOut.length > 0 ? guarded(stateOwner, stripped, (key) => stateKeptOut.includes(key)) : stripped; const initial = snapshot && (0, exports.isRecord)(snapshot.initialState) ? snapshot.initialState : undefined; const pausedWith = initial !== undefined && Array.isArray(initial.history) ? initial : undefined; return { events: own, ...(runId !== undefined && { runId }), ...(sessionId !== undefined && { sessionId }), scope: runId === undefined ? 'unfiltered' : 'own-run', foreign: raw.length - own.length, unread, servedUnderPolicy: served, ...(state !== undefined && { state }), stateKeptOut, isStateKeptOut: (key) => { noteName(ASKED, stateOwner, key); return stateKeptOut.includes(key); }, ...(recorded !== undefined && { stateAsRecorded: recorded }), ...(pausedWith !== undefined && { pausedWith }), ofType, keptOutRead: () => [ ...new Set([ ...own.flatMap((e) => unasked(e).map((name) => `${e.type} · ${name}`)), ...unasked(stateOwner).map((key) => `state · ${key}`), ]), ].sort(), first: (type) => ofType(type)[0], last: (type) => { const list = ofType(type); return list[list.length - 1]; }, }; } exports.recordingView = recordingView; //# sourceMappingURL=view.js.map