Skip to content

Commit 17bd166

Browse files
authored
refactor(effect): move tool descriptions into registry (#21795)
1 parent 16c60c9 commit 17bd166

8 files changed

Lines changed: 87 additions & 54 deletions

File tree

packages/opencode/src/tool/registry.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { EditTool } from "./edit"
55
import { GlobTool } from "./glob"
66
import { GrepTool } from "./grep"
77
import { ReadTool } from "./read"
8-
import { TaskDescription, TaskTool } from "./task"
8+
import { TaskTool } from "./task"
99
import { TodoWriteTool } from "./todo"
1010
import { WebFetchTool } from "./webfetch"
1111
import { WriteTool } from "./write"
1212
import { InvalidTool } from "./invalid"
13-
import { SkillDescription, SkillTool } from "./skill"
13+
import { SkillTool } from "./skill"
1414
import { Tool } from "./tool"
1515
import { Config } from "../config/config"
1616
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
@@ -38,6 +38,8 @@ import { FileTime } from "../file/time"
3838
import { Instruction } from "../session/instruction"
3939
import { AppFileSystem } from "../filesystem"
4040
import { Agent } from "../agent/agent"
41+
import { Skill } from "../skill"
42+
import { Permission } from "@/permission"
4143

4244
export namespace ToolRegistry {
4345
const log = Log.create({ service: "tool.registry" })
@@ -73,6 +75,7 @@ export namespace ToolRegistry {
7375
| Question.Service
7476
| Todo.Service
7577
| Agent.Service
78+
| Skill.Service
7679
| LSP.Service
7780
| FileTime.Service
7881
| Instruction.Service
@@ -82,6 +85,8 @@ export namespace ToolRegistry {
8285
Effect.gen(function* () {
8386
const config = yield* Config.Service
8487
const plugin = yield* Plugin.Service
88+
const agents = yield* Agent.Service
89+
const skill = yield* Skill.Service
8590

8691
const task = yield* TaskTool
8792
const read = yield* ReadTool
@@ -199,6 +204,40 @@ export namespace ToolRegistry {
199204
return (yield* all()).map((tool) => tool.id)
200205
})
201206

207+
const describeSkill = Effect.fn("ToolRegistry.describeSkill")(function* (agent: Agent.Info) {
208+
const list = yield* skill.available(agent)
209+
if (list.length === 0) return "No skills are currently available."
210+
return [
211+
"Load a specialized skill that provides domain-specific instructions and workflows.",
212+
"",
213+
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
214+
"",
215+
"The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
216+
"",
217+
'Tool output includes a `<skill_content name="...">` block with the loaded content.',
218+
"",
219+
"The following skills provide specialized sets of instructions for particular tasks",
220+
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
221+
"",
222+
Skill.fmt(list, { verbose: false }),
223+
].join("\n")
224+
})
225+
226+
const describeTask = Effect.fn("ToolRegistry.describeTask")(function* (agent: Agent.Info) {
227+
const items = (yield* agents.list()).filter((item) => item.mode !== "primary")
228+
const filtered = items.filter(
229+
(item) => Permission.evaluate("task", item.name, agent.permission).action !== "deny",
230+
)
231+
const list = filtered.toSorted((a, b) => a.name.localeCompare(b.name))
232+
const description = list
233+
.map(
234+
(item) =>
235+
`- ${item.name}: ${item.description ?? "This subagent should only be called manually by the user."}`,
236+
)
237+
.join("\n")
238+
return ["Available agent types and the tools they have access to:", description].join("\n")
239+
})
240+
202241
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
203242
const filtered = (yield* all()).filter((tool) => {
204243
if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) {
@@ -227,8 +266,8 @@ export namespace ToolRegistry {
227266
id: tool.id,
228267
description: [
229268
output.description,
230-
tool.id === TaskTool.id ? yield* TaskDescription(input.agent) : undefined,
231-
tool.id === SkillTool.id ? yield* SkillDescription(input.agent) : undefined,
269+
tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined,
270+
tool.id === SkillTool.id ? yield* describeSkill(input.agent) : undefined,
232271
]
233272
.filter(Boolean)
234273
.join("\n"),
@@ -257,7 +296,9 @@ export namespace ToolRegistry {
257296
Layer.provide(Plugin.defaultLayer),
258297
Layer.provide(Question.defaultLayer),
259298
Layer.provide(Todo.defaultLayer),
299+
Layer.provide(Skill.defaultLayer),
260300
Layer.provide(Agent.defaultLayer),
301+
Layer.provide(Skill.defaultLayer),
261302
Layer.provide(LSP.defaultLayer),
262303
Layer.provide(FileTime.defaultLayer),
263304
Layer.provide(Instruction.defaultLayer),

packages/opencode/src/tool/skill.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Effect } from "effect"
21
import path from "path"
32
import { pathToFileURL } from "url"
43
import z from "zod"
@@ -98,23 +97,3 @@ export const SkillTool = Tool.define("skill", async () => {
9897
},
9998
}
10099
})
101-
102-
export const SkillDescription: Tool.DynamicDescription = (agent) =>
103-
Effect.gen(function* () {
104-
const list = yield* Effect.promise(() => Skill.available(agent))
105-
if (list.length === 0) return "No skills are currently available."
106-
return [
107-
"Load a specialized skill that provides domain-specific instructions and workflows.",
108-
"",
109-
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
110-
"",
111-
"The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
112-
"",
113-
'Tool output includes a `<skill_content name="...">` block with the loaded content.',
114-
"",
115-
"The following skills provide specialized sets of instructions for particular tasks",
116-
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
117-
"",
118-
Skill.fmt(list, { verbose: false }),
119-
].join("\n")
120-
})

packages/opencode/src/tool/task.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { MessageV2 } from "../session/message-v2"
77
import { Agent } from "../agent/agent"
88
import { SessionPrompt } from "../session/prompt"
99
import { Config } from "../config/config"
10-
import { Permission } from "@/permission"
1110
import { Effect } from "effect"
1211
import { Log } from "@/util/log"
1312

@@ -176,18 +175,3 @@ export const TaskTool = Tool.defineEffect(
176175
}
177176
}),
178177
)
179-
180-
export const TaskDescription: Tool.DynamicDescription = (agent) =>
181-
Effect.gen(function* () {
182-
const items = yield* Effect.promise(() =>
183-
Agent.list().then((items) => items.filter((item) => item.mode !== "primary")),
184-
)
185-
const filtered = items.filter((item) => Permission.evaluate(id, item.name, agent.permission).action !== "deny")
186-
const list = filtered.toSorted((a, b) => a.name.localeCompare(b.name))
187-
const description = list
188-
.map(
189-
(item) => `- ${item.name}: ${item.description ?? "This subagent should only be called manually by the user."}`,
190-
)
191-
.join("\n")
192-
return ["Available agent types and the tools they have access to:", description].join("\n")
193-
})

packages/opencode/src/worktree/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,15 @@ export namespace Worktree {
171171
export const layer: Layer.Layer<
172172
Service,
173173
never,
174-
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Project.Service
174+
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Git.Service | Project.Service
175175
> = Layer.effect(
176176
Service,
177177
Effect.gen(function* () {
178178
const scope = yield* Scope.Scope
179179
const fs = yield* AppFileSystem.Service
180180
const pathSvc = yield* Path.Path
181181
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
182+
const gitSvc = yield* Git.Service
182183
const project = yield* Project.Service
183184

184185
const git = Effect.fnUntraced(
@@ -516,7 +517,7 @@ export namespace Worktree {
516517

517518
const worktreePath = entry.path
518519

519-
const base = yield* Effect.promise(() => Git.defaultBranch(Instance.worktree))
520+
const base = yield* gitSvc.defaultBranch(Instance.worktree)
520521
if (!base) {
521522
throw new ResetFailedError({ message: "Default branch not found" })
522523
}
@@ -583,6 +584,7 @@ export namespace Worktree {
583584
)
584585

585586
const defaultLayer = layer.pipe(
587+
Layer.provide(Git.defaultLayer),
586588
Layer.provide(CrossSpawnSpawner.defaultLayer),
587589
Layer.provide(Project.defaultLayer),
588590
Layer.provide(AppFileSystem.defaultLayer),

packages/opencode/test/session/prompt-effect.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { SessionPrompt } from "../../src/session/prompt"
2828
import { SessionRunState } from "../../src/session/run-state"
2929
import { MessageID, PartID, SessionID } from "../../src/session/schema"
3030
import { SessionStatus } from "../../src/session/status"
31+
import { Skill } from "../../src/skill"
3132
import { Shell } from "../../src/shell/shell"
3233
import { Snapshot } from "../../src/snapshot"
3334
import { ToolRegistry } from "../../src/tool/registry"
@@ -166,6 +167,7 @@ function makeHttp() {
166167
const question = Question.layer.pipe(Layer.provideMerge(deps))
167168
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
168169
const registry = ToolRegistry.layer.pipe(
170+
Layer.provide(Skill.defaultLayer),
169171
Layer.provideMerge(todo),
170172
Layer.provideMerge(question),
171173
Layer.provideMerge(deps),

packages/opencode/test/session/snapshot-tool-race.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { Permission } from "../../src/permission"
3939
import { Plugin } from "../../src/plugin"
4040
import { Provider as ProviderSvc } from "../../src/provider/provider"
4141
import { Question } from "../../src/question"
42+
import { Skill } from "../../src/skill"
4243
import { Todo } from "../../src/session/todo"
4344
import { SessionCompaction } from "../../src/session/compaction"
4445
import { Instruction } from "../../src/session/instruction"
@@ -131,6 +132,7 @@ function makeHttp() {
131132
const question = Question.layer.pipe(Layer.provideMerge(deps))
132133
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
133134
const registry = ToolRegistry.layer.pipe(
135+
Layer.provide(Skill.defaultLayer),
134136
Layer.provideMerge(todo),
135137
Layer.provideMerge(question),
136138
Layer.provideMerge(deps),

packages/opencode/test/tool/skill.test.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { pathToFileURL } from "url"
55
import type { Permission } from "../../src/permission"
66
import type { Tool } from "../../src/tool/tool"
77
import { Instance } from "../../src/project/instance"
8-
import { SkillTool, SkillDescription } from "../../src/tool/skill"
8+
import { SkillTool } from "../../src/tool/skill"
9+
import { ToolRegistry } from "../../src/tool/registry"
910
import { tmpdir } from "../fixture/fixture"
1011
import { SessionID, MessageID } from "../../src/session/schema"
1112

@@ -49,9 +50,11 @@ description: Skill for tool tests.
4950
await Instance.provide({
5051
directory: tmp.path,
5152
fn: async () => {
52-
const desc = await Effect.runPromise(
53-
SkillDescription({ name: "build", mode: "primary" as const, permission: [], options: {} }),
54-
)
53+
const desc = await ToolRegistry.tools({
54+
providerID: "opencode" as any,
55+
modelID: "gpt-5" as any,
56+
agent: { name: "build", mode: "primary" as const, permission: [], options: {} },
57+
}).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
5558
expect(desc).toContain(`**tool-skill**: Skill for tool tests.`)
5659
},
5760
})
@@ -92,8 +95,14 @@ description: ${description}
9295
directory: tmp.path,
9396
fn: async () => {
9497
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
95-
const first = await Effect.runPromise(SkillDescription(agent))
96-
const second = await Effect.runPromise(SkillDescription(agent))
98+
const load = () =>
99+
ToolRegistry.tools({
100+
providerID: "opencode" as any,
101+
modelID: "gpt-5" as any,
102+
agent,
103+
}).then((tools) => tools.find((tool) => tool.id === SkillTool.id)?.description ?? "")
104+
const first = await load()
105+
const second = await load()
97106

98107
expect(first).toBe(second)
99108

packages/opencode/test/tool/task.test.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { MessageV2 } from "../../src/session/message-v2"
99
import { SessionPrompt } from "../../src/session/prompt"
1010
import { MessageID, PartID } from "../../src/session/schema"
1111
import { ModelID, ProviderID } from "../../src/provider/schema"
12-
import { TaskDescription, TaskTool } from "../../src/tool/task"
12+
import { TaskTool } from "../../src/tool/task"
13+
import { ToolRegistry } from "../../src/tool/registry"
1314
import { provideTmpdirInstance } from "../fixture/fixture"
1415
import { testEffect } from "../lib/effect"
1516

@@ -23,7 +24,13 @@ const ref = {
2324
}
2425

2526
const it = testEffect(
26-
Layer.mergeAll(Agent.defaultLayer, Config.defaultLayer, CrossSpawnSpawner.defaultLayer, Session.defaultLayer),
27+
Layer.mergeAll(
28+
Agent.defaultLayer,
29+
Config.defaultLayer,
30+
CrossSpawnSpawner.defaultLayer,
31+
Session.defaultLayer,
32+
ToolRegistry.defaultLayer,
33+
),
2734
)
2835

2936
const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
@@ -92,8 +99,13 @@ describe("tool.task", () => {
9299
Effect.gen(function* () {
93100
const agent = yield* Agent.Service
94101
const build = yield* agent.get("build")
95-
const first = yield* TaskDescription(build)
96-
const second = yield* TaskDescription(build)
102+
const registry = yield* ToolRegistry.Service
103+
const get = Effect.fnUntraced(function* () {
104+
const tools = yield* registry.tools({ ...ref, agent: build })
105+
return tools.find((tool) => tool.id === TaskTool.id)?.description ?? ""
106+
})
107+
const first = yield* get()
108+
const second = yield* get()
97109

98110
expect(first).toBe(second)
99111

@@ -130,7 +142,9 @@ describe("tool.task", () => {
130142
Effect.gen(function* () {
131143
const agent = yield* Agent.Service
132144
const build = yield* agent.get("build")
133-
const description = yield* TaskDescription(build)
145+
const registry = yield* ToolRegistry.Service
146+
const description =
147+
(yield* registry.tools({ ...ref, agent: build })).find((tool) => tool.id === TaskTool.id)?.description ?? ""
134148

135149
expect(description).toContain("- alpha: Alpha agent")
136150
expect(description).not.toContain("- zebra: Zebra agent")

0 commit comments

Comments
 (0)