@credence/snapshots
v0.6.0
Published
Credence epistemic snapshots + semantic diff — what the agent knew, and what changed.
Downloads
576
Readme
@credence/snapshots
Epistemic snapshots + semantic diff. Freeze what the agent knew, didn't know, and was unsure about — typically right before a decision — then answer "what changed since?" with structure, not a text diff.
import { createSnapshot, diffSnapshots, renderDiff } from "@credence/snapshots";
const before = await createSnapshot(ledger, caseId, { trigger: "pre_decision" });
// ...the agent does more work...
const after = await createSnapshot(ledger, caseId, { trigger: "manual" });
const diff = await diffSnapshots(ledger, before.id, after.id);
console.log(renderDiff(diff));diff reports added, removed, valueDeltas, statusTransitions (with
clarified when confidence rose, e.g. reported → verified), and
resolvedUnknowns / newUnknowns / stillUnknown.
Two things the diff is careful about, because both were once wrong:
- Claims are compared by ref, not by a single representative per
(subject, key)group. Collapsing a group hid a second, contradicting claim and hid a withdrawal whenever a sibling survived — the diff answered "No epistemic change" while the ledger had moved. Value and status deltas are still reported, computed on the group's effective answer (its strongest live claim), which is a function of the whole group rather than of sort order. resolvedUnknownsmeans CLOSED, not necessarily resolved. A payload carries only open findings, so a finding that left the list may have been resolved (the condition went away) or dismissed (a person judged it, with a reason). The render says "Closed unknowns — resolved or dismissed"; readfinding.statusfrom the ledger when the difference matters.
A snapshot also records what was still owed (payload.obligations), so a
pre_decision snapshot answers "what did we know when we decided?" without
omitting what we had not yet been given.
It records whether the case was stopped for human review the same way
(payload.oversight): held, how many times it had ever been stopped, by whom and
why, the cited intervention point, the three-state autonomy report, and the
blocker reasons in force. A pre_decision snapshot that omitted this would prove
what was known and hide that the decision was taken while a human had the case
stopped.
The diff reports that too. oversight is an OversightDelta
(stopped / released / holdsPlaced / autonomyFrom → autonomyTo /
blockersAdded / blockersRemoved), and a stop or a release is never
"No epistemic change" — including a stop and a release inside the same window,
which holdsPlaced catches even though the case ends where it began. When either
snapshot predates oversight tracking the diff sets oversightIndeterminate and
says so in words, exactly as it does for relationsIndeterminate: an absent field
means not recorded, never "the case was not held". Reading absence as safety is
the single worst thing this payload could do.
What produced the state, and what it rested on
Two snapshots that differ leave a reader with one question the state alone cannot answer: did the world change, or did I change? Three fields exist for that.
const snap = await createSnapshot(ledger, caseId, {
trigger: "pre_decision",
meta: { promptHash, policyBundle: "v7", model: "claude-opus-4-8" },
host: async (caseId) => myPipeline.stateFor(caseId),
});payload.meta— provenance the host supplies: prompt hash, policy bundle version, model id, playbook revision. Opaque to Credence, but part ofpayloadHash, so swapping the model defeats dedupe instead of reading as identical state. Both sides absent is not indeterminate; one side absent is.payload.sourceRefs— every source backing live evidence, as refs, sorted and deduped. Source ids are content-addressed over(kind, uri, meta), so this is simultaneously the inventory and the fingerprint of the evidence base. Replacing the PDF under an unchanged claim shows up here even though the claim's own hash does not move. Credence always writes it now, so absence on either side dates the snapshot and is reported as indeterminate.payload.host— state the host froze alongside the ledger's: documents in a pipeline, tickets, workflow objects a knowledge ledger has no business modelling. Canonicalized intopayloadHashlike everything else.
Host state is compared only through a comparator you supply, because Credence has no idea what it means:
const diff = await diffSnapshots(ledger, before.id, after.id, {
hostDiff: (from, to) => ({ changed: from.stage !== to.stage, lines: [`stage: ${from.stage} → ${to.stage}`] }),
});With no comparator the diff reports hostIndeterminate rather than "unchanged" —
the same discipline as relations and oversight, for the same reason.
Asking without writing
createSnapshot is content-addressed and therefore harmless when nothing changed,
but it mints a row and emits snapshot.taken the first time anything has. A
staleness check should not leave a snapshot behind, so live state can be built
without one:
import { currentPayload, diffPayloads } from "@credence/snapshots";
const live = await currentPayload(ledger, caseId);
const diff = diffPayloads(pinnedSnapshot, { payload: live });diffPayloads takes anything carrying a payload (SnapshotState), so a stored
snapshot and live state compare the same way. This is what
assessDecision uses to
answer "is this decision still standing on what it was standing on?" without
turning the question into part of the record.
Snapshots are content-addressed by payload, so re-snapshotting an unchanged state is a no-op (dedupe for free).
