agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
183 lines • 7.77 kB
JavaScript
/**
* SkillRegistry — centralized governance for Skills across one or
* more Agents.
*
* Most apps register Skills directly on the Agent via `.skill(s)`.
* Once you have multiple Agents that share a common skill catalog
* (a customer-support pool, a research team, a multi-step workflow
* where each step uses different overlapping skills), hand-syncing
* `.skill()` calls per Agent gets fragile.
*
* `SkillRegistry` is the answer: register once, attach to many
* Agents via `.skills(registry)`. Add a Skill to the registry and
* every consumer Agent picks it up at next build.
*
* @example
* const registry = new SkillRegistry();
* registry.register(billingSkill);
* registry.register(refundSkill);
*
* const supportAgent = Agent.create({ provider }).skills(registry).build();
* const escalationAgent = Agent.create({ provider }).skills(registry).build();
*
* // Add a new skill — both agents pick it up at next build.
* registry.register(complianceSkill);
*/
import { buildListSkillsTool, buildReadSkillTool } from './skillTools.js';
import { resolveSurfaceMode } from './factories/defineSkill.js';
export class SkillRegistry {
skills = new Map();
opts;
/**
* Construct an empty registry. Optional `{ surfaceMode, providerHint }`
* fields set registry-level defaults; absent both, the registry is a
* pure container (the v2.4 surface).
*/
constructor(opts = {}) {
this.opts = Object.freeze({ ...opts });
}
/** Registry-level default `surfaceMode`, or `undefined` if unset. */
get surfaceMode() {
return this.opts.surfaceMode;
}
/** Registry-level provider hint, or `undefined` if unset. */
get providerHint() {
return this.opts.providerHint;
}
/**
* Register a skill. Throws if `skill.flavor !== 'skill'` or if a
* skill with the same id is already registered (use `.replace(...)`
* to overwrite intentionally).
*/
register(skill) {
if (skill.flavor !== 'skill') {
throw new Error(`SkillRegistry.register: expected a Skill (flavor: 'skill'), got flavor: '${skill.flavor}' (id: '${skill.id}'). Use defineSkill(...) to construct.`);
}
if (this.skills.has(skill.id)) {
throw new Error(`SkillRegistry.register: skill '${skill.id}' is already registered. Use .replace('${skill.id}', skill) to overwrite.`);
}
this.skills.set(skill.id, skill);
return this;
}
/** Replace an existing skill by id. Throws if id is not registered. */
replace(id, skill) {
if (!this.skills.has(id)) {
throw new Error(`SkillRegistry.replace: no skill registered at '${id}'. Use .register(...) for new skills.`);
}
if (skill.flavor !== 'skill') {
throw new Error(`SkillRegistry.replace: expected a Skill (flavor: 'skill'), got '${skill.flavor}'.`);
}
if (skill.id !== id) {
throw new Error(`SkillRegistry.replace: skill.id ('${skill.id}') does not match the slot id ('${id}'). Either use .register(...) for the new id or pass a skill with the matching id.`);
}
this.skills.set(id, skill);
return this;
}
/** Remove a skill by id. No-op if not registered. */
unregister(id) {
this.skills.delete(id);
return this;
}
/** Look up by id. Returns undefined if not registered. */
get(id) {
return this.skills.get(id);
}
/** True iff a skill with the given id is registered. */
has(id) {
return this.skills.has(id);
}
/** All registered skills. Order matches registration order. */
list() {
return [...this.skills.values()];
}
/** Number of registered skills. */
get size() {
return this.skills.size;
}
/** Drop all registrations. */
clear() {
this.skills.clear();
}
/**
* Materialize the LLM-facing skill discovery tools from the current
* registry contents. Returns `{ listSkills, readSkill }`:
*
* - `list_skills` — no-arg tool the LLM calls to enumerate
* `{ id, description }` for every registered skill. Lets the
* LLM discover skills without paying the prompt-token cost of
* a static catalog in the system prompt.
*
* - `read_skill({ id })` — activates the named skill for the
* NEXT iteration. The Agent's tool-calls subflow inspects this
* tool call by name and updates `scope.activatedInjectionIds`
* so the InjectionEngine on iter N+1 includes the skill in the
* active set (body lands in the system slot; gated tools land
* in the tools slot).
*
* Both entries are `undefined` when the registry is empty — filter
* before adding to a tool list:
*
* const { listSkills, readSkill } = registry.toTools();
* const tools = [listSkills, readSkill, ...other].filter(Boolean) as Tool[];
*
* Composes with `gatedTools` from `agentfootprint/providers`
* so PermissionPolicy can scope which roles see the skill discovery
* surface.
*
* @returns A `SkillToolPair` (`{ listSkills, readSkill }`).
*/
toTools() {
const skills = this.list();
return {
listSkills: buildListSkillsTool(skills),
readSkill: buildReadSkillTool(skills),
};
}
/**
* Resolve the effective `surfaceMode` for a skill, applying the
* cascade:
*
* 1. If the skill's own `metadata.surfaceMode` is concrete
* (`'system-prompt'` / `'tool-only'` / `'both'`), return it.
* Per-skill explicit choice always wins.
* 2. Else if the registry was constructed with a concrete
* `surfaceMode`, return that.
* 3. Else delegate to `resolveSurfaceMode(provider, model)` using
* the explicit `provider` arg (or `this.providerHint` if
* omitted). Falls back to `'tool-only'` when no provider is
* known.
*
* Callers pass the agent's provider + model to get the RECOMMENDED
* surface mode for a skill. Note this is a recommendation, not the
* delivery decision: body delivery compares the DECLARED
* `metadata.surfaceMode` string, so a skill left on `'auto'` is
* delivered as `'system-prompt'` regardless of what this returns.
*
* Throws if the skill is not registered (catches typos at the
* caller site rather than silently resolving against a stranger).
*
* @param skillOrId A registered Skill `Injection` OR its `id`.
* @param provider Provider name override (wins over `providerHint`).
* @param model Model name for the per-provider attention rule.
*/
resolveForSkill(skillOrId, provider, model) {
const skill = typeof skillOrId === 'string' ? this.get(skillOrId) : skillOrId;
if (!skill) {
const id = typeof skillOrId === 'string' ? skillOrId : skillOrId.id;
throw new Error(`SkillRegistry.resolveForSkill: no skill registered at id '${id}'.`);
}
if (skill.flavor !== 'skill') {
throw new Error(`SkillRegistry.resolveForSkill: '${skill.id}' has flavor '${skill.flavor}', expected 'skill'.`);
}
const meta = skill.metadata;
const skillMode = meta?.surfaceMode;
if (skillMode && skillMode !== 'auto')
return skillMode;
if (this.opts.surfaceMode && this.opts.surfaceMode !== 'auto') {
return this.opts.surfaceMode;
}
const effectiveProvider = provider ?? this.opts.providerHint ?? 'unknown';
return resolveSurfaceMode(effectiveProvider, model);
}
}
//# sourceMappingURL=SkillRegistry.js.map