2026-01-09 02:24:43 +09:00
import { tool , type PluginInput , type ToolDefinition } from "@opencode-ai/plugin"
import { existsSync , readdirSync } from "node:fs"
import { join } from "node:path"
import type { BackgroundManager } from "../../features/background-agent"
2026-01-16 17:34:40 +09:00
import type { DelegateTaskArgs } from "./types"
2026-01-11 18:02:36 +08:00
import type { CategoryConfig , CategoriesConfig , GitMasterConfig } from "../../config/schema"
2026-01-20 15:52:13 +09:00
import { DELEGATE_TASK_DESCRIPTION , DEFAULT_CATEGORIES , CATEGORY_PROMPT_APPENDS , CATEGORY_MODEL_CATALOG } from "./constants"
2026-01-13 07:39:25 +07:00
import { findNearestMessageWithFields , findFirstMessageWithAgent , MESSAGE_STORAGE } from "../../features/hook-message-injector"
2026-01-16 01:57:57 +09:00
import { resolveMultipleSkillsAsync } from "../../features/opencode-skill-loader/skill-content"
import { discoverSkills } from "../../features/opencode-skill-loader"
2026-01-09 02:24:43 +09:00
import { getTaskToastManager } from "../../features/task-toast-manager"
2026-01-13 08:58:47 +07:00
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
2026-01-13 07:39:25 +07:00
import { subagentSessions , getSessionAgent } from "../../features/claude-code-session-state"
2026-01-18 22:08:27 -05:00
import { log , getAgentToolRestrictions , resolveModel , getOpenCodeConfigPaths } from "../../shared"
2026-01-09 02:24:43 +09:00
type OpencodeClient = PluginInput [ "client" ]
const SISYPHUS_JUNIOR_AGENT = "Sisyphus-Junior"
const CATEGORY_EXAMPLES = Object . keys ( DEFAULT_CATEGORIES ) . map ( k = > ` ' ${ k } ' ` ) . join ( ", " )
function parseModelString ( model : string ) : { providerID : string ; modelID : string } | undefined {
const parts = model . split ( "/" )
if ( parts . length >= 2 ) {
return { providerID : parts [ 0 ] , modelID : parts.slice ( 1 ) . join ( "/" ) }
}
return undefined
}
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 `
if ( minutes > 0 ) return ` ${ minutes } m ${ seconds % 60 } s `
return ` ${ seconds } s `
}
2026-01-15 18:54:04 +09:00
interface ErrorContext {
operation : string
2026-01-16 17:34:40 +09:00
args? : DelegateTaskArgs
2026-01-15 18:54:04 +09:00
sessionID? : string
agent? : string
category? : string
}
function formatDetailedError ( error : unknown , ctx : ErrorContext ) : string {
const message = error instanceof Error ? error.message : String ( error )
const stack = error instanceof Error ? error.stack : undefined
const lines : string [ ] = [
2026-01-17 20:40:35 +09:00
` ${ ctx . operation } failed ` ,
2026-01-15 18:54:04 +09:00
"" ,
` **Error**: ${ message } ` ,
]
if ( ctx . sessionID ) {
lines . push ( ` **Session ID**: ${ ctx . sessionID } ` )
}
if ( ctx . agent ) {
lines . push ( ` **Agent**: ${ ctx . agent } ${ ctx . category ? ` (category: ${ ctx . category } ) ` : "" } ` )
}
if ( ctx . args ) {
lines . push ( "" , "**Arguments**:" )
lines . push ( ` - description: " ${ ctx . args . description } " ` )
lines . push ( ` - category: ${ ctx . args . category ? ? "(none)" } ` )
lines . push ( ` - subagent_type: ${ ctx . args . subagent_type ? ? "(none)" } ` )
lines . push ( ` - run_in_background: ${ ctx . args . run_in_background } ` )
lines . push ( ` - skills: [ ${ ctx . args . skills ? . join ( ", " ) ? ? "" } ] ` )
if ( ctx . args . resume ) {
lines . push ( ` - resume: ${ ctx . args . resume } ` )
}
}
if ( stack ) {
lines . push ( "" , "**Stack Trace**:" )
lines . push ( "```" )
lines . push ( stack . split ( "\n" ) . slice ( 0 , 10 ) . join ( "\n" ) )
lines . push ( "```" )
}
return lines . join ( "\n" )
}
2026-01-09 02:24:43 +09:00
type ToolContextWithMetadata = {
sessionID : string
messageID : string
agent : string
abort : AbortSignal
metadata ? : ( input : { title? : string ; metadata? : Record < string , unknown > } ) = > void
}
2026-01-17 12:51:03 -05:00
export function resolveCategoryConfig (
2026-01-09 02:24:43 +09:00
categoryName : string ,
2026-01-14 14:45:01 -05:00
options : {
userCategories? : CategoriesConfig
2026-01-17 12:51:03 -05:00
inheritedModel? : string
systemDefaultModel : string
2026-01-14 14:45:01 -05:00
}
2026-01-17 12:51:03 -05:00
) : { config : CategoryConfig ; promptAppend : string ; model : string } | null {
const { userCategories , inheritedModel , systemDefaultModel } = options
2026-01-09 02:24:43 +09:00
const defaultConfig = DEFAULT_CATEGORIES [ categoryName ]
const userConfig = userCategories ? . [ categoryName ]
const defaultPromptAppend = CATEGORY_PROMPT_APPENDS [ categoryName ] ? ? ""
if ( ! defaultConfig && ! userConfig ) {
return null
}
2026-01-17 12:51:03 -05:00
// Model priority: user override > inherited from parent > system default
const model = resolveModel ( {
userModel : userConfig?.model ,
inheritedModel ,
systemDefault : systemDefaultModel ,
} )
2026-01-09 02:24:43 +09:00
const config : CategoryConfig = {
. . . defaultConfig ,
. . . userConfig ,
2026-01-14 14:45:01 -05:00
model ,
2026-01-09 02:24:43 +09:00
}
let promptAppend = defaultPromptAppend
if ( userConfig ? . prompt_append ) {
promptAppend = defaultPromptAppend
? defaultPromptAppend + "\n\n" + userConfig.prompt_append
: userConfig.prompt_append
}
2026-01-14 14:45:01 -05:00
return { config , promptAppend , model }
2026-01-09 02:24:43 +09:00
}
2026-01-16 17:34:40 +09:00
export interface DelegateTaskToolOptions {
2026-01-09 02:24:43 +09:00
manager : BackgroundManager
client : OpencodeClient
2026-01-13 06:27:56 +01:00
directory : string
2026-01-09 02:24:43 +09:00
userCategories? : CategoriesConfig
2026-01-11 18:02:36 +08:00
gitMasterConfig? : GitMasterConfig
2026-01-09 02:24:43 +09:00
}
export interface BuildSystemContentInput {
skillContent? : string
categoryPromptAppend? : string
}
export function buildSystemContent ( input : BuildSystemContentInput ) : string | undefined {
const { skillContent , categoryPromptAppend } = input
if ( ! skillContent && ! categoryPromptAppend ) {
return undefined
}
if ( skillContent && categoryPromptAppend ) {
return ` ${ skillContent } \ n \ n ${ categoryPromptAppend } `
}
return skillContent || categoryPromptAppend
}
2026-01-16 17:34:40 +09:00
export function createDelegateTask ( options : DelegateTaskToolOptions ) : ToolDefinition {
2026-01-13 06:27:56 +01:00
const { manager , client , directory , userCategories , gitMasterConfig } = options
2026-01-09 02:24:43 +09:00
return tool ( {
2026-01-16 17:34:40 +09:00
description : DELEGATE_TASK_DESCRIPTION ,
2026-01-09 02:24:43 +09:00
args : {
description : tool.schema.string ( ) . describe ( "Short task description" ) ,
prompt : tool.schema.string ( ) . describe ( "Full detailed prompt for the agent" ) ,
category : tool.schema.string ( ) . optional ( ) . describe ( ` Category name (e.g., ${ CATEGORY_EXAMPLES } ). Mutually exclusive with subagent_type. ` ) ,
subagent_type : tool.schema.string ( ) . optional ( ) . describe ( "Agent name directly (e.g., 'oracle', 'explore'). Mutually exclusive with category." ) ,
run_in_background : tool.schema.boolean ( ) . describe ( "Run in background. MUST be explicitly set. Use false for task delegation, true only for parallel exploration." ) ,
resume : tool.schema.string ( ) . optional ( ) . describe ( "Session ID to resume - continues previous agent session with full context" ) ,
2026-01-17 20:40:35 +09:00
skills : tool.schema.array ( tool . schema . string ( ) ) . describe ( "Array of skill names to prepend to the prompt. Use [] (empty array) if no skills needed." ) ,
2026-01-09 02:24:43 +09:00
} ,
2026-01-16 17:34:40 +09:00
async execute ( args : DelegateTaskArgs , toolContext ) {
2026-01-09 02:24:43 +09:00
const ctx = toolContext as ToolContextWithMetadata
if ( args . run_in_background === undefined ) {
2026-01-17 20:40:35 +09:00
return ` Invalid arguments: 'run_in_background' parameter is REQUIRED. Use run_in_background=false for task delegation, run_in_background=true only for parallel exploration. `
2026-01-09 02:24:43 +09:00
}
if ( args . skills === undefined ) {
2026-01-17 20:40:35 +09:00
return ` Invalid arguments: 'skills' parameter is REQUIRED. Use skills=[] if no skills are needed, or provide an array of skill names. `
2026-01-16 13:12:48 +09:00
}
2026-01-17 20:40:35 +09:00
if ( args . skills === null ) {
return ` Invalid arguments: skills=null is not allowed. Use skills=[] (empty array) if no skills are needed. `
2026-01-09 02:24:43 +09:00
}
const runInBackground = args . run_in_background === true
let skillContent : string | undefined
2026-01-17 20:40:35 +09:00
if ( args . skills . length > 0 ) {
2026-01-16 01:57:57 +09:00
const { resolved , notFound } = await resolveMultipleSkillsAsync ( args . skills , { gitMasterConfig } )
2026-01-09 02:24:43 +09:00
if ( notFound . length > 0 ) {
2026-01-16 01:57:57 +09:00
const allSkills = await discoverSkills ( { includeClaudeCodePaths : true } )
const available = allSkills . map ( s = > s . name ) . join ( ", " )
2026-01-17 20:40:35 +09:00
return ` Skills not found: ${ notFound . join ( ", " ) } . Available: ${ available } `
2026-01-09 02:24:43 +09:00
}
skillContent = Array . from ( resolved . values ( ) ) . join ( "\n\n" )
}
const messageDir = getMessageDir ( ctx . sessionID )
const prevMessage = messageDir ? findNearestMessageWithFields ( messageDir ) : null
2026-01-13 07:39:25 +07:00
const firstMessageAgent = messageDir ? findFirstMessageWithAgent ( messageDir ) : null
const sessionAgent = getSessionAgent ( ctx . sessionID )
const parentAgent = ctx . agent ? ? sessionAgent ? ? firstMessageAgent ? ? prevMessage ? . agent
2026-01-16 17:34:40 +09:00
log ( "[delegate_task] parentAgent resolution" , {
2026-01-13 07:39:25 +07:00
sessionID : ctx.sessionID ,
messageDir ,
ctxAgent : ctx.agent ,
sessionAgent ,
firstMessageAgent ,
prevMessageAgent : prevMessage?.agent ,
resolvedParentAgent : parentAgent ,
} )
2026-01-09 02:24:43 +09:00
const parentModel = prevMessage ? . model ? . providerID && prevMessage ? . model ? . modelID
? { providerID : prevMessage.model.providerID , modelID : prevMessage.model.modelID }
: undefined
if ( args . resume ) {
if ( runInBackground ) {
try {
const task = await manager . resume ( {
sessionId : args.resume ,
prompt : args.prompt ,
parentSessionID : ctx.sessionID ,
parentMessageID : ctx.messageID ,
parentModel ,
parentAgent ,
} )
ctx . metadata ? . ( {
title : ` Resume: ${ task . description } ` ,
metadata : { sessionId : task.sessionID } ,
} )
return ` Background task resumed.
Task ID: ${ task . id }
Session ID: ${ task . sessionID }
Description: ${ task . description }
Agent: ${ task . agent }
Status: ${ task . status }
Agent continues with full previous context preserved.
Use \` background_output \` with task_id=" ${ task . id } " to check progress. `
} catch ( error ) {
2026-01-15 18:54:04 +09:00
return formatDetailedError ( error , {
operation : "Resume background task" ,
args ,
sessionID : args.resume ,
} )
2026-01-09 02:24:43 +09:00
}
}
const toastManager = getTaskToastManager ( )
const taskId = ` resume_sync_ ${ args . resume . slice ( 0 , 8 ) } `
const startTime = new Date ( )
if ( toastManager ) {
toastManager . addTask ( {
id : taskId ,
description : args.description ,
agent : "resume" ,
isBackground : false ,
} )
}
ctx . metadata ? . ( {
title : ` Resume: ${ args . description } ` ,
metadata : { sessionId : args.resume , sync : true } ,
} )
try {
2026-01-16 02:33:44 +09:00
let resumeAgent : string | undefined
let resumeModel : { providerID : string ; modelID : string } | undefined
try {
const messagesResp = await client . session . messages ( { path : { id : args.resume } } )
const messages = ( messagesResp . data ? ? [ ] ) as Array < {
2026-01-16 13:54:22 +09:00
info ? : { agent? : string ; model ? : { providerID : string ; modelID : string } ; modelID? : string ; providerID? : string }
2026-01-16 02:33:44 +09:00
} >
for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
const info = messages [ i ] . info
2026-01-16 13:54:22 +09:00
if ( info ? . agent || info ? . model || ( info ? . modelID && info ? . providerID ) ) {
2026-01-16 02:33:44 +09:00
resumeAgent = info . agent
2026-01-16 13:54:22 +09:00
resumeModel = info . model ? ? ( info . providerID && info . modelID ? { providerID : info.providerID , modelID : info.modelID } : undefined )
2026-01-16 02:33:44 +09:00
break
}
}
} catch {
const resumeMessageDir = getMessageDir ( args . resume )
const resumeMessage = resumeMessageDir ? findNearestMessageWithFields ( resumeMessageDir ) : null
resumeAgent = resumeMessage ? . agent
resumeModel = resumeMessage ? . model ? . providerID && resumeMessage ? . model ? . modelID
? { providerID : resumeMessage.model.providerID , modelID : resumeMessage.model.modelID }
: undefined
}
2026-01-14 18:55:46 +09:00
2026-01-09 02:24:43 +09:00
await client . session . prompt ( {
path : { id : args.resume } ,
body : {
2026-01-14 18:55:46 +09:00
. . . ( resumeAgent !== undefined ? { agent : resumeAgent } : { } ) ,
. . . ( resumeModel !== undefined ? { model : resumeModel } : { } ) ,
2026-01-09 02:24:43 +09:00
tools : {
2026-01-16 17:11:34 +09:00
. . . ( resumeAgent ? getAgentToolRestrictions ( resumeAgent ) : { } ) ,
2026-01-09 02:24:43 +09:00
task : false ,
2026-01-16 17:34:40 +09:00
delegate_task : false ,
2026-01-13 21:00:00 +09:00
call_omo_agent : true ,
2026-01-09 02:24:43 +09:00
} ,
parts : [ { type : "text" , text : args.prompt } ] ,
} ,
} )
} catch ( promptError ) {
if ( toastManager ) {
toastManager . removeTask ( taskId )
}
const errorMessage = promptError instanceof Error ? promptError.message : String ( promptError )
2026-01-17 20:40:35 +09:00
return ` Failed to send resume prompt: ${ errorMessage } \ n \ nSession ID: ${ args . resume } `
2026-01-09 02:24:43 +09:00
}
2026-01-10 13:00:25 +08:00
// Wait for message stability after prompt completes
const POLL_INTERVAL_MS = 500
const MIN_STABILITY_TIME_MS = 5000
const STABILITY_POLLS_REQUIRED = 3
const pollStart = Date . now ( )
let lastMsgCount = 0
let stablePolls = 0
while ( Date . now ( ) - pollStart < 60000 ) {
await new Promise ( resolve = > setTimeout ( resolve , POLL_INTERVAL_MS ) )
const elapsed = Date . now ( ) - pollStart
if ( elapsed < MIN_STABILITY_TIME_MS ) continue
const messagesCheck = await client . session . messages ( { path : { id : args.resume } } )
const msgs = ( ( messagesCheck as { data? : unknown } ) . data ? ? messagesCheck ) as Array < unknown >
const currentMsgCount = msgs . length
if ( currentMsgCount > 0 && currentMsgCount === lastMsgCount ) {
stablePolls ++
if ( stablePolls >= STABILITY_POLLS_REQUIRED ) break
} else {
stablePolls = 0
lastMsgCount = currentMsgCount
}
}
2026-01-09 02:24:43 +09:00
const messagesResult = await client . session . messages ( {
path : { id : args.resume } ,
} )
if ( messagesResult . error ) {
if ( toastManager ) {
toastManager . removeTask ( taskId )
}
2026-01-17 20:40:35 +09:00
return ` Error fetching result: ${ messagesResult . error } \ n \ nSession ID: ${ args . resume } `
2026-01-09 02:24:43 +09:00
}
const messages = ( ( messagesResult as { data? : unknown } ) . data ? ? messagesResult ) as Array < {
info ? : { role? : string ; time ? : { created? : number } }
parts? : Array < { type ? : string ; text? : string } >
} >
const assistantMessages = messages
. filter ( ( m ) = > m . info ? . role === "assistant" )
. sort ( ( a , b ) = > ( b . info ? . time ? . created ? ? 0 ) - ( a . info ? . time ? . created ? ? 0 ) )
const lastMessage = assistantMessages [ 0 ]
if ( toastManager ) {
toastManager . removeTask ( taskId )
}
if ( ! lastMessage ) {
2026-01-17 20:40:35 +09:00
return ` No assistant response found. \ n \ nSession ID: ${ args . resume } `
2026-01-09 02:24:43 +09:00
}
2026-01-10 13:00:25 +08:00
// Extract text from both "text" and "reasoning" parts (thinking models use "reasoning")
const textParts = lastMessage ? . parts ? . filter ( ( p ) = > p . type === "text" || p . type === "reasoning" ) ? ? [ ]
2026-01-09 02:24:43 +09:00
const textContent = textParts . map ( ( p ) = > p . text ? ? "" ) . filter ( Boolean ) . join ( "\n" )
const duration = formatDuration ( startTime )
return ` Task resumed and completed in ${ duration } .
Session ID: ${ args . resume }
---
${ textContent || "(No text output)" } `
}
if ( args . category && args . subagent_type ) {
2026-01-17 20:40:35 +09:00
return ` Invalid arguments: Provide EITHER category OR subagent_type, not both. `
2026-01-09 02:24:43 +09:00
}
if ( ! args . category && ! args . subagent_type ) {
2026-01-17 20:40:35 +09:00
return ` Invalid arguments: Must provide either category or subagent_type. `
2026-01-09 02:24:43 +09:00
}
2026-01-14 14:45:01 -05:00
// Fetch OpenCode config at boundary to get system default model
let systemDefaultModel : string | undefined
try {
const openCodeConfig = await client . config . get ( )
2026-01-18 22:08:27 -05:00
systemDefaultModel = ( openCodeConfig as { data ? : { model? : string } } ) ? . data ? . model
2026-01-14 14:45:01 -05:00
} catch {
// Config fetch failed, proceed without system default
systemDefaultModel = undefined
}
2026-01-09 02:24:43 +09:00
let agentToUse : string
2026-01-10 21:44:20 +00:00
let categoryModel : { providerID : string ; modelID : string ; variant? : string } | undefined
2026-01-09 02:24:43 +09:00
let categoryPromptAppend : string | undefined
2026-01-17 12:51:03 -05:00
const inheritedModel = parentModel
2026-01-13 08:58:47 +07:00
? ` ${ parentModel . providerID } / ${ parentModel . modelID } `
: undefined
let modelInfo : ModelFallbackInfo | undefined
2026-01-09 02:24:43 +09:00
if ( args . category ) {
2026-01-17 12:51:03 -05:00
// Guard: require system default model for category delegation
if ( ! systemDefaultModel ) {
2026-01-18 22:08:27 -05:00
const paths = getOpenCodeConfigPaths ( { binary : "opencode" , version : null } )
return (
'oh-my-opencode requires a default model.\n\n' +
` Add this to ${ paths . configJsonc } : \ n \ n ` +
' "model": "anthropic/claude-sonnet-4-5"\n\n' +
'(Replace with your preferred provider/model)'
)
2026-01-17 12:51:03 -05:00
}
2026-01-14 14:45:01 -05:00
const resolved = resolveCategoryConfig ( args . category , {
userCategories ,
2026-01-17 12:51:03 -05:00
inheritedModel ,
2026-01-14 14:45:01 -05:00
systemDefaultModel ,
} )
2026-01-09 02:24:43 +09:00
if ( ! resolved ) {
2026-01-17 20:40:35 +09:00
return ` Unknown category: " ${ args . category } ". Available: ${ Object . keys ( { . . . DEFAULT_CATEGORIES , . . . userCategories } ).join(", ")} `
2026-01-09 02:24:43 +09:00
}
2026-01-14 22:15:05 +07:00
// Determine model source by comparing against the actual resolved model
2026-01-14 14:45:01 -05:00
const actualModel = resolved . model
2026-01-14 22:15:05 +07:00
const userDefinedModel = userCategories ? . [ args . category ] ? . model
2026-01-14 14:45:01 -05:00
if ( ! parseModelString ( actualModel ) ) {
2026-01-17 20:40:35 +09:00
return ` Invalid model format " ${ actualModel } ". Expected "provider/model" format (e.g., "anthropic/claude-sonnet-4-5"). `
2026-01-14 14:45:01 -05:00
}
switch ( actualModel ) {
case userDefinedModel :
modelInfo = { model : actualModel , type : "user-defined" }
break
2026-01-17 12:51:03 -05:00
case inheritedModel :
2026-01-14 14:45:01 -05:00
modelInfo = { model : actualModel , type : "inherited" }
break
case systemDefaultModel :
modelInfo = { model : actualModel , type : "system-default" }
break
2026-01-13 08:58:47 +07:00
}
2026-01-09 02:24:43 +09:00
agentToUse = SISYPHUS_JUNIOR_AGENT
2026-01-14 14:45:01 -05:00
const parsedModel = parseModelString ( actualModel )
2026-01-10 21:44:20 +00:00
categoryModel = parsedModel
? ( resolved . config . variant
? { . . . parsedModel , variant : resolved.config.variant }
: parsedModel )
: undefined
2026-01-09 02:24:43 +09:00
categoryPromptAppend = resolved . promptAppend || undefined
} else {
2026-01-14 14:45:01 -05:00
if ( ! args . subagent_type ? . trim ( ) ) {
2026-01-17 20:40:35 +09:00
return ` Agent name cannot be empty. `
2026-01-09 02:24:43 +09:00
}
2026-01-14 14:45:01 -05:00
const agentName = args . subagent_type . trim ( )
agentToUse = agentName
2026-01-09 02:24:43 +09:00
// Validate agent exists and is callable (not a primary agent)
try {
const agentsResult = await client . app . agents ( )
type AgentInfo = { name : string ; mode ? : "subagent" | "primary" | "all" }
const agents = ( agentsResult as { data? : AgentInfo [ ] } ) . data ? ? agentsResult as unknown as AgentInfo [ ]
const callableAgents = agents . filter ( ( a ) = > a . mode !== "primary" )
const callableNames = callableAgents . map ( ( a ) = > a . name )
if ( ! callableNames . includes ( agentToUse ) ) {
const isPrimaryAgent = agents . some ( ( a ) = > a . name === agentToUse && a . mode === "primary" )
if ( isPrimaryAgent ) {
2026-01-17 20:40:35 +09:00
return ` Cannot call primary agent " ${ agentToUse } " via delegate_task. Primary agents are top-level orchestrators. `
2026-01-09 02:24:43 +09:00
}
const availableAgents = callableNames
. sort ( )
. join ( ", " )
2026-01-17 20:40:35 +09:00
return ` Unknown agent: " ${ agentToUse } ". Available agents: ${ availableAgents } `
2026-01-09 02:24:43 +09:00
}
} catch {
// If we can't fetch agents, proceed anyway - the session.prompt will fail with a clearer error
}
}
const systemContent = buildSystemContent ( { skillContent , categoryPromptAppend } )
if ( runInBackground ) {
try {
const task = await manager . launch ( {
description : args.description ,
prompt : args.prompt ,
agent : agentToUse ,
parentSessionID : ctx.sessionID ,
parentMessageID : ctx.messageID ,
parentModel ,
parentAgent ,
model : categoryModel ,
2026-01-17 20:40:35 +09:00
skills : args.skills.length > 0 ? args.skills : undefined ,
2026-01-09 02:24:43 +09:00
skillContent : systemContent ,
} )
ctx . metadata ? . ( {
title : args.description ,
metadata : { sessionId : task.sessionID , category : args.category } ,
} )
return ` Background task launched.
Task ID: ${ task . id }
Session ID: ${ task . sessionID }
Description: ${ task . description }
Agent: ${ task . agent } ${ args . category ? ` (category: ${ args . category } ) ` : "" }
Status: ${ task . status }
System notifies on completion. Use \` background_output \` with task_id=" ${ task . id } " to check. `
} catch ( error ) {
2026-01-15 18:54:04 +09:00
return formatDetailedError ( error , {
operation : "Launch background task" ,
args ,
agent : agentToUse ,
category : args.category ,
} )
2026-01-09 02:24:43 +09:00
}
}
const toastManager = getTaskToastManager ( )
let taskId : string | undefined
let syncSessionID : string | undefined
try {
2026-01-13 13:41:41 -05:00
const parentSession = client . session . get
? await client . session . get ( { path : { id : ctx.sessionID } } ) . catch ( ( ) = > null )
: null
2026-01-13 06:27:56 +01:00
const parentDirectory = parentSession ? . data ? . directory ? ? directory
2026-01-09 02:24:43 +09:00
const createResult = await client . session . create ( {
body : {
parentID : ctx.sessionID ,
title : ` Task: ${ args . description } ` ,
} ,
2026-01-13 06:27:56 +01:00
query : {
directory : parentDirectory ,
} ,
2026-01-09 02:24:43 +09:00
} )
if ( createResult . error ) {
2026-01-17 20:40:35 +09:00
return ` Failed to create session: ${ createResult . error } `
2026-01-09 02:24:43 +09:00
}
const sessionID = createResult . data . id
syncSessionID = sessionID
subagentSessions . add ( sessionID )
taskId = ` sync_ ${ sessionID . slice ( 0 , 8 ) } `
const startTime = new Date ( )
if ( toastManager ) {
toastManager . addTask ( {
id : taskId ,
description : args.description ,
agent : agentToUse ,
isBackground : false ,
2026-01-17 20:40:35 +09:00
skills : args.skills.length > 0 ? args.skills : undefined ,
2026-01-13 08:58:47 +07:00
modelInfo ,
2026-01-09 02:24:43 +09:00
} )
}
ctx . metadata ? . ( {
title : args.description ,
metadata : { sessionId : sessionID , category : args.category , sync : true } ,
} )
2026-01-12 19:26:32 +11:00
try {
await client . session . prompt ( {
path : { id : sessionID } ,
body : {
agent : agentToUse ,
system : systemContent ,
tools : {
task : false ,
2026-01-16 17:34:40 +09:00
delegate_task : false ,
2026-01-13 21:00:00 +09:00
call_omo_agent : true ,
2026-01-12 19:26:32 +11:00
} ,
parts : [ { type : "text" , text : args.prompt } ] ,
. . . ( categoryModel ? { model : categoryModel } : { } ) ,
2026-01-09 02:24:43 +09:00
} ,
2026-01-12 19:26:32 +11:00
} )
} catch ( promptError ) {
2026-01-09 02:24:43 +09:00
if ( toastManager && taskId !== undefined ) {
toastManager . removeTask ( taskId )
}
2026-01-12 19:26:32 +11:00
const errorMessage = promptError instanceof Error ? promptError.message : String ( promptError )
2026-01-09 02:24:43 +09:00
if ( errorMessage . includes ( "agent.name" ) || errorMessage . includes ( "undefined" ) ) {
2026-01-15 18:54:04 +09:00
return formatDetailedError ( new Error ( ` Agent " ${ agentToUse } " not found. Make sure the agent is registered in your opencode.json or provided by a plugin. ` ) , {
operation : "Send prompt to agent" ,
args ,
sessionID ,
agent : agentToUse ,
category : args.category ,
} )
2026-01-09 02:24:43 +09:00
}
2026-01-15 18:54:04 +09:00
return formatDetailedError ( promptError , {
operation : "Send prompt" ,
args ,
sessionID ,
agent : agentToUse ,
category : args.category ,
} )
2026-01-09 02:24:43 +09:00
}
2026-01-10 13:00:25 +08:00
// Poll for session completion with stability detection
// The session may show as "idle" before messages appear, so we also check message stability
2026-01-09 02:24:43 +09:00
const POLL_INTERVAL_MS = 500
const MAX_POLL_TIME_MS = 10 * 60 * 1000
2026-01-10 13:00:25 +08:00
const MIN_STABILITY_TIME_MS = 10000 // Minimum 10s before accepting completion
const STABILITY_POLLS_REQUIRED = 3
2026-01-09 02:24:43 +09:00
const pollStart = Date . now ( )
2026-01-10 13:00:25 +08:00
let lastMsgCount = 0
let stablePolls = 0
2026-01-13 21:00:00 +09:00
let pollCount = 0
2026-01-16 17:34:40 +09:00
log ( "[delegate_task] Starting poll loop" , { sessionID , agentToUse } )
2026-01-09 02:24:43 +09:00
while ( Date . now ( ) - pollStart < MAX_POLL_TIME_MS ) {
2026-01-13 21:00:00 +09:00
if ( ctx . abort ? . aborted ) {
2026-01-16 17:34:40 +09:00
log ( "[delegate_task] Aborted by user" , { sessionID } )
2026-01-13 21:00:00 +09:00
if ( toastManager && taskId ) toastManager . removeTask ( taskId )
return ` Task aborted. \ n \ nSession ID: ${ sessionID } `
}
2026-01-09 02:24:43 +09:00
await new Promise ( resolve = > setTimeout ( resolve , POLL_INTERVAL_MS ) )
2026-01-13 21:00:00 +09:00
pollCount ++
2026-01-09 02:24:43 +09:00
const statusResult = await client . session . status ( )
const allStatuses = ( statusResult . data ? ? { } ) as Record < string , { type : string } >
const sessionStatus = allStatuses [ sessionID ]
2026-01-13 21:00:00 +09:00
if ( pollCount % 10 === 0 ) {
2026-01-16 17:34:40 +09:00
log ( "[delegate_task] Poll status" , {
2026-01-13 21:00:00 +09:00
sessionID ,
pollCount ,
elapsed : Math.floor ( ( Date . now ( ) - pollStart ) / 1000 ) + "s" ,
sessionStatus : sessionStatus?.type ? ? "not_in_status" ,
stablePolls ,
lastMsgCount ,
} )
}
2026-01-10 13:00:25 +08:00
if ( sessionStatus && sessionStatus . type !== "idle" ) {
stablePolls = 0
lastMsgCount = 0
continue
}
const elapsed = Date . now ( ) - pollStart
if ( elapsed < MIN_STABILITY_TIME_MS ) {
2026-01-13 21:00:00 +09:00
continue
2026-01-10 13:00:25 +08:00
}
const messagesCheck = await client . session . messages ( { path : { id : sessionID } } )
const msgs = ( ( messagesCheck as { data? : unknown } ) . data ? ? messagesCheck ) as Array < unknown >
const currentMsgCount = msgs . length
2026-01-13 21:00:00 +09:00
if ( currentMsgCount === lastMsgCount ) {
2026-01-10 13:00:25 +08:00
stablePolls ++
if ( stablePolls >= STABILITY_POLLS_REQUIRED ) {
2026-01-16 17:34:40 +09:00
log ( "[delegate_task] Poll complete - messages stable" , { sessionID , pollCount , currentMsgCount } )
2026-01-13 21:00:00 +09:00
break
2026-01-10 13:00:25 +08:00
}
} else {
stablePolls = 0
lastMsgCount = currentMsgCount
2026-01-09 02:24:43 +09:00
}
}
2026-01-13 21:00:00 +09:00
if ( Date . now ( ) - pollStart >= MAX_POLL_TIME_MS ) {
2026-01-16 17:34:40 +09:00
log ( "[delegate_task] Poll timeout reached" , { sessionID , pollCount , lastMsgCount , stablePolls } )
2026-01-13 21:00:00 +09:00
}
2026-01-09 02:24:43 +09:00
const messagesResult = await client . session . messages ( {
path : { id : sessionID } ,
} )
if ( messagesResult . error ) {
2026-01-17 20:40:35 +09:00
return ` Error fetching result: ${ messagesResult . error } \ n \ nSession ID: ${ sessionID } `
2026-01-09 02:24:43 +09:00
}
const messages = ( ( messagesResult as { data? : unknown } ) . data ? ? messagesResult ) as Array < {
info ? : { role? : string ; time ? : { created? : number } }
parts? : Array < { type ? : string ; text? : string } >
} >
const assistantMessages = messages
. filter ( ( m ) = > m . info ? . role === "assistant" )
. sort ( ( a , b ) = > ( b . info ? . time ? . created ? ? 0 ) - ( a . info ? . time ? . created ? ? 0 ) )
const lastMessage = assistantMessages [ 0 ]
if ( ! lastMessage ) {
2026-01-17 20:40:35 +09:00
return ` No assistant response found. \ n \ nSession ID: ${ sessionID } `
2026-01-09 02:24:43 +09:00
}
2026-01-10 13:00:25 +08:00
// Extract text from both "text" and "reasoning" parts (thinking models use "reasoning")
const textParts = lastMessage ? . parts ? . filter ( ( p ) = > p . type === "text" || p . type === "reasoning" ) ? ? [ ]
2026-01-09 02:24:43 +09:00
const textContent = textParts . map ( ( p ) = > p . text ? ? "" ) . filter ( Boolean ) . join ( "\n" )
const duration = formatDuration ( startTime )
if ( toastManager ) {
toastManager . removeTask ( taskId )
}
subagentSessions . delete ( sessionID )
return ` Task completed in ${ duration } .
Agent: ${ agentToUse } ${ args . category ? ` (category: ${ args . category } ) ` : "" }
Session ID: ${ sessionID }
---
${ textContent || "(No text output)" } `
} catch ( error ) {
if ( toastManager && taskId !== undefined ) {
toastManager . removeTask ( taskId )
}
if ( syncSessionID ) {
subagentSessions . delete ( syncSessionID )
}
2026-01-15 18:54:04 +09:00
return formatDetailedError ( error , {
operation : "Execute task" ,
args ,
sessionID : syncSessionID ,
agent : agentToUse ,
category : args.category ,
} )
2026-01-09 02:24:43 +09:00
}
} ,
} )
}