UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

1,042 lines • 59.8 kB
/**
 * skillGraph — a declarative, visualizable skill-dependency graph (proposal 002).
 *
 * The consumer declares skills + routing EDGES; `skillGraph()` compiles each edge
 * to the existing injection-engine TRIGGER on the target skill — so the dynamic,
 * token-efficient loading the engine already does becomes *declared* and *drawn*.
 *
 *   .entry(skill, { when? })              → trigger: `always` (or `rule` if when)
 *   .route(a, b, { onToolReturn | when }) → b compiles to a CURSOR-GATED `rule`
 *   (a skill with no declared incoming edge keeps its default `llm-activated`
 *    trigger — still reachable via `read_skill`, drawn as a dashed "model" edge)
 *
 * **v2 keystone — `from` IS enforced (a sticky cursor state machine).** A skill
 * graph is a state machine over skills; the engine tracks which node it is in via
 * `InjectionContext.currentSkillId` (the cursor). One pure resolver — `resolveCursor`
 * (see `makeResolveCursor`), of which `nextSkill(ctx)` is the `.to` projection — is the
 * single source of truth: each route target B
 * compiles to the trigger `nextSkill(ctx) === B`, which delivers `from`-gating
 * (an edge `A→B` fires only while the cursor is on A — no cross-skill edge bleed),
 * stickiness (the cursor stays on B until an edge leaves B), and a clean handoff
 * (B deactivates the same iteration C activates). The Injection Engine's Evaluate
 * stage advances the cursor with the SAME ctx (`currentSkillId = nextSkill(ctx)`),
 * so the active set and the persisted cursor never disagree. The DRAWN edge kind
 * (`on-tool-return` vs `predicate`) is preserved for rendering even though the
 * compiled trigger is always a `rule`. `toMermaid()` renders declared === drawn.
 *
 * A decision `tree()` routes per-iteration by stable `ctx` predicates (no cursor)
 * and is unaffected by `from`-gating. It has no cursor for `read_skill` to move
 * either, so `reachableSkills()` is EMPTY there and the gate refuses a leaf pick
 * (8.5.0) — see `reachableSkills` for why honouring it would break three of this
 * module's own invariants.
 *
 * **One skill's turn at a time (8.15.0).** The keystone's third property — "B
 * deactivates the same iteration C activates" — now holds for EVERY node in a flat
 * graph, including an entry that carries a `when`. Such an entry used to compile to
 * `when(ctx) || cursor === id`, and the leftover rule clause meant an entry that
 * routed onward stayed loaded beside its own successor, on that iteration and every
 * one after it. A conditional entry is active exactly while the cursor is on it;
 * `when` decides where a turn STARTS. The one declared way to be co-active beside
 * the cursor is an entry with no `when` at all (`{ kind: 'always' }`), which is
 * untouched. A rule that matched while the cursor was elsewhere is reported, not
 * swallowed — see `supersededEntries`.
 *
 * **The model moves too (`read_skill`).** Scoped `read_skill` bounds the model to
 * `reachableSkills(cursor)` — the declared successors of where it stands, plus the
 * entries. A pick the gate ACCEPTS moves the cursor exactly like a declared edge
 * does (`ctx.pendingSkillPick`, honoured in `makeResolveCursor`), so what the gate
 * allows and what actually takes effect are the same set. A declared edge that
 * fires the same turn wins (`D1 > D2`, docs/design/skill-graph.md §4A.1) and the
 * run emits `agentfootprint.skill.reroute_superseded` rather than leaving the
 * model's answered-"activated" claim quietly unmet.
 *
 * **The resolver says WHY, it is not asked to be guessed.** `explainNextSkill(ctx)`
 * returns the destination AND the winning clause (`CursorMove`), decided at the same
 * `return`. The agent stamps it on `context.evaluated` as `cursorMove`, which is how
 * `routeRecorder()` can tell a model pick from a declared edge that happens to point
 * at the same skill (8.5.0 — before, the drawn build-time provenance was read as the
 * per-hop cause, so a model pick was recorded under a declared edge's label).
 */
import type { ToolResultStatus } from '../semantics/toolOutcome.js';
import type { Injection, InjectionContext } from './types.js';
import type { Embedder } from '../memory/embedding/types.js';
import type { EntryScore, EntryScoring, EntryScorer } from './entryScorer.js';
import { type GraphCheckup } from './skillGraphCheckup.js';
import { type RouteWitness, type SkillMatch, type SkillMatchData } from './skillMatch.js';
import { type GuardConditionEvidence, type SkillGuard, type SkillGuardData } from './skillGuard.js';
import type { IntentScorer } from './intentScorer.js';
import { type RoutingPolicy } from './routingPolicy.js';
import { type TurnRoutingPlan } from './skillIntent.js';
export { formatCheckup } from './skillGraphCheckup.js';
export type { GraphCheckup, GraphProblem, GraphProblemCode } from './skillGraphCheckup.js';
export type { SkillMatch, SkillMatchData, RouteWitness } from './skillMatch.js';
export { GUARD_HOP_KEYS, plainGuardCaption, type GuardConditionData, type GuardConditionEvidence, type GuardOperator, type GuardValue, type SkillGuard, type SkillGuardData, type SkillGuardOps, } from './skillGuard.js';
export type { TurnRoutingPlan } from './skillIntent.js';
export type { TurnRoute } from './routingPolicy.js';
/** How `.build({ check })` reacts to the graph check-up. */
export type GraphCheckMode = 'throw' | 'warn' | 'off';
/** Options for `.build()`. */
export interface BuildOptions {
    /**
     * Run the build-time check-up (see `graph.checkup()`):
     *   • `'throw'` — throw if any ERROR-level problem (unknown-skill / no-entry);
     *   • `'warn'`  — console.warn every problem in dev mode (`enableDevMode()`), silent otherwise;
     *   • `'off'`   — skip it entirely.
     *
     * **Default `'throw'` since 8.7.0** (was `'warn'`), matching the object-literal
     * form. A graph with an error-level problem cannot start a turn at all, and under
     * the old default it built silently outside dev mode and failed at run time
     * instead. `'warn'` still means exactly what it says — never throws — so an
     * explicit `check: 'warn'` keeps the old soft behavior.
     *
     * `graph.checkup()` is always available regardless.
     */
    readonly check?: GraphCheckMode;
    /**
     * Tool names the AGENT exposes to every skill (`.tool()` / `.tools()` / a baseline
     * `ToolProvider`). Without them the skill-body contract check reads a body's
     * `lookup_order(id)` as a typo, because the graph only knows the tools its own
     * skills carry. Same field as `checkup({ knownTools })` — see `SkillGraph.checkup`.
     *
     * Omitted, the body-contract checks (`body-foreign-tool` / `body-unknown-tool`)
     * are DEFERRED out of this build pass and run once at Agent build instead, where
     * the full tool registry exists — see {@link SkillGraph.deferredBodyContract}.
     */
    readonly knownTools?: readonly string[];
    /**
     * FLAT graphs: stamp `autoActivate: 'currentSkill'` on every WIRED skill (every
     * skill an entry or a route mentions), exactly as a decision `.tree()` stamps its
     * leaves — so a skill's tools reach the LLM only while the graph is on it, instead
     * of every skill's tools landing in the always-on registry from iteration 1.
     *
     * Default `false` — additive behavior. Set `true` to scope tools explicitly.
     * A skill that declared its OWN `autoActivate` in `defineSkill(...)` always keeps
     * it: this fills the default, it never overrides. A listed-but-unwired skill is
     * not stamped — the graph does not route it, so the graph does not scope it.
     *
     * A decision `.tree()` takes this dial on `.tree(root, { scopeTools })` (object
     * form: the tree arm's own `scopeTools` field); setting it here alongside a tree
     * is refused so one dial cannot live in two homes.
     */
    readonly scopeTools?: boolean;
}
/** Options for `graph.checkup()`. */
export interface CheckupOptions {
    /**
     * Tool names the AGENT exposes to every skill — `.tool()` / `.tools()`
     * registrations and any always-on `ToolProvider`. A skill body may name them
     * freely: they are callable from every skill, so they are neither `body-unknown-tool`
     * (they exist) nor `body-foreign-tool` (they are not somebody else's).
     *
     * @example
     *   graph.checkup({ knownTools: ['lookup_order', 'list_skills'] });
     */
    readonly knownTools?: readonly string[];
}
/**
 * One start rule: route the turn's start to `use` when the rule matches.
 * Exactly ONE of `match` (data) / `when` (code) per rule — the union enforces it
 * at the keystroke and the build refuses it for everyone else. A rule exists to
 * be conditional; for an unconditional start use `start: 'id'` / `{ use }`.
 */
