fix(tui): background foreground subagents

This commit is contained in:
Kit Langton 2026-06-30 13:07:40 -04:00
commit 3005e8bb98
7 changed files with 388 additions and 84 deletions

View file

@ -28,6 +28,7 @@ export const Output = Schema.Struct({
sessionID: SessionSchema.ID,
status: Schema.Literals(["completed", "running"]),
output: Schema.String,
background: Schema.Boolean.pipe(Schema.optional),
})
export const description = [
@ -146,7 +147,7 @@ export const Plugin = {
if (background) {
yield* runtime.job.background(info.id)
yield* notifyWhenDone(context.sessionID, child.id, input.description)
return { sessionID: child.id, status: "running" as const, output: BACKGROUND_STARTED }
return { sessionID: child.id, status: "running" as const, output: BACKGROUND_STARTED, background: true }
}
const result = yield* runtime.job.block({ id: child.id, sessionID: context.sessionID }).pipe(
@ -158,7 +159,7 @@ export const Plugin = {
)
if (result?.type === "backgrounded") {
yield* notifyWhenDone(context.sessionID, child.id, input.description)
return { sessionID: child.id, status: "running" as const, output: BACKGROUND_STARTED }
return { sessionID: child.id, status: "running" as const, output: BACKGROUND_STARTED, background: true }
}
if (result?.info.status === "error")
return yield* new ToolFailure({ message: result.info.error ?? "Subagent failed" })

View file

@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Layer, Schema } from "effect"
import { DateTime, Effect, Fiber, Layer, Schema } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
@ -43,7 +43,9 @@ const executionNode = makeGlobalNode({
const completed = new Set<SessionV2.ID>()
const complete = Effect.fn("SubagentTest.complete")(function* (sessionID: SessionV2.ID) {
if (completed.has(sessionID)) return
if ((yield* store.get(sessionID))?.title.includes("fail")) {
const session = yield* store.get(sessionID)
if (session?.title.includes("manual background")) yield* Effect.never
if (session?.title.includes("fail")) {
yield* new SessionRunnerModel.ModelNotSelectedError({ sessionID })
return
}
@ -191,6 +193,7 @@ describe("SubagentTool", () => {
})
expect(settled.output?.structured).toMatchObject({ status: "completed", output: childText })
expect(Schema.decodeUnknownSync(SubagentTool.Output)(settled.output?.structured).background).toBeUndefined()
const child = yield* sessions.get(outputSessionID(settled.output?.structured))
expect(child).toMatchObject({
parentID: parent.id,
@ -274,7 +277,7 @@ describe("SubagentTool", () => {
},
})
const childID = outputSessionID(settled.output?.structured)
expect(settled.output?.structured).toMatchObject({ status: "running" })
expect(settled.output?.structured).toMatchObject({ status: "running", background: true })
yield* Effect.yieldNow
const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic")
@ -285,4 +288,48 @@ describe("SubagentTool", () => {
),
),
)
it.live("marks foreground subagents as backgrounded when blocking wait is backgrounded", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
const sessions = yield* SessionV2.Service
const jobs = yield* Job.Service
const parent = yield* sessions.create({ location })
yield* withSubagent(parent.location)
const locations = yield* LocationServiceMap.Service
const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location)))
const settled = yield* settleTool(registry, {
sessionID: parent.id,
...toolIdentity,
call: {
type: "tool-call",
id: "call-manual-background-subagent",
name: SubagentTool.name,
input: { agent: "reviewer", description: "manual background review", prompt: "review" },
},
}).pipe(Effect.forkScoped)
const backgrounded = yield* Effect.gen(function* () {
for (const _ of Array.from({ length: 20 })) {
const result = yield* jobs.backgroundAll({ sessionID: parent.id, type: SubagentTool.name })
if (result.length > 0) return result
yield* Effect.sleep("10 millis")
}
return []
})
expect(backgrounded).toHaveLength(1)
expect((yield* Fiber.join(settled)).output?.structured).toMatchObject({
status: "running",
background: true,
})
}),
),
),
)
})