|
|
@@ -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,
|
|
|
+)
|