agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
169 lines • 9.03 kB
TypeScript
/**
* 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/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; supplied advisory relevance %s),
* names the cursor, and states STAY as a first-class
* option ("answer without calling read_skill to stay in '<cursor>'").
*
* 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.
* Descriptions appear once per skill id in this offer. Later catalog or
* reachability rows retain the id and refer to its description above.
*/
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<Record<string, never>, 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;
//# sourceMappingURL=skillToolDescriptors.d.ts.map