export * as ToolRegistry from "./registry" import { Tool, ToolFailure, ToolOutput, ToolResultValue as ToolResult, type Tool as TypedTool, type ToolCall, type ToolResultValue, type ToolSchema, type ToolSettlement, } from "@opencode-ai/llm" import { Context, Effect, Layer, Schema, Scope } from "effect" import { castDraft, enableMapSet } from "immer" import { PermissionV2 } from "../permission" import { State } from "../state" import { SessionSchema } from "../session/schema" import type { SessionV2 } from "../session" import { ApplicationTools } from "./application-tools" import { AgentV2 } from "../agent" export type ExecuteInput = { readonly sessionID: SessionSchema.ID readonly agent?: AgentV2.ID readonly call: ToolCall } /** * Narrow cross-cutting context for one registry invocation. Leaf tools retain * ownership of sequence-sensitive policy decisions; the registry only binds * identity and shared helper behavior consistently. * * TODO: Add `source` when the runner can pass the durable owning assistant * message ID alongside the call ID. Do not infer it from the tool call alone. * TODO: Add cancellation and progress only when the runner exposes a real * signal and durable/live progress sink. */ export type Invocation = ExecuteInput & { readonly source?: PermissionV2.Source readonly assertPermission: ( input: Omit, ) => Effect.Effect } /** Kept as the leaf entry input name for backwards-compatible execute usage. */ export type AuthorizeInput = Invocation & { readonly parameters: Parameters } export type Entry< Parameters extends ToolSchema = ToolSchema, Success extends ToolSchema = ToolSchema, > = { readonly tool: TypedTool readonly authorize?: (input: AuthorizeInput>) => Effect.Effect readonly execute?: ( input: AuthorizeInput>, ) => Effect.Effect, ToolFailure> } type Data = { readonly entries: Map } export type Editor = { readonly list: () => ReadonlyArray readonly get: (name: string) => Entry | undefined readonly set: , Success extends ToolSchema>( name: string, entry: Entry, ) => void readonly remove: (name: string) => void } export interface Interface { readonly transform: State.Interface["transform"] readonly contribute: (update: State.Transform) => Effect.Effect readonly definitions: () => Effect.Effect[number]>> readonly execute: (input: ExecuteInput) => Effect.Effect readonly settle: (input: ExecuteInput) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/ToolRegistry") {} enableMapSet() export const layer = Layer.effect( Service, Effect.gen(function* () { const permission = yield* PermissionV2.Service const applications = yield* ApplicationTools.Service const state = State.create({ initial: () => ({ entries: new Map() }), editor: (draft) => ({ list: () => Array.from(draft.entries.entries()) as Array<[string, Entry]>, get: (name) => draft.entries.get(name) as Entry | undefined, set: (name, entry) => { draft.entries.set( name, castDraft(entry) as typeof draft.entries extends Map ? Value : never, ) }, remove: (name) => { draft.entries.delete(name) }, }), }) const definitions = Effect.fn("ToolRegistry.definitions")(function* () { const tools = new Map(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool] as const)) // Location tools own their names. Application tools fill otherwise-unclaimed names. for (const [name, tool] of applications.entries()) { if (!tools.has(name)) tools.set(name, tool.definition) } return Tool.toDefinitions(Object.fromEntries(tools)) }) const entry = (name: string): Entry | undefined => { const local = state.get().entries.get(name) if (local !== undefined) return local const tool = applications.entries().get(name) if (tool === undefined) return return { tool: tool.definition, execute: ({ parameters, sessionID, call }) => tool.execute(parameters, { sessionID, id: call.id, name: call.name }), } } const invocation = (input: ExecuteInput): Invocation => ({ ...input, // Source needs the durable owning assistant message ID, which the registry does not receive yet. assertPermission: (request) => permission.assert({ ...request, sessionID: input.sessionID, ...(input.agent ? { agent: input.agent } : {}) }), }) const settleEntry = Effect.fn("ToolRegistry.settleEntry")(function* ( entry: Entry | undefined, input: ExecuteInput, ) { if (!entry) return { result: { type: "error" as const, value: `Unknown tool: ${input.call.name}` } } if (!entry.execute && !entry.tool.execute) return { result: { type: "error" as const, value: `Tool has no execute handler: ${input.call.name}` } } return yield* entry.tool._decode(input.call.input).pipe( Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })), Effect.flatMap((parameters) => { const context = { ...invocation(input), parameters } const execute = entry.execute?.(context) ?? entry.tool.execute!(parameters, { id: input.call.id, name: input.call.name }) return ( entry.authorize === undefined ? execute : entry.authorize(context).pipe(Effect.andThen(execute)) ).pipe( Effect.flatMap((value) => entry.tool._encode(value).pipe( Effect.mapError( (error) => new ToolFailure({ message: `Tool returned an invalid value for its success schema: ${error.message}`, }), ), ), ), Effect.map((value): ToolSettlement => { if (entry.tool._legacyResult && ToolResult.is(value)) return { result: value, output: ToolOutput.fromResultValue(value) } const output = entry.tool._project(parameters, input.call.id, value) const result = ToolOutput.toResultValue(output) return result.type === "error" ? { result } : { result, output } }), ) }), Effect.catchTag("LLM.ToolFailure", (failure) => Effect.succeed({ result: { type: "error" as const, value: failure.message } }), ), ) }) const settle = Effect.fn("ToolRegistry.settle")((input: ExecuteInput) => settleEntry(entry(input.call.name), input)) const execute = Effect.fn("ToolRegistry.execute")(function* (input: ExecuteInput) { return (yield* settle(input)).result }) return Service.of({ transform: state.transform, contribute: Effect.fn("ToolRegistry.contribute")(function* (update) { const transform = yield* state.transform() yield* transform(update) }), definitions, execute, settle, }) }), ) export const defaultLayer = layer.pipe(Layer.provide(ApplicationTools.layer))