refactor: remove ToolRegistry runtime facade (#22307)

This commit is contained in:
Kit Langton
2026-04-13 11:09:32 -04:00
committed by GitHub
parent 7164662be2
commit 9ae8dc2d01
6 changed files with 243 additions and 227 deletions
+7 -1
View File
@@ -18,6 +18,7 @@ const seed = async () => {
const { Project } = await import("../src/project/project") const { Project } = await import("../src/project/project")
const { ModelID, ProviderID } = await import("../src/provider/schema") const { ModelID, ProviderID } = await import("../src/provider/schema")
const { ToolRegistry } = await import("../src/tool/registry") const { ToolRegistry } = await import("../src/tool/registry")
const { Effect } = await import("effect")
try { try {
await Instance.provide({ await Instance.provide({
@@ -25,7 +26,12 @@ const seed = async () => {
init: () => AppRuntime.runPromise(InstanceBootstrap), init: () => AppRuntime.runPromise(InstanceBootstrap),
fn: async () => { fn: async () => {
await Config.waitForDependencies() 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 session = await Session.create({ title })
const messageID = MessageID.ascending() const messageID = MessageID.ascending()
+12 -5
View File
@@ -12,6 +12,7 @@ import { Permission } from "../../../permission"
import { iife } from "../../../util/iife" import { iife } from "../../../util/iife"
import { bootstrap } from "../../bootstrap" import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd" import { cmd } from "../cmd"
import { AppRuntime } from "@/effect/app-runtime"
export const AgentCommand = cmd({ export const AgentCommand = cmd({
command: "agent <name>", command: "agent <name>",
@@ -71,11 +72,17 @@ export const AgentCommand = cmd({
}) })
async function getAvailableTools(agent: Agent.Info) { async function getAvailableTools(agent: Agent.Info) {
const model = agent.model ?? (await Provider.defaultModel()) return AppRuntime.runPromise(
return ToolRegistry.tools({ Effect.gen(function* () {
...model, const provider = yield* Provider.Service
agent, 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>>) { async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
@@ -162,7 +162,13 @@ export const ExperimentalRoutes = lazy(() =>
}, },
}), }),
async (c) => { 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( .get(
@@ -205,11 +211,17 @@ export const ExperimentalRoutes = lazy(() =>
), ),
async (c) => { async (c) => {
const { provider, model } = c.req.valid("query") const { provider, model } = c.req.valid("query")
const tools = await ToolRegistry.tools({ const tools = await AppRuntime.runPromise(
providerID: ProviderID.make(provider), Effect.gen(function* () {
modelID: ModelID.make(model), const agents = yield* Agent.Service
agent: await Agent.get(await Agent.defaultAgent()), 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( return c.json(
tools.map((t) => ({ tools.map((t) => ({
id: t.id, id: t.id,
-15
View File
@@ -36,7 +36,6 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import { Format } from "../format" import { Format } from "../format"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { Env } from "../env" import { Env } from "../env"
import { Question } from "../question" import { Question } from "../question"
import { Todo } from "../session/todo" import { Todo } from "../session/todo"
@@ -344,18 +343,4 @@ export namespace ToolRegistry {
Layer.provide(Truncate.defaultLayer), 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))
}
} }
+128 -131
View File
@@ -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 path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture" import { Effect, Layer } from "effect"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { ToolRegistry } from "../../src/tool/registry" 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 () => { afterEach(async () => {
await Instance.disposeAll() await Instance.disposeAll()
}) })
describe("tool.registry", () => { describe("tool.registry", () => {
test("loads tools from .opencode/tool (singular)", async () => { it.live("loads tools from .opencode/tool (singular)", () =>
await using tmp = await tmpdir({ provideTmpdirInstance((dir) =>
init: async (dir) => { Effect.gen(function* () {
const opencodeDir = path.join(dir, ".opencode") const opencode = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true }) const tool = path.join(opencode, "tool")
yield* Effect.promise(() => fs.mkdir(tool, { recursive: true }))
const toolDir = path.join(opencodeDir, "tool") yield* Effect.promise(() =>
await fs.mkdir(toolDir, { recursive: true }) Bun.write(
path.join(tool, "hello.ts"),
await Bun.write( [
path.join(toolDir, "hello.ts"), "export default {",
[ " description: 'hello tool',",
"export default {", " args: {},",
" description: 'hello tool',", " execute: async () => {",
" args: {},", " return 'hello world'",
" execute: async () => {", " },",
" return 'hello world'", "}",
" },", "",
"}", ].join("\n"),
"", ),
].join("\n"),
) )
}, const registry = yield* ToolRegistry.Service
}) const ids = yield* registry.ids()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("hello") expect(ids).toContain("hello")
}, }),
}) ),
}) )
test("loads tools from .opencode/tools (plural)", async () => { it.live("loads tools from .opencode/tools (plural)", () =>
await using tmp = await tmpdir({ provideTmpdirInstance((dir) =>
init: async (dir) => { Effect.gen(function* () {
const opencodeDir = path.join(dir, ".opencode") const opencode = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true }) const tools = path.join(opencode, "tools")
yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
const toolsDir = path.join(opencodeDir, "tools") yield* Effect.promise(() =>
await fs.mkdir(toolsDir, { recursive: true }) Bun.write(
path.join(tools, "hello.ts"),
await Bun.write( [
path.join(toolsDir, "hello.ts"), "export default {",
[ " description: 'hello tool',",
"export default {", " args: {},",
" description: 'hello tool',", " execute: async () => {",
" args: {},", " return 'hello world'",
" execute: async () => {", " },",
" return 'hello world'", "}",
" },", "",
"}", ].join("\n"),
"", ),
].join("\n"),
) )
}, const registry = yield* ToolRegistry.Service
}) const ids = yield* registry.ids()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("hello") expect(ids).toContain("hello")
}, }),
}) ),
}) )
test("loads tools with external dependencies without crashing", async () => { it.live("loads tools with external dependencies without crashing", () =>
await using tmp = await tmpdir({ provideTmpdirInstance((dir) =>
init: async (dir) => { Effect.gen(function* () {
const opencodeDir = path.join(dir, ".opencode") const opencode = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true }) const tools = path.join(opencode, "tools")
yield* Effect.promise(() => fs.mkdir(tools, { recursive: true }))
const toolsDir = path.join(opencodeDir, "tools") yield* Effect.promise(() =>
await fs.mkdir(toolsDir, { recursive: true }) Bun.write(
path.join(opencode, "package.json"),
await Bun.write( JSON.stringify({
path.join(opencodeDir, "package.json"), name: "custom-tools",
JSON.stringify({ dependencies: {
name: "custom-tools", "@opencode-ai/plugin": "^0.0.0",
dependencies: { cowsay: "^1.6.0",
"@opencode-ai/plugin": "^0.0.0", },
cowsay: "^1.6.0", }),
}, ),
}),
) )
yield* Effect.promise(() =>
await Bun.write( Bun.write(
path.join(opencodeDir, "package-lock.json"), path.join(opencode, "package-lock.json"),
JSON.stringify({ JSON.stringify({
name: "custom-tools", name: "custom-tools",
lockfileVersion: 3, lockfileVersion: 3,
packages: { packages: {
"": { "": {
dependencies: { dependencies: {
"@opencode-ai/plugin": "^0.0.0", "@opencode-ai/plugin": "^0.0.0",
cowsay: "^1.6.0", cowsay: "^1.6.0",
},
}, },
}, },
}, }),
}), ),
) )
const cowsayDir = path.join(opencodeDir, "node_modules", "cowsay") const cowsay = path.join(opencode, "node_modules", "cowsay")
await fs.mkdir(cowsayDir, { recursive: true }) yield* Effect.promise(() => fs.mkdir(cowsay, { recursive: true }))
await Bun.write( yield* Effect.promise(() =>
path.join(cowsayDir, "package.json"), Bun.write(
JSON.stringify({ path.join(cowsay, "package.json"),
name: "cowsay", JSON.stringify({
type: "module", name: "cowsay",
exports: "./index.js", type: "module",
}), exports: "./index.js",
}),
),
) )
await Bun.write( yield* Effect.promise(() =>
path.join(cowsayDir, "index.js"), Bun.write(
["export function say({ text }) {", " return `moo ${text}`", "}", ""].join("\n"), path.join(cowsay, "index.js"),
["export function say({ text }) {", " return `moo ${text}`", "}", ""].join("\n"),
),
) )
yield* Effect.promise(() =>
await Bun.write( Bun.write(
path.join(toolsDir, "cowsay.ts"), path.join(tools, "cowsay.ts"),
[ [
"import { say } from 'cowsay'", "import { say } from 'cowsay'",
"export default {", "export default {",
" description: 'tool that imports cowsay at top level',", " description: 'tool that imports cowsay at top level',",
" args: { text: { type: 'string' } },", " args: { text: { type: 'string' } },",
" execute: async ({ text }: { text: string }) => {", " execute: async ({ text }: { text: string }) => {",
" return say({ text })", " return say({ text })",
" },", " },",
"}", "}",
"", "",
].join("\n"), ].join("\n"),
),
) )
}, const registry = yield* ToolRegistry.Service
}) const ids = yield* registry.ids()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("cowsay") expect(ids).toContain("cowsay")
}, }),
}) ),
}) )
}) })
+78 -69
View File
@@ -1,3 +1,4 @@
import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
import { Effect, Layer, ManagedRuntime } from "effect" import { Effect, Layer, ManagedRuntime } from "effect"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import { Skill } from "../../src/skill" import { Skill } from "../../src/skill"
@@ -11,8 +12,9 @@ import type { Tool } from "../../src/tool/tool"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { SkillTool } from "../../src/tool/skill" import { SkillTool } from "../../src/tool/skill"
import { ToolRegistry } from "../../src/tool/registry" 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 { SessionID, MessageID } from "../../src/session/schema"
import { testEffect } from "../lib/effect"
const baseCtx: Omit<Tool.Context, "ask"> = { const baseCtx: Omit<Tool.Context, "ask"> = {
sessionID: SessionID.make("ses_test"), sessionID: SessionID.make("ses_test"),
@@ -28,85 +30,94 @@ afterEach(async () => {
await Instance.disposeAll() 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", () => { describe("tool.skill", () => {
test("description lists skill location URL", async () => { it.live("description lists skill location URL", () =>
await using tmp = await tmpdir({ provideTmpdirInstance(
git: true, (dir) =>
init: async (dir) => { Effect.gen(function* () {
const skillDir = path.join(dir, ".opencode", "skill", "tool-skill") const skill = path.join(dir, ".opencode", "skill", "tool-skill")
await Bun.write( yield* Effect.promise(() =>
path.join(skillDir, "SKILL.md"), Bun.write(
`--- path.join(skill, "SKILL.md"),
`---
name: tool-skill name: tool-skill
description: Skill for tool tests. description: Skill for tool tests.
--- ---
# Tool Skill # Tool Skill
`, `,
) ),
}, )
}) 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 },
),
)
const home = process.env.OPENCODE_TEST_HOME it.live("description sorts skills by name and is stable across calls", () =>
process.env.OPENCODE_TEST_HOME = tmp.path provideTmpdirInstance(
(dir) =>
try { Effect.gen(function* () {
await Instance.provide({ for (const [name, description] of [
directory: tmp.path, ["zeta-skill", "Zeta skill."],
fn: async () => { ["alpha-skill", "Alpha skill."],
const desc = await ToolRegistry.tools({ ["middle-skill", "Middle skill."],
providerID: "opencode" as any, ]) {
modelID: "gpt-5" as any, const skill = path.join(dir, ".opencode", "skill", name)
agent: { name: "build", mode: "primary" as const, permission: [], options: {} }, yield* Effect.promise(() =>
}).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "") Bun.write(
expect(desc).toContain(`**tool-skill**: Skill for tool tests.`) path.join(skill, "SKILL.md"),
}, `---
})
} 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"),
`---
name: ${name} name: ${name}
description: ${description} description: ${description}
--- ---
# ${name} # ${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 agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
const load = () => const registry = yield* ToolRegistry.Service
ToolRegistry.tools({ const load = Effect.fnUntraced(function* () {
providerID: "opencode" as any, return (
modelID: "gpt-5" as any, (yield* registry.tools({
agent, providerID: "opencode" as any,
}).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "") modelID: "gpt-5" as any,
const first = await load() agent,
const second = await load() })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
)
})
const first = yield* load()
const second = yield* load()
expect(first).toBe(second) expect(first).toBe(second)
@@ -117,12 +128,10 @@ description: ${description}
expect(alpha).toBeGreaterThan(-1) expect(alpha).toBeGreaterThan(-1)
expect(middle).toBeGreaterThan(alpha) expect(middle).toBeGreaterThan(alpha)
expect(zeta).toBeGreaterThan(middle) expect(zeta).toBeGreaterThan(middle)
}, }),
}) { git: true },
} finally { ),
process.env.OPENCODE_TEST_HOME = home )
}
})
test("execute returns skill content block with files", async () => { test("execute returns skill content block with files", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({