/** * skillBrains — per-skill model switching (9.19.0). ONE owner of the brains * fold: the declared shapes (`ProviderChoice`, `EscalationPolicy`), the * build-time fold + check-up over BOTH declaration homes, and the runtime * `brainFor` consult callLLM makes. * * "THE CURSOR PICKS THE BRAIN." A skill graph already decides WHERE the run * is (the one cursor); this feature lets that same position decide WHO * answers — a cheap triage skill on a small model, the refund skill on the * strong one — with zero new stages: `callLLM` consults `brainFor(cursor, * escalated)` once at the top and resolves field by field down the stated * precedence chain: * * **escalation > per-skill brain > `.configure()` `resolvedModel` > * build-time default** — the more specific context wins (evidence > node * > run > build). Field-by-field at the call site: `provider = * brain?.provider ?? deps.provider`; `model = brain?.model ?? * (runConfigured ? scope.resolvedModel : undefined) ?? deps.model`; * `cacheStrategy = brain?.cacheStrategy ?? deps.cacheStrategy`. A brain * naming only a MODEL inherits the agent's provider; a brain naming only a * foreign PROVIDER may not inherit the agent's model id (that id belongs * to another vendor's namespace and would fail mid-turn, on exactly the * iteration the cursor enters the skill) — refused at build instead. * * TWO declaration homes, one meaning: `defineSkill({ provider, model })` * keeps the choice beside the skill it serves; `SkillGraphOptions.providers` * keeps a fleet's choices in one place at the mount. The same id declared in * both with different choices is refused naming both homes — configuration * that disagrees with itself must not pick a silent winner. * * ESCALATE-ON-EVIDENCE. `escalation: { provider, model?, afterRefusals: N }` * — when the gate refuses N routing picks in ONE turn (`skill.rejected` — * reachability, posture, OR a self-call: all three refusal arms count, and * they are real recorded refusals, never vibes), the rest * of the turn runs on the escalation brain, `skill.escalated` goes on the * record once, and the next turn's seed de-escalates. The loop the model is * flubbing gets the bigger brain until the turn resolves. * * Vendor-neutral by construction: a brain is an `LLMProvider` OBJECT (any * port implementation), never a vendor name this library resolves. */ import type { LLMProvider } from '../../adapters/types.js'; import type { CacheStrategy } from '../../cache/types.js'; import type { Injection } from '../../lib/injection-engine/types.js'; /** One brain: a provider port, optionally pinned to a model. `model` absent * → resolved down the precedence chain (legal only while the provider is * the agent's own — see the module header for why a foreign provider must * name its model). */ export interface ProviderChoice { readonly provider: LLMProvider; readonly model?: string; } /** Escalate-on-evidence policy (see the module header). */ export interface EscalationPolicy extends ProviderChoice { /** Gate refusals (`skill.rejected`) in ONE turn that flip the rest of the * turn onto this brain. Integer ≥ 1. * * ALL THREE refusal arms count — an unreachable pick, a pick a `strictness` * posture declined, and a SELF-CALL (`read_skill` naming the cursor's own * skill). The self-call arm composes a notice rather than a refusal, and it * still counts here on purpose: what this budget measures is a model asking * the graph where it stands instead of working, which is the same stuck loop * whichever arm answered it. */ readonly afterRefusals: number; } /** A brain as the fold stores it: `defineSkill({ model })` alone is legal * (the agent's own provider, another model), so `provider` is optional * HERE while the declared `ProviderChoice` requires it. */ export interface SkillBrainDecl { readonly provider?: LLMProvider; readonly model?: string; } /** The two per-skill declaration homes, folded — plus the out-of-band * policies. Produced by {@link foldSkillBrains} at `Agent.build()`. */ export interface FoldedSkillBrains { /** Per-skill brains, keyed by skill id (both homes merged, conflicts * refused). Empty map = no per-skill brain anywhere. */ readonly bySkill: ReadonlyMap; readonly escalation?: EscalationPolicy; /** Tier-3 decider — consumed by the RouteTurn stage, never by callLLM. */ readonly decider?: ProviderChoice; } /** What one `brainFor` consult resolves to — the winning rung, with the * cache strategy that provider needs (cache markers are provider-aware). * `provider`/`model` absent = that field falls to the next rung of the * precedence chain at the call site. */ export interface ResolvedBrain { readonly provider?: LLMProvider; readonly model?: string; readonly cacheStrategy: CacheStrategy; /** WHY this brain won — stamped on `llm_start.brain` so the record says * which rung answered, not just which model. */ readonly via: 'skill' | 'escalation'; /** The tenure that picked it (via `'skill'` only). */ readonly skillId?: string; } /** The consult callLLM makes once at the top of the stage, with the * ADVANCED cursor and the run's escalation flag. Undefined result = no * brain rung won; the agent's own configuration resolves as always. */ export type BrainFor = (activeCursor: string | undefined, escalated: boolean) => ResolvedBrain | undefined; /** The per-skill brain declared on `defineSkill({ provider, model })`, * read off the metadata bag (the engine-ignores-unknown precedent — * `projectActiveInjection` never projects it, so the live provider object * stays on the closure-held list and never crosses a scope boundary). */ export declare function skillBrainOf(injection: Injection): SkillBrainDecl | undefined; /** Inputs to the fold — everything the check-up judges against. */ export interface FoldSkillBrainsArgs { /** The FINAL injection list (every declaration home has landed). */ readonly injections: readonly Injection[]; /** The mount options' three fields, verbatim. */ readonly providers?: Readonly>; readonly escalation?: EscalationPolicy; readonly decider?: ProviderChoice; /** Whether a skill graph is mounted at all (`.skillGraph()` ran). */ readonly graphMounted: boolean; /** The mounted graph's node ids — undefined when the graph object carries * no `nodes` (a structurally-typed graph built before they existed); * brains cannot be validated against such a graph and are refused. */ readonly nodeIds?: ReadonlySet; /** The agent's own provider name — the foreign-provider check's anchor. */ readonly agentProviderName: string; } /** * Fold both declaration homes into one frozen map, running every build-time * refusal (the check-up). Returns `undefined` when nothing was declared — * the zero-cost gate every caller branches on. */ export declare function foldSkillBrains(args: FoldSkillBrainsArgs): FoldedSkillBrains | undefined; /** Per-brain cache strategies, resolved ONCE at chart build (the brain set * is static; cache markers are provider-aware). A brain on the agent's own * provider keeps the agent's strategy — including an explicit override — * rather than re-resolving the default behind the caller's back. */ export interface BuildBrainForArgs { readonly brains: FoldedSkillBrains; readonly agentProviderName: string; readonly agentCacheStrategy: CacheStrategy; } /** Build the runtime consult. See {@link BrainFor}. */ export declare function buildBrainFor(args: BuildBrainForArgs): BrainFor; /** Describe the brain that WOULD serve a cursor pre-escalation — the * `skill.escalated` event's honest `from` field, resolved by the same * precedence chain callLLM applies (skill brain > configured > default). */ export declare function describeServingBrain(args: { readonly brains: FoldedSkillBrains; readonly cursor: string | undefined; readonly agentProviderName: string; readonly defaultModel: string; readonly resolvedModel?: string; }): { readonly provider: string; readonly model: string; };