agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
163 lines • 9.98 kB
TypeScript
/**
* absent — an absence that names its own coverage.
*
* Pattern: minted by a helper, RECOGNIZED by the framework (the tool-effects
* envelope precedent). A return shape the framework does not
* understand is a convention, and a convention cannot set a status,
* keep a value out of the evidence corpus, or stop a retry loop.
* Role: core/ layer, pure. The dispatch loop calls `readAbsence` at the
* execute boundary; `absent()` is what a tool author writes.
* Emits: N/A (the caller emits `agentfootprint.tools.absent`).
*
* ## The direction-of-error argument — the whole point of this file
*
* A tool that finds nothing returns *something*: an empty array, a `null`, a
* sentence. From any of those a model cannot tell **"I looked and there is
* nothing"** from **"I could not look"**. That confusion is not symmetric,
* and the asymmetry is what makes it worth a primitive:
*
* • a *nothing-found* misread as an *outage* sends someone to investigate a
* collector that is working perfectly — expensive, and self-correcting.
* • an *outage* misread as *nothing-found* declares a system healthy that
* was never checked — cheap, silent, and wrong in the direction that
* hurts.
*
* So the two must not share a shape. An error is a `role: 'tool'` result with
* `error: true`, a message, and no coverage. An absence is this: a result
* that RAN, carries the ground it covered, and says out loud that asking
* again changes nothing.
*
* ## Why the note is never interpolated
*
* Tool results are the evidence gate's corpus, so an absence that echoed the
* model's own arguments into that corpus would GROUND every identifier a model
* invented, as long as it handed the invention to one tool that found nothing.
* That is laundering an invention through a failed lookup — the same bug
* `evidence/frames.ts` exists to stop on the other side of the conversation.
*
* The gate therefore withholds `looked_for` (see `coverage/evidence.ts`): it
* is the one field whose job is to quote the request. The record-only item
* keys `short` and `kind` are withheld with it — a short form restates
* `what`, so nothing is lost, and it is the field an author is most tempted to
* personalise. Everything else the absence carries — the coverage lists, the
* author's `tryInstead` and `tryInsteadTool`, any extra key the tool attached
* to the envelope — is the TOOL speaking about the world and does ground,
* because an answer that follows the absence's own advice must not be called
* ungrounded for doing so.
* The note is kept free of interpolation for the same reason the exclusion
* exists: static library text has nothing of the caller's to leak.
*
* ## A suggestion to try another tool is typed, never parsed (9.113.0)
*
* `tryInstead` is ONE sentence, for the model; nothing in this library reads
* a tool name out of it. When the sentence points at another tool, the author
* names that tool beside it — `tryInsteadTool: { tool, why? }` — and the
* envelope carries it under its OWN key, `try_instead_tool`. Its own key, not
* `try_instead`: that field has been a string since it shipped and every
* reader typed against `ToolAbsence` reads it as one, so a second shape there
* would be dropped by each of them without a word. `tools.absent` carries
* both, as declared. ONE rule set decides what each is ({@link readSentence},
* {@link readToolSuggestion}): `absent()` refuses by it and the recognizer
* reads by it, so a mint and a read cannot disagree.
*/
import type { AbsenceAmongDeclaration, AbsenceDeclaration, Coverage, DeclaredAbsenceDeclaration, DeclaredToolAbsence, ToolAbsence, TryInsteadTool } from './types.js';
import { ABSENCE_MARKER, readAbsence } from './recognize.js';
export { ABSENCE_MARKER, readAbsence };
/**
* The static sentence every absence carries. Says the three things the field
* implementation proved a model needs: that the call SUCCEEDED, that nothing
* was substituted, and that a retry is futile. The third clause is the one
* that ends the loop.
*/
export declare const ABSENCE_NOTE = "The search ran and matched nothing. This is an ANSWER, not an error: nothing failed, nothing was substituted for what was asked, and calling this tool again with the same arguments returns this same result. `checked` is the ground this answer covers; anything under `not_checked` or `cannot_cover` is ground it does NOT cover, and reaching that needs a different question, not a retry.";
/**
* The note a COMPOSED absence is served (`Tool.search`): `checked` there is
* what the library composed from the call's arguments and the tool's declared
* defaults, so the note cannot say "nothing was substituted for what was
* asked" — a declared default, or an argument the call left out, may have
* narrowed the search. It keeps what stays true (the call ran, a retry
* returns the same) and points at the gaps. Static, never interpolated.
*/
export declare const ABSENCE_NOTE_COMPOSED = "The search ran and matched nothing \u2014 the call did not fail, and calling this tool again with the same arguments returns this same result. `checked` is the ground this answer covers, as the arguments of the call and the defaults the tool declares set it: where an argument was not sent, a default the tool applies may have narrowed the search. Anything under `not_checked` or `cannot_cover` is ground it does NOT cover, and reaching that needs a different question, not a retry.";
/**
* The note the MODEL is served in place of {@link ABSENCE_NOTE} when the
* absence's own period is `partly-held` or `not-held` (honesty step 7b, bench
* round 1) — `coverage/read.ts` · `strip` swaps it in and appends the period's
* one clause. It keeps what stays true (the call ran, a retry returns the same
* result) and drops the two completeness claims that do not: "nothing was
* substituted for what was asked" and `checked` as the whole ground. Static,
* never interpolated, never minted — the tool's own output keeps
* {@link ABSENCE_NOTE}.
*/
export declare const ABSENCE_NOTE_HELD_ONLY: string;
/**
* Say "I looked, and there is nothing" in a way a model cannot read as a
* failure — and cannot productively retry.
*
* Returns the value a tool's `execute` should return. The framework
* recognizes it at the dispatch boundary and gives it a delivered status of
* `'absent'` (routable by `onToolStatus`), a `tools.absent` event, and an
* evidence-corpus rule of its own.
*
* Refuses (throws, where it is called) a declaration it cannot honor — and
* any key the declaration does not have, naming the spelling meant when the
* key is a casing slip (`not_checked` → `notChecked`), so a list declared
* from plain JavaScript or JSON cannot vanish without a word. Every refusal
* starts `refused: `: inside `execute` it becomes the call's error result,
* which the model reads.
*
* Since honesty step 7b an absence also says WHERE it looked and WHEN —
* `provenance: { measuredAt, source }`, the shape and rules `describedResult()`
* uses — and what time its read covered — `period: { queried, held, readAt? }`,
* ISO 8601 instants with a zone (`held` may be `'unknown'`). "Searched the 02:00
* export — nothing" becomes data; the results layer judges the period.
*
* A name looked up in a known list is a search over that list: declare the
* list as `among` (see {@link AbsenceAmongDeclaration}) and the mint composes
* the names facet, the gap "names outside <source>" — which only
* `among.authoritative` removes; a `partial` read also gets "not every name in
* <source> was read", which nothing removes — and the `provenance`. The names are read only
* to check `count`; none is served.
*
* @example a port-lookup tool that found no matching FLOGI
* defineTool({
* name: 'flogi_for_port',
* description: 'FLOGI entries for one interface',
* inputSchema: { type: 'object', properties: { switch: { type: 'string' },
* port: { type: 'string' } }, required: ['switch', 'port'] },
* execute: ({ switch: sw, port }) => {
* const rows = fcns.flogi(sw, port);
* if (rows.length > 0) return rows;
* return absent({
* what: `FLOGI entries on ${port}`,
* checked: [
* `${sw}: the live fcns database`,
* { what: 'window: the last 24h', why: 'FLOGI history retention on this fabric' },
* ],
* notChecked: [{ what: 'the archived FLOGI history', why: 'older than the 24h window' }],
* cannotCover: [{ what: 'ports on the peer fabric',
* why: 'this collector is scoped to one fabric' }],
* tryInstead: 'Ask for a different interface, or query the peer fabric by name.',
* });
* },
* });
*/
export declare function absent(decl: DeclaredAbsenceDeclaration): DeclaredToolAbsence;
export declare function absent(decl: AbsenceDeclaration | AbsenceAmongDeclaration): ToolAbsence;
/** The absence's coverage, in the normalized three-list shape everything
* downstream (the event, the answer block) reads. */
export declare function coverageOfAbsence(absence: ToolAbsence): Coverage;
/**
* The absence's sentence, read by the rules `absent()` mints by. `undefined`
* when it declared none, AND when it declared one those rules refuse: an
* envelope minted elsewhere (a non-JS tool, a hand-built value) is read,
* never repaired, and the absence itself is still an absence either way.
*/
export declare function tryInsteadOfAbsence(absence: ToolAbsence): string | undefined;
/**
* The absence's typed tool, read by the same rules — a copy, never the
* object the envelope holds. `undefined` when it declared none, or one those
* rules refuse (see {@link tryInsteadOfAbsence}).
*/
export declare function tryInsteadToolOfAbsence(absence: ToolAbsence): TryInsteadTool | undefined;
//# sourceMappingURL=absent.d.ts.map