UNPKG

agentfootprint

Version:

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

132 lines • 5.84 kB
/** * artifacts/capability — `ctx.artifacts`, the scope-bound door a tool gets. * * Shaped exactly like `ctx.credentials`: * • ALWAYS present — with no store attached every method is a teaching * refusal naming how to attach one, so a tool can never silently * optional-chain past a store that is not there; * • fail-closed — the refusal throws; `ctx.hasArtifacts` is the fact to * branch on for an intentional degraded mode; * • scope composed by the FRAMEWORK from the run's identity/session — the * capability closes over it, so a tool cannot name, widen or replace the * scope it resolves under. The five verbs here take no scope argument on * purpose: that argument was already answered by whoever built the * context. * * Every hop lands on the record AS IT HAPPENS: this module reports facts to a * neutral sink (a plain callback), and the tool-dispatch layer adapts that * sink onto the typed event channel. Neutral so this folder never imports the * event system or the agent — core wires artifacts, never the reverse. */ import { ArtifactIntegrityError, InvalidArtifactError, UnknownParentRefError, } from './types.js'; /** * Bind a store to one run's scope — the framework's move, made where the * scope is known and a tool cannot reach. */ export function bindArtifacts(store, scope, options = {}) { const { origin, onEvent } = options; const report = (fact) => { onEvent?.(fact); }; return { async put(input) { try { // `origin` is the framework's field on EVERY path: whatever a caller // managed to put there (it is not on the type, but a JavaScript caller // can still try) is dropped, and the binding stamps its own facts — or // nothing, when it has none to stamp. Keeping a caller's origin // whenever the binding had none would let an artifact join any run it // named, another person's included. const { origin: _callersOrigin, ...owned } = input; void _callersOrigin; const result = await store.put(scope, { ...owned, ...(origin !== undefined ? { origin } : {}), }); for (const swept of result.swept) report({ type: 'expired', swept }); report({ type: 'minted', meta: result.meta }); return result.meta; } catch (err) { if (err instanceof UnknownParentRefError) { report({ type: 'refused', op: 'put', reason: 'unknown-parent', detail: err.message }); } else if (err instanceof InvalidArtifactError) { report({ type: 'refused', op: 'put', reason: 'invalid-input', detail: err.message }); } throw err; } }, async head(ref) { const meta = await store.head(scope, ref); if (meta === null) { report({ type: 'refused', op: 'head', reason: 'missing-or-expired', ref }); return null; } report({ type: 'resolved', ref: meta.ref, via: 'head', kind: meta.kind, bytes: meta.bytes }); return meta; }, async get(ref) { let record; try { record = await store.get(scope, ref); } catch (err) { if (err instanceof ArtifactIntegrityError) { report({ type: 'refused', op: 'get', reason: 'digest-mismatch', ref, detail: err.message, }); } throw err; } if (record === null) { report({ type: 'refused', op: 'get', reason: 'missing-or-expired', ref }); return null; } report({ type: 'resolved', ref: record.meta.ref, via: 'get', kind: record.meta.kind, bytes: record.meta.bytes, }); return record; }, delete(ref) { return store.delete(scope, ref); }, list(options) { return store.list(scope, options); }, }; } /** * The fail-closed capability used when NO store is attached. Every verb * throws the same teaching refusal — loud, named, and on the record — so * `ctx.artifacts` is never `undefined` and a missing store can never read as * an empty one. Branch on `ctx.hasArtifacts` for an intentional no-store * mode. (The `unconfiguredCredentialProvider` law, verb for verb.) */ export function unconfiguredArtifacts(onEvent) { const refuse = (op, ref) => { onEvent?.({ type: 'refused', op, reason: 'no-store', ...(ref !== undefined && { ref }) }); return Promise.reject(new Error(`No artifact store is attached, but this tool called ctx.artifacts.${op}(...). ` + `Pass \`artifacts\` to Agent.create({ ..., artifacts }) — inMemoryArtifacts() for ` + `an in-process store, fileArtifacts({ directory }) or sqliteArtifacts({ file }) for ` + `one that survives a restart. A tool that intentionally supports running without a ` + `store should branch on \`ctx.hasArtifacts\` instead of catching this.`)); }; return { put: () => refuse('put'), head: (ref) => refuse('head', ref), get: (ref) => refuse('get', ref), delete: (ref) => refuse('delete', ref), list: () => refuse('list'), }; } //# sourceMappingURL=capability.js.map