import type { BackgroundManager } from "../../features/background-agent" import type { CategoriesConfig, GitMasterConfig, BrowserAutomationProvider } from "../../config/schema" import type { ModelFallbackInfo } from "../../features/task-toast-manager/types" import type { DelegateTaskArgs, ToolContextWithMetadata, OpencodeClient } from "./types" import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS, isPlanAgent } from "./constants" import { getTimingConfig } from "./timing" import { parseModelString, getMessageDir, formatDuration, formatDetailedError } from "./helpers" import { resolveCategoryConfig } from "./categories" import { buildSystemContent } from "./prompt-builder" import { findNearestMessageWithFields, findFirstMessageWithAgent } from "../../features/hook-message-injector" import { resolveMultipleSkillsAsync } from "../../features/opencode-skill-loader/skill-content" import { discoverSkills } from "../../features/opencode-skill-loader" import { getTaskToastManager } from "../../features/task-toast-manager" import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state" import { log, getAgentToolRestrictions, resolveModelPipeline, promptWithModelSuggestionRetry } from "../../shared" import { fetchAvailableModels, isModelAvailable } from "../../shared/model-availability" import { readConnectedProvidersCache } from "../../shared/connected-providers-cache" import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements" const SISYPHUS_JUNIOR_AGENT = "sisyphus-junior" export interface ExecutorContext { manager: BackgroundManager client: OpencodeClient directory: string userCategories?: CategoriesConfig gitMasterConfig?: GitMasterConfig sisyphusJuniorModel?: string browserProvider?: BrowserAutomationProvider onSyncSessionCreated?: (event: { sessionID: string; parentID: string; title: string }) => Promise } export interface ParentContext { sessionID: string messageID: string agent?: string model?: { providerID: string; modelID: string; variant?: string } } interface SessionMessage { info?: { role?: string; time?: { created?: number }; agent?: string; model?: { providerID: string; modelID: string }; modelID?: string; providerID?: string } parts?: Array<{ type?: string; text?: string }> } export async function resolveSkillContent( skills: string[], options: { gitMasterConfig?: GitMasterConfig; browserProvider?: BrowserAutomationProvider } ): Promise<{ content: string | undefined; error: string | null }> { if (skills.length === 0) { return { content: undefined, error: null } } const { resolved, notFound } = await resolveMultipleSkillsAsync(skills, options) if (notFound.length > 0) { const allSkills = await discoverSkills({ includeClaudeCodePaths: true }) const available = allSkills.map(s => s.name).join(", ") return { content: undefined, error: `Skills not found: ${notFound.join(", ")}. Available: ${available}` } } return { content: Array.from(resolved.values()).join("\n\n"), error: null } } export function resolveParentContext(ctx: ToolContextWithMetadata): ParentContext { const messageDir = getMessageDir(ctx.sessionID) const prevMessage = messageDir ? findNearestMessageWithFields(messageDir) : null const firstMessageAgent = messageDir ? findFirstMessageWithAgent(messageDir) : null const sessionAgent = getSessionAgent(ctx.sessionID) const parentAgent = ctx.agent ?? sessionAgent ?? firstMessageAgent ?? prevMessage?.agent log("[delegate_task] parentAgent resolution", { sessionID: ctx.sessionID, messageDir, 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 return { sessionID: ctx.sessionID, messageID: ctx.messageID, agent: parentAgent, model: parentModel, } } export async function executeBackgroundContinuation( args: DelegateTaskArgs, ctx: ToolContextWithMetadata, executorCtx: ExecutorContext, parentContext: ParentContext ): Promise { const { manager } = executorCtx try { const task = await manager.resume({ sessionId: args.session_id!, prompt: args.prompt, parentSessionID: parentContext.sessionID, parentMessageID: parentContext.messageID, parentModel: parentContext.model, parentAgent: parentContext.agent, }) ctx.metadata?.({ title: `Continue: ${task.description}`, 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, command: args.command, }, }) return `Background task continued. Task ID: ${task.id} 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. session_id: ${task.sessionID} ` } catch (error) { return formatDetailedError(error, { operation: "Continue background task", args, sessionID: args.session_id, }) } } export async function executeSyncContinuation( args: DelegateTaskArgs, ctx: ToolContextWithMetadata, executorCtx: ExecutorContext ): Promise { const { client } = executorCtx const toastManager = getTaskToastManager() const taskId = `resume_sync_${args.session_id!.slice(0, 8)}` const startTime = new Date() if (toastManager) { toastManager.addTask({ id: taskId, description: args.description, agent: "continue", isBackground: false, }) } ctx.metadata?.({ title: `Continue: ${args.description}`, metadata: { prompt: args.prompt, load_skills: args.load_skills, description: args.description, run_in_background: args.run_in_background, sessionId: args.session_id, sync: true, command: args.command, }, }) try { let resumeAgent: string | undefined let resumeModel: { providerID: string; modelID: string } | undefined try { const messagesResp = await client.session.messages({ path: { id: args.session_id! } }) const messages = (messagesResp.data ?? []) as SessionMessage[] for (let i = messages.length - 1; i >= 0; i--) { const info = messages[i].info if (info?.agent || info?.model || (info?.modelID && info?.providerID)) { resumeAgent = info.agent resumeModel = info.model ?? (info.providerID && info.modelID ? { providerID: info.providerID, modelID: info.modelID } : undefined) break } } } catch { const resumeMessageDir = getMessageDir(args.session_id!) 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 } await client.session.prompt({ path: { id: args.session_id! }, body: { ...(resumeAgent !== undefined ? { agent: resumeAgent } : {}), ...(resumeModel !== undefined ? { model: resumeModel } : {}), tools: { ...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}), task: false, delegate_task: false, call_omo_agent: true, question: false, }, parts: [{ type: "text", text: args.prompt }], }, }) } catch (promptError) { if (toastManager) { toastManager.removeTask(taskId) } const errorMessage = promptError instanceof Error ? promptError.message : String(promptError) return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}` } const timing = getTimingConfig() const pollStart = Date.now() let lastMsgCount = 0 let stablePolls = 0 while (Date.now() - pollStart < 60000) { await new Promise(resolve => setTimeout(resolve, timing.POLL_INTERVAL_MS)) const elapsed = Date.now() - pollStart if (elapsed < timing.SESSION_CONTINUATION_STABILITY_MS) continue const messagesCheck = await client.session.messages({ path: { id: args.session_id! } }) const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array const currentMsgCount = msgs.length if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) { stablePolls++ if (stablePolls >= timing.STABILITY_POLLS_REQUIRED) break } else { stablePolls = 0 lastMsgCount = currentMsgCount } } const messagesResult = await client.session.messages({ path: { id: args.session_id! }, }) if (messagesResult.error) { if (toastManager) { toastManager.removeTask(taskId) } return `Error fetching result: ${messagesResult.error}\n\nSession ID: ${args.session_id}` } const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as SessionMessage[] 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) { return `No assistant response found.\n\nSession ID: ${args.session_id}` } 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) return `Task continued and completed in ${duration}. --- ${textContent || "(No text output)"} session_id: ${args.session_id} ` } export async function executeUnstableAgentTask( args: DelegateTaskArgs, ctx: ToolContextWithMetadata, executorCtx: ExecutorContext, parentContext: ParentContext, agentToUse: string, categoryModel: { providerID: string; modelID: string; variant?: string } | undefined, systemContent: string | undefined, actualModel: string | undefined ): Promise { const { manager, client } = executorCtx try { const task = await manager.launch({ description: args.description, prompt: args.prompt, agent: agentToUse, parentSessionID: parentContext.sessionID, parentMessageID: parentContext.messageID, parentModel: parentContext.model, parentAgent: parentContext.agent, model: categoryModel, skills: args.load_skills.length > 0 ? args.load_skills : undefined, skillContent: systemContent, category: args.category, }) 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)) } const sessionID = task.sessionID if (!sessionID) { return formatDetailedError(new Error(`Task failed to start within timeout (30s). Task ID: ${task.id}, Status: ${task.status}`), { operation: "Launch monitored background task", args, agent: agentToUse, category: args.category, }) } ctx.metadata?.({ title: args.description, 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, command: args.command, }, }) const startTime = new Date() const timingCfg = getTimingConfig() const pollStart = Date.now() let lastMsgCount = 0 let stablePolls = 0 while (Date.now() - pollStart < timingCfg.MAX_POLL_TIME_MS) { if (ctx.abort?.aborted) { return `Task aborted (was running in background mode).\n\nSession ID: ${sessionID}` } await new Promise(resolve => setTimeout(resolve, timingCfg.POLL_INTERVAL_MS)) const statusResult = await client.session.status() const allStatuses = (statusResult.data ?? {}) as Record const sessionStatus = allStatuses[sessionID] if (sessionStatus && sessionStatus.type !== "idle") { stablePolls = 0 lastMsgCount = 0 continue } if (Date.now() - pollStart < timingCfg.MIN_STABILITY_TIME_MS) continue const messagesCheck = await client.session.messages({ path: { id: sessionID } }) const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array const currentMsgCount = msgs.length if (currentMsgCount === lastMsgCount) { stablePolls++ if (stablePolls >= timingCfg.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 SessionMessage[] 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) { return `No assistant response found (task ran in background mode).\n\nSession ID: ${sessionID}` } 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) return `SUPERVISED TASK COMPLETED SUCCESSFULLY 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} Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""} 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 --- RESULT: ${textContent || "(No text output)"} session_id: ${sessionID} ` } catch (error) { return formatDetailedError(error, { operation: "Launch monitored background task", args, agent: agentToUse, category: args.category, }) } } export async function executeBackgroundTask( args: DelegateTaskArgs, ctx: ToolContextWithMetadata, executorCtx: ExecutorContext, parentContext: ParentContext, agentToUse: string, categoryModel: { providerID: string; modelID: string; variant?: string } | undefined, systemContent: string | undefined ): Promise { const { manager } = executorCtx try { const task = await manager.launch({ description: args.description, prompt: args.prompt, agent: agentToUse, parentSessionID: parentContext.sessionID, parentMessageID: parentContext.messageID, parentModel: parentContext.model, parentAgent: parentContext.agent, model: categoryModel, skills: args.load_skills.length > 0 ? args.load_skills : undefined, skillContent: systemContent, category: args.category, }) ctx.metadata?.({ title: args.description, 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, command: args.command, }, }) return `Background task launched. Task ID: ${task.id} 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. session_id: ${task.sessionID} ` } catch (error) { return formatDetailedError(error, { operation: "Launch background task", args, agent: agentToUse, category: args.category, }) } } export async function executeSyncTask( args: DelegateTaskArgs, ctx: ToolContextWithMetadata, executorCtx: ExecutorContext, parentContext: ParentContext, agentToUse: string, categoryModel: { providerID: string; modelID: string; variant?: string } | undefined, systemContent: string | undefined, modelInfo?: ModelFallbackInfo ): Promise { const { client, directory, onSyncSessionCreated } = executorCtx const toastManager = getTaskToastManager() let taskId: string | undefined let syncSessionID: string | undefined try { const parentSession = client.session.get ? await client.session.get({ path: { id: parentContext.sessionID } }).catch(() => null) : null const parentDirectory = parentSession?.data?.directory ?? directory const createResult = await client.session.create({ body: { parentID: parentContext.sessionID, title: `${args.description} (@${agentToUse} subagent)`, permission: [ { permission: "question", action: "deny" as const, pattern: "*" }, ], } as any, query: { directory: parentDirectory, }, }) if (createResult.error) { return `Failed to create session: ${createResult.error}` } const sessionID = createResult.data.id syncSessionID = sessionID subagentSessions.add(sessionID) if (onSyncSessionCreated) { log("[delegate_task] Invoking onSyncSessionCreated callback", { sessionID, parentID: parentContext.sessionID }) await onSyncSessionCreated({ sessionID, parentID: parentContext.sessionID, title: args.description, }).catch((err) => { log("[delegate_task] onSyncSessionCreated callback failed", { error: String(err) }) }) await new Promise(r => setTimeout(r, 200)) } taskId = `sync_${sessionID.slice(0, 8)}` const startTime = new Date() if (toastManager) { toastManager.addTask({ id: taskId, description: args.description, agent: agentToUse, isBackground: false, category: args.category, skills: args.load_skills, modelInfo, }) } ctx.metadata?.({ title: args.description, 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, command: args.command, }, }) try { const allowDelegateTask = isPlanAgent(agentToUse) await promptWithModelSuggestionRetry(client, { path: { id: sessionID }, body: { agent: agentToUse, system: systemContent, tools: { task: false, delegate_task: allowDelegateTask, call_omo_agent: true, question: false, }, parts: [{ type: "text", text: args.prompt }], ...(categoryModel ? { model: { providerID: categoryModel.providerID, modelID: categoryModel.modelID } } : {}), ...(categoryModel?.variant ? { variant: categoryModel.variant } : {}), }, }) } catch (promptError) { if (toastManager && taskId !== undefined) { toastManager.removeTask(taskId) } const errorMessage = promptError instanceof Error ? promptError.message : String(promptError) if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) { 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, }) } return formatDetailedError(promptError, { operation: "Send prompt", args, sessionID, agent: agentToUse, category: args.category, }) } const syncTiming = getTimingConfig() const pollStart = Date.now() let lastMsgCount = 0 let stablePolls = 0 let pollCount = 0 log("[delegate_task] Starting poll loop", { sessionID, agentToUse }) while (Date.now() - pollStart < syncTiming.MAX_POLL_TIME_MS) { if (ctx.abort?.aborted) { log("[delegate_task] Aborted by user", { sessionID }) if (toastManager && taskId) toastManager.removeTask(taskId) return `Task aborted.\n\nSession ID: ${sessionID}` } await new Promise(resolve => setTimeout(resolve, syncTiming.POLL_INTERVAL_MS)) pollCount++ const statusResult = await client.session.status() const allStatuses = (statusResult.data ?? {}) as Record const sessionStatus = allStatuses[sessionID] if (pollCount % 10 === 0) { log("[delegate_task] Poll status", { sessionID, pollCount, elapsed: Math.floor((Date.now() - pollStart) / 1000) + "s", sessionStatus: sessionStatus?.type ?? "not_in_status", stablePolls, lastMsgCount, }) } if (sessionStatus && sessionStatus.type !== "idle") { stablePolls = 0 lastMsgCount = 0 continue } const elapsed = Date.now() - pollStart if (elapsed < syncTiming.MIN_STABILITY_TIME_MS) { continue } const messagesCheck = await client.session.messages({ path: { id: sessionID } }) const msgs = ((messagesCheck as { data?: unknown }).data ?? messagesCheck) as Array const currentMsgCount = msgs.length if (currentMsgCount === lastMsgCount) { stablePolls++ if (stablePolls >= syncTiming.STABILITY_POLLS_REQUIRED) { log("[delegate_task] Poll complete - messages stable", { sessionID, pollCount, currentMsgCount }) break } } else { stablePolls = 0 lastMsgCount = currentMsgCount } } if (Date.now() - pollStart >= syncTiming.MAX_POLL_TIME_MS) { log("[delegate_task] Poll timeout reached", { sessionID, pollCount, lastMsgCount, stablePolls }) } const messagesResult = await client.session.messages({ path: { id: sessionID }, }) if (messagesResult.error) { return `Error fetching result: ${messagesResult.error}\n\nSession ID: ${sessionID}` } const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as SessionMessage[] 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) { return `No assistant response found.\n\nSession ID: ${sessionID}` } 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) if (toastManager) { toastManager.removeTask(taskId) } subagentSessions.delete(sessionID) return `Task completed in ${duration}. Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""} --- ${textContent || "(No text output)"} session_id: ${sessionID} ` } catch (error) { if (toastManager && taskId !== undefined) { toastManager.removeTask(taskId) } if (syncSessionID) { subagentSessions.delete(syncSessionID) } return formatDetailedError(error, { operation: "Execute task", args, sessionID: syncSessionID, agent: agentToUse, category: args.category, }) } } export interface CategoryResolutionResult { agentToUse: string categoryModel: { providerID: string; modelID: string; variant?: string } | undefined categoryPromptAppend: string | undefined modelInfo: ModelFallbackInfo | undefined actualModel: string | undefined isUnstableAgent: boolean error?: string } export async function resolveCategoryExecution( args: DelegateTaskArgs, executorCtx: ExecutorContext, inheritedModel: string | undefined, systemDefaultModel: string | undefined ): Promise { const { client, userCategories, sisyphusJuniorModel } = executorCtx const connectedProviders = readConnectedProvidersCache() const availableModels = await fetchAvailableModels(client, { connectedProviders: connectedProviders ?? undefined, }) const resolved = resolveCategoryConfig(args.category!, { userCategories, inheritedModel, systemDefaultModel, availableModels, }) if (!resolved) { return { agentToUse: "", categoryModel: undefined, categoryPromptAppend: undefined, modelInfo: undefined, actualModel: undefined, isUnstableAgent: false, error: `Unknown category: "${args.category}". Available: ${Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories }).join(", ")}`, } } const requirement = CATEGORY_MODEL_REQUIREMENTS[args.category!] let actualModel: string | undefined let modelInfo: ModelFallbackInfo | undefined let categoryModel: { providerID: string; modelID: string; variant?: string } | undefined if (!requirement) { actualModel = resolved.model if (actualModel) { modelInfo = { model: actualModel, type: "system-default", source: "system-default" } } } else { const resolution = resolveModelPipeline({ intent: { userModel: userCategories?.[args.category!]?.model, categoryDefaultModel: resolved.model ?? sisyphusJuniorModel, }, constraints: { availableModels }, policy: { fallbackChain: requirement.fallbackChain, systemDefaultModel, }, }) if (resolution) { const { model: resolvedModel, provenance, variant: resolvedVariant } = resolution actualModel = resolvedModel if (!parseModelString(actualModel)) { return { agentToUse: "", categoryModel: undefined, categoryPromptAppend: undefined, modelInfo: undefined, actualModel: undefined, isUnstableAgent: false, error: `Invalid model format "${actualModel}". Expected "provider/model" format (e.g., "anthropic/claude-sonnet-4-5").`, } } let type: "user-defined" | "inherited" | "category-default" | "system-default" const source = provenance switch (provenance) { case "override": type = "user-defined" break case "category-default": case "provider-fallback": type = "category-default" break case "system-default": type = "system-default" break } modelInfo = { model: actualModel, type, source } const parsedModel = parseModelString(actualModel) const variantToUse = userCategories?.[args.category!]?.variant ?? resolvedVariant ?? resolved.config.variant categoryModel = parsedModel ? (variantToUse ? { ...parsedModel, variant: variantToUse } : parsedModel) : undefined } } if (!categoryModel && actualModel) { const parsedModel = parseModelString(actualModel) categoryModel = parsedModel ?? undefined } const categoryPromptAppend = resolved.promptAppend || undefined if (!categoryModel && !actualModel) { const categoryNames = Object.keys({ ...DEFAULT_CATEGORIES, ...userCategories }) return { agentToUse: "", categoryModel: undefined, categoryPromptAppend: undefined, modelInfo: undefined, actualModel: undefined, isUnstableAgent: false, error: `Model not configured for category "${args.category}". Configure in one of: 1. OpenCode: Set "model" in opencode.json 2. Oh-My-OpenCode: Set category model in oh-my-opencode.json 3. Provider: Connect a provider with available models Current category: ${args.category} Available categories: ${categoryNames.join(", ")}`, } } const unstableModel = actualModel?.toLowerCase() const isUnstableAgent = resolved.config.is_unstable_agent === true || (unstableModel ? unstableModel.includes("gemini") || unstableModel.includes("minimax") : false) return { agentToUse: SISYPHUS_JUNIOR_AGENT, categoryModel, categoryPromptAppend, modelInfo, actualModel, isUnstableAgent, } } export async function resolveSubagentExecution( args: DelegateTaskArgs, executorCtx: ExecutorContext, parentAgent: string | undefined, categoryExamples: string ): Promise<{ agentToUse: string; categoryModel: { providerID: string; modelID: string } | undefined; error?: string }> { const { client } = executorCtx if (!args.subagent_type?.trim()) { return { agentToUse: "", categoryModel: undefined, error: `Agent name cannot be empty.` } } const agentName = args.subagent_type.trim() if (agentName.toLowerCase() === SISYPHUS_JUNIOR_AGENT.toLowerCase()) { return { agentToUse: "", categoryModel: undefined, error: `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.`, } } if (isPlanAgent(agentName) && isPlanAgent(parentAgent)) { return { agentToUse: "", categoryModel: undefined, error: `You are prometheus. You cannot delegate to prometheus via delegate_task. Create the work plan directly - that's your job as the planning agent.`, } } let agentToUse = agentName let categoryModel: { providerID: string; modelID: string } | undefined try { const agentsResult = await client.app.agents() type AgentInfo = { name: string; mode?: "subagent" | "primary" | "all"; model?: { providerID: string; modelID: string } } const agents = (agentsResult as { data?: AgentInfo[] }).data ?? agentsResult as unknown as AgentInfo[] const callableAgents = agents.filter((a) => a.mode !== "primary") const matchedAgent = callableAgents.find( (agent) => agent.name.toLowerCase() === agentToUse.toLowerCase() ) if (!matchedAgent) { const isPrimaryAgent = agents .filter((a) => a.mode === "primary") .find((agent) => agent.name.toLowerCase() === agentToUse.toLowerCase()) if (isPrimaryAgent) { return { agentToUse: "", categoryModel: undefined, error: `Cannot call primary agent "${isPrimaryAgent.name}" via delegate_task. Primary agents are top-level orchestrators.`, } } const availableAgents = callableAgents .map((a) => a.name) .sort() .join(", ") return { agentToUse: "", categoryModel: undefined, error: `Unknown agent: "${agentToUse}". Available agents: ${availableAgents}`, } } agentToUse = matchedAgent.name if (matchedAgent.model) { categoryModel = matchedAgent.model } } catch { // Proceed anyway - session.prompt will fail with clearer error if agent doesn't exist } return { agentToUse, categoryModel } }