export type SkillStartRule = {
    readonly use: string;
    /** The code form — an opaque predicate over the iteration context. */
    readonly when: (ctx: InjectionContext) => boolean;
    readonly match?: never;
    /** The phrasings this rule claims — build-time TEST material. See
     *  {@link SkillEntryOptions.examples}. */
    readonly examples?: readonly string[];
} | {
    readonly use: string;
    /** The data form — comparable, drawable, stored. See {@link SkillMatch}. */
    readonly match: SkillMatch;
    readonly when?: never;
    /** The phrasings this rule claims — build-time TEST material. See
     *  {@link SkillEntryOptions.examples}. */
    readonly examples?: readonly string[];
};
/** Where a turn starts, in the object-literal (flat) form. */
export type SkillGraphStart = string | {
    readonly use: string;
} | {
    readonly rules: ReadonlyArray<SkillStartRule>;
    /** The intent classifier (SG-C) — REQUIRED iff any rule declares
     *  `match: { intent }`. Same machine as `.classify(scorer)`. */
    readonly classify?: IntentScorer;
    /** Tie-policy override — the ONE override home, beside the scorer it
     *  governs. Same machine as `.classify(scorer, policy)`. */
    readonly routing?: Partial<RoutingPolicy>;
} | {
    readonly entries: readonly string[];
    /** Rank the entries with a scorer strategy (`keywordScorer()`,
     *  `embeddingScorer(e)`, or your own). Takes precedence over `byRelevance`. */
    readonly scoredBy?: EntryScorer;
    /** Sugar: rank the entries with an embedder (cosine/softmax). Omit both → the
     *  LLM reads the menu and picks (`.entryByRead()`) — no model call. */
    readonly byRelevance?: Embedder;
};
/** One tool-result transition in the object-literal (flat) form. */
export interface SkillGraphStep {
    readonly from: string;
    readonly to: string;
    readonly when?: SkillRouteOptions['when'];
    readonly onToolReturn?: string | RegExp;
    /** Route on the result's declared outcome status (9.19.0) — see
     *  {@link SkillRouteOptions.onToolStatus}. */
    readonly onToolStatus?: SkillRouteOptions['onToolStatus'];
    /** The edge's condition as DATA (9.51.0) — see
     *  {@link SkillRouteOptions.guard}. */
    readonly guard?: SkillRouteOptions['guard'];
    readonly label?: string;
}
/**
 * Object-literal form, FLAT arm — `start` + `steps` declare the routing.
 * `tree` is typed `never` here so `{ tree, start }` is a COMPILE error, not just
 * a build-time refusal (see `SkillGraphConfig`).
 */
export interface SkillGraphFlatConfig {
    /** Every skill in the graph (wired or not). */
    readonly skills: readonly Injection[];
    /** Where a turn starts. */
    readonly start?: SkillGraphStart;
    /** Tool-result transitions; `from`/`to` are skill ids resolved against `skills`. */
    readonly steps?: readonly SkillGraphStep[];
    readonly tree?: never;
    /**
     * Stamp `autoActivate: 'currentSkill'` on every WIRED skill (every skill an
     * entry or a step mentions), exactly as the tree arm already stamps its leaves —
     * a skill's tools reach the LLM only while the graph is on it. **Default `false`**
     * (today's additive behavior — every skill's tools visible from iteration 1);
     * A skill whose author set its own
     * `autoActivate` keeps it: the graph level is a default, never an override.
     * See {@link BuildOptions.scopeTools}.
     */
    readonly scopeTools?: boolean;
    /**
     * Phrasings this graph must claim NOWHERE — the negative routing rows. Same
     * machine as `.neverRoutes([...])`; see that method for what the check
     * proves and what it deliberately does not. A row a declared start rule
     * claims is an ERROR (`never-routes-claimed`), naming the rule.
     *
     * @example
     *   skillGraph({ skills, start: { rules }, neverRoutes: ['what is the weather'] });
     */
    readonly neverRoutes?: readonly string[];
    readonly check?: GraphCheckMode;
    /** Baseline agent tool names — see {@link BuildOptions.knownTools}. */
    readonly knownTools?: readonly string[];
}
/**
 * Object-literal form, TREE arm — a decision tree owns the routing, so there is
 * no entry menu and no cursor for `start`/`steps` to describe. Both are typed
 * `never` so the contradiction is a compile error.
 */
export interface SkillGraphTreeConfig {
    /** Every skill in the graph. Under `tree` this must be exactly the leaf set —
     *  a listed skill that is not a leaf would never load, and is refused. */
    readonly skills: readonly Injection[];
    /** A decision tree (instead of `start` + `steps`). */
    readonly tree: DecisionNode | Injection;
    /**
     * Scope each leaf's tools to the routed leaf. The object form's half of
     * `.tree(root, { scopeTools })` — added in 8.7.0, because until then this form
     * hard-coded `true` and the fluent form's only opt-out had no object-form twin.
     * Default `true`. See {@link TreeOptions.scopeTools}.
     */
    readonly scopeTools?: boolean;
    readonly start?: never;
    readonly steps?: never;
    /** A tree has no start RULES for a negative row to be judged against, so the
     *  type refuses it here and `build()` refuses it at runtime (see
     *  {@link SkillGraphBuilder.neverRoutes}). */
    readonly neverRoutes?: never;
    readonly check?: GraphCheckMode;
    /** Baseline agent tool names — see {@link BuildOptions.knownTools}. */
    readonly knownTools?: readonly string[];
}
/**
 * Object-literal form of a skill graph — an alternative to the fluent builder.
 * Listing `skills` INDEPENDENTLY of the wiring is the point: the check-up can then
 * flag a skill that was listed but never wired (the fluent builder only ever sees
 * skills that appear in an edge). Compiles to the SAME `SkillGraph`. `check`
 * defaults to `'throw'` here (a new surface, fail-loud).
 *
 * A UNION of two arms, because `tree` and `start`/`steps` are two ways to declare
 * the same thing and only one of them compiles: `{ tree, start }` is a type error
 * for a TypeScript consumer and a build-time refusal for everyone else (8.4.0 —
 * before, the tree silently won and the flat wiring was discarded). Valid tree-only
 * and flat-only configs typecheck exactly as they did.
 */
