diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 800c45ced0..dbb8fb2860 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 `` 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 276f3931d0..e0777d00f7 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 `` 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 b97b53bb9f..900938f0d3 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 b34364ccd8..54986d65cd 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 81288f0ca1..e9893760c9 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 9cc4d750c2..75ba8ef16c 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 e6269a4f38..ea9aeeaf9e 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 8ebfa59d23..e3e6d58d3c 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")