agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
53 lines (52 loc) • 2.8 kB
TypeScript
/**
* A set a model-facing sentence is allowed to NAME, after the role filter has
* run over it (9.86.0) — and the one fact a filtered list cannot be written
* without.
*
* ── OMIT, NEVER DENY ────────────────────────────────────────────────────
*
* A narrowing may take a name off a sentence. It may not turn that name's
* absence into a claim that the thing is gone: the graph, the dispatch map and
* the menu are all still holding it, and a model told "no skill was reachable"
* stops asking for the one it may not be told about. Omission costs nothing and
* is always true; the negative sentence is a denial of what the run holds.
*
* Every composer took plain arrays and branched on `length > 0`, so "the set
* was empty" and "the filter emptied it" produced the SAME sentence — one
* shape, two facts, in three separate places. `held` is that missing fact, and
* it is REQUIRED rather than optional on purpose: the compiler now asks every
* caller the question the call sites each forgot to answer.
*
* ── WHY THIS IS A LEAF AND NOT A HELPER IN THE GATE ─────────────────────
*
* It lived in `core/agent/stages/toolCalls.ts`, which put it on the wrong side
* of the skill-graph fence: `read_skill`'s DESCRIPTION is composed inside
* `src/injection-engine/`, which may not import the agent loop, so the one
* surface the model reads to CHOOSE a skill could not use the fact its own
* refusals were being repaired with — and it went on printing "Nothing is
* reachable from here" over a graph whose only hop the role filter had removed.
* The same reason `src/lib/saidByPerson.ts` is a leaf: the writer and the
* recogniser share one rule, so they cannot drift.
*
* @example
* ```ts
* const hidden = new Set(['beta']);
* const hops = spoken(['beta'], (id) => !hidden.has(id));
* // → { named: [], held: true } — the clause is DROPPED, not negated.
* ```
*/
export interface SpokenIds {
/** The members this sentence may name — what survived the filter. */
readonly named: readonly string[];
/** Did the UNFILTERED set hold anything? `true` with an empty `named` is the
* filtered-to-empty case: the clause is omitted rather than negated. */
readonly held: boolean;
}
/**
* Apply the role filter to one set and keep both halves (9.86.0).
*
* `mayName` is the caller's visibility predicate — skill ids test membership of
* `scope.hiddenSkillIds`, tool names test whether every skill declaring them is
* hidden. Both questions have one answer shape, and this is it.
*/
export declare function spoken(all: readonly string[], mayName: (id: string) => boolean): SpokenIds;