Bläddra i källkod

refactor: remove ToolRegistry runtime facade (#22307)

Kit Langton 4 månader sedan
förälder
incheckning
9ae8dc2d01

+ 7 - 1
packages/opencode/script/seed-e2e.ts

@@ -18,6 +18,7 @@ const seed = async () => {
   const { Project } = await import("../src/project/project")
   const { ModelID, ProviderID } = await import("../src/provider/schema")
   const { ToolRegistry } = await import("../src/tool/registry")
+  const { Effect } = await import("effect")
 
   try {
     await Instance.provide({
@@ -25,7 +26,12 @@ const seed = async () => {
       init: () => AppRuntime.runPromise(InstanceBootstrap),
       fn: async () => {
         await Config.waitForDependencies()
-        await ToolRegistry.ids()
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const registry = yield* ToolRegistry.Service
+            yield* registry.ids()
+          }),
+        )
 
         const session = await Session.create({ title })
         const messageID = MessageID.ascending()

+ 12 - 5
packages/opencode/src/cli/cmd/debug/agent.ts

@@ -12,6 +12,7 @@ import { Permission } from "../../../permission"
 import { iife } from "../../../util/iife"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"
+import { AppRuntime } from "@/effect/app-runtime"
 
 export const AgentCommand = cmd({
   command: "agent <name>",
@@ -71,11 +72,17 @@ export const AgentCommand = cmd({
 })
 
 async function getAvailableTools(agent: Agent.Info) {
-  const model = agent.model ?? (await Provider.defaultModel())
-  return ToolRegistry.tools({
-    ...model,
-    agent,
-  })
+  return AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const provider = yield* Provider.Service
+      const registry = yield* ToolRegistry.Service
+      const model = agent.model ?? (yield* provider.defaultModel())
+      return yield* registry.tools({
+        ...model,
+        agent,
+      })
+    }),
+  )
 }
 
 async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {

+ 18 - 6
packages/opencode/src/server/instance/experimental.ts

@@ -162,7 +162,13 @@ export const ExperimentalRoutes = lazy(() =>
         },
       }),
       async (c) => {
-        return c.json(await ToolRegistry.ids())
+        const ids = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const registry = yield* ToolRegistry.Service
+            return yield* registry.ids()
+          }),
+        )
+        return c.json(ids)
       },
     )
     .get(
@@ -205,11 +211,17 @@ export const ExperimentalRoutes = lazy(() =>
       ),
       async (c) => {
         const { provider, model } = c.req.valid("query")
-        const tools = await ToolRegistry.tools({
-          providerID: ProviderID.make(provider),
-          modelID: ModelID.make(model),
-          agent: await Agent.get(await Agent.defaultAgent()),
-        })
+        const tools = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const agents = yield* Agent.Service
+            const registry = yield* ToolRegistry.Service
+            return yield* registry.tools({
+              providerID: ProviderID.make(provider),
+              modelID: ModelID.make(model),
+              agent: yield* agents.get(yield* agents.defaultAgent()),
+            })
+          }),
+        )
         return c.json(
           tools.map((t) => ({
             id: t.id,

+ 0 - 15
packages/opencode/src/tool/registry.ts

@@ -36,7 +36,6 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { Ripgrep } from "../file/ripgrep"
 import { Format } from "../format"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Env } from "../env"
 import { Question } from "../question"
 import { Todo } from "../session/todo"
@@ -344,18 +343,4 @@ export namespace ToolRegistry {
       Layer.provide(Truncate.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))
-  }
 }

+ 130 - 133
packages/opencode/test/tool/registry.test.ts

@@ -1,157 +1,154 @@
-import { afterEach, describe, expect, test } from "bun:test"
+import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
+import { afterEach, describe, expect } from "bun:test"
 import path from "path"
 import fs from "fs/promises"
-import { tmpdir } from "../fixture/fixture"
+import { Effect, Layer } from "effect"
 import { Instance } from "../../src/project/instance"
 import { ToolRegistry } from "../../src/tool/registry"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+const node = NodeChildProcessSpawner.layer.pipe(
+  Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
+)
+
+const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
 
 afterEach(async () => {
   await Instance.disposeAll()
 })
 
 describe("tool.registry", () => {
-  test("loads tools from .opencode/tool (singular)", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        const opencodeDir = path.join(dir, ".opencode")
-        await fs.mkdir(opencodeDir, { recursive: true })
-
-        const toolDir = path.join(opencodeDir, "tool")
-        await fs.mkdir(toolDir, { recursive: true })
-
-        await Bun.write(
-          path.join(toolDir, "hello.ts"),
-          [
-            "export default {",
-            "  description: 'hello tool',",
-            "  args: {},",
-            "  execute: async () => {",
-            "    return 'hello world'",
-            "  },",
-            "}",
-            "",
-          ].join("\n"),
+  it.live("loads tools from .opencode/tool (singular)", () =>
+    provideTmpdirInstance((dir) =>
+      Effect.gen(function* () {
+        const opencode = path.join(dir, ".opencode")
+        const tool = path.join(opencode, "tool")
+        yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(tool, "hello.ts"),
+            [
+              "export default {",
+              "  description: 'hello tool',",
+              "  args: {},",
+              "  execute: async () => {",
+              "    return 'hello world'",
+              "  },",
+              "}",
+              "",
+            ].join("\n"),
+          ),
         )
-      },
-    })
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const ids = await ToolRegistry.ids()
+        const registry = yield* ToolRegistry.Service
+        const ids = yield* registry.ids()
         expect(ids).toContain("hello")
-      },
-    })
-  })
-
-  test("loads tools from .opencode/tools (plural)", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        const opencodeDir = path.join(dir, ".opencode")
-        await fs.mkdir(opencodeDir, { recursive: true })
-
-        const toolsDir = path.join(opencodeDir, "tools")
-        await fs.mkdir(toolsDir, { recursive: true })
-
-        await Bun.write(
-          path.join(toolsDir, "hello.ts"),
-          [
-            "export default {",
-            "  description: 'hello tool',",
-            "  args: {},",
-            "  execute: async () => {",
-            "    return 'hello world'",
-            "  },",
-            "}",
-            "",
-          ].join("\n"),
+      }),
+    ),
+  )
+
+  it.live("loads tools from .opencode/tools (plural)", () =>
+    provideTmpdirInstance((dir) =>
+      Effect.gen(function* () {
+        const opencode = path.join(dir, ".opencode")
+        const tools = path.join(opencode, "tools")
+        yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(tools, "hello.ts"),
+            [
+              "export default {",
+              "  description: 'hello tool',",
+              "  args: {},",
+              "  execute: async () => {",
+              "    return 'hello world'",
+              "  },",
+              "}",
+              "",
+            ].join("\n"),
+          ),
         )
-      },
-    })
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const ids = await ToolRegistry.ids()
+        const registry = yield* ToolRegistry.Service
+        const ids = yield* registry.ids()
         expect(ids).toContain("hello")
