// The tool-serving harness. A module declares its tools through registerModuleTools; a runtime // (the mesh's per-node tool host) collects the registrations and serves them through the command // surface. HOW a tool is declared and served is settled and lives here; the tools themselves, and // the API client they call, live in the module (novox/hq ADR 0044). import type { ToolDefinition } from "../contracts/index.js"; import type { Broker } from "../messaging/index.js"; export type { ToolDefinition }; /** One tool invocation crossing the broker: which tool, and its arguments. */ export interface Invocation { readonly tool: string; readonly args: Readonly>; } /** A module contributes its tools as a function of its resolved environment. Returning [] (e.g. * when a token is absent) is normal — the module simply exposes nothing until it can. */ export type ToolContributor = (env: NodeJS.ProcessEnv) => ToolDefinition[]; interface Registration { readonly module: string; readonly contribute: ToolContributor; } const registrations: Registration[] = []; /** * Declare the tools a module exposes. Called once, at module-tool load time, from the module's * tools/ entrypoint. The client and the tool implementations are imported from the module itself. */ export function registerModuleTools(module: string, contribute: ToolContributor): void { registrations.push({ module, contribute }); } /** * Collect every registered module's tools against an environment. The tool runtime calls this * after loading the assigned modules' tool entrypoints. A module whose contributor throws is * skipped with its error surfaced, never taking the others down. */ export function collectTools(env: NodeJS.ProcessEnv = process.env): { module: string; tools: ToolDefinition[] }[] { return registrations.map(({ module, contribute }) => { try { return { module, tools: contribute(env) }; } catch (err) { console.error(`[tools] ${module}: contributor failed, exposing none: ${err}`); return { module, tools: [] }; } }); } /** List the tools every registered module exposes — for discovery, without invoking anything. */ export function listTools(env: NodeJS.ProcessEnv = process.env): { module: string; name: string; description: string }[] { return collectTools(env).flatMap(({ module, tools }) => tools.map((t) => ({ module, name: t.name, description: t.description })), ); } /** * Serve the registered modules' tools over the mesh broker — the tool runtime's core. It collects * every module's tools, indexes them by name, and answers `tools.invoke` requests by running the * named tool and returning its result. This is the stable half of serving; a specific tool's work * (and its client) lives in its module. Returns a stop function. * * A duplicate tool name across modules is refused loudly rather than one silently shadowing the * other — two tools answering one name is a fault, not a race to resolve. */ export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = process.env): Promise<() => void> { // Each tool is served on its own key, namespaced by its module (novox/hq ADR 0052): a caller // invokes `.`, only the module that serves it answers, and the module's account is // scoped to serve..* — so one module cannot answer another's calls. The module is the // namespace, so a tool name need only be unique within its module, not across the whole mesh. const stops: Array<() => void> = []; for (const { module, tools } of collectTools(env)) { const seen = new Set(); for (const t of tools) { if (seen.has(t.name)) { throw new Error(`${module} exposes two tools named ${t.name} — refused`); } seen.add(t.name); const stop = await broker.handle>, unknown>( toolKey(module, t.name), (args) => t.run(args ?? {}), ); stops.push(stop); } } return () => { for (const stop of stops) stop(); }; } /** The broker key a tool is served on and invoked by — the module namespaces the tool (ADR 0052). */ export function toolKey(module: string, tool: string): string { return `${module}.${tool}`; } /** Invoke a module's tool over the broker — the caller's side of serving. */ export async function invokeTool( broker: Broker, module: string, tool: string, args: Readonly> = {}, ): Promise { return broker.request(toolKey(module, tool), args); } /** Testing/inspection: drop all registrations. */ export function resetTools(): void { registrations.length = 0; }