export type SkillGraphConfig = SkillGraphFlatConfig | SkillGraphTreeConfig;
export type { EntryScore, EntryScoring };
/** Deterministic routing into a skill, keyed on the last tool result. */
export interface SkillRouteOptions {
    /** Predicate on the previous iteration's tool result → activate the target
     *  on the next iteration. The common, controllable edge. `status` is
     *  present when the tool declared one on its result envelope (9.19.0).
     *
     *  `result` is the string the MODEL read — which artifact placement can
     *  replace with a claim ticket, so an operator raising or lowering
     *  `artifacts.placement.maxInlineChars` can change whether a text-matching
     *  edge fires. See {@link InjectionContext.lastToolResult}; route on
     *  `onToolReturn` / `onToolStatus` for a guard placement cannot move. */
    readonly when?: (result: {
        readonly toolName: string;
        readonly result: string;
        readonly status?: ToolResultStatus;
    }) => boolean;
    /** Sugar for "activate whenever this tool returns (any result)". String is an
     *  exact match; RegExp is tested against the tool name. */
    readonly onToolReturn?: string | RegExp;
    /**
     * Route on the result's declared OUTCOME, not its prose (9.19.0) — the
     * data half of the outcome-status normalization: a `'denied'` call must
     * never route like a `'success'`. Matches when a tool result of the batch
     * carries one of the named statuses on its envelope; a result with NO
     * declared status can never match (an undeclared outcome is not
     * evidence). Compose with `onToolReturn` to pin the tool too ("when
     * `refund` returns `'denied'`"); alone, any tool's matching status fires.
     * Data — comparable, drawable (`toMermaid()` captions it), stored. At
     * most one of `when` / `onToolStatus`: code or data, never both.
     */
    readonly onToolStatus?: ToolResultStatus | ReadonlyArray<ToolResultStatus>;
    /**
     * The DATA form of `when` (9.51.0) — the edge's condition declared as
     * comparable, drawable, recordable conditions instead of an opaque
     * predicate: `{ key: { eq | ne | gt | gte | lt | lte | in | notIn: value } }`,
     * every condition ANDed (the operator grammar deliberately mirrors
     * footprintjs's `WhereFilter` — see {@link SkillGuard}). This is the
     * SkillWalker's guard mover as data: the map is data, entry matchers are
     * data, `onToolStatus` arms are data — the guard was the last opaque
     * function on a route edge.
     *
     * Judged per tool result of the previous iteration's batch, exactly where a
     * `when` runs. Six hop keys read the hop directly (`toolName`, `result`,
     * `status`, `iteration`, `userMessage`, `currentSkillId`); any OTHER key
     * reads the top-level field of that name from the RESULT parsed as JSON —
     * `guard: { riskLevel: { gte: 'high' } }` routes on a tool that returned
     * `{"riskLevel":"high"}`. Being data buys four things a `when` can never
     * have: the check-up proves contradictions (`guard-unsatisfiable`),
     * `toMermaid()` captions the edge ("when riskLevel ≥ high"),
     * `skill.graph_declared` carries it into every recording, and each
     * evaluation that DECIDES a hop — taken or refused — leaves per-condition
     * evidence on `cursorMove.guard` / `cursorMove.guardsClosed`.
     *
     * COMPOSES with `onToolReturn` / `onToolStatus` ("this tool, this outcome,
     * AND these conditions"). At most one of `when` / `guard`: code or data,
     * never both — to combine declared conditions with extra logic, fold the
     * checks into your `when` predicate.
     */
    readonly guard?: SkillGuard;
    /** Caption rendered on the edge. Defaults to a derived label. */
    readonly label?: string;
}
/** Where a turn starts. `when` (optional) makes entry intent-conditional. */
export interface SkillEntryOptions {
    /**
     * Which entry the turn STARTS on — a predicate over the iteration context
     * (e.g. `ctx.userMessage`). The first entry whose `when` passes wins the
     * cold-start cursor.
     *
     * **It decides the start; it does not keep the skill on the wire (8.15.0).**
     * A conditional entry is active exactly while the cursor is on it — a route out
     * of it ends its turn, and its rule cannot bring it back while the graph is
     * somewhere else. Before 8.15.0 the rule re-activated it on every iteration, so an
     * entry that routed onward stayed loaded beside its own successor.
     *
     * Omit `when` → the skill is `always` active: a persistent base procedure, on
     * beside whatever the cursor is on. That is the declared way to ask for an
     * always-on skill; `when: () => true` is NOT the same thing any more. For "on
     * whenever this matches, wherever the graph is", use the flavor built for it —
     * `.steering(...)` / `.skill(...)` with its own `rule` trigger — rather than an
     * entry, which is a position in a state machine.
     */
    readonly when?: (ctx: InjectionContext) => boolean;
    /**
     * The DATA form of `when` — a declared matcher over the user's message instead
     * of a predicate (see {@link SkillMatch}): comparable by the check-up, captioned
     * by `toMermaid()`, stored on the compiled skill's provenance. Same start
     * semantics as `when` in every other way. At most ONE of `match`/`when` — both
     * set is refused at build time. Omitting both keeps this an `always` entry,
     * exactly as before.
     */
    readonly match?: SkillMatch;
    /**
     * The phrasings this rule CLAIMS — real messages a user would type that
     * should start the turn here. Optional, additive, and **fed to nothing at
     * run time**: the check-up reads them at build time and the routing never
     * sees them, so a rule with examples routes byte-identically to the same
     * rule without them.
     *
     * Things become PROVABLE once a phrase is declared, by RUNNING the compiled
     * matchers rather than comparing them:
     *
     *   • `example-misses-own-rule` — this rule does not claim its own example.
     *     An ERROR for a data `match` (it reads the user message and nothing
     *     else, so the no-match holds under every context) or for a predicate
     *     that THREW; a WARNING for an opaque `when` that returned false, which
     *     may be gated on conversation state and claim the phrase on a later
     *     turn (see the context note below);
     *   • `example-shadowed-by-earlier` (error) — an EARLIER rule claims the
     *     phrase first, so the turn starts somewhere your own example denies.
     *     This is the one `rules-shadowed-by-order` must stay silent about when
     *     the two rules use different regexes (intersection is not decided
     *     anywhere in this library) — a witness phrase decides it instead;
     *   • `example-shadowed-by-default` (warning) — the earlier claimant is an
     *     UNCONDITIONAL entry. The declaration-order cold start stops at a
     *     default; the turn-start cascade (a classifier, or `continuity:
     *     'conversation'`) reads the conditional rules only and skips it — and
     *     which one applies is decided at AGENT MOUNT, so the report names both
     *     readings instead of asserting one against the router;
     *   • `example-unclaimed` (warning) — NO rule claims the phrase, so the turn
     *     falls through to the model tier. Absence, which no matcher-vs-matcher
     *     analysis can catch.
     *
     * **The context they are judged on.** Every condition here runs on ONE
     * context — iteration 1, the phrase as `userMessage`, empty `history`, no
     * cursor — the context a turn's FIRST iteration hands a start rule. Turn 2 of
     * a conversation also starts cold in cursor terms while CARRYING history, so
     * a `when` gated on conversation state may claim the phrase on a turn this
     * check cannot run. That is why its no-match is a warning, and why every
     * message names the context it judged under.
     *
     * **Tier difference, and it matters.** In `match: { intent, examples }`
     * (tier 2) the examples are SCORING material: the classifier reads them at
     * RUN time to judge new messages. Here (tier 1) they are TEST material only.
     * The author-facing meaning is the same — "the phrasings this rule claims" —
     * the runtime role is not, and a rule may not carry both lists (refused at
     * build, naming the difference).
     *
     * The boundary: these checks prove things about the phrases you declared and
     * nothing about phrases nobody wrote. No warning is not proof of coverage —
     * `graph.checkup().notes` says so on the report itself.
     *
     * @example
     *   .entry(arrayInventory, {
     *     match: /\b(array|volume|pool)\b/i,
     *     examples: ["what's running on shpstrprncl101"],
     *   })
     */
    readonly examples?: readonly string[];
    readonly label?: string;
}
/** Options for a decision `tree()`. */
export interface TreeOptions {
    /**
     * Scope the tool list to the routed leaf (the on-demand-tools default).
     *
     * A decision tree routes to EXACTLY ONE skill per iteration, so each leaf is
     * stamped `autoActivate: 'currentSkill'` — its `inject.tools` reach the LLM
     * ONLY when the tree routes there, instead of every skill's tools landing in
     * the always-on static registry on every call.
     *
     * `read_skill` cannot reach another LEAF mid-run (8.5.0): a tree has no cursor to
     * move, and this "exactly one leaf" property is one of the reasons — admitting a
     * second leaf would put two leaves' tools on the wire and make the dev-mode
     * exactly-one monitor warn. It said otherwise until 8.5.0, and the pick was
     * accepted and then silently dropped. The escape hatch is a skill registered
     * BESIDE the graph (`.skill(x)`, `.selfExplain()`), which really does activate by
     * `read_skill` and is admitted from anywhere.
     *
     * Default `true`. Set `false` for the legacy additive behavior (all leaves'
     * tools always visible). A leaf that sets its OWN `autoActivate` in
     * `defineSkill(...)` is always respected — this only fills the default.
     */
    readonly scopeTools?: boolean;
}
export type SkillEdgeKind = 'entry' | 'predicate' | 'on-tool-return' | 'on-tool-status' | 'guard' | 'model';
export interface SkillEdge {
    /** Source skill id, or `null` for the synthetic START (an entry edge). */
    readonly from: string | null;
    readonly to: string;
    readonly kind: SkillEdgeKind;
    readonly label?: string;
    /** The DATA matcher on an entry edge, when the rule was declared as data
     *  (`match:`). `toMermaid()` captions the edge with it when no explicit
     *  `label` was given. */
    readonly match?: SkillMatchData;
    /** The DATA guard on a route edge, when one was declared (9.51.0) — rides
     *  additively beside the kind exactly as `match` rides entry edges (a
     *  guard composed with `onToolReturn`/`onToolStatus` keeps that kind; a
     *  guard alone is kind `'guard'`). `toMermaid()` captions a guard-only
     *  edge with it when no explicit `label` was given, and
     *  `skill.graph_declared` carries it into every recording. */
    readonly guard?: SkillGuardData;
}
/**
 * A decision-tree node (v3): a predicate that branches to a subtree (or a skill
 * LEAF) on each side. The tree compiles to per-skill triggers — each leaf's
 * trigger is the conjunction of the predicates on its root→leaf path (with
 * earlier-sibling negation for if/else exclusivity), evaluated per iteration. So
 * "predicate nodes that route" needs NO engine change — same evaluator.
 */
