From b7a3b651068d2ef325b62a2c2d105374f9f442a2 Mon Sep 17 00:00:00 2001 From: ismeth Date: Thu, 19 Feb 2026 01:32:30 +0100 Subject: [PATCH] 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 --- src/tools/background-task/constants.ts | 2 + .../background-task/create-background-wait.ts | 126 ++++++++++++++++++ src/tools/background-task/index.ts | 1 + src/tools/background-task/tools.ts | 1 + src/tools/index.ts | 2 + 5 files changed, 132 insertions(+) create mode 100644 src/tools/background-task/create-background-wait.ts diff --git a/src/tools/background-task/constants.ts b/src/tools/background-task/constants.ts index e7ff93b0..eb0575ed 100644 --- a/src/tools/background-task/constants.ts +++ b/src/tools/background-task/constants.ts @@ -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.` diff --git a/src/tools/background-task/create-background-wait.ts b/src/tools/background-task/create-background-wait.ts new file mode 100644 index 00000000..20dc792e --- /dev/null +++ b/src/tools/background-task/create-background-wait.ts @@ -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 { + 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}` +} diff --git a/src/tools/background-task/index.ts b/src/tools/background-task/index.ts index 22324f8d..8c9113ac 100644 --- a/src/tools/background-task/index.ts +++ b/src/tools/background-task/index.ts @@ -2,6 +2,7 @@ export { createBackgroundTask, createBackgroundOutput, createBackgroundCancel, + createBackgroundWait, } from "./tools" export type * from "./types" diff --git a/src/tools/background-task/tools.ts b/src/tools/background-task/tools.ts index ce30adb9..8fb0b438 100644 --- a/src/tools/background-task/tools.ts +++ b/src/tools/background-task/tools.ts @@ -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" diff --git a/src/tools/index.ts b/src/tools/index.ts index 9eda5b83..6c5f484c 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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), } }