agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
130 lines • 6.7 kB
TypeScript
/**
* hosting/answerAccounts — the machinery behind the `answer-account` wire op:
* one host's cache, its single-flight, and the pure step that turns a stored
* recording into `{ account, shown }`.
*
* The op itself — its order, its refusals, its one door fact — lives inside
* `standingAgent.ts · answerArtifact`, beside `head` and `get`, so its
* ownership path cannot drift from theirs. This file owns only what that
* branch must not re-derive per request:
*
* • **The options, validated at boot.** `declarations` are the HOST's (a bad
* one throws when `standingAgent` is called, never per request), the
* ceiling and the cache size are numbers that must mean something.
* • **The cache key** — `(scope, ref, template-set version, declarations
* digest)`. The account is a pure function of the recording and the
* declarations, and the template-set version names the words, so a key
* that carries all four can never serve stale sentences: a library upgrade
* or a declarations change is a miss by construction.
* • **Single-flight** — ten concurrent clicks on one answer cause ONE store
* read and ONE parse. Joined waiters get the leader's outcome, failure
* included.
* • **An LRU bounded twice** — by entries (default 32) and by total
* serialized size ({@link ANSWER_ACCOUNT_CACHE_MAX_BYTES}). Only an
* account is ever cached; a failure is never an entry, so the next request
* tries again.
*
* What it deliberately does NOT own: whether the recording still exists. The
* branch asks the store (a SILENT head) before it asks this cache — so a
* swept or expired recording is "not available" even while its account sits
* here (review R3-S1). A cache that answered first would serve derived record
* data past the store's own retention.
*/
import type { ArtifactScope } from '../artifacts/types.js';
import type { AnswerAccountDeclarations } from '../answer-account/types.js';
import type { AnswerAccountWireBody } from './artifactWire.js';
/**
* Opt in to the `answer-account` wire op — `standingAgent({ answerAccounts })`.
*
* `true` opts in with every default.
*/
export interface AnswerAccountsOptions {
/**
* What the APP declares about its skills, tools and routing (labels, a
* result's `rowsAt`, "the app decides entries"). Validated once, when
* `standingAgent` is called; every line filled from it is vouched `app`.
* Never taken from a request.
*/
readonly declarations?: AnswerAccountDeclarations;
/**
* The largest recording this host will explain, in bytes (the ticket's
* `bytes`). Default {@link DEFAULT_ANSWER_ACCOUNT_MAX_RECORDING_BYTES}
* (16 MiB). A recording over it is refused with
* `RecordingTooLargeForAccountError` before any of its payload is read — it
* bounds the worst single parse on the event loop every session shares. A
* PACKED recording is held to it twice: its stored bytes, and the plain
* recording it stands for (measured over the packed form, before anything is
* expanded) — a few KB packed can stand for gigabytes.
*/
readonly maxRecordingBytes?: number;
/**
* How many accounts to keep, least recently used out first. Default
* {@link DEFAULT_ANSWER_ACCOUNT_CACHE_ENTRIES}. `0` caches nothing (the
* single-flight still joins concurrent requests).
*/
readonly cacheEntries?: number;
}
/** The default recording ceiling: 16 MiB — above the field median (≈7.5 MB). */
export declare const DEFAULT_ANSWER_ACCOUNT_MAX_RECORDING_BYTES: number;
/** The default number of cached accounts. */
export declare const DEFAULT_ANSWER_ACCOUNT_CACHE_ENTRIES = 32;
/** The cache's total bound, by the serialized size of what it holds: 8 MiB. */
export declare const ANSWER_ACCOUNT_CACHE_MAX_BYTES: number;
/** A stored recording as the branch loaded it: the payload and the run it is OF. */
export interface LoadedRecording {
readonly data: unknown;
/** The ticket's `meta.origin.runId` — preferred over the recording's own. */
readonly runId?: string;
}
/**
* One host's account service.
*
* @internal Built by `standingAgent` from {@link AnswerAccountsOptions}.
*/
export interface AnswerAccounts {
readonly maxRecordingBytes: number;
/** The cache key for a ref redeemed under `scope`. */
keyFor(scope: ArtifactScope, ref: string): string;
/** A cached answer, marked most recently used — or `undefined`. */
cached(key: string): AnswerAccountWireBody | undefined;
/**
* Compute (or join the computation of) the answer for `key`. `load` runs at
* most once per concurrent key and returns `null` when the record is not
* available. Resolves `null` for anything that cannot be explained, and
* rejects with `RecordingTooLargeForAccountError` when the payload in hand is
* over the ceiling whatever its ticket said — neither is ever cached.
*/
compute(key: string, load: () => Promise<LoadedRecording | null>): Promise<AnswerAccountWireBody | null>;
}
/**
* Build one host's service, validating the options. Throws — at boot — naming
* the first thing wrong.
*/
export declare function answerAccounts(options: AnswerAccountsOptions | true): AnswerAccounts;
/**
* The pure step: a stored recording → `{ account, shown }`, frozen — or `null`
* when the payload is not a recording this can read.
*
* Synchronous on purpose, and the reason for the ceiling: parse + fold +
* show-me run on the event loop. Measured on the field recording in the af-3
* worklog.
*
* The ceiling holds for a PACKED recording too, on the recording it stands for:
* the fold and show-me walk the expanded recording as a tree, and a few KB
* packed can stand for gigabytes (a value referred to ten times by values each
* referred to ten times, …). So a packed payload whose plain form is over
* `maxRecordingBytes` is refused exactly as that plain payload would be —
* measured over the packed form, before anything is expanded.
*
* @throws RecordingTooLargeForAccountError for a packed recording whose plain
* form is over `maxRecordingBytes`.
*/
export declare function explainRecording(loaded: LoadedRecording, declarations: AnswerAccountDeclarations, maxRecordingBytes?: number): AnswerAccountWireBody | null;
/**
* @internal Exported for its test.
*
* Freeze what the cache hands out. The same object is served to every reader
* of that answer; a host that edited one reply would otherwise edit the next.
*/
export declare function deepFreeze<T>(value: T, seen?: WeakSet<object>): T;
//# sourceMappingURL=answerAccounts.d.ts.map