diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts
index 04ce1ca8d43..0f77a4e43c9 100644
--- a/packages/core/test/plugin/promise.test.ts
+++ b/packages/core/test/plugin/promise.test.ts
@@ -20,6 +20,7 @@ import { Project } from "@opencode-ai/core/project"
import { Workspace } from "@opencode-ai/core/workspace"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { define } from "@opencode-ai/plugin/promise/plugin"
+import type { Info } from "@opencode-ai/plugin/promise/tool"
import { Money } from "@opencode-ai/schema/money"
import type { SessionHooks } from "@opencode-ai/plugin/effect/session"
import { testEffect } from "../lib/effect"
@@ -645,6 +646,76 @@ describe("fromPromise", () => {
}),
)
+ it.live("adapts listed and retrieved tool executors without invoking them eagerly", () =>
+ Effect.gen(function* () {
+ const plugins = yield* Plugin.Service
+ const host = yield* PluginHost.make(plugins)
+ const calls: string[] = []
+ const failure = new Tool.Error({ message: "executor failed" })
+ yield* host.tool.transform((draft) => {
+ draft.add({
+ name: "hello",
+ description: "Hello",
+ options: { namespace: "acme", codemode: false },
+ input: Schema.Struct({ name: Schema.String }),
+ output: Schema.String,
+ execute: ({ name }, context) => {
+ calls.push(name)
+ if (name === "failure") return Effect.fail(failure)
+ return context.progress({ name }).pipe(Effect.as({ output: name }))
+ },
+ })
+ })
+ yield* PluginPromise.fromPromise(
+ define({
+ id: "promise-tool-reads",
+ setup: async (ctx) => {
+ const tools: Info[] = []
+ await ctx.tool.transform((draft) => {
+ expect(draft.list().map((tool) => tool.id)).toEqual(["acme_hello"])
+ tools.push(...draft.list())
+ const tool = draft.get("acme_hello")
+ if (!tool) throw new Error("Tool was not found")
+ expect(tool.id).toBe("acme_hello")
+ tools.push(tool)
+ })
+ expect(tools).toHaveLength(2)
+ expect(calls).toEqual([])
+ await Promise.all(
+ tools.map(async (tool) => {
+ const progress: Tool.Metadata[] = []
+ const context = {
+ sessionID: Session.ID.make("ses_promise_tool_reads"),
+ agent: Agent.ID.make("build"),
+ messageID: SessionMessage.ID.make("msg_promise_tool_reads"),
+ id: Tool.CallID.make("call_reads"),
+ progress: async (update: Tool.Metadata) => {
+ progress.push(update)
+ },
+ }
+ expect(await tool.execute({ name: "world" }, context)).toEqual({ output: "world" })
+ expect(progress).toEqual([{ name: "world" }])
+ await expect(tool.execute({ name: "failure" }, context)).rejects.toBe(failure)
+ const error = new Error("progress failed")
+ await expect(
+ tool.execute(
+ { name: "world" },
+ {
+ ...context,
+ progress: async () => {
+ throw error
+ },
+ },
+ ),
+ ).rejects.toBe(error)
+ }),
+ )
+ },
+ }),
+ ).effect(host)
+ }),
+ )
+
it.live("reloads and disposes Promise tools while preserving older snapshots", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
@@ -789,6 +860,61 @@ describe("fromPromise", () => {
}),
)
+ it.live("clears deleted tool options while retaining the namespace", () =>
+ Effect.gen(function* () {
+ const plugins = yield* Plugin.Service
+ const registry = yield* Tool.Service
+ const host = yield* PluginHost.make(plugins)
+ yield* host.tool.transform((draft) => {
+ draft.add({
+ name: "hello",
+ description: "Hello",
+ options: { namespace: "acme", codemode: false },
+ input: Schema.Struct({}),
+ output: Schema.String,
+ execute: () => Effect.succeed({ output: "Hello" }),
+ })
+ })
+ const original = yield* registry.snapshot()
+ expect(original.definitions.map((tool) => tool.name)).toEqual(["acme_hello", "execute"])
+ expect(original.codeModeCatalog).toEqual([])
+
+ yield* PluginPromise.fromPromise(
+ define({
+ id: "promise-tool-options",
+ setup: async (ctx) => {
+ await ctx.tool.transform((draft) => {
+ draft.update("acme_hello", (tool) => {
+ delete tool.options
+ })
+ expect(draft.get("acme_hello")?.options).toEqual({ namespace: "acme" })
+ })
+ },
+ }),
+ ).effect(host)
+
+ const snapshot = yield* registry.snapshot()
+ expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["execute"])
+ expect(snapshot.codeModeCatalog?.map((tool) => tool.path)).toEqual(["acme.hello"])
+ expect(original.definitions.map((tool) => tool.name)).toEqual(["acme_hello", "execute"])
+ expect(
+ yield* snapshot.execute({
+ sessionID: Session.ID.make("ses_promise_tool_options"),
+ agent: Agent.ID.make("build"),
+ messageID: SessionMessage.ID.make("msg_promise_tool_options"),
+ call: {
+ type: "tool-call",
+ id: "call_options",
+ name: "execute",
+ input: { code: "return await tools.acme.hello({})" },
+ },
+ }),
+ ).toMatchObject({
+ output: { output: "Hello", toolCalls: [{ tool: "acme.hello", status: "completed" }] },
+ })
+ }),
+ )
+
it.effect("returns content-only plugin results through Code Mode", () =>
Effect.gen(function* () {
const plugins = yield* Plugin.Service
diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts
index 73e79852524..9b0d3c6faab 100644
--- a/packages/plugin/src/promise/adapter.ts
+++ b/packages/plugin/src/promise/adapter.ts
@@ -99,19 +99,15 @@ export function fromPromise(plugin: Plugin) {
const run = (effect: Effect.Effect) => Effect.runPromiseWith(context)(effect)
- const promiseTool = (tool: Tool.Info & { readonly id: string }): Info & { readonly id: string } => {
- const execute = tool.execute
- return {
- ...tool,
- execute: (input, context) =>
- run(
- execute(input, {
- ...context,
- progress: (update) => Effect.promise(() => context.progress(update)),
- }),
- ),
- }
- }
+ const promiseExecutor =
+ (execute: Tool.Info["execute"]): Info["execute"] =>
+ (input, context) =>
+ run(
+ execute(input, {
+ ...context,
+ progress: (update) => Effect.promise(() => context.progress(update)),
+ }),
+ )
const adaptApiMethod = (
endpoint: HttpApiEndpoint.Top,
@@ -310,10 +306,10 @@ export function fromPromise(plugin: Plugin) {
register(
host.tool.transform((draft) =>
callback({
- list: () => draft.list().map((tool) => promiseTool(tool)),
+ list: () => draft.list().map((tool) => ({ ...tool, execute: promiseExecutor(tool.execute) })),
get: (id) => {
const tool = draft.get(id)
- return tool ? promiseTool(tool) : undefined
+ return tool ? { ...tool, execute: promiseExecutor(tool.execute) } : undefined
},
add: (tool: Info) =>
draft.add({
@@ -322,16 +318,9 @@ export function fromPromise(plugin: Plugin) {
}),
update: (id, update) =>
draft.update(id, (tool) => {
- const execute = tool.execute
const value: Info = {
...tool,
- execute: (input, context) =>
- run(
- execute(input, {
- ...context,
- progress: (update) => Effect.promise(() => context.progress(update)),
- }),
- ),
+ execute: promiseExecutor(tool.execute),
}
update(value)
Object.assign(tool, value, {