-      },
-    })
-  })
-
-  test("loads tools with external dependencies without crashing", async () => {
-    await using tmp = await tmpdir({
-      init: async (dir) => {
-        const opencodeDir = path.join(dir, ".opencode")
-        await fs.mkdir(opencodeDir, { recursive: true })
-
-        const toolsDir = path.join(opencodeDir, "tools")
-        await fs.mkdir(toolsDir, { recursive: true })
-
-        await Bun.write(
-          path.join(opencodeDir, "package.json"),
-          JSON.stringify({
-            name: "custom-tools",
-            dependencies: {
-              "@opencode-ai/plugin": "^0.0.0",
-              cowsay: "^1.6.0",
-            },
-          }),
+      }),
+    ),
+  )
+
+  it.live("loads tools with external dependencies without crashing", () =>
+    provideTmpdirInstance((dir) =>
+      Effect.gen(function* () {
+        const opencode = path.join(dir, ".opencode")
+        const tools = path.join(opencode, "tools")
+        yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(opencode, "package.json"),
+            JSON.stringify({
+              name: "custom-tools",
+              dependencies: {
+                "@opencode-ai/plugin": "^0.0.0",
+                cowsay: "^1.6.0",
+              },
+            }),
+          ),
         )
-
-        await Bun.write(
-          path.join(opencodeDir, "package-lock.json"),
-          JSON.stringify({
-            name: "custom-tools",
-            lockfileVersion: 3,
-            packages: {
-              "": {
-                dependencies: {
-                  "@opencode-ai/plugin": "^0.0.0",
-                  cowsay: "^1.6.0",
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(opencode, "package-lock.json"),
+            JSON.stringify({
+              name: "custom-tools",
+              lockfileVersion: 3,
+              packages: {
+                "": {
+                  dependencies: {
+                    "@opencode-ai/plugin": "^0.0.0",
+                    cowsay: "^1.6.0",
+                  },
                 },
               },
-            },
-          }),
+            }),
+          ),
         )
 
-        const cowsayDir = path.join(opencodeDir, "node_modules", "cowsay")
-        await fs.mkdir(cowsayDir, { recursive: true })
-        await Bun.write(
-          path.join(cowsayDir, "package.json"),
-          JSON.stringify({
-            name: "cowsay",
-            type: "module",
-            exports: "./index.js",
-          }),
+        const cowsay = path.join(opencode, "node_modules", "cowsay")
+        yield* Effect.promise(() => fs.mkdir(cowsay, { recursive: true }))
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(cowsay, "package.json"),
+            JSON.stringify({
+              name: "cowsay",
+              type: "module",
+              exports: "./index.js",
+            }),
+          ),
         )
-        await Bun.write(
-          path.join(cowsayDir, "index.js"),
-          ["export function say({ text }) {", "  return `moo ${text}`", "}", ""].join("\n"),
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(cowsay, "index.js"),
+            ["export function say({ text }) {", "  return `moo ${text}`", "}", ""].join("\n"),
+          ),
         )
-
-        await Bun.write(
-          path.join(toolsDir, "cowsay.ts"),
-          [
-            "import { say } from 'cowsay'",
-            "export default {",
-            "  description: 'tool that imports cowsay at top level',",
-            "  args: { text: { type: 'string' } },",
-            "  execute: async ({ text }: { text: string }) => {",
-            "    return say({ text })",
-            "  },",
-            "}",
-            "",
-          ].join("\n"),
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(tools, "cowsay.ts"),
+            [
+              "import { say } from 'cowsay'",
+              "export default {",
+              "  description: 'tool that imports cowsay at top level',",
+              "  args: { text: { type: 'string' } },",
+              "  execute: async ({ text }: { text: string }) => {",
+              "    return say({ text })",
+              "  },",
+              "}",
+              "",
+            ].join("\n"),
+          ),
         )
-      },
-    })
-
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const ids = await ToolRegistry.ids()
+        const registry = yield* ToolRegistry.Service
+        const ids = yield* registry.ids()
         expect(ids).toContain("cowsay")
