diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index 7f451e98c0..022e4ac988 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -71,7 +71,7 @@ export const AgentCommand = cmd({ async function getAvailableTools(agent: Agent.Info) { const model = agent.model ?? (await Provider.defaultModel()) - return ToolRegistry.tools(model, agent) + return ToolRegistry.tools(model) } async function resolveTools(agent: Agent.Info, availableTools: Awaited>) { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 24996c8d4b..66ce25da2e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -11,7 +11,6 @@ import { Provider } from "../provider/provider" import { ModelID, ProviderID } from "../provider/schema" import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai" import { SessionCompaction } from "./compaction" -import { Instance } from "../project/instance" import { Bus } from "../bus" import { ProviderTransform } from "../provider/transform" import { SystemPrompt } from "./system" @@ -24,7 +23,6 @@ import { ToolRegistry } from "../tool/registry" import { Runner } from "@/effect/runner" import { MCP } from "../mcp" import { LSP } from "../lsp" -import { ReadTool } from "../tool/read" import { FileTime } from "../file/time" import { Flag } from "../flag/flag" import { ulid } from "ulid" @@ -37,7 +35,6 @@ import { ConfigMarkdown } from "../config/markdown" import { SessionSummary } from "./summary" import { NamedError } from "@opencode-ai/util/error" import { SessionProcessor } from "./processor" -import { TaskTool } from "@/tool/task" import { Tool } from "@/tool/tool" import { Permission } from "@/permission" import { SessionStatus } from "./status" @@ -50,6 +47,8 @@ import { Process } from "@/util/process" import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" +import { TaskTool } from "@/tool/task" +import { ReadTool } from "@/tool/read" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -433,10 +432,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the ), }) - for (const item of yield* registry.tools( - { modelID: ModelID.make(input.model.api.id), providerID: input.model.providerID }, - input.agent, - )) { + for (const item of yield* registry.tools({ + modelID: ModelID.make(input.model.api.id), + providerID: input.model.providerID, + })) { const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters)) tools[item.id] = tool({ id: item.id as any, @@ -560,7 +559,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the }) { const { task, model, lastUser, sessionID, session, msgs } = input const ctx = yield* InstanceState.context - const taskTool = yield* Effect.promise(() => registry.named.task.init()) + const taskTool = yield* registry.fromID(TaskTool.id) const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({ id: MessageID.ascending(), @@ -583,7 +582,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the sessionID: assistantMessage.sessionID, type: "tool", callID: ulid(), - tool: registry.named.task.id, + tool: TaskTool.id, state: { status: "running", input: { @@ -1110,7 +1109,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the text: `Called the Read tool with the following input: ${JSON.stringify(args)}`, }, ] - const read = yield* Effect.promise(() => registry.named.read.init()).pipe( + const read = yield* registry.fromID(ReadTool.id).pipe( Effect.flatMap((t) => provider.getModel(info.model.providerID, info.model.modelID).pipe( Effect.flatMap((mdl) => @@ -1174,7 +1173,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (part.mime === "application/x-directory") { const args = { filePath: filepath } - const result = yield* Effect.promise(() => registry.named.read.init()).pipe( + const result = yield* registry.fromID(ReadTool.id).pipe( Effect.flatMap((t) => Effect.promise(() => t.execute(args, { diff --git a/packages/opencode/src/tool/question.ts b/packages/opencode/src/tool/question.ts index dd99688880..23c9b35c89 100644 --- a/packages/opencode/src/tool/question.ts +++ b/packages/opencode/src/tool/question.ts @@ -41,6 +41,6 @@ export const QuestionTool = Tool.defineEffect + } }), ) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index b538a9e880..566ec08c7e 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -12,10 +12,8 @@ import { WebFetchTool } from "./webfetch" import { WriteTool } from "./write" import { InvalidTool } from "./invalid" import { SkillTool } from "./skill" -import type { Agent } from "../agent/agent" import { Tool } from "./tool" import { Config } from "../config/config" -import path from "path" import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin" import z from "zod" import { Plugin } from "../plugin" @@ -35,24 +33,21 @@ import { makeRuntime } from "@/effect/run-service" import { Env } from "../env" import { Question } from "../question" import { Todo } from "../session/todo" +import path from "path" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) type State = { - custom: Tool.Info[] + custom: Tool.Def[] + builtin: Tool.Def[] } export interface Interface { readonly ids: () => Effect.Effect - readonly named: { - task: Tool.Info - read: Tool.Info - } - readonly tools: ( - model: { providerID: ProviderID; modelID: ModelID }, - agent?: Agent.Info, - ) => Effect.Effect<(Tool.Def & { id: string })[]> + readonly all: () => Effect.Effect + readonly tools: (model: { providerID: ProviderID; modelID: ModelID }) => Effect.Effect + readonly fromID: (id: string) => Effect.Effect } export class Service extends ServiceMap.Service()("@opencode/ToolRegistry") {} @@ -65,33 +60,31 @@ export namespace ToolRegistry { const plugin = yield* Plugin.Service const build = (tool: T | Effect.Effect) => - Effect.isEffect(tool) ? tool : Effect.succeed(tool) + Effect.isEffect(tool) ? tool.pipe(Effect.flatMap(Tool.init)) : Tool.init(tool) const state = yield* InstanceState.make( Effect.fn("ToolRegistry.state")(function* (ctx) { - const custom: Tool.Info[] = [] + const custom: Tool.Def[] = [] - function fromPlugin(id: string, def: ToolDefinition): Tool.Info { + function fromPlugin(id: string, def: ToolDefinition): Tool.Def { return { id, - init: async (initCtx) => ({ - parameters: z.object(def.args), - description: def.description, - execute: async (args, toolCtx) => { - const pluginCtx = { - ...toolCtx, - directory: ctx.directory, - worktree: ctx.worktree, - } as unknown as PluginToolContext - const result = await def.execute(args as any, pluginCtx) - const out = await Truncate.output(result, {}, initCtx?.agent) - return { - title: "", - output: out.truncated ? out.content : result, - metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined }, - } - }, - }), + parameters: z.object(def.args), + description: def.description, + execute: async (args, toolCtx) => { + const pluginCtx = { + ...toolCtx, + directory: ctx.directory, + worktree: ctx.worktree, + } as unknown as PluginToolContext + const result = await def.execute(args as any, pluginCtx) + const out = await Truncate.output(result, {}) + return { + title: "", + output: out.truncated ? out.content : result, + metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined }, + } + }, } } @@ -117,71 +110,60 @@ export namespace ToolRegistry { } } - return { custom } + const cfg = yield* config.get() + const question = + ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL + + return { + custom, + builtin: yield* Effect.forEach( + [ + InvalidTool, + BashTool, + ReadTool, + GlobTool, + GrepTool, + EditTool, + WriteTool, + TaskTool, + WebFetchTool, + TodoWriteTool, + WebSearchTool, + CodeSearchTool, + SkillTool, + ApplyPatchTool, + ...(question ? [QuestionTool] : []), + ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), + ...(cfg.experimental?.batch_tool === true ? [BatchTool] : []), + ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool] : []), + ], + build, + { concurrency: "unbounded" }, + ), + } }), ) - const invalid = yield* build(InvalidTool) - const ask = yield* build(QuestionTool) - const bash = yield* build(BashTool) - const read = yield* build(ReadTool) - const glob = yield* build(GlobTool) - const grep = yield* build(GrepTool) - const edit = yield* build(EditTool) - const write = yield* build(WriteTool) - const task = yield* build(TaskTool) - const fetch = yield* build(WebFetchTool) - const todo = yield* build(TodoWriteTool) - const search = yield* build(WebSearchTool) - const code = yield* build(CodeSearchTool) - const skill = yield* build(SkillTool) - const patch = yield* build(ApplyPatchTool) - const lsp = yield* build(LspTool) - const batch = yield* build(BatchTool) - const plan = yield* build(PlanExitTool) + const all = Effect.fn("ToolRegistry.all")(function* () { + const s = yield* InstanceState.get(state) + return [...s.builtin, ...s.custom] as Tool.Def[] + }) - const all = Effect.fn("ToolRegistry.all")(function* (custom: Tool.Info[]) { - const cfg = yield* config.get() - const question = - ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL - - return [ - invalid, - ...(question ? [ask] : []), - bash, - read, - glob, - grep, - edit, - write, - task, - fetch, - todo, - search, - code, - skill, - patch, - ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [lsp] : []), - ...(cfg.experimental?.batch_tool === true ? [batch] : []), - ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [plan] : []), - ...custom, - ] + const fromID = Effect.fn("ToolRegistry.fromID")(function* (id: string) { + const allTools = yield* all() + const match = allTools.find((tool) => tool.id === id) + if (!match) return yield* Effect.die(`Tool not found: ${id}`) + return match }) const ids = Effect.fn("ToolRegistry.ids")(function* () { - const s = yield* InstanceState.get(state) - const tools = yield* all(s.custom) - return tools.map((t) => t.id) + return yield* all().pipe(Effect.map((t) => t.map((x) => x.id))) }) - const tools = Effect.fn("ToolRegistry.tools")(function* ( - model: { providerID: ProviderID; modelID: ModelID }, - agent?: Agent.Info, - ) { - const s = yield* InstanceState.get(state) - const allTools = yield* all(s.custom) + const tools = Effect.fn("ToolRegistry.tools")(function* (model: { providerID: ProviderID; modelID: ModelID }) { + const allTools = yield* all() const filtered = allTools.filter((tool) => { - if (tool.id === "codesearch" || tool.id === "websearch") { + if (tool.id === CodeSearchTool.id || tool.id === WebSearchTool.id) { return model.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA } @@ -195,27 +177,26 @@ export namespace ToolRegistry { }) return yield* Effect.forEach( filtered, - Effect.fnUntraced(function* (tool: Tool.Info) { + Effect.fnUntraced(function* (tool: Tool.Def) { using _ = log.time(tool.id) - const next = yield* Effect.promise(() => tool.init({ agent })) const output = { - description: next.description, - parameters: next.parameters, + description: tool.description, + parameters: tool.parameters, } yield* plugin.trigger("tool.definition", { toolID: tool.id }, output) return { id: tool.id, description: output.description, parameters: output.parameters, - execute: next.execute, - formatValidationError: next.formatValidationError, + execute: tool.execute, + formatValidationError: tool.formatValidationError, } }), { concurrency: "unbounded" }, ) }) - return Service.of({ ids, named: { task, read }, tools }) + return Service.of({ ids, tools, all, fromID }) }), ) @@ -236,13 +217,10 @@ export namespace ToolRegistry { return runPromise((svc) => svc.ids()) } - export async function tools( - model: { - providerID: ProviderID - modelID: ModelID - }, - agent?: Agent.Info, - ): Promise<(Tool.Def & { id: string })[]> { - return runPromise((svc) => svc.tools(model, agent)) + export async function tools(model: { + providerID: ProviderID + modelID: ModelID + }): Promise<(Tool.Def & { id: string })[]> { + return runPromise((svc) => svc.tools(model)) } } diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index c5a77e58d2..1d2b216542 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -10,8 +10,8 @@ const Parameters = z.object({ name: z.string().describe("The name of the skill from available_skills"), }) -export const SkillTool = Tool.define("skill", async (ctx) => { - const list = await Skill.available(ctx?.agent) +export const SkillTool = Tool.define("skill", async () => { + const list = await Skill.all() const description = list.length === 0 diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index af130a70d9..f19f18f36f 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -4,13 +4,11 @@ import z from "zod" import { Session } from "../session" import { SessionID, MessageID } from "../session/schema" import { MessageV2 } from "../session/message-v2" -import { Identifier } from "../id/id" import { Agent } from "../agent/agent" import { SessionPrompt } from "../session/prompt" import { iife } from "@/util/iife" import { defer } from "@/util/defer" import { Config } from "../config/config" -import { Permission } from "@/permission" const parameters = z.object({ description: z.string().describe("A short (3-5 words) description of the task"), @@ -25,19 +23,12 @@ const parameters = z.object({ command: z.string().describe("The command that triggered this task").optional(), }) -export const TaskTool = Tool.define("task", async (ctx) => { +export const TaskTool = Tool.define("task", async () => { const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary")) - - // Filter agents by permissions if agent provided - const caller = ctx?.agent - const accessibleAgents = caller - ? agents.filter((a) => Permission.evaluate("task", a.name, caller.permission).action !== "deny") - : agents - const list = accessibleAgents.toSorted((a, b) => a.name.localeCompare(b.name)) - const description = DESCRIPTION.replace( "{agents}", - list + agents + .toSorted((a, b) => a.name.localeCompare(b.name)) .map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`) .join("\n"), ) diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts index d10e84931a..92318164c6 100644 --- a/packages/opencode/src/tool/todo.ts +++ b/packages/opencode/src/tool/todo.ts @@ -43,6 +43,6 @@ export const TodoWriteTool = Tool.defineEffect + } satisfies Tool.DefWithoutID }), ) diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index a107dad7e8..2b222cdfb5 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -1,20 +1,16 @@ import z from "zod" import { Effect } from "effect" import type { MessageV2 } from "../session/message-v2" -import type { Agent } from "../agent/agent" import type { Permission } from "../permission" import type { SessionID, MessageID } from "../session/schema" import { Truncate } from "./truncate" +import { Agent } from "@/agent/agent" export namespace Tool { interface Metadata { [key: string]: any } - export interface InitContext { - agent?: Agent.Info - } - export type Context = { sessionID: SessionID messageID: MessageID @@ -26,7 +22,9 @@ export namespace Tool { metadata(input: { title?: string; metadata?: M }): void ask(input: Omit): Promise } + export interface Def { + id: string description: string parameters: Parameters execute( @@ -40,10 +38,14 @@ export namespace Tool { }> formatValidationError?(error: z.ZodError): string } + export type DefWithoutID = Omit< + Def, + "id" + > export interface Info { id: string - init: (ctx?: InitContext) => Promise> + init: () => Promise> } export type InferParameters = @@ -57,10 +59,10 @@ export namespace Tool { function wrap( id: string, - init: ((ctx?: InitContext) => Promise>) | Def, + init: (() => Promise>) | DefWithoutID, ) { - return async (initCtx?: InitContext) => { - const toolInfo = init instanceof Function ? await init(initCtx) : { ...init } + return async () => { + const toolInfo = init instanceof Function ? await init() : { ...init } const execute = toolInfo.execute toolInfo.execute = async (args, ctx) => { try { @@ -78,7 +80,7 @@ export namespace Tool { if (result.metadata.truncated !== undefined) { return result } - const truncated = await Truncate.output(result.output, {}, initCtx?.agent) + const truncated = await Truncate.output(result.output, {}, await Agent.get(ctx.agent)) return { ...result, output: truncated.content, @@ -95,7 +97,7 @@ export namespace Tool { export function define( id: string, - init: ((ctx?: InitContext) => Promise>) | Def, + init: (() => Promise>) | DefWithoutID, ): Info { return { id, @@ -105,8 +107,18 @@ export namespace Tool { export function defineEffect( id: string, - init: Effect.Effect<((ctx?: InitContext) => Promise>) | Def, never, R>, + init: Effect.Effect<(() => Promise>) | DefWithoutID, never, R>, ): Effect.Effect, never, R> { return Effect.map(init, (next) => ({ id, init: wrap(id, next) })) } + + export function init(info: Info): Effect.Effect { + return Effect.gen(function* () { + const init = yield* Effect.promise(() => info.init()) + return { + ...init, + id: info.id, + } + }) + } } diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index aae48a30ab..f1fa2c314d 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -28,9 +28,8 @@ describe("tool.task", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const build = await Agent.get("build") - const first = await TaskTool.init({ agent: build }) - const second = await TaskTool.init({ agent: build }) + const first = await TaskTool.init() + const second = await TaskTool.init() expect(first.description).toBe(second.description)