mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-30 00:52:12 +00:00
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:
parent
83ff4253f5
commit
df7dae3706
2 changed files with 138 additions and 23 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue