UNPKG

agentfootprint

Version:

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

250 lines • 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.bindWriteOptions = exports.assertStoreHonours = exports.refuseUnhonouredWriteOptions = exports.requestedWriteOptions = exports.UnsupportedMemoryWriteOptionError = exports.NO_SERVER_EXTRACTION = exports.MEMORY_WRITE_OPTION_NAMES = void 0; /** Every write option, in the order refusals name them. */ exports.MEMORY_WRITE_OPTION_NAMES = [ 'extraction', 'namespaceScope', ]; /** * The reason a store with NO server-side extraction gives — the shared * sentence, so every such store refuses in the same words, remedy included. */ exports.NO_SERVER_EXTRACTION = 'it keeps exactly the entries it is given and derives no long-term records from ' + 'them — there is no extraction to skip and no derived namespace to scope (its ' + 'tenant boundary is the identity tuple). Leave the option off for this store, or ' + 'use a store that declares it in `writeOptions`'; /** * Thrown when a write asks for an option the store does not honour. * * Carries the store and the option by name; never a value — a namespace scope * names a tenant, and an error message is the one place a tenant id must not * travel. The remedy is in the store's own reason. */ class UnsupportedMemoryWriteOptionError extends Error { code = 'ERR_UNSUPPORTED_MEMORY_WRITE_OPTION'; /** The store that refused, e.g. `'InMemoryStore'`. */ store; /** The option it cannot honour. */ option; constructor(store, option, why) { super(`[memory] ${store} cannot honour the write option \`${option}\`: ${why}. It is ` + 'refused rather than ignored — a write that dropped it would look exactly like one ' + 'that honoured it.'); this.name = 'UnsupportedMemoryWriteOptionError'; this.store = store; this.option = option; } } exports.UnsupportedMemoryWriteOptionError = UnsupportedMemoryWriteOptionError; /** * The options this write actually asks for, its values checked. * * A field that is `undefined` is not asked for; an empty `namespaceScope` asks * for nothing either. A value of the wrong shape is a programming error, so it * throws a `TypeError` naming the option — and never echoes the value. */ function requestedWriteOptions(options) { if (options === undefined || options === null) return []; const asked = []; if (options.extraction !== undefined) { assertExtraction(options.extraction); asked.push('extraction'); } if (options.namespaceScope !== undefined) { assertScopeShape(options.namespaceScope); if (Object.keys(options.namespaceScope).length > 0) asked.push('namespaceScope'); } return asked; } exports.requestedWriteOptions = requestedWriteOptions; /** * Refuse, by name, every write option `options` asks for that `honoured` does * not contain. Call it first thing in `put`, `putMany` and `putIfVersion` — * before anything is written. * * @param store the store's name, as the refusal should say it * @param honoured the store's own declaration (`MemoryStore.writeOptions`) * @param options what this write asked for * @param why why this store cannot honour what it does not declare, and * what to do instead * @throws {UnsupportedMemoryWriteOptionError} for the first option not honoured */ function refuseUnhonouredWriteOptions(store, honoured, options, why) { for (const option of requestedWriteOptions(options)) { if (!(honoured ?? []).includes(option)) { throw new UnsupportedMemoryWriteOptionError(store, option, why); } } } exports.refuseUnhonouredWriteOptions = refuseUnhonouredWriteOptions; /** * Refuse a memory's write options or scope at DEFINITION time when its store * does not declare them, or when a fixed one is malformed — the first turn * should not be where a configuration that can never work is discovered. * * @throws {UnsupportedMemoryWriteOptionError} naming the store and the option * @throws {TypeError} for a malformed option */ function assertStoreHonours(store, config, caller) { const asked = []; if (config.extraction !== undefined) { assertExtraction(config.extraction); asked.push('extraction'); } if (config.namespaceScope !== undefined) { if (typeof config.namespaceScope !== 'function') { assertScopeShape(config.namespaceScope); assertNonEmptyScope(config.namespaceScope, caller); } asked.push('namespaceScope'); } for (const option of asked) { if (!(store.writeOptions ?? []).includes(option)) { throw new UnsupportedMemoryWriteOptionError(storeName(store), option, `it does not declare \`${option}\` in \`writeOptions\` (checked by ${caller}, before ` + 'any turn ran). A store that declares it can take it; otherwise leave the option off'); } } } exports.assertStoreHonours = assertStoreHonours; /** * The store with the memory's write options applied to every write it makes — * and its namespace scope applied to every read. * * Writes (`put`, `putMany`, `putIfVersion`) carry the options. A call's own * `extraction` overrides the bound one only where it asks for something (an * `undefined` field never erases it). A bound `namespaceScope` is NEVER * replaced per call: it comes from the run's identity through the configured * resolver and nowhere else, so a call that brings its own is refused. * * Reads follow the same rule as writes: `search` hands a store that declares * `'namespaceScope'` the scope resolved for that identity * (`SearchOptions.namespaceScope`), so a store that files derived records by it * reads within it. Every other member passes straight through. * * @throws (as a rejected promise, at a write or a search) a `TypeError` when * the scope function returns an empty or malformed scope — refused rather * than sent outside every tenant's namespace — or when a call brings its * own `namespaceScope` to a memory that binds one. */ function bindWriteOptions(store, config) { const scopeIsBound = config.namespaceScope !== undefined; const refuseOwnScope = (own) => { if (scopeIsBound && own !== undefined && Object.keys(own).length > 0) { throw new TypeError('defineMemory: this memory binds `namespaceScope` to the run’s identity, and a call ' + 'cannot replace it — a scope chosen per ' + 'call could file or read one tenant’s records under another’s. Change the resolver, ' + 'not the call.'); } }; const resolve = (identity, own) => { refuseOwnScope(own?.namespaceScope); const scope = boundScope(config, identity); const ownScope = own?.namespaceScope; return { ...(config.extraction !== undefined && { extraction: config.extraction }), ...(scope !== undefined && { namespaceScope: scope }), ...(own?.extraction !== undefined && { extraction: own.extraction }), // Only reachable when nothing is bound: the store checks it like any write. ...(ownScope !== undefined && Object.keys(ownScope).length > 0 && { namespaceScope: ownScope }), }; }; // Reads carry the scope only to a store that files by it — the same store // `assertStoreHonours` let the scope be bound to. const readsByScope = scopeIsBound && (store.writeOptions ?? []).includes('namespaceScope'); const readOptions = (identity, options) => { refuseOwnScope(options?.namespaceScope); if (!readsByScope) return options; const scope = boundScope(config, identity, 'this search'); return scope === undefined ? options : { ...options, namespaceScope: scope }; }; const bound = { ...(store.supportsVectorSearch !== undefined && { supportsVectorSearch: store.supportsVectorSearch, }), ...(store.ranksBy !== undefined && { ranksBy: store.ranksBy }), ...(store.writeOptions !== undefined && { writeOptions: store.writeOptions }), get: (identity, id) => store.get(identity, id), // `async`, so a refused scope is a rejected promise, as the port promises — // never a synchronous throw from a method the caller awaits. put: async (identity, entry, own) => store.put(identity, entry, resolve(identity, own)), putMany: async (identity, entries, own) => // The port's empty-batch law: no write, so nothing to resolve or refuse. entries.length === 0 ? store.putMany(identity, entries) : store.putMany(identity, entries, resolve(identity, own)), putIfVersion: async (identity, entry, expectedVersion, own) => store.putIfVersion(identity, entry, expectedVersion, resolve(identity, own)), list: (identity, options) => store.list(identity, options), delete: (identity, id) => store.delete(identity, id), seen: (identity, signature) => store.seen(identity, signature), recordSignature: (identity, signature) => store.recordSignature(identity, signature), feedback: (identity, id, usefulness) => store.feedback(identity, id, usefulness), getFeedback: (identity, id) => store.getFeedback(identity, id), forget: (identity) => store.forget(identity), // Present exactly when the store's is: `if (store.search)` is how callers // feature-detect it, so the bound store must not grow or lose one. ...(store.search !== undefined && { search: async (identity, query, options) => store.search(identity, query, readOptions(identity, options)), }), }; return bound; } exports.bindWriteOptions = bindWriteOptions; // ── Checks ───────────────────────────────────────────────────────────── /** The configured scope for this identity — present, plain, non-empty, or refused. */ function boundScope(config, identity, what = 'this write') { if (config.namespaceScope === undefined) return undefined; const scope = typeof config.namespaceScope === 'function' ? config.namespaceScope(identity) : config.namespaceScope; assertScopeShape(scope); // Copy FIRST, then check the copy: the values checked are the values every // store sees — whatever the resolver's object does afterwards, or does on a // second read (a getter, a Proxy that reports one value and returns another). const copy = Object.freeze({ ...scope }); assertScopeShape(copy); assertNonEmptyScope(copy, what); return copy; } function assertExtraction(value) { if (value !== 'skip') { throw new TypeError("[memory] the write option `extraction` takes one value, 'skip' — got " + `${typeof value === 'string' ? JSON.stringify(value) : typeof value}.`); } } function assertScopeShape(value) { if (!isStringRecord(value)) { throw new TypeError('[memory] the write option `namespaceScope` must be a plain object of string values ' + '(variable name → value). The value given is not quoted here: it names a tenant.'); } } function assertNonEmptyScope(scope, where) { if (Object.keys(scope).length === 0) { throw new TypeError(`[memory] the memory's \`namespaceScope\` came back EMPTY for ${where}. It is refused ` + 'rather than run without one: a write with no scope files its derived records outside ' + 'every tenant’s namespace, and a search with none is confined to no tenant. Return the ' + 'scope for every identity, or refuse the identity yourself.'); } } /** The store's own name for a refusal — its class, or what the caller holds. */ function storeName(store) { const ctor = store.constructor?.name; return ctor !== undefined && ctor !== 'Object' && ctor !== '' ? ctor : 'the store you passed'; } /** A PLAIN object (no class, no Map, no Promise) whose own values are all strings. */ function isStringRecord(value) { if (value === null || typeof value !== 'object' || Array.isArray(value)) return false; const proto = Object.getPrototypeOf(value); if (proto !== Object.prototype && proto !== null) return false; // Data properties only: a getter could answer the check one value and the // store another. return Object.values(Object.getOwnPropertyDescriptors(value)).every((d) => 'value' in d && typeof d.value === 'string'); } //# sourceMappingURL=writeOptions.js.map