export interface DecisionNode {
    readonly kind: 'decision';
    readonly predicate: (ctx: InjectionContext) => boolean;
    readonly whenTrue: DecisionNode | Injection;
    readonly whenFalse: DecisionNode | Injection;
    /** Caption for the predicate node when drawn (e.g. "io intent?"). */
    readonly label?: string;
}
/** Build a decision node. Leaves are skills (an `Injection`); internal nodes are
 *  other `decideSkill(...)` results. (Renamed from `decide` in v7 to avoid
 *  colliding with footprintjs's `decide()`.) */
export declare function decideSkill(predicate: (ctx: InjectionContext) => boolean, whenTrue: DecisionNode | Injection, whenFalse: DecisionNode | Injection, label?: string): DecisionNode;
/**
 * WHY the cursor landed where it did on one iteration — the winning clause of the
 * one cursor resolver, reported rather than guessed (8.5.0).
 *
 *   • `'entry'`      — cold start: the first entry whose `when` passed (or, on a
 *                      cascade graph, a tier-1 start rule won the turn);
 *   • `'route'`      — a declared, `from`-gated edge fired (D1);
 *   • `'model-pick'` — no declared edge fired, so the model's gate-accepted
 *                      `read_skill` pick moved the cursor (D2), at cold start or mid-run;
 *   • `'tool-proposal'` — no declared edge fired and an ACCEPTED
 *                      `propose-transition` tool effect moved the cursor
 *                      (9.19.0) — deterministic tool evidence, ranked between
 *                      the author's edges (D1 still wins) and the model's
 *                      pick (a tool outranks a guess);
 *   • `'intent'`     — the turn-start cascade's tier-2 scorer decisively routed
 *                      the turn (SG-C; `turn_routed` carries the numbers);
 *   • `'continuity'` — the inherited conversation cursor held the turn's start
 *                      (SG-C, `continuity: 'conversation'`);
 *   • `'decider'`    — the configured tier-3 decider resolved an outstanding
 *                      menu out-of-band and the turn starts on its pick
 *                      (9.19.0);
 *   • `'stay'`       — nothing fired; the cursor is sticky and stayed put;
 *   • `'none'`       — no cursor at all (cold start with nothing to enter, or a
 *                      decision `tree()`, which routes by predicate and has no cursor).
 *
 * This exists because the DRAWN provenance on a skill (`metadata.skillGraph`) answers
 * "how is this skill reachable" — a build-time fact — and was being read as "how did
 * we get here this turn". A model pick into a skill that also has a declared edge was
 * therefore attributed to that edge, label and all, in the recorded route.
 */
