From de4b6a8403b17974384e05714afdf9b4d53a53fc Mon Sep 17 00:00:00 2001 From: root Date: Mon, 1 Jun 2026 18:10:11 +1000 Subject: [PATCH] feat(ai): extensible agent tool registry Co-Authored-By: Claude Opus 4.8 --- lib/ai/agent/registry.js | 17 +++++++++++++++++ tests/ai/agent/registry.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 lib/ai/agent/registry.js create mode 100644 tests/ai/agent/registry.test.js diff --git a/lib/ai/agent/registry.js b/lib/ai/agent/registry.js new file mode 100644 index 0000000..7f5a525 --- /dev/null +++ b/lib/ai/agent/registry.js @@ -0,0 +1,17 @@ +export function createRegistry() { + const tools = new Map(); + return { + registerTool(def) { + if (!def?.name) throw new Error('tool def needs a name'); + if (tools.has(def.name)) throw new Error(`tool "${def.name}" already registered`); + tools.set(def.name, def); + }, + getTool(name) { return tools.get(name); }, + listTools() { return [...tools.values()]; }, + // Anthropic tool-use schema — handlers are intentionally stripped. + toAnthropicTools() { + return [...tools.values()].map(({ name, description, input_schema }) => + ({ name, description, input_schema })); + } + }; +} diff --git a/tests/ai/agent/registry.test.js b/tests/ai/agent/registry.test.js new file mode 100644 index 0000000..ca5780c --- /dev/null +++ b/tests/ai/agent/registry.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { createRegistry } from '../../../lib/ai/agent/registry.js'; + +describe('tool registry', () => { + const def = { + name: 'echo', + description: 'echo back', + input_schema: { type: 'object', properties: { x: { type: 'string' } }, required: ['x'] }, + handler: async ({ x }) => ({ ok: true, x }) + }; + + it('registers and retrieves a tool', () => { + const r = createRegistry(); + r.registerTool(def); + expect(r.getTool('echo')).toBe(def); + expect(r.listTools().map(t => t.name)).toEqual(['echo']); + }); + + it('rejects duplicate names', () => { + const r = createRegistry(); + r.registerTool(def); + expect(() => r.registerTool(def)).toThrow(/already registered/); + }); + + it('serialises to the Anthropic tools shape (no handler leak)', () => { + const r = createRegistry(); + r.registerTool(def); + expect(r.toAnthropicTools()).toEqual([ + { name: 'echo', description: 'echo back', input_schema: def.input_schema } + ]); + }); +});