summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-13 11:09:32 -0400
committerGitHub <[email protected]>2026-04-13 11:09:32 -0400
commit9ae8dc2d017316c9ff0b9833926719bf088ce873 (patch)
treeaba5fae1661a5ec103516fb28137c82cc2fc12c9
parent7164662be2fcf72410e3f895c9dee9b564b750a3 (diff)
downloadopencode-9ae8dc2d017316c9ff0b9833926719bf088ce873.tar.gz
opencode-9ae8dc2d017316c9ff0b9833926719bf088ce873.zip
refactor: remove ToolRegistry runtime facade (#22307)
-rw-r--r--packages/opencode/script/seed-e2e.ts8
-rw-r--r--packages/opencode/src/cli/cmd/debug/agent.ts17
-rw-r--r--packages/opencode/src/server/instance/experimental.ts24
-rw-r--r--packages/opencode/src/tool/registry.ts15
-rw-r--r--packages/opencode/test/tool/registry.test.ts263
-rw-r--r--packages/opencode/test/tool/skill.test.ts149
6 files changed, 246 insertions, 230 deletions
diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts
index 6d414ec7f..ec15bbe81 100644
--- a/packages/opencode/script/seed-e2e.ts
+++ b/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()
diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts
index 32d10d5d7..25a32d23b 100644
--- a/packages/opencode/src/cli/cmd/debug/agent.ts
+++ b/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>>) {
diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/instance/experimental.ts
index 464617c69..978aa03a9 100644
--- a/packages/opencode/src/server/instance/experimental.ts
+++ b/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,
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index afb19a468..3ed9e4b18 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/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))
- }
}
diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts
index e3a274bb2..5b59e314e 100644
--- a/packages/opencode/test/tool/registry.test.ts
+++ b/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")
- },
- })
- })
+ }),
+ ),
+ )
})
diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts
index a3873dbeb..1cebf342d 100644
--- a/packages/opencode/test/tool/skill.test.ts
+++ b/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({