import { PlanExitTool } from "./plan" import { QuestionTool } from "./question" import { EditTool } from "./edit" import { GlobTool } from "./glob" import { GrepTool } from "./grep" import { ReadTool } from "./read" import { TaskDescription, TaskTool } from "./task" import { TodoWriteTool } from "./todo" import { WebFetchTool } from "./webfetch" import { WriteTool } from "./write" import { InvalidTool } from "./invalid" import { SkillDescription, SkillTool } from "./skill" import { Tool } from "./tool" import { Config } from "../config/config" import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin" import z from "zod" import { Plugin } from "../plugin" import { ProviderID, type ModelID } from "../provider/schema" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" import { Flag } from "@/flag/flag" import { Log } from "@/util/log" import { LspTool } from "./lsp" import { Truncate } from "./truncate" import { ApplyPatchTool } from "./apply_patch" import { Glob } from "../util/glob" import path from "path" import { pathToFileURL } from "url" import { Effect, Layer, ServiceMap } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" import { ShellTool } from "./shell/tool" import { ShellToolID } from "./shell/id" import { Env } from "../env" import { Question } from "../question" import { Todo } from "../session/todo" import { LSP } from "../lsp" import { FileTime } from "../file/time" import { Instruction } from "../session/instruction" import { AppFileSystem } from "../filesystem" import { Agent } from "../agent/agent" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) type State = { custom: Tool.Def[] builtin: Tool.Def[] } export interface Interface { readonly ids: () => Effect.Effect readonly all: () => Effect.Effect readonly tools: (model: { providerID: ProviderID modelID: ModelID agent: Agent.Info }) => Effect.Effect readonly fromID: (id: string) => Effect.Effect } export class Service extends ServiceMap.Service()("@opencode/ToolRegistry") {} export const layer: Layer.Layer< Service, never, | Config.Service | Plugin.Service | Question.Service | Todo.Service | LSP.Service | FileTime.Service | Instruction.Service | AppFileSystem.Service > = Layer.effect( Service, Effect.gen(function* () { const config = yield* Config.Service const plugin = yield* Plugin.Service const build = (tool: T | Effect.Effect) => Effect.isEffect(tool) ? tool.pipe(Effect.flatMap(Tool.init)) : Tool.init(tool) const state = yield* InstanceState.make( Effect.fn("ToolRegistry.state")(function* (ctx) { const custom: Tool.Def[] = [] function fromPlugin(id: string, def: ToolDefinition): Tool.Def { return { id, parameters: z.object(def.args), description: def.description, execute: async (args, toolCtx) => { const pluginCtx = { ...toolCtx, directory: ctx.directory, worktree: ctx.worktree, } as unknown as PluginToolContext const result = await def.execute(args as any, pluginCtx) const out = await Truncate.output(result, {}, await Agent.get(toolCtx.agent)) return { title: "", output: out.truncated ? out.content : result, metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined, }, } }, } } const dirs = yield* config.directories() const matches = dirs.flatMap((dir) => Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }), ) if (matches.length) yield* config.waitForDependencies() for (const match of matches) { const namespace = path.basename(match, path.extname(match)) const mod = yield* Effect.promise( () => import(process.platform === "win32" ? match : pathToFileURL(match).href), ) for (const [id, def] of Object.entries(mod)) { custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def)) } } const plugins = yield* plugin.list() for (const p of plugins) { for (const [id, def] of Object.entries(p.tool ?? {})) { custom.push(fromPlugin(id, def)) } } const question = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL return { custom, builtin: yield* Effect.forEach( [ InvalidTool, ShellTool, ReadTool, GlobTool, GrepTool, EditTool, WriteTool, TaskTool, WebFetchTool, TodoWriteTool, WebSearchTool, CodeSearchTool, SkillTool, ApplyPatchTool, ...(question ? [QuestionTool] : []), ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool] : []), ], build, { concurrency: "unbounded" }, ), } }), ) const all: Interface["all"] = Effect.fn("ToolRegistry.all")(function* () { const s = yield* InstanceState.get(state) return [...s.builtin, ...s.custom] as Tool.Def[] }) const fromID: Interface["fromID"] = Effect.fn("ToolRegistry.fromID")(function* (id: string) { const tools = yield* all() const match = tools.find((tool) => tool.id === ShellToolID.normalize(id)) if (!match) return yield* Effect.die(`Tool not found: ${id}`) return match }) const ids: Interface["ids"] = Effect.fn("ToolRegistry.ids")(function* () { return (yield* all()).map((tool) => tool.id) }) const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) { const filtered = (yield* all()).filter((tool) => { if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) { return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA } const usePatch = !!Env.get("OPENCODE_E2E_LLM_URL") || (input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")) if (tool.id === ApplyPatchTool.id) return usePatch if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch return true }) return yield* Effect.forEach( filtered, Effect.fnUntraced(function* (tool: Tool.Def) { using _ = log.time(tool.id) const output = { description: tool.description, parameters: tool.parameters, } yield* plugin.trigger("tool.definition", { toolID: tool.id }, output) return { id: tool.id, description: [ output.description, // TODO: remove this hack tool.id === TaskTool.id ? yield* TaskDescription(input.agent) : undefined, tool.id === SkillTool.id ? yield* SkillDescription(input.agent) : undefined, ] .filter(Boolean) .join("\n"), parameters: output.parameters, execute: tool.execute, formatValidationError: tool.formatValidationError, } }), { concurrency: "unbounded" }, ) }) return Service.of({ ids, tools, all, fromID }) }), ) export const defaultLayer = Layer.unwrap( Effect.sync(() => layer.pipe( Layer.provide(Config.defaultLayer), Layer.provide(Plugin.defaultLayer), Layer.provide(Question.defaultLayer), Layer.provide(Todo.defaultLayer), Layer.provide(LSP.defaultLayer), Layer.provide(FileTime.defaultLayer), Layer.provide(Instruction.defaultLayer), Layer.provide(AppFileSystem.defaultLayer), ), ), ) const { runPromise } = makeRuntime(Service, defaultLayer) export async function ids() { return runPromise((svc) => svc.ids()) } export async function tools(input: { providerID: ProviderID modelID: ModelID agent: Agent.Info }): Promise<(Tool.Def & { id: string })[]> { return runPromise((svc) => svc.tools(input)) } }