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:
parent
3d5c96e651
commit
b7a3b65106
@ -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_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.`
|
export const BACKGROUND_CANCEL_DESCRIPTION = `Cancel running background task(s). Use all=true to cancel ALL before final answer.`
|
||||||
|
|||||||
126
src/tools/background-task/create-background-wait.ts
Normal file
126
src/tools/background-task/create-background-wait.ts
Normal 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}`
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ export {
|
|||||||
createBackgroundTask,
|
createBackgroundTask,
|
||||||
createBackgroundOutput,
|
createBackgroundOutput,
|
||||||
createBackgroundCancel,
|
createBackgroundCancel,
|
||||||
|
createBackgroundWait,
|
||||||
} from "./tools"
|
} from "./tools"
|
||||||
|
|
||||||
export type * from "./types"
|
export type * from "./types"
|
||||||
|
|||||||
@ -9,3 +9,4 @@ export type {
|
|||||||
export { createBackgroundTask } from "./create-background-task"
|
export { createBackgroundTask } from "./create-background-task"
|
||||||
export { createBackgroundOutput } from "./create-background-output"
|
export { createBackgroundOutput } from "./create-background-output"
|
||||||
export { createBackgroundCancel } from "./create-background-cancel"
|
export { createBackgroundCancel } from "./create-background-cancel"
|
||||||
|
export { createBackgroundWait } from "./create-background-wait"
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export { createSkillMcpTool } from "./skill-mcp"
|
|||||||
import {
|
import {
|
||||||
createBackgroundOutput,
|
createBackgroundOutput,
|
||||||
createBackgroundCancel,
|
createBackgroundCancel,
|
||||||
|
createBackgroundWait,
|
||||||
type BackgroundOutputManager,
|
type BackgroundOutputManager,
|
||||||
type BackgroundCancelClient,
|
type BackgroundCancelClient,
|
||||||
} from "./background-task"
|
} from "./background-task"
|
||||||
@ -53,6 +54,7 @@ export function createBackgroundTools(manager: BackgroundManager, client: Openco
|
|||||||
return {
|
return {
|
||||||
background_output: createBackgroundOutput(outputManager, client),
|
background_output: createBackgroundOutput(outputManager, client),
|
||||||
background_cancel: createBackgroundCancel(manager, cancelClient),
|
background_cancel: createBackgroundCancel(manager, cancelClient),
|
||||||
|
background_wait: createBackgroundWait(outputManager, client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user