import type { ModelFallbackInfo } from "../../features/task-toast-manager/types" import type { DelegateTaskArgs, ToolContextWithMetadata, DelegatedModelConfig } from "./types" import type { ExecutorContext, ParentContext } from "./executor-types" import { getTaskToastManager } from "../../features/task-toast-manager" import { publishToolMetadata } from "../../features/tool-metadata-store" import { subagentSessions, syncSubagentSessions, setSessionAgent } from "../../features/claude-code-session-state" import { log } from "../../shared/logger" import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { formatDuration } from "./time-formatter" import { formatDetailedError } from "./error-formatting" import { syncTaskDeps, type SyncTaskDeps } from "./sync-task-deps" import { retrySyncPromptWithFallbacks } from "./sync-task-fallback" import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract" import { resolveMetadataModel } from "./resolve-metadata-model" export async function executeSyncTask( args: DelegateTaskArgs, ctx: ToolContextWithMetadata, executorCtx: ExecutorContext, parentContext: ParentContext, agentToUse: string, categoryModel: DelegatedModelConfig | undefined, systemContent: string | undefined, modelInfo?: ModelFallbackInfo, fallbackChain?: import("../../shared/model-requirements").FallbackEntry[], deps: SyncTaskDeps = syncTaskDeps ): Promise { const { manager, client, directory, onSyncSessionCreated, syncPollTimeoutMs } = executorCtx const toastManager = getTaskToastManager() let taskId: string | undefined let syncSessionID: string | undefined let spawnReservation: | Awaited> | undefined try { if (typeof manager?.reserveSubagentSpawn === "function") { spawnReservation = await manager.reserveSubagentSpawn(parentContext.sessionID) } // Depth guard. We must NOT silently fall back to childDepth: 1 // when the manager is unavailable or lacks the spawn methods, because that // would let subagents recurse without bound. The only safe fallback is // when the manager genuinely cannot enforce limits (legacy SDK), in which // case we still record childDepth: 1 but log a warning so regressions are // visible. let spawnContext: { rootSessionID: string; parentDepth: number; childDepth: number } if (spawnReservation?.spawnContext) { spawnContext = spawnReservation.spawnContext } else if (typeof manager?.assertCanSpawn === "function") { spawnContext = await manager.assertCanSpawn(parentContext.sessionID) } else { log( "[task] WARNING: BackgroundManager has no spawn enforcement methods (reserveSubagentSpawn / assertCanSpawn). " + "Depth limits cannot be enforced for this task. This indicates an old SDK or a misconfiguration.", { parentSessionID: parentContext.sessionID } ) spawnContext = { rootSessionID: parentContext.sessionID, parentDepth: 0, childDepth: 1, } } const createSessionResult = await deps.createSyncSession(client, { parentSessionID: parentContext.sessionID, agentToUse, description: args.description, defaultDirectory: directory, }) if (!createSessionResult.ok) { spawnReservation?.rollback() return createSessionResult.error } const sessionID = createSessionResult.sessionID spawnReservation?.commit() syncSessionID = sessionID subagentSessions.add(sessionID) syncSubagentSessions.add(sessionID) setSessionAgent(sessionID, agentToUse) executorCtx.modelFallbackControllerAccessor?.setSessionFallbackChain(sessionID, fallbackChain) if (args.category) { SessionCategoryRegistry.register(sessionID, args.category) } if (onSyncSessionCreated) { log("[task] Invoking onSyncSessionCreated callback", { sessionID, parentID: parentContext.sessionID }) try { await onSyncSessionCreated({ sessionID, parentID: parentContext.sessionID, title: args.description, }) } catch (error) { log("[task] onSyncSessionCreated callback failed", { error: String(error) }) } await new Promise(r => setTimeout(r, 200)) } taskId = `sync_${sessionID.slice(0, 8)}` const startTime = new Date() if (toastManager) { toastManager.addTask({ id: taskId, sessionID, description: args.description, agent: agentToUse, isBackground: false, category: args.category, skills: args.load_skills, modelInfo, }) } const syncTaskMeta = { title: args.description, metadata: { prompt: args.prompt, agent: agentToUse, category: args.category, ...(args.requested_subagent_type !== undefined ? { requested_subagent_type: args.requested_subagent_type } : {}), load_skills: args.load_skills, description: args.description, run_in_background: args.run_in_background, taskId: sessionID, sessionId: sessionID, sync: true, spawnDepth: spawnContext.childDepth, command: args.command, model: resolveMetadataModel(categoryModel, parentContext.model), }, } await publishToolMetadata(ctx, syncTaskMeta) const syncPromptInput = { sessionID, agentToUse, args, systemContent, toastManager, taskId, sisyphusAgentConfig: executorCtx.sisyphusAgentConfig, } let effectiveCategoryModel = categoryModel let promptError = await deps.sendSyncPrompt(client, { ...syncPromptInput, categoryModel: effectiveCategoryModel, }) if (promptError) { const promptResult = await retrySyncPromptWithFallbacks({ sessionID, initialError: promptError, categoryModel: effectiveCategoryModel, fallbackChain, sendPrompt: async (fallbackModel) => { return deps.sendSyncPrompt(client, { ...syncPromptInput, categoryModel: fallbackModel, }) }, }) promptError = promptResult.promptError effectiveCategoryModel = promptResult.categoryModel if (promptError) { return promptError } } try { const pollError = await deps.pollSyncSession(ctx, client, { sessionID, agentToUse, toastManager, taskId, }, syncPollTimeoutMs) if (pollError) { return pollError } const result = await deps.fetchSyncResult(client, sessionID) if (!result.ok) { return result.error } const duration = formatDuration(startTime) // 检测模型路由是否与父 session 不同,给用户可见的提示 const actualModelStr = effectiveCategoryModel ? `${effectiveCategoryModel.providerID}/${effectiveCategoryModel.modelID}` : undefined const parentModelStr = parentContext.model ? `${parentContext.model.providerID}/${parentContext.model.modelID}` : undefined let modelRoutingNote = "" if (actualModelStr && parentModelStr && actualModelStr !== parentModelStr) { modelRoutingNote = `\n⚠️ Model routing: parent used ${parentModelStr}, this subagent used ${actualModelStr} (via category: ${args.category ?? "unknown"})` } else if (actualModelStr) { modelRoutingNote = `\nModel: ${actualModelStr}${args.category ? ` (category: ${args.category})` : ""}` } return `Task completed in ${duration}. Agent: ${agentToUse}${args.category ? ` (category: ${args.category})` : ""}${modelRoutingNote} --- ${result.textContent || "(No text output)"} ${buildTaskMetadataBlock({ sessionId: sessionID, taskId: sessionID, agent: agentToUse, category: args.category, })}` } finally { if (toastManager && taskId !== undefined) { toastManager.removeTask(taskId) } } } catch (error) { spawnReservation?.rollback() return formatDetailedError(error, { operation: "Execute task", args, sessionID: syncSessionID, agent: agentToUse, category: args.category, }) } finally { if (syncSessionID) { subagentSessions.delete(syncSessionID) syncSubagentSessions.delete(syncSessionID) executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(syncSessionID) SessionCategoryRegistry.remove(syncSessionID) } } }