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