diff --git a/src/tools/background-task/constants.ts b/src/tools/background-task/constants.ts deleted file mode 100644 index 9a2e1fc9..00000000 --- a/src/tools/background-task/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const BACKGROUND_TASK_DESCRIPTION = `Run agent task in background. Returns task_id immediately; notifies on completion. - -Use \`background_output\` to get results. Prompts MUST be in English.` - -export const BACKGROUND_OUTPUT_DESCRIPTION = `Get output from background task. System notifies on completion, so block=true rarely needed.` - -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/index.ts b/src/tools/background-task/index.ts deleted file mode 100644 index 22324f8d..00000000 --- a/src/tools/background-task/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - createBackgroundTask, - createBackgroundOutput, - createBackgroundCancel, -} from "./tools" - -export type * from "./types" -export * from "./constants" diff --git a/src/tools/background-task/tools.ts b/src/tools/background-task/tools.ts deleted file mode 100644 index b9637e23..00000000 --- a/src/tools/background-task/tools.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin" -import { existsSync, readdirSync } from "node:fs" -import { join } from "node:path" -import type { BackgroundManager, BackgroundTask } from "../../features/background-agent" -import type { BackgroundTaskArgs, BackgroundOutputArgs, BackgroundCancelArgs } from "./types" -import { BACKGROUND_TASK_DESCRIPTION, BACKGROUND_OUTPUT_DESCRIPTION, BACKGROUND_CANCEL_DESCRIPTION } from "./constants" -import { findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector" - -type OpencodeClient = PluginInput["client"] - -function getMessageDir(sessionID: string): string | null { - if (!existsSync(MESSAGE_STORAGE)) return null - - const directPath = join(MESSAGE_STORAGE, sessionID) - if (existsSync(directPath)) return directPath - - for (const dir of readdirSync(MESSAGE_STORAGE)) { - const sessionPath = join(MESSAGE_STORAGE, dir, sessionID) - if (existsSync(sessionPath)) return sessionPath - } - - return null -} - -function formatDuration(start: Date, end?: Date): string { - const duration = (end ?? new Date()).getTime() - start.getTime() - const seconds = Math.floor(duration / 1000) - const minutes = Math.floor(seconds / 60) - const hours = Math.floor(minutes / 60) - - if (hours > 0) { - return `${hours}h ${minutes % 60}m ${seconds % 60}s` - } else if (minutes > 0) { - return `${minutes}m ${seconds % 60}s` - } else { - return `${seconds}s` - } -} - -type ToolContextWithMetadata = { - sessionID: string - messageID: string - agent: string - abort: AbortSignal - metadata?: (input: { title?: string; metadata?: Record }) => void -} - -export function createBackgroundTask(manager: BackgroundManager): ToolDefinition { - return tool({ - description: BACKGROUND_TASK_DESCRIPTION, - args: { - description: tool.schema.string().describe("Short task description (shown in status)"), - prompt: tool.schema.string().describe("Full detailed prompt for the agent"), - agent: tool.schema.string().describe("Agent type to use (any registered agent)"), - }, - async execute(args: BackgroundTaskArgs, toolContext) { - const ctx = toolContext as ToolContextWithMetadata - - if (!args.agent || args.agent.trim() === "") { - return `❌ Agent parameter is required. Please specify which agent to use (e.g., "explore", "librarian", "build", etc.)` - } - - try { - const messageDir = getMessageDir(ctx.sessionID) - const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null - const parentModel = prevMessage?.model?.providerID && prevMessage?.model?.modelID - ? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID } - : undefined - - const task = await manager.launch({ - description: args.description, - prompt: args.prompt, - agent: args.agent.trim(), - parentSessionID: ctx.sessionID, - parentMessageID: ctx.messageID, - parentModel, - }) - - ctx.metadata?.({ - title: args.description, - metadata: { sessionId: task.sessionID }, - }) - - return `Background task launched successfully. - -Task ID: ${task.id} -Session ID: ${task.sessionID} -Description: ${task.description} -Agent: ${task.agent} -Status: ${task.status} - -The system will notify you when the task completes. -Use \`background_output\` tool with task_id="${task.id}" to check progress: -- block=false (default): Check status immediately - returns full status info -- block=true: Wait for completion (rarely needed since system notifies)` - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - return `❌ Failed to launch background task: ${message}` - } - }, - }) -} - -function delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -function truncateText(text: string, maxLength: number): string { - if (text.length <= maxLength) return text - return text.slice(0, maxLength) + "..." -} - -function formatTaskStatus(task: BackgroundTask): string { - const duration = formatDuration(task.startedAt, task.completedAt) - const promptPreview = truncateText(task.prompt, 500) - - let progressSection = "" - if (task.progress?.lastTool) { - progressSection = `\n| Last tool | ${task.progress.lastTool} |` - } - - let lastMessageSection = "" - if (task.progress?.lastMessage) { - const truncated = truncateText(task.progress.lastMessage, 500) - const messageTime = task.progress.lastMessageAt - ? task.progress.lastMessageAt.toISOString() - : "N/A" - lastMessageSection = ` - -## Last Message (${messageTime}) - -\`\`\` -${truncated} -\`\`\`` - } - - let statusNote = "" - if (task.status === "running") { - statusNote = ` - -> **Note**: No need to wait explicitly - the system will notify you when this task completes.` - } else if (task.status === "error") { - statusNote = ` - -> **Failed**: The task encountered an error. Check the last message for details.` - } - - return `# Task Status - -| Field | Value | -|-------|-------| -| Task ID | \`${task.id}\` | -| Description | ${task.description} | -| Agent | ${task.agent} | -| Status | **${task.status}** | -| Duration | ${duration} | -| Session ID | \`${task.sessionID}\` |${progressSection} -${statusNote} -## Original Prompt - -\`\`\` -${promptPreview} -\`\`\`${lastMessageSection}` -} - -async function formatTaskResult(task: BackgroundTask, client: OpencodeClient): Promise { - const messagesResult = await client.session.messages({ - path: { id: task.sessionID }, - }) - - if (messagesResult.error) { - return `Error fetching messages: ${messagesResult.error}` - } - - // Handle both SDK response structures: direct array or wrapped in .data - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const messages = ((messagesResult as any).data ?? messagesResult) as Array<{ - info?: { role?: string } - parts?: Array<{ type?: string; text?: string }> - }> - - if (!Array.isArray(messages) || messages.length === 0) { - return `Task Result - -Task ID: ${task.id} -Description: ${task.description} -Duration: ${formatDuration(task.startedAt, task.completedAt)} -Session ID: ${task.sessionID} - ---- - -(No messages found)` - } - - const assistantMessages = messages.filter( - (m) => m.info?.role === "assistant" - ) - - if (assistantMessages.length === 0) { - return `Task Result - -Task ID: ${task.id} -Description: ${task.description} -Duration: ${formatDuration(task.startedAt, task.completedAt)} -Session ID: ${task.sessionID} - ---- - -(No assistant response found)` - } - - const lastMessage = assistantMessages[assistantMessages.length - 1] - const textParts = lastMessage?.parts?.filter( - (p) => p.type === "text" - ) ?? [] - const textContent = textParts - .map((p) => p.text ?? "") - .filter((text) => text.length > 0) - .join("\n") - - const duration = formatDuration(task.startedAt, task.completedAt) - - return `Task Result - -Task ID: ${task.id} -Description: ${task.description} -Duration: ${duration} -Session ID: ${task.sessionID} - ---- - -${textContent || "(No text output)"}` -} - -export function createBackgroundOutput(manager: BackgroundManager, client: OpencodeClient): ToolDefinition { - return tool({ - description: BACKGROUND_OUTPUT_DESCRIPTION, - args: { - task_id: tool.schema.string().describe("Task ID to get output from"), - block: tool.schema.boolean().optional().describe("Wait for completion (default: false). System notifies when done, so blocking is rarely needed."), - timeout: tool.schema.number().optional().describe("Max wait time in ms (default: 60000, max: 600000)"), - }, - async execute(args: BackgroundOutputArgs) { - try { - const task = manager.getTask(args.task_id) - if (!task) { - return `Task not found: ${args.task_id}` - } - - const shouldBlock = args.block === true - const timeoutMs = Math.min(args.timeout ?? 60000, 600000) - - // Already completed: return result immediately (regardless of block flag) - if (task.status === "completed") { - return await formatTaskResult(task, client) - } - - // Error or cancelled: return status immediately - if (task.status === "error" || task.status === "cancelled") { - return formatTaskStatus(task) - } - - // Non-blocking and still running: return status - if (!shouldBlock) { - return formatTaskStatus(task) - } - - // Blocking: poll until completion or timeout - const startTime = Date.now() - - while (Date.now() - startTime < timeoutMs) { - await delay(1000) - - const currentTask = manager.getTask(args.task_id) - if (!currentTask) { - return `Task was deleted: ${args.task_id}` - } - - if (currentTask.status === "completed") { - return await formatTaskResult(currentTask, client) - } - - if (currentTask.status === "error" || currentTask.status === "cancelled") { - return formatTaskStatus(currentTask) - } - } - - // Timeout exceeded: return current status - const finalTask = manager.getTask(args.task_id) - if (!finalTask) { - return `Task was deleted: ${args.task_id}` - } - return `Timeout exceeded (${timeoutMs}ms). Task still ${finalTask.status}.\n\n${formatTaskStatus(finalTask)}` - } catch (error) { - return `Error getting output: ${error instanceof Error ? error.message : String(error)}` - } - }, - }) -} - -export function createBackgroundCancel(manager: BackgroundManager, client: OpencodeClient): ToolDefinition { - return tool({ - description: BACKGROUND_CANCEL_DESCRIPTION, - args: { - taskId: tool.schema.string().optional().describe("Task ID to cancel (required if all=false)"), - all: tool.schema.boolean().optional().describe("Cancel all running background tasks (default: false)"), - }, - async execute(args: BackgroundCancelArgs, toolContext) { - try { - const cancelAll = args.all === true - - if (!cancelAll && !args.taskId) { - return `❌ Invalid arguments: Either provide a taskId or set all=true to cancel all running tasks.` - } - - if (cancelAll) { - const tasks = manager.getAllDescendantTasks(toolContext.sessionID) - const runningTasks = tasks.filter(t => t.status === "running") - - if (runningTasks.length === 0) { - return `✅ No running background tasks to cancel.` - } - - const results: string[] = [] - for (const task of runningTasks) { - client.session.abort({ - path: { id: task.sessionID }, - }).catch(() => {}) - - task.status = "cancelled" - task.completedAt = new Date() - results.push(`- ${task.id}: ${task.description}`) - } - - return `✅ Cancelled ${runningTasks.length} background task(s): - -${results.join("\n")}` - } - - const task = manager.getTask(args.taskId!) - if (!task) { - return `❌ Task not found: ${args.taskId}` - } - - if (task.status !== "running") { - return `❌ Cannot cancel task: current status is "${task.status}". -Only running tasks can be cancelled.` - } - - // Fire-and-forget: abort 요청을 보내고 await 하지 않음 - // await 하면 메인 세션까지 abort 되는 문제 발생 - client.session.abort({ - path: { id: task.sessionID }, - }).catch(() => {}) - - task.status = "cancelled" - task.completedAt = new Date() - - return `✅ Task cancelled successfully - -Task ID: ${task.id} -Description: ${task.description} -Session ID: ${task.sessionID} -Status: ${task.status}` - } catch (error) { - return `❌ Error cancelling task: ${error instanceof Error ? error.message : String(error)}` - } - }, - }) -} diff --git a/src/tools/background-task/types.ts b/src/tools/background-task/types.ts deleted file mode 100644 index 1b6cf879..00000000 --- a/src/tools/background-task/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface BackgroundTaskArgs { - description: string - prompt: string - agent: string -} - -export interface BackgroundOutputArgs { - task_id: string - block?: boolean - timeout?: number -} - -export interface BackgroundCancelArgs { - taskId?: string - all?: boolean -}