refactor(plugin): reuse tool executor conversion (#45572)

Reuse the private executor/progress conversion for Promise tool list, get, and update while preserving captured executors, identifiers, and explicit optional-field clearing. Add focused adapter regression coverage.
This commit is contained in:
Kit Langton 2026-08-27 11:25:12 -04:00 committed by GitHub
parent 83ff4253f5
commit df7dae3706
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 138 additions and 23 deletions

View file

@ -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

View file

@ -99,19 +99,15 @@ export function fromPromise(plugin: Plugin) {
const run = <A, E>(effect: Effect.Effect<A, E>) => 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 = <PromiseMethod>(
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, {