import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { initialActiveTools, LAZY_TOOLS } from "../active-tools.ts"; /** * Lazy tool loading for the niche `cbm_*` tools. * * The common exploration tools (list_projects, search_graph, search_code, * get_code_snippet, index_repository) stay active. The rarer graph tools below * are registered but kept OUT of the active set until the model asks for them * via `cbm_search_tools`, which activates matches additively. This keeps their * schemas out of context on every request (see extensions.md "Dynamic Tool * Loading"). Edit LAZY_TOOLS to change the split. */ export function registerSearchTools(pi: ExtensionAPI) { pi.registerTool({ name: "cbm_search_tools", label: "cbm:search_tools", description: "Search for and enable advanced codebase-memory graph tools (Cypher query_graph, trace_path, detect_changes, get_architecture, check_index_coverage, manage_adr, ingest_traces, get_graph_schema) that are not loaded by default. Call this when the active cbm_* tools can't do what a task needs.", parameters: Type.Object({ query: Type.String({ description: "Capability or task to search for (e.g. 'cypher', 'trace call path', 'diff impact', 'adr')." }), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })), }), async execute(_toolCallId, params) { const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean); const matches = pi .getAllTools() .filter((t) => LAZY_TOOLS.has(t.name)) .map((t) => ({ name: t.name, score: terms.reduce( (s, term) => s + (`${t.name} ${t.description}`.toLowerCase().includes(term) ? 1 : 0), 0, ), })) .filter((m) => m.score > 0) .sort((a, b) => b.score - a.score) .slice(0, params.limit ?? 3) .map((m) => m.name); if (matches.length === 0) { // No keyword hit: expose the whole lazy set so the model can still proceed. const all = [...LAZY_TOOLS]; const active = pi.getActiveTools(); const added = all.filter((n) => !active.includes(n)); if (added.length) pi.setActiveTools([...new Set([...active, ...added])]); return { content: [{ type: "text", text: `No keyword match for "${params.query}". Loaded all advanced cbm tools: ${all.join(", ")}`, }], details: { matches: all, added }, }; } const active = pi.getActiveTools(); const added = matches.filter((n) => !active.includes(n)); if (added.length) pi.setActiveTools([...new Set([...active, ...added])]); return { content: [{ type: "text", text: added.length ? `Loaded tools: ${added.join(", ")}` : `Matching tools already active: ${matches.join(", ")}`, }], details: { matches, added }, }; }, }); // On every session start (startup / reload / new / resume / fork), drop the // lazy tools and obsolete schema-less generator tools. Keep the loader, // eager cbm_* tools, built-ins, and unrelated extension tools. pi.on("session_start", () => { pi.setActiveTools(initialActiveTools(pi.getActiveTools())); }); }