|
1 | 1 | export * as ToolRegistry from "./registry" |
2 | 2 |
|
3 | 3 | import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/ai" |
4 | | -import { Context, Effect, Layer, Scope } from "effect" |
| 4 | +import { Context, Effect, Layer, Scope, Semaphore } from "effect" |
5 | 5 | import type { AgentV2 } from "../agent" |
6 | 6 | import { Image } from "../image" |
7 | 7 | import { PermissionV2 } from "../permission" |
@@ -45,6 +45,13 @@ export interface Interface { |
45 | 45 | tools: Readonly<Record<string, AnyTool>>, |
46 | 46 | options?: Tools.RegisterOptions, |
47 | 47 | ) => Effect.Effect<void, RegistrationError, Scope.Scope> |
| 48 | + /** Internal atomic registration capability used by plugin transforms. */ |
| 49 | + readonly registerBatch: ( |
| 50 | + registrations: ReadonlyArray<{ |
| 51 | + readonly tools: Readonly<Record<string, AnyTool>> |
| 52 | + readonly options?: Tools.RegisterOptions |
| 53 | + }>, |
| 54 | + ) => Effect.Effect<void, RegistrationError, Scope.Scope> |
48 | 55 | } |
49 | 56 |
|
50 | 57 | export interface Materialization { |
@@ -107,6 +114,7 @@ const registryLayer = Layer.effect( |
107 | 114 | readonly codemode: boolean |
108 | 115 | } |
109 | 116 | const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>() |
| 117 | + const registrationLock = Semaphore.makeUnsafe(1) |
110 | 118 |
|
111 | 119 | const settleTool = Effect.fn("ToolRegistry.settleTool")(function* (input: ExecuteInput, tool: AnyTool) { |
112 | 120 | // Hooks fire only for hosted/local tools; provider-executed calls never reach settleTool. |
@@ -192,82 +200,111 @@ const registryLayer = Layer.effect( |
192 | 200 | } |
193 | 201 | }) |
194 | 202 |
|
195 | | - return Service.of({ |
196 | | - register: Effect.fn("ToolRegistry.register")(function* (tools, options) { |
197 | | - if (options?.namespace !== undefined) yield* validateNamespace(options.namespace) |
198 | | - const entries = registrationEntries(tools, options?.namespace) |
199 | | - if (entries.length === 0) return |
200 | | - const codemode = options?.codemode ?? true |
201 | | - const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute") |
202 | | - if (reserved) |
203 | | - return yield* Effect.fail( |
204 | | - new RegistrationError({ name: reserved.key, message: 'Tool name "execute" is reserved for CodeMode' }), |
205 | | - ) |
206 | | - yield* Effect.uninterruptible( |
| 203 | + const registerBatch: Interface["registerBatch"] = Effect.fn("ToolRegistry.registerBatch")( |
| 204 | + function* (registrations) { |
| 205 | + const planned = yield* Effect.forEach(registrations, ({ tools, options }) => |
207 | 206 | Effect.gen(function* () { |
208 | | - const token = {} |
209 | | - for (const entry of entries) |
210 | | - local.set(entry.key, [ |
211 | | - ...(local.get(entry.key) ?? []), |
212 | | - { |
213 | | - token, |
214 | | - registration: { |
215 | | - tool: entry.tool, |
216 | | - name: entry.name, |
217 | | - namespace: entry.namespace, |
218 | | - codemode, |
219 | | - }, |
220 | | - }, |
221 | | - ]) |
222 | | - yield* Effect.addFinalizer(() => |
223 | | - Effect.sync(() => { |
224 | | - for (const entry of entries) { |
225 | | - const registrations = |
226 | | - local.get(entry.key)?.filter((registration) => registration.token !== token) ?? [] |
227 | | - if (registrations.length > 0) local.set(entry.key, registrations) |
228 | | - else local.delete(entry.key) |
229 | | - } |
230 | | - }), |
231 | | - ) |
| 207 | + if (options?.namespace !== undefined) yield* validateNamespace(options.namespace) |
| 208 | + const entries = registrationEntries(tools, options?.namespace) |
| 209 | + const codemode = options?.codemode ?? true |
| 210 | + const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute") |
| 211 | + if (reserved) |
| 212 | + return yield* Effect.fail( |
| 213 | + new RegistrationError({ name: reserved.key, message: 'Tool name "execute" is reserved for CodeMode' }), |
| 214 | + ) |
| 215 | + return { entries, codemode } |
232 | 216 | }), |
233 | 217 | ) |
234 | | - }), |
235 | | - materialize: Effect.fn("ToolRegistry.materialize")(function* (permissions) { |
236 | | - const direct = new Map<string, Registration>() |
237 | | - const codemode = new Map<string, Registration>() |
238 | | - const rules = permissions ?? [] |
239 | | - for (const [name, entries] of local) { |
240 | | - const registration = entries.at(-1)?.registration |
241 | | - if (!registration) continue |
242 | | - if (whollyDisabled(permission(registration.tool, name), rules)) continue |
243 | | - if (registration.codemode) codemode.set(name, registration) |
244 | | - else direct.set(name, registration) |
245 | | - } |
246 | | - const execute = |
247 | | - codemode.size > 0 && !whollyDisabled("execute", rules) ? ExecuteTool.create(codemode) : undefined |
248 | | - return { |
249 | | - definitions: [ |
250 | | - ...Array.from(direct, ([name, registration]) => definition(name, registration.tool)), |
251 | | - ...(execute ? [definition("execute", execute)] : []), |
252 | | - ], |
253 | | - settle: (input) => { |
254 | | - if (input.call.name === "execute" && execute) return settleTool(input, execute) |
255 | | - const registration = direct.get(input.call.name) |
256 | | - if (registration) return settleTool(input, registration.tool) |
257 | | - return Effect.succeed({ |
258 | | - result: { type: "error", value: `Unknown tool: ${input.call.name}` }, |
259 | | - error: { type: "tool.unknown", message: `Unknown tool: ${input.call.name}` }, |
260 | | - }) |
| 218 | + if (planned.every((plan) => plan.entries.length === 0)) return |
| 219 | + yield* Effect.uninterruptible( |
| 220 | + registrationLock.withPermit( |
| 221 | + Effect.gen(function* () { |
| 222 | + const token = {} |
| 223 | + for (const { entries, codemode } of planned) |
| 224 | + for (const entry of entries) |
| 225 | + local.set(entry.key, [ |
| 226 | + ...(local.get(entry.key) ?? []), |
| 227 | + { |
| 228 | + token, |
| 229 | + registration: { |
| 230 | + tool: entry.tool, |
| 231 | + name: entry.name, |
| 232 | + namespace: entry.namespace, |
| 233 | + codemode, |
| 234 | + }, |
| 235 | + }, |
| 236 | + ]) |
| 237 | + yield* Effect.addFinalizer(() => |
| 238 | + registrationLock.withPermit( |
| 239 | + Effect.sync(() => { |
| 240 | + for (const { entries } of planned) |
| 241 | + for (const entry of entries) { |
| 242 | + const registrations = |
| 243 | + local.get(entry.key)?.filter((registration) => registration.token !== token) ?? [] |
| 244 | + if (registrations.length > 0) local.set(entry.key, registrations) |
| 245 | + else local.delete(entry.key) |
| 246 | + } |
| 247 | + }), |
| 248 | + ), |
| 249 | + ) |
| 250 | + }), |
| 251 | + ), |
| 252 | + ) |
| 253 | + }, |
| 254 | + ) |
| 255 | + |
| 256 | + return Service.of({ |
| 257 | + register: Effect.fn("ToolRegistry.register")((tools, options) => |
| 258 | + registerBatch([ |
| 259 | + { |
| 260 | + tools, |
| 261 | + ...(options === undefined ? {} : { options }), |
261 | 262 | }, |
262 | | - } |
263 | | - }), |
| 263 | + ]), |
| 264 | + ), |
| 265 | + registerBatch, |
| 266 | + materialize: Effect.fn("ToolRegistry.materialize")((permissions) => |
| 267 | + registrationLock.withPermit( |
| 268 | + Effect.sync(() => { |
| 269 | + const direct = new Map<string, Registration>() |
| 270 | + const codemode = new Map<string, Registration>() |
| 271 | + const rules = permissions ?? [] |
| 272 | + for (const [name, entries] of local) { |
| 273 | + const registration = entries.at(-1)?.registration |
| 274 | + if (!registration) continue |
| 275 | + if (whollyDisabled(permission(registration.tool, name), rules)) continue |
| 276 | + if (registration.codemode) codemode.set(name, registration) |
| 277 | + else direct.set(name, registration) |
| 278 | + } |
| 279 | + const execute = |
| 280 | + codemode.size > 0 && !whollyDisabled("execute", rules) ? ExecuteTool.create(codemode) : undefined |
| 281 | + return { |
| 282 | + definitions: [ |
| 283 | + ...Array.from(direct, ([name, registration]) => definition(name, registration.tool)), |
| 284 | + ...(execute ? [definition("execute", execute)] : []), |
| 285 | + ], |
| 286 | + settle: (input: ExecuteInput) => { |
| 287 | + if (input.call.name === "execute" && execute) return settleTool(input, execute) |
| 288 | + const registration = direct.get(input.call.name) |
| 289 | + if (registration) return settleTool(input, registration.tool) |
| 290 | + return Effect.succeed({ |
| 291 | + result: { type: "error", value: `Unknown tool: ${input.call.name}` }, |
| 292 | + error: { type: "tool.unknown", message: `Unknown tool: ${input.call.name}` }, |
| 293 | + }) |
| 294 | + }, |
| 295 | + } |
| 296 | + }), |
| 297 | + ), |
| 298 | + ), |
264 | 299 | }) |
265 | 300 | }), |
266 | 301 | ) |
267 | 302 |
|
268 | 303 | const layer = Layer.effect( |
269 | 304 | Tools.Service, |
270 | | - Service.use((registry) => Effect.succeed(Tools.Service.of({ register: registry.register }))), |
| 305 | + Service.use((registry) => |
| 306 | + Effect.succeed(Tools.Service.of({ register: registry.register, registerBatch: registry.registerBatch })), |
| 307 | + ), |
271 | 308 | ).pipe(Layer.provideMerge(registryLayer)) |
272 | 309 |
|
273 | 310 | function whollyDisabled(action: string, rules: PermissionV2.Ruleset) { |
|
0 commit comments