agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
79 lines (78 loc) • 4.66 kB
TypeScript
/**
* findings/answerText — the ONE reader of the reserved key in an answer's
* TEXT (9.114.2).
*
* Pattern: an incremental scanner — a character state machine with no scope
* and no I/O, fed chunk by chunk (`reservedMemberFilter`) or whole
* (`withoutReservedMembers`), with the same bytes out either way.
* Role: core/ layer leaf. `stages/callLLM.ts` feeds it the provider's
* stream under the arm, so `agentfootprint.stream.token` never
* carries the model's notes; `peel.ts · peelAnswerFindings` feeds
* it the whole answer, so the answer the run returns is exactly the
* text the stream showed.
*
* THE RULE
* Every JSON object written in the text — the whole answer, one in a code
* block, one in the prose, one in a list — loses its own `_findings` member:
* the key, the value and the separator that joined it to its neighbours,
* and nothing else. A `_findings` inside another object's VALUE is data and
* stays. Each removed value is handed back (`removed`), as written, for the
* peel to read as the answer's declaration.
*
* - An object left with nothing in it goes whole — its line too — when it
* stood on a line of its own outside any list: the whole answer, a
* paragraph of its own, a code block of its own. A code block left empty
* goes with its fences. The removed line's indentation and the whole
* blank lines after it go; the next line keeps its own indentation. No
* blank lines are left at either end of the text. The whole answer, left with nothing, is `{}` (a JSON answer stays
* JSON). Anywhere else an emptied object stays as `{}`, so the JSON or
* code around it keeps its shape. A string inside a list is read as a
* string, so a `]` or `{` in it neither ends the list nor opens an object.
* - An answer that held nothing but notes — a code block of them, or
* several objects of them — is left empty; only an answer that was one
* bare JSON object becomes `{}`.
* - Text that stops being JSON part way (`{see below}`, a stray quote or a
* raw line break inside a `_findings` value) is given back exactly as it
* was written; nothing is removed from it except a `_findings` member
* that was already complete. An object that held nothing but such
* complete notes when the text stopped being JSON (a model one closing
* brace short) ends there, and goes as an emptied object does. Keys and
* values are read by JSON's own grammar (`JsonValue`), so the first
* character JSON cannot take decides it — not a count of brackets, which
* prose may never balance.
* - Text that ends inside a `_findings` value that is JSON so far (a
* cut-off stream) keeps it hidden and reports it `complete: false`.
* - Nothing removed ⇒ the text comes back byte for byte, whatever the
* chunking. What is held back while undecided is bounded by a line:
* whitespace, a key (a raw line break ends one — JSON allows none), `{`
* until its first key is read, a separator, a code fence line, the rest
* of an emptied object's line. The one exception is a `_findings` value:
* it is never shown while it is JSON so far, and one that stops being
* JSON is held until the character that breaks it and then given back as
* written — so that hold can span as many lines as the notes do.
*/
/** One `_findings` member the scanner took out of the text. */
export interface RemovedMember {
/** Which object in the text held it, counted from 0 in text order. */
readonly object: number;
/** The member's value exactly as written — cut short when the text ended inside it. */
readonly valueText: string;
/** False when the text ended before the value did. */
readonly complete: boolean;
}
/** The scanner, fed piece by piece. */
export interface ReservedMemberFilter {
/** Feed the next piece of text; returns what can be shown now. */
push(chunk: string): string;
/** No more text: returns what was held back. After it, every call returns `''`. */
end(): string;
/** Every member removed so far, in text order. */
readonly removed: readonly RemovedMember[];
}
/** A fresh scanner for one text (one provider call's content). */
export declare function reservedMemberFilter(key?: string): ReservedMemberFilter;
/** The whole-text form: the text without its reserved members, and what was removed. */
export declare function withoutReservedMembers(text: string, key?: string): {
readonly text: string;
readonly removed: readonly RemovedMember[];
};