| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905 |
- import { afterEach, describe, expect } from "bun:test"
- import { SessionV1 } from "@kirincode-ai/core/v1/session"
- import { Database } from "@kirincode-ai/core/database/database"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import { SessionProjector } from "@kirincode-ai/core/session/projector"
- import { Deferred, Effect, Exit, Fiber, Layer } from "effect"
- import { Agent } from "../../src/agent/agent"
- import { BackgroundJob } from "@/background/job"
- import { EventV2Bridge } from "@/event-v2-bridge"
- import { Config } from "@/config/config"
- import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
- import { Ripgrep } from "@kirincode-ai/core/ripgrep"
- import { Session } from "@/session/session"
- import type { SessionPrompt } from "../../src/session/prompt"
- import { MessageID, PartID, SessionID } from "../../src/session/schema"
- import { SessionRunState } from "@/session/run-state"
- import { SessionStatus } from "@/session/status"
- import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
- import { Truncate } from "@/tool/truncate"
- import { ToolRegistry } from "@/tool/registry"
- import { RuntimeFlags } from "@/effect/runtime-flags"
- import { disposeAllInstances } from "../fixture/fixture"
- import { testEffect } from "../lib/effect"
- import { ProviderV2 } from "@kirincode-ai/core/provider"
- import { ModelV2 } from "@kirincode-ai/core/model"
- afterEach(async () => {
- await disposeAllInstances()
- })
- const ref = {
- providerID: ProviderV2.ID.make("test"),
- modelID: ModelV2.ID.make("test-model"),
- }
- const layer = (flags: Partial<RuntimeFlags.Info> = {}) =>
- LayerNode.compile(
- LayerNode.group([
- Agent.node,
- BackgroundJob.node,
- EventV2Bridge.node,
- Config.node,
- CrossSpawnSpawner.node,
- Session.node,
- SessionProjector.node,
- SessionRunState.node,
- SessionStatus.node,
- Truncate.node,
- ToolRegistry.node,
- Database.node,
- RuntimeFlags.node,
- Ripgrep.node,
- ]),
- [[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
- )
- const it = testEffect(layer())
- const background = testEffect(layer({ experimentalBackgroundSubagents: true }))
- function defer<T>() {
- let resolve!: (value: T | PromiseLike<T>) => void
- const promise = new Promise<T>((done) => {
- resolve = done
- })
- return { promise, resolve }
- }
- const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
- const session = yield* Session.Service
- const chat = yield* session.create({ title })
- const user = yield* session.updateMessage({
- id: MessageID.ascending(),
- role: "user",
- sessionID: chat.id,
- agent: "build",
- model: ref,
- time: { created: Date.now() },
- })
- const assistant: SessionV1.Assistant = {
- id: MessageID.ascending(),
- role: "assistant",
- parentID: user.id,
- sessionID: chat.id,
- mode: "build",
- agent: "build",
- cost: 0,
- path: { cwd: "/tmp", root: "/tmp" },
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- modelID: ref.modelID,
- providerID: ref.providerID,
- variant: "xhigh",
- time: { created: Date.now() },
- }
- yield* session.updateMessage(assistant)
- return { chat, assistant }
- })
- function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; text?: string }): TaskPromptOps {
- return {
- cancel: () => Effect.void,
- resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]),
- prompt: (input) =>
- Effect.sync(() => {
- opts?.onPrompt?.(input)
- return reply(input, opts?.text ?? "done")
- }),
- }
- }
- function reply(input: SessionPrompt.PromptInput, text: string): SessionV1.WithParts {
- const id = MessageID.ascending()
- return {
- info: {
- id,
- role: "assistant",
- parentID: input.messageID ?? MessageID.ascending(),
- sessionID: input.sessionID,
- mode: input.agent ?? "general",
- agent: input.agent ?? "general",
- cost: 0,
- path: { cwd: "/tmp", root: "/tmp" },
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- modelID: input.model?.modelID ?? ref.modelID,
- providerID: input.model?.providerID ?? ref.providerID,
- time: { created: Date.now() },
- finish: "stop",
- },
- parts: [
- {
- id: PartID.ascending(),
- messageID: id,
- sessionID: input.sessionID,
- type: "text",
- text,
- },
- ],
- }
- }
- describe("tool.task", () => {
- it.instance(
- "description sorts subagents by name and is stable across calls",
- () =>
- Effect.gen(function* () {
- const agent = yield* Agent.Service
- const build = yield* agent.get("build")
- const registry = yield* ToolRegistry.Service
- const get = Effect.fnUntraced(function* () {
- const tools = yield* registry.tools({ ...ref, agent: build })
- return tools.find((tool) => tool.id === TaskTool.id)?.description ?? ""
- })
- const first = yield* get()
- const second = yield* get()
- expect(first).toBe(second)
- const alpha = first.indexOf("- alpha: Alpha agent")
- const explore = first.indexOf("- explore:")
- const general = first.indexOf("- general:")
- const zebra = first.indexOf("- zebra: Zebra agent")
- expect(alpha).toBeGreaterThan(-1)
- expect(explore).toBeGreaterThan(alpha)
- expect(general).toBeGreaterThan(explore)
- expect(zebra).toBeGreaterThan(general)
- }),
- {
- config: {
- agent: {
- zebra: {
- description: "Zebra agent",
- mode: "subagent",
- },
- alpha: {
- description: "Alpha agent",
- mode: "subagent",
- },
- },
- },
- },
- )
- it.instance(
- "description hides denied subagents for the caller",
- () =>
- Effect.gen(function* () {
- const agent = yield* Agent.Service
- const build = yield* agent.get("build")
- const registry = yield* ToolRegistry.Service
- const description =
- (yield* registry.tools({ ...ref, agent: build })).find((tool) => tool.id === TaskTool.id)?.description ?? ""
- expect(description).toContain("- alpha: Alpha agent")
- expect(description).not.toContain("- zebra: Zebra agent")
- }),
- {
- config: {
- permission: {
- task: {
- "*": "allow",
- zebra: "deny",
- },
- },
- agent: {
- zebra: {
- description: "Zebra agent",
- mode: "subagent",
- },
- alpha: {
- description: "Alpha agent",
- mode: "subagent",
- },
- },
- },
- },
- )
- it.instance("execute resumes an existing task session from task_id", () =>
- Effect.gen(function* () {
- const sessions = yield* Session.Service
- const { chat, assistant } = yield* seed()
- const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
- const tool = yield* TaskTool
- const def = yield* tool.init()
- let seen: SessionPrompt.PromptInput | undefined
- const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) })
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- task_id: child.id,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- const kids = yield* sessions.children(chat.id)
- expect(kids).toHaveLength(1)
- expect(kids[0]?.id).toBe(child.id)
- expect(result.metadata.sessionId).toBe(child.id)
- expect(result.output).toContain(`<task id="${child.id}" state="completed">`)
- expect(seen?.sessionID).toBe(child.id)
- expect(seen?.variant).toBe("xhigh")
- }),
- )
- it.instance("execute asks by default and skips checks when bypassed", () =>
- Effect.gen(function* () {
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const calls: unknown[] = []
- const promptOps = stubOps()
- const exec = (extra?: Record<string, any>) =>
- def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps, ...extra },
- messages: [],
- metadata: () => Effect.void,
- ask: (input) =>
- Effect.sync(() => {
- calls.push(input)
- }),
- },
- )
- yield* exec()
- yield* exec({ bypassAgentCheck: true })
- expect(calls).toHaveLength(1)
- expect(calls[0]).toEqual({
- permission: "task",
- patterns: ["general"],
- always: ["*"],
- metadata: {
- description: "inspect bug",
- subagent_type: "general",
- },
- })
- }),
- )
- it.instance("execute cancels child session when abort signal fires", () =>
- Effect.gen(function* () {
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const ready = defer<SessionPrompt.PromptInput>()
- const cancelled = defer<SessionID>()
- const abort = new AbortController()
- const promptOps: TaskPromptOps = {
- cancel: (sessionID) =>
- Effect.sync(() => {
- cancelled.resolve(sessionID)
- }),
- resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]),
- prompt: (input) =>
- Effect.promise(() => {
- ready.resolve(input)
- return cancelled.promise
- }).pipe(Effect.as(reply(input, "cancelled"))),
- }
- const fiber = yield* def
- .execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: abort.signal,
- extra: { promptOps },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- .pipe(Effect.forkChild)
- const input = yield* Effect.promise(() => ready.promise)
- abort.abort()
- expect(yield* Effect.promise(() => cancelled.promise)).toBe(input.sessionID)
- const exit = yield* Fiber.await(fiber)
- expect(Exit.isSuccess(exit)).toBe(true)
- }),
- )
- it.instance("execute creates a child when task_id does not exist", () =>
- Effect.gen(function* () {
- const sessions = yield* Session.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- let seen: SessionPrompt.PromptInput | undefined
- const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) })
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- task_id: "ses_missing",
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- const kids = yield* sessions.children(chat.id)
- expect(kids).toHaveLength(1)
- expect(kids[0]?.id).toBe(result.metadata.sessionId)
- expect(result.metadata.sessionId).not.toBe("ses_missing")
- expect(result.output).toContain(`<task id="${result.metadata.sessionId}" state="completed">`)
- expect(seen?.sessionID).toBe(result.metadata.sessionId)
- }),
- )
- it.instance(
- "execute shapes child permissions for task, todowrite, and primary tools",
- () =>
- Effect.gen(function* () {
- const sessions = yield* Session.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- let seen: SessionPrompt.PromptInput | undefined
- const promptOps = stubOps({ onPrompt: (input) => (seen = input) })
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "reviewer",
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- const child = yield* sessions.get(result.metadata.sessionId)
- expect(child.parentID).toBe(chat.id)
- expect(child.agent).toBe("reviewer")
- expect(child.permission).toEqual([
- {
- permission: "todowrite",
- pattern: "*",
- action: "deny",
- },
- {
- permission: "bash",
- pattern: "*",
- action: "deny",
- },
- {
- permission: "read",
- pattern: "*",
- action: "deny",
- },
- ])
- expect(seen?.tools).toBeUndefined()
- }),
- {
- config: {
- agent: {
- reviewer: {
- mode: "subagent",
- permission: {
- task: "allow",
- },
- },
- },
- experimental: {
- primary_tools: ["bash", "read"],
- },
- },
- },
- )
- it.instance("rejects background execution when the experiment is disabled", () =>
- Effect.gen(function* () {
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const exit = yield* def
- .execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps: stubOps() },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- .pipe(Effect.exit)
- expect(Exit.isFailure(exit)).toBe(true)
- }),
- )
- it.instance("promotes a running foreground task without restarting it", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const ready = yield* Deferred.make<void>()
- const done = yield* Deferred.make<void>()
- const injected = yield* Deferred.make<SessionPrompt.PromptInput>()
- let runs = 0
- const promptOps: TaskPromptOps = {
- cancel: () => Effect.void,
- resolvePromptParts: (template) => Effect.succeed([{ type: "text" as const, text: template }]),
- prompt: (input) => {
- if (input.sessionID === chat.id) {
- return Deferred.succeed(injected, input).pipe(Effect.as(reply(input, "injected")))
- }
- return Effect.gen(function* () {
- runs += 1
- yield* Deferred.succeed(ready, undefined)
- yield* Deferred.await(done)
- return reply(input, "background done")
- })
- },
- }
- const fiber = yield* def
- .execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- .pipe(Effect.forkChild)
- yield* Deferred.await(ready)
- const job = (yield* jobs.list())[0]
- expect(job).toBeDefined()
- if (!job) throw new Error("task job not found")
- expect(job.metadata?.parentSessionId).toBe(chat.id)
- yield* jobs.promote(job.id)
- const result = yield* Fiber.join(fiber)
- expect(result.metadata.background).toBe(true)
- expect(result.output).toContain(`state="running"`)
- expect((yield* jobs.get(result.metadata.sessionId))?.status).toBe("running")
- expect(runs).toBe(1)
- yield* Deferred.succeed(done, undefined)
- expect((yield* jobs.wait({ id: result.metadata.sessionId })).info?.output).toBe("background done")
- expect((yield* Deferred.await(injected)).parts[0]?.type).toBe("text")
- expect(runs).toBe(1)
- }),
- )
- background.instance("execute launches background tasks without waiting for completion", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: {
- promptOps: {
- ...stubOps(),
- prompt: () => Effect.never,
- } satisfies TaskPromptOps,
- },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- const job = yield* jobs.get(result.metadata.sessionId)
- expect(result.metadata.background).toBe(true)
- expect(result.output).toContain(`state="running"`)
- expect(job?.status).toBe("running")
- }),
- )
- background.instance("background task completion waits for running updates", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const first = defer<void>()
- const second = defer<void>()
- const updated = defer<SessionPrompt.PromptInput>()
- const injected = defer<SessionPrompt.PromptInput>()
- let prompts = 0
- const promptOps: TaskPromptOps = {
- ...stubOps(),
- prompt: (input) => {
- if (input.sessionID === chat.id) {
- injected.resolve(input)
- return Effect.succeed(reply(input, "done"))
- }
- prompts++
- if (prompts === 1) return Effect.promise(() => first.promise).pipe(Effect.as(reply(input, "first done")))
- updated.resolve(input)
- return Effect.promise(() => second.promise).pipe(Effect.as(reply(input, "second done")))
- },
- }
- const context = {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- }
- const started = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- context,
- )
- const result = yield* def.execute(
- {
- description: "add investigation scope",
- prompt: "also inspect cancellation",
- subagent_type: "general",
- task_id: started.metadata.sessionId,
- },
- context,
- )
- expect(result.metadata.sessionId).toBe(started.metadata.sessionId)
- expect(result.metadata.background).toBe(true)
- expect(result.output).toContain("Background task updated")
- first.resolve()
- expect((yield* jobs.get(started.metadata.sessionId))?.status).toBe("running")
- expect((yield* Effect.promise(() => updated.promise)).parts).toEqual([
- { type: "text", text: "also inspect cancellation" },
- ])
- second.resolve()
- const waited = yield* jobs.wait({ id: started.metadata.sessionId, timeout: 1_000 })
- expect(waited.info?.status).toBe("completed")
- expect(waited.info?.output).toBe("second done")
- const notification = yield* Effect.promise(() => injected.promise)
- expect(notification.variant).toBe("xhigh")
- expect(notification.parts[0]?.type).toBe("text")
- if (notification.parts[0]?.type === "text") expect(notification.parts[0].text).toContain("second done")
- }),
- )
- background.instance("background tasks complete through the background job service", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: { promptOps: stubOps({ text: "background done" }) },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- const waited = yield* jobs.wait({ id: result.metadata.sessionId, timeout: 1_000 })
- expect(waited.timedOut).toBe(false)
- expect(waited.info?.status).toBe("completed")
- expect(waited.info?.output).toBe("background done")
- }),
- )
- background.instance("background task completion does not wait for the parent async prompt", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: {
- promptOps: {
- ...stubOps({ text: "background done" }),
- prompt: (input) =>
- input.sessionID === chat.id ? Effect.never : Effect.succeed(reply(input, "background done")),
- } satisfies TaskPromptOps,
- },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- const waited = yield* jobs.wait({ id: result.metadata.sessionId, timeout: 1_000 })
- expect(waited.timedOut).toBe(false)
- expect(waited.info?.status).toBe("completed")
- }),
- )
- background.instance("removing the parent session cancels running background tasks", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const sessions = yield* Session.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: {
- promptOps: {
- ...stubOps(),
- prompt: () => Effect.never,
- } satisfies TaskPromptOps,
- },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- yield* sessions.remove(chat.id)
- const waited = yield* jobs.wait({ id: result.metadata.sessionId, timeout: 1_000 })
- expect(waited.timedOut).toBe(false)
- expect(waited.info?.status).toBe("cancelled")
- }),
- )
- background.instance("removing the child task session cancels its running background task", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const sessions = yield* Session.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: {
- promptOps: {
- ...stubOps(),
- prompt: () => Effect.never,
- } satisfies TaskPromptOps,
- },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- yield* sessions.remove(result.metadata.sessionId)
- const waited = yield* jobs.wait({ id: result.metadata.sessionId, timeout: 1_000 })
- expect(waited.timedOut).toBe(false)
- expect(waited.info?.status).toBe("cancelled")
- }),
- )
- background.instance("cancelling the parent run cancels running background tasks", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const runState = yield* SessionRunState.Service
- const { chat, assistant } = yield* seed()
- const tool = yield* TaskTool
- const def = yield* tool.init()
- const result = yield* def.execute(
- {
- description: "inspect bug",
- prompt: "look into the cache key path",
- subagent_type: "general",
- background: true,
- },
- {
- sessionID: chat.id,
- messageID: assistant.id,
- agent: "build",
- abort: new AbortController().signal,
- extra: {
- promptOps: {
- ...stubOps(),
- prompt: () => Effect.never,
- } satisfies TaskPromptOps,
- },
- messages: [],
- metadata: () => Effect.void,
- ask: () => Effect.void,
- },
- )
- yield* runState.cancel(chat.id)
- const waited = yield* jobs.wait({ id: result.metadata.sessionId, timeout: 1_000 })
- expect(waited.timedOut).toBe(false)
- expect(waited.info?.status).toBe("cancelled")
- }),
- )
- it.instance("cancelling a child run cancels its own pre-runner task job", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const runState = yield* SessionRunState.Service
- const sessions = yield* Session.Service
- const { chat } = yield* seed()
- const child = yield* sessions.create({ parentID: chat.id, title: "child" })
- yield* jobs.start({
- id: child.id,
- type: "task",
- metadata: { parentSessionId: chat.id, sessionId: child.id },
- run: Effect.never,
- })
- yield* runState.cancel(child.id)
- expect((yield* jobs.get(child.id))?.status).toBe("cancelled")
- }),
- )
- it.instance("cancelling a parent run recursively cancels descendant background tasks", () =>
- Effect.gen(function* () {
- const jobs = yield* BackgroundJob.Service
- const runState = yield* SessionRunState.Service
- const sessions = yield* Session.Service
- const { chat } = yield* seed()
- const child = yield* sessions.create({ parentID: chat.id, title: "child" })
- const grandchild = yield* sessions.create({ parentID: child.id, title: "grandchild" })
- yield* jobs.start({
- id: child.id,
- type: "task",
- metadata: { parentSessionId: chat.id, sessionId: child.id },
- run: Effect.never,
- })
- yield* jobs.start({
- id: grandchild.id,
- type: "task",
- metadata: { parentSessionId: child.id, sessionId: grandchild.id },
- run: Effect.never,
- })
- yield* runState.cancel(chat.id)
- expect((yield* jobs.get(child.id))?.status).toBe("cancelled")
- expect((yield* jobs.get(grandchild.id))?.status).toBe("cancelled")
- }),
- )
- })
|