agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
165 lines • 7.23 kB
JavaScript
;
/**
* artifacts/inMemoryArtifacts — the claim-check store in a Map.
*
* For tests, local development, and single-process agents whose artifacts may
* die with the process. Exactly as durable as the process — which is the
* point of the port: swap the one argument for `fileArtifacts` or
* `sqliteArtifacts` and nothing else changes.
*
* ── Bounded, and counting its drops (the innerRunRecords laws) ─────────────
* A retained payload is retained memory, and an agent minting a dataset per
* turn for a week would otherwise pin every one of them — the failure would
* look like a leak rather than like a feature. So this adapter is ALWAYS
* bounded: it has a default byte budget and row budget per scope, an explicit
* retention only replaces those dials, and there is deliberately no spelling
* for "unbounded" here — a memory store without a ceiling is a leak wearing
* configuration. When the budget forces an eviction the least-recently-USED
* artifact leaves first (reading refreshes recency, so the dataset a chart is
* actively rendered from is not the next casualty), every eviction is
* reported by the `put` that caused it, and `dropped` COUNTS them — "we kept
* the last 200 of 340" is an answer a debugging session can act on; a
* silently missing ref is not.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.inMemoryArtifacts = exports.DEFAULT_IN_MEMORY_ARTIFACT_RETENTION = void 0;
const index_js_1 = require("../memory/identity/index.js");
const minting_js_1 = require("./minting.js");
const naming_js_1 = require("./naming.js");
const payload_js_1 = require("./payload.js");
const retention_js_1 = require("./retention.js");
const types_js_1 = require("./types.js");
/** The always-on bounds. Override dials via `retention`; there is no "off". */
exports.DEFAULT_IN_MEMORY_ARTIFACT_RETENTION = {
/** 32 MiB per scope — a working set, not an archive. */
maxBytesPerScope: 32 * 1024 * 1024,
/** 256 artifacts per scope. */
maxCountPerScope: 256,
};
/**
* A bounded, drop-counting, per-scope-isolated artifact store in process
* memory.
*
* @example
* const store = inMemoryArtifacts({ retention: { ttlMs: 15 * 60_000 } });
* const agent = Agent.create({ provider, artifacts: store });
*/
function inMemoryArtifacts(options = {}) {
(0, retention_js_1.assertRetention)('inMemoryArtifacts', options.retention);
const retention = {
...exports.DEFAULT_IN_MEMORY_ARTIFACT_RETENTION,
...options.retention,
};
const now = options._now ?? Date.now;
/** namespace → (ref → row). Insertion order is NOT recency; rows carry it. */
const scopes = new Map();
let dropped = 0;
const shelf = (scope) => {
const ns = (0, index_js_1.identityNamespace)(scope);
let held = scopes.get(ns);
if (held === undefined) {
held = new Map();
scopes.set(ns, held);
}
return held;
};
/** Resolve a live row, sweeping it when the calendar says it is gone. */
const liveRow = (scope, ref) => {
if (!(0, naming_js_1.isArtifactRef)(ref))
return undefined;
const held = shelf(scope);
const row = held.get(ref);
if (row === undefined)
return undefined;
if (row.meta.expiresAt !== undefined && row.meta.expiresAt <= now()) {
held.delete(ref);
return undefined;
}
return row;
};
return {
get dropped() {
return dropped;
},
retention,
async put(scope, input) {
const held = shelf(scope);
const at = now();
const { meta } = await (0, minting_js_1.prepareArtifact)(input, (parent) => liveRow(scope, parent) !== undefined, retention, at);
const rows = [...held.values()].map((row) => ({
ref: row.meta.ref,
kind: row.meta.kind,
bytes: row.meta.bytes,
...(row.meta.expiresAt !== undefined && { expiresAt: row.meta.expiresAt }),
lastAccessedAt: row.lastAccessedAt,
}));
const plan = (0, retention_js_1.planRetention)(rows, meta.bytes, retention, at);
if (plan.refusal !== undefined)
throw new types_js_1.InvalidArtifactError(plan.refusal);
for (const swept of plan.swept) {
held.delete(swept.ref);
if (swept.reason !== 'ttl')
dropped++;
}
held.set(meta.ref, { meta, payload: (0, payload_js_1.encodeArtifactData)(input.data), lastAccessedAt: at });
return { meta, swept: plan.swept };
},
// eslint-disable-next-line @typescript-eslint/require-await
async head(scope, ref) {
const row = liveRow(scope, ref);
if (row === undefined)
return null;
row.lastAccessedAt = now();
return row.meta;
},
async get(scope, ref) {
const row = liveRow(scope, ref);
if (row === undefined)
return null;
row.lastAccessedAt = now();
const data = (0, payload_js_1.decodeArtifactData)(row.payload);
if (row.meta.digest !== undefined) {
const actual = await (0, payload_js_1.computeArtifactDigest)(data);
if (actual !== row.meta.digest) {
throw new types_js_1.ArtifactIntegrityError(row.meta.ref, row.meta.digest, actual);
}
}
return { meta: row.meta, data };
},
// eslint-disable-next-line @typescript-eslint/require-await
async delete(scope, ref) {
if (!(0, naming_js_1.isArtifactRef)(ref))
return;
shelf(scope).delete(ref);
},
// eslint-disable-next-line @typescript-eslint/require-await
async list(scope, listOptions) {
const at = now();
const held = shelf(scope);
// Sweep-on-access: an expired row is "no data" everywhere, lists included.
for (const [ref, row] of held) {
if (row.meta.expiresAt !== undefined && row.meta.expiresAt <= at)
held.delete(ref);
}
const all = [...held.values()]
.map((row) => row.meta)
.sort((a, b) => b.createdAt - a.createdAt || (a.ref < b.ref ? -1 : 1));
const offset = decodeCursor(listOptions?.cursor);
const limit = Math.max(1, Math.floor(listOptions?.limit ?? 50));
const page = all.slice(offset, offset + limit);
const nextOffset = offset + page.length;
return {
artifacts: page,
...(nextOffset < all.length && { cursor: String(nextOffset) }),
};
},
};
}
exports.inMemoryArtifacts = inMemoryArtifacts;
function decodeCursor(cursor) {
if (cursor === undefined)
return 0;
const parsed = Number(cursor);
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : 0;
}
//# sourceMappingURL=inMemoryArtifacts.js.map