/** * 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 — `nextSkill(ctx)` * (see `makeNextSkill`) — 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. Scoped `read_skill` (bounding the * model-reachable set by graph position) remains deferred — see proposal 002. */ 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'; export type { GraphCheckup, GraphProblem, GraphProblemCode } from './skillGraphCheckup.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 `'warn'`. `graph.checkup()` is always available regardless. */ readonly check?: GraphCheckMode; } /** * 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). */ export interface SkillGraphConfig { /** Every skill in the graph (wired or not). */ readonly skills: readonly Injection[]; /** Where a turn starts. Omit when using `tree`. */ readonly start?: string | { readonly use: string; } | { readonly rules: ReadonlyArray<{ readonly when: (ctx: InjectionContext) => boolean; readonly use: string; }>; } | { 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; }; /** Tool-result transitions; `from`/`to` are skill ids resolved against `skills`. */ readonly steps?: ReadonlyArray<{ readonly from: string; readonly to: string; readonly when?: SkillRouteOptions['when']; readonly onToolReturn?: string | RegExp; readonly label?: string; }>; /** A decision tree (instead of `start` + `steps`). */ readonly tree?: DecisionNode | Injection; readonly check?: GraphCheckMode; } 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. */ readonly when?: (result: { readonly toolName: string; readonly result: string; }) => 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; /** 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 { /** Predicate on the iteration context (e.g. `ctx.userMessage`). Omit → the * skill is always active (a persistent base procedure). */ readonly when?: (ctx: InjectionContext) => boolean; 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` stays available as * the escape hatch to reach another skill mid-run. * * 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' | '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; } /** * 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; /** 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 metadata key carrying a skill's routing provenance. */ export declare const SKILL_GRAPH_METADATA_KEY: "skillGraph"; 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`; * • a `from`-gated route whose predicate matches `ctx.lastToolResult` → its target; * • otherwise the current cursor unchanged (sticky stay). * 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`. */ nextSkill(ctx: InjectionContext): string | undefined; /** * 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). * Pure + deterministic. A decision `tree()` has no cursor, so it returns ALL leaf * skills — `read_skill` stays a full escape hatch there. */ 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). Pure + side-effect-free; call it whenever. * `ok` is false iff there's an error-level problem (`unknown-skill` / `no-entry`). */ checkup(): GraphCheckup; } 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()`. * * Caveat: prefer UNCONDITIONAL entries here. A `when`-gated entry may still be * offered in the read_skill menu (the cold-start gate can't evaluate `when`), but * if the model picks it while its `when` is false it won't load — the turn wastes * an iteration with no skill. For intent-gating, use `.entryByRelevance()` or plain * `.entry(s, { when })` (v1 always-on) instead. */ entryByRead(): SkillGraphBuilder; build(opts?: BuildOptions): SkillGraph; } export declare function skillGraph(): SkillGraphBuilder; export declare function skillGraph(config: SkillGraphConfig): SkillGraph; //# sourceMappingURL=skillGraph.d.ts.map