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 { 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
View File
@@ -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>>) {
@@ -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,
-15
View File
@@ -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))
}
}
+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 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")
},
})
})
}),
),
)
})
+78 -69
View File
@@ -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 = 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
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"),
`---
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({