ToolRegistration is now pure data (pluginKey, metadata, schemas) with no invoke closures. Plugins register a ToolInvoker that the registry delegates to at call time. SDK: - ToolRegistration: remove invoke closure, add pluginKey field - ToolInvoker: new interface (invoke by toolId) - ToolRegistry: add registerInvoker(pluginKey, invoker) - In-memory registry routes invoke() through invoker map OpenAPI plugin: - OperationBinding: minimal invocation data (method, path, params, body) - OpenApiOperationStore: plugin's own pluggable storage interface - makeOpenApiInvoker: single invoker looks up binding from store - plugin.ts is just registration (166 lines, down from 302) - No per-tool closures, no duplicated schemas In-memory tools plugin: - Single invoker with shared handler map - Registrations are pure data
Unified · Split
| @@ -1,12 +1,13 @@ | ||
| 1 | 1 | import { Effect } from "effect"; |
| 2 | 2 | |
| 3 | 3 | import { ToolId } from "../ids"; |
| 4 | −import { ToolNotFoundError } from "../errors"; | |
| 5 | −import type { ToolRegistration, InvokeOptions } from "../tools"; | |
| 4 | +import { ToolNotFoundError, ToolInvocationError } from "../errors"; | |
| 5 | +import type { ToolRegistration, ToolInvoker, InvokeOptions } from "../tools"; | |
| 6 | 6 | import { reattachDefs } from "../schema-refs"; |
| 7 | 7 | |
| 8 | 8 | export const makeInMemoryToolRegistry = () => { |
| 9 | 9 | const tools = new Map<string, ToolRegistration>(); |
| 10 | + const invokers = new Map<string, ToolInvoker>(); | |
| 10 | 11 | const sharedDefs = new Map<string, unknown>(); |
| 11 | 12 | |
| 12 | 13 | return { |
| @@ -64,12 +65,25 @@ export const makeInMemoryToolRegistry = () => { · 51 unmodified lines | ||
| 64 | 65 | } |
| 65 | 66 | }), |
| 66 | 67 | |
| 68 | + registerInvoker: (pluginKey: string, invoker: ToolInvoker) => | |
| 69 | + Effect.sync(() => { | |
| 70 | + invokers.set(pluginKey, invoker); | |
| 71 | + }), | |
| 72 | + | |
| 67 | 73 | invoke: (toolId: ToolId, args: unknown, options?: InvokeOptions) => |
| 68 | 74 | Effect.gen(function* () { |
| 69 | 75 | const tool = yield* Effect.fromNullable(tools.get(toolId)).pipe( |
| 70 | 76 | Effect.mapError(() => new ToolNotFoundError({ toolId })), |
| 71 | 77 | ); |
| 72 | − return yield* tool.invoke(args, options); | |
| 78 | + const invoker = invokers.get(tool.pluginKey); | |
| 79 | + if (!invoker) { | |
| 80 | + return yield* new ToolInvocationError({ | |
| 81 | + toolId, | |
| 82 | + message: `No invoker registered for plugin "${tool.pluginKey}"`, | |
| 83 | + cause: undefined, | |
| 84 | + }); | |
| 85 | + } | |
| 86 | + return yield* invoker.invoke(toolId, args, options); | |
| 73 | 87 | }), |
| 74 | 88 | |
| 75 | 89 | register: (newTools: readonly ToolRegistration[]) => |
| @@ -17,6 +17,7 @@ export { · 16 unmodified lines | ||
| 17 | 17 | ToolInvocationResult, |
| 18 | 18 | ToolRegistry, |
| 19 | 19 | type ToolRegistration, |
| 20 | + type ToolInvoker, | |
| 20 | 21 | type InvokeOptions, |
| 21 | 22 | } from "./tools"; |
| 22 | 23 | |
| @@ -7,6 +7,7 @@ import type { Secret } from "../secrets"; · 6 unmodified lines | ||
| 7 | 7 | import { |
| 8 | 8 | ToolInvocationResult, |
| 9 | 9 | type ToolRegistration, |
| 10 | + type ToolInvoker, | |
| 10 | 11 | type InvokeOptions, |
| 11 | 12 | } from "../tools"; |
| 12 | 13 | import { |
| @@ -75,26 +76,33 @@ export interface InMemoryToolsPluginExtension { · 62 unmodified lines | ||
| 75 | 76 | } |
| 76 | 77 | |
| 77 | 78 | // --------------------------------------------------------------------------- |
| 78 | −// Registration builder | |
| 79 | +// Internal handler entry | |
| 79 | 80 | // --------------------------------------------------------------------------- |
| 80 | 81 | |
| 81 | −const toRegistrationWithDefs = ( | |
| 82 | +interface HandlerEntry { | |
| 83 | + readonly decode: (args: unknown) => unknown; | |
| 84 | + readonly handler: MemoryToolHandler<unknown>; | |
| 85 | + readonly isEffect: boolean; | |
| 86 | +} | |
| 87 | + | |
| 88 | +// --------------------------------------------------------------------------- | |
| 89 | +// Registration builder — returns pure data + handler entry | |
| 90 | +// --------------------------------------------------------------------------- | |
| 91 | + | |
| 92 | +const buildRegistration = ( | |
| 82 | 93 | namespace: string, |
| 83 | 94 | def: MemoryToolDefinition, |
| 84 | − pluginCtx: PluginContext, | |
| 85 | −): { registration: ToolRegistration; definitions: Record<string, unknown> } => { | |
| 95 | +): { registration: ToolRegistration; entry: HandlerEntry; definitions: Record<string, unknown> } => { | |
| 86 | 96 | const id = ToolId.make(`${namespace}.${def.name}`); |
| 87 | 97 | const decode = Schema.decodeUnknownSync(def.inputSchema); |
| 88 | − const isEffectHandler = def.handler.length >= 2; | |
| 98 | + const isEffect = def.handler.length >= 2; | |
| 89 | 99 | |
| 90 | − // Convert to JSON Schema and hoist definitions | |
| 91 | 100 | const inputJson = JSONSchema.make(def.inputSchema); |
| 92 | 101 | const outputJson = def.outputSchema ? JSONSchema.make(def.outputSchema) : undefined; |
| 93 | 102 | |
| 94 | 103 | const inputHoist = hoistDefinitions(inputJson); |
| 95 | 104 | const outputHoist = hoistDefinitions(outputJson); |
| 96 | 105 | |
| 97 | − // Merge all definitions | |
| 98 | 106 | const allDefs: Record<string, unknown> = { |
| 99 | 107 | ...inputHoist.defs, |
| 100 | 108 | ...outputHoist.defs, |
| @@ -102,121 +110,143 @@ const toRegistrationWithDefs = ( · 1 unmodified line | ||
| 102 | 110 | |
| 103 | 111 | const registration: ToolRegistration = { |
| 104 | 112 | id, |
| 113 | + pluginKey: "inMemoryTools", | |
| 105 | 114 | name: def.name, |
| 106 | 115 | description: def.description, |
| 107 | 116 | tags: def.tags ? [...def.tags] : undefined, |
| 108 | 117 | inputSchema: inputHoist.stripped, |
| 109 | 118 | outputSchema: outputHoist.stripped, |
| 110 | − mayElicit: isEffectHandler, | |
| 111 | − invoke: (args, options?: InvokeOptions) => { | |
| 112 | − const parsed = Effect.try({ | |
| 113 | − try: () => decode(args), | |
| 114 | − catch: (err) => | |
| 115 | − new ToolInvocationError({ | |
| 116 | − toolId: id, | |
| 117 | − message: `Invalid input: ${err instanceof Error ? err.message : String(err)}`, | |
| 118 | − cause: err, | |
| 119 | − }), | |
| 120 | − }); | |
| 121 | − | |
| 122 | − if (!isEffectHandler) { | |
| 123 | − return parsed.pipe( | |
| 124 | − Effect.flatMap((input) => | |
| 125 | − Effect.try({ | |
| 126 | − try: () => | |
| 127 | − new ToolInvocationResult({ | |
| 128 | − data: (def.handler as (args: unknown) => unknown)(input), | |
| 129 | − error: null, | |
| 130 | − }), | |
| 131 | − catch: (err) => | |
| 132 | − new ToolInvocationError({ | |
| 133 | − toolId: id, | |
| 134 | − message: | |
| 135 | − err instanceof Error ? err.message : String(err), | |
| 136 | − cause: err, | |
| 137 | − }), | |
| 138 | − }), | |
| 139 | − ), | |
| 140 | − ); | |
| 141 | − } | |
| 142 | − | |
| 143 | − // Effect handler — build context with elicit + sdk access | |
| 144 | − const ctx: MemoryToolContext = { | |
| 145 | − sdk: { | |
| 146 | − secrets: { | |
| 147 | − list: () => pluginCtx.secrets.list(pluginCtx.scope.id), | |
| 148 | − resolve: (secretId) => pluginCtx.secrets.resolve(secretId), | |
| 149 | − store: (input) => | |
| 150 | − pluginCtx.secrets.store({ | |
| 151 | − ...input, | |
| 152 | − scopeId: pluginCtx.scope.id, | |
| 153 | − }), | |
| 154 | − remove: (secretId) => pluginCtx.secrets.remove(secretId), | |
| 155 | − }, | |
| 156 | − }, | |
| 157 | − elicit: (request) => | |
| 158 | − Effect.gen(function* () { | |
| 159 | − const handler = options?.onElicitation; | |
| 160 | − if (!handler) { | |
| 161 | − return yield* new ElicitationDeclinedError({ | |
| 162 | − toolId: id, | |
| 163 | − action: "decline", | |
| 164 | − }); | |
| 165 | − } | |
| 166 | − const response = yield* handler({ | |
| 167 | − toolId: id, | |
| 168 | − args, | |
| 169 | − request, | |
| 170 | − }); | |
| 171 | − if (response.action !== "accept") { | |
| 172 | − return yield* new ElicitationDeclinedError({ | |
| 173 | − toolId: id, | |
| 174 | − action: response.action as "decline" | "cancel", | |
| 175 | − }); | |
| 176 | − } | |
| 177 | − return response.content ?? {}; | |
| 178 | − }), | |
| 179 | − }; | |
| 119 | + mayElicit: isEffect, | |
| 120 | + }; | |
| 121 | + | |
| 122 | + const entry: HandlerEntry = { decode, handler: def.handler as MemoryToolHandler<unknown>, isEffect }; | |
| 123 | + | |
| 124 | + return { registration, entry, definitions: allDefs }; | |
| 125 | +}; | |
| 126 | + | |
| 127 | +// --------------------------------------------------------------------------- | |
| 128 | +// Invoker — single function that handles all in-memory tools | |
| 129 | +// --------------------------------------------------------------------------- | |
| 130 | + | |
| 131 | +const makeInvoker = ( | |
| 132 | + handlers: Map<string, HandlerEntry>, | |
| 133 | + pluginCtx: PluginContext, | |
| 134 | +): ToolInvoker => ({ | |
| 135 | + invoke: (toolId: ToolId, args: unknown, options?: InvokeOptions) => { | |
| 136 | + const entry = handlers.get(toolId); | |
| 137 | + if (!entry) { | |
| 138 | + return Effect.fail( | |
| 139 | + new ToolInvocationError({ | |
| 140 | + toolId, | |
| 141 | + message: `No handler registered for tool "${toolId}"`, | |
| 142 | + cause: undefined, | |
| 143 | + }), | |
| 144 | + ); | |
| 145 | + } | |
| 180 | 146 | |
| 181 | − const effectHandler = def.handler as ( | |
| 182 | − args: unknown, | |
| 183 | − ctx: MemoryToolContext, | |
| 184 | − ) => Effect.Effect<unknown, unknown>; | |
| 147 | + const parsed = Effect.try({ | |
| 148 | + try: () => entry.decode(args), | |
| 149 | + catch: (err) => | |
| 150 | + new ToolInvocationError({ | |
| 151 | + toolId, | |
| 152 | + message: `Invalid input: ${err instanceof Error ? err.message : String(err)}`, | |
| 153 | + cause: err, | |
| 154 | + }), | |
| 155 | + }); | |
| 185 | 156 | |
| 157 | + if (!entry.isEffect) { | |
| 186 | 158 | return parsed.pipe( |
| 187 | − Effect.flatMap((input) => effectHandler(input, ctx)), | |
| 188 | − Effect.map( | |
| 189 | − (data) => new ToolInvocationResult({ data, error: null }), | |
| 190 | − ), | |
| 191 | − Effect.catchAll( | |
| 192 | − (err): Effect.Effect< | |
| 193 | − ToolInvocationResult, | |
| 194 | − ToolInvocationError | ElicitationDeclinedError | |
| 195 | − > => { | |
| 196 | − if ( | |
| 197 | − err != null && | |
| 198 | − typeof err === "object" && | |
| 199 | − "_tag" in err && | |
| 200 | − (err as { _tag: string })._tag === "ElicitationDeclinedError" | |
| 201 | − ) { | |
| 202 | − return Effect.fail(err as ElicitationDeclinedError); | |
| 203 | − } | |
| 204 | − return Effect.fail( | |
| 159 | + Effect.flatMap((input) => | |
| 160 | + Effect.try({ | |
| 161 | + try: () => | |
| 162 | + new ToolInvocationResult({ | |
| 163 | + data: (entry.handler as (args: unknown) => unknown)(input), | |
| 164 | + error: null, | |
| 165 | + }), | |
| 166 | + catch: (err) => | |
| 205 | 167 | new ToolInvocationError({ |
| 206 | − toolId: id, | |
| 207 | − message: | |
| 208 | − err instanceof Error ? err.message : String(err), | |
| 168 | + toolId, | |
| 169 | + message: err instanceof Error ? err.message : String(err), | |
| 209 | 170 | cause: err, |
| 210 | 171 | }), |
| 211 | − ); | |
| 212 | − }, | |
| 172 | + }), | |
| 213 | 173 | ), |
| 214 | 174 | ); |
| 215 | − }, | |
| 216 | − }; | |
| 175 | + } | |
| 217 | 176 | |
| 218 | − return { registration, definitions: allDefs }; | |
| 219 | −}; | |
| 177 | + // Effect handler — build context with elicit + sdk access | |
| 178 | + const ctx: MemoryToolContext = { | |
| 179 | + sdk: { | |
| 180 | + secrets: { | |
| 181 | + list: () => pluginCtx.secrets.list(pluginCtx.scope.id), | |
| 182 | + resolve: (secretId) => pluginCtx.secrets.resolve(secretId), | |
| 183 | + store: (input) => | |
| 184 | + pluginCtx.secrets.store({ | |
| 185 | + ...input, | |
| 186 | + scopeId: pluginCtx.scope.id, | |
| 187 | + }), | |
| 188 | + remove: (secretId) => pluginCtx.secrets.remove(secretId), | |
| 189 | + }, | |
| 190 | + }, | |
| 191 | + elicit: (request) => | |
| 192 | + Effect.gen(function* () { | |
| 193 | + const handler = options?.onElicitation; | |
| 194 | + if (!handler) { | |
| 195 | + return yield* new ElicitationDeclinedError({ | |
| 196 | + toolId, | |
| 197 | + action: "decline", | |
| 198 | + }); | |
| 199 | + } | |
| 200 | + const response = yield* handler({ | |
| 201 | + toolId, | |
| 202 | + args, | |
| 203 | + request, | |
| 204 | + }); | |
| 205 | + if (response.action !== "accept") { | |
| 206 | + return yield* new ElicitationDeclinedError({ | |
| 207 | + toolId, | |
| 208 | + action: response.action as "decline" | "cancel", | |
| 209 | + }); | |
| 210 | + } | |
| 211 | + return response.content ?? {}; | |
| 212 | + }), | |
| 213 | + }; | |
| 214 | + | |
| 215 | + const effectHandler = entry.handler as ( | |
| 216 | + args: unknown, | |
| 217 | + ctx: MemoryToolContext, | |
| 218 | + ) => Effect.Effect<unknown, unknown>; | |
| 219 | + | |
| 220 | + return parsed.pipe( | |
| 221 | + Effect.flatMap((input) => effectHandler(input, ctx)), | |
| 222 | + Effect.map( | |
| 223 | + (data) => new ToolInvocationResult({ data, error: null }), | |
| 224 | + ), | |
| 225 | + Effect.catchAll( | |
| 226 | + (err): Effect.Effect< | |
| 227 | + ToolInvocationResult, | |
| 228 | + ToolInvocationError | ElicitationDeclinedError | |
| 229 | + > => { | |
| 230 | + if ( | |
| 231 | + err != null && | |
| 232 | + typeof err === "object" && | |
| 233 | + "_tag" in err && | |
| 234 | + (err as { _tag: string })._tag === "ElicitationDeclinedError" | |
| 235 | + ) { | |
| 236 | + return Effect.fail(err as ElicitationDeclinedError); | |
| 237 | + } | |
| 238 | + return Effect.fail( | |
| 239 | + new ToolInvocationError({ | |
| 240 | + toolId, | |
| 241 | + message: err instanceof Error ? err.message : String(err), | |
| 242 | + cause: err, | |
| 243 | + }), | |
| 244 | + ); | |
| 245 | + }, | |
| 246 | + ), | |
| 247 | + ); | |
| 248 | + }, | |
| 249 | +}); | |
| 220 | 250 | |
| 221 | 251 | // --------------------------------------------------------------------------- |
| 222 | 252 | // Tool definition helper — infers TInput from the schema |
| @@ -242,8 +272,16 @@ export const inMemoryToolsPlugin = (config: { · 19 unmodified lines | ||
| 242 | 272 | key: "inMemoryTools", |
| 243 | 273 | init: (ctx: PluginContext) => |
| 244 | 274 | Effect.gen(function* () { |
| 245 | − const results = config.tools.map((t) => toRegistrationWithDefs(ns, t, ctx)); | |
| 246 | − | |
| 275 | + // Shared handler map for all tools in this plugin | |
| 276 | + const handlers = new Map<string, HandlerEntry>(); | |
| 277 | + const invoker = makeInvoker(handlers, ctx); | |
| 278 | + | |
| 279 | + // Register the invoker once | |
| 280 | + yield* ctx.tools.registerInvoker("inMemoryTools", invoker); | |
| 281 | + | |
| 282 | + // Build registrations + handler entries | |
| 283 | + const results = config.tools.map((t) => buildRegistration(ns, t)); | |
| 284 | + | |
| 247 | 285 | // Register all definitions first |
| 248 | 286 | const allDefs: Record<string, unknown> = {}; |
| 249 | 287 | for (const { definitions } of results) { |
| @@ -251,7 +289,10 @@ export const inMemoryToolsPlugin = (config: { · 1 unmodified line | ||
| 251 | 289 | } |
| 252 | 290 | yield* ctx.tools.registerDefinitions(allDefs); |
| 253 | 291 | |
| 254 | − // Then register tools with stripped schemas | |
| 292 | + // Store handler entries + register tool data | |
| 293 | + for (const { registration, entry } of results) { | |
| 294 | + handlers.set(registration.id, entry); | |
| 295 | + } | |
| 255 | 296 | const registrations = results.map(({ registration }) => registration); |
| 256 | 297 | yield* ctx.tools.register(registrations); |
| 257 | 298 | |
| @@ -259,20 +300,28 @@ export const inMemoryToolsPlugin = (config: { · 1 unmodified line | ||
| 259 | 300 | extension: { |
| 260 | 301 | addTools: (newTools: readonly MemoryToolDefinition<any, any>[]) => |
| 261 | 302 | Effect.gen(function* () { |
| 262 | − const newResults = newTools.map((t) => toRegistrationWithDefs(ns, t, ctx)); | |
| 263 | − | |
| 303 | + const newResults = newTools.map((t) => buildRegistration(ns, t)); | |
| 304 | + | |
| 264 | 305 | const newDefs: Record<string, unknown> = {}; |
| 265 | 306 | for (const { definitions } of newResults) { |
| 266 | 307 | Object.assign(newDefs, definitions); |
| 267 | 308 | } |
| 268 | 309 | yield* ctx.tools.registerDefinitions(newDefs); |
| 269 | 310 | |
| 311 | + for (const { registration, entry } of newResults) { | |
| 312 | + handlers.set(registration.id, entry); | |
| 313 | + } | |
| 270 | 314 | const newRegistrations = newResults.map(({ registration }) => registration); |
| 271 | 315 | yield* ctx.tools.register(newRegistrations); |
| 272 | 316 | }), |
| 273 | 317 | }, |
| 274 | 318 | close: () => |
| 275 | − ctx.tools.unregister(registrations.map((r) => r.id)), | |
| 319 | + Effect.gen(function* () { | |
| 320 | + yield* ctx.tools.unregister(registrations.map((r) => r.id)); | |
| 321 | + for (const { registration } of results) { | |
| 322 | + handlers.delete(registration.id); | |
| 323 | + } | |
| 324 | + }), | |
| 276 | 325 | }; |
| 277 | 326 | }), |
| 278 | 327 | }); |
| @@ -82,6 +82,15 @@ export class ToolRegistry extends Context.Tag("@executor/sdk/ToolRegistry")< · 81 unmodified lines | ||
| 82 | 82 | defs: Record<string, unknown>, |
| 83 | 83 | ) => Effect.Effect<void>; |
| 84 | 84 | |
| 85 | + /** | |
| 86 | + * Register a plugin invoker. Must be called before registering tools | |
| 87 | + * with the corresponding pluginKey. | |
| 88 | + */ | |
| 89 | + readonly registerInvoker: ( | |
| 90 | + pluginKey: string, | |
| 91 | + invoker: ToolInvoker, | |
| 92 | + ) => Effect.Effect<void>; | |
| 93 | + | |
| 85 | 94 | /** Register tools (used by plugins to push tools into the registry) */ |
| 86 | 95 | readonly register: ( |
| 87 | 96 | tools: readonly ToolRegistration[], |
| @@ -95,26 +104,31 @@ export class ToolRegistry extends Context.Tag("@executor/sdk/ToolRegistry")< · 7 unmodified lines | ||
| 95 | 104 | >() {} |
| 96 | 105 | |
| 97 | 106 | // --------------------------------------------------------------------------- |
| 98 | −// ToolRegistration — what a plugin provides when registering a tool | |
| 107 | +// ToolInvoker — plugin-provided invocation handler | |
| 108 | +// --------------------------------------------------------------------------- | |
| 109 | + | |
| 110 | +export interface ToolInvoker { | |
| 111 | + readonly invoke: ( | |
| 112 | + toolId: ToolId, | |
| 113 | + args: unknown, | |
| 114 | + options?: InvokeOptions, | |
| 115 | + ) => Effect.Effect< | |
| 116 | + ToolInvocationResult, | |
| 117 | + ToolInvocationError | ElicitationDeclinedError | |
| 118 | + >; | |
| 119 | +} | |
| 120 | + | |
| 121 | +// --------------------------------------------------------------------------- | |
| 122 | +// ToolRegistration — pure data, no closures | |
| 99 | 123 | // --------------------------------------------------------------------------- |
| 100 | 124 | |
| 101 | 125 | export interface ToolRegistration { |
| 102 | 126 | readonly id: ToolId; |
| 127 | + readonly pluginKey: string; | |
| 103 | 128 | readonly name: string; |
| 104 | 129 | readonly description?: string; |
| 105 | 130 | readonly tags?: readonly string[]; |
| 106 | 131 | readonly mayElicit?: boolean; |
| 107 | 132 | readonly inputSchema?: unknown; |
| 108 | 133 | readonly outputSchema?: unknown; |
| 109 | − /** | |
| 110 | − * The tool's invoke function. Receives args and an optional elicitation | |
| 111 | − * handler that it can call to request user input mid-invocation. | |
| 112 | − */ | |
| 113 | − readonly invoke: ( | |
| 114 | − args: unknown, | |
| 115 | − options?: InvokeOptions, | |
| 116 | − ) => Effect.Effect< | |
| 117 | − ToolInvocationResult, | |
| 118 | − ToolInvocationError | ElicitationDeclinedError | |
| 119 | − >; | |
| 120 | 134 | } |
| @@ -1,7 +1,16 @@ | ||
| 1 | 1 | export { parse } from "./parse"; |
| 2 | 2 | export { extract } from "./extract"; |
| 3 | −export { invoke } from "./invoke"; | |
| 4 | −export { openApiPlugin, type OpenApiSpecConfig, type OpenApiPluginExtension } from "./plugin"; | |
| 3 | +export { invoke, makeOpenApiInvoker } from "./invoke"; | |
| 4 | +export { | |
| 5 | + openApiPlugin, | |
| 6 | + type OpenApiSpecConfig, | |
| 7 | + type OpenApiPluginExtension, | |
| 8 | +} from "./plugin"; | |
| 9 | +export { | |
| 10 | + makeInMemoryOperationStore, | |
| 11 | + type OpenApiOperationStore, | |
| 12 | +} from "./operation-store"; | |
| 13 | +export { DocResolver, resolveBaseUrl, preferredContent } from "./openapi-utils"; | |
| 5 | 14 | |
| 6 | 15 | export { |
| 7 | 16 | OpenApiParseError, |
| @@ -18,6 +27,7 @@ export { · 10 unmodified lines | ||
| 18 | 27 | ExtractionResult, |
| 19 | 28 | InvocationConfig, |
| 20 | 29 | InvocationResult, |
| 30 | + OperationBinding, | |
| 21 | 31 | OperationParameter, |
| 22 | 32 | OperationRequestBody, |
| 23 | 33 | ServerInfo, |
| @@ -1,13 +1,21 @@ | ||
| 1 | −import { Effect, Match, Option } from "effect"; | |
| 1 | +import { Effect, Layer, Match, Option } from "effect"; | |
| 2 | 2 | import { |
| 3 | 3 | HttpClient, |
| 4 | 4 | HttpClientRequest, |
| 5 | 5 | } from "@effect/platform"; |
| 6 | 6 | |
| 7 | +import { | |
| 8 | + type ToolId, | |
| 9 | + type ToolInvoker, | |
| 10 | + ToolInvocationResult, | |
| 11 | + ToolInvocationError, | |
| 12 | +} from "@executor/sdk"; | |
| 13 | + | |
| 7 | 14 | import { OpenApiInvocationError } from "./errors"; |
| 15 | +import type { OpenApiOperationStore } from "./operation-store"; | |
| 8 | 16 | import { |
| 9 | 17 | type AuthConfig, |
| 10 | − type ExtractedOperation, | |
| 18 | + type OperationBinding, | |
| 11 | 19 | InvocationConfig, |
| 12 | 20 | InvocationResult, |
| 13 | 21 | type OperationParameter, |
| @@ -157,9 +165,9 @@ const isJsonContentType = (ct: string | null | undefined): boolean => { · 143 unmodified lines | ||
| 157 | 165 | // Public API |
| 158 | 166 | // --------------------------------------------------------------------------- |
| 159 | 167 | |
| 160 | −/** Invoke an extracted OpenAPI operation. Requires HttpClient in the context. */ | |
| 168 | +/** Invoke an OpenAPI operation binding. Requires HttpClient in the context. */ | |
| 161 | 169 | export const invoke = Effect.fn("OpenApi.invoke")(function* ( |
| 162 | − operation: ExtractedOperation, | |
| 170 | + operation: OperationBinding, | |
| 163 | 171 | args: Record<string, unknown>, |
| 164 | 172 | config: InvocationConfig, |
| 165 | 173 | ) { |
| @@ -251,3 +259,61 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 85 unmodified lines | ||
| 251 | 259 | error: ok ? null : responseBody, |
| 252 | 260 | }); |
| 253 | 261 | }); |
| 262 | + | |
| 263 | +// --------------------------------------------------------------------------- | |
| 264 | +// ToolInvoker — bridges operation store + HTTP client into SDK invoker | |
| 265 | +// --------------------------------------------------------------------------- | |
| 266 | + | |
| 267 | +export const makeOpenApiInvoker = ( | |
| 268 | + operationStore: OpenApiOperationStore, | |
| 269 | + httpClientLayer: Layer.Layer<HttpClient.HttpClient>, | |
| 270 | +): ToolInvoker => ({ | |
| 271 | + invoke: (toolId: ToolId, args: unknown, _options) => | |
| 272 | + Effect.gen(function* () { | |
| 273 | + const entry = yield* operationStore.get(toolId); | |
| 274 | + if (!entry) { | |
| 275 | + return yield* new ToolInvocationError({ | |
| 276 | + toolId, | |
| 277 | + message: `No operation found for tool "${toolId}"`, | |
| 278 | + cause: undefined, | |
| 279 | + }); | |
| 280 | + } | |
| 281 | + | |
| 282 | + const { binding, config } = entry; | |
| 283 | + const baseUrl = config.baseUrl; | |
| 284 | + | |
| 285 | + const clientWithBaseUrl = baseUrl | |
| 286 | + ? Layer.effect( | |
| 287 | + HttpClient.HttpClient, | |
| 288 | + Effect.map( | |
| 289 | + HttpClient.HttpClient, | |
| 290 | + HttpClient.mapRequest( | |
| 291 | + HttpClientRequest.prependUrl(baseUrl), | |
| 292 | + ), | |
| 293 | + ), | |
| 294 | + ).pipe(Layer.provide(httpClientLayer)) | |
| 295 | + : httpClientLayer; | |
| 296 | + | |
| 297 | + const result = yield* invoke( | |
| 298 | + binding, | |
| 299 | + (args ?? {}) as Record<string, unknown>, | |
| 300 | + config, | |
| 301 | + ).pipe(Effect.provide(clientWithBaseUrl)); | |
| 302 | + | |
| 303 | + return new ToolInvocationResult({ | |
| 304 | + data: result.data, | |
| 305 | + error: result.error, | |
| 306 | + status: result.status, | |
| 307 | + }); | |
| 308 | + }).pipe( | |
| 309 | + Effect.mapError((err) => | |
| 310 | + err._tag === "ToolInvocationError" | |
| 311 | + ? err | |
| 312 | + : new ToolInvocationError({ | |
| 313 | + toolId, | |
| 314 | + message: `OpenAPI invocation failed: ${err.message}`, | |
| 315 | + cause: err, | |
| 316 | + }), | |
| 317 | + ), | |
| 318 | + ), | |
| 319 | +}); | |
| @@ -4,6 +4,7 @@ · 3 unmodified lines | ||
| 4 | 4 | // Wraps the openapi-types V3/V3_1 union mess and provides clean ref resolution. |
| 5 | 5 | // --------------------------------------------------------------------------- |
| 6 | 6 | |
| 7 | +import { Option } from "effect"; | |
| 7 | 8 | import type { OpenAPIV3, OpenAPIV3_1 } from "openapi-types"; |
| 8 | 9 | import type { ParsedDocument } from "./parse"; |
| 9 | 10 | |
| @@ -49,6 +50,25 @@ export class DocResolver { · 39 unmodified lines | ||
| 49 | 50 | const isRef = (value: unknown): value is { $ref: string } => |
| 50 | 51 | typeof value === "object" && value !== null && "$ref" in value; |
| 51 | 52 | |
| 53 | +// --------------------------------------------------------------------------- | |
| 54 | +// Server URL resolution | |
| 55 | +// --------------------------------------------------------------------------- | |
| 56 | + | |
| 57 | +export const resolveBaseUrl = ( | |
| 58 | + servers: readonly { url: string; variables: import("effect/Option").Option<Record<string, string>> }[], | |
| 59 | +): string => { | |
| 60 | + const server = servers[0]; | |
| 61 | + if (!server) return ""; | |
| 62 | + | |
| 63 | + let url = server.url; | |
| 64 | + if (Option.isSome(server.variables)) { | |
| 65 | + for (const [name, value] of Object.entries(server.variables.value)) { | |
| 66 | + url = url.replaceAll(`{${name}}`, value); | |
| 67 | + } | |
| 68 | + } | |
| 69 | + return url; | |
| 70 | +}; | |
| 71 | + | |
| 52 | 72 | // --------------------------------------------------------------------------- |
| 53 | 73 | // Content negotiation |
| 54 | 74 | // --------------------------------------------------------------------------- |
| @@ -0,0 +1,77 @@ | ||
| 1 | +import { Effect } from "effect"; | |
| 2 | +import type { ToolId } from "@executor/sdk"; | |
| 3 | + | |
| 4 | +import type { OperationBinding, InvocationConfig } from "./types"; | |
| 5 | + | |
| 6 | +// --------------------------------------------------------------------------- | |
| 7 | +// Operation store — plugin's own storage for invocation data | |
| 8 | +// --------------------------------------------------------------------------- | |
| 9 | + | |
| 10 | +export interface OpenApiOperationStore { | |
| 11 | + readonly get: ( | |
| 12 | + toolId: ToolId, | |
| 13 | + ) => Effect.Effect<{ binding: OperationBinding; config: InvocationConfig } | null>; | |
| 14 | + | |
| 15 | + readonly put: ( | |
| 16 | + toolId: ToolId, | |
| 17 | + namespace: string, | |
| 18 | + binding: OperationBinding, | |
| 19 | + config: InvocationConfig, | |
| 20 | + ) => Effect.Effect<void>; | |
| 21 | + | |
| 22 | + readonly remove: (toolId: ToolId) => Effect.Effect<void>; | |
| 23 | + | |
| 24 | + /** List all tool IDs for a given namespace */ | |
| 25 | + readonly listByNamespace: (namespace: string) => Effect.Effect<readonly ToolId[]>; | |
| 26 | + | |
| 27 | + /** Remove all entries for a namespace */ | |
| 28 | + readonly removeByNamespace: (namespace: string) => Effect.Effect<readonly ToolId[]>; | |
| 29 | +} | |
| 30 | + | |
| 31 | +// --------------------------------------------------------------------------- | |
| 32 | +// In-memory implementation | |
| 33 | +// --------------------------------------------------------------------------- | |
| 34 | + | |
| 35 | +export const makeInMemoryOperationStore = (): OpenApiOperationStore => { | |
| 36 | + const store = new Map< | |
| 37 | + string, | |
| 38 | + { binding: OperationBinding; config: InvocationConfig; namespace: string } | |
| 39 | + >(); | |
| 40 | + | |
| 41 | + return { | |
| 42 | + get: (toolId) => | |
| 43 | + Effect.sync(() => { | |
| 44 | + const entry = store.get(toolId); | |
| 45 | + return entry ? { binding: entry.binding, config: entry.config } : null; | |
| 46 | + }), | |
| 47 | + | |
| 48 | + put: (toolId, namespace, binding, config) => | |
| 49 | + Effect.sync(() => { | |
| 50 | + store.set(toolId, { binding, config, namespace }); | |
| 51 | + }), | |
| 52 | + | |
| 53 | + remove: (toolId) => | |
| 54 | + Effect.sync(() => { | |
| 55 | + store.delete(toolId); | |
| 56 | + }), | |
| 57 | + | |
| 58 | + listByNamespace: (namespace) => | |
| 59 | + Effect.sync(() => | |
| 60 | + [...store.entries()] | |
| 61 | + .filter(([, v]) => v.namespace === namespace) | |
| 62 | + .map(([k]) => k as ToolId), | |
| 63 | + ), | |
| 64 | + | |
| 65 | + removeByNamespace: (namespace) => | |
| 66 | + Effect.sync(() => { | |
| 67 | + const ids: ToolId[] = []; | |
| 68 | + for (const [k, v] of store) { | |
| 69 | + if (v.namespace === namespace) { | |
| 70 | + ids.push(k as ToolId); | |
| 71 | + store.delete(k); | |
| 72 | + } | |
| 73 | + } | |
| 74 | + return ids; | |
| 75 | + }), | |
| 76 | + }; | |
| 77 | +}; | |
| @@ -1,57 +1,51 @@ | ||
| 1 | −import { Effect, Layer, Option } from "effect"; | |
| 2 | −import { | |
| 3 | − FetchHttpClient, | |
| 4 | − HttpClient, | |
| 5 | − HttpClientRequest, | |
| 6 | −} from "@effect/platform"; | |
| 1 | +import { Effect, Option } from "effect"; | |
| 2 | +import { FetchHttpClient, HttpClient } from "@effect/platform"; | |
| 3 | +import type { Layer } from "effect"; | |
| 7 | 4 | |
| 8 | 5 | import { |
| 9 | 6 | definePlugin, |
| 10 | 7 | type ExecutorPlugin, |
| 11 | 8 | type PluginContext, |
| 12 | 9 | ToolId, |
| 13 | − ToolInvocationResult, | |
| 14 | − ToolInvocationError, | |
| 15 | 10 | type ToolRegistration, |
| 16 | 11 | } from "@executor/sdk"; |
| 17 | 12 | |
| 18 | 13 | import { parse } from "./parse"; |
| 19 | 14 | import { extract } from "./extract"; |
| 20 | −import { invoke } from "./invoke"; | |
| 15 | +import { makeOpenApiInvoker } from "./invoke"; | |
| 16 | +import { resolveBaseUrl } from "./openapi-utils"; | |
| 17 | +import { | |
| 18 | + makeInMemoryOperationStore, | |
| 19 | + type OpenApiOperationStore, | |
| 20 | +} from "./operation-store"; | |
| 21 | 21 | import { |
| 22 | 22 | type AuthConfig, |
| 23 | 23 | type ExtractedOperation, |
| 24 | 24 | InvocationConfig, |
| 25 | 25 | NoAuth, |
| 26 | − type ServerInfo, | |
| 26 | + OperationBinding, | |
| 27 | 27 | } from "./types"; |
| 28 | 28 | |
| 29 | 29 | // --------------------------------------------------------------------------- |
| 30 | −// Plugin config — what you pass when adding a spec | |
| 30 | +// Plugin config | |
| 31 | 31 | // --------------------------------------------------------------------------- |
| 32 | 32 | |
| 33 | 33 | export interface OpenApiSpecConfig { |
| 34 | − /** Raw spec text (JSON or YAML) or a URL to fetch */ | |
| 35 | 34 | readonly spec: string; |
| 36 | − /** Override the base URL (defaults to first server in spec) */ | |
| 37 | 35 | readonly baseUrl?: string; |
| 38 | − /** Namespace prefix for tool IDs (e.g. "stripe" → "stripe.listCustomers") */ | |
| 39 | 36 | readonly namespace?: string; |
| 40 | − /** Auth configuration */ | |
| 41 | 37 | readonly auth?: AuthConfig; |
| 42 | 38 | } |
| 43 | 39 | |
| 44 | 40 | // --------------------------------------------------------------------------- |
| 45 | −// Plugin extension — the public API on executor.openapi | |
| 41 | +// Plugin extension | |
| 46 | 42 | // --------------------------------------------------------------------------- |
| 47 | 43 | |
| 48 | 44 | export interface OpenApiPluginExtension { |
| 49 | − /** Add an OpenAPI spec and register its operations as tools */ | |
| 50 | 45 | readonly addSpec: ( |
| 51 | 46 | config: OpenApiSpecConfig, |
| 52 | 47 | ) => Effect.Effect<{ readonly toolCount: number }, Error>; |
| 53 | 48 | |
| 54 | − /** Remove all tools from a previously added spec by namespace */ | |
| 55 | 49 | readonly removeSpec: (namespace: string) => Effect.Effect<void>; |
| 56 | 50 | } |
| 57 | 51 | |
| @@ -59,21 +53,6 @@ export interface OpenApiPluginExtension { · 1 unmodified line | ||
| 59 | 53 | // Helpers |
| 60 | 54 | // --------------------------------------------------------------------------- |
| 61 | 55 | |
| 62 | −const resolveBaseUrl = ( | |
| 63 | − servers: readonly ServerInfo[], | |
| 64 | −): string => { | |
| 65 | − const server = servers[0]; | |
| 66 | − if (!server) return ""; | |
| 67 | − | |
| 68 | − let url = server.url; | |
| 69 | − if (Option.isSome(server.variables)) { | |
| 70 | − for (const [name, value] of Object.entries(server.variables.value)) { | |
| 71 | − url = url.replaceAll(`{${name}}`, value); | |
| 72 | − } | |
| 73 | − } | |
| 74 | − return url; | |
| 75 | −}; | |
| 76 | − | |
| 77 | 56 | const operationDescription = (op: ExtractedOperation): string => |
| 78 | 57 | Option.getOrElse(op.description, () => |
| 79 | 58 | Option.getOrElse(op.summary, () => |
| @@ -81,130 +60,107 @@ const operationDescription = (op: ExtractedOperation): string => · 1 unmodified line | ||
| 81 | 60 | ), |
| 82 | 61 | ); |
| 83 | 62 | |
| 84 | −const operationToRegistration = ( | |
| 63 | +const toRegistration = ( | |
| 85 | 64 | op: ExtractedOperation, |
| 86 | 65 | namespace: string, |
| 87 | − invocationConfig: InvocationConfig, | |
| 88 | − clientLayer: Layer.Layer<HttpClient.HttpClient>, | |
| 89 | −): ToolRegistration => { | |
| 90 | − const id = ToolId.make(`${namespace}.${op.operationId}`); | |
| 91 | − | |
| 92 | − return { | |
| 93 | − id, | |
| 94 | − name: op.operationId as string, | |
| 95 | − description: operationDescription(op), | |
| 96 | − tags: [...op.tags, "openapi", namespace], | |
| 97 | − inputSchema: Option.getOrUndefined(op.inputSchema), | |
| 98 | − outputSchema: Option.getOrUndefined(op.outputSchema), | |
| 99 | − invoke: (args) => | |
| 100 | − invoke(op, (args ?? {}) as Record<string, unknown>, invocationConfig).pipe( | |
| 101 | − Effect.map( | |
| 102 | − (result) => | |
| 103 | − new ToolInvocationResult({ | |
| 104 | − data: result.data, | |
| 105 | − error: result.error, | |
| 106 | − status: result.status, | |
| 107 | − }), | |
| 108 | − ), | |
| 109 | − Effect.mapError( | |
| 110 | − (err) => | |
| 111 | − new ToolInvocationError({ | |
| 112 | − toolId: id, | |
| 113 | − message: `OpenAPI invocation failed: ${err.message}`, | |
| 114 | − cause: err, | |
| 115 | − }), | |
| 116 | − ), | |
| 117 | − Effect.provide(clientLayer), | |
| 118 | − ), | |
| 119 | − }; | |
| 120 | −}; | |
| 66 | +): ToolRegistration => ({ | |
| 67 | + id: ToolId.make(`${namespace}.${op.operationId}`), | |
| 68 | + pluginKey: "openapi", | |
| 69 | + name: op.operationId as string, | |
| 70 | + description: operationDescription(op), | |
| 71 | + tags: [...op.tags, "openapi", namespace], | |
| 72 | + inputSchema: Option.getOrUndefined(op.inputSchema), | |
| 73 | + outputSchema: Option.getOrUndefined(op.outputSchema), | |
| 74 | +}); | |
| 75 | + | |
| 76 | +const toBinding = (op: ExtractedOperation): OperationBinding => | |
| 77 | + new OperationBinding({ | |
| 78 | + method: op.method, | |
| 79 | + pathTemplate: op.pathTemplate, | |
| 80 | + parameters: [...op.parameters], | |
| 81 | + requestBody: op.requestBody, | |
| 82 | + }); | |
| 121 | 83 | |
| 122 | 84 | // --------------------------------------------------------------------------- |
| 123 | 85 | // Plugin factory |
| 124 | 86 | // --------------------------------------------------------------------------- |
| 125 | 87 | |
| 126 | 88 | export const openApiPlugin = (options?: { |
| 127 | − /** Provide a custom HttpClient layer. Defaults to FetchHttpClient. */ | |
| 128 | 89 | readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>; |
| 90 | + readonly operationStore?: OpenApiOperationStore; | |
| 129 | 91 | }): ExecutorPlugin<"openapi", OpenApiPluginExtension> => { |
| 130 | − const httpClientLayer = | |
| 131 | − options?.httpClientLayer ?? FetchHttpClient.layer; | |
| 132 | − const registeredTools = new Map<string, readonly ToolId[]>(); | |
| 92 | + const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer; | |
| 93 | + const operationStore = options?.operationStore ?? makeInMemoryOperationStore(); | |
| 133 | 94 | |
| 134 | 95 | return definePlugin({ |
| 135 | 96 | key: "openapi", |
| 136 | 97 | init: (ctx: PluginContext) => |
| 137 | − Effect.succeed({ | |
| 138 | − extension: { | |
| 139 | − addSpec: (config: OpenApiSpecConfig) => | |
| 140 | − Effect.gen(function* () { | |
| 141 | − const doc = yield* parse(config.spec); | |
| 142 | − const result = yield* extract(doc); | |
| 143 | − | |
| 144 | − const namespace = | |
| 145 | − config.namespace ?? | |
| 146 | − Option.getOrElse(result.title, () => "api") | |
| 147 | − .toLowerCase() | |
| 148 | − .replace(/[^a-z0-9]+/g, "_"); | |
| 149 | − | |
| 150 | − // Register shared component schemas first | |
| 151 | − const components = doc.components; | |
| 152 | − if (components?.schemas) { | |
| 153 | − yield* ctx.tools.registerDefinitions(components.schemas); | |
| 154 | − } | |
| 155 | − | |
| 156 | − const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers); | |
| 157 | − | |
| 158 | − const auth = config.auth ?? new NoAuth(); | |
| 159 | − const invocationConfig = new InvocationConfig({ baseUrl, auth }); | |
| 160 | − | |
| 161 | − // Build a client layer with the base URL prepended to every request | |
| 162 | − const clientWithBaseUrl = baseUrl | |
| 163 | − ? Layer.effect( | |
| 164 | − HttpClient.HttpClient, | |
| 165 | − Effect.map( | |
| 166 | − HttpClient.HttpClient, | |
| 167 | − HttpClient.mapRequest( | |
| 168 | − HttpClientRequest.prependUrl(baseUrl), | |
| 169 | − ), | |
| 98 | + Effect.gen(function* () { | |
| 99 | + yield* ctx.tools.registerInvoker( | |
| 100 | + "openapi", | |
| 101 | + makeOpenApiInvoker(operationStore, httpClientLayer), | |
| 102 | + ); | |
| 103 | + | |
| 104 | + return { | |
| 105 | + extension: { | |
| 106 | + addSpec: (config: OpenApiSpecConfig) => | |
| 107 | + Effect.gen(function* () { | |
| 108 | + const doc = yield* parse(config.spec); | |
| 109 | + const result = yield* extract(doc); | |
| 110 | + | |
| 111 | + const namespace = | |
| 112 | + config.namespace ?? | |
| 113 | + Option.getOrElse(result.title, () => "api") | |
| 114 | + .toLowerCase() | |
| 115 | + .replace(/[^a-z0-9]+/g, "_"); | |
| 116 | + | |
| 117 | + if (doc.components?.schemas) { | |
| 118 | + yield* ctx.tools.registerDefinitions(doc.components.schemas); | |
| 119 | + } | |
| 120 | + | |
| 121 | + const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers); | |
| 122 | + const invocationConfig = new InvocationConfig({ | |
| 123 | + baseUrl, | |
| 124 | + auth: config.auth ?? new NoAuth(), | |
| 125 | + }); | |
| 126 | + | |
| 127 | + const registrations = result.operations.map((op) => | |
| 128 | + toRegistration(op, namespace), | |
| 129 | + ); | |
| 130 | + | |
| 131 | + yield* Effect.forEach( | |
| 132 | + result.operations, | |
| 133 | + (op) => | |
| 134 | + operationStore.put( | |
| 135 | + ToolId.make(`${namespace}.${op.operationId}`), | |
| 136 | + namespace, | |
| 137 | + toBinding(op), | |
| 138 | + invocationConfig, | |
| 170 | 139 | ), |
| 171 | − ).pipe(Layer.provide(httpClientLayer)) | |
| 172 | − : httpClientLayer; | |
| 173 | − | |
| 174 | − const registrations = result.operations.map((op) => | |
| 175 | − operationToRegistration( | |
| 176 | − op, | |
| 177 | − namespace, | |
| 178 | − invocationConfig, | |
| 179 | − clientWithBaseUrl, | |
| 180 | − ), | |
| 181 | − ); | |
| 182 | − | |
| 183 | − yield* ctx.tools.register(registrations); | |
| 184 | − | |
| 185 | − const toolIds = registrations.map((r) => r.id); | |
| 186 | − registeredTools.set(namespace, toolIds); | |
| 187 | − | |
| 188 | − return { toolCount: registrations.length }; | |
| 189 | − }), | |
| 190 | − | |
| 191 | − removeSpec: (namespace: string) => | |
| 140 | + { discard: true }, | |
| 141 | + ); | |
| 142 | + | |
| 143 | + yield* ctx.tools.register(registrations); | |
| 144 | + return { toolCount: registrations.length }; | |
| 145 | + }), | |
| 146 | + | |
| 147 | + removeSpec: (namespace: string) => | |
| 148 | + Effect.gen(function* () { | |
| 149 | + const toolIds = yield* operationStore.removeByNamespace(namespace); | |
| 150 | + if (toolIds.length > 0) { | |
| 151 | + yield* ctx.tools.unregister(toolIds); | |
| 152 | + } | |
| 153 | + }), | |
| 154 | + }, | |
| 155 | + | |
| 156 | + close: () => | |
| 192 | 157 | Effect.gen(function* () { |
| 193 | − const toolIds = registeredTools.get(namespace); | |
| 194 | − if (toolIds) { | |
| 195 | − yield* ctx.tools.unregister(toolIds); | |
| 196 | − registeredTools.delete(namespace); | |
| 158 | + const allTools = yield* ctx.tools.list({ tags: ["openapi"] }); | |
| 159 | + if (allTools.length > 0) { | |
| 160 | + yield* ctx.tools.unregister(allTools.map((t) => t.id)); | |
| 197 | 161 | } |
| 198 | 162 | }), |
| 199 | − }, | |
| 200 | − | |
| 201 | − close: () => | |
| 202 | − Effect.gen(function* () { | |
| 203 | − for (const toolIds of registeredTools.values()) { | |
| 204 | − yield* ctx.tools.unregister(toolIds); | |
| 205 | − } | |
| 206 | − registeredTools.clear(); | |
| 207 | − }), | |
| 163 | + }; | |
| 208 | 164 | }), |
| 209 | 165 | }); |
| 210 | 166 | }; |
| @@ -104,6 +104,19 @@ export class ExtractionResult extends Schema.Class<ExtractionResult>( · 103 unmodified lines | ||
| 104 | 104 | operations: Schema.Array(ExtractedOperation), |
| 105 | 105 | }) {} |
| 106 | 106 | |
| 107 | +// --------------------------------------------------------------------------- | |
| 108 | +// Operation binding — minimal invocation data (no schemas/metadata) | |
| 109 | +// --------------------------------------------------------------------------- | |
| 110 | + | |
| 111 | +export class OperationBinding extends Schema.Class<OperationBinding>( | |
| 112 | + "OperationBinding", | |
| 113 | +)({ | |
| 114 | + method: HttpMethod, | |
| 115 | + pathTemplate: Schema.String, | |
| 116 | + parameters: Schema.Array(OperationParameter), | |
| 117 | + requestBody: Schema.optionalWith(OperationRequestBody, { as: "Option" }), | |
| 118 | +}) {} | |
| 119 | + | |
| 107 | 120 | // --------------------------------------------------------------------------- |
| 108 | 121 | // Invocation |
| 109 | 122 | // --------------------------------------------------------------------------- |