fix: propagate abort signal to inline read tool (#21584)

This commit is contained in:
Kit Langton
2026-04-08 21:07:55 -04:00
committed by GitHub
parent 51535d8ef3
commit 2bdd279467
4 changed files with 185 additions and 79 deletions

View File

@@ -631,31 +631,22 @@ it.live(
const ready = defer<void>()
const aborted = defer<void>()
const registry = yield* ToolRegistry.Service
const init = registry.named.task.init
registry.named.task.init = async () => ({
description: "task",
parameters: z.object({
description: z.string(),
prompt: z.string(),
subagent_type: z.string(),
task_id: z.string().optional(),
command: z.string().optional(),
}),
execute: async (_args, ctx) => {
ready.resolve()
ctx.abort.addEventListener("abort", () => aborted.resolve(), { once: true })
await new Promise<void>(() => {})
return {
title: "",
metadata: {
sessionId: SessionID.make("task"),
model: ref,
},
output: "",
}
},
})
yield* Effect.addFinalizer(() => Effect.sync(() => void (registry.named.task.init = init)))
const { task } = yield* registry.named()
const original = task.execute
task.execute = async (_args, ctx) => {
ready.resolve()
ctx.abort.addEventListener("abort", () => aborted.resolve(), { once: true })
await new Promise<void>(() => {})
return {
title: "",
metadata: {
sessionId: SessionID.make("task"),
model: ref,
},
output: "",
}
}
yield* Effect.addFinalizer(() => Effect.sync(() => void (task.execute = original)))
const { prompt, chat } = yield* boot()
const msg = yield* user(chat.id, "hello")
@@ -1240,3 +1231,109 @@ unix(
),
30_000,
)
// Abort signal propagation tests for inline tool execution
/** Override a tool's execute to hang until aborted. Returns ready/aborted defers and a finalizer. */
function hangUntilAborted(tool: { execute: (...args: any[]) => any }) {
const ready = defer<void>()
const aborted = defer<void>()
const original = tool.execute
tool.execute = async (_args: any, ctx: any) => {
ready.resolve()
ctx.abort.addEventListener("abort", () => aborted.resolve(), { once: true })
await new Promise<void>(() => {})
return { title: "", metadata: {}, output: "" }
}
const restore = Effect.addFinalizer(() => Effect.sync(() => void (tool.execute = original)))
return { ready, aborted, restore }
}
it.live(
"interrupt propagates abort signal to read tool via file part (text/plain)",
() =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const { read } = yield* registry.named()
const { ready, aborted, restore } = hangUntilAborted(read)
yield* restore
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({ title: "Abort Test" })
const testFile = path.join(dir, "test.txt")
yield* Effect.promise(() => Bun.write(testFile, "hello world"))
const fiber = yield* prompt
.prompt({
sessionID: chat.id,
agent: "build",
parts: [
{ type: "text", text: "read this" },
{ type: "file", url: `file://${testFile}`, filename: "test.txt", mime: "text/plain" },
],
})
.pipe(Effect.forkChild)
yield* Effect.promise(() => ready.promise)
yield* Fiber.interrupt(fiber)
yield* Effect.promise(() =>
Promise.race([
aborted.promise,
new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error("abort signal not propagated within 2s")), 2_000),
),
]),
)
}),
{ git: true, config: cfg },
),
30_000,
)
it.live(
"interrupt propagates abort signal to read tool via file part (directory)",
() =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const { read } = yield* registry.named()
const { ready, aborted, restore } = hangUntilAborted(read)
yield* restore
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({ title: "Abort Test" })
const fiber = yield* prompt
.prompt({
sessionID: chat.id,
agent: "build",
parts: [
{ type: "text", text: "read this" },
{ type: "file", url: `file://${dir}`, filename: "dir", mime: "application/x-directory" },
],
})
.pipe(Effect.forkChild)
yield* Effect.promise(() => ready.promise)
yield* Fiber.interrupt(fiber)
yield* Effect.promise(() =>
Promise.race([
aborted.promise,
new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error("abort signal not propagated within 2s")), 2_000),
),
]),
)
}),
{ git: true, config: cfg },
),
30_000,
)