UNPKG

agentfootprint

Version:

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

199 lines • 9.6 kB
/** * callerIdentity — WHO a resumed run is for, read back from the run it resumes. * * Seed (`stages/seed.ts · seedFrom`) writes the run's memory namespace on one of * three rungs: the caller's EXPLICIT identity; `{ conversationId: sessionId }` * for a session-bound run that named none (recorded as * `runIdentitySource: 'session'`); or the per-run default * `{ conversationId: '<runId>' }`. Only the first rung is a caller's identity — * the other two are derivations this library made, and `Agent.lastRunIdentity` * (what `checkpoint()`, `EventMeta.principal` and a tool's `ctx.identity` read) * must stay absent for them, so "absent" keeps meaning "nobody named one". * * `Agent.resume` does not re-seed: the resumed run keeps the paused run's * `runIdentity`, restored from the flowchart checkpoint. So the checkpoint is * the one carrier of "who the paused run was for" that survives a shared * instance, a pooled instance rebuilt after eviction, and a restart. This * module is the inverse of seed's rungs over that carrier. * * THE LAW: never drop an identity the caller named, never promote a derived * one. The session rung is recorded, so it is certain. The per-run default is * NOT recorded (writing a marker would change the committed keys of every * run that names nobody), and a flowchart checkpoint does not carry the paused * run's id — so it is recognised by the strongest evidence available, in order: * * 1. **The caller named exactly this identity on this instance** (`named`, the * instance's last caller identity) → it is the caller's. This is what keeps * a single-user app's `{ conversationId: 'run-1-1' }` across a bare resume. * 2. **This instance minted that conversation id as a run id** (`minted`) → * derived, for certain. * 3. **The checkpoint's own receipt names that id as its run** (the paused run's * `receipt.basis.runId`, present once it called a model) → derived, for * certain. * 4. **LAST RESORT — the SHAPE `makeRunId` mints** (`isMintedRunId`), for a * checkpoint from another instance with no receipt to match → read as * derived. The one stated edge: a caller who names exactly * `{ conversationId: 'run-<digits>-<digits>' }` and resumes it BARE loses * it whenever step 1 cannot see it — on another instance, or on the same * instance once anyone else has run in between — and no receipt matches. * The fail-closed direction: a derived namespace is never published as a * person. Pass `identity` on `resume` to keep it. * * A checkpoint names who it is for; it is not proof. A host that lets * checkpoints leave its trust boundary signs them or keeps them server-side — * see `Agent.resume`. */ import { isMintedRunId } from '../RunnerBase.js'; /** * The identity the CALLER of the run whose state this is named, as a COPY, or * undefined when that run's identity was derived (session rung, per-run * default), is malformed, or the state carries none. * * @example * ```ts * // Agent.resume — a resuming call that names a DIFFERENT identity is refused: * const named = callerIdentityOf(checkpoint.sharedState, { named: last, minted }); * ``` */ export function callerIdentityOf(state, evidence = {}) { if (state === null || typeof state !== 'object') return undefined; const seeded = state; if (seeded.runIdentitySource !== undefined) return undefined; const identity = wellFormed(seeded.runIdentity); if (identity === undefined) return undefined; if (evidence.named !== undefined && sameIdentity(evidence.named, identity)) return identity; if (conversationOnly(identity)) { const id = identity.conversationId; if (evidence.minted?.has(id) === true) return undefined; if (readReceiptRunId(seeded) === id) return undefined; if (isMintedRunId(id)) return undefined; } return identity; } /** * The identity the paused run's memory namespace and credentials are restored * from — `scope.runIdentity` as the checkpoint carries it, whatever its source — * as a validated copy; `undefined` when absent; `'malformed'` when the * checkpoint's own fields disagree: a value that is not an identity, or a * session-rung marker (`runIdentitySource: 'session'`) on anything but exactly * `{ conversationId }` — the only shape seed's session rung writes. */ export function restoredIdentityOf(state) { if (state === null || typeof state !== 'object') return undefined; const seeded = state; if (seeded.runIdentity === undefined) return undefined; const identity = wellFormed(seeded.runIdentity); if (identity === undefined) return 'malformed'; if (seeded.runIdentitySource !== undefined) { const derived = seeded.runIdentitySource === 'session' || seeded.runIdentitySource === 'composition'; if (!derived || !conversationOnly(identity)) return 'malformed'; } return identity; } /** * The session the paused run ran under, as its own state records it: the * `runSessionId` seed writes on every session-bound run, else — for a * checkpoint written before that key existed — the session rung's * `runIdentity.conversationId`. `undefined` when neither says. */ export function pausedSessionOf(state) { if (state === null || typeof state !== 'object') return undefined; const seeded = state; if (typeof seeded.runSessionId === 'string') return seeded.runSessionId; // `null` is this release's record of "a caller-named run with NO session". if (seeded.runSessionId === null) return undefined; const identity = wellFormed(seeded.runIdentity); if (identity === undefined) return undefined; if (seeded.runIdentitySource === 'session') { return conversationOnly(identity) ? identity.conversationId : undefined; } // No marker at all on a caller-named identity: a checkpoint written BEFORE // `runSessionId` existed (recheck NIT 3). Whether that run had a session is // not recorded, and filing its resumed turn as sessionless would hand it to // the next sessionless caller — so the session is recovered from the // identity's `conversationId`, which is what `standingAgent` composed it from // (`identityForRequest`). A run with no identity (the per-run default) had no // session to recover. const perRunDefault = conversationOnly(identity) && isMintedRunId(identity.conversationId); return perRunDefault ? undefined : identity.conversationId; } /** * Refuse, when a run BEGINS, an identity that is not one: not an object, a * field that is present and not a string, or no field at all. TypeScript * already forbids these; a JavaScript caller can still pass them, and without * this door the run would start, pause, and then every resume of it — bare ones * included — would refuse as a malformed checkpoint (recheck NIT 2). The * refusal lands where the mistake was made. * * @throws TypeError naming the door and the field. */ export function assertIdentityShape(identity, door) { if (identity === undefined) return; if (identity === null || typeof identity !== 'object' || Array.isArray(identity)) { throw new TypeError(`${door}: identity must be an object like { conversationId, principal?, tenant? }.`); } const raw = identity; for (const key of ['conversationId', 'tenant', 'principal']) { if (raw[key] !== undefined && typeof raw[key] !== 'string') { throw new TypeError(`${door}: identity.${key} must be a string when present (got ${typeof raw[key]}).`); } } if (raw.conversationId === undefined && raw.tenant === undefined && raw.principal === undefined) { throw new TypeError(`${door}: identity names nothing — pass { conversationId, principal?, tenant? }, or omit identity.`); } } /** Do two identities name the same tenant, principal and conversation? */ export function sameIdentity(a, b) { return (a.conversationId === b.conversationId && a.principal === b.principal && a.tenant === b.tenant); } /** A fresh `MemoryIdentity` from an untrusted value — strings only, or nothing. */ function wellFormed(value) { if (value === null || typeof value !== 'object') return undefined; const raw = value; // Every field a string or absent (a JS caller can omit `conversationId`; // what they named is still theirs), and at least one present. for (const key of ['conversationId', 'tenant', 'principal']) { if (raw[key] !== undefined && typeof raw[key] !== 'string') return undefined; } if (raw.conversationId === undefined && raw.tenant === undefined && raw.principal === undefined) { return undefined; } return { ...(typeof raw.conversationId === 'string' && { conversationId: raw.conversationId }), ...(typeof raw.tenant === 'string' && { tenant: raw.tenant }), ...(typeof raw.principal === 'string' && { principal: raw.principal }), }; } /** Seed's third rung has exactly one field, a string conversation id. */ function conversationOnly(identity) { return (typeof identity.conversationId === 'string' && identity.tenant === undefined && identity.principal === undefined); } function readReceiptRunId(state) { try { return state.receipt?.basis?.runId; } catch { return undefined; } } //# sourceMappingURL=callerIdentity.js.map