/** * defineSkill — sugar for LLM-activated Injections that target both * system-prompt + tools. * * A Skill is a bundle of (1) a body of guidance and (2) optionally some * tools. The LLM decides when a Skill is needed by calling a designated * activation tool — by default `read_skill()`. * * Activation is about the BODY. A Skill's tools are registered up front * and callable from iteration 1 unless the Skill sets * `autoActivate: 'currentSkill'` — see that option. * * **Activation lifetime: the rest of the turn.** Once the model picks a * skill with `read_skill`, the activation lasts until the turn ends — every * later iteration of that `run()` keeps the body active, and there is no * mid-turn deactivation (nothing removes an id from * `ctx.activatedInjectionIds`; the next `run()` starts clean). This is * deliberate: the cache layer's skill history and the context ledger both * lean on the cumulative property. A skill that should stop applying * mid-turn is a `rule` trigger or a `skillGraph()` route, not a read_skill * pick. * * Produces an `Injection` with: * - flavor: `'skill'` * - trigger: `{ kind: 'llm-activated', viaToolName: 'read_skill' }` * - inject: `{ systemPrompt: body, tools }` * * The Agent integration auto-attaches the `read_skill` tool when one * or more Skills are present. When the LLM calls * `read_skill('billing')`, the engine adds `'billing'` to * `ctx.activatedInjectionIds`; the next iteration's evaluator * matches this Skill's `id`, activates it, and the body lands in the * slot subflows (plus the tools, for an `autoActivate` Skill — every * other Skill's tools were already there). * * @example * const billingSkill = defineSkill({ * id: 'billing', * description: 'Use for refunds, charges, billing questions.', * body: 'When handling billing: confirm identity first, then…', * tools: [refundTool, chargeHistoryTool], * }); */ import type { Injection } from '../types.js'; import type { Tool } from '../../../core/tools.js'; import type { CachePolicy } from '../../../cache/types.js'; import { type OnSkipPolicy, type SkillStep } from '../skillSteps.js'; /** * Where the Skill's body lands when activated. * * Delivery reads the mode you DECLARED, literally — `buildSystemPromptSlot` * decides the system slot, the `read_skill` tool decides its own result: * * - `'system-prompt'` — body appended to the system slot on the * iteration after activation; the `read_skill` result is a one-line * confirmation. Best on Claude ≥ 3.5 (training-time adherence to * system-prompt instructions is strong). * - `'tool-only'` — body SUPPRESSED from the system slot and returned as * the `read_skill` tool result instead. Recency-first by protocol; * doesn't rely on the model's training to honor system-prompt * anchoring. Legal only on a Skill that `read_skill` really activates — * a Skill a skill graph routes to is refused at build time, because the * tool call that would carry the body never happens (`skillBodyDelivery.ts`). * - `'both'` — body lands in the system slot AND in the tool result. * Belt-and-suspenders for high-stakes Skills on long-context runs. * - `'auto'` (the default) — delivered exactly like `'system-prompt'`: * body in the system slot, tool result is a confirmation. It is NOT * resolved per provider on the delivery path. * * `resolveSurfaceMode(provider, model)` — Claude ≥ 3.5 → `'both'`, else * `'tool-only'` — is the per-provider RECOMMENDATION, and it runs only where * something asks for it: `SkillRegistry.resolveForSkill(...)` (the skill → * registry → provider cascade) and `resolvedSurfaceModeOf(skill, provider, * model)`. Nothing on the delivery path calls it, so feed its answer back in * as an explicit `surfaceMode` if you want it honored. */ export type SurfaceMode = 'auto' | 'system-prompt' | 'tool-only' | 'both'; /** * When (if ever) to re-deliver a Skill's body in long-running runs. * * Even on providers with strong system-prompt adherence, attention to * the system slot decays past long contexts. `refreshPolicy` was declared * to re-inject the body via tool result past a token threshold so the LLM * sees it fresh again. * * @deprecated **DEPRECATED-pending-steps (9.16.0) — stored, never read, and * it will stay that way.** `defineSkill` records what you pass on * `skill.metadata.refreshPolicy` and nothing in the engine has ever read it: * no re-injection happens today, on any version. The hook is superseded by a * planned steps-as-data feature, which will own re-delivery declaratively — * this field will NOT be wired up in the meantime, and will be removed in the * next major after steps ship. The field stays accepted (additive-only law) * so existing declarations keep compiling; dev mode warns once per process * when one is set. If you need a body re-surfaced in a long run today, * deliver it yourself (e.g. `surfaceMode: 'both'`, so every `read_skill` * call returns the body afresh). */ export interface RefreshPolicy { /** * Re-inject the Skill body once the run has consumed this many input * tokens since the Skill was last surfaced. Recommended: 50_000 for * 200k-context models; 20_000 for 32k-context models. */ readonly afterTokens: number; /** * How to re-inject. `'tool-result'` synthesizes a fresh tool result * carrying the body text (recency-first). Other modes reserved. */ readonly via: 'tool-result'; } export interface DefineSkillOptions { readonly id: string; /** Visible to the LLM via the activation tool's description. */ readonly description: string; /** Body appended to the system-prompt slot once activated. */ readonly body: string; /** Tools this Skill contributes. **By default they are added to the agent's tool * registry at build time and are visible to the model from the first iteration, * whether or not the Skill is ever activated** — activation adds the Skill's * body, not its tools. To make the tools appear only while the Skill is active, * set `autoActivate: 'currentSkill'`; `skillGraph().tree()` sets it for you on * every leaf. If a tool must never be offered before activation, that is not a * default — say so with `autoActivate`. */ readonly tools?: readonly Tool[]; /** * Where the body lands when activated. See `SurfaceMode`. Default * `'auto'`, which delivers like `'system-prompt'`; name a mode * explicitly to get the other channels. */ readonly surfaceMode?: SurfaceMode; /** * Intent to re-deliver the body past a token threshold, to defend * against long-context attention decay. Default: undefined. * * @deprecated DEPRECATED-pending-steps (9.16.0): recorded on the Skill's * metadata, never acted on by the engine, and superseded by a planned * steps-as-data feature — it will not be wired up. Dev mode warns once per * process when set. See `RefreshPolicy` for what to do instead today. */ readonly refreshPolicy?: RefreshPolicy; /** * Per-skill tool gating — the field that makes this Skill's `tools` * appear only while the Skill is active. * * - `'currentSkill'` — this Skill's `tools` are held out of the agent's * static tool list and offered to the model only on iterations where * the Skill is active. Outside the Agent's own wiring, materialize the * same gate with `skillScopedTools(id, tools)` from * `agentfootprint/providers`. * - `undefined` (default) — additive: this Skill's tools go into the * agent's registry at BUILD time and the model can see and call them * from iteration 1, activated or not. * * Saying it once for the whole agent instead of once per skill: * `.toolsFromActiveSkill()` on the builder (9.36.0) stamps this field on * every tool-carrying skill, and `skillGraph({ scopeTools: true })` stamps it * on the skills a graph wires. Both are DEFAULTS — a skill that declared its * own keeps it — and since `'currentSkill'` is the only legal value, none of * the three can contradict another. * * Wired at runtime since v2.5: `buildToolRegistry` holds these tools out of the * static registry and `buildToolsSlot` readmits them per-iteration from the * active injections. Dispatch is unaffected either way — an autoActivate tool * stays callable by name once activated. Read `skill.metadata.autoActivate` if * you compose your own ToolProvider. */ readonly autoActivate?: AutoActivateMode; /** * The procedure, as data (9.18.0). An ordered list of `{ tool, note }` * pairs naming this skill's OWN tools (a step naming a tool the skill * does not carry is refused here, where both arrive together). * * While this skill holds the tenure, the framework owns sequence and * scope at the protocol level: the tools slot offers the CURRENT step's * tool (its description led by `[Step k of n — ]`) plus `skip_step` * — and every escape hatch stays offered (`read_skill`, other active * skills' tools, the baseline `.tool()` registry, provider tools), so an * input the author never imagined still has the whole normal surface. * The model owns judgment inside a step: run it, skip it with a recorded * reason, work around it, or stop and say why. * * Absent → this skill is byte-identical to today (zero-cost-when-unused: * no tool, no scope key, no event, no slot change). * * Steps are TURN-scoped: the pointer resets on every cursor move and on * every new run. Under a graph's `continuity: 'conversation'` the CURSOR * carries across turns and the re-tenured skill starts at step 1 on the * continued turn — the pointer is subordinate to the cursor, made * visible. (The prior turn's completed step results are still in the * restored history; the record is not lost, the pointer is fresh.) */ readonly steps?: readonly SkillStep[]; /** * What the framework does when the model skips a step with `skip_step` * (9.18.0): `'advance'` (default) — record the skip and move to the next * step; `'hold'` — record the skip and keep the step current (its tool * stays the offer; the model may retry it, use an escape hatch, or finish * and explain). Legal only beside `steps`. */ readonly onSkip?: OnSkipPolicy; /** * Artifact KINDS this skill leaves behind (9.25.0) — the producer half of * its data vocabulary: `produces: ['chart/spec']`. * * A DECLARATION, not machinery. Nothing at run time reads it, nothing is * enforced at dispatch (that is `wants`' job, against the live store), and * a skill that declares none is byte-identical to one that never heard of * them. What it buys: the build-time check that a consumer's kind has a * producer somewhere (`artifact-kind-unsatisfied`), and a fact on this * skill's metadata that a lens can draw — the data legs of a run, before * the run. */ readonly produces?: readonly string[]; /** * Artifact KINDS this skill needs to have arrived (9.25.0): * `consumes: ['dataset/rows']` — "somebody upstream made a dataset; this * skill turns it into something". * * Checked at build against what the agent declares it produces. The check * is deliberately weak-but-true: it warns only when NOTHING on the agent * declares that kind, and it never fires when one of this skill's tools * declares `wants` for it (that ref is redeemed at dispatch from a store * that outlives the turn, so the kind can legitimately arrive from another * agent or an earlier run). See `skillVocabulary.ts` for the whole rule and * everything it cannot see. */ readonly consumes?: readonly string[]; /** * This skill's BRAIN (9.19.0) — "the cursor picks the brain": while a * mounted skill graph's cursor is on this skill, `callLLM` runs on this * provider instead of the agent's. Any `LLMProvider` port implementation; * vendor-neutral by construction. A provider whose `name` differs from * the agent's MUST also name `model` (the agent's model id belongs to * another vendor's namespace — refused at `Agent.build()` otherwise). * Legal only on agents that mount a graph; the same skill id may also be * declared in `skillGraph(graph, { providers })` — same choice is fine, * different choices are refused naming both homes. Absent → this skill is * byte-identical to today. */ readonly provider?: import('../../../adapters/types.js').LLMProvider; /** * The model this skill's calls run on (9.19.0). Legal ALONE — the agent's * own provider, another model ("triage runs on the small one") — or * beside `provider`. Absent with `provider` set: inherits down the * precedence chain (escalation > skill brain > `.configure()` > * build-time default), which is legal only while the provider is the * agent's own. */ readonly model?: string; /** * Cache policy for this skill's body. Defaults to `'while-active'` — * the body caches while the skill is in `activeInjections[]` (i.e., * while it's the most-recently-activated skill); invalidates the * moment it deactivates. * * For skills with stable, frequently-accessed bodies, consider * `'always'` to keep the body cached even when temporarily inactive. * For skills with bodies that depend on per-iter state, use * `'never'` or `{ until: ... }`. * * See `CachePolicy` in `agentfootprint/src/cache/types.ts`. */ readonly cache?: CachePolicy; } /** * Per-skill tool gating mode. See `DefineSkillOptions.autoActivate`. * * Reserved future values: `'always'` (always show this Skill's tools * regardless of activation), `'group'` (gate by a named skill group). */ export type AutoActivateMode = 'currentSkill'; /** * Resolve `surfaceMode: 'auto'` to a concrete mode based on provider * + model. The defaults match the per-provider attention profile * documented in the Skills, explained essay: * * - Claude >= 3.5 → 'both' (cheap to cache, high adherence) * - Claude pre-3.5 → 'tool-only' (recency-first more reliable) * - OpenAI / Bedrock / Ollama / Mock / unknown → 'tool-only' * * Pure function — no side effects. Consumers can call directly to * inspect what `'auto'` will resolve to in their stack. */ export declare function resolveSurfaceMode(provider: string, model?: string): SurfaceMode; export declare function defineSkill(opts: DefineSkillOptions): Injection;