summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/specs/effect-migration.md2
-rw-r--r--packages/opencode/src/tool/registry.ts290
2 files changed, 169 insertions, 123 deletions
diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md
index d00bc766b..12017b0e4 100644
--- a/packages/opencode/specs/effect-migration.md
+++ b/packages/opencode/specs/effect-migration.md
@@ -162,7 +162,7 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
Still open and likely worth migrating:
- [x] `Plugin`
-- [ ] `ToolRegistry`
+- [x] `ToolRegistry`
- [ ] `Pty`
- [ ] `Worktree`
- [ ] `Bus`
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 6d648a097..6381fcfbc 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -7,14 +7,13 @@ import { GrepTool } from "./grep"
import { BatchTool } from "./batch"
import { ReadTool } from "./read"
import { TaskTool } from "./task"
-import { TodoWriteTool, TodoReadTool } from "./todo"
+import { TodoWriteTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
import { SkillTool } from "./skill"
import type { Agent } from "../agent/agent"
import { Tool } from "./tool"
-import { Instance } from "../project/instance"
import { Config } from "../config/config"
import path from "path"
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
@@ -27,106 +26,186 @@ 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 { pathToFileURL } from "url"
+import { Effect, Layer, ServiceMap } from "effect"
+import { InstanceState } from "@/effect/instance-state"
+import { makeRunPromise } from "@/effect/run-service"
export namespace ToolRegistry {
const log = Log.create({ service: "tool.registry" })
- export const state = Instance.state(async () => {
- const custom = [] as Tool.Info[]
-
- const matches = await Config.directories().then((dirs) =>
- dirs.flatMap((dir) =>
- Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
- ),
- )
- if (matches.length) await Config.waitForDependencies()
- for (const match of matches) {
- const namespace = path.basename(match, path.extname(match))
- const mod = await import(process.platform === "win32" ? match : pathToFileURL(match).href)
- for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
- custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
- }
- }
-
- const plugins = await Plugin.list()
- for (const plugin of plugins) {
- for (const [id, def] of Object.entries(plugin.tool ?? {})) {
- custom.push(fromPlugin(id, def))
- }
- }
-
- return { custom }
- })
-
- function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
- return {
- id,
- init: async (initCtx) => ({
- parameters: z.object(def.args),
- description: def.description,
- execute: async (args, ctx) => {
- const pluginCtx = {
- ...ctx,
- directory: Instance.directory,
- worktree: Instance.worktree,
- } as unknown as PluginToolContext
- const result = await def.execute(args as any, pluginCtx)
- const out = await Truncate.output(result, {}, initCtx?.agent)
- return {
- title: "",
- output: out.truncated ? out.content : result,
- metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined },
- }
- },
- }),
- }
+ type State = {
+ custom: Tool.Info[]
}
- export async function register(tool: Tool.Info) {
- const { custom } = await state()
- const idx = custom.findIndex((t) => t.id === tool.id)
- if (idx >= 0) {
- custom.splice(idx, 1, tool)
- return
- }
- custom.push(tool)
+ export interface Interface {
+ readonly register: (tool: Tool.Info) => Effect.Effect<void>
+ readonly ids: () => Effect.Effect<string[]>
+ readonly tools: (
+ model: { providerID: ProviderID; modelID: ModelID },
+ agent?: Agent.Info,
+ ) => Effect.Effect<(Awaited<ReturnType<Tool.Info["init"]>> & { id: string })[]>
}
- async function all(): Promise<Tool.Info[]> {
- const custom = await state().then((x) => x.custom)
- const config = await Config.get()
- const question = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
-
- return [
- InvalidTool,
- ...(question ? [QuestionTool] : []),
- BashTool,
- ReadTool,
- GlobTool,
- GrepTool,
- EditTool,
- WriteTool,
- TaskTool,
- WebFetchTool,
- TodoWriteTool,
- // TodoReadTool,
- WebSearchTool,
- CodeSearchTool,
- SkillTool,
- ApplyPatchTool,
- ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
- ...(config.experimental?.batch_tool === true ? [BatchTool] : []),
- ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool] : []),
- ...custom,
- ]
+ export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ToolRegistry") {}
+
+ export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const cache = yield* InstanceState.make<State>(
+ Effect.fn("ToolRegistry.state")(function* (ctx) {
+ const custom: Tool.Info[] = []
+
+ function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
+ return {
+ id,
+ init: async (initCtx) => ({
+ 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, {}, initCtx?.agent)
+ return {
+ title: "",
+ output: out.truncated ? out.content : result,
+ metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined },
+ }
+ },
+ }),
+ }
+ }
+
+ yield* Effect.promise(async () => {
+ const matches = await Config.directories().then((dirs) =>
+ dirs.flatMap((dir) =>
+ Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
+ ),
+ )
+ if (matches.length) await Config.waitForDependencies()
+ for (const match of matches) {
+ const namespace = path.basename(match, path.extname(match))
+ const mod = await import(process.platform === "win32" ? match : pathToFileURL(match).href)
+ for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
+ custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
+ }
+ }
+
+ const plugins = await Plugin.list()
+ for (const plugin of plugins) {
+ for (const [id, def] of Object.entries(plugin.tool ?? {})) {
+ custom.push(fromPlugin(id, def))
+ }
+ }
+ })
+
+ return { custom }
+ }),
+ )
+
+ async function all(custom: Tool.Info[]): Promise<Tool.Info[]> {
+ const cfg = await Config.get()
+ const question = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
+
+ return [
+ InvalidTool,
+ ...(question ? [QuestionTool] : []),
+ BashTool,
+ ReadTool,
+ GlobTool,
+ GrepTool,
+ EditTool,
+ WriteTool,
+ TaskTool,
+ WebFetchTool,
+ TodoWriteTool,
+ WebSearchTool,
+ CodeSearchTool,
+ SkillTool,
+ ApplyPatchTool,
+ ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
+ ...(cfg.experimental?.batch_tool === true ? [BatchTool] : []),
+ ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool] : []),
+ ...custom,
+ ]
+ }
+
+ const register = Effect.fn("ToolRegistry.register")(function* (tool: Tool.Info) {
+ const state = yield* InstanceState.get(cache)
+ const idx = state.custom.findIndex((t) => t.id === tool.id)
+ if (idx >= 0) {
+ state.custom.splice(idx, 1, tool)
+ return
+ }
+ state.custom.push(tool)
+ })
+
+ const ids = Effect.fn("ToolRegistry.ids")(function* () {
+ const state = yield* InstanceState.get(cache)
+ const tools = yield* Effect.promise(() => all(state.custom))
+ return tools.map((t) => t.id)
+ })
+
+ const tools = Effect.fn("ToolRegistry.tools")(function* (
+ model: { providerID: ProviderID; modelID: ModelID },
+ agent?: Agent.Info,
+ ) {
+ const state = yield* InstanceState.get(cache)
+ const allTools = yield* Effect.promise(() => all(state.custom))
+ return yield* Effect.promise(() =>
+ Promise.all(
+ allTools
+ .filter((tool) => {
+ // Enable websearch/codesearch for zen users OR via enable flag
+ if (tool.id === "codesearch" || tool.id === "websearch") {
+ return model.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
+ }
+
+ // use apply tool in same format as codex
+ const usePatch =
+ model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4")
+ if (tool.id === "apply_patch") return usePatch
+ if (tool.id === "edit" || tool.id === "write") return !usePatch
+
+ return true
+ })
+ .map(async (tool) => {
+ using _ = log.time(tool.id)
+ const next = await tool.init({ agent })
+ const output = {
+ description: next.description,
+ parameters: next.parameters,
+ }
+ await Plugin.trigger("tool.definition", { toolID: tool.id }, output)
+ return {
+ id: tool.id,
+ ...next,
+ description: output.description,
+ parameters: output.parameters,
+ }
+ }),
+ ),
+ )
+ })
+
+ return Service.of({ register, ids, tools })
+ }),
+ )
+
+ const runPromise = makeRunPromise(Service, layer)
+
+ export async function register(tool: Tool.Info) {
+ return runPromise((svc) => svc.register(tool))
}
export async function ids() {
- return all().then((x) => x.map((t) => t.id))
+ return runPromise((svc) => svc.ids())
}
export async function tools(
@@ -136,39 +215,6 @@ export namespace ToolRegistry {
},
agent?: Agent.Info,
) {
- const tools = await all()
- const result = await Promise.all(
- tools
- .filter((t) => {
- // Enable websearch/codesearch for zen users OR via enable flag
- if (t.id === "codesearch" || t.id === "websearch") {
- return model.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
- }
-
- // use apply tool in same format as codex
- const usePatch =
- model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4")
- if (t.id === "apply_patch") return usePatch
- if (t.id === "edit" || t.id === "write") return !usePatch
-
- return true
- })
- .map(async (t) => {
- using _ = log.time(t.id)
- const tool = await t.init({ agent })
- const output = {
- description: tool.description,
- parameters: tool.parameters,
- }
- await Plugin.trigger("tool.definition", { toolID: t.id }, output)
- return {
- id: t.id,
- ...tool,
- description: output.description,
- parameters: output.parameters,
- }
- }),
- )
- return result
+ return runPromise((svc) => svc.tools(model, agent))
}
}