UNPKG

agentfootprint

Version:

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

236 lines (235 loc) • 13.6 kB
/** * emptiness — the ONE reader of "what came back?" for a finished tool result. * * Pattern: typed routes over two inputs — the value the MODEL read, and the * door the RECORD says the call returned — and never a guess. * Role: core/ layer, pure. It imports the recognizers only * (`semantics/coverage/recognize.ts`; the artifact ref grammar and the * placement ticket's guard, `artifacts/naming.ts`, * `artifacts/placement.ts`), never a mint, so a post-hoc reader * loads it without the tool-name checks and refusal sentences. * Callers: the `/observe` answer account (`answer-account/facts/calls.ts` * for this run's calls, `answer-account/facts/inView.ts` for an * earlier answer's result) and the standing fold * (`core/agent/assessment/assess.ts` · `assessAnswer`). One rule, so * the person's account and the answer's standing read the same bytes * the same way; they differ only where they hold a different door * (the list under "The door decides" below). * Emits: N/A. * * ## The routes, in order * * | What the record says came back | Reading | Rows counted by | * |---|---|---| * | an absence — bare, inside a `coverage()`, or the delivered status `'absent'` | `declared-absent` | — | * | a `describedResult()` with data | `non-empty`, with a count per kind | library | * | a `describedResult()` with only `clarify` | `clarify` — a question handed back, no data | — | * | a `coverage()` envelope | its wrapped `result`, read by these same routes, marked `bounded`; an empty wrapped rowset reads `declared-absent` | library or app | * | a bare top-level array | `undeclared-empty` or `non-empty` | library | * | the library's placement ticket (`placedToolResult`) — the whole result is in the store | `unknown`, said: the ticket counts bytes, never rows | — | * | an object whose key the app declared in `rowsAt` | `undeclared-empty` or `non-empty` | app | * | an object whose `rowsAt` rows went to the store — the dataset ticket left in their place | the ticket's whole-number `rows`: `undeclared-empty` (0) or `non-empty`; no count → `unknown`, said | app | * | an object with no list and no ticket at the declared `rowsAt` | `unknown`, said: the declared key holds no list | — | * | anything else | `unknown` — the record cannot read it | — | * * ## Rows that travel by reference * * A projection may move a rowset out of the value the model reads and leave a * TICKET to the artifact store in its place — `withDatasetArtifacts` / * `stageDatasetArtifacts` stage the rows, and a projection (or an after-tool * `allow(replacement)`) keeps the ticket and drops the rows. The library's * spelling of that ticket, the one this reader counts: * * ```ts * { …, datasets: { rows: { ref: 'art_…', kind: 'dataset/rows', rows: 2, sourceField: 'rows' } }, * dataset: { ref: 'art_…', kind: 'dataset/rows', rows: 2, sourceField: 'rows' } } // the principal one * ``` * * `ref` is a store ref (`isArtifactRef`), `rows` the whole-number row count, * `sourceField` the result key the rows came from. The ticket is read only for * the key the APP declared (`rowsAt`), only when that key is gone from the * value (moved, not merely beside it), and only when the ticket names it — * keyed under `datasets[rowsAt]` (a `sourceField`, when present, agreeing) or * the principal `dataset` with `sourceField: rowsAt`. Two declarations meet * there: the app's key and the ticket's field. A ticket with no whole-number * `rows` is never guessed: whether the result was empty cannot be told, and * the reading says why (`rowsUnread`). The library's own placement ticket * (`artifacts/placement.ts`) puts the WHOLE result in the store and counts * bytes, so it reads the same way. Take 4 of the demo video: a ticketed * `pscale_client_health` (2 rows, `sourceField: 'rows'`, the app's `rowsAt: * 'rows'`) read as "its shape is not declared", and the account's tone stayed * unknown. * * ## The door decides, when the record holds one * * A marker in the bytes is not a declaration: an envelope a tool returned as * JSON TEXT (an `mcpClient` in its default text mode) reaches the model and * the history byte-for-byte like a recognized one, but the run never * recognized it — no delivered status, no coverage row, no limits block. So * when the caller holds the call's door (`EmptinessContext.door` — this run's * calls, read from the coverage rows or events), the door alone says whether * an absence or a boundary was declared, and a marker the door does not vouch * for is read as plain data. Only when the caller holds NO door for the result * is the door read off the value itself, by the rule the run's own recognizer * applies (`declaredByValue`, mirroring `semantics/coverage/read.ts` · * `readCoverageResult`) — the only evidence left, and the same answer the run * would have filed for an object. Which caller holds a door: * * - the answer account, for this run's calls — its EVENTS are the door * (`answer-account/facts/calls.ts`), and a call's `tool_end` is always * in the record, so a JSON-text envelope the run never recognized reads as * data in "It found"; * - the standing fold, only for a call that has a committed coverage row * (`core/agent/assessment/assess.ts` · `readTurnResults`). Committed state * can LOSE a row the run filed (a history restored by `resumeOnError` does * not carry `coverageDeclared`; a trimmed recording drops it), so for a call * with no row the fold passes no door and the envelope in the committed * history is read — never read as silence. For a JSON-text envelope the run * never recognized (an `mcpClient` in text mode) the fold then says more * than "It found" does: it may over-report, it never hides; * - nobody, for an earlier answer's result (`facts/inView.ts`). * * Recognizing JSON-text envelopes at run time belongs at the execute boundary, * with one owner, not in a reader. */ /** * Whether the result the model read came back empty, and who says so. * * - `declared-absent` — the tool declared that nothing matched (an absence, or * an empty rowset inside a declared boundary); * - `undeclared-empty` — an empty rowset that did not say what it searched; * - `non-empty` — rows, or described data, came back; * - `clarify` — a described result handed back a question and no data; * - `unknown` — the record cannot read it (a shape nobody declared, a * decorated or non-JSON string, an error text). */ export type Emptiness = 'declared-absent' | 'undeclared-empty' | 'non-empty' | 'clarify' | 'unknown'; /** A described result's data, counted per kind by the library from the recorded envelope. */ export interface DescribedCounts { readonly facts?: number; readonly series?: number; readonly edges?: number; } /** What the RECORD says one call returned. */ export interface ReturnedDoor { /** The call returned an absence — bare, inside a `coverage()`, or with the delivered status `'absent'`. */ readonly absent: boolean; /** The call declared a `coverage()` boundary (a `ledger` coverage row / a `tools.coverage_declared` event). */ readonly bounded: boolean; /** * The `describedResult()` envelope as the record keeps it — * the `tools.semantics_declared` event's `semantics`. The model read only its * projection, which carries no marker, so without this the reader cannot * tell a described result from any other object. Committed state does not * keep it (the projection replaces it at the dispatch door), so the standing * fold never has it. */ readonly described?: unknown; } export interface EmptinessContext { /** Where the APP says an object result of this tool keeps its rows — a top-level key. */ readonly rowsAt?: string; /** * What the record says the call returned. ABSENT when the record holds no * door for this result — an earlier answer's result, whose run is not in * this record, or (for the standing fold) a call with no committed coverage * row — and the strict recognizers then read the value itself * (`declaredByValue`). */ readonly door?: ReturnedDoor; /** * For a reader of a record SERVED under a redaction policy (an agent's * `redact`): whether a value is the policy's placeholder. The app's rows key * holding one reads as kept out (`rowsUnread: 'redacted'`), never as "no * list". Absent — the live run, or a record no policy covered — every * value is read as the value it is. */ readonly keptOut?: (value: unknown) => boolean; } export interface EmptinessReading { readonly emptiness: Emptiness; /** Rows counted in the value the model read (a counted rowset only). */ readonly rows?: number; /** `app` when the count rests on the app's declared `rowsAt`. */ readonly source?: 'library' | 'app'; /** The rows key the reading used (the app's `rowsAt`), when it used one. */ readonly rowsAt?: string; /** * Where in the value read the count was taken (path segments; `[]` the value * itself; `result` for each declared boundary read through): the rowset, or * the `rows` of the dataset ticket standing for it. */ readonly countedAt?: readonly string[]; /** A described result's data, per kind. */ readonly described?: DescribedCounts; /** Read through a declared `coverage()` boundary. */ readonly bounded?: true; /** An object result with no declared shape — whether it was empty cannot be told. */ readonly undeclaredShape: boolean; /** * Why the rows could not be counted when the shape IS known — so a reader * never says "not declared" of a declared key: * - `no-list` — the app declared `rowsAt`, and the value holds neither a list * there nor a ticket standing for it; * - `uncounted-ticket` — the rows went to the artifact store and the ticket * left in their place carries no whole-number count (a dataset ticket * without `rows`, or the library's placement ticket, which counts bytes); * - `redacted` — the result itself is kept out of the record: a redaction * policy (an agent's `redact`) left its placeholder where the value was, * so nothing about the rows can be read. Set by the answer account's call * reader (`answer-account/facts/calls.ts`), which never hands a * placeholder to this reader as if it were a result. */ readonly rowsUnread?: 'no-list' | 'uncounted-ticket' | 'redacted'; } /** * The ONE rule for an app's declared rows key: a non-empty TOP-LEVEL key of the * object result — no `/` and no `.`, so it can never be read as a path. Returns * what is wrong with a value (`'empty'`, `'nested'`), or `undefined` when it * keeps the rule. Both readers' declarations are checked by it (the answer * account's `answer-account/declarations.ts` · `validateDeclarations`, the * standing fold's `core/agent/assessment/assess.ts` · `assessAnswer`), so the * two cannot accept different keys. */ export declare function rowsAtProblem(rowsAt: unknown): 'empty' | 'nested' | undefined; /** JSON text that is an object or array is read as data; anything else as found. */ export declare function parseMaybeJson(value: unknown): unknown; /** What a value's OWN envelope declares — see {@link declaredByValue}. */ export interface ValueDeclaration { /** An absence — bare, or directly inside a `coverage()` boundary. */ readonly absent: boolean; /** A `coverage()` boundary. */ readonly bounded: boolean; /** * Ground the envelope says it did not check or can never cover — a * non-empty `not_checked` / `cannot_cover`, on the boundary or on the * absence it wraps. */ readonly gap: boolean; } /** * What a value's own envelope declares, read by the strict recognizers only and * by the rule the RUN's recognizer files rows for a value it is handed * (`semantics/coverage/read.ts` · `readCoverageResult`): a bare absence, or a boundary * with an absence directly inside it, and the gaps either lists. `undefined` * when the value is neither envelope. JSON text is parsed once, at the top. * * For a reader whose record holds no row for the call: an earlier answer's * result (its run is not in this record), or a result whose row was never * filed or was lost on the way here — a history restored by `resumeOnError` * carries no `coverageDeclared`, and a trimmed recording may drop it. The * envelope in the bytes is then the only evidence left, and reading it can * only ADD a declaration, never hide one. */ export declare function declaredByValue(value: unknown): ValueDeclaration | undefined; /** * Read one finished tool result. Typed routes only — see the header's table. * * @param value the value the MODEL read — a history message's `content`, or * `tool_end.modelResult ?? result`. JSON text is parsed once, at * this top level (the library's own serialization boundary); * strings inside the value are the tool's and are never parsed. * @param context the app's `rowsAt` for this tool, and the door the record * holds for this call (absent for an earlier answer's result). */ export declare function readEmptiness(value: unknown, context?: EmptinessContext): EmptinessReading;