UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

112 lines • 6.44 kB
"use strict"; /** * skillScopedTools — ToolProvider that exposes a tool subset only while a * specific Skill is the one the model most recently loaded. * * **You probably don't need this.** If the tools belong to the skill, hand them * to the skill: `defineSkill({ tools: [...], autoActivate: 'currentSkill' })` * already narrows the LLM's tool list to the active skill, with no provider * wiring at all. This provider is for tools that CAN'T ride on the skill — * a list assembled elsewhere, discovered at runtime, or owned by another module. * * Three facts worth knowing before you wire it: * * 1. **`ctx.activeSkillId` is the last `read_skill` activation, not the graph * cursor.** The runtime fills it from the tail of `scope.activatedInjectionIds`, * which only `read_skill` ever appends to. A skill that activated because an * entry RULE matched, or because a `skillGraph()` edge routed into it, does * NOT set it — `list(ctx)` will return `[]` for that skill. Scope by graph * position with the skill's own `tools:[]` instead. * 2. **One ToolProvider per agent.** `AgentBuilder.toolProvider()` throws on the * second call. Compose several scopes into ONE provider (see the example * below) rather than calling it in a loop. * 3. It is pure compute — no Agent-runtime dependency — so it is equally usable * from a test or a design-time inspection. * * @example One skill's tools, scoped by activation * const billingTools = skillScopedTools('billing', [refundTool, chargeTool]); * billingTools.list({ iteration: 1, activeSkillId: 'billing' }); * // → [refundTool, chargeTool] * billingTools.list({ iteration: 1, activeSkillId: 'refund' }); * // → [] (a different skill was loaded) * billingTools.list({ iteration: 1 }); * // → [] (nothing loaded via read_skill — see fact 1 above) * * @example Compose with baseline + multiple skills * const baseline = staticTools([lookupOrderTool, listSkills, readSkill]); * const billingTbx = skillScopedTools('billing', [refundTool, chargeTool]); * const refundTbx = skillScopedTools('refund', [reverseTool]); * * // ONE provider for the agent (see fact 2) — concatenate the scopes: * const provider: ToolProvider = { * id: 'composite', * list: (ctx) => [ * ...baseline.list(ctx), * ...billingTbx.list(ctx), * ...refundTbx.list(ctx), * ], * }; * Agent.create({ provider: llm, model }).toolProvider(provider).build(); */ Object.defineProperty(exports, "__esModule", { value: true }); exports.skillScopedTools = exports.skillScopedToolsTarget = exports.SKILL_SCOPED_TOOLS_ID_PREFIX = void 0; const footprintjs_1 = require("footprintjs"); /** The `ToolProvider.id` prefix every `skillScopedTools(...)` carries. */ exports.SKILL_SCOPED_TOOLS_ID_PREFIX = 'skill-scoped:'; /** Recover the skill id from a `skillScopedTools` provider id, or `undefined`. */ function skillScopedToolsTarget(providerId) { if (!providerId?.startsWith(exports.SKILL_SCOPED_TOOLS_ID_PREFIX)) return undefined; const id = providerId.slice(exports.SKILL_SCOPED_TOOLS_ID_PREFIX.length); return id.length > 0 ? id : undefined; } exports.skillScopedToolsTarget = skillScopedToolsTarget; // LENS · tool-list · request-ephemeral // reads: ctx.activeSkillId — the TAIL of scope.activatedInjectionIds, which only read_skill appends to // known gap: that is not the graph cursor and not the mount kernel's engagement fold, so an engaged map // can still have its tools dropped here while a parked one loses them by a different mechanism // (the park hold-out, src/core/slots/buildToolsSlot.ts · parkHoldOut). Two authorities over one wire surface — see fact 1 below. // law: may omit, never deny; every clause anchored to the call it was composed on. // #region skillScopedTools function skillScopedTools(skillId, tools) { if (!skillId || skillId.trim().length === 0) { throw new Error('skillScopedTools: `skillId` is required and must be non-empty.'); } // Capture the tool list once. `list()` returns a fresh array each // call (matches the staticTools / gatedTools convention so the // agent's reference-equality check always sees an update). const captured = [...tools]; // Dev-mode latch: fact 1 above is a one-time lesson, not a per-iteration one. let warnedNotByReadSkill = false; return { id: `${exports.SKILL_SCOPED_TOOLS_ID_PREFIX}${skillId}`, list(ctx) { // Empty list when the skill is not active. if (ctx.activeSkillId !== skillId) { // The silent-empty this provider is most often wired into (8.7.0): the skill // IS loaded this iteration — an entry rule matched, or a skillGraph() edge // routed into it — but it did not arrive by `read_skill`, and `activeSkillId` // only ever reports a `read_skill` activation. Fact 1 in the header says so; // nothing said it at the moment it happened, so the tools simply never // appeared and the run looked fine. `activeSkillIds` (8.7.0) is the real // active set, which is what makes this detectable from inside the provider — // composed into a bigger provider or not. if ((0, footprintjs_1.isDevMode)() && !warnedNotByReadSkill && ctx.activeSkillIds?.includes(skillId)) { warnedNotByReadSkill = true; // eslint-disable-next-line no-console console.warn(`agentfootprint skillScopedTools('${skillId}'): skill '${skillId}' IS active on ` + `iteration ${ctx.iteration}, but it was not activated by read_skill — so ` + `ctx.activeSkillId does not name it and this provider returned NO tools. That is ` + `what happens to a skill an entry rule or a skillGraph() edge activates. Put the ` + `tools on the skill itself (defineSkill({ tools, autoActivate: 'currentSkill' })), ` + `or scope on ctx.activeSkillIds, which follows the graph's position.`); } return []; } return [...captured]; }, }; } exports.skillScopedTools = skillScopedTools; // #endregion skillScopedTools //# sourceMappingURL=skillScopedTools.js.map