From 88dc960af82573c6ba269a664eb719dcf051bb04 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 3 Jul 2026 09:03:53 -0400 Subject: [PATCH] refactor(core): migrate built-in tools to internal plugins (#34956) --- packages/core/src/location-services.ts | 2 - packages/core/src/plugin/internal.ts | 47 ++++++++++ packages/core/src/tool/apply-patch.ts | 22 ++--- packages/core/src/tool/builtins.ts | 51 ----------- packages/core/src/tool/edit.ts | 22 ++--- packages/core/src/tool/grep.ts | 22 ++--- packages/core/src/tool/question.ts | 22 ++--- packages/core/src/tool/read.ts | 36 +++----- packages/core/src/tool/skill.ts | 22 ++--- packages/core/src/tool/todowrite.ts | 22 ++--- packages/core/src/tool/webfetch.ts | 23 ++--- packages/core/src/tool/websearch.ts | 20 ++--- packages/core/src/tool/write.ts | 22 ++--- packages/core/test/lib/tool.ts | 27 +++++- packages/core/test/location-layer.test.ts | 22 ++++- .../core/test/session-instructions.test.ts | 86 ++++++++++++------- packages/core/test/tool-apply-patch.test.ts | 11 ++- packages/core/test/tool-edit.test.ts | 11 ++- packages/core/test/tool-question.test.ts | 11 ++- packages/core/test/tool-read.test.ts | 21 ++++- packages/core/test/tool-skill.test.ts | 12 ++- packages/core/test/tool-todowrite.test.ts | 11 ++- packages/core/test/tool-webfetch.test.ts | 11 ++- packages/core/test/tool-websearch.test.ts | 11 ++- packages/core/test/tool-write.test.ts | 11 ++- 25 files changed, 310 insertions(+), 268 deletions(-) delete mode 100644 packages/core/src/tool/builtins.ts diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 830b9b48be5..b1749b34f79 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -41,7 +41,6 @@ import { InstructionContext } from "./instruction-context" import { SystemContextBuiltIns } from "./system-context/builtins" import { SessionContextEntry } from "./session/context-entry" import { SessionInstructions } from "./session/instructions" -import { BuiltInTools } from "./tool/builtins" import { McpTool } from "./tool/mcp" import { ReadToolFileSystem } from "./tool/read-filesystem" import { ToolRegistry } from "./tool/registry" @@ -89,7 +88,6 @@ export const locationServices = LayerNode.group([ QuestionV2.node, Generate.node, ReadToolFileSystem.node, - BuiltInTools.node, McpTool.node, SessionInstructions.node, SessionRunnerModel.node, diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index f522e85a669..939bb4e731b 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -15,9 +15,11 @@ import { ConfigProviderPlugin } from "../config/plugin/provider" import { ConfigReferencePlugin } from "../config/plugin/reference" import { ConfigSkillPlugin } from "../config/plugin/skill" import { EventV2 } from "../event" +import { FileMutation } from "../file-mutation" import { FileSystem } from "../filesystem" import { FSUtil } from "../fs-util" import { Global } from "../global" +import { Image } from "../image" import { Integration } from "../integration" import { Location } from "../location" import { LocationMutation } from "../location-mutation" @@ -26,8 +28,11 @@ import { Npm } from "../npm" import { PluginV2 } from "../plugin" import { PluginRuntime } from "../plugin/runtime" import { PermissionV2 } from "../permission" +import { QuestionV2 } from "../question" import { Reference } from "../reference" import { Ripgrep } from "../ripgrep" +import { SessionInstructions } from "../session/instructions" +import { SessionTodo } from "../session/todo" import { Shell } from "../shell" import { SkillV2 } from "../skill" import { State } from "../state" @@ -41,9 +46,20 @@ import { ProviderPlugins } from "./provider" import { SdkPlugins } from "./sdk" import { SkillPlugin } from "./skill" import { VariantPlugin } from "./variant" +import { ApplyPatchTool } from "../tool/apply-patch" +import { EditTool } from "../tool/edit" import { GlobTool } from "../tool/glob" +import { GrepTool } from "../tool/grep" +import { QuestionTool } from "../tool/question" +import { ReadTool } from "../tool/read" +import { ReadToolFileSystem } from "../tool/read-filesystem" import { ShellTool } from "../tool/shell" +import { SkillTool } from "../tool/skill" import { SubagentTool } from "../tool/subagent" +import { TodoWriteTool } from "../tool/todowrite" +import { WebFetchTool } from "../tool/webfetch" +import { WebSearchTool } from "../tool/websearch" +import { WriteTool } from "../tool/write" export type Requirements = | AgentV2.Service @@ -51,10 +67,12 @@ export type Requirements = | CommandV2.Service | Config.Service | EventV2.Service + | FileMutation.Service | FileSystem.Service | FSUtil.Service | Global.Service | HttpClient.HttpClient + | Image.Service | Integration.Service | Location.Service | LocationMutation.Service @@ -62,11 +80,16 @@ export type Requirements = | Npm.Service | PermissionV2.Service | PluginRuntime.Service + | QuestionV2.Service + | ReadToolFileSystem.Service | Reference.Service | Ripgrep.Service + | SessionInstructions.Service + | SessionTodo.Service | Shell.Service | SkillV2.Service | Tools.Service + | WebSearchTool.ConfigService export interface Plugin { readonly id: string @@ -96,13 +119,20 @@ const layer = Layer.effectDiscard( Context.make(Global.Service, yield* Global.Service), Context.make(HttpClient.HttpClient, yield* HttpClient.HttpClient), Context.make(LocationMutation.Service, yield* LocationMutation.Service), + Context.make(FileMutation.Service, yield* FileMutation.Service), + Context.make(Image.Service, yield* Image.Service), Context.make(PermissionV2.Service, yield* PermissionV2.Service), + Context.make(QuestionV2.Service, yield* QuestionV2.Service), + Context.make(ReadToolFileSystem.Service, yield* ReadToolFileSystem.Service), + Context.make(SessionInstructions.Service, yield* SessionInstructions.Service), + Context.make(SessionTodo.Service, yield* SessionTodo.Service), Context.make(SkillV2.Service, yield* SkillV2.Service), Context.make(Reference.Service, yield* Reference.Service), Context.make(Ripgrep.Service, yield* Ripgrep.Service), Context.make(Shell.Service, yield* Shell.Service), Context.make(Tools.Service, yield* Tools.Service), Context.make(PluginRuntime.Service, yield* PluginRuntime.Service), + Context.make(WebSearchTool.ConfigService, yield* WebSearchTool.ConfigService), ) const add = (input: Plugin) => plugin.add(PluginV2.ID.make(input.id), (context: PluginContext) => @@ -117,9 +147,19 @@ const layer = Layer.effectDiscard( yield* add(SkillPlugin.Plugin) yield* add(ModelsDevPlugin) yield* add(ConfigExternalPlugin.Plugin) + yield* add(ApplyPatchTool.Plugin) + yield* add(EditTool.Plugin) yield* add(GlobTool.Plugin) + yield* add(GrepTool.Plugin) + yield* add(QuestionTool.Plugin) + yield* add(ReadTool.Plugin) yield* add(ShellTool.Plugin) + yield* add(SkillTool.Plugin) yield* add(SubagentTool.Plugin) + yield* add(TodoWriteTool.Plugin) + yield* add(WebFetchTool.Plugin) + yield* add(WebSearchTool.Plugin) + yield* add(WriteTool.Plugin) yield* add(ConfigAgentPlugin.Plugin) yield* add(ConfigCommandPlugin.Plugin) yield* add(ConfigSkillPlugin.Plugin) @@ -145,6 +185,8 @@ export const node = makeLocationNode({ Config.node, Location.node, LocationMutation.node, + FileMutation.node, + Image.node, ModelsDev.node, Npm.node, EventV2.node, @@ -153,6 +195,10 @@ export const node = makeLocationNode({ Global.node, httpClient, PermissionV2.node, + QuestionV2.node, + ReadToolFileSystem.node, + SessionInstructions.node, + SessionTodo.node, SkillV2.node, Reference.node, Ripgrep.node, @@ -160,5 +206,6 @@ export const node = makeLocationNode({ ToolRegistry.toolsNode, PluginRuntime.node, SdkPlugins.node, + WebSearchTool.configNode, ], }) diff --git a/packages/core/src/tool/apply-patch.ts b/packages/core/src/tool/apply-patch.ts index 3d7105a31a2..2f481d8a0d5 100644 --- a/packages/core/src/tool/apply-patch.ts +++ b/packages/core/src/tool/apply-patch.ts @@ -1,18 +1,16 @@ export * as ApplyPatchTool from "./apply-patch" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" import { FileDiff } from "@opencode-ai/schema/file-diff" import { createTwoFilesPatch, diffLines } from "diff" -import { Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "../effect/app-node" +import { Effect, Schema } from "effect" import { FileMutation } from "../file-mutation" import { FSUtil } from "../fs-util" import { LocationMutation } from "../location-mutation" import { Patch } from "../patch" import { PermissionV2 } from "../permission" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "apply_patch" @@ -56,15 +54,15 @@ type Prepared = readonly after: string }) -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-apply-patch-tool", + effect: Effect.fn("ApplyPatchTool.Plugin")(function* (ctx: PluginContext) { const mutation = yield* LocationMutation.Service const files = yield* FileMutation.Service const fs = yield* FSUtil.Service const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.withPermission( Tool.make({ @@ -194,13 +192,7 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/apply-patch", - layer, - deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], -}) +} function patchFile(change: Prepared): typeof FileDiff.Info.Type { const counts = diffLines(change.before, change.after).reduce( diff --git a/packages/core/src/tool/builtins.ts b/packages/core/src/tool/builtins.ts deleted file mode 100644 index c72d07d0e71..00000000000 --- a/packages/core/src/tool/builtins.ts +++ /dev/null @@ -1,51 +0,0 @@ -export * as BuiltInTools from "./builtins" - -import { makeLocationNode } from "../effect/app-node" -import { Context, Layer } from "effect" -import { ApplyPatchTool } from "./apply-patch" -import { EditTool } from "./edit" -import { GrepTool } from "./grep" -import { QuestionTool } from "./question" -import { ReadTool } from "./read" -import { ReadToolFileSystem } from "./read-filesystem" -import { SkillTool } from "./skill" -import { TodoWriteTool } from "./todowrite" -import { WebFetchTool } from "./webfetch" -import { WebSearchTool } from "./websearch" -import { WriteTool } from "./write" - -export class Service extends Context.Service>()("@opencode/v2/BuiltInTools") {} - -/** - * Composes only the shipped Location-scoped built-in tool transforms. - * Each tool retains its implementation and focused tests independently. Dynamic - * MCP and plugin tools later use separate scoped canonical registrations, while - * provider/model filtering belongs to a future materialization phase rather - * than this static list. The caller intentionally supplies shared Location - * services once to this merged set. - * - * TODO: Port the remaining launch-follow-up leaves deliberately: edit fuzzy - * parity, task, LSP, - * repo_clone, repo_overview, plan_exit, and Rune/code mode. Keep MCP and plugin - * transforms separate from this static built-in list. - */ -const layer = Layer.succeed(Service, Service.of({})) - -export const node = makeLocationNode({ - service: Service, - layer, - deps: [ - ApplyPatchTool.node, - EditTool.node, - GrepTool.node, - QuestionTool.node, - ReadTool.node, - ReadToolFileSystem.node, - SkillTool.node, - TodoWriteTool.node, - WebFetchTool.node, - WebSearchTool.node, - WebSearchTool.configNode, - WriteTool.node, - ], -}) diff --git a/packages/core/src/tool/edit.ts b/packages/core/src/tool/edit.ts index f0bdb488a06..17ff28cfc17 100644 --- a/packages/core/src/tool/edit.ts +++ b/packages/core/src/tool/edit.ts @@ -6,18 +6,16 @@ */ export * as EditTool from "./edit" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" import { FileDiff } from "@opencode-ai/schema/file-diff" import { createTwoFilesPatch, diffLines } from "diff" -import { Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "../effect/app-node" +import { Effect, Schema } from "effect" import { FileMutation } from "../file-mutation" import { FSUtil } from "../fs-util" import { LocationMutation } from "../location-mutation" import { PermissionV2 } from "../permission" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "edit" @@ -87,15 +85,15 @@ export const toModelOutput = (output: Output, oldString: string, newString: stri // TODO: Add snapshots / undo after design exists. // TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-edit-tool", + effect: Effect.fn("EditTool.Plugin")(function* (ctx: PluginContext) { const mutation = yield* LocationMutation.Service const files = yield* FileMutation.Service const fs = yield* FSUtil.Service const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.withPermission( Tool.make({ @@ -214,10 +212,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/edit", - layer, - deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], -}) +} diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts index f455bd4c8a0..e5256066098 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/grep.ts @@ -1,18 +1,16 @@ export * as GrepTool from "./grep" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" -import { Effect, Layer, Schema } from "effect" +import { Effect, Schema } from "effect" import path from "path" -import { makeLocationNode } from "../effect/app-node" import { FileSystem } from "../filesystem" import { FSUtil } from "../fs-util" import { Location } from "../location" import { PermissionV2 } from "../permission" import { Ripgrep } from "../ripgrep" import { RelativePath } from "../schema" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "grep" @@ -50,15 +48,15 @@ export const toModelOutput = (output: ModelOutput) => { } /** Grep leaf that defaults its filesystem root to the active Location. */ -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-grep-tool", + effect: Effect.fn("GrepTool.Plugin")(function* (ctx: PluginContext) { const fs = yield* FSUtil.Service const ripgrep = yield* Ripgrep.Service const location = yield* Location.Service const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.make({ description: @@ -128,10 +126,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/grep", - layer, - deps: [ToolRegistry.node, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node], -}) +} diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/question.ts index e5ae0d7426a..218edb57e92 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/question.ts @@ -1,13 +1,11 @@ export * as QuestionTool from "./question" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" -import { Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "../effect/app-node" +import { Effect, Schema } from "effect" import { PermissionV2 } from "../permission" import { QuestionV2 } from "../question" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "question" @@ -44,13 +42,13 @@ export const toModelOutput = ( return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.` } -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-question-tool", + effect: Effect.fn("QuestionTool.Plugin")(function* (ctx: PluginContext) { const question = yield* QuestionV2.Service const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.make({ description, @@ -85,10 +83,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/question", - layer, - deps: [ToolRegistry.node, PermissionV2.node, QuestionV2.node], -}) +} diff --git a/packages/core/src/tool/read.ts b/packages/core/src/tool/read.ts index c94418f94c1..028456a4e1d 100644 --- a/packages/core/src/tool/read.ts +++ b/packages/core/src/tool/read.ts @@ -1,9 +1,9 @@ export * as ReadTool from "./read" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { dirname } from "path" import { ToolFailure } from "@opencode-ai/llm" -import { Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "../effect/app-node" +import { Effect, Schema } from "effect" import { FileSystem } from "../filesystem" import { FSUtil } from "../fs-util" import { Image } from "../image" @@ -13,9 +13,7 @@ import { PermissionV2 } from "../permission" import { SessionInstructions } from "../session/instructions" import { AbsolutePath } from "../schema" import { ReadToolFileSystem } from "./read-filesystem" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "read" const FILENAME = "AGENTS.md" @@ -32,9 +30,9 @@ const LocationInput = Schema.Struct({ const Input = LocationInput const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage]) -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-read-tool", + effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) { const reader = yield* ReadToolFileSystem.Service const mutation = yield* LocationMutation.Service const image = yield* Image.Service @@ -43,7 +41,7 @@ const layer = Layer.effectDiscard( const fs = yield* FSUtil.Service const location = yield* Location.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.make({ description: @@ -111,7 +109,10 @@ const layer = Layer.effectDiscard( const candidates = discovered.map(FSUtil.resolve).filter((file) => dirname(file) !== root) if (candidates.length === 0) return yield* sessionInstructions.load({ sessionID: context.sessionID, paths: candidates }) - }).pipe(Effect.catch(() => Effect.void), Effect.catchDefect(() => Effect.void)) + }).pipe( + Effect.catch(() => Effect.void), + Effect.catchDefect(() => Effect.void), + ) if ("encoding" in content && content.encoding === "base64" && SUPPORTED_IMAGE_MIMES.has(content.mime)) { return yield* image .normalize(resource, { ...content, encoding: "base64" }) @@ -137,19 +138,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/read", - layer, - deps: [ - ToolRegistry.node, - ReadToolFileSystem.node, - LocationMutation.node, - Image.node, - PermissionV2.node, - SessionInstructions.node, - FSUtil.node, - Location.node, - ], -}) +} diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/skill.ts index 1f8b1229035..1c5d23a047f 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/skill.ts @@ -1,15 +1,13 @@ export * as SkillTool from "./skill" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import path from "path" import { ToolFailure } from "@opencode-ai/llm" -import { Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "../effect/app-node" +import { Effect, Schema } from "effect" import { FSUtil } from "../fs-util" import { SkillV2 } from "../skill" import { PermissionV2 } from "../permission" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "skill" const FILE_LIMIT = 10 @@ -54,13 +52,13 @@ export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray) const unableToLoad = (name: string, error?: unknown) => new ToolFailure({ message: `Unable to load skill ${name}`, error }) -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-skill-tool", + effect: Effect.fn("SkillTool.Plugin")(function* (ctx: PluginContext) { const fs = yield* FSUtil.Service const skills = yield* SkillV2.Service const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.make({ description, @@ -100,10 +98,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/skill", - layer, - deps: [ToolRegistry.node, FSUtil.node, SkillV2.node, PermissionV2.node], -}) +} diff --git a/packages/core/src/tool/todowrite.ts b/packages/core/src/tool/todowrite.ts index bc1ba1fbb3f..5f34ebd5aaa 100644 --- a/packages/core/src/tool/todowrite.ts +++ b/packages/core/src/tool/todowrite.ts @@ -1,13 +1,11 @@ export * as TodoWriteTool from "./todowrite" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" -import { Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "../effect/app-node" +import { Effect, Schema } from "effect" import { PermissionV2 } from "../permission" import { SessionTodo } from "../session/todo" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "todowrite" @@ -22,13 +20,13 @@ export type Output = typeof Output.Type export const toModelOutput = (output: Output) => JSON.stringify(output.todos, null, 2) -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-todowrite-tool", + effect: Effect.fn("TodoWriteTool.Plugin")(function* (ctx: PluginContext) { const todos = yield* SessionTodo.Service const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.make({ description: @@ -53,10 +51,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/todowrite", - layer, - deps: [ToolRegistry.node, PermissionV2.node, SessionTodo.node], -}) +} diff --git a/packages/core/src/tool/webfetch.ts b/packages/core/src/tool/webfetch.ts index d3889d6a7ab..efac4a2a75c 100644 --- a/packages/core/src/tool/webfetch.ts +++ b/packages/core/src/tool/webfetch.ts @@ -1,17 +1,14 @@ export * as WebFetchTool from "./webfetch" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" -import { Duration, Effect, Layer, Schema } from "effect" +import { Duration, Effect, Schema } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { Parser } from "htmlparser2" import TurndownService from "turndown" -import { makeLocationNode } from "../effect/app-node" -import { LayerNodePlatform } from "../effect/app-node-platform" import { PermissionV2 } from "../permission" import { collectBoundedResponseBody } from "./http-body" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "webfetch" export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 @@ -115,13 +112,13 @@ const convert = (content: string, contentType: string, format: Format) => { return content } -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-webfetch-tool", + effect: Effect.fn("WebFetchTool.Plugin")(function* (ctx: PluginContext) { const http = yield* HttpClient.HttpClient const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.make({ description, @@ -178,13 +175,7 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/webfetch", - layer, - deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient], -}) +} export function extractTextFromHTML(html: string) { let text = "" diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/websearch.ts index 6d62236316a..80a2422cee9 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/websearch.ts @@ -1,19 +1,17 @@ export * as WebSearchTool from "./websearch" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" import { Context, Duration, Effect, Layer, Schema } from "effect" import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { makeLocationNode } from "../effect/app-node" -import { LayerNodePlatform } from "../effect/app-node-platform" import { truthy } from "../flag/flag" import { InstallationVersion } from "../installation/version" import { PositiveInt } from "../schema" import { PermissionV2 } from "../permission" import { Tool } from "./tool" -import { Tools } from "./tools" import { collectBoundedResponseBody } from "./http-body" import { checksum } from "../util/encode" -import { ToolRegistry } from "./registry" export const name = "websearch" export const NO_RESULTS = "No search results found. Please try a different query." @@ -189,14 +187,14 @@ const Output = Schema.Struct({ text: Schema.String, }) -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-websearch-tool", + effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) { const http = yield* HttpClient.HttpClient const config = yield* ConfigService const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.make({ description, @@ -251,10 +249,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/websearch", - layer, - deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient, configNode], -}) +} diff --git a/packages/core/src/tool/write.ts b/packages/core/src/tool/write.ts index 39ad0b20fb8..73885b21873 100644 --- a/packages/core/src/tool/write.ts +++ b/packages/core/src/tool/write.ts @@ -6,15 +6,13 @@ */ export * as WriteTool from "./write" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" import { ToolFailure } from "@opencode-ai/llm" -import { Effect, Layer, Schema } from "effect" -import { makeLocationNode } from "../effect/app-node" +import { Effect, Schema } from "effect" import { FileMutation } from "../file-mutation" import { LocationMutation } from "../location-mutation" import { PermissionV2 } from "../permission" -import { ToolRegistry } from "./registry" import { Tool } from "./tool" -import { Tools } from "./tools" export const name = "write" @@ -44,14 +42,14 @@ export const toModelOutput = (output: Output) => // TODO: Add snapshots / undo after design exists. // TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. -const layer = Layer.effectDiscard( - Effect.gen(function* () { - const tools = yield* Tools.Service +export const Plugin = { + id: "core-write-tool", + effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) { const mutation = yield* LocationMutation.Service const files = yield* FileMutation.Service const permission = yield* PermissionV2.Service - yield* tools + yield* ctx.tool .register({ [name]: Tool.withPermission( Tool.make({ @@ -92,10 +90,4 @@ const layer = Layer.effectDiscard( }) .pipe(Effect.orDie) }), -) - -export const node = makeLocationNode({ - name: "tool/write", - layer, - deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, PermissionV2.node], -}) +} diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index fa625a591b2..74c1b710ab6 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -2,7 +2,9 @@ import { AgentV2 } from "@opencode-ai/core/agent" import type { PermissionV2 } from "@opencode-ai/core/permission" import { SessionMessage } from "@opencode-ai/core/session/message" import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { Effect } from "effect" +import { Tools } from "@opencode-ai/core/tool/tools" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" +import { Effect, type Scope } from "effect" export const toolIdentity = { agent: AgentV2.ID.make("build"), @@ -34,6 +36,29 @@ export function waitForTool( }) } +/** + * Registers a core tool plugin's tools against the real registry without booting the + * full plugin host. Only the tool domain is live; focused tool tests exercise + * registration, materialization, and settlement through the same path production uses. + */ +export const registerToolPlugin = (plugin: { + readonly id: string + readonly effect: (context: PluginContext) => Effect.Effect +}): Effect.Effect => + Effect.gen(function* () { + const tools = yield* Tools.Service + const context: Pick = { + tool: { + register: tools.register, + execute: { + before: () => Effect.die("registerToolPlugin does not support tool hooks"), + after: () => Effect.die("registerToolPlugin does not support tool hooks"), + }, + }, + } + yield* plugin.effect(context as PluginContext) + }) + export const settleTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput, model = testModel) => registry.materialize({ model }).pipe(Effect.flatMap((materialized) => materialized.settle(input))) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index a6a2c18fa81..77b7207c42b 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -73,9 +73,25 @@ describe("LocationServiceMap", () => { const catalog = yield* Catalog.Service yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) const registry = yield* ToolRegistry.Service - yield* waitForTool(registry, "glob") - yield* waitForTool(registry, "shell") - yield* waitForTool(registry, "subagent") + // Tool plugins register during the forked PluginInternal boot; wait for + // every expected tool rather than relying on batch ordering. + yield* Effect.forEach( + [ + "edit", + "glob", + "grep", + "question", + "read", + "shell", + "skill", + "subagent", + "todowrite", + "webfetch", + "websearch", + "write", + ], + (name) => waitForTool(registry, name), + ) return { providers: yield* catalog.provider.all(), tools: yield* toolDefinitions(registry), diff --git a/packages/core/test/session-instructions.test.ts b/packages/core/test/session-instructions.test.ts index b9e401be4bc..e87fce7234e 100644 --- a/packages/core/test/session-instructions.test.ts +++ b/packages/core/test/session-instructions.test.ts @@ -33,8 +33,24 @@ import { ToolHooks } from "@opencode-ai/core/tool/hooks" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { tempLocationLayer } from "./fixture/location" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" import { testEffect } from "./lib/effect" -import { settleTool, testModel } from "./lib/tool" +import { registerToolPlugin, settleTool, testModel } from "./lib/tool" + +const readToolNode = makeLocationNode({ + name: "test/read-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(ReadTool.Plugin)), + deps: [ + ToolRegistry.toolsNode, + ReadToolFileSystem.node, + LocationMutation.node, + Image.node, + PermissionV2.node, + SessionInstructions.node, + FSUtil.node, + Location.node, + ], +}) const projects = Layer.succeed( ProjectV2.Service, @@ -69,7 +85,7 @@ const testLayer = AppNodeBuilder.build( FSUtil.node, LocationMutation.node, ReadToolFileSystem.node, - ReadTool.node, + readToolNode, ToolRegistry.node, ToolRegistry.toolsNode, ToolHooks.node, @@ -156,7 +172,9 @@ describe("SessionInstructions", () => { expect(firstInjected[0]!.text).toBe( `Instructions from: ${deepPath}\ndeep-instructions\n\nInstructions from: ${subPath}\nsub-instructions`, ) - expect(firstInjected[0]!.description).toBe(`Loaded ${path.relative(dir, deepPath)}, ${path.relative(dir, subPath)}`) + expect(firstInjected[0]!.description).toBe( + `Loaded ${path.relative(dir, deepPath)}, ${path.relative(dir, subPath)}`, + ) // The synthetic's metadata carries the durable dedup ledger. expect(firstInjected[0]!.metadata).toEqual({ instruction: { paths: [deepPath, subPath] } }) expect(firstInjected[0]!.text).not.toContain("root-instructions") @@ -192,47 +210,49 @@ describe("SessionInstructions", () => { // Seed the durable history with a prior synthetic that already claims sub's AGENTS.md // via the instruction metadata ledger. yield* seedSynthetic(sessionID, [subPath]) - expect((yield* synthetics(sessionID))).toHaveLength(1) + expect(yield* synthetics(sessionID)).toHaveLength(1) yield* settleTool(registry, readCall(sessionID, "call-sub", "sub/file.txt")) // The durable claim on the prior synthetic prevents re-injection; no new synthetic. - expect((yield* synthetics(sessionID))).toHaveLength(1) + expect(yield* synthetics(sessionID)).toHaveLength(1) }), ) - it.effect("discovers AGENTS.md on a directory listing, including the listed directory's own, and dedups with a later file read", () => - Effect.gen(function* () { - const location = yield* Location.Service - const dir = location.directory - const rootPath = path.resolve(dir, "AGENTS.md") - const pkgPath = path.resolve(dir, "packages", "foo", "AGENTS.md") - yield* mkdir(path.resolve(dir, "packages", "foo")) - yield* writeAgents(rootPath, "root-instructions") - yield* writeAgents(pkgPath, "pkg-instructions") - yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "packages", "foo", "file.txt"), "content")) + it.effect( + "discovers AGENTS.md on a directory listing, including the listed directory's own, and dedups with a later file read", + () => + Effect.gen(function* () { + const location = yield* Location.Service + const dir = location.directory + const rootPath = path.resolve(dir, "AGENTS.md") + const pkgPath = path.resolve(dir, "packages", "foo", "AGENTS.md") + yield* mkdir(path.resolve(dir, "packages", "foo")) + yield* writeAgents(rootPath, "root-instructions") + yield* writeAgents(pkgPath, "pkg-instructions") + yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "packages", "foo", "file.txt"), "content")) - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service - const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id + const session = yield* SessionV2.Service + const registry = yield* ToolRegistry.Service + const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id - // Listing packages/foo/ discovers its own AGENTS.md, walking up to but excluding - // the Location root (already supplied by the core/instructions baseline). - yield* settleTool(registry, readCall(sessionID, "call-list", "packages/foo")) + // Listing packages/foo/ discovers its own AGENTS.md, walking up to but excluding + // the Location root (already supplied by the core/instructions baseline). + yield* settleTool(registry, readCall(sessionID, "call-list", "packages/foo")) - const firstInjected = yield* synthetics(sessionID) - expect(firstInjected).toHaveLength(1) - expect(firstInjected[0]!.text).toBe(`Instructions from: ${pkgPath}\npkg-instructions`) - expect(firstInjected[0]!.description).toBe(`Loaded ${path.relative(dir, pkgPath)}`) - expect(firstInjected[0]!.metadata).toEqual({ instruction: { paths: [pkgPath] } }) - expect(firstInjected[0]!.text).not.toContain("root-instructions") + const firstInjected = yield* synthetics(sessionID) + expect(firstInjected).toHaveLength(1) + expect(firstInjected[0]!.text).toBe(`Instructions from: ${pkgPath}\npkg-instructions`) + expect(firstInjected[0]!.description).toBe(`Loaded ${path.relative(dir, pkgPath)}`) + expect(firstInjected[0]!.metadata).toEqual({ instruction: { paths: [pkgPath] } }) + expect(firstInjected[0]!.text).not.toContain("root-instructions") - // A subsequent file read under the listed directory is a dedup: pkg's AGENTS.md is - // already injected for this session, so nothing new is emitted. - yield* settleTool(registry, readCall(sessionID, "call-file", "packages/foo/file.txt")) + // A subsequent file read under the listed directory is a dedup: pkg's AGENTS.md is + // already injected for this session, so nothing new is emitted. + yield* settleTool(registry, readCall(sessionID, "call-file", "packages/foo/file.txt")) - expect((yield* synthetics(sessionID))).toHaveLength(1) - }), + expect(yield* synthetics(sessionID)).toHaveLength(1) + }), ) it.effect("listing the Location root directory injects no instructions", () => @@ -253,7 +273,7 @@ describe("SessionInstructions", () => { // dropped by the dirname filter, and up() only walks upward so nested dirs are unseen. yield* settleTool(registry, readCall(sessionID, "call-root-list", ".")) - expect((yield* synthetics(sessionID))).toHaveLength(0) + expect(yield* synthetics(sessionID)).toHaveLength(0) }), ) diff --git a/packages/core/test/tool-apply-patch.test.ts b/packages/core/test/tool-apply-patch.test.ts index 8084cbc24ea..a717b799e33 100644 --- a/packages/core/test/tool-apply-patch.test.ts +++ b/packages/core/test/tool-apply-patch.test.ts @@ -16,8 +16,15 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { ApplyPatchTool } from "@opencode-ai/core/tool/apply-patch" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const applyPatchToolNode = makeLocationNode({ + name: "test/apply-patch-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(ApplyPatchTool.Plugin)), + deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], +}) const sessionID = SessionV2.ID.make("ses_apply_patch_tool_test") const assertions: PermissionV2.AssertInput[] = [] @@ -101,7 +108,7 @@ const withTool = (directory: string, body: (registry: ToolRegistry.Inte ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, - ApplyPatchTool.node, + applyPatchToolNode, ]), [ [FSUtil.node, filesystem], diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index 2c684523b79..9df73c9b7d9 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -17,8 +17,15 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { EditTool } from "@opencode-ai/core/tool/edit" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const editToolNode = makeLocationNode({ + name: "test/edit-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(EditTool.Plugin)), + deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], +}) const sessionID = SessionV2.ID.make("ses_edit_tool_test") const assertions: PermissionV2.AssertInput[] = [] @@ -91,7 +98,7 @@ const withTool = (directory: string, body: (registry: ToolRegistry.Inte ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, - EditTool.node, + editToolNode, ]), [ [FSUtil.node, filesystem], diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index 5c5c95da616..dc015630220 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -9,7 +9,8 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { QuestionTool } from "@opencode-ai/core/tool/question" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" const sessionID = SessionV2.ID.make("ses_question_tool_test") const assertions: PermissionV2.AssertInput[] = [] @@ -43,8 +44,14 @@ const question = Layer.succeed( list: () => Effect.die("unused"), }), ) +const questionToolNode = makeLocationNode({ + name: "test/question-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(QuestionTool.Plugin)), + deps: [ToolRegistry.toolsNode, PermissionV2.node, QuestionV2.node], +}) + const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, QuestionTool.node]), [ + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, questionToolNode]), [ [PermissionV2.node, permission], [QuestionV2.node, question], [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index 3c94888d57f..f6f60df4855 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -19,8 +19,25 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { ReadTool } from "@opencode-ai/core/tool/read" import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" +import { SessionInstructions } from "@opencode-ai/core/session/instructions" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const readToolNode = makeLocationNode({ + name: "test/read-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(ReadTool.Plugin)), + deps: [ + ToolRegistry.toolsNode, + ReadToolFileSystem.node, + LocationMutation.node, + Image.node, + PermissionV2.node, + SessionInstructions.node, + FSUtil.node, + Location.node, + ], +}) const assertions: PermissionV2.AssertInput[] = [] const missingPath = "__missing_read_target__.txt" @@ -130,7 +147,7 @@ const unavailableImage = Layer.succeed( Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }), ) const readLayer = (imageLayer: Layer.Layer) => - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, ReadTool.node]), [ + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, readToolNode]), [ [ReadToolFileSystem.node, reader], [PermissionV2.node, permission], [Config.node, config], diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index 4831acb7993..23e51f6d08c 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -13,7 +13,15 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { tmpdir } from "./fixture/tmpdir" import { it } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const skillToolNode = makeLocationNode({ + name: "test/skill-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(SkillTool.Plugin)), + deps: [ToolRegistry.toolsNode, FSUtil.node, SkillV2.node, PermissionV2.node], +}) const sessionID = SessionV2.ID.make("ses_skill_tool_test") @@ -66,7 +74,7 @@ describe("SkillTool", () => { }), ) const skillToolLayer = AppNodeBuilder.build( - LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, SkillTool.node]), + LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, skillToolNode]), [ [PermissionV2.node, permission], [SkillV2.node, skills], diff --git a/packages/core/test/tool-todowrite.test.ts b/packages/core/test/tool-todowrite.test.ts index 170d1230a3f..977ece9fc84 100644 --- a/packages/core/test/tool-todowrite.test.ts +++ b/packages/core/test/tool-todowrite.test.ts @@ -15,7 +15,14 @@ import { TodoWriteTool } from "@opencode-ai/core/tool/todowrite" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const todoWriteToolNode = makeLocationNode({ + name: "test/todowrite-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(TodoWriteTool.Plugin)), + deps: [ToolRegistry.toolsNode, PermissionV2.node, SessionTodo.node], +}) const sessionID = SessionV2.ID.make("ses_todowrite_tool_test") const assertions: PermissionV2.AssertInput[] = [] @@ -43,7 +50,7 @@ const it = testEffect( SessionTodo.node, ToolRegistry.node, ToolRegistry.toolsNode, - TodoWriteTool.node, + todoWriteToolNode, ]), [ [PermissionV2.node, permission], diff --git a/packages/core/test/tool-webfetch.test.ts b/packages/core/test/tool-webfetch.test.ts index 2fe46f0ae54..421c09f8764 100644 --- a/packages/core/test/tool-webfetch.test.ts +++ b/packages/core/test/tool-webfetch.test.ts @@ -10,8 +10,15 @@ import { SessionV2 } from "@opencode-ai/core/session" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { WebFetchTool } from "@opencode-ai/core/tool/webfetch" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const webFetchToolNode = makeLocationNode({ + name: "test/webfetch-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(WebFetchTool.Plugin)), + deps: [ToolRegistry.toolsNode, PermissionV2.node, LayerNodePlatform.httpClient], +}) const sessionID = SessionV2.ID.make("ses_webfetch_test") const requests: Array<{ readonly url: string; readonly headers: Record }> = [] @@ -40,7 +47,7 @@ const permission = Layer.succeed( }), ) const toolLayer = (replacements: LayerNode.Replacements = []) => - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebFetchTool.node]), [ + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, webFetchToolNode]), [ [PermissionV2.node, permission], [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ...replacements, diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 0b99a80ecb3..53984ef69b7 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -9,8 +9,15 @@ import { SessionV2 } from "@opencode-ai/core/session" import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { WebSearchTool } from "@opencode-ai/core/tool/websearch" import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const webSearchToolNode = makeLocationNode({ + name: "test/websearch-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(WebSearchTool.Plugin)), + deps: [ToolRegistry.toolsNode, PermissionV2.node, LayerNodePlatform.httpClient, WebSearchTool.configNode], +}) const sessionID = SessionV2.ID.make("ses_websearch_test") const payload = (text: string) => @@ -125,7 +132,7 @@ const websearchConfig = Layer.succeed( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearchTool.configNode, WebSearchTool.node]), + LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearchTool.configNode, webSearchToolNode]), [ [PermissionV2.node, permission], [LayerNodePlatform.httpClient, http], diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index 2bd950f8a05..80fb011cc22 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -17,8 +17,15 @@ import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { WriteTool } from "@opencode-ai/core/tool/write" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" +import { makeLocationNode } from "@opencode-ai/core/effect/app-node" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool" + +const writeToolNode = makeLocationNode({ + name: "test/write-tool-plugin", + layer: Layer.effectDiscard(registerToolPlugin(WriteTool.Plugin)), + deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, PermissionV2.node], +}) const sessionID = SessionV2.ID.make("ses_write_tool_test") const assertions: PermissionV2.AssertInput[] = [] @@ -75,7 +82,7 @@ const withTool = (directory: string, body: (registry: ToolRegistry.Inte ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, - WriteTool.node, + writeToolNode, ]), [ [FSUtil.node, filesystem],