summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-09 22:20:27 -0400
committerGitHub <[email protected]>2026-04-09 22:20:27 -0400
commit17bd16667c9706c1f210bb36e01a0d23bcdddb02 (patch)
tree3c854ddd5b46bae0321eeb97a2889b8a9d7bbcff /packages
parent16c60c9ee782285530ce88f5f36ea5eb7898d1c2 (diff)
downloadopencode-17bd16667c9706c1f210bb36e01a0d23bcdddb02.tar.gz
opencode-17bd16667c9706c1f210bb36e01a0d23bcdddb02.zip
refactor(effect): move tool descriptions into registry (#21795)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/tool/registry.ts49
-rw-r--r--packages/opencode/src/tool/skill.ts21
-rw-r--r--packages/opencode/src/tool/task.ts16
-rw-r--r--packages/opencode/src/worktree/index.ts6
-rw-r--r--packages/opencode/test/session/prompt-effect.test.ts2
-rw-r--r--packages/opencode/test/session/snapshot-tool-race.test.ts2
-rw-r--r--packages/opencode/test/tool/skill.test.ts21
-rw-r--r--packages/opencode/test/tool/task.test.ts24
8 files changed, 87 insertions, 54 deletions
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 800c45ced..dbb8fb286 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -5,12 +5,12 @@ import { EditTool } from "./edit"
import { GlobTool } from "./glob"
import { GrepTool } from "./grep"
import { ReadTool } from "./read"
-import { TaskDescription, TaskTool } from "./task"
+import { TaskTool } from "./task"
import { TodoWriteTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
-import { SkillDescription, SkillTool } from "./skill"
+import { SkillTool } from "./skill"
import { Tool } from "./tool"
import { Config } from "../config/config"
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
@@ -38,6 +38,8 @@ import { FileTime } from "../file/time"
import { Instruction } from "../session/instruction"
import { AppFileSystem } from "../filesystem"
import { Agent } from "../agent/agent"
+import { Skill } from "../skill"
+import { Permission } from "@/permission"
export namespace ToolRegistry {
const log = Log.create({ service: "tool.registry" })
@@ -73,6 +75,7 @@ export namespace ToolRegistry {
| Question.Service
| Todo.Service
| Agent.Service
+ | Skill.Service
| LSP.Service
| FileTime.Service
| Instruction.Service
@@ -82,6 +85,8 @@ export namespace ToolRegistry {
Effect.gen(function* () {
const config = yield* Config.Service
const plugin = yield* Plugin.Service
+ const agents = yield* Agent.Service
+ const skill = yield* Skill.Service
const task = yield* TaskTool
const read = yield* ReadTool
@@ -199,6 +204,40 @@ export namespace ToolRegistry {
return (yield* all()).map((tool) => tool.id)
})
+ const describeSkill = Effect.fn("ToolRegistry.describeSkill")(function* (agent: Agent.Info) {
+ const list = yield* skill.available(agent)
+ if (list.length === 0) return "No skills are currently available."
+ return [
+ "Load a specialized skill that provides domain-specific instructions and workflows.",
+ "",
+ "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
+ "",
+ "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
+ "",
+ 'Tool output includes a `<skill_content name="...">` block with the loaded content.',
+ "",
+ "The following skills provide specialized sets of instructions for particular tasks",
+ "Invoke this tool to load a skill when a task matches one of the available skills listed below:",
+ "",
+ Skill.fmt(list, { verbose: false }),
+ ].join("\n")
+ })
+
+ const describeTask = Effect.fn("ToolRegistry.describeTask")(function* (agent: Agent.Info) {
+ const items = (yield* agents.list()).filter((item) => item.mode !== "primary")
+ const filtered = items.filter(
+ (item) => Permission.evaluate("task", item.name, agent.permission).action !== "deny",
+ )
+ const list = filtered.toSorted((a, b) => a.name.localeCompare(b.name))
+ const description = list
+ .map(
+ (item) =>
+ `- ${item.name}: ${item.description ?? "This subagent should only be called manually by the user."}`,
+ )
+ .join("\n")
+ return ["Available agent types and the tools they have access to:", description].join("\n")
+ })
+
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
const filtered = (yield* all()).filter((tool) => {
if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) {
@@ -227,8 +266,8 @@ export namespace ToolRegistry {
id: tool.id,
description: [
output.description,
- tool.id === TaskTool.id ? yield* TaskDescription(input.agent) : undefined,
- tool.id === SkillTool.id ? yield* SkillDescription(input.agent) : undefined,
+ tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined,
+ tool.id === SkillTool.id ? yield* describeSkill(input.agent) : undefined,
]
.filter(Boolean)
.join("\n"),
@@ -257,7 +296,9 @@ export namespace ToolRegistry {
Layer.provide(Plugin.defaultLayer),
Layer.provide(Question.defaultLayer),
Layer.provide(Todo.defaultLayer),
+ Layer.provide(Skill.defaultLayer),
Layer.provide(Agent.defaultLayer),
+ Layer.provide(Skill.defaultLayer),
Layer.provide(LSP.defaultLayer),
Layer.provide(FileTime.defaultLayer),
Layer.provide(Instruction.defaultLayer),
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index 276f3931d..e0777d00f 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -1,4 +1,3 @@
-import { Effect } from "effect"
import path from "path"
import { pathToFileURL } from "url"
import z from "zod"
@@ -98,23 +97,3 @@ export const SkillTool = Tool.define("skill", async () => {
},
}
})
-
-export const SkillDescription: Tool.DynamicDescription = (agent) =>
- Effect.gen(function* () {
- const list = yield* Effect.promise(() => Skill.available(agent))
- if (list.length === 0) return "No skills are currently available."
- return [
- "Load a specialized skill that provides domain-specific instructions and workflows.",
- "",
- "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
- "",
- "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
- "",
- 'Tool output includes a `<skill_content name="...">` block with the loaded content.',
- "",
- "The following skills provide specialized sets of instructions for particular tasks",
- "Invoke this tool to load a skill when a task matches one of the available skills listed below:",
- "",
- Skill.fmt(list, { verbose: false }),
- ].join("\n")
- })
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index b97b53bb9..900938f0d 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -7,7 +7,6 @@ import { MessageV2 } from "../session/message-v2"
import { Agent } from "../agent/agent"
import { SessionPrompt } from "../session/prompt"
import { Config } from "../config/config"
-import { Permission } from "@/permission"
import { Effect } from "effect"
import { Log } from "@/util/log"
@@ -176,18 +175,3 @@ export const TaskTool = Tool.defineEffect(
}
}),
)
-
-export const TaskDescription: Tool.DynamicDescription = (agent) =>
- Effect.gen(function* () {
- const items = yield* Effect.promise(() =>
- Agent.list().then((items) => items.filter((item) => item.mode !== "primary")),
- )
- const filtered = items.filter((item) => Permission.evaluate(id, item.name, agent.permission).action !== "deny")
- const list = filtered.toSorted((a, b) => a.name.localeCompare(b.name))
- const description = list
- .map(
- (item) => `- ${item.name}: ${item.description ?? "This subagent should only be called manually by the user."}`,
- )
- .join("\n")
- return ["Available agent types and the tools they have access to:", description].join("\n")
- })
diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts
index b34364ccd..54986d65c 100644
--- a/packages/opencode/src/worktree/index.ts
+++ b/packages/opencode/src/worktree/index.ts
@@ -171,7 +171,7 @@ export namespace Worktree {
export const layer: Layer.Layer<
Service,
never,
- AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Project.Service
+ AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Git.Service | Project.Service
> = Layer.effect(
Service,
Effect.gen(function* () {
@@ -179,6 +179,7 @@ export namespace Worktree {
const fs = yield* AppFileSystem.Service
const pathSvc = yield* Path.Path
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
+ const gitSvc = yield* Git.Service
const project = yield* Project.Service
const git = Effect.fnUntraced(
@@ -516,7 +517,7 @@ export namespace Worktree {
const worktreePath = entry.path
- const base = yield* Effect.promise(() => Git.defaultBranch(Instance.worktree))
+ const base = yield* gitSvc.defaultBranch(Instance.worktree)
if (!base) {
throw new ResetFailedError({ message: "Default branch not found" })
}
@@ -583,6 +584,7 @@ export namespace Worktree {
)
const defaultLayer = layer.pipe(
+ Layer.provide(Git.defaultLayer),
Layer.provide(CrossSpawnSpawner.defaultLayer),
Layer.provide(Project.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts
index 81288f0ca..e9893760c 100644
--- a/packages/opencode/test/session/prompt-effect.test.ts
+++ b/packages/opencode/test/session/prompt-effect.test.ts
@@ -28,6 +28,7 @@ import { SessionPrompt } from "../../src/session/prompt"
import { SessionRunState } from "../../src/session/run-state"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
+import { Skill } from "../../src/skill"
import { Shell } from "../../src/shell/shell"
import { Snapshot } from "../../src/snapshot"
import { ToolRegistry } from "../../src/tool/registry"
@@ -166,6 +167,7 @@ function makeHttp() {
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = ToolRegistry.layer.pipe(
+ Layer.provide(Skill.defaultLayer),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts
index 9cc4d750c..75ba8ef16 100644
--- a/packages/opencode/test/session/snapshot-tool-race.test.ts
+++ b/packages/opencode/test/session/snapshot-tool-race.test.ts
@@ -39,6 +39,7 @@ import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider/provider"
import { Question } from "../../src/question"
+import { Skill } from "../../src/skill"
import { Todo } from "../../src/session/todo"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
@@ -131,6 +132,7 @@ function makeHttp() {
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = ToolRegistry.layer.pipe(
+ Layer.provide(Skill.defaultLayer),
Layer.provideMerge(todo),
Layer.provideMerge(question),
Layer.provideMerge(deps),
diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts
index e6269a4f3..ea9aeeaf9 100644
--- a/packages/opencode/test/tool/skill.test.ts
+++ b/packages/opencode/test/tool/skill.test.ts
@@ -5,7 +5,8 @@ import { pathToFileURL } from "url"
import type { Permission } from "../../src/permission"
import type { Tool } from "../../src/tool/tool"
import { Instance } from "../../src/project/instance"
-import { SkillTool, SkillDescription } from "../../src/tool/skill"
+import { SkillTool } from "../../src/tool/skill"
+import { ToolRegistry } from "../../src/tool/registry"
import { tmpdir } from "../fixture/fixture"
import { SessionID, MessageID } from "../../src/session/schema"
@@ -49,9 +50,11 @@ description: Skill for tool tests.
await Instance.provide({
directory: tmp.path,
fn: async () => {
- const desc = await Effect.runPromise(
- SkillDescription({ name: "build", mode: "primary" as const, permission: [], options: {} }),
- )
+ 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.`)
},
})
@@ -92,8 +95,14 @@ description: ${description}
directory: tmp.path,
fn: async () => {
const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
- const first = await Effect.runPromise(SkillDescription(agent))
- const second = await Effect.runPromise(SkillDescription(agent))
+ 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()
expect(first).toBe(second)
diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts
index 8ebfa59d2..e3e6d58d3 100644
--- a/packages/opencode/test/tool/task.test.ts
+++ b/packages/opencode/test/tool/task.test.ts
@@ -9,7 +9,8 @@ import { MessageV2 } from "../../src/session/message-v2"
import { SessionPrompt } from "../../src/session/prompt"
import { MessageID, PartID } from "../../src/session/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
-import { TaskDescription, TaskTool } from "../../src/tool/task"
+import { TaskTool } from "../../src/tool/task"
+import { ToolRegistry } from "../../src/tool/registry"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -23,7 +24,13 @@ const ref = {
}
const it = testEffect(
- Layer.mergeAll(Agent.defaultLayer, Config.defaultLayer, CrossSpawnSpawner.defaultLayer, Session.defaultLayer),
+ Layer.mergeAll(
+ Agent.defaultLayer,
+ Config.defaultLayer,
+ CrossSpawnSpawner.defaultLayer,
+ Session.defaultLayer,
+ ToolRegistry.defaultLayer,
+ ),
)
const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
@@ -92,8 +99,13 @@ describe("tool.task", () => {
Effect.gen(function* () {
const agent = yield* Agent.Service
const build = yield* agent.get("build")
- const first = yield* TaskDescription(build)
- const second = yield* TaskDescription(build)
+ const registry = yield* ToolRegistry.Service
+ const get = Effect.fnUntraced(function* () {
+ const tools = yield* registry.tools({ ...ref, agent: build })
+ return tools.find((tool) => tool.id === TaskTool.id)?.description ?? ""
+ })
+ const first = yield* get()
+ const second = yield* get()
expect(first).toBe(second)
@@ -130,7 +142,9 @@ describe("tool.task", () => {
Effect.gen(function* () {
const agent = yield* Agent.Service
const build = yield* agent.get("build")
- const description = yield* TaskDescription(build)
+ const registry = yield* ToolRegistry.Service
+ const description =
+ (yield* registry.tools({ ...ref, agent: build })).find((tool) => tool.id === TaskTool.id)?.description ?? ""
expect(description).toContain("- alpha: Alpha agent")
expect(description).not.toContain("- zebra: Zebra agent")