export type CursorMoveCause = 'entry' | 'route' | 'model-pick' | 'tool-proposal' | 'intent' | 'continuity' | 'decider' | 'stay' | 'none';
/**
 * One tool result's routing implication inside a parallel batch (9.16.0) —
 * which call it was, and the edge target it matched. Named by
 * `skill.route_conflict` as the winner or a suppressed loser.
 */
export interface RouteBatchOutcome {
    /** The provider's tool_use id for the call. Absent only for a context that
     *  supplied the singular `lastToolResult` (which can never conflict). */
    readonly toolCallId?: string;
    /** The tool whose result matched an edge. */
    readonly toolName: string;
    /** The edge target that result routed to. */
    readonly target: string;
}
/**
 * Two or more results of ONE parallel batch matched edges to DIFFERENT
 * targets (9.16.0). The first in call order wins the cursor; the rest are
 * suppressed — and reported here rather than silently dropped, so the record
 * explains the hop the run did NOT take. Same-target matches are not a
 * conflict (they all asked for the move that happened).
 */
export interface RouteBatchConflict {
    /** The call-order-first match — the one that moved the cursor. */
    readonly winner: RouteBatchOutcome;
    /** Later matches to other targets, in call order, that did not move it. */
    readonly losers: readonly RouteBatchOutcome[];
}
/**
 * One guard's evaluation against one tool result (9.51.0) — the evidence a
 * data guard leaves whenever it DECIDES a hop: which edge, which result it
 * judged, and every condition with the value it saw. Two homes on the move:
 * `CursorMove.guard` (the taken hop's evaluation, verdict `true`) and
 * `CursorMove.guardsClosed` (the refusals, verdict `false`) — both ride
 * `context.evaluated.cursorMove` onto the record.
 */
export interface GuardEvaluation {
    /** The guarded edge. */
    readonly from: string;
    readonly to: string;
    /** The tool result this evaluation judged. */
    readonly toolName: string;
    /** The provider's tool_use id for that call, when the batch carried one. */
    readonly toolCallId?: string;
    /** `true` — the guard passed and the edge fired; `false` — the guard
     *  refused a hop whose other declared conditions were already met. */
    readonly verdict: boolean;
    /** Per-condition evidence, in declaration order — every condition, the
     *  summarized value it was judged against, and whether it passed. */
    readonly conditions: readonly GuardConditionEvidence[];
}
/** The cursor resolver's full answer: where, and by which clause. */
export interface CursorMove {
    /** The cursor after this iteration (what `nextSkill` returns). */
    readonly to?: string;
    /** The cursor before it. */
    readonly from?: string;
    /** The winning clause. */
    readonly by: CursorMoveCause;
    /** Present only when `by: 'route'` resolved a parallel batch whose results
     *  matched edges to different targets — the suppression, on the record.
     *  The Evaluate stage emits it as `agentfootprint.skill.route_conflict`. */
    readonly conflict?: RouteBatchConflict;
    /**
     * The EVIDENCE a tier-1 DATA matcher routed on (9.28.0) — the text out of the
     * user message that made the entry's rule true, bounded (see
     * {@link RouteWitness}). Present only for `by: 'entry'` moves decided by a
     * `match:` (RegExp / `{ keywords }` / `{ all }`) rule; a `when` predicate is
     * opaque code, an unconditional entry matched nothing, and a scorer's
     * evidence is its scores — all three record no witness.
     */
    readonly witness?: RouteWitness;
    /**
     * The EVIDENCE the winning guard routed on (9.51.0) — present only for a
     * `by: 'route'` move whose firing edge declared a `guard:`. The full
     * per-condition evaluation (verdict `true`), decided at the same return as
     * the destination, so the record can never quote a different judgment than
     * the one that routed.
     */
    readonly guard?: GuardEvaluation;
    /**
     * The guards that REFUSED this iteration (9.51.0) — every guarded edge out
     * of the cursor whose OTHER declared conditions a result met and whose
     * guard said no (verdict `false`; at most one record per edge, the first
     * refusal in call order). Present on whatever move resulted — including a
     * `'stay'`, where it answers "why didn't my guarded edge fire?" with the
     * conditions and the values that closed it, the same honesty
     * `supersededEntries` gives suppressed entries. An edge whose
     * `onToolReturn`/`onToolStatus` preconditions never matched is NOT here:
     * its guard never decided anything.
     */
    readonly guardsClosed?: readonly GuardEvaluation[];
}
/** A node in the drawn graph — a `predicate` diamond or a `skill` box. */
export interface SkillNode {
    readonly id: string;
    readonly kind: 'predicate' | 'skill';
    readonly label?: string;
}
/** One predicate on a skill's root→leaf decision path, and the branch taken. */
export interface SkillRoutingStep {
    /** The predicate's caption (the `decide(...)` label). */
    readonly label: string;
    /** Which side of the predicate leads to this skill. */
    readonly branch: 'yes' | 'no';
}
/**
 * The routing PROVENANCE stamped onto a compiled skill's `metadata.skillGraph`
 * — *why* this skill is reachable. It rides through to the `context.evaluated`
 * event when the skill activates, so commentary + the lens can narrate the real
 * routing (not just "a skill activated"). Observability only; the trigger logic
 * is unchanged.
 */
export interface SkillRouting {
    /** How the skill is reached: a decision `tree` leaf, a flat `entry`, a
     *  deterministic `route` edge, or `model` (read_skill-reachable). */
    readonly via: 'tree' | 'entry' | 'route' | 'model';
    /** Decision path (tree only): the predicates from root→leaf + branch taken.
     *  For a skill used as MULTIPLE tree leaves this is the FIRST path; all
     *  paths are in `paths`. */
    readonly path?: readonly SkillRoutingStep[];
    /** All decision paths reaching this skill (tree only; present when the same
     *  skill is the leaf of more than one branch — the compiler merges repeated
     *  leaves into ONE injection whose trigger ORs the path predicates). */
    readonly paths?: ReadonlyArray<readonly SkillRoutingStep[]>;
    /** Entry/route edge caption. */
    readonly label?: string;
    /** Source skill id (route only). */
    readonly from?: string;
    /** The compiled trigger kind for a route (`rule` / `on-tool-return`). */
    readonly triggerKind?: string;
    /** The DATA matcher that routes here (entry only, when declared as `match:`) —
     *  serializable, so commentary/lens can say WHICH pattern chose the skill. */
    readonly match?: SkillMatchData;
    /** The DATA guard on the first deterministic edge in (route only, when
     *  declared as `guard:`, 9.51.0) — serializable, the `match` twin. */
    readonly guard?: SkillGuardData;
}
/** The metadata key carrying a skill's routing provenance. */
export declare const SKILL_GRAPH_METADATA_KEY: "skillGraph";
/**
 * The note a graph leaves for Agent build when its own build pass DEFERRED the
 * skill-body ↔ tool-contract checks (`body-foreign-tool` / `body-unknown-tool`) —
 * see {@link SkillGraph.deferredBodyContract}.
 */
