agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
140 lines • 8.01 kB
TypeScript
/**
* skillGraph check-up — build-time validation of a declared graph.
*
* Pure + side-effect-free. Catches wiring mistakes at authoring time instead of
* mid-run: a skill nobody can reach, an edge to a skill that isn't in the graph,
* two un-prioritized edges leaving one skill, a graph with no start, a self-loop,
* an entry menu with no way to choose from it, a transition the cursor can never take.
*
* Surfaced two ways:
* • `graph.checkup()` → `{ ok, problems }` — always available, call it whenever.
* • `.build({ check: 'throw' | 'warn' | 'off' })` — run it at build time.
*
* **Only `unknown-skill` and `no-entry` are ERRORS.** Everything else is a WARNING,
* and deliberately so: this file reports what it can PROVE from the declaration, and
* a graph the model can still navigate is not a broken graph. `unreachable-skill`,
* `model-edge-only` and `dead-entry-step` all describe skills or transitions that
* deterministic routing cannot reach — but `read_skill` can, so calling them errors
* would claim more than the declaration supports.
*
* ## What "reachable" means here (8.7.0)
*
* The BFS walks **deterministic** edges only — an edge with a `when` or an
* `onToolReturn`. A bare `.route(a, b)` compiles to no trigger at all (`b` keeps its
* `llm-activated` default), so counting it as reachability answered a question nobody
* asked: the graph does not route there, the model does. Bare edges get their own
* code, `model-edge-only`, which says exactly that and names the one cursor position
* the gate will grant the jump from.
*/
import { type SkillMatchData } from './skillMatch.js';
import { type SkillGuardData } from './skillGuard.js';
/**
* The compiled trigger kinds this file needs to tell apart. Mirrors
* `InjectionTrigger['kind']` without importing it — the check-up is pure over
* strings and must not depend on the engine's types (`skillMatch.ts` is itself
* engine-type-free, which is what keeps that law intact here).
*/
export type CheckupTriggerKind = 'always' | 'rule' | 'on-tool-return' | 'llm-activated';
export type GraphProblemCode = 'unusable-tool-name' | 'unknown-skill' | 'no-entry' | 'unreachable-skill' | 'model-edge-only' | 'multi-entry-fanout' | 'dead-entry-step' | 'ambiguous-routes' | 'self-loop' | 'guard-unsatisfiable' | 'rule-id-exists' | 'overlapping-rules' | 'rules-shadowed-by-order' | 'intent-without-classify' | 'duplicate-intent-example' | 'overlapping-intents' | 'body-foreign-tool' | 'body-unknown-tool' | 'example-misses-own-rule' | 'example-shadowed-by-earlier' | 'example-shadowed-by-default' | 'example-unclaimed' | 'never-routes-claimed' | 'never-routes-by-default' | 'never-routes-contradicts-example' | 'tools-share-prefix' | 'few-declared-edges' | 'one-way-entries' | 'no-negative-evidence' | 'skill-wraps-one-tool' | 'artifact-kind-unsatisfied';
/** One issue found by the check-up. `kind: 'error'` fails `ok` (and `'throw'`). */
export interface GraphProblem {
readonly kind: 'error' | 'warning';
readonly code: GraphProblemCode;
readonly message: string;
/** The skill the problem is about (unreachable/ambiguous source). */
readonly skill?: string;
readonly from?: string;
readonly to?: string;
/**
* The declared phrase this problem is about — present on the `example-*`
* codes and on the `never-routes-*` ones, the checks that reason about a
* concrete phrase (in the positive and the negative direction respectively).
* The message always quotes it too; this is the same string as data, so a
* tool can group by phrase without parsing prose.
*/
readonly example?: string;
}
/** Result of `graph.checkup()`. `ok` is false iff there is ≥1 `error`. */
export interface GraphCheckup {
readonly ok: boolean;
readonly problems: readonly GraphProblem[];
/**
* What this report does NOT cover — present only when a check ran whose
* SILENCE could be misread as proof. Today that is the start-rule examples
* (skillExamples.ts): they prove things about the phrases the author
* declared and nothing about phrases nobody wrote, so a clean report is not
* proof of coverage — and the report says so itself rather than leaving it
* to prose docs. Absent (not empty) when nothing needed saying, so a graph
* that never heard of examples reports the exact same object as always.
*/
readonly notes?: readonly string[];
}
/** One declared entry, in declaration order. */
export interface CheckupEntry {
readonly id: string;
/** Has a `when` predicate or a `match` data matcher — i.e. it does NOT
* unconditionally win the cold-start cursor. */
readonly conditional: boolean;
/** The DATA matcher behind the condition, when the rule was declared as data
* (`match:`) rather than code (`when:`). Only rules that carry it can be
* compared (`overlapping-rules` / `rules-shadowed-by-order`) — a `when`
* predicate is opaque, and this file never claims to have checked one. */
readonly match?: SkillMatchData;
}
export interface CheckupInput {
/** Every skill id IN the graph. */
readonly skillIds: ReadonlySet<string>;
/** Declared entries, in declaration order (the order the cursor resolver reads). */
readonly entries: readonly CheckupEntry[];
/** Declared edges; `deterministic` = has a `when`/`onToolReturn`/
* `onToolStatus`/`guard` condition. A `guard` rides as DATA so this file
* can prove contradictions; `onToolReturnExact`/`onToolStatuses` are the
* only preconditions that are provably comparable with it (a RegExp
* `onToolReturn` is not decided, so it never arrives here). */
readonly routes: ReadonlyArray<{
fromId: string;
toId: string;
deterministic: boolean;
guard?: SkillGuardData;
onToolReturnExact?: string;
onToolStatuses?: readonly string[];
}>;
/** Decision-`tree()` graphs are exhaustive by construction — only id checks apply. */
readonly isTree: boolean;
/**
* The entries are EXCLUSIVE — a scorer (`.entryBy()`/`.entryByRelevance()`) or
* `.entryByRead()` picks exactly one. When true, the fan-out checks do not apply:
* choosing among the entries is precisely what those strategies do.
*/
readonly exclusiveEntries: boolean;
/**
* The COMPILED trigger kind per skill id. Read only to keep `unreachable-skill`'s
* sentence true: "the model can still open it with read_skill" holds for an
* `llm-activated` trigger and for no other kind — the agent's gate admits an open
* pick only for that one (`Agent.openSkillIds`). A skill that arrived carrying a
* hand-authored `rule` trigger keeps it (`deriveTrigger` returns null for an
* unwired skill), and for that skill the old sentence was false.
*/
readonly triggerKinds: ReadonlyMap<string, CheckupTriggerKind>;
/**
* A classifier is configured (`.classify()` / `start.classify`, SG-C). Two
* consequences here: an intent entry is judgeable (no
* `intent-without-classify`), and the tier-1 pairwise rule checks RUN even
* though the entries are exclusive — the cold cascade reads rule entries in
* declaration order again (first match wins), so the checks' premise holds,
* unlike under a pure scorer (which ranks ALL matching candidates) or
* `.entryByRead()` (the model picks).
*/
readonly hasClassifier?: boolean;
}
/** Run the check-up. Pure. */
export declare function checkupGraph(input: CheckupInput): GraphCheckup;
/**
* Format a check-up for a thrown error / console warning.
*
* Notes render AFTER the problems, tagged `[note]`: they are statements about
* the report's own reach, not findings. A check-up with no notes formats
* byte-identically to every version before them.
*/
export declare function formatCheckup(checkup: GraphCheckup): string;
//# sourceMappingURL=skillGraphCheckup.d.ts.map