-      },
-    })
-  })
+      }),
+    ),
+  )
 })

+ 79 - 70
packages/opencode/test/tool/skill.test.ts

@@ -1,3 +1,4 @@
+import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
 import { Effect, Layer, ManagedRuntime } from "effect"
 import { Agent } from "../../src/agent/agent"
 import { Skill } from "../../src/skill"
@@ -11,8 +12,9 @@ import type { Tool } from "../../src/tool/tool"
 import { Instance } from "../../src/project/instance"
 import { SkillTool } from "../../src/tool/skill"
 import { ToolRegistry } from "../../src/tool/registry"
-import { tmpdir } from "../fixture/fixture"
+import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
 import { SessionID, MessageID } from "../../src/session/schema"
+import { testEffect } from "../lib/effect"
 
 const baseCtx: Omit<Tool.Context, "ask"> = {
   sessionID: SessionID.make("ses_test"),
@@ -28,85 +30,94 @@ afterEach(async () => {
   await Instance.disposeAll()
 })
 
+const node = NodeChildProcessSpawner.layer.pipe(
+  Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
+)
+
+const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
+
 describe("tool.skill", () => {
-  test("description lists skill location URL", async () => {
-    await using tmp = await tmpdir({
-      git: true,
-      init: async (dir) => {
-        const skillDir = path.join(dir, ".opencode", "skill", "tool-skill")
-        await Bun.write(
-          path.join(skillDir, "SKILL.md"),
-          `---
+  it.live("description lists skill location URL", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          const skill = path.join(dir, ".opencode", "skill", "tool-skill")
+          yield* Effect.promise(() =>
+            Bun.write(
+              path.join(skill, "SKILL.md"),
+              `---
 name: tool-skill
 description: Skill for tool tests.
 ---
 
 # Tool Skill
 `,
-        )
-      },
-    })
-
-    const home = process.env.OPENCODE_TEST_HOME
-    process.env.OPENCODE_TEST_HOME = tmp.path
-
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          const desc = await ToolRegistry.tools({
-            providerID: "opencode" as any,
-            modelID: "gpt-5" as any,
-            agent: { name: "build", mode: "primary" as const, permission: [], options: {} },
-          }).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
-          expect(desc).toContain(`**tool-skill**: Skill for tool tests.`)
-        },
-      })
-    } finally {
-      process.env.OPENCODE_TEST_HOME = home
-    }
-  })
-
-  test("description sorts skills by name and is stable across calls", async () => {
-    await using tmp = await tmpdir({
-      git: true,
-      init: async (dir) => {
-        for (const [name, description] of [
-          ["zeta-skill", "Zeta skill."],
-          ["alpha-skill", "Alpha skill."],
-          ["middle-skill", "Middle skill."],
-        ]) {
-          const skillDir = path.join(dir, ".opencode", "skill", name)
-          await Bun.write(
-            path.join(skillDir, "SKILL.md"),
-            `---
+            ),
+          )
+          const home = process.env.OPENCODE_TEST_HOME
+          process.env.OPENCODE_TEST_HOME = dir
+          yield* Effect.addFinalizer(() =>
+            Effect.sync(() => {
+              process.env.OPENCODE_TEST_HOME = home
+            }),
+          )
+          const registry = yield* ToolRegistry.Service
+          const desc =
+            (yield* registry.tools({
+              providerID: "opencode" as any,
+              modelID: "gpt-5" as any,
+              agent: { name: "build", mode: "primary", permission: [], options: {} },
+            })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
+          expect(desc).toContain("**tool-skill**: Skill for tool tests.")
+        }),
+      { git: true },
+    ),
+  )
+
+  it.live("description sorts skills by name and is stable across calls", () =>
+    provideTmpdirInstance(
+      (dir) =>
+        Effect.gen(function* () {
+          for (const [name, description] of [
+            ["zeta-skill", "Zeta skill."],
+            ["alpha-skill", "Alpha skill."],
+            ["middle-skill", "Middle skill."],
+          ]) {
+            const skill = path.join(dir, ".opencode", "skill", name)
+            yield* Effect.promise(() =>
+              Bun.write(
+                path.join(skill, "SKILL.md"),
+                `---
 name: ${name}
 description: ${description}
 ---
 
 # ${name}
 `,
+              ),
+            )
+          }
+          const home = process.env.OPENCODE_TEST_HOME
+          process.env.OPENCODE_TEST_HOME = dir
+          yield* Effect.addFinalizer(() =>
+            Effect.sync(() => {
+              process.env.OPENCODE_TEST_HOME = home
+            }),
           )
-        }
-      },
-    })
-
-    const home = process.env.OPENCODE_TEST_HOME
-    process.env.OPENCODE_TEST_HOME = tmp.path
 
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
           const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
-          const load = () =>
-            ToolRegistry.tools({
-              providerID: "opencode" as any,
-              modelID: "gpt-5" as any,
-              agent,
-            }).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
-          const first = await load()
-          const second = await load()
+          const registry = yield* ToolRegistry.Service
+          const load = Effect.fnUntraced(function* () {
+            return (
+              (yield* registry.tools({
+                providerID: "opencode" as any,
+                modelID: "gpt-5" as any,
+                agent,
+              })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
+            )
+          })
+          const first = yield* load()
+          const second = yield* load()
 
           expect(first).toBe(second)
 
@@ -117,12 +128,10 @@ description: ${description}
           expect(alpha).toBeGreaterThan(-1)
           expect(middle).toBeGreaterThan(alpha)
           expect(zeta).toBeGreaterThan(middle)
-        },
-      })
-    } finally {
-      process.env.OPENCODE_TEST_HOME = home
-    }
-  })
+        }),
+      { git: true },
+    ),
+  )
 
   test("execute returns skill content block with files", async () => {
     await using tmp = await tmpdir({