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-22 22:46:23 +09:00
import { DEFAULT_CATEGORIES , CATEGORY_PROMPT_APPENDS , CATEGORY_DESCRIPTIONS } 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-23 00:55:01 +09:00
import { log , getAgentToolRestrictions , resolveModel , getOpenCodeConfigPaths , findByNameCaseInsensitive , equalsIgnoreCase } from "../../shared"
2026-01-22 22:46:23 +09:00
import { fetchAvailableModels } from "../../shared/model-availability"
import { resolveModelWithFallback } from "../../shared/model-resolver"
import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
2026-01-09 02:24:43 +09:00
type OpencodeClient = PluginInput [ "client" ]
const SISYPHUS_JUNIOR_AGENT = "Sisyphus-Junior"
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 } ` )
2026-01-22 22:46:23 +09:00
lines . push ( ` - load_skills: [ ${ ctx . args . load_skills ? . join ( ", " ) ? ? "" } ] ` )
2026-01-15 18:54:04 +09:00
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-20 17:05:17 +09:00
// Model priority for categories: user override > category default > system default
// Categories have explicit models - no inheritance from parent session
2026-01-17 12:51:03 -05:00
const model = resolveModel ( {
userModel : userConfig?.model ,
2026-01-20 17:05:17 +09:00
inheritedModel : defaultConfig?.model , // Category's built-in model takes precedence over system default
systemDefault : systemDefaultModel ,
2026-01-17 12:51:03 -05:00
} )
2026-01-09 02:24:43 +09:00
const config : CategoryConfig = {
. . . defaultConfig ,
. . . userConfig ,
2026-01-14 14:45:01 -05:00
model ,
2026-01-20 16:22:53 +09:00
variant : userConfig?.variant ? ? defaultConfig ? . variant ,
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
2026-01-22 22:46:23 +09:00
const allCategories = { . . . DEFAULT_CATEGORIES , . . . userCategories }
const categoryNames = Object . keys ( allCategories )
const categoryExamples = categoryNames . map ( k = > ` ' ${ k } ' ` ) . join ( ", " )
const categoryList = categoryNames . map ( name = > {
const userDesc = userCategories ? . [ name ] ? . description
const builtinDesc = CATEGORY_DESCRIPTIONS [ name ]
const desc = userDesc || builtinDesc
return desc ? ` - ${ name } : ${ desc } ` : ` - ${ name } `
} ) . join ( "\n" )
const description = ` Spawn agent task with category-based or direct agent selection.
MUTUALLY EXCLUSIVE: Provide EITHER category OR subagent_type, not both (unless resuming).
- load_skills: ALWAYS REQUIRED. Pass at least one skill name (e.g., ["playwright"], ["git-master", "frontend-ui-ux"]).
- category: Use predefined category → Spawns Sisyphus-Junior with category config
Available categories:
${ categoryList }
- subagent_type: Use specific agent directly (e.g., "oracle", "explore")
- run_in_background: true=async (returns task_id), false=sync (waits for result). Default: false. Use background=true ONLY for parallel exploration with 5+ independent queries.
- resume: Session ID to resume (from previous task output). Continues agent with FULL CONTEXT PRESERVED - saves tokens, maintains continuity.
**WHEN TO USE resume:**
- Task failed/incomplete → resume with "fix: [specific issue]"
- Need follow-up on previous result → resume with additional question
- Multi-turn conversation with same agent → always resume instead of new task
Prompts MUST be in English. `
2026-01-09 02:24:43 +09:00
return tool ( {
2026-01-22 22:46:23 +09:00
description ,
2026-01-09 02:24:43 +09:00
args : {
2026-01-22 22:46:23 +09:00
load_skills : tool.schema.array ( tool . schema . string ( ) ) . describe ( "Skill names to inject. REQUIRED - pass [] if no skills needed, but IT IS HIGHLY RECOMMENDED to pass proper skills like [\"playwright\"], [\"git-master\"] for best results." ) ,
description : tool.schema.string ( ) . describe ( "Short task description (3-5 words)" ) ,
2026-01-09 02:24:43 +09:00
prompt : tool.schema.string ( ) . describe ( "Full detailed prompt for the agent" ) ,
2026-01-22 22:46:23 +09:00
run_in_background : tool.schema.boolean ( ) . describe ( "true=async (returns task_id), false=sync (waits). Default: false" ) ,
category : tool.schema.string ( ) . optional ( ) . describe ( ` Category (e.g., ${ categoryExamples } ). Mutually exclusive with subagent_type. ` ) ,
subagent_type : tool.schema.string ( ) . optional ( ) . describe ( "Agent name (e.g., 'oracle', 'explore'). Mutually exclusive with category." ) ,
resume : tool.schema.string ( ) . optional ( ) . describe ( "Session ID to resume" ) ,
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-22 22:46:23 +09:00
throw new Error ( ` 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
}
2026-01-22 22:46:23 +09:00
if ( args . load_skills === undefined ) {
throw new Error ( ` Invalid arguments: 'load_skills' parameter is REQUIRED. Pass [] if no skills needed, but IT IS HIGHLY RECOMMENDED to pass proper skills like ["playwright"], ["git-master"] for best results. ` )
2026-01-16 13:12:48 +09:00
}
2026-01-22 22:46:23 +09:00
if ( args . load_skills === null ) {
throw new Error ( ` Invalid arguments: load_skills=null is not allowed. Pass [] if no skills needed, but IT IS HIGHLY RECOMMENDED to pass proper skills. ` )
2026-01-09 02:24:43 +09:00
}
const runInBackground = args . run_in_background === true
let skillContent : string | undefined
2026-01-22 22:46:23 +09:00
if ( args . load_skills . length > 0 ) {
const { resolved , notFound } = await resolveMultipleSkillsAsync ( args . load_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-22 22:46:23 +09:00
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 } ` ,
2026-01-22 22:46:23 +09:00
metadata : {
prompt : args.prompt ,
agent : task.agent ,
load_skills : args.load_skills ,
description : args.description ,
run_in_background : args.run_in_background ,
sessionId : task.sessionID ,
} ,
2026-01-09 02:24:43 +09:00
} )
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 } ` ,
2026-01-22 22:46:23 +09:00
metadata : {
prompt : args.prompt ,
load_skills : args.load_skills ,
description : args.description ,
run_in_background : args.run_in_background ,
sessionId : args.resume ,
sync : true ,
} ,
2026-01-09 02:24:43 +09:00
} )
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 ) )
2026-01-22 22:46:23 +09:00
2026-01-10 13:00:25 +08:00
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 }
---
2026-01-22 22:46:23 +09:00
${ textContent || "(No text output)" }
---
To resume this session: resume=" ${ args . resume } " `
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: 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-22 22:46:23 +09:00
// Fetch OpenCode config at boundary to get system default model
let systemDefaultModel : string | undefined
try {
const openCodeConfig = await client . config . get ( )
systemDefaultModel = ( openCodeConfig as { data ? : { model? : string } } ) ? . data ? . model
} catch {
// Config fetch failed, proceed without system default
systemDefaultModel = undefined
}
let agentToUse : string
let categoryModel : { providerID : string ; modelID : string ; variant? : string } | undefined
let categoryPromptAppend : string | undefined
const inheritedModel = parentModel
? ` ${ parentModel . providerID } / ${ parentModel . modelID } `
: undefined
let modelInfo : ModelFallbackInfo | undefined
if ( args . category ) {
// Guard: require system default model for category delegation
if ( ! systemDefaultModel ) {
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)'
)
}
const availableModels = await fetchAvailableModels ( client )
const resolved = resolveCategoryConfig ( args . category , {
userCategories ,
inheritedModel ,
systemDefaultModel ,
} )
if ( ! resolved ) {
return ` Unknown category: " ${ args . category } ". Available: ${ Object . keys ( { . . . DEFAULT_CATEGORIES , . . . userCategories } ).join(", ")} `
}
const requirement = CATEGORY_MODEL_REQUIREMENTS [ args . category ]
let actualModel : string
if ( ! requirement ) {
actualModel = resolved . model
modelInfo = { model : actualModel , type : "system-default" , source : "system-default" }
2026-01-23 02:20:32 +09:00
} else {
const { model : resolvedModel , source , variant : resolvedVariant } = resolveModelWithFallback ( {
2026-01-22 22:46:23 +09:00
userModel : userCategories?. [ args . category ] ? . model ,
fallbackChain : requirement.fallbackChain ,
availableModels ,
systemDefaultModel ,
} )
2026-01-14 14:45:01 -05:00
2026-01-22 22:46:23 +09:00
actualModel = resolvedModel
2026-01-09 02:24:43 +09:00
2026-01-22 22:46:23 +09:00
if ( ! parseModelString ( actualModel ) ) {
return ` Invalid model format " ${ actualModel } ". Expected "provider/model" format (e.g., "anthropic/claude-sonnet-4-5"). `
}
2026-01-13 08:58:47 +07:00
2026-01-22 22:46:23 +09:00
let type : "user-defined" | "inherited" | "category-default" | "system-default"
switch ( source ) {
case "override" :
type = "user-defined"
break
case "provider-fallback" :
type = "category-default"
break
case "system-default" :
type = "system-default"
break
}
2026-01-17 12:51:03 -05:00
2026-01-22 22:46:23 +09:00
modelInfo = { model : actualModel , type , source }
2026-01-23 02:20:32 +09:00
const parsedModel = parseModelString ( actualModel )
const variantToUse = userCategories ? . [ args . category ] ? . variant ? ? resolvedVariant
categoryModel = parsedModel
? ( variantToUse ? { . . . parsedModel , variant : variantToUse } : parsedModel )
: undefined
2026-01-22 22:46:23 +09:00
}
2026-01-09 02:24:43 +09:00
2026-01-22 22:46:23 +09:00
agentToUse = SISYPHUS_JUNIOR_AGENT
2026-01-23 02:20:32 +09:00
if ( ! categoryModel ) {
const parsedModel = parseModelString ( actualModel )
categoryModel = parsedModel ? ? undefined
}
2026-01-22 22:46:23 +09:00
categoryPromptAppend = resolved . promptAppend || undefined
2026-01-14 14:45:01 -05:00
2026-01-22 22:46:23 +09:00
const isUnstableAgent = resolved . config . is_unstable_agent === true || actualModel . toLowerCase ( ) . includes ( "gemini" )
// Handle both boolean false and string "false" due to potential serialization
const isRunInBackgroundExplicitlyFalse = args . run_in_background === false || args . run_in_background === "false" as unknown as boolean
2026-01-14 14:45:01 -05:00
2026-01-22 22:46:23 +09:00
log ( "[delegate_task] unstable agent detection" , {
category : args.category ,
actualModel ,
isUnstableAgent ,
run_in_background_value : args.run_in_background ,
run_in_background_type : typeof args . run_in_background ,
isRunInBackgroundExplicitlyFalse ,
willForceBackground : isUnstableAgent && isRunInBackgroundExplicitlyFalse ,
} )
2026-01-13 08:58:47 +07:00
2026-01-22 22:46:23 +09:00
if ( isUnstableAgent && isRunInBackgroundExplicitlyFalse ) {
2026-01-20 18:48:13 +09:00
const systemContent = buildSystemContent ( { skillContent , categoryPromptAppend } )
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-22 22:46:23 +09:00
skills : args.load_skills.length > 0 ? args.load_skills : undefined ,
2026-01-20 18:48:13 +09:00
skillContent : systemContent ,
} )
2026-01-22 22:46:23 +09:00
// Wait for sessionID to be set (task transitions from pending to running)
// launch() returns immediately with status="pending", sessionID is set async in startTask()
const WAIT_FOR_SESSION_INTERVAL_MS = 100
const WAIT_FOR_SESSION_TIMEOUT_MS = 30000
const waitStart = Date . now ( )
while ( ! task . sessionID && Date . now ( ) - waitStart < WAIT_FOR_SESSION_TIMEOUT_MS ) {
if ( ctx . abort ? . aborted ) {
return ` Task aborted while waiting for session to start. \ n \ nTask ID: ${ task . id } `
}
await new Promise ( resolve = > setTimeout ( resolve , WAIT_FOR_SESSION_INTERVAL_MS ) )
}
2026-01-21 00:06:05 +09:00
const sessionID = task . sessionID
if ( ! sessionID ) {
2026-01-22 22:46:23 +09:00
return formatDetailedError ( new Error ( ` Task failed to start within timeout (30s). Task ID: ${ task . id } , Status: ${ task . status } ` ) , {
operation : "Launch monitored background task" ,
2026-01-21 00:06:05 +09:00
args ,
agent : agentToUse ,
category : args.category ,
} )
}
2026-01-20 18:48:13 +09:00
ctx . metadata ? . ( {
title : args.description ,
2026-01-22 22:46:23 +09:00
metadata : {
prompt : args.prompt ,
agent : agentToUse ,
category : args.category ,
load_skills : args.load_skills ,
description : args.description ,
run_in_background : args.run_in_background ,
sessionId : sessionID ,
} ,
2026-01-20 18:48:13 +09:00
} )
2026-01-21 00:06:05 +09:00
const startTime = new Date ( )
2026-01-20 18:48:13 +09:00
2026-01-21 00:06:05 +09:00
// Poll for completion (same logic as sync mode)
const POLL_INTERVAL_MS = 500
const MAX_POLL_TIME_MS = 10 * 60 * 1000
const MIN_STABILITY_TIME_MS = 10000
const STABILITY_POLLS_REQUIRED = 3
const pollStart = Date . now ( )
let lastMsgCount = 0
let stablePolls = 0
2026-01-20 18:48:13 +09:00
2026-01-21 00:06:05 +09:00
while ( Date . now ( ) - pollStart < MAX_POLL_TIME_MS ) {
if ( ctx . abort ? . aborted ) {
2026-01-22 22:46:23 +09:00
return ` Task aborted (was running in background mode). \ n \ nSession ID: ${ sessionID } `
2026-01-21 00:06:05 +09:00
}
await new Promise ( resolve = > setTimeout ( resolve , POLL_INTERVAL_MS ) )
const statusResult = await client . session . status ( )
const allStatuses = ( statusResult . data ? ? { } ) as Record < string , { type : string } >
const sessionStatus = allStatuses [ sessionID ]
2026-01-20 18:48:13 +09:00
2026-01-21 00:06:05 +09:00
if ( sessionStatus && sessionStatus . type !== "idle" ) {
stablePolls = 0
lastMsgCount = 0
continue
}
if ( Date . now ( ) - pollStart < MIN_STABILITY_TIME_MS ) continue
const messagesCheck = await client . session . messages ( { path : { id : sessionID } } )
const msgs = ( ( messagesCheck as { data? : unknown } ) . data ? ? messagesCheck ) as Array < unknown >
const currentMsgCount = msgs . length
if ( currentMsgCount === lastMsgCount ) {
stablePolls ++
if ( stablePolls >= STABILITY_POLLS_REQUIRED ) break
} else {
stablePolls = 0
lastMsgCount = currentMsgCount
}
}
const messagesResult = await client . session . messages ( { path : { id : sessionID } } )
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-22 22:46:23 +09:00
return ` No assistant response found (task ran in background mode). \ n \ nSession ID: ${ sessionID } `
2026-01-21 00:06:05 +09:00
}
const textParts = lastMessage ? . parts ? . filter ( ( p ) = > p . type === "text" || p . type === "reasoning" ) ? ? [ ]
const textContent = textParts . map ( ( p ) = > p . text ? ? "" ) . filter ( Boolean ) . join ( "\n" )
const duration = formatDuration ( startTime )
2026-01-22 22:46:23 +09:00
return ` SUPERVISED TASK COMPLETED SUCCESSFULLY
2026-01-21 00:06:05 +09:00
2026-01-22 22:46:23 +09:00
IMPORTANT: This model ( ${ actualModel } ) is marked as unstable/experimental.
Your run_in_background=false was automatically converted to background mode for reliability monitoring.
Duration: ${ duration }
2026-01-21 00:06:05 +09:00
Agent: ${ agentToUse } ${ args . category ? ` (category: ${ args . category } ) ` : "" }
Session ID: ${ sessionID }
2026-01-22 22:46:23 +09:00
MONITORING INSTRUCTIONS:
- The task was monitored and completed successfully
- If you observe this agent behaving erratically in future calls, actively monitor its progress
- Use background_cancel(task_id="...") to abort if the agent seems stuck or producing garbage output
- Do NOT retry automatically if you see this message - the task already succeeded
2026-01-21 00:06:05 +09:00
---
2026-01-22 22:46:23 +09:00
RESULT:
${ textContent || "(No text output)" }
---
To resume this session: resume=" ${ sessionID } " `
2026-01-20 18:48:13 +09:00
} catch ( error ) {
return formatDetailedError ( error , {
2026-01-22 22:46:23 +09:00
operation : "Launch monitored background task" ,
2026-01-20 18:48:13 +09:00
args ,
agent : agentToUse ,
category : args.category ,
} )
}
}
2026-01-09 02:24:43 +09:00
} 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 ( )
2026-01-22 22:46:23 +09:00
2026-01-23 00:55:01 +09:00
if ( equalsIgnoreCase ( agentName , SISYPHUS_JUNIOR_AGENT ) ) {
2026-01-22 22:46:23 +09:00
return ` Cannot use subagent_type=" ${ SISYPHUS_JUNIOR_AGENT } " directly. Use category parameter instead (e.g., ${ categoryExamples } ).
Sisyphus-Junior is spawned automatically when you specify a category. Pick the appropriate category for your task domain. `
}
2026-01-14 14:45:01 -05:00
agentToUse = agentName
2026-01-09 02:24:43 +09:00
// Validate agent exists and is callable (not a primary agent)
2026-01-23 00:55:01 +09:00
// Uses case-insensitive matching to allow "Oracle", "oracle", "ORACLE" etc.
2026-01-09 02:24:43 +09:00
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" )
2026-01-23 00:55:01 +09:00
const matchedAgent = findByNameCaseInsensitive ( callableAgents , agentToUse )
if ( ! matchedAgent ) {
const isPrimaryAgent = findByNameCaseInsensitive (
agents . filter ( ( a ) = > a . mode === "primary" ) ,
agentToUse
)
2026-01-09 02:24:43 +09:00
if ( isPrimaryAgent ) {
2026-01-23 00:55:01 +09:00
return ` Cannot call primary agent " ${ isPrimaryAgent . name } " via delegate_task. Primary agents are top-level orchestrators. `
2026-01-09 02:24:43 +09:00
}
2026-01-23 00:55:01 +09:00
const availableAgents = callableAgents
. map ( ( a ) = > a . name )
2026-01-09 02:24:43 +09:00
. 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
}
2026-01-23 00:55:01 +09:00
// Use the canonical agent name from registration
agentToUse = matchedAgent . name
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-22 22:46:23 +09:00
skills : args.load_skills.length > 0 ? args.load_skills : undefined ,
2026-01-09 02:24:43 +09:00
skillContent : systemContent ,
} )
ctx . metadata ? . ( {
title : args.description ,
2026-01-22 22:46:23 +09:00
metadata : {
prompt : args.prompt ,
agent : task.agent ,
category : args.category ,
load_skills : args.load_skills ,
description : args.description ,
run_in_background : args.run_in_background ,
sessionId : task.sessionID ,
} ,
2026-01-09 02:24:43 +09:00
} )
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 }
2026-01-22 22:46:23 +09:00
System notifies on completion. Use \` background_output \` with task_id=" ${ task . id } " to check.
To resume this session: resume=" ${ task . sessionID } " `
2026-01-09 02:24:43 +09:00
} 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-22 22:46:23 +09:00
category : args.category ,
skills : args.load_skills ,
2026-01-13 08:58:47 +07:00
modelInfo ,
2026-01-09 02:24:43 +09:00
} )
}
ctx . metadata ? . ( {
title : args.description ,
2026-01-22 22:46:23 +09:00
metadata : {
prompt : args.prompt ,
agent : agentToUse ,
category : args.category ,
load_skills : args.load_skills ,
description : args.description ,
run_in_background : args.run_in_background ,
sessionId : sessionID ,
sync : true ,
} ,
2026-01-09 02:24:43 +09:00
} )
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 ]
2026-01-22 22:46:23 +09:00
2026-01-09 02:24:43 +09:00
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-22 22:46:23 +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 }
---
2026-01-22 22:46:23 +09:00
${ textContent || "(No text output)" }
---
To resume this session: resume=" ${ sessionID } " `
2026-01-09 02:24:43 +09:00
} 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
}
} ,
} )
}