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
TypeScript
/**
* 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;