agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
71 lines • 3.61 kB
JavaScript
/**
* middleware/ledger — INTERNAL. Where a chain's decisions become record.
*
* Pattern: one writer for one committed key.
* Role: core/ layer. `runChain` decides; this file is the only thing
* that commits what it decided and emits the matching event, so
* the three call sites cannot record the same fact three
* different ways.
* Emits: `agentfootprint.middleware.decision`, one per row.
*
* ## Why the original value is committed too
*
* A transform makes the trace and the wire disagree — the model saw one
* string, the run remembers another. Committing the pair, with the
* middleware's name and its `why` beside it, is what turns that
* disagreement from a lie into a record. Every slice taken afterwards can
* then find the moment the value changed and who changed it.
*
* The same reasoning covers the `'after-tool'` moment from the other side. A
* result the model was not allowed to read still HAPPENED — the tool ran, the
* side effect landed — so the refusal row carries the real result. A run that
* dropped it would record an agent that called a tool and got nothing back,
* which is not what occurred.
*
* This is not a redaction layer, and it is not trying to be one. For the
* `'input'` phase the seed stage commits the transformed message, so the
* model and the conversation never hold the original — but this row is NOT
* its only copy: the run's input as passed (`run.entry` payload), a crash
* checkpoint's `originalInput` and a paused
* run's checkpoint (`RunnerPauseOutcome.checkpoint`, which `standingAgent`
* stores for a paused session) keep it too — the full list, and what an app
* must redact, is in `./README.md`, the one place it is maintained. An app
* whose threat model says the original must not survive in the RECORD names
* it in `Agent.create({ redact })`: `middlewareDecisions` (this state key —
* every row's `before` / `after`; the `middleware.decision` events below carry
* neither) is then the placeholder in every record. The checkpoints keep the real
* values — a resume runs on them — so where the app stores a checkpoint, it
* protects it.
*/
import { typedEmit } from '../../../recorders/core/typedEmit.js';
/**
* Append decision rows to the run's ledger and emit one event per row.
*
* No-op on an empty list, so an agent with no middleware never writes the
* key at all and its commit log is the one it always had.
*/
export function recordDecisions(scope, rows) {
if (rows.length === 0)
return;
const prev = scope.middlewareDecisions ?? [];
// Spread into a plain local array first: a TypedScope array read is a live
// deep-proxy view, and both the commit and the event payloads below must be
// detached plain data (RFC-001 'clone' capture under deferred delivery).
scope.middlewareDecisions = [...prev, ...rows];
for (const row of rows) {
typedEmit(scope, 'agentfootprint.middleware.decision', {
middleware: row.middleware,
moment: row.moment,
at: row.at,
...(row.phase !== undefined && { phase: row.phase }),
...(row.toolName !== undefined && { toolName: row.toolName }),
...(row.toolCallId !== undefined && { toolCallId: row.toolCallId }),
iteration: row.iteration,
outcome: row.outcome,
changed: row.changed,
...(row.why !== undefined && { why: row.why }),
...(row.componentId !== undefined && { componentId: row.componentId }),
});
}
}
//# sourceMappingURL=ledger.js.map