feat(athena): add background_wait tool for race-style task collection

New tool that takes multiple task IDs and blocks until ANY one completes (Promise.race pattern). Returns the completed task's result plus a progress summary with remaining IDs. Enables Athena to show incremental council progress without polling.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
ismeth 2026-02-19 01:32:30 +01:00 committed by YeonGyu-Kim
parent 3d5c96e651
commit b7a3b65106
5 changed files with 132 additions and 0 deletions

View File

@ -4,4 +4,6 @@ Use \`background_output\` to get results. Prompts MUST be in English.`
export const BACKGROUND_OUTPUT_DESCRIPTION = `Get output from background task. Use full_session=true to fetch session messages with filters. System notifies on completion, so block=true rarely needed.`
export const BACKGROUND_WAIT_DESCRIPTION = `Wait for the next background task to complete from a set of task IDs. Returns as soon as ANY one finishes, with its result and a progress summary. Call repeatedly with remaining IDs until all are done.`
export const BACKGROUND_CANCEL_DESCRIPTION = `Cancel running background task(s). Use all=true to cancel ALL before final answer.`

View File

@ -0,0 +1,126 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin"
import type { BackgroundOutputManager, BackgroundOutputClient } from "./clients"
import { BACKGROUND_WAIT_DESCRIPTION } from "./constants"
import { delay } from "./delay"
import { formatTaskResult } from "./task-result-format"
import { formatTaskStatus } from "./task-status-format"
const DEFAULT_TIMEOUT_MS = 120_000
const MAX_TIMEOUT_MS = 600_000
const TERMINAL_STATUSES = new Set(["completed", "error", "cancelled", "interrupt"])
function isTerminal(status: string): boolean {
return TERMINAL_STATUSES.has(status)
}
export function createBackgroundWait(manager: BackgroundOutputManager, client: BackgroundOutputClient): ToolDefinition {
return tool({
description: BACKGROUND_WAIT_DESCRIPTION,
args: {
task_ids: tool.schema.array(tool.schema.string()).describe("Task IDs to monitor — returns when ANY one reaches a terminal state"),
timeout: tool.schema.number().optional().describe("Max wait in ms. Default: 120000 (2 min). The tool returns immediately when any task finishes, so large values are fine."),
},
async execute(args: { task_ids: string[]; timeout?: number }) {
const taskIds = args.task_ids
if (!taskIds || taskIds.length === 0) {
return "Error: task_ids array is required and must not be empty."
}
const timeoutMs = Math.min(args.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS)
const alreadyTerminal = findFirstTerminal(manager, taskIds)
if (alreadyTerminal) {
return await buildCompletionResult(alreadyTerminal, manager, client, taskIds)
}
const startTime = Date.now()
while (Date.now() - startTime < timeoutMs) {
await delay(1000)
const found = findFirstTerminal(manager, taskIds)
if (found) {
return await buildCompletionResult(found, manager, client, taskIds)
}
}
return buildProgressSummary(manager, taskIds, true)
},
})
}
function findFirstTerminal(manager: BackgroundOutputManager, taskIds: string[]): { id: string; status: string } | undefined {
for (const id of taskIds) {
const task = manager.getTask(id)
if (!task) continue
if (isTerminal(task.status)) {
return { id, status: task.status }
}
}
return undefined
}
async function buildCompletionResult(
completed: { id: string; status: string },
manager: BackgroundOutputManager,
client: BackgroundOutputClient,
allIds: string[],
): Promise<string> {
const task = manager.getTask(completed.id)
if (!task) return `Task was deleted: ${completed.id}`
const taskResult = task.status === "completed"
? await formatTaskResult(task, client)
: formatTaskStatus(task)
const summary = buildProgressSummary(manager, allIds, false)
const remaining = allIds.filter((id) => !isTerminal(manager.getTask(id)?.status ?? ""))
const lines = [summary, "", "---", "", taskResult]
if (remaining.length > 0) {
const idList = remaining.map((id) => `"${id}"`).join(", ")
lines.push("", `**${remaining.length} task${remaining.length === 1 ? "" : "s"} still running.** Call background_wait again with task_ids: [${idList}]`)
} else {
lines.push("", "**All tasks complete.** Proceed with synthesis.")
}
return lines.join("\n")
}
function buildProgressSummary(manager: BackgroundOutputManager, taskIds: string[], isTimeout: boolean): string {
const done = taskIds.filter((id) => isTerminal(manager.getTask(id)?.status ?? ""))
const total = taskIds.length
const header = isTimeout
? `## Still Waiting: [${progressBar(done.length, total)}] ${done.length}/${total}`
: `## Council Progress: [${progressBar(done.length, total)}] ${done.length}/${total}`
const lines = [header, ""]
for (const id of taskIds) {
const t = manager.getTask(id)
if (!t) {
lines.push(`- \`${id}\` — not found`)
continue
}
const marker = isTerminal(t.status) ? "received" : "waiting..."
lines.push(`- ${t.description || t.id}${marker}`)
}
if (isTimeout) {
const remaining = taskIds.filter((id) => !isTerminal(manager.getTask(id)?.status ?? ""))
if (remaining.length > 0) {
const idList = remaining.map((id) => `"${id}"`).join(", ")
lines.push("", `**Timeout — tasks still running.** Call background_wait again with task_ids: [${idList}]`)
}
}
return lines.join("\n")
}
function progressBar(done: number, total: number): string {
const filled = "#".repeat(done)
const empty = "-".repeat(total - done)
return `${filled}${empty}`
}

View File

@ -2,6 +2,7 @@ export {
createBackgroundTask,
createBackgroundOutput,
createBackgroundCancel,
createBackgroundWait,
} from "./tools"
export type * from "./types"

View File

@ -9,3 +9,4 @@ export type {
export { createBackgroundTask } from "./create-background-task"
export { createBackgroundOutput } from "./create-background-output"
export { createBackgroundCancel } from "./create-background-cancel"
export { createBackgroundWait } from "./create-background-wait"

View File

@ -25,6 +25,7 @@ export { createSkillMcpTool } from "./skill-mcp"
import {
createBackgroundOutput,
createBackgroundCancel,
createBackgroundWait,
type BackgroundOutputManager,
type BackgroundCancelClient,
} from "./background-task"
@ -53,6 +54,7 @@ export function createBackgroundTools(manager: BackgroundManager, client: Openco
return {
background_output: createBackgroundOutput(outputManager, client),
background_cancel: createBackgroundCancel(manager, cancelClient),
background_wait: createBackgroundWait(outputManager, client),
}
}