feat(tui): allow backgrounding synchronous subagents (#30488)
This commit is contained in:
parent
8c0edca175
commit
3003867c25
26 changed files with 527 additions and 35 deletions
|
|
@ -1,14 +1,13 @@
|
|||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { SessionV1 } from "@opencode-ai/core/v1/session"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { Effect, Exit, Fiber, Layer } from "effect"
|
||||
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 "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import type { SessionPrompt } from "../../src/session/prompt"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionRunState } from "@/session/run-state"
|
||||
|
|
@ -484,6 +483,72 @@ describe("tool.task", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
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
|
||||
|
|
@ -576,14 +641,14 @@ describe("tool.task", () => {
|
|||
context,
|
||||
)
|
||||
|
||||
expect((yield* Effect.promise(() => updated.promise)).parts).toEqual([
|
||||
{ type: "text", text: "also inspect cancellation" },
|
||||
])
|
||||
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 })
|
||||
|
|
@ -784,6 +849,27 @@ describe("tool.task", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue