/** * skillToolDescriptors — the graph DESCRIBING the tools it needs (9.34.0). * * `read_skill` is not an ordinary tool. It is how the model asks to move the * skill-graph cursor: its enum is the catalog, its description is the offer * (what is reachable from here, what will be refused, what this turn's menu * proposes), and its result is what the model reads about the skill it just * activated. Every one of those sentences is the GRAPH's to write. * * What the graph must NOT do is construct a framework's tool object. That is * why this module produces {@link SkillToolDescriptor} POJOs — name, * description, input schema, a pure `execute` — and `skillTools.ts` (the host * adapter) is the one line that turns each into an agentfootprint `Tool` with * `defineTool`. A host on another framework calls the same functions and * wraps the result in its own tool type instead. * * ONE catalog, ONE grammar: the Agent's auto-attach path, `SkillRegistry` * and any foreign host all read the descriptions from here, so they cannot * drift the way a second hand-written `read_skill` would. * * Zone: PURE CORE. Pinned by * `test/lib/injection-engine/skill-graph-fence.test.ts`. */ import type { SkillToolDescriptor } from './hostContract.js'; import type { Injection } from './types.js'; /** * What the gate will ACTUALLY grant right now — the ids `read_skill` should be * offering, as opposed to the ids it can dispatch (8.5.0). * * Without this the tool enumerated every registered skill while the skill-graph gate * admitted only `reachableSkills(cursor) ∪ open`, so a route target the cursor cannot * reach was advertised on every iteration and refused on every call. The model was * being asked to choose from a menu the library knew it would reject. */ export interface ReadSkillOffer { /** * Ids the gate will grant from the current cursor. Omit when there is no * graph — then the description is the plain catalog it has always been, * minus anything {@link ReadSkillOffer.hiddenIds} removes. */ readonly grantable?: readonly string[]; /** Say the rest are refusable rather than hiding them. Hiding a skill it can see * in `list_skills` (and in the enum) would just move the confusion; naming the * boundary lets the model route around it in one step instead of guessing. */ readonly showRefusable?: boolean; /** * Ids removed from the description ENTIRELY — not listed as reachable, not * listed as refusable, not named at all (9.11.0). * * The opposite treatment from the graph's `shut` set above, and deliberately * so. A graph refusal is about WHERE THE CURSOR IS: the skill exists for this * caller, it is simply not reachable from here, so naming it lets the model * route to it in one step. A hidden skill is about WHO IS ASKING: no cursor * move makes it available, and naming it would tell a role about a capability * it will never be allowed to use — leaking the shape of somebody else's * permissions into this model's prompt. * * The ENUM keeps the full catalog either way (see below). Narrowing it would * turn a policy refusal into a generic schema error, and the model would * never read the policy's own message. */ readonly hiddenIds?: readonly string[]; /** * WHERE THE CURSOR STANDS — the skill the model is already IN (9.84.0). * * Passed on every call that has a cursor, menu or no menu. It does two jobs * the offer could not do before: it NAMES the current skill in the * description (previously only the menu's stay clause ever did, and a * decisively-routed turn has no menu), and it keeps that skill out of the * "Not reachable from here" list — where it appeared purely as an artefact of * being filtered out of its own successor set. * * {@link ReadSkillOffer.menu}'s `cursorId` is the same value and still * honoured; this one is simply not conditional on a menu being outstanding. * * It is NOT exempt from {@link ReadSkillOffer.hiddenIds}. A cursor the * caller's role may not see is not named — see the filter in `describeOffer`. */ readonly cursorId?: string; /** * Turn-start MENU (SG-C): when set, the description LEADS with these * candidates (id + one-line description; advisory relevance %s beside * near-tie candidates), names the cursor, and states STAY as a first-class * option ("answer without calling read_skill to stay in ''"). * * Set only while the turn's menu verdict is outstanding (turn start, before * any accepted pick) — the tools slot reads it off the SAME verdict the * record carries, so the offer can never drift from `turn_routed.offered`. * The enum stays the full catalog (the 8.5.0 law below — narrowing it would * retire four honesty mechanisms), and {@link ReadSkillOffer.hiddenIds} * filters menu rows exactly as it filters every other list: a role-hidden * skill is never NAMED, menu or no menu. */ readonly menu?: { readonly candidates: ReadonlyArray<{ readonly id: string; readonly relevance?: number; }>; /** Names the cursor the STAY sentence refers to ("you are in billing"). */ readonly cursorId?: string; /** STAY spelled out as a first-class option (mid-conversation menus). */ readonly stay?: boolean; }; /** * The mounted graph is a decision `tree()` (9.86.0). * * A tree routes by predicate on every iteration and keeps no cursor, so * `reachableSkills()` is empty from every position and EVERY routing pick is * refused by construction. Offering `read_skill` anyway asked the model to * choose from a menu the library knew it would reject — the same defect * 8.5.0 fixed for the flat graph's unreachable ids, left standing for the * one shape where it is total. * * With no open skill left after {@link ReadSkillOffer.hiddenIds}, the tool is * not offered at all ({@link readSkillDescriptor} returns `undefined`); with * open skills, the description says a tree has no cursor to move and names * exactly the skills a pick CAN open. Either way the name stays dispatchable * — position governs the offer, never dispatch — so a model that calls it * from a restored transcript still reads the gate's own refusal. */ readonly treeRouted?: boolean; } /** * Describe the `list_skills` tool — a no-arg tool that returns the registered * skills as `{ id, description }[]`. Lets the LLM discover skills without * paying the prompt-token cost of embedding the catalog into every system * prompt. * * Pairs with {@link readSkillDescriptor} (which actually activates a skill by * id). Returns `undefined` when there are no skills — a tool offering an * empty catalog is noise. */ export declare function listSkillsDescriptor(skills: readonly Injection[]): SkillToolDescriptor, string> | undefined; /** * Describe the `read_skill` tool — THE cursor door. The LLM picks WHICH skill * via the `id` argument; the host's loop is what turns an accepted pick into a * cursor move (see `SkillGraphHost`, obligations 2 and 3). * * `execute` returns a confirmation string, or the skill body verbatim for the * `'tool-only'` / `'both'` surface modes (recency-first delivery). The actual * bookkeeping — appending the requested id to the activated set, gating it * against `reachableSkills`, publishing the accepted pick — is the HOST's, * because only the host sees the tool call. * * Pass `offer` to scope the DESCRIPTION to what the graph's gate will actually * grant from the current cursor (8.5.0); the enum stays the full catalog * either way. Omit it and the description is byte-identical to the plain * catalog it has always been. * * Returns `undefined` when there are no skills. */ export declare function readSkillDescriptor(skills: readonly Injection[], offer?: ReadSkillOffer): SkillToolDescriptor<{ id: string; }, string> | undefined; /** * What `read_skill` answers when it activated a skill but did NOT carry its body * (surface modes `'system-prompt'` / `'auto'` — the body lands via the system * slot next iteration). * * Exported because the skill-graph gate has to tell this sentence apart from a * body: a SELF-CALL keeps the body (the model asked to re-read where it is, and * under `'tool-only'` the tool result is the only place that body ever appears) * but must not keep a promise of an activation that is not pending. Comparing * against the one owner of the string beats guessing at its shape. */ export declare function skillActivationConfirmation(id: string): string;