UNPKG

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
"use strict"; /** * 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