export interface DeferredBodyContract {
    /** The graph's `check` mode — the severity the deferred run respects
     *  (`'off'` never defers: it stays off at agent build too). */
    readonly mode: 'throw' | 'warn';
}
/**
 * The metadata key carrying the {@link DeferredBodyContract} note on each COMPILED
 * skill. The graph-level `graph.deferredBodyContract` note names the deferral, but
 * skills reach an agent through more than one door — `.skillGraph(graph)` sees the
 * graph, `.skills({ list: () => graph.skills })` sees only the skills — so the note
 * also rides each skill's own metadata. Agent build collects it from the final
 * injection list, whichever door the skills came through, and runs the deferred
 * checks exactly once (skills found by BOTH the metadata and the graph note are
 * deduped by id).
 */
export declare const SKILL_GRAPH_DEFERRED_CONTRACT_KEY: "skillGraphDeferredBodyContract";
export interface SkillGraph {
    /** Skills with graph-derived triggers — feed to the Agent (`.skillGraph()` or
     *  `.skills({ list: () => graph.skills })`). */
    readonly skills: readonly Injection[];
    /** The declared edges (for tooling, overlays, tests). */
    readonly edges: readonly SkillEdge[];
    /** Drawn nodes: skill boxes for the flat entry/route model; predicate diamonds
     *  + skill leaves for a decision `tree`. Always present. */
    readonly nodes: readonly SkillNode[];
    /** A Mermaid flowchart of the declared graph — declared === drawn. */
    toMermaid(): string;
    /**
     * The CURSOR resolver — given an iteration context, where is the graph next?
     * Returns the skill id the graph should be *in* after this iteration:
     *   • cold start (`ctx.currentSkillId` unset) → the first matching `entry`,
     *     else the entry the model picked with `read_skill`;
     *   • a `from`-gated route whose predicate matches a result of the previous
     *     iteration's tool batch (`ctx.toolResults`, in call order; falls back to
     *     the singular `ctx.lastToolResult`) → its target;
     *   • else the model's `read_skill` pick (`ctx.pendingSkillPick`), which the
     *     runtime sets only after the reachability gate accepted it;
     *   • otherwise the current cursor unchanged (sticky stay).
     * A declared edge that fires always beats a same-turn model pick.
     * Pure + deterministic — the single source of truth shared by the compiled
     * route triggers and the agent loop's cursor-update stage, so the two can never
     * disagree. Flat entry/route graphs only; a decision `tree()` routes per-iteration
     * by predicate (no cursor) and returns the unchanged `ctx.currentSkillId`.
     *
     * **The cursor is PER RUN.** It describes where the graph is inside ONE
     * `agent.run()`, across that run's iterations. A second `run()` on the same agent
     * starts cold — at the entry — whatever skill the first run ended on: a skill graph
     * is a per-turn state machine, not conversation memory, and `currentSkillId` lives
     * in the run's own state. To resume where the last turn stopped, persist the id
     * yourself and start the next turn's graph from it (`start: { rules: [...] }`);
     * nothing in the graph will carry it across for you.
     */
    nextSkill(ctx: InjectionContext): string | undefined;
    /**
     * The same answer as `nextSkill`, plus WHICH CLAUSE produced it — the resolver
     * reporting its own reasoning instead of a consumer inferring it from the drawn
     * provenance (8.5.0). `explainNextSkill(ctx).to === nextSkill(ctx)`, always: there
     * is one resolver and `nextSkill` is a thin projection of this one, so the two can
     * never drift.
     *
     * The agent threads this into the injection engine, which stamps the result on
     * `agentfootprint.context.evaluated` as `cursorMove` — that is what lets
     * `routeRecorder()` mark a model-pick hop as a model pick instead of borrowing the
     * label of a declared edge that never fired.
     */
    explainNextSkill(ctx: InjectionContext): CursorMove;
    /**
     * The entries whose OWN `when` matched this iteration but which the cursor
     * SUPERSEDED — declaration order, ids only (8.15.0). Empty for a graph with no
     * conditional entries, for a decision `tree()`, and whenever the graph has no
     * cursor at all.
     *
     * A conditional entry is active exactly while the cursor is on it. That is a
     * suppression, and a suppression the run must not swallow: the agent threads this
     * into the injection engine, which stamps it on `agentfootprint.context.evaluated`
     * as `supersededIds`, beside the `cursorMove` that says where the graph went
     * instead. Reading the two together answers "why isn't my entry loading?" without
     * anyone having to re-run a predicate to guess.
     *
     * It rides the per-iteration event rather than `skill.reroute_superseded` on
     * purpose: this is a CONTINUOUS condition (an entry whose rule stays true while
     * the cursor is parked elsewhere is suppressed every iteration), while that event
     * means a discrete broken promise — a `read_skill` pick the gate accepted and
     * something else outranked. Per-iteration state belongs on the per-iteration event.
     *
     * Pure + deterministic. A predicate that throws is reported by the evaluator as
     * `skipped: 'predicate-threw'`, not here.
     */
    supersededEntries(ctx: InjectionContext): readonly string[];
    /**
     * The REACHABLE set — which skills the model may `read_skill`-jump to from the
     * current cursor. The agent's runtime gate rejects any `read_skill('id')` whose
     * `id` is not in this set (so the model can't leave the graph mid-run).
     *   • cold start (`currentSkillId` undefined) → the entry skills;
     *   • otherwise → the current skill's direct successors ∪ the entry skills, minus
     *     the current skill itself (deliberate "stay" is the no-tool-call ReAct stop).
     *
     * A decision `tree()` returns EMPTY (8.5.0). A tree routes by predicate on every
     * iteration and has no cursor to jump: its leaves compile to `rule` triggers, and a
     * `read_skill` call writes only `activatedInjectionIds`, which a `rule` trigger does
     * not read. Until 8.5.0 this returned all the leaves, so the gate accepted a leaf
     * pick, the tool answered "activated for the next iteration", and the leaf never
     * activated. That is the same clause 8.4.0 already applies everywhere else — a skill
     * is open only when its trigger is `llm-activated` — reaching the one set that had
     * escaped it. The escape hatch under a tree is the OPEN skills (anything registered
     * beside the graph: `.skill(x)`, `.selfExplain()`), which the agent's gate still
     * admits from any cursor.
     *
     * Pure + deterministic.
     */
    reachableSkills(currentSkillId?: string): readonly string[];
    /**
     * Score the entry candidates by relevance to the user's message — present ONLY
     * when the graph was built with `.entryByRelevance(embedder)`. Embeds
     * `ctx.userMessage` and each `when`-passing entry's `description`, cosine-scores
     * them, and softmaxes into a `relevance` share. The agent's PickEntry stage uses
     * `chosen` as the starting cursor (LLM-free, off the hot loop). Flat graphs only.
     */
    scoreEntries?(ctx: InjectionContext, signal?: AbortSignal): Promise<EntryScoring>;
    /**
     * Build-time check-up — inspect the declared graph for wiring mistakes (a skill
     * nobody can reach, an edge to an unknown skill, two un-prioritized edges from one
     * skill, no entry, a self-loop, an entry menu with no way to choose from it, a
     * transition the cold-start cursor can never take). Pure + side-effect-free; call
     * it whenever. `ok` is false iff there's an error-level problem (`unknown-skill` /
     * `no-entry`) — everything else is a warning, because a graph the model can still
     * navigate is not a broken graph.
     *
     * Pass `knownTools` when the agent registers baseline tools with `.tool()`: the
     * graph only knows the tools its own skills carry, so without them a body that
     * says `lookup_order(id)` is reported as naming a tool that exists nowhere.
     *
     * @example
     *   const report = graph.checkup({ knownTools: ['lookup_order'] });
     *   if (!report.ok) throw new Error(formatCheckup(report));
     */
    checkup(options?: CheckupOptions): GraphCheckup;
    /**
     * The turn-routing plan (SG-C) — what the agent's RouteTurn stage consumes:
     * the configured classifier (if any), the resolved tie policy, tier-1 rule
     * evaluation and the intent-candidate projection. Present on every FLAT
     * graph (a decision `tree()` routes by predicate and has no turn start to
     * route). Consumers never call it directly; `.skillGraph()` threads it.
     */
    readonly turnRouting?: TurnRoutingPlan;
    /**
     * How a turn's starting entry is chosen (SG-C) — `'scorer'`
     * (`.entryBy()`/`.entryByRelevance()`), `'model-read'` (`.entryByRead()`),
     * `'classify'` (`.classify()`), absent = the declaration-order cold walk.
     * Read by the agent mount for exactly one refusal: `strictness: 'rails'`
     * cannot honor `'model-read'` (that mode's entire entry mechanism is a
     * model pick).
     */
    readonly entrySelection?: 'scorer' | 'model-read' | 'classify';
    /**
     * The ASYNC intent audit (SG-C) — present ONLY when a classifier is
     * configured. Leave-one-out over the declared example corpus with the
     * CONFIGURED scorer (the router that will actually run — auditing with a
     * different scorer would prove nothing about production): each example is
     * scored against every intent, its own intent represented by its remaining
     * examples; a cross-match or near-tie is an `overlapping-intents` warning
     * naming the example, both intents and both numbers.
     *
     * Costs one scorer call per declared example — on `llmClassifier` that is
     * one MODEL call per example, so run it in CI, not per request. Honesty
     * boundaries ride every message: only the configured scorer's view is
     * checked, `when` predicates are opaque and never claimed checked, and
     * tier-1 rules that fire before the classifier are named as unaudited
     * shadowers. Absent without a classifier rather than answering
     * `{ ok: true }` for a question it cannot ask.
     */
    checkupIntents?(signal?: AbortSignal): Promise<GraphCheckup>;
    /**
     * Present when this graph's own build pass DEFERRED the skill-body ↔
     * tool-contract checks (`body-foreign-tool` / `body-unknown-tool`): built
     * WITHOUT `knownTools`, the graph cannot tell a typo from a baseline tool the
     * agent registers later — `lookup_order(id)` in a body and `lokup_order(id)`
     * look identical to it — so instead of reporting what it cannot prove, it
     * leaves this note and Agent build runs those checks exactly once, against the
     * agent's full tool registry. The note also rides each compiled skill's own
     * metadata ({@link SKILL_GRAPH_DEFERRED_CONTRACT_KEY}), so it is honored
     * whichever way the skills arrive — `.skillGraph(graph)` or
     * `.skills({ list: () => graph.skills })`.
     *
     * Absent when the checks already ran at graph build (`knownTools` was given —
     * the manual override stays an override) or were switched off (`check: 'off'`);
     * the agent then never re-runs them, so one problem is reported at one build
     * point, never both. `graph.checkup()` is unaffected — it always runs every
     * check over what it can see, and remains the graph-only lint surface.
     */
    readonly deferredBodyContract?: DeferredBodyContract;
}
export interface SkillGraphBuilder {
    /** Mark a skill as reachable at turn start (optionally intent-conditional). */
    entry(skill: Injection, opts?: SkillEntryOptions): SkillGraphBuilder;
    /** Declare an edge: after `from`'s work, `to` activates when the edge fires. */
    route(from: Injection, to: Injection, opts?: SkillRouteOptions): SkillGraphBuilder;
    /** Declare a decision TREE (v3): predicate nodes → skill leaves. Compiles each
     *  leaf to a path-conjunction trigger; renders as diamonds → boxes. By default
     *  each leaf is tool-scoped (`autoActivate: 'currentSkill'`) so only the routed
     *  skill's tools reach the LLM — opt out with `{ scopeTools: false }`. */
    tree(root: DecisionNode | Injection, opts?: TreeOptions): SkillGraphBuilder;
    /**
     * Pick the STARTING entry with a pluggable scorer STRATEGY — `keywordScorer()`
     * (no dependency, word overlap), `embeddingScorer(embedder)` (semantic), or your
     * own `EntryScorer`. The agent's PickEntry stage runs it ONCE per turn off the
     * hot loop and starts the cursor at the winner. Like `.entryByRead()`, this makes
     * the entries EXCLUSIVE (only the chosen one loads, token-efficient). The surfaced
     * `relevance` % powers the "Why this skill?" panel. Flat graphs only (a decision
     * `tree()` already routes by predicate). Mutually exclusive with `.entryByRead()`.
     */
    entryBy(scorer: EntryScorer): SkillGraphBuilder;
    /**
     * Sugar for `.entryBy(embeddingScorer(embedder))` — pick the starting entry by
     * SEMANTIC relevance (embed the message + each entry's `description`, cosine-score,
     * softmax → best match). LLM-free (an embedder, no extra model call), reproducible.
     * For a no-embedder router, use `.entryBy(keywordScorer())`.
     */
    entryByRelevance(embedder: Embedder): SkillGraphBuilder;
    /**
     * Let the LLM pick the STARTING entry by reading the menu — no embedder, no extra
     * model call. Like `.entryByRelevance()`, the entries become EXCLUSIVE (only the
     * chosen one loads, token-efficient), but the choice is the model's: on the first
     * turn no entry auto-loads, the agent is offered the entries via `read_skill`, and
     * its pick becomes the cursor. Use this when you have NO embedder (or embeddings
     * route poorly for your domain) — the agent's own LLM understands the request.
     * Flat graphs only; mutually exclusive with `.entryByRelevance()`.
     *
     * A `when` on an entry gates the AUTOMATIC pick only: if the model explicitly
     * picks a `when`-gated entry from the menu, it loads (8.3.0 — before, the pick
     * was accepted, reported as activated, and then silently dropped). Use `when`
     * here to say "don't route here on your own", never as a lock.
     */
    entryByRead(): SkillGraphBuilder;
    /**
     * Configure the turn-start INTENT CLASSIFIER (SG-C) — the tier-2 judge of
     * `match: { intent, examples }` entries. `classify` is the honest verb: it
     * classifies the NEW message against declared intents, where `.entryBy()`
     * means "rank descriptions and pick" — a different machine (menu-exclusive
     * winner, no floor, no stay). The entries become EXCLUSIVE (only the routed
     * one loads); the cold-start declaration-order walk is suppressed (the
     * cascade decides, or offers a menu — never the first-declared entry by
     * accident).
     *
     * The cascade, per turn: tier 1 — regex/keyword/`when` rules in declaration
     * order (binary, decisive); tier 2 — `scorer` over the declared intents,
     * judged under `policy` (defaults: `NEAR_TIE_MARGIN`/`MENU_SIZE`); tier 3 —
     * a near-tie or unmatched verdict offers a MENU through `read_skill`'s own
     * description, and the model picks (or stays). Every verdict is recorded on
     * `agentfootprint.skill.turn_routed` with the losers and the thresholds.
     *
     * `policy` here is the ONE override home for the tie policy (mirrors the
     * one-dial-one-home law `scopeTools` follows). Mutually exclusive with
     * `.entryBy()` / `.entryByRelevance()` / `.entryByRead()` — one entry
     * router per graph. Flat graphs only.
     */
    classify(scorer: IntentScorer, policy?: Partial<RoutingPolicy>): SkillGraphBuilder;
    /**
     * Declare phrasings this graph must claim **NOWHERE** — the negative form of
     * a rule's `examples`, and the one that catches the expensive failure.
     *
     * An under-triggering skill costs a turn (the model tier picks instead). An
     * OVER-triggering skill costs the answer: the wrong body enters the system
     * prompt and the wrong tools enter the tools slot, and everything said after
     * that is shaped by a skill with no business in the turn.
     *
     * It is declared on the GRAPH, not on a skill, because a phrase that must
     * route nowhere belongs to no skill: the assertion is satisfied only when
     * EVERY rule declines, and a row hung on one skill would vanish the day that
     * skill was deleted — exactly when a graph is re-partitioned, which is
     * exactly when over-triggering appears.
     *
     * `graph.checkup()` runs every declared start condition over the phrase on a
     * cold-start context and reports the rule that claims it, BY NAME, as an
     * ERROR (`never-routes-claimed`) — so a default `.build()` refuses.
     *
     * **What it proves, exactly:** no declared start RULE claims the phrase. Not
     * "no routing at all": an intent rule is judged by a classifier at run time, a
     * scorer / `.entryByRead()` menu ranks descriptions, and `read_skill` can
     * always open an open skill by name. That statement rides the report itself
     * (`checkup().notes`).
     *
     * Accepts one phrase or a list; call it as often as you like (rows
     * accumulate). A duplicate row is refused — one row already asserts it
     * against every rule.
     *
     * @example
     *   skillGraph()
     *     .entry(billing, { match: { keywords: ['refund', 'charge'] } })
     *     .neverRoutes(["what's the weather in Berlin"])
     *     .build();
     */
    neverRoutes(phrases: string | readonly string[]): SkillGraphBuilder;
    build(opts?: BuildOptions): SkillGraph;
}
export declare function skillGraph(): SkillGraphBuilder;
export declare function skillGraph(config: SkillGraphConfig): SkillGraph;
/**
 * The official vocabulary (9.51.0): you declare the **SkillMap**; the agent
 * is the **SkillWalker**; the recording carries both.
 *
 * `defineSkillMap` is a PERMANENT thin alias of {@link skillGraph} — the same
 * function object (reference-equal), same overloads, both names exported
 * forever; neither is a rename of the other. Use whichever reads better:
 * `defineSkillMap({...})` beside `defineSkill({...})` says what you are
 * doing; `skillGraph()` says what you get.
 *
 * There is deliberately NO `SkillWalker` export. The walker is not a thing
 * you construct — it is the agent itself (`.skillGraph(map)` mounts the map;
 * the agent walks it), moving the cursor by exactly three movers, every move
 * on the record as `cursorMove`:
 *
 *   • **llm**    — the model picks via `read_skill`, bounded by the gate
 *                  (`by: 'model-pick'`; refusals are `skill.rejected`);
 *   • **guard**  — your DATA decides: a `when`/`onToolReturn`/`onToolStatus`/
 *                  `guard:` edge fires, evidence recorded (`by: 'route'`,
 *                  with `cursorMove.guard` when a data guard judged it);
 *   • **linear** — no choice: a bare hand-off that fires every time its
 *                  source finishes (an `onToolReturn` edge with one target —
 *                  the degenerate guard).
 */
