/** * 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 './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` — today's additive behavior (10.0.0 flips the default to `true`). * 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; /** 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; } | { 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); * 10.0.0 flips the default to `true`. 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; /** * 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; /** 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; /** * 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; /** * 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): 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;