2026-02-15 15:09:59 +09:00
|
|
|
import { tool, type PluginInput, type ToolDefinition } from "@opencode-ai/plugin"
|
2026-02-08 15:01:42 +09:00
|
|
|
import type { BackgroundManager } from "../../features/background-agent"
|
|
|
|
|
import type { BackgroundTaskArgs } from "./types"
|
|
|
|
|
import { BACKGROUND_TASK_DESCRIPTION } from "./constants"
|
2026-02-16 18:20:27 +09:00
|
|
|
import { resolveMessageContext } from "../../features/hook-message-injector"
|
2026-02-08 15:01:42 +09:00
|
|
|
import { getSessionAgent } from "../../features/claude-code-session-state"
|
|
|
|
|
import { storeToolMetadata } from "../../features/tool-metadata-store"
|
|
|
|
|
import { log } from "../../shared/logger"
|
|
|
|
|
import { delay } from "./delay"
|
|
|
|
|
import { getMessageDir } from "./message-dir"
|
|
|
|
|
|
|
|
|
|
type ToolContextWithMetadata = {
|
|
|
|
|
sessionID: string
|
|
|
|
|
messageID: string
|
|
|
|
|
agent: string
|
|
|
|
|
abort: AbortSignal
|
|
|
|
|
metadata?: (input: { title?: string; metadata?: Record<string, unknown> }) => void
|
|
|
|
|
callID?: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 15:09:59 +09:00
|
|
|
export function createBackgroundTask(
|
|
|
|
|
manager: BackgroundManager,
|
|
|
|
|
client: PluginInput["client"]
|
|
|
|
|
): ToolDefinition {
|
2026-02-08 15:01:42 +09:00
|
|
|
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 `[ERROR] Agent parameter is required. Please specify which agent to use (e.g., "explore", "librarian", "build", etc.)`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const messageDir = getMessageDir(ctx.sessionID)
|
2026-02-16 18:20:27 +09:00
|
|
|
const { prevMessage, firstMessageAgent } = await resolveMessageContext(
|
|
|
|
|
ctx.sessionID,
|
|
|
|
|
client,
|
|
|
|
|
messageDir
|
|
|
|
|
)
|
2026-02-15 15:09:59 +09:00
|
|
|
|
2026-02-08 15:01:42 +09:00
|
|
|
const sessionAgent = getSessionAgent(ctx.sessionID)
|
|
|
|
|
const parentAgent = ctx.agent ?? sessionAgent ?? firstMessageAgent ?? prevMessage?.agent
|
|
|
|
|
|
|
|
|
|
log("[background_task] parentAgent resolution", {
|
|
|
|
|
sessionID: ctx.sessionID,
|
|
|
|
|
ctxAgent: ctx.agent,
|
|
|
|
|
sessionAgent,
|
|
|
|
|
firstMessageAgent,
|
|
|
|
|
prevMessageAgent: prevMessage?.agent,
|
|
|
|
|
resolvedParentAgent: parentAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const parentModel =
|
|
|
|
|
prevMessage?.model?.providerID && prevMessage?.model?.modelID
|
|
|
|
|
? {
|
|
|
|
|
providerID: prevMessage.model.providerID,
|
|
|
|
|
modelID: prevMessage.model.modelID,
|
|
|
|
|
...(prevMessage.model.variant ? { variant: prevMessage.model.variant } : {}),
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
|
|
|
|
|
|
|
|
|
const task = await manager.launch({
|
|
|
|
|
description: args.description,
|
|
|
|
|
prompt: args.prompt,
|
|
|
|
|
agent: args.agent.trim(),
|
|
|
|
|
parentSessionID: ctx.sessionID,
|
|
|
|
|
parentMessageID: ctx.messageID,
|
|
|
|
|
parentModel,
|
|
|
|
|
parentAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const WAIT_FOR_SESSION_INTERVAL_MS = 50
|
|
|
|
|
const WAIT_FOR_SESSION_TIMEOUT_MS = 30000
|
|
|
|
|
const waitStart = Date.now()
|
|
|
|
|
let sessionId = task.sessionID
|
|
|
|
|
while (!sessionId && Date.now() - waitStart < WAIT_FOR_SESSION_TIMEOUT_MS) {
|
|
|
|
|
if (ctx.abort?.aborted) {
|
|
|
|
|
await manager.cancelTask(task.id)
|
|
|
|
|
return `Task aborted and cancelled while waiting for session to start.\n\nTask ID: ${task.id}`
|
|
|
|
|
}
|
|
|
|
|
await delay(WAIT_FOR_SESSION_INTERVAL_MS)
|
|
|
|
|
const updated = manager.getTask(task.id)
|
2026-02-10 19:20:59 +09:00
|
|
|
if (!updated || updated.status === "error" || updated.status === "cancelled" || updated.status === "interrupt") {
|
2026-02-08 15:01:42 +09:00
|
|
|
return `Task ${!updated ? "was deleted" : `entered error state`}\.\n\nTask ID: ${task.id}`
|
|
|
|
|
}
|
|
|
|
|
sessionId = updated?.sessionID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bgMeta = {
|
|
|
|
|
title: args.description,
|
2026-03-12 01:02:12 +09:00
|
|
|
metadata: {
|
|
|
|
|
...(sessionId ? { sessionId } : {}),
|
|
|
|
|
},
|
2026-02-08 15:01:42 +09:00
|
|
|
}
|
2026-03-12 01:02:12 +09:00
|
|
|
ctx.metadata?.(bgMeta)
|
2026-02-08 15:01:42 +09:00
|
|
|
|
|
|
|
|
if (ctx.callID) {
|
|
|
|
|
storeToolMetadata(ctx.sessionID, ctx.callID, bgMeta)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `Background task launched successfully.
|
|
|
|
|
|
|
|
|
|
Task ID: ${task.id}
|
2026-03-12 01:02:12 +09:00
|
|
|
Session ID: ${sessionId ?? "(not yet assigned)"}
|
2026-02-08 15:01:42 +09:00
|
|
|
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 `[ERROR] Failed to launch background task: ${message}`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|