export declare const defineSkillMap: typeof skillGraph;
/** The declared graph a SkillWalker walks — a permanent alias of
 *  {@link SkillGraph}, exported forever beside it (9.51.0). */
export type SkillMap = SkillGraph;
/**
 * WHICH CLASS OF TARGET a `read_skill` id is, from one cursor — the ONE owner
 * of the question every gate, description and refusal used to answer for
 * itself (9.86.0).
 *
 * ── THE LAW ───────────────────────────────────────────────────────────────
 *
 *   THE CURSOR IS A LEGITIMATE TARGET OF A READ (a stay), NEVER OF A MOVE.
 *
 * {@link makeReachableSkills} filters the cursor out of its own successor set,
 * and that is correct for a MOVE: there is nowhere to move to. It was then read
 * by five different consumers as "not available", which is a different claim and
 * a false one — the cursor's body is in the prompt and its tools are on the wire.
 * Each consumer that noticed wrote its own `requested === cursor` check; the two
 * that did not (the tool-effects `propose-transition` judge and the `skill_read`
 * permission gate) refused a stay as if it were a move.
 *
 * Four classes, in the order the gate must consider them:
 *
 *   • `'self'`        — the cursor itself. A stay: nothing activates, nothing
 *                       moves, nothing is read that was not already sent.
 *   • `'hop'`         — a declared successor of the cursor. The one class that
 *                       MOVES the cursor.
 *   • `'open'`        — a skill the graph wires no edge into (a `.selfExplain()`
 *                       skill, a `.skill()` registered beside the graph). It
 *                       activates and never moves the cursor.
 *   • `'unreachable'` — everything else.
 *
 * `'hop'` beats `'open'` when an id is in both sets: the move is the stronger
 * fact, and a caller that reports the id as merely open would lose it.
 *
 * Pure and total: no cursor (cold start) simply means nothing can be `'self'`.
 */
export type SkillTargetClass = 'self' | 'hop' | 'open' | 'unreachable';
/**
 * Classify one `read_skill` target against one cursor. See
 * {@link SkillTargetClass} for the law this function owns.
 *
 * @example
 * ```ts
 * classifySkillTarget({ cursor: 'billing', target: 'billing' });               // 'self'
 * classifySkillTarget({ cursor: 'billing', target: 'refunds', hops: ['refunds'] }); // 'hop'
 * classifySkillTarget({ cursor: 'billing', target: 'debug', open: ['debug'] });     // 'open'
 * classifySkillTarget({ cursor: 'billing', target: 'vault' });                 // 'unreachable'
 * ```
 */
export declare function classifySkillTarget(args: {
    /** Where the cursor stands. `undefined` = cold start (nothing is a stay). */
    readonly cursor?: string;
    /** The id the model asked for. */
    readonly target: string;
    /** Declared successors of the cursor — `reachableSkills(cursor)`. */
    readonly hops?: readonly string[];
    /** Skills the graph wires no edge into. */
    readonly open?: readonly string[];
}): SkillTargetClass;
//# sourceMappingURL=skillGraph.d.ts.map