summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-15 23:56:54 -0400
committerGitHub <[email protected]>2026-04-16 03:56:54 +0000
commit5011465c8118491fb839e020c78ecc721a377846 (patch)
tree3e579d49cd105bcb84ef927950fc6953c506120f /packages
parentf6cc228684ef9022c93a158b3fd1cd69c677ec1a (diff)
downloadopencode-5011465c8118491fb839e020c78ecc721a377846.tar.gz
opencode-5011465c8118491fb839e020c78ecc721a377846.zip
feat: unwrap tool namespaces to flat exports + barrel (#22762)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/agent/agent.ts2
-rw-r--r--packages/opencode/src/cli/cmd/debug/agent.ts2
-rw-r--r--packages/opencode/src/cli/cmd/run.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx2
-rw-r--r--packages/opencode/src/effect/app-runtime.ts4
-rw-r--r--packages/opencode/src/server/instance/experimental.ts2
-rw-r--r--packages/opencode/src/session/prompt.ts6
-rw-r--r--packages/opencode/src/tool/apply_patch.ts2
-rw-r--r--packages/opencode/src/tool/bash.ts4
-rw-r--r--packages/opencode/src/tool/codesearch.ts2
-rw-r--r--packages/opencode/src/tool/edit.ts2
-rw-r--r--packages/opencode/src/tool/external-directory.ts2
-rw-r--r--packages/opencode/src/tool/glob.ts2
-rw-r--r--packages/opencode/src/tool/grep.ts2
-rw-r--r--packages/opencode/src/tool/index.ts3
-rw-r--r--packages/opencode/src/tool/invalid.ts2
-rw-r--r--packages/opencode/src/tool/lsp.ts2
-rw-r--r--packages/opencode/src/tool/multiedit.ts2
-rw-r--r--packages/opencode/src/tool/plan.ts2
-rw-r--r--packages/opencode/src/tool/question.ts2
-rw-r--r--packages/opencode/src/tool/read.ts2
-rw-r--r--packages/opencode/src/tool/registry.ts536
-rw-r--r--packages/opencode/src/tool/skill.ts2
-rw-r--r--packages/opencode/src/tool/task.ts2
-rw-r--r--packages/opencode/src/tool/todo.ts2
-rw-r--r--packages/opencode/src/tool/tool.ts250
-rw-r--r--packages/opencode/src/tool/truncate.ts254
-rw-r--r--packages/opencode/src/tool/webfetch.ts2
-rw-r--r--packages/opencode/src/tool/websearch.ts2
-rw-r--r--packages/opencode/src/tool/write.ts2
-rw-r--r--packages/opencode/test/agent/agent.test.ts8
-rw-r--r--packages/opencode/test/session/prompt-effect.test.ts4
-rw-r--r--packages/opencode/test/session/snapshot-tool-race.test.ts4
-rw-r--r--packages/opencode/test/tool/apply_patch.test.ts2
-rw-r--r--packages/opencode/test/tool/bash.test.ts2
-rw-r--r--packages/opencode/test/tool/edit.test.ts2
-rw-r--r--packages/opencode/test/tool/external-directory.test.ts2
-rw-r--r--packages/opencode/test/tool/glob.test.ts2
-rw-r--r--packages/opencode/test/tool/grep.test.ts2
-rw-r--r--packages/opencode/test/tool/question.test.ts2
-rw-r--r--packages/opencode/test/tool/read.test.ts4
-rw-r--r--packages/opencode/test/tool/registry.test.ts2
-rw-r--r--packages/opencode/test/tool/skill.test.ts4
-rw-r--r--packages/opencode/test/tool/task.test.ts4
-rw-r--r--packages/opencode/test/tool/tool-define.test.ts4
-rw-r--r--packages/opencode/test/tool/truncation.test.ts2
-rw-r--r--packages/opencode/test/tool/webfetch.test.ts2
-rw-r--r--packages/opencode/test/tool/write.test.ts4
48 files changed, 578 insertions, 581 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index b027c8c94..f7e3a3515 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -4,7 +4,7 @@ import { Provider } from "../provider"
import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai"
import { Instance } from "../project/instance"
-import { Truncate } from "../tool/truncate"
+import { Truncate } from "../tool"
import { Auth } from "../auth"
import { ProviderTransform } from "../provider/transform"
diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts
index 29d6ace59..10b6d5c9e 100644
--- a/packages/opencode/src/cli/cmd/debug/agent.ts
+++ b/packages/opencode/src/cli/cmd/debug/agent.ts
@@ -6,7 +6,7 @@ import { Provider } from "../../../provider"
import { Session } from "../../../session"
import type { MessageV2 } from "../../../session/message-v2"
import { MessageID, PartID } from "../../../session/schema"
-import { ToolRegistry } from "../../../tool/registry"
+import { ToolRegistry } from "../../../tool"
import { Instance } from "../../../project/instance"
import { Permission } from "../../../permission"
import { iife } from "../../../util/iife"
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index da7237237..0874beee1 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -12,7 +12,7 @@ import { Server } from "../../server/server"
import { Provider } from "../../provider"
import { Agent } from "../../agent/agent"
import { Permission } from "../../permission"
-import { Tool } from "../../tool/tool"
+import { Tool } from "../../tool"
import { GlobTool } from "../../tool/glob"
import { GrepTool } from "../../tool/grep"
import { ReadTool } from "../../tool/read"
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 75098b608..1a64c21d0 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -34,7 +34,7 @@ import type {
} from "@opencode-ai/sdk/v2"
import { useLocal } from "@tui/context/local"
import { Locale } from "@/util"
-import type { Tool } from "@/tool/tool"
+import type { Tool } from "@/tool"
import type { ReadTool } from "@/tool/read"
import type { WriteTool } from "@/tool/write"
import { BashTool } from "@/tool/bash"
diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts
index 0b76e96a8..60bbfe0ef 100644
--- a/packages/opencode/src/effect/app-runtime.ts
+++ b/packages/opencode/src/effect/app-runtime.ts
@@ -37,8 +37,8 @@ import { LSP } from "@/lsp"
import { MCP } from "@/mcp"
import { McpAuth } from "@/mcp/auth"
import { Command } from "@/command"
-import { Truncate } from "@/tool/truncate"
-import { ToolRegistry } from "@/tool/registry"
+import { Truncate } from "@/tool"
+import { ToolRegistry } from "@/tool"
import { Format } from "@/format"
import { Project } from "@/project"
import { Vcs } from "@/project"
diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/instance/experimental.ts
index 610d67df0..fe80173a8 100644
--- a/packages/opencode/src/server/instance/experimental.ts
+++ b/packages/opencode/src/server/instance/experimental.ts
@@ -2,7 +2,7 @@ import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { ProviderID, ModelID } from "../../provider/schema"
-import { ToolRegistry } from "../../tool/registry"
+import { ToolRegistry } from "../../tool"
import { Worktree } from "../../worktree"
import { Instance } from "../../project/instance"
import { Project } from "../../project"
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 65fc7c8c7..44073c850 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -19,7 +19,7 @@ import { Plugin } from "../plugin"
import PROMPT_PLAN from "../session/prompt/plan.txt"
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
import MAX_STEPS from "../session/prompt/max-steps.txt"
-import { ToolRegistry } from "../tool/registry"
+import { ToolRegistry } from "../tool"
import { MCP } from "../mcp"
import { LSP } from "../lsp"
import { FileTime } from "../file/time"
@@ -34,13 +34,13 @@ import { ConfigMarkdown } from "../config"
import { SessionSummary } from "./summary"
import { NamedError } from "@opencode-ai/shared/util/error"
import { SessionProcessor } from "./processor"
-import { Tool } from "@/tool/tool"
+import { Tool } from "@/tool"
import { Permission } from "@/permission"
import { SessionStatus } from "./status"
import { LLM } from "./llm"
import { Shell } from "@/shell/shell"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
-import { Truncate } from "@/tool/truncate"
+import { Truncate } from "@/tool"
import { decodeDataUrl } from "@/util/data-url"
import { Process } from "@/util"
import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts
index 2d8a50101..7da7dd255 100644
--- a/packages/opencode/src/tool/apply_patch.ts
+++ b/packages/opencode/src/tool/apply_patch.ts
@@ -1,7 +1,7 @@
import z from "zod"
import * as path from "path"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { Bus } from "../bus"
import { FileWatcher } from "../file/watcher"
import { Instance } from "../project/instance"
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index 1edd75414..6260b2221 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -1,7 +1,7 @@
import z from "zod"
import os from "os"
import { createWriteStream } from "node:fs"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import path from "path"
import DESCRIPTION from "./bash.txt"
import { Log } from "../util"
@@ -15,7 +15,7 @@ import { Flag } from "@/flag/flag"
import { Shell } from "@/shell/shell"
import { BashArity } from "@/permission/arity"
-import { Truncate } from "./truncate"
+import * as Truncate from "./truncate"
import { Plugin } from "@/plugin"
import { Effect, Stream } from "effect"
import { ChildProcess } from "effect/unstable/process"
diff --git a/packages/opencode/src/tool/codesearch.ts b/packages/opencode/src/tool/codesearch.ts
index d4d5779bf..ac9961e25 100644
--- a/packages/opencode/src/tool/codesearch.ts
+++ b/packages/opencode/src/tool/codesearch.ts
@@ -1,7 +1,7 @@
import z from "zod"
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import * as McpExa from "./mcp-exa"
import DESCRIPTION from "./codesearch.txt"
diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts
index 2303618a0..62b96cba8 100644
--- a/packages/opencode/src/tool/edit.ts
+++ b/packages/opencode/src/tool/edit.ts
@@ -6,7 +6,7 @@
import z from "zod"
import * as path from "path"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { LSP } from "../lsp"
import { createTwoFilesPatch, diffLines } from "diff"
import DESCRIPTION from "./edit.txt"
diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts
index 810206f81..88b73da50 100644
--- a/packages/opencode/src/tool/external-directory.ts
+++ b/packages/opencode/src/tool/external-directory.ts
@@ -2,7 +2,7 @@ import path from "path"
import { Effect } from "effect"
import { EffectLogger } from "@/effect"
import { InstanceState } from "@/effect"
-import type { Tool } from "./tool"
+import type * as Tool from "./tool"
import { Instance } from "../project/instance"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts
index 0a0a8f1e2..673bb9cc8 100644
--- a/packages/opencode/src/tool/glob.ts
+++ b/packages/opencode/src/tool/glob.ts
@@ -7,7 +7,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Ripgrep } from "../file/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./glob.txt"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
export const GlobTool = Tool.define(
"glob",
diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts
index b6b4a063f..caa75edad 100644
--- a/packages/opencode/src/tool/grep.ts
+++ b/packages/opencode/src/tool/grep.ts
@@ -6,7 +6,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Ripgrep } from "../file/ripgrep"
import { assertExternalDirectoryEffect } from "./external-directory"
import DESCRIPTION from "./grep.txt"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
const MAX_LINE_LENGTH = 2000
diff --git a/packages/opencode/src/tool/index.ts b/packages/opencode/src/tool/index.ts
new file mode 100644
index 000000000..5b2463b50
--- /dev/null
+++ b/packages/opencode/src/tool/index.ts
@@ -0,0 +1,3 @@
+export * as Truncate from "./truncate"
+export * as ToolRegistry from "./registry"
+export * as Tool from "./tool"
diff --git a/packages/opencode/src/tool/invalid.ts b/packages/opencode/src/tool/invalid.ts
index b9794ed5f..aca3618b6 100644
--- a/packages/opencode/src/tool/invalid.ts
+++ b/packages/opencode/src/tool/invalid.ts
@@ -1,6 +1,6 @@
import z from "zod"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
export const InvalidTool = Tool.define(
"invalid",
diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts
index 36cab3c1c..263bfe81d 100644
--- a/packages/opencode/src/tool/lsp.ts
+++ b/packages/opencode/src/tool/lsp.ts
@@ -1,6 +1,6 @@
import z from "zod"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import path from "path"
import { LSP } from "../lsp"
import DESCRIPTION from "./lsp.txt"
diff --git a/packages/opencode/src/tool/multiedit.ts b/packages/opencode/src/tool/multiedit.ts
index 449df3343..004d3c870 100644
--- a/packages/opencode/src/tool/multiedit.ts
+++ b/packages/opencode/src/tool/multiedit.ts
@@ -1,6 +1,6 @@
import z from "zod"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { EditTool } from "./edit"
import DESCRIPTION from "./multiedit.txt"
import path from "path"
diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts
index cc52c2abd..fd7276e09 100644
--- a/packages/opencode/src/tool/plan.ts
+++ b/packages/opencode/src/tool/plan.ts
@@ -1,7 +1,7 @@
import z from "zod"
import path from "path"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { Question } from "../question"
import { Session } from "../session"
import { MessageV2 } from "../session/message-v2"
diff --git a/packages/opencode/src/tool/question.ts b/packages/opencode/src/tool/question.ts
index 50e4b1c51..e5bb33aa6 100644
--- a/packages/opencode/src/tool/question.ts
+++ b/packages/opencode/src/tool/question.ts
@@ -1,6 +1,6 @@
import z from "zod"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { Question } from "../question"
import DESCRIPTION from "./question.txt"
diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts
index 4dc984d0e..c6d1461cd 100644
--- a/packages/opencode/src/tool/read.ts
+++ b/packages/opencode/src/tool/read.ts
@@ -4,7 +4,7 @@ import { createReadStream } from "fs"
import { open } from "fs/promises"
import * as path from "path"
import { createInterface } from "readline"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { LSP } from "../lsp"
import { FileTime } from "../file/time"
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 80115884d..fa442fd3a 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -12,7 +12,7 @@ import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
import { SkillTool } from "./skill"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { Config } from "../config"
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
import z from "zod"
@@ -24,7 +24,7 @@ import { CodeSearchTool } from "./codesearch"
import { Flag } from "@/flag/flag"
import { Log } from "@/util"
import { LspTool } from "./lsp"
-import { Truncate } from "./truncate"
+import * as Truncate from "./truncate"
import { ApplyPatchTool } from "./apply_patch"
import { Glob } from "@opencode-ai/shared/util/glob"
import path from "path"
@@ -47,299 +47,297 @@ import { Agent } from "../agent/agent"
import { Skill } from "../skill"
import { Permission } from "@/permission"
-export namespace ToolRegistry {
- const log = Log.create({ service: "tool.registry" })
+const log = Log.create({ service: "tool.registry" })
- type TaskDef = Tool.InferDef<typeof TaskTool>
- type ReadDef = Tool.InferDef<typeof ReadTool>
+type TaskDef = Tool.InferDef<typeof TaskTool>
+type ReadDef = Tool.InferDef<typeof ReadTool>
- type State = {
- custom: Tool.Def[]
- builtin: Tool.Def[]
- task: TaskDef
- read: ReadDef
- }
+type State = {
+ custom: Tool.Def[]
+ builtin: Tool.Def[]
+ task: TaskDef
+ read: ReadDef
+}
- export interface Interface {
- readonly ids: () => Effect.Effect<string[]>
- readonly all: () => Effect.Effect<Tool.Def[]>
- readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
- readonly tools: (model: {
- providerID: ProviderID
- modelID: ModelID
- agent: Agent.Info
- }) => Effect.Effect<Tool.Def[]>
- }
+export interface Interface {
+ readonly ids: () => Effect.Effect<string[]>
+ readonly all: () => Effect.Effect<Tool.Def[]>
+ readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
+ readonly tools: (model: {
+ providerID: ProviderID
+ modelID: ModelID
+ agent: Agent.Info
+ }) => Effect.Effect<Tool.Def[]>
+}
- export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/ToolRegistry") {}
- export const layer: Layer.Layer<
- Service,
- never,
- | Config.Service
- | Plugin.Service
- | Question.Service
- | Todo.Service
- | Agent.Service
- | Skill.Service
- | Session.Service
- | Provider.Service
- | LSP.Service
- | FileTime.Service
- | Instruction.Service
- | AppFileSystem.Service
- | Bus.Service
- | HttpClient.HttpClient
- | ChildProcessSpawner
- | Ripgrep.Service
- | Format.Service
- | Truncate.Service
- > = Layer.effect(
- Service,
- Effect.gen(function* () {
- const config = yield* Config.Service
- const plugin = yield* Plugin.Service
- const agents = yield* Agent.Service
- const skill = yield* Skill.Service
- const truncate = yield* Truncate.Service
+export const layer: Layer.Layer<
+ Service,
+ never,
+ | Config.Service
+ | Plugin.Service
+ | Question.Service
+ | Todo.Service
+ | Agent.Service
+ | Skill.Service
+ | Session.Service
+ | Provider.Service
+ | LSP.Service
+ | FileTime.Service
+ | Instruction.Service
+ | AppFileSystem.Service
+ | Bus.Service
+ | HttpClient.HttpClient
+ | ChildProcessSpawner
+ | Ripgrep.Service
+ | Format.Service
+ | Truncate.Service
+> = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const config = yield* Config.Service
+ const plugin = yield* Plugin.Service
+ const agents = yield* Agent.Service
+ const skill = yield* Skill.Service
+ const truncate = yield* Truncate.Service
- const invalid = yield* InvalidTool
- const task = yield* TaskTool
- const read = yield* ReadTool
- const question = yield* QuestionTool
- const todo = yield* TodoWriteTool
- const lsptool = yield* LspTool
- const plan = yield* PlanExitTool
- const webfetch = yield* WebFetchTool
- const websearch = yield* WebSearchTool
- const bash = yield* BashTool
- const codesearch = yield* CodeSearchTool
- const globtool = yield* GlobTool
- const writetool = yield* WriteTool
- const edit = yield* EditTool
- const greptool = yield* GrepTool
- const patchtool = yield* ApplyPatchTool
- const skilltool = yield* SkillTool
- const agent = yield* Agent.Service
+ const invalid = yield* InvalidTool
+ const task = yield* TaskTool
+ const read = yield* ReadTool
+ const question = yield* QuestionTool
+ const todo = yield* TodoWriteTool
+ const lsptool = yield* LspTool
+ const plan = yield* PlanExitTool
+ const webfetch = yield* WebFetchTool
+ const websearch = yield* WebSearchTool
+ const bash = yield* BashTool
+ const codesearch = yield* CodeSearchTool
+ const globtool = yield* GlobTool
+ const writetool = yield* WriteTool
+ const edit = yield* EditTool
+ const greptool = yield* GrepTool
+ const patchtool = yield* ApplyPatchTool
+ const skilltool = yield* SkillTool
+ const agent = yield* Agent.Service
- const state = yield* InstanceState.make<State>(
- Effect.fn("ToolRegistry.state")(function* (ctx) {
- const custom: Tool.Def[] = []
+ const state = yield* InstanceState.make<State>(
+ Effect.fn("ToolRegistry.state")(function* (ctx) {
+ const custom: Tool.Def[] = []
- function fromPlugin(id: string, def: ToolDefinition): Tool.Def {
- return {
- id,
- parameters: z.object(def.args),
- description: def.description,
- execute: (args, toolCtx) =>
- Effect.gen(function* () {
- const pluginCtx: PluginToolContext = {
- ...toolCtx,
- ask: (req) => toolCtx.ask(req),
- directory: ctx.directory,
- worktree: ctx.worktree,
- }
- const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))
- const info = yield* agent.get(toolCtx.agent)
- const out = yield* truncate.output(result, {}, info)
- return {
- title: "",
- output: out.truncated ? out.content : result,
- metadata: {
- truncated: out.truncated,
- outputPath: out.truncated ? out.outputPath : undefined,
- },
- }
- }),
- }
+ function fromPlugin(id: string, def: ToolDefinition): Tool.Def {
+ return {
+ id,
+ parameters: z.object(def.args),
+ description: def.description,
+ execute: (args, toolCtx) =>
+ Effect.gen(function* () {
+ const pluginCtx: PluginToolContext = {
+ ...toolCtx,
+ ask: (req) => toolCtx.ask(req),
+ directory: ctx.directory,
+ worktree: ctx.worktree,
+ }
+ const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx))
+ const info = yield* agent.get(toolCtx.agent)
+ const out = yield* truncate.output(result, {}, info)
+ return {
+ title: "",
+ output: out.truncated ? out.content : result,
+ metadata: {
+ truncated: out.truncated,
+ outputPath: out.truncated ? out.outputPath : undefined,
+ },
+ }
+ }),
}
+ }
- const dirs = yield* config.directories()
- const matches = dirs.flatMap((dir) =>
- Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
+ const dirs = yield* config.directories()
+ const matches = dirs.flatMap((dir) =>
+ Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
+ )
+ if (matches.length) yield* config.waitForDependencies()
+ for (const match of matches) {
+ const namespace = path.basename(match, path.extname(match))
+ const mod = yield* Effect.promise(
+ () => import(process.platform === "win32" ? match : pathToFileURL(match).href),
)
- if (matches.length) yield* config.waitForDependencies()
- for (const match of matches) {
- const namespace = path.basename(match, path.extname(match))
- const mod = yield* Effect.promise(
- () => import(process.platform === "win32" ? match : pathToFileURL(match).href),
- )
- for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
- custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
- }
+ for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
+ custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
}
+ }
- const plugins = yield* plugin.list()
- for (const p of plugins) {
- for (const [id, def] of Object.entries(p.tool ?? {})) {
- custom.push(fromPlugin(id, def))
- }
+ const plugins = yield* plugin.list()
+ for (const p of plugins) {
+ for (const [id, def] of Object.entries(p.tool ?? {})) {
+ custom.push(fromPlugin(id, def))
}
+ }
- yield* config.get()
- const questionEnabled =
- ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
+ yield* config.get()
+ const questionEnabled =
+ ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
- const tool = yield* Effect.all({
- invalid: Tool.init(invalid),
- bash: Tool.init(bash),
- read: Tool.init(read),
- glob: Tool.init(globtool),
- grep: Tool.init(greptool),
- edit: Tool.init(edit),
- write: Tool.init(writetool),
- task: Tool.init(task),
- fetch: Tool.init(webfetch),
- todo: Tool.init(todo),
- search: Tool.init(websearch),
- code: Tool.init(codesearch),
- skill: Tool.init(skilltool),
- patch: Tool.init(patchtool),
- question: Tool.init(question),
- lsp: Tool.init(lsptool),
- plan: Tool.init(plan),
- })
+ const tool = yield* Effect.all({
+ invalid: Tool.init(invalid),
+ bash: Tool.init(bash),
+ read: Tool.init(read),
+ glob: Tool.init(globtool),
+ grep: Tool.init(greptool),
+ edit: Tool.init(edit),
+ write: Tool.init(writetool),
+ task: Tool.init(task),
+ fetch: Tool.init(webfetch),
+ todo: Tool.init(todo),
+ search: Tool.init(websearch),
+ code: Tool.init(codesearch),
+ skill: Tool.init(skilltool),
+ patch: Tool.init(patchtool),
+ question: Tool.init(question),
+ lsp: Tool.init(lsptool),
+ plan: Tool.init(plan),
+ })
- return {
- custom,
- builtin: [
- tool.invalid,
- ...(questionEnabled ? [tool.question] : []),
- tool.bash,
- tool.read,
- tool.glob,
- tool.grep,
- tool.edit,
- tool.write,
- tool.task,
- tool.fetch,
- tool.todo,
- tool.search,
- tool.code,
- tool.skill,
- tool.patch,
- ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []),
- ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [tool.plan] : []),
- ],
- task: tool.task,
- read: tool.read,
- }
- }),
- )
+ return {
+ custom,
+ builtin: [
+ tool.invalid,
+ ...(questionEnabled ? [tool.question] : []),
+ tool.bash,
+ tool.read,
+ tool.glob,
+ tool.grep,
+ tool.edit,
+ tool.write,
+ tool.task,
+ tool.fetch,
+ tool.todo,
+ tool.search,
+ tool.code,
+ tool.skill,
+ tool.patch,
+ ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []),
+ ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [tool.plan] : []),
+ ],
+ task: tool.task,
+ read: tool.read,
+ }
+ }),
+ )
- const all: Interface["all"] = Effect.fn("ToolRegistry.all")(function* () {
- const s = yield* InstanceState.get(state)
- return [...s.builtin, ...s.custom] as Tool.Def[]
- })
+ const all: Interface["all"] = Effect.fn("ToolRegistry.all")(function* () {
+ const s = yield* InstanceState.get(state)
+ return [...s.builtin, ...s.custom] as Tool.Def[]
+ })
- const ids: Interface["ids"] = Effect.fn("ToolRegistry.ids")(function* () {
- return (yield* all()).map((tool) => tool.id)
- })
+ const ids: Interface["ids"] = Effect.fn("ToolRegistry.ids")(function* () {
+ 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 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 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."}`,
)
- 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) {
- return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
- }
+ .join("\n")
+ return ["Available agent types and the tools they have access to:", description].join("\n")
+ })
- const usePatch =
- input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")
- if (tool.id === ApplyPatchTool.id) return usePatch
- if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch
+ 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) {
+ return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
+ }
- return true
- })
+ const usePatch =
+ input.modelID.includes("gpt-") && !input.modelID.includes("oss") && !input.modelID.includes("gpt-4")
+ if (tool.id === ApplyPatchTool.id) return usePatch
+ if (tool.id === EditTool.id || tool.id === WriteTool.id) return !usePatch
- return yield* Effect.forEach(
- filtered,
- Effect.fnUntraced(function* (tool: Tool.Def) {
- using _ = log.time(tool.id)
- const output = {
- description: tool.description,
- parameters: tool.parameters,
- }
- yield* plugin.trigger("tool.definition", { toolID: tool.id }, output)
- return {
- id: tool.id,
- description: [
- output.description,
- tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined,
- tool.id === SkillTool.id ? yield* describeSkill(input.agent) : undefined,
- ]
- .filter(Boolean)
- .join("\n"),
- parameters: output.parameters,
- execute: tool.execute,
- formatValidationError: tool.formatValidationError,
- }
- }),
- { concurrency: "unbounded" },
- )
+ return true
})
- const named: Interface["named"] = Effect.fn("ToolRegistry.named")(function* () {
- const s = yield* InstanceState.get(state)
- return { task: s.task, read: s.read }
- })
+ return yield* Effect.forEach(
+ filtered,
+ Effect.fnUntraced(function* (tool: Tool.Def) {
+ using _ = log.time(tool.id)
+ const output = {
+ description: tool.description,
+ parameters: tool.parameters,
+ }
+ yield* plugin.trigger("tool.definition", { toolID: tool.id }, output)
+ return {
+ id: tool.id,
+ description: [
+ output.description,
+ tool.id === TaskTool.id ? yield* describeTask(input.agent) : undefined,
+ tool.id === SkillTool.id ? yield* describeSkill(input.agent) : undefined,
+ ]
+ .filter(Boolean)
+ .join("\n"),
+ parameters: output.parameters,
+ execute: tool.execute,
+ formatValidationError: tool.formatValidationError,
+ }
+ }),
+ { concurrency: "unbounded" },
+ )
+ })
- return Service.of({ ids, all, named, tools })
- }),
- )
+ const named: Interface["named"] = Effect.fn("ToolRegistry.named")(function* () {
+ const s = yield* InstanceState.get(state)
+ return { task: s.task, read: s.read }
+ })
- export const defaultLayer = Layer.suspend(() =>
- layer.pipe(
- Layer.provide(Config.defaultLayer),
- Layer.provide(Plugin.defaultLayer),
- Layer.provide(Question.defaultLayer),
- Layer.provide(Todo.defaultLayer),
- Layer.provide(Skill.defaultLayer),
- Layer.provide(Agent.defaultLayer),
- Layer.provide(Session.defaultLayer),
- Layer.provide(Provider.defaultLayer),
- Layer.provide(LSP.defaultLayer),
- Layer.provide(FileTime.defaultLayer),
- Layer.provide(Instruction.defaultLayer),
- Layer.provide(AppFileSystem.defaultLayer),
- Layer.provide(Bus.layer),
- Layer.provide(FetchHttpClient.layer),
- Layer.provide(Format.defaultLayer),
- Layer.provide(CrossSpawnSpawner.defaultLayer),
- Layer.provide(Ripgrep.defaultLayer),
- Layer.provide(Truncate.defaultLayer),
- ),
- )
-}
+ return Service.of({ ids, all, named, tools })
+ }),
+)
+
+export const defaultLayer = Layer.suspend(() =>
+ layer.pipe(
+ Layer.provide(Config.defaultLayer),
+ Layer.provide(Plugin.defaultLayer),
+ Layer.provide(Question.defaultLayer),
+ Layer.provide(Todo.defaultLayer),
+ Layer.provide(Skill.defaultLayer),
+ Layer.provide(Agent.defaultLayer),
+ Layer.provide(Session.defaultLayer),
+ Layer.provide(Provider.defaultLayer),
+ Layer.provide(LSP.defaultLayer),
+ Layer.provide(FileTime.defaultLayer),
+ Layer.provide(Instruction.defaultLayer),
+ Layer.provide(AppFileSystem.defaultLayer),
+ Layer.provide(Bus.layer),
+ Layer.provide(FetchHttpClient.layer),
+ Layer.provide(Format.defaultLayer),
+ Layer.provide(CrossSpawnSpawner.defaultLayer),
+ Layer.provide(Ripgrep.defaultLayer),
+ Layer.provide(Truncate.defaultLayer),
+ ),
+)
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index eaec667e5..58a66ee74 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -6,7 +6,7 @@ import * as Stream from "effect/Stream"
import { EffectLogger } from "@/effect"
import { Ripgrep } from "../file/ripgrep"
import { Skill } from "../skill"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
const Parameters = z.object({
name: z.string().describe("The name of the skill from available_skills"),
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index 8f7104e80..3da0664f3 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -1,4 +1,4 @@
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import DESCRIPTION from "./task.txt"
import z from "zod"
import { Session } from "../session"
diff --git a/packages/opencode/src/tool/todo.ts b/packages/opencode/src/tool/todo.ts
index 253bcfa32..5090f17a7 100644
--- a/packages/opencode/src/tool/todo.ts
+++ b/packages/opencode/src/tool/todo.ts
@@ -1,6 +1,6 @@
import z from "zod"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import DESCRIPTION_WRITE from "./todowrite.txt"
import { Todo } from "../session/todo"
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
index ca2586234..db3907348 100644
--- a/packages/opencode/src/tool/tool.ts
+++ b/packages/opencode/src/tool/tool.ts
@@ -3,146 +3,144 @@ import { Effect } from "effect"
import type { MessageV2 } from "../session/message-v2"
import type { Permission } from "../permission"
import type { SessionID, MessageID } from "../session/schema"
-import { Truncate } from "./truncate"
+import * as Truncate from "./truncate"
import { Agent } from "@/agent/agent"
-export namespace Tool {
- interface Metadata {
- [key: string]: any
- }
+interface Metadata {
+ [key: string]: any
+}
- // TODO: remove this hack
- export type DynamicDescription = (agent: Agent.Info) => Effect.Effect<string>
+// TODO: remove this hack
+export type DynamicDescription = (agent: Agent.Info) => Effect.Effect<string>
- export type Context<M extends Metadata = Metadata> = {
- sessionID: SessionID
- messageID: MessageID
- agent: string
- abort: AbortSignal
- callID?: string
- extra?: { [key: string]: any }
- messages: MessageV2.WithParts[]
- metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
- ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
- }
+export type Context<M extends Metadata = Metadata> = {
+ sessionID: SessionID
+ messageID: MessageID
+ agent: string
+ abort: AbortSignal
+ callID?: string
+ extra?: { [key: string]: any }
+ messages: MessageV2.WithParts[]
+ metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
+ ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
+}
- export interface ExecuteResult<M extends Metadata = Metadata> {
- title: string
- metadata: M
- output: string
- attachments?: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[]
- }
+export interface ExecuteResult<M extends Metadata = Metadata> {
+ title: string
+ metadata: M
+ output: string
+ attachments?: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[]
+}
- export interface Def<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
- id: string
- description: string
- parameters: Parameters
- execute(args: z.infer<Parameters>, ctx: Context): Effect.Effect<ExecuteResult<M>>
- formatValidationError?(error: z.ZodError): string
- }
- export type DefWithoutID<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> = Omit<
- Def<Parameters, M>,
- "id"
- >
+export interface Def<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
+ id: string
+ description: string
+ parameters: Parameters
+ execute(args: z.infer<Parameters>, ctx: Context): Effect.Effect<ExecuteResult<M>>
+ formatValidationError?(error: z.ZodError): string
+}
+export type DefWithoutID<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> = Omit<
+ Def<Parameters, M>,
+ "id"
+>
- export interface Info<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
- id: string
- init: () => Effect.Effect<DefWithoutID<Parameters, M>>
- }
+export interface Info<Parameters extends z.ZodType = z.ZodType, M extends Metadata = Metadata> {
+ id: string
+ init: () => Effect.Effect<DefWithoutID<Parameters, M>>
+}
- type Init<Parameters extends z.ZodType, M extends Metadata> =
- | DefWithoutID<Parameters, M>
- | (() => Effect.Effect<DefWithoutID<Parameters, M>>)
+type Init<Parameters extends z.ZodType, M extends Metadata> =
+ | DefWithoutID<Parameters, M>
+ | (() => Effect.Effect<DefWithoutID<Parameters, M>>)
- export type InferParameters<T> =
- T extends Info<infer P, any>
+export type InferParameters<T> =
+ T extends Info<infer P, any>
+ ? z.infer<P>
+ : T extends Effect.Effect<Info<infer P, any>, any, any>
? z.infer<P>
- : T extends Effect.Effect<Info<infer P, any>, any, any>
- ? z.infer<P>
- : never
- export type InferMetadata<T> =
- T extends Info<any, infer M> ? M : T extends Effect.Effect<Info<any, infer M>, any, any> ? M : never
+ : never
+export type InferMetadata<T> =
+ T extends Info<any, infer M> ? M : T extends Effect.Effect<Info<any, infer M>, any, any> ? M : never
- export type InferDef<T> =
- T extends Info<infer P, infer M>
+export type InferDef<T> =
+ T extends Info<infer P, infer M>
+ ? Def<P, M>
+ : T extends Effect.Effect<Info<infer P, infer M>, any, any>
? Def<P, M>
- : T extends Effect.Effect<Info<infer P, infer M>, any, any>
- ? Def<P, M>
- : never
+ : never
- function wrap<Parameters extends z.ZodType, Result extends Metadata>(
- id: string,
- init: Init<Parameters, Result>,
- truncate: Truncate.Interface,
- agents: Agent.Interface,
- ) {
- return () =>
- Effect.gen(function* () {
- const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }
- const execute = toolInfo.execute
- toolInfo.execute = (args, ctx) => {
- const attrs = {
- "tool.name": id,
- "session.id": ctx.sessionID,
- "message.id": ctx.messageID,
- ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}),
- }
- return Effect.gen(function* () {
- yield* Effect.try({
- try: () => toolInfo.parameters.parse(args),
- catch: (error) => {
- if (error instanceof z.ZodError && toolInfo.formatValidationError) {
- return new Error(toolInfo.formatValidationError(error), { cause: error })
- }
- return new Error(
- `The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`,
- { cause: error },
- )
- },
- })
- const result = yield* execute(args, ctx)
- if (result.metadata.truncated !== undefined) {
- return result
- }
- const agent = yield* agents.get(ctx.agent)
- const truncated = yield* truncate.output(result.output, {}, agent)
- return {
- ...result,
- output: truncated.content,
- metadata: {
- ...result.metadata,
- truncated: truncated.truncated,
- ...(truncated.truncated && { outputPath: truncated.outputPath }),
- },
- }
- }).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs }))
+function wrap<Parameters extends z.ZodType, Result extends Metadata>(
+ id: string,
+ init: Init<Parameters, Result>,
+ truncate: Truncate.Interface,
+ agents: Agent.Interface,
+) {
+ return () =>
+ Effect.gen(function* () {
+ const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }
+ const execute = toolInfo.execute
+ toolInfo.execute = (args, ctx) => {
+ const attrs = {
+ "tool.name": id,
+ "session.id": ctx.sessionID,
+ "message.id": ctx.messageID,
+ ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}),
}
- return toolInfo
- })
- }
-
- export function define<Parameters extends z.ZodType, Result extends Metadata, R, ID extends string = string>(
- id: ID,
- init: Effect.Effect<Init<Parameters, Result>, never, R>,
- ): Effect.Effect<Info<Parameters, Result>, never, R | Truncate.Service | Agent.Service> & { id: ID } {
- return Object.assign(
- Effect.gen(function* () {
- const resolved = yield* init
- const truncate = yield* Truncate.Service
- const agents = yield* Agent.Service
- return { id, init: wrap(id, resolved, truncate, agents) }
- }),
- { id },
- )
- }
-
- export function init<P extends z.ZodType, M extends Metadata>(info: Info<P, M>): Effect.Effect<Def<P, M>> {
- return Effect.gen(function* () {
- const init = yield* info.init()
- return {
- ...init,
- id: info.id,
+ return Effect.gen(function* () {
+ yield* Effect.try({
+ try: () => toolInfo.parameters.parse(args),
+ catch: (error) => {
+ if (error instanceof z.ZodError && toolInfo.formatValidationError) {
+ return new Error(toolInfo.formatValidationError(error), { cause: error })
+ }
+ return new Error(
+ `The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`,
+ { cause: error },
+ )
+ },
+ })
+ const result = yield* execute(args, ctx)
+ if (result.metadata.truncated !== undefined) {
+ return result
+ }
+ const agent = yield* agents.get(ctx.agent)
+ const truncated = yield* truncate.output(result.output, {}, agent)
+ return {
+ ...result,
+ output: truncated.content,
+ metadata: {
+ ...result.metadata,
+ truncated: truncated.truncated,
+ ...(truncated.truncated && { outputPath: truncated.outputPath }),
+ },
+ }
+ }).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs }))
}
+ return toolInfo
})
- }
+}
+
+export function define<Parameters extends z.ZodType, Result extends Metadata, R, ID extends string = string>(
+ id: ID,
+ init: Effect.Effect<Init<Parameters, Result>, never, R>,
+): Effect.Effect<Info<Parameters, Result>, never, R | Truncate.Service | Agent.Service> & { id: ID } {
+ return Object.assign(
+ Effect.gen(function* () {
+ const resolved = yield* init
+ const truncate = yield* Truncate.Service
+ const agents = yield* Agent.Service
+ return { id, init: wrap(id, resolved, truncate, agents) }
+ }),
+ { id },
+ )
+}
+
+export function init<P extends z.ZodType, M extends Metadata>(info: Info<P, M>): Effect.Effect<Def<P, M>> {
+ return Effect.gen(function* () {
+ const init = yield* info.init()
+ return {
+ ...init,
+ id: info.id,
+ }
+ })
}
diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts
index d2aa944a8..d990e7adf 100644
--- a/packages/opencode/src/tool/truncate.ts
+++ b/packages/opencode/src/tool/truncate.ts
@@ -9,136 +9,134 @@ import { Log } from "../util"
import { ToolID } from "./schema"
import { TRUNCATION_DIR } from "./truncation-dir"
-export namespace Truncate {
- const log = Log.create({ service: "truncation" })
- const RETENTION = Duration.days(7)
-
- export const MAX_LINES = 2000
- export const MAX_BYTES = 50 * 1024
- export const DIR = TRUNCATION_DIR
- export const GLOB = path.join(TRUNCATION_DIR, "*")
-
- export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string }
-
- export interface Options {
- maxLines?: number
- maxBytes?: number
- direction?: "head" | "tail"
- }
-
- function hasTaskTool(agent?: Agent.Info) {
- if (!agent?.permission) return false
- return evaluate("task", "*", agent.permission).action !== "deny"
- }
-
- export interface Interface {
- readonly cleanup: () => Effect.Effect<void>
- readonly write: (text: string) => Effect.Effect<string>
- /**
- * Returns output unchanged when it fits within the limits, otherwise writes the full text
- * to the truncation directory and returns a preview plus a hint to inspect the saved file.
- */
- readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
- }
-
- export class Service extends Context.Service<Service, Interface>()("@opencode/Truncate") {}
-
- export const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const fs = yield* AppFileSystem.Service
-
- const cleanup = Effect.fn("Truncate.cleanup")(function* () {
- const cutoff = Identifier.timestamp(
- Identifier.create("tool", "ascending", Date.now() - Duration.toMillis(RETENTION)),
- )
- const entries = yield* fs.readDirectory(TRUNCATION_DIR).pipe(
- Effect.map((all) => all.filter((name) => name.startsWith("tool_"))),
- Effect.catch(() => Effect.succeed([])),
- )
- for (const entry of entries) {
- if (Identifier.timestamp(entry) >= cutoff) continue
- yield* fs.remove(path.join(TRUNCATION_DIR, entry)).pipe(Effect.catch(() => Effect.void))
- }
- })
-
- const write = Effect.fn("Truncate.write")(function* (text: string) {
- const file = path.join(TRUNCATION_DIR, ToolID.ascending())
- yield* fs.ensureDir(TRUNCATION_DIR).pipe(Effect.orDie)
- yield* fs.writeFileString(file, text).pipe(Effect.orDie)
- return file
- })
-
- const output = Effect.fn("Truncate.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) {
- const maxLines = options.maxLines ?? MAX_LINES
- const maxBytes = options.maxBytes ?? MAX_BYTES
- const direction = options.direction ?? "head"
- const lines = text.split("\n")
- const totalBytes = Buffer.byteLength(text, "utf-8")
-
- if (lines.length <= maxLines && totalBytes <= maxBytes) {
- return { content: text, truncated: false } as const
- }
+const log = Log.create({ service: "truncation" })
+const RETENTION = Duration.days(7)
- const out: string[] = []
- let i = 0
- let bytes = 0
- let hitBytes = false
-
- if (direction === "head") {
- for (i = 0; i < lines.length && i < maxLines; i++) {
- const size = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0)
- if (bytes + size > maxBytes) {
- hitBytes = true
- break
- }
- out.push(lines[i])
- bytes += size
- }
- } else {
- for (i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
- const size = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0)
- if (bytes + size > maxBytes) {
- hitBytes = true
- break
- }
- out.unshift(lines[i])
- bytes += size
- }
- }
+export const MAX_LINES = 2000
+export const MAX_BYTES = 50 * 1024
+export const DIR = TRUNCATION_DIR
+export const GLOB = path.join(TRUNCATION_DIR, "*")
- const removed = hitBytes ? totalBytes - bytes : lines.length - out.length
- const unit = hitBytes ? "bytes" : "lines"
- const preview = out.join("\n")
- const file = yield* write(text)
-
- const hint = hasTaskTool(agent)
- ? `The tool call succeeded but the output was truncated. Full output saved to: ${file}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.`
- : `The tool call succeeded but the output was truncated. Full output saved to: ${file}\nUse Grep to search the full content or Read with offset/limit to view specific sections.`
-
- return {
- content:
- direction === "head"
- ? `${preview}\n\n...${removed} ${unit} truncated...\n\n${hint}`
- : `...${removed} ${unit} truncated...\n\n${hint}\n\n${preview}`,
- truncated: true,
- outputPath: file,
- } as const
- })
-
- yield* cleanup().pipe(
- Effect.catchCause((cause) => {
- log.error("truncation cleanup failed", { cause: Cause.pretty(cause) })
- return Effect.void
- }),
- Effect.repeat(Schedule.spaced(Duration.hours(1))),
- Effect.delay(Duration.minutes(1)),
- Effect.forkScoped,
- )
+export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string }
+
+export interface Options {
+ maxLines?: number
+ maxBytes?: number
+ direction?: "head" | "tail"
+}
- return Service.of({ cleanup, write, output })
- }),
- )
+function hasTaskTool(agent?: Agent.Info) {
+ if (!agent?.permission) return false
+ return evaluate("task", "*", agent.permission).action !== "deny"
+}
- export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer))
+export interface Interface {
+ readonly cleanup: () => Effect.Effect<void>
+ readonly write: (text: string) => Effect.Effect<string>
+ /**
+ * Returns output unchanged when it fits within the limits, otherwise writes the full text
+ * to the truncation directory and returns a preview plus a hint to inspect the saved file.
+ */
+ readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/Truncate") {}
+
+export const layer = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const fs = yield* AppFileSystem.Service
+
+ const cleanup = Effect.fn("Truncate.cleanup")(function* () {
+ const cutoff = Identifier.timestamp(
+ Identifier.create("tool", "ascending", Date.now() - Duration.toMillis(RETENTION)),
+ )
+ const entries = yield* fs.readDirectory(TRUNCATION_DIR).pipe(
+ Effect.map((all) => all.filter((name) => name.startsWith("tool_"))),
+ Effect.catch(() => Effect.succeed([])),
+ )
+ for (const entry of entries) {
+ if (Identifier.timestamp(entry) >= cutoff) continue
+ yield* fs.remove(path.join(TRUNCATION_DIR, entry)).pipe(Effect.catch(() => Effect.void))
+ }
+ })
+
+ const write = Effect.fn("Truncate.write")(function* (text: string) {
+ const file = path.join(TRUNCATION_DIR, ToolID.ascending())
+ yield* fs.ensureDir(TRUNCATION_DIR).pipe(Effect.orDie)
+ yield* fs.writeFileString(file, text).pipe(Effect.orDie)
+ return file
+ })
+
+ const output = Effect.fn("Truncate.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) {
+ const maxLines = options.maxLines ?? MAX_LINES
+ const maxBytes = options.maxBytes ?? MAX_BYTES
+ const direction = options.direction ?? "head"
+ const lines = text.split("\n")
+ const totalBytes = Buffer.byteLength(text, "utf-8")
+
+ if (lines.length <= maxLines && totalBytes <= maxBytes) {
+ return { content: text, truncated: false } as const
+ }
+
+ const out: string[] = []
+ let i = 0
+ let bytes = 0
+ let hitBytes = false
+
+ if (direction === "head") {
+ for (i = 0; i < lines.length && i < maxLines; i++) {
+ const size = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0)
+ if (bytes + size > maxBytes) {
+ hitBytes = true
+ break
+ }
+ out.push(lines[i])
+ bytes += size
+ }
+ } else {
+ for (i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
+ const size = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0)
+ if (bytes + size > maxBytes) {
+ hitBytes = true
+ break
+ }
+ out.unshift(lines[i])
+ bytes += size
+ }
+ }
+
+ const removed = hitBytes ? totalBytes - bytes : lines.length - out.length
+ const unit = hitBytes ? "bytes" : "lines"
+ const preview = out.join("\n")
+ const file = yield* write(text)
+
+ const hint = hasTaskTool(agent)
+ ? `The tool call succeeded but the output was truncated. Full output saved to: ${file}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.`
+ : `The tool call succeeded but the output was truncated. Full output saved to: ${file}\nUse Grep to search the full content or Read with offset/limit to view specific sections.`
+
+ return {
+ content:
+ direction === "head"
+ ? `${preview}\n\n...${removed} ${unit} truncated...\n\n${hint}`
+ : `...${removed} ${unit} truncated...\n\n${hint}\n\n${preview}`,
+ truncated: true,
+ outputPath: file,
+ } as const
+ })
+
+ yield* cleanup().pipe(
+ Effect.catchCause((cause) => {
+ log.error("truncation cleanup failed", { cause: Cause.pretty(cause) })
+ return Effect.void
+ }),
+ Effect.repeat(Schedule.spaced(Duration.hours(1))),
+ Effect.delay(Duration.minutes(1)),
+ Effect.forkScoped,
+ )
+
+ return Service.of({ cleanup, write, output })
+ }),
+)
+
+export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer))
diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts
index 14d546584..6498b871f 100644
--- a/packages/opencode/src/tool/webfetch.ts
+++ b/packages/opencode/src/tool/webfetch.ts
@@ -1,7 +1,7 @@
import z from "zod"
import { Effect } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import TurndownService from "turndown"
import DESCRIPTION from "./webfetch.txt"
diff --git a/packages/opencode/src/tool/websearch.ts b/packages/opencode/src/tool/websearch.ts
index 968e1e34b..34cefd031 100644
--- a/packages/opencode/src/tool/websearch.ts
+++ b/packages/opencode/src/tool/websearch.ts
@@ -1,7 +1,7 @@
import z from "zod"
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import * as McpExa from "./mcp-exa"
import DESCRIPTION from "./websearch.txt"
diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts
index 337c2708c..c5871eb0e 100644
--- a/packages/opencode/src/tool/write.ts
+++ b/packages/opencode/src/tool/write.ts
@@ -1,7 +1,7 @@
import z from "zod"
import * as path from "path"
import { Effect } from "effect"
-import { Tool } from "./tool"
+import * as Tool from "./tool"
import { LSP } from "../lsp"
import { createTwoFilesPatch } from "diff"
import DESCRIPTION from "./write.txt"
diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts
index 409a0ed60..7e9a6fe90 100644
--- a/packages/opencode/test/agent/agent.test.ts
+++ b/packages/opencode/test/agent/agent.test.ts
@@ -84,7 +84,7 @@ test("explore agent denies edit and write", async () => {
})
test("explore agent asks for external directories and allows Truncate.GLOB", async () => {
- const { Truncate } = await import("../../src/tool/truncate")
+ const { Truncate } = await import("../../src/tool")
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
@@ -496,7 +496,7 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a
})
test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => {
- const { Truncate } = await import("../../src/tool/truncate")
+ const { Truncate } = await import("../../src/tool")
await using tmp = await tmpdir({
config: {
permission: {
@@ -516,7 +516,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
})
test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
- const { Truncate } = await import("../../src/tool/truncate")
+ const { Truncate } = await import("../../src/tool")
await using tmp = await tmpdir({
config: {
agent: {
@@ -540,7 +540,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen
})
test("explicit Truncate.GLOB deny is respected", async () => {
- const { Truncate } = await import("../../src/tool/truncate")
+ const { Truncate } = await import("../../src/tool")
await using tmp = await tmpdir({
config: {
permission: {
diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts
index 6819da481..121d662e5 100644
--- a/packages/opencode/test/session/prompt-effect.test.ts
+++ b/packages/opencode/test/session/prompt-effect.test.ts
@@ -34,8 +34,8 @@ import { Skill } from "../../src/skill"
import { SystemPrompt } from "../../src/session/system"
import { Shell } from "../../src/shell/shell"
import { Snapshot } from "../../src/snapshot"
-import { ToolRegistry } from "../../src/tool/registry"
-import { Truncate } from "../../src/tool/truncate"
+import { ToolRegistry } from "../../src/tool"
+import { Truncate } from "../../src/tool"
import { Log } from "../../src/util"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep"
diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts
index 38aed4376..1f66ccb99 100644
--- a/packages/opencode/test/session/snapshot-tool-race.test.ts
+++ b/packages/opencode/test/session/snapshot-tool-race.test.ts
@@ -50,8 +50,8 @@ import { SessionProcessor } from "../../src/session/processor"
import { SessionRunState } from "../../src/session/run-state"
import { SessionStatus } from "../../src/session/status"
import { Snapshot } from "../../src/snapshot"
-import { ToolRegistry } from "../../src/tool/registry"
-import { Truncate } from "../../src/tool/truncate"
+import { ToolRegistry } from "../../src/tool"
+import { Truncate } from "../../src/tool"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep"
diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts
index c0448c78c..ebfa9a531 100644
--- a/packages/opencode/test/tool/apply_patch.test.ts
+++ b/packages/opencode/test/tool/apply_patch.test.ts
@@ -9,7 +9,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Format } from "../../src/format"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { tmpdir } from "../fixture/fixture"
import { SessionID, MessageID } from "../../src/session/schema"
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index 6a3eac15e..d66cfc3e3 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -9,7 +9,7 @@ import { Filesystem } from "../../src/util"
import { tmpdir } from "../fixture/fixture"
import type { Permission } from "../../src/permission"
import { Agent } from "../../src/agent/agent"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { SessionID, MessageID } from "../../src/session/schema"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts
index 37a19a5fd..2e3dfa8a6 100644
--- a/packages/opencode/test/tool/edit.test.ts
+++ b/packages/opencode/test/tool/edit.test.ts
@@ -12,7 +12,7 @@ import { Format } from "../../src/format"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { BusEvent } from "../../src/bus/bus-event"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { SessionID, MessageID } from "../../src/session/schema"
const ctx = {
diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts
index ee8cb5396..8cbfe7827 100644
--- a/packages/opencode/test/tool/external-directory.test.ts
+++ b/packages/opencode/test/tool/external-directory.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import { Effect } from "effect"
-import type { Tool } from "../../src/tool/tool"
+import type { Tool } from "../../src/tool"
import { Instance } from "../../src/project/instance"
import { assertExternalDirectory } from "../../src/tool/external-directory"
import { Filesystem } from "../../src/util"
diff --git a/packages/opencode/test/tool/glob.test.ts b/packages/opencode/test/tool/glob.test.ts
index 20e761fc1..87d35715d 100644
--- a/packages/opencode/test/tool/glob.test.ts
+++ b/packages/opencode/test/tool/glob.test.ts
@@ -6,7 +6,7 @@ import { SessionID, MessageID } from "../../src/session/schema"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Ripgrep } from "../../src/file/ripgrep"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { Agent } from "../../src/agent/agent"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts
index 35467aeab..388828f6e 100644
--- a/packages/opencode/test/tool/grep.test.ts
+++ b/packages/opencode/test/tool/grep.test.ts
@@ -5,7 +5,7 @@ import { GrepTool } from "../../src/tool/grep"
import { provideInstance, provideTmpdirInstance } from "../fixture/fixture"
import { SessionID, MessageID } from "../../src/session/schema"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { Agent } from "../../src/agent/agent"
import { Ripgrep } from "../../src/file/ripgrep"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts
index 629e5d2d2..17718b2b3 100644
--- a/packages/opencode/test/tool/question.test.ts
+++ b/packages/opencode/test/tool/question.test.ts
@@ -5,7 +5,7 @@ import { Question } from "../../src/question"
import { SessionID, MessageID } from "../../src/session/schema"
import { Agent } from "../../src/agent/agent"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts
index fa65068f8..8e1724b47 100644
--- a/packages/opencode/test/tool/read.test.ts
+++ b/packages/opencode/test/tool/read.test.ts
@@ -11,8 +11,8 @@ import { Instance } from "../../src/project/instance"
import { SessionID, MessageID } from "../../src/session/schema"
import { Instruction } from "../../src/session/instruction"
import { ReadTool } from "../../src/tool/read"
-import { Truncate } from "../../src/tool/truncate"
-import { Tool } from "../../src/tool/tool"
+import { Truncate } from "../../src/tool"
+import { Tool } from "../../src/tool"
import { Filesystem } from "../../src/util"
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts
index dea84bdcd..dbb89e09a 100644
--- a/packages/opencode/test/tool/registry.test.ts
+++ b/packages/opencode/test/tool/registry.test.ts
@@ -4,7 +4,7 @@ import fs from "fs/promises"
import { Effect, Layer } from "effect"
import { Instance } from "../../src/project/instance"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
-import { ToolRegistry } from "../../src/tool/registry"
+import { ToolRegistry } from "../../src/tool"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts
index 9b92a8cd3..55e126ab4 100644
--- a/packages/opencode/test/tool/skill.test.ts
+++ b/packages/opencode/test/tool/skill.test.ts
@@ -4,10 +4,10 @@ import { afterEach, describe, expect } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import type { Permission } from "../../src/permission"
-import type { Tool } from "../../src/tool/tool"
+import type { Tool } from "../../src/tool"
import { Instance } from "../../src/project/instance"
import { SkillTool } from "../../src/tool/skill"
-import { ToolRegistry } from "../../src/tool/registry"
+import { ToolRegistry } from "../../src/tool"
import { provideTmpdirInstance } from "../fixture/fixture"
import { SessionID, MessageID } from "../../src/session/schema"
import { testEffect } from "../lib/effect"
diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts
index bc90dc0f2..b94dd5208 100644
--- a/packages/opencode/test/tool/task.test.ts
+++ b/packages/opencode/test/tool/task.test.ts
@@ -10,8 +10,8 @@ import type { SessionPrompt } from "../../src/session/prompt"
import { MessageID, PartID } from "../../src/session/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
-import { Truncate } from "../../src/tool/truncate"
-import { ToolRegistry } from "../../src/tool/registry"
+import { Truncate } from "../../src/tool"
+import { ToolRegistry } from "../../src/tool"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
diff --git a/packages/opencode/test/tool/tool-define.test.ts b/packages/opencode/test/tool/tool-define.test.ts
index b8003e475..00d1e039a 100644
--- a/packages/opencode/test/tool/tool-define.test.ts
+++ b/packages/opencode/test/tool/tool-define.test.ts
@@ -2,8 +2,8 @@ import { describe, test, expect } from "bun:test"
import { Effect, Layer, ManagedRuntime } from "effect"
import z from "zod"
import { Agent } from "../../src/agent/agent"
-import { Tool } from "../../src/tool/tool"
-import { Truncate } from "../../src/tool/truncate"
+import { Tool } from "../../src/tool"
+import { Truncate } from "../../src/tool"
const runtime = ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer))
diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts
index d0873046d..d3cec4cd9 100644
--- a/packages/opencode/test/tool/truncation.test.ts
+++ b/packages/opencode/test/tool/truncation.test.ts
@@ -1,7 +1,7 @@
import { describe, test, expect } from "bun:test"
import { NodeFileSystem } from "@effect/platform-node"
import { Effect, FileSystem, Layer } from "effect"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { Identifier } from "../../src/id/id"
import { Process } from "../../src/util"
import { Filesystem } from "../../src/util"
diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts
index 7d2ff1dca..699e388fb 100644
--- a/packages/opencode/test/tool/webfetch.test.ts
+++ b/packages/opencode/test/tool/webfetch.test.ts
@@ -3,7 +3,7 @@ import path from "path"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Agent } from "../../src/agent/agent"
-import { Truncate } from "../../src/tool/truncate"
+import { Truncate } from "../../src/tool"
import { Instance } from "../../src/project/instance"
import { WebFetchTool } from "../../src/tool/webfetch"
import { SessionID, MessageID } from "../../src/session/schema"
diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts
index e83ec2efd..46bbe2e40 100644
--- a/packages/opencode/test/tool/write.test.ts
+++ b/packages/opencode/test/tool/write.test.ts
@@ -9,8 +9,8 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { FileTime } from "../../src/file/time"
import { Bus } from "../../src/bus"
import { Format } from "../../src/format"
-import { Truncate } from "../../src/tool/truncate"
-import { Tool } from "../../src/tool/tool"
+import { Truncate } from "../../src/tool"
+import { Tool } from "../../src/tool"
import { Agent } from "../../src/agent/agent"
import { SessionID, MessageID } from "../../src/session/schema"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"