refactor: remove ToolRegistry runtime facade (#22307)
This commit is contained in:
@@ -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,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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
},
|
||||
})
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user