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
JavaScript
;
/**
* `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