mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(plugin): forward Promise tool cancellation (#50190)
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Agent } from "@opencode/core/agent"
|
||||
import { Plugin } from "@opencode/core/plugin"
|
||||
import { PluginHost } from "@opencode/core/plugin/host"
|
||||
import { PluginPromise } from "@opencode/core/plugin/promise"
|
||||
import { Tool } from "@opencode/core/tool"
|
||||
import { Session } from "@opencode/schema/session"
|
||||
import { SessionMessage } from "@opencode/schema/session-message"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber } from "effect"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
const it = testEffect(PluginTestLayer)
|
||||
|
||||
it.live("Promise tool executors receive interruption through their AbortSignal", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* Plugin.Service
|
||||
const tools = yield* Tool.Service
|
||||
const started = yield* Deferred.make<AbortSignal>()
|
||||
yield* PluginPromise.fromPromise({
|
||||
id: "cancel-tool",
|
||||
async setup(context) {
|
||||
await context.tool.transform((editor) =>
|
||||
editor.add({
|
||||
name: "wait",
|
||||
description: "Wait until cancelled",
|
||||
input: { type: "object", properties: {}, additionalProperties: false },
|
||||
options: { codemode: false },
|
||||
execute: (_input, tool) =>
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
tool.signal.addEventListener("abort", () => reject(new Error("cancelled")), { once: true })
|
||||
Effect.runSync(Deferred.succeed(started, tool.signal))
|
||||
}),
|
||||
}),
|
||||
)
|
||||
},
|
||||
}).effect(yield* PluginHost.make(plugins))
|
||||
|
||||
const snapshot = yield* tools.snapshot()
|
||||
const fiber = yield* snapshot
|
||||
.execute({
|
||||
sessionID: Session.ID.make("ses_promise_tool_cancel"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_promise_tool_cancel"),
|
||||
call: { type: "tool-call", id: "call_promise_tool_cancel", name: "wait", input: {} },
|
||||
})
|
||||
.pipe(Effect.forkScoped)
|
||||
const signal = yield* Deferred.await(started)
|
||||
expect(signal.aborted).toBe(false)
|
||||
yield* Fiber.interrupt(fiber)
|
||||
const exit = yield* Fiber.await(fiber)
|
||||
expect(signal.aborted).toBe(true)
|
||||
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
|
||||
}),
|
||||
)
|
||||
@@ -250,12 +250,13 @@ export function fromPromise(plugin: Plugin) {
|
||||
|
||||
const promiseExecutor =
|
||||
(execute: Tool.Info["execute"]): Info["execute"] =>
|
||||
(input, context) =>
|
||||
run(
|
||||
(input, tool) =>
|
||||
Effect.runPromiseWith(context)(
|
||||
execute(input, {
|
||||
...context,
|
||||
progress: (update) => Effect.promise(() => context.progress(update)),
|
||||
...tool,
|
||||
progress: (update) => Effect.promise(() => tool.progress(update)),
|
||||
}),
|
||||
{ signal: tool.signal },
|
||||
)
|
||||
|
||||
const adaptApiMethod = <PromiseMethod>(
|
||||
@@ -612,9 +613,10 @@ function attempt<A>(evaluate: (signal: AbortSignal) => PromiseLike<A>) {
|
||||
type RuntimeSchema = Schema.Codec<unknown, unknown>
|
||||
|
||||
const executePromiseTool = (tool: Info, input: any, context: Tool.Context) =>
|
||||
Effect.promise(() =>
|
||||
Effect.promise((signal) =>
|
||||
tool.execute(input, {
|
||||
...context,
|
||||
progress: (update) => Effect.runPromise(context.progress(update)),
|
||||
signal,
|
||||
progress: (update) => Effect.runPromise(context.progress(update), { signal }),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Types } from "effect"
|
||||
import type { Hooks, Transform } from "./registration.js"
|
||||
|
||||
export interface ToolContext extends Omit<Tool.Context, "progress"> {
|
||||
readonly signal: AbortSignal
|
||||
readonly progress: (update: Tool.Metadata) => Promise<void>
|
||||
}
|
||||
|
||||
|
||||
@@ -927,6 +927,16 @@ const registration = await ctx.tool.transform((editor) => {
|
||||
})
|
||||
```
|
||||
|
||||
Promise tool executors receive `tool.signal`. Pass it to cancellable work such as `fetch` so stopping the Session also
|
||||
stops the underlying operation.
|
||||
|
||||
```ts
|
||||
execute: async ({ url }, tool) => {
|
||||
const response = await fetch(url, { signal: tool.signal })
|
||||
return { content: await response.text() }
|
||||
}
|
||||
```
|
||||
|
||||
Call `reload()` after changing source data captured by the callback. Reload replays the active transforms without
|
||||
changing their order; it does not rerun plugin setup.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user