/** * Progressive capability gateway (experimental, opt-in). * * Issue #1: keep the full tool/skill ecosystem reachable without paying to * expose every extension-tool schema and skill description on every request. * * Behavior: * - On by default; disable via SELESAI_CAPABILITY_GATEWAY=0. * - At session start, extension tools become dormant except the gateway's own * tools and Graft's code-context tools: they stay registered but are removed * from the active tool set. Built-in tools are never touched. * - A compact catalog tool lists eligible tools/skills with one-line summaries. * - capability_discover readies exactly one capability. With `name` it validates * a catalogued tool and activates its native definition for the current agent * run; with `job` the agent describes what it is about to do and Jev answers, * from catalog metadata alone, which tool and which skill fit (each possibly * none), so the decision costs a bounded request instead of loading every schema. Either way the agent gets the * chosen tool's compact invocation contract, and the real schema is live on * its next turn. capability_skill_show loads exactly the selected skill * instructions. * - A deterministic router activates a uniquely matched tool before the run. * Skills and ambiguous matches remain discoverable through the catalog * without injecting fuzzy hints into the model context. * - Default-on Jev-assisted routing (capabilityGateway.routing.jev in settings.json) * has two callers. The host-side one is only a bounded tie-breaker: when the * deterministic router returns an ambiguous lexical hint among optional tools, * the gateway offers just those hinted tools (two or three) and the current * prompt to the Jev decisions model as one constrained choice question. The * agent-side one is capability_discover's `job` argument, which offers the * catalog metadata of every offered capability. Jev may answer `none` or one * canonical name; the gateway revalidates the choice against the live catalog * and activates it for the current run only. No hint, a unique activation, a * skill match, or an already-activated tool never reaches the host-side path. * Every Jev failure is an ordinary abstention that leaves deterministic * behavior in place. Without Token-In credentials, no Jev request is sent and * the user is prompted to add an account with `/tokenin add`. * - Temporary activations reset at agent_settled, restoring the baseline * active-tool set; a tool_execution_start for one of them records whether the * activation was actually used (content-free tool name + source only). * - The system-prompt skill index is replaced by a compact capability * instruction; full skill instructions load only on explicit show/invoke. * - Telemetry events are emitted on the shared event bus. */ import { readFileSync } from "node:fs"; import { dirname } from "node:path"; import { stripFrontmatter, type ExtensionAPI, type ExtensionContext, type ToolInfo } from "@selesai/code"; import { StringEnum } from "@earendil-works/pi-ai"; import { Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { buildSkillCatalog, buildToolCatalog, BUILTIN_TOOL_NAMES, firstSentence, route, type CatalogEntry, } from "./catalog.ts"; import { gatewayJevConnection, hintedToolCandidates, MIN_GATEWAY_JEV_CANDIDATES, readGatewayJevConfig, routeJobToJev, routeToJevTool, type JevJobPick, JEV_UNAVAILABLE_REASONS, } from "./routing.ts"; import { confidenceBucket, jevUnavailable, warnJevUnavailableOnce } from "../jev/decisions.ts"; export const GATEWAY_ENV = "SELESAI_CAPABILITY_GATEWAY"; export const GATEWAY_TOOLS = new Set(["capability_catalog", "capability_discover", "capability_skill_show"]); // Graft supplies pre-turn hybrid context and must remain callable for precise // follow-ups; making it dormant defeats both paths. `ask_jev` is the agent's own // decision surface for Jev (and `jev_find` its file finder), so neither is something the agent must discover. const ALWAYS_ACTIVE_EXTENSION_TOOLS = new Set([ "ask_jev", "jev_find", "graft_check_freshness", "graft_file_api", "graft_find_all", "graft_find_code", "graft_repo_map", "graft_trace_calls", ]); const JOB_ROUTE_LINE = "- Call capability_discover with `job` (what you are about to do, in your own words) when a task may need a specialized capability you do not have active. Jev answers two things from metadata alone: which tool (callable code, usable many times) and which skill (a written procedure, read once), each possibly none. A chosen tool comes back with its parameters and is callable on your next turn; a chosen skill is loaded with capability_skill_show."; /** * The single skill-index entry the gateway installs. The `job` line is offered only while Jev can * answer it: telling the agent about a route that always fails costs it a turn every time. */ export function capabilityInstruction(jobRoute: boolean): string { return [ "Optional capabilities (extension tools and skills) are not listed here by default. To use one:", ...(jobRoute ? [JOB_ROUTE_LINE] : []), jobRoute ? '- Call capability_discover with an exact `name`, or search the compact catalog with capability_catalog (kind: "tool" or "skill", natural-language query) when you already know what you are looking for.' : '- Search the compact catalog with capability_catalog (kind: "tool" or "skill", natural-language query), then call capability_discover with the exact `name`.', "- Load a skill's full instructions with capability_skill_show before applying it.", "Never invent optional tool names, actions, or fields; discover them first.", "Never tell the user a tool is unavailable, or that you cannot do something, until you have checked for it here: a tool named in a message, instruction, or task that is not in your tool list is usually an optional capability, so call capability_discover with its `name` and use it.", ].join("\n"); } export const CAPABILITY_INSTRUCTION = capabilityInstruction(true); /** Longest description kept in an invocation contract; the full text is live in the schema. */ export const MAX_CONTRACT_DESCRIPTION_CHARS = 240; /** Longest per-parameter description kept in an invocation contract. */ export const MAX_CONTRACT_PARAMETER_CHARS = 120; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } /** Gateway tool results: text for the model, plus a shape-only details payload persisted in the session. */ type ToolAnswer = { content: Array<{ type: "text"; text: string }>; details: Record }; function clip(text: string, max: number): string { const trimmed = text.trim(); return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed; } /** A parameter's shape in one token; nested schemas are named, never expanded. */ export function schemaShape(schema: Record): string { if (Array.isArray(schema.enum)) return schema.enum.map((value) => JSON.stringify(value)).join(" | "); if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) return "one of several"; if (schema.type === "array") { const items = isRecord(schema.items) ? schema.items : {}; return `array<${typeof items.type === "string" ? items.type : "value"}>`; } return typeof schema.type === "string" ? schema.type : "value"; } /** * The compact invocation contract for one tool: what it does and the parameters it takes. * * Deliberately not the schema itself. An activated tool is callable on the next turn, where the * provider already carries the full schema, so repeating it here would pay for the same tokens * twice. This is enough to plan the call and to abandon it before loading anything. */ export function invocationContract(tool: ToolInfo): string { const schema = isRecord(tool.parameters) ? tool.parameters : {}; const properties = isRecord(schema.properties) ? schema.properties : {}; const required = new Set( Array.isArray(schema.required) ? schema.required.filter((name): name is string => typeof name === "string") : [], ); const lines = [ `${tool.name} — ${clip(tool.discovery?.summary?.trim() || firstSentence(tool.description) || tool.name, MAX_CONTRACT_DESCRIPTION_CHARS)}`, ]; const parameters = Object.entries(properties); if (parameters.length === 0) { lines.push("Parameters: none"); return lines.join("\n"); } lines.push("Parameters:"); for (const [name, raw] of parameters) { const property = isRecord(raw) ? raw : {}; const description = typeof property.description === "string" ? `: ${clip(firstSentence(property.description), MAX_CONTRACT_PARAMETER_CHARS)}` : ""; lines.push(`- ${name} (${schemaShape(property)}, ${required.has(name) ? "required" : "optional"})${description}`); } return lines.join("\n"); } function isEnabled(): boolean { return process.env[GATEWAY_ENV] !== "0"; } function eligibleTools(pi: ExtensionAPI): ToolInfo[] { return pi .getAllTools() .filter((tool) => !GATEWAY_TOOLS.has(tool.name) && !ALWAYS_ACTIVE_EXTENSION_TOOLS.has(tool.name) && !BUILTIN_TOOL_NAMES.has(tool.name)); } function catalogEntries(pi: ExtensionAPI): CatalogEntry[] { const tools = buildToolCatalog(eligibleTools(pi), GATEWAY_TOOLS); const skills = buildSkillCatalog(pi.getResolvedSkills()); return [...tools, ...skills]; } const EMBEDDED_SKILL_BLOCK = /]*>[\s\S]*?<\/skill>/gi; const GITHUB_OR_OPEN_SOURCE_QUERY = /\b(?:github|open[\s-]*source)\b/i; function routePrompt(prompt: string, entries: CatalogEntry[]): ReturnType { const loadedSkills = new Set([...prompt.matchAll(EMBEDDED_SKILL_BLOCK)].map((match) => match[1]!.toLowerCase())); const query = prompt.replace(EMBEDDED_SKILL_BLOCK, " "); const candidates = entries.filter((entry) => entry.kind !== "skill" || !loadedSkills.has(entry.name.toLowerCase())); if (GITHUB_OR_OPEN_SOURCE_QUERY.test(query)) { const grepAppSearch = candidates.find( (entry) => entry.kind === "tool" && entry.eligible && entry.name === "grep_app_search", ); if (grepAppSearch) return { action: "activate", entry: grepAppSearch }; } return route(query, candidates); } function formatCatalog(entries: CatalogEntry[]): string { const lines = entries.map( (entry) => `- ${entry.kind} ${entry.name}${entry.category ? ` [${entry.category}]` : ""}: ${entry.summary}`, ); return lines.length > 0 ? lines.join("\n") : "(no matching capabilities)"; } function findEntry(entries: CatalogEntry[], name: string): CatalogEntry | undefined { const normalized = name.trim().toLowerCase(); return entries.find( (entry) => entry.name.toLowerCase() === normalized || entry.aliases.some((alias) => alias.toLowerCase() === normalized), ); } function skillByFile(pi: ExtensionAPI, filePath: string): { name: string; body: string } | undefined { const skill = pi.getResolvedSkills().find((s) => s.filePath === filePath); if (!skill) return undefined; try { const body = stripFrontmatter(readFileSync(skill.filePath, "utf-8")).trim(); return { name: skill.name, body }; } catch { return undefined; } } function emitTelemetry(pi: ExtensionAPI, event: string, data: Record): void { try { pi.events.emit("capability-gateway", { event, ...data }); } catch { // Telemetry must never break the session. } } /** One kind's line: the pick, `none` (honest about what Jev did not see), or why there is no answer. */ export function describePick(label: string, side: JevJobPick, chosen: string | undefined, noneMeans: string): string { const notSeen = side.dropped > 0 ? ` — among ${side.offered} of ${side.offered + side.dropped} offered; the rest were not considered, so search capability_catalog if one might fit` : ""; if (side.pick.selected) { return chosen ? `${label}: Jev picked "${chosen}" (confidence ${side.pick.confidence.toFixed(2)}).` : `${label}: Jev picked "${side.pick.name}", which is no longer catalogued; nothing was readied.`; } if (side.pick.reason === "none") return `${label}: none — ${noneMeans}${notSeen}.`; if (side.offered === 0) return `${label}: none catalogued.`; // Jev leaned somewhere but not firmly enough to act on: for the agent that is "nothing clearly fits". if (side.pick.reason === "low-confidence" || side.pick.reason === "unquantified") return `${label}: no confident pick${notSeen}.`; return `${label}: no decision (${side.pick.reason}).`; } /** Where a run-local tool activation came from; recorded so use telemetry can attribute it. */ type ActivationSource = "deterministic" | "jev" | "discover"; export default function capabilityGatewayExtension(pi: ExtensionAPI): void { if (!isEnabled()) return; // Tools this gateway activated for the current run, and whether they were invoked. // Cleared at agent_settled with the activations themselves. const activations = new Map(); // Whether the skill index currently offers the `job` route; re-derived before every run. let jobRouteOffered = true; /** Replace the eager skill index with one compact capability entry. Full instructions load only on show. */ function installSkillIndex(): void { pi.setSkillsIndexFilter(() => [ { name: "capability-gateway", description: capabilityInstruction(jobRouteOffered), filePath: "", baseDir: "", sourceInfo: { path: "", source: "builtin", scope: "user", origin: "top-level" }, disableModelInvocation: false, }, ]); } /** Activate one tool for the current run, keeping the rest of the loadout untouched. */ function activateTool(name: string, source: ActivationSource): void { const active = pi.getActiveTools(); if (!active.includes(name)) { pi.setActiveTools([...active, name]); } activations.set(name, { source, used: false }); } // ------------------------------------------------------------------ // Session start: snapshot baseline, make extension tools dormant, and // install the compact skill index (action methods are stubs until bind). // ------------------------------------------------------------------ pi.on("session_start", (_event, ctx) => { const baseline = pi.getActiveTools(); const keep = baseline.filter( (name) => !eligibleTools(pi).some((tool) => tool.name === name), ); pi.setActiveTools(keep); installSkillIndex(); emitTelemetry(pi, "session_start", { baselineCount: baseline.length, dormantCount: baseline.length - keep.length }); void ctx; }); // ------------------------------------------------------------------ // Tools // ------------------------------------------------------------------ pi.registerTool({ name: "capability_catalog", label: "Capability Catalog", description: "Search the compact capability catalog for optional extension tools and skills. Returns name, kind (tool or skill), category, and a one-line purpose for each match. Use when no active tool fits or a specialized integration/workflow is requested.", promptSnippet: "Search the compact catalog of optional tools and skills", parameters: Type.Object({ query: Type.Optional(Type.String({ minLength: 1, description: "Natural-language query; omit to list all." })), kind: Type.Optional(StringEnum(["tool", "skill"] as const, { description: "Filter by capability kind." })), }), async execute(_id, params): Promise { const entries = catalogEntries(pi); const filtered = entries.filter( (entry) => !params.kind || entry.kind === params.kind, ); const matched = params.query ? route(params.query, filtered) : { action: "none" as const }; const shown = params.query && matched.candidates ? matched.candidates : params.query && matched.entry ? [matched.entry] : params.query ? filtered.filter((entry) => { const q = params.query!.toLowerCase(); return ( entry.name.toLowerCase().includes(q) || entry.summary.toLowerCase().includes(q) || entry.aliases.some((alias) => alias.toLowerCase().includes(q)) ); }) : filtered; const text = formatCatalog(shown); // Content-free: catalog query and filter arguments never travel. emitTelemetry(pi, "catalog", { results: shown.length }); return { content: [{ type: "text", text }], details: { count: shown.length, total: filtered.length }, }; }, renderCall(args, theme) { const query = typeof args.query === "string" && args.query.length > 0 ? args.query : "(all)"; let text = theme.fg("toolTitle", theme.bold("capability_catalog ")); text += theme.fg("accent", `"${query}"`); if (args.kind) text += " " + theme.fg("muted", args.kind); return new Text(text, 0, 0); }, }); /** Make one catalogued tool callable for this run and return how to invoke it. */ function readyTool(entry: CatalogEntry, source: ActivationSource): string { // Already in the loadout (routed before the turn, or discovered earlier): re-activating would // only misattribute it, and repeating the contract teaches nothing. Say what the pick means. if (pi.getActiveTools().includes(entry.name)) { return `"${entry.name}" is already active, so nothing changed. If it failed, the problem is in the tool itself (its error says what), not in choosing it: fix that, or pick a different capability by name.`; } activateTool(entry.name, source); const tool = eligibleTools(pi).find((candidate) => candidate.name === entry.name); return [ `Activated "${entry.name}" for this run: call it on your next turn, with its full schema live in your tool list.`, "", tool ? invocationContract(tool) : `${entry.name} — ${entry.summary}`, ].join("\n"); } /** * The agent's on-demand route. Jev decides over catalog metadata only, so the decision costs one * bounded request instead of every schema. A tool and a skill are separate answers: a picked tool * is activated with its invocation contract, a picked skill is named for capability_skill_show. */ async function discoverByJob(job: string, kind: "tool" | "skill" | "both", ctx: ExtensionContext): Promise { const answer = (text: string, details: Record = {}): ToolAnswer => ({ content: [{ type: "text" as const, text }], details, }); const config = readGatewayJevConfig(); if (!config.enabled) { return answer( "Jev capability routing is off (capabilityGateway.routing.jev.enabled is false). Browse with capability_catalog and pass an exact `name`.", ); } const entries = catalogEntries(pi); const tools = kind === "skill" ? [] : entries.filter((entry) => entry.kind === "tool"); const skills = kind === "tool" ? [] : entries.filter((entry) => entry.kind === "skill"); const route = await routeJobToJev(tools, skills, job, ctx, config); if (route.tool.offered + route.skill.offered === 0) { return answer( "No catalogued capability fits one decision request. Narrow it with capability_catalog and pass an exact `name`.", ); } // Revalidate each accepted name against the live catalog: one that vanished mid-call is gone. const live = catalogEntries(pi); const resolve = ({ pick }: JevJobPick, of: "tool" | "skill") => pick.selected ? live.find((entry) => entry.kind === of && entry.name === pick.name) : undefined; const tool = resolve(route.tool, "tool"); const skill = resolve(route.skill, "skill"); // Jev could not be reached at all (as opposed to answering `none`): one reason covers both questions. const unavailable = [route.tool.pick, route.skill.pick].flatMap((pick) => !pick.selected && JEV_UNAVAILABLE_REASONS.has(pick.reason) ? [pick.reason] : [], )[0]; emitTelemetry(pi, "route", { source: "agent", outcome: tool || skill ? "selected" : unavailable ? "unavailable" : "abstained", ...(tool ? { tool: tool.name } : {}), ...(skill ? { skill: skill.name } : {}), candidates: route.tool.offered + route.skill.offered, dropped: route.tool.dropped + route.skill.dropped, durationMs: route.elapsedMs, }); if (!tool && !skill && (unavailable === "no-credential" || unavailable === "no-template")) { if (ctx.hasUI) warnJevUnavailableOnce(ctx.ui, config.provider); return answer( "Choosing by `job` needs Jev, which has no credential in this session. Search capability_catalog and pass an exact `name` instead, or use the built-in tools.", { selected: false, reason: unavailable }, ); } if (!tool && !skill && unavailable) { return answer( `Jev could not decide this job (${unavailable}). Browse with capability_catalog and pass an exact \`name\`, or use the built-in tools.`, { selected: false, reason: unavailable }, ); } const lines: string[] = []; const details: Record = { selected: Boolean(tool || skill) }; if (kind !== "skill") { lines.push(describePick("Tool", route.tool, tool?.name, "the built-in tools (read, bash, edit, write, grep, find, ls) or a direct answer are enough")); details.tool = tool?.name ?? null; } if (kind !== "tool") { lines.push(describePick("Skill", route.skill, skill?.name, "no written procedure is needed")); if (skill) lines.push(` ${skill.summary}\n Load it with capability_skill_show before applying it.`); details.skill = skill?.name ?? null; } if (tool) lines.push("", readyTool(tool, "jev")); return answer(lines.join("\n"), details); } pi.registerTool({ name: "capability_discover", label: "Capability Discover", description: "Ready optional capabilities without loading them. Pass `job` (what you are about to do) and Jev answers, from catalog metadata alone, which tool (callable code from an extension or MCP server, usable many times) and which skill (a written procedure from a user, agent, or teammate, read once) fit it — each may be `none`. A chosen tool is activated for this run and its parameters are returned, so you can call it on your next turn; a chosen skill comes back by name for capability_skill_show. Pass `name` instead when capability_catalog already gave you one.", promptSnippet: "Let Jev pick the capability for a job (or name one), then see how to invoke it", parameters: Type.Object({ job: Type.Optional( Type.String({ minLength: 1, description: "What you are about to do, in your own words. Jev picks the capability." }), ), name: Type.Optional(Type.String({ minLength: 1, description: "Exact catalogued name, when you already know it." })), kind: Type.Optional( StringEnum(["tool", "skill", "both"] as const, { description: "Ask only about tools or only about skills. Default: both, answered separately.", }), ), }), async execute(_id, params, _signal, _onUpdate, ctx: ExtensionContext): Promise { const job = params.job?.trim(); const name = params.name?.trim(); if (job && name) { return { content: [{ type: "text", text: "Pass either `job` (let Jev choose) or `name` (you know it), not both." }], details: {}, }; } if (job) return discoverByJob(job, params.kind ?? "both", ctx); if (!name) { return { content: [ { type: "text", text: "Pass `job` to have Jev choose a capability, or `name` from the catalog." }, ], details: {}, }; } const entries = catalogEntries(pi); const entry = findEntry(entries, name); if (!entry) { return { content: [ { type: "text", text: `Unknown capability "${name}". Search capability_catalog for the exact name, or pass a \`job\` and let Jev choose.`, }, ], details: { activated: false }, }; } if (entry.kind === "skill") { return { content: [ { type: "text", text: `"${entry.name}" is a skill: call capability_skill_show with that name to load its instructions.`, }, ], details: { activated: false, skill: entry.name }, }; } emitTelemetry(pi, "discover", { source: "name", tool: entry.name }); const alreadyActive = pi.getActiveTools().includes(entry.name); return { content: [{ type: "text", text: readyTool(entry, "discover") }], details: { activated: true, tool: entry.name, ...(alreadyActive ? { alreadyActive: true } : {}) }, }; }, }); pi.registerTool({ name: "capability_skill_show", label: "Capability Skill Show", description: "Load the complete instructions for one skill from the resolved skill catalog. Use the exact skill name from capability_catalog. This is the explicit boundary that loads full SKILL.md content.", promptSnippet: "Load a skill's full instructions by name", parameters: Type.Object({ name: Type.String({ minLength: 1, description: "Exact skill name." }), }), async execute(_id, params): Promise { const skill = pi.getResolvedSkills().find((s) => s.name === params.name); if (!skill) { return { content: [ { type: "text", text: `Unknown skill "${params.name}". Search capability_catalog (kind: "skill") for the exact name.`, }, ], details: { loaded: false }, }; } const loaded = skillByFile(pi, skill.filePath); if (!loaded) { return { content: [{ type: "text", text: `Skill "${params.name}" exists but its file could not be read.` }], details: { loaded: false }, }; } emitTelemetry(pi, "skill_show", { skill: loaded.name }); return { content: [ { type: "text", text: `\nThe full instructions for this skill are embedded inline below; do not read its file again.\nReferences are relative to ${dirname(skill.filePath)}.\n\n${loaded.body}\n`, }, ], details: { loaded: true, skill: loaded.name }, }; }, }); // ------------------------------------------------------------------ // Routing: the deterministic catalog router first; default-on Jev routing is // only a bounded tie-breaker for its ambiguous/hint result. // ------------------------------------------------------------------ pi.on("before_agent_start", async (event, ctx) => { // Offer the `job` route only while Jev can answer it; `/tokenin add` brings it back on the // next run without a reload. Silent: the warning belongs to a path that actually needed Jev. const jevConfig = readGatewayJevConfig(); const jobReady = jevConfig.enabled && (await jevUnavailable(ctx, gatewayJevConnection(jevConfig))) === undefined; if (jobReady !== jobRouteOffered) { jobRouteOffered = jobReady; installSkillIndex(); } const entries = catalogEntries(pi); const result = routePrompt(event.prompt, entries); if (result.action === "activate" && result.entry) { activateTool(result.entry.name, "deterministic"); emitTelemetry(pi, "route", { source: "deterministic", outcome: "activated", tool: result.entry.name, candidates: 0, durationMs: 0, }); return undefined; } // Do not inject fuzzy recommendations or catalog hints into the model // context. It can discover capabilities when it actually needs one. // Jev is a tie-breaker, never a classifier: only the ambiguous/hint // result counts. No lexical signal or unique skill recommendation // reaches it, and skills are never offered as candidates. if (result.action !== "hint") return undefined; const config = readGatewayJevConfig(); if (!config.enabled) return undefined; const candidates = hintedToolCandidates(result.candidates); if (candidates.length < MIN_GATEWAY_JEV_CANDIDATES) return undefined; emitTelemetry(pi, "route", { source: "jev", outcome: "attempt", candidates: candidates.length }); const jevRoute = await routeToJevTool(candidates, event.prompt, ctx, config); if (!jevRoute.selected) { if ((jevRoute.reason === "no-credential" || jevRoute.reason === "no-template") && ctx.hasUI) { warnJevUnavailableOnce(ctx.ui, config.provider); } emitTelemetry(pi, "route", { source: "jev", outcome: JEV_UNAVAILABLE_REASONS.has(jevRoute.reason) ? "unavailable" : "abstained", reason: jevRoute.reason, candidates: jevRoute.candidates, durationMs: jevRoute.elapsedMs, }); return undefined; } // Revalidate the accepted choice against the live catalog immediately // before activation: a stale or no-longer-eligible name activates nothing. const live = eligibleTools(pi).find((tool) => tool.name === jevRoute.tool); if (live) activateTool(live.name, "jev"); emitTelemetry(pi, "route", { source: "jev", outcome: live ? "activated" : "abstained", ...(live ? { tool: live.name } : { tool: jevRoute.tool }), confidence: confidenceBucket(jevRoute.confidence), candidates: jevRoute.candidates, durationMs: jevRoute.elapsedMs, }); return undefined; }); // ------------------------------------------------------------------ // Selected -> used: a run-local activation is "used" when its tool is // actually executed before the run settles. Content-free: name + source. // ------------------------------------------------------------------ pi.on("tool_execution_start", (event) => { const activation = activations.get(event.toolName); if (!activation || activation.used) return; activation.used = true; emitTelemetry(pi, "use", { tool: event.toolName, source: activation.source }); }); // ------------------------------------------------------------------ // Reset: restore the baseline active-tool set after the run settles and // report how many run-local activations were actually used. // ------------------------------------------------------------------ pi.on("agent_settled", () => { const baseline = pi.getActiveTools().filter((name) => !eligibleTools(pi).some((tool) => tool.name === name)); pi.setActiveTools(baseline); let used = 0; for (const activation of activations.values()) if (activation.used) used += 1; emitTelemetry(pi, "reset", { activeCount: baseline.length, activated: activations.size, used }); activations.clear(); }); // ------------------------------------------------------------------ // Command: /capability-gateway status // ------------------------------------------------------------------ pi.registerCommand("capability-gateway", { description: "Show capability gateway status and catalog counts.", async handler(_args, ctx) { const tools = buildToolCatalog(eligibleTools(pi), GATEWAY_TOOLS); const skills = buildSkillCatalog(pi.getResolvedSkills()); const active = pi.getActiveTools(); const dormant = tools.filter((t) => !active.includes(t.name)).length; const text = [ `Capability gateway: enabled`, `catalogued tools: ${tools.length} (${dormant} dormant)`, `catalogued skills: ${skills.length}`, `active tools: ${active.join(", ") || "(none)"}`, ].join("\n"); ctx.ui.notify(text); }, }); }