From efeff6374966c2cf8bea3fc42b65700db5dd765d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 10 Jul 2026 00:17:37 -0400 Subject: [PATCH] refactor(plugin): simplify promise tool declarations --- packages/core/src/plugin/promise.ts | 24 ++++++++++- packages/core/test/plugin/promise.test.ts | 39 ++++++++++++++++- packages/docs/plugins.mdx | 25 +++++------ packages/plugin/src/v2/promise/README.md | 17 ++++---- packages/plugin/src/v2/promise/index.ts | 2 - packages/plugin/src/v2/promise/tool.ts | 43 +++++-------------- .../plugin/test/contract-identity.test.ts | 1 - 7 files changed, 91 insertions(+), 60 deletions(-) diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index 2e175fa85d9..f372a9fbabc 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -1,7 +1,9 @@ export * as PluginPromise from "./promise" import { Plugin } from "@opencode-ai/plugin/v2/effect" +import type { AnyTool, RegisterOptions } from "@opencode-ai/plugin/v2/tool" import { Effect, Scope, Stream } from "effect" +import { Tool } from "../tool/tool" type HostRegistration = { readonly dispose: Effect.Effect } type Registration = { readonly dispose: () => Promise } @@ -108,7 +110,15 @@ export function fromPromise(plugin: PromisePlugin) { reload: () => run(host.skill.reload()), }, tool: { - transform: transform(host.tool), + transform: (callback) => + register( + host.tool.transform((draft) => + callback({ + add: (name: string, tool: AnyTool, options?: RegisterOptions) => + draft.add(name, fromPromiseTool(tool), options), + }), + ), + ), hook: (name, callback) => register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))), }, @@ -125,3 +135,15 @@ export function fromPromise(plugin: PromisePlugin) { }), }) } + +function fromPromiseTool(tool: AnyTool) { + if ("jsonSchema" in tool) + return Tool.make({ + ...tool, + execute: (input, context) => Effect.promise(() => tool.execute(input, context)), + }) + return Tool.make({ + ...tool, + execute: (input, context) => Effect.promise(() => tool.execute(input, context)), + }) +} diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 05a20693b5c..443cc0a8e4a 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -1,9 +1,12 @@ import { describe, expect } from "bun:test" -import { Effect } from "effect" +import { Effect, Schema } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { PluginPromise } from "@opencode-ai/core/plugin/promise" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { ToolRegistry } from "@opencode-ai/core/tool/registry" import { Plugin } from "@opencode-ai/plugin/v2" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -93,4 +96,38 @@ describe("fromPromise", () => { expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined() }), ) + + it.effect("constructs plain Promise tool declarations in the host", () => + Effect.gen(function* () { + const plugins = yield* PluginV2.Service + const registry = yield* ToolRegistry.Service + const host = yield* PluginHost.make(plugins) + const promisePlugin = Plugin.define({ + id: "promise-tool", + setup: async (ctx) => { + await ctx.tool.transform((tools) => { + tools.add("hello", { + description: "Hello", + input: Schema.Struct({ name: Schema.String }), + output: Schema.String, + execute: async ({ name }) => `Hello, ${name}!`, + }) + }) + }, + }) + + yield* PluginPromise.fromPromise(promisePlugin).effect(host) + + const materialized = yield* registry.materialize() + expect(materialized.definitions).toContainEqual(expect.objectContaining({ name: "hello", description: "Hello" })) + expect( + yield* materialized.settle({ + sessionID: SessionV2.ID.make("ses_promise_tool"), + agent: AgentV2.ID.make("build"), + assistantMessageID: SessionMessage.ID.make("msg_promise_tool"), + call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } }, + }), + ).toMatchObject({ result: { type: "text", value: "Hello, world!" } }) + }), + ) }) diff --git a/packages/docs/plugins.mdx b/packages/docs/plugins.mdx index c75792a8d04..398a79ce124 100644 --- a/packages/docs/plugins.mdx +++ b/packages/docs/plugins.mdx @@ -271,25 +271,23 @@ handle expected errors inside the callback. ## Add a tool -Use `Tool.make` with Effect schemas. Promise tools use async executors: +Pass a plain object with Effect schemas to `tools.add`. Promise tools use async +executors: ```ts title=".opencode/plugins/greeting.ts" import { Plugin } from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" import { Schema } from "effect" -const greeting = Tool.make({ - description: "Create a greeting", - input: Schema.Struct({ name: Schema.String }), - output: Schema.String, - execute: async ({ name }) => `Hello, ${name}!`, -}) - export default Plugin.define({ id: "acme.greeting", setup: async (ctx) => { await ctx.tool.transform((tools) => { - tools.add("greeting", greeting) + tools.add("greeting", { + description: "Create a greeting", + input: Schema.Struct({ name: Schema.String }), + output: Schema.String, + execute: async ({ name }) => `Hello, ${name}!`, + }) }) }, }) @@ -305,10 +303,9 @@ letters, digits, underscores, or hyphens. `tools.add` also accepts tool instead of exposing it directly. The executor receives a second context argument containing `sessionID`, -`agent`, `assistantMessageID`, and `toolCallID`. Use -`Tool.withPermission(tool, "permission-name")` to assign a permission key. -Effect plugins import the helper from `@opencode-ai/plugin/v2/effect/tool` and -return an `Effect` from `execute`. +`agent`, `assistantMessageID`, and `toolCallID`. Effect plugins import their +tool contracts from `@opencode-ai/plugin/v2/effect/tool` and return an `Effect` +from `execute`. ## Types diff --git a/packages/plugin/src/v2/promise/README.md b/packages/plugin/src/v2/promise/README.md index 3e96cefc278..1310af5d133 100644 --- a/packages/plugin/src/v2/promise/README.md +++ b/packages/plugin/src/v2/promise/README.md @@ -85,20 +85,19 @@ await ctx.session.hook("request", (event) => { }) ``` -Promise tools use the same schemas and registration model as Effect tools, with async executors: +Promise tools use plain object declarations with async executors: ```ts import { Schema } from "effect" -import { Tool } from "@opencode-ai/plugin/v2/tool" -const echo = Tool.make({ - description: "Echo text", - input: Schema.Struct({ text: Schema.String }), - output: Schema.Struct({ text: Schema.String }), - execute: async ({ text }) => ({ text }), +await ctx.tool.transform((tools) => { + tools.add("echo", { + description: "Echo text", + input: Schema.Struct({ text: Schema.String }), + output: Schema.Struct({ text: Schema.String }), + execute: async ({ text }) => ({ text }), + }) }) - -await ctx.tool.transform((tools) => tools.add("echo", echo)) ``` ## Reloading A Domain diff --git a/packages/plugin/src/v2/promise/index.ts b/packages/plugin/src/v2/promise/index.ts index 17ee7e37bf4..ae8e6d18a19 100644 --- a/packages/plugin/src/v2/promise/index.ts +++ b/packages/plugin/src/v2/promise/index.ts @@ -1,8 +1,6 @@ export type { PluginOptions } from "../options.js" export * as Plugin from "./plugin.js" -export { Tool } from "./tool.js" - export { Agent } from "@opencode-ai/schema/agent" export { Command } from "@opencode-ai/schema/command" export { Connection } from "@opencode-ai/schema/connection" diff --git a/packages/plugin/src/v2/promise/tool.ts b/packages/plugin/src/v2/promise/tool.ts index 8ee9ce9439a..2175e78508f 100644 --- a/packages/plugin/src/v2/promise/tool.ts +++ b/packages/plugin/src/v2/promise/tool.ts @@ -1,24 +1,16 @@ -export * as Tool from "./tool.js" - -import { Tool } from "../effect/tool.js" +import type { Tool } from "../effect/tool.js" import type { Agent } from "@opencode-ai/schema/agent" import type { Session } from "@opencode-ai/schema/session" import type { SessionMessage } from "@opencode-ai/schema/session-message" -import { Effect, type JsonSchema, type Schema } from "effect" +import type { JsonSchema, Schema } from "effect" import type { Hooks, Transform } from "./registration.js" export type Context = Tool.Context export type SchemaType = Tool.SchemaType -export type Definition, Output extends SchemaType> = Tool.Definition -export type AnyTool = Tool.AnyTool -export const Failure = Tool.Failure -export type Failure = Tool.Failure -export const RegistrationError = Tool.RegistrationError -export type RegistrationError = Tool.RegistrationError export type Content = Tool.Content export type DynamicOutput = Tool.DynamicOutput -type Config< +export type Definition< Input extends SchemaType, Output extends SchemaType, Structured extends SchemaType = Output, @@ -41,32 +33,14 @@ type Config< }) => ReadonlyArray } -type DynamicConfig = { +export type DynamicDefinition = { readonly description: string readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema readonly execute: (input: unknown, context: Context) => Promise } -export function make< - Input extends SchemaType, - Output extends SchemaType, - Structured extends SchemaType = Output, ->(config: Config): Definition -export function make(config: DynamicConfig): AnyTool -export function make(config: Config | DynamicConfig): AnyTool { - if ("jsonSchema" in config) - return Tool.make({ - ...config, - execute: (input, context) => Effect.promise(() => config.execute(input, context)), - }) - return Tool.make({ - ...config, - execute: (input, context) => Effect.promise(() => config.execute(input, context)), - }) -} - -export const withPermission = Tool.withPermission +export type AnyTool = Definition | DynamicDefinition export interface ToolExecuteBeforeEvent { readonly tool: string @@ -95,7 +69,12 @@ export interface RegisterOptions { } export interface ToolDraft { - add(name: string, tool: AnyTool, options?: RegisterOptions): void + add< + Input extends SchemaType, + Output extends SchemaType, + Structured extends SchemaType = Output, + >(name: string, tool: Definition, options?: RegisterOptions): void + add(name: string, tool: DynamicDefinition, options?: RegisterOptions): void } export interface ToolHooks { diff --git a/packages/plugin/test/contract-identity.test.ts b/packages/plugin/test/contract-identity.test.ts index 7c430c7b090..79e97a22f70 100644 --- a/packages/plugin/test/contract-identity.test.ts +++ b/packages/plugin/test/contract-identity.test.ts @@ -36,6 +36,5 @@ test.each([ "Provider", "Reference", "Skill", - ...(name === "promise" ? ["Tool"] : []), ]) })