agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
159 lines (158 loc) • 8.73 kB
TypeScript
/**
* 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 '../../provider-contract/types.js';
import type { CacheStrategy } from '../../cache/types.js';
import type { Injection } from '../../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<string, SkillBrainDecl>;
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<Record<string, ProviderChoice>>;
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<string>;
/** 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 — the same provider OBJECT — keeps the agent's strategy,
* including an explicit override, rather than re-resolving behind the
* caller's back; any other provider gets the strategy its own declared
* `promptCaching` selects (`cacheStrategyFor`), never one looked up by name. */
export interface BuildBrainForArgs {
readonly brains: FoldedSkillBrains;
readonly agentProvider: LLMProvider;
readonly agentCacheStrategy: CacheStrategy;
readonly agentModel?: string;
/** A host-supplied strategy remains authoritative for this provider,
* including model-only brains. Never inspect its unused declaration. */
readonly agentCacheStrategyIsOverride?: true;
}
/** 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;
};