import { Context, Effect, Layer, Ref } from 'effect'; import type { Tool, ToolSchemaMetadata } from './tool'; import { withInferredCapabilities } from './capabilities'; import { ToolNotFoundError, ToolAlreadyExistsError, ToolValidationError } from './errors'; import { validateToolSchema } from './validation'; import { normalizeToolDefinition } from './utils'; /** Internal type erasure for heterogeneous registry storage. */ type AnyTool = Tool; /** Variance-free view used to accept differently typed tools in one batch. */ type BatchTool = Omit & { readonly schema?: { readonly input: unknown; readonly success: unknown; readonly failure?: unknown; readonly metadata?: ToolSchemaMetadata; }; readonly execute: (args: never) => unknown; }; const eraseToolForStorage = (tool: BatchTool): AnyTool => tool as unknown as AnyTool; /** * Effect-wrapped tool schema validation */ const validateToolSchemaEffect = (tool: BatchTool): Effect.Effect => Effect.try({ try: () => validateToolSchema(tool), catch: (error) => new ToolValidationError({ id: tool.id, message: error instanceof Error ? error.message : String(error) }) }); /** * ToolRegistryService interface for Effect-based tool management */ export interface ToolRegistryService { /** * Register a tool in the registry */ registerTool( tool: Tool, ): Effect.Effect; /** * Register multiple tools at once */ registerTools( tools: Tools, ): Effect.Effect; /** * Get a tool by ID */ getTool(id: string): Effect.Effect; /** * Get multiple tools by their IDs (returns only found tools) */ getTools(ids: string[]): Effect.Effect; /** * Get missing tool IDs from a list */ getMissingToolIds(ids: string[]): Effect.Effect; /** * Get all registered tools */ getAllTools(): Effect.Effect; /** * Check if a tool exists */ hasTool(id: string): Effect.Effect; /** * Remove a tool from the registry */ removeTool(id: string): Effect.Effect; /** * Clear all tools from the registry */ clear(): Effect.Effect; /** * Get the number of registered tools */ size(): Effect.Effect; /** * Get normalized tools for AI execution */ normalizeTools(ids: string[]): Effect.Effect; /** * Backwards-compatible alias for normalized tools as Record */ toAISDKTools(ids: string[]): Effect.Effect>; } export const ToolRegistryService = Context.GenericTag( 'ToolRegistryService' ); /** * Implementation of ToolRegistryService */ class ToolRegistryServiceImpl implements ToolRegistryService { constructor(private tools: Ref.Ref>) {} registerTool( tool: Tool, ): Effect.Effect { const self = this; return Effect.gen(function* () { const tools = yield* Ref.get(self.tools); if (tools.has(tool.id)) { return yield* Effect.fail(new ToolAlreadyExistsError({ id: tool.id })); } const toolWithCapabilities = withInferredCapabilities(tool); // Validate tool schema using Effect yield* validateToolSchemaEffect(toolWithCapabilities); const newTools = new Map(tools); newTools.set(tool.id, eraseToolForStorage(toolWithCapabilities)); yield* Ref.set(self.tools, newTools); }); } registerTools( tools: Tools, ): Effect.Effect { const self = this; return Effect.gen(function* () { const currentTools = yield* Ref.get(self.tools); const nextTools = new Map(currentTools); for (const tool of tools) { if (nextTools.has(tool.id)) { return yield* Effect.fail(new ToolAlreadyExistsError({ id: tool.id })); } const toolWithCapabilities = withInferredCapabilities(tool); yield* validateToolSchemaEffect(toolWithCapabilities); nextTools.set(tool.id, eraseToolForStorage(toolWithCapabilities)); } yield* Ref.set(self.tools, nextTools); }); } getTool(id: string): Effect.Effect { const self = this; return Effect.gen(function* () { const tools = yield* Ref.get(self.tools); const tool = tools.get(id); if (!tool) { return yield* Effect.fail(new ToolNotFoundError({ id })); } return tool; }); } getTools(ids: string[]): Effect.Effect { const self = this; return Ref.get(self.tools).pipe( Effect.map((tools) => ids.filter(id => tools.has(id)).map(id => tools.get(id)!)) ); } getMissingToolIds(ids: string[]): Effect.Effect { const self = this; return Ref.get(self.tools).pipe( Effect.map((tools) => ids.filter(id => !tools.has(id))) ); } getAllTools(): Effect.Effect { const self = this; return Ref.get(self.tools).pipe( Effect.map((tools) => Array.from(tools.values())) ); } hasTool(id: string): Effect.Effect { const self = this; return Ref.get(self.tools).pipe( Effect.map((tools) => tools.has(id)) ); } removeTool(id: string): Effect.Effect { const self = this; return Effect.gen(function* () { const tools = yield* Ref.get(self.tools); const newTools = new Map(tools); const result = newTools.delete(id); yield* Ref.set(self.tools, newTools); return result; }); } clear(): Effect.Effect { return Ref.set(this.tools, new Map()); } size(): Effect.Effect { return Ref.get(this.tools).pipe( Effect.map((tools) => tools.size) ); } normalizeTools(ids: string[]): Effect.Effect { const self = this; return self.getTools(ids).pipe( Effect.map((tools) => tools.map(tool => normalizeToolDefinition(tool, tool.execute))) ); } toAISDKTools(ids: string[]): Effect.Effect> { const self = this; return self.normalizeTools(ids).pipe( Effect.map((tools) => Object.fromEntries(tools.map(tool => [tool.id, tool]))) ); } } /** * Live layer providing ToolRegistryService */ export const ToolRegistryServiceLive = Layer.effect( ToolRegistryService, Effect.gen(function* () { const tools = yield* Ref.make(new Map()); return new ToolRegistryServiceImpl(tools); }) );