/ tools / AgentTool / resumeAgent.ts
resumeAgent.ts
  1  import { promises as fsp } from 'fs'
  2  import { getSdkAgentProgressSummariesEnabled } from '../../bootstrap/state.js'
  3  import { getSystemPrompt } from '../../constants/prompts.js'
  4  import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'
  5  import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
  6  import type { ToolUseContext } from '../../Tool.js'
  7  import { registerAsyncAgent } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
  8  import { assembleToolPool } from '../../tools.js'
  9  import { asAgentId } from '../../types/ids.js'
 10  import { runWithAgentContext } from '../../utils/agentContext.js'
 11  import { runWithCwdOverride } from '../../utils/cwd.js'
 12  import { logForDebugging } from '../../utils/debug.js'
 13  import {
 14    createUserMessage,
 15    filterOrphanedThinkingOnlyMessages,
 16    filterUnresolvedToolUses,
 17    filterWhitespaceOnlyAssistantMessages,
 18  } from '../../utils/messages.js'
 19  import { getAgentModel } from '../../utils/model/agent.js'
 20  import { getQuerySourceForAgent } from '../../utils/promptCategory.js'
 21  import {
 22    getAgentTranscript,
 23    readAgentMetadata,
 24  } from '../../utils/sessionStorage.js'
 25  import { buildEffectiveSystemPrompt } from '../../utils/systemPrompt.js'
 26  import type { SystemPrompt } from '../../utils/systemPromptType.js'
 27  import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
 28  import { getParentSessionId } from '../../utils/teammate.js'
 29  import { reconstructForSubagentResume } from '../../utils/toolResultStorage.js'
 30  import { runAsyncAgentLifecycle } from './agentToolUtils.js'
 31  import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js'
 32  import { FORK_AGENT, isForkSubagentEnabled } from './forkSubagent.js'
 33  import type { AgentDefinition } from './loadAgentsDir.js'
 34  import { isBuiltInAgent } from './loadAgentsDir.js'
 35  import { runAgent } from './runAgent.js'
 36  
 37  export type ResumeAgentResult = {
 38    agentId: string
 39    description: string
 40    outputFile: string
 41  }
 42  export async function resumeAgentBackground({
 43    agentId,
 44    prompt,
 45    toolUseContext,
 46    canUseTool,
 47    invokingRequestId,
 48  }: {
 49    agentId: string
 50    prompt: string
 51    toolUseContext: ToolUseContext
 52    canUseTool: CanUseToolFn
 53    invokingRequestId?: string
 54  }): Promise<ResumeAgentResult> {
 55    const startTime = Date.now()
 56    const appState = toolUseContext.getAppState()
 57    // In-process teammates get a no-op setAppState; setAppStateForTasks
 58    // reaches the root store so task registration/progress/kill stay visible.
 59    const rootSetAppState =
 60      toolUseContext.setAppStateForTasks ?? toolUseContext.setAppState
 61    const permissionMode = appState.toolPermissionContext.mode
 62  
 63    const [transcript, meta] = await Promise.all([
 64      getAgentTranscript(asAgentId(agentId)),
 65      readAgentMetadata(asAgentId(agentId)),
 66    ])
 67    if (!transcript) {
 68      throw new Error(`No transcript found for agent ID: ${agentId}`)
 69    }
 70    const resumedMessages = filterWhitespaceOnlyAssistantMessages(
 71      filterOrphanedThinkingOnlyMessages(
 72        filterUnresolvedToolUses(transcript.messages),
 73      ),
 74    )
 75    const resumedReplacementState = reconstructForSubagentResume(
 76      toolUseContext.contentReplacementState,
 77      resumedMessages,
 78      transcript.contentReplacements,
 79    )
 80    // Best-effort: if the original worktree was removed externally, fall back
 81    // to parent cwd rather than crashing on chdir later.
 82    const resumedWorktreePath = meta?.worktreePath
 83      ? await fsp.stat(meta.worktreePath).then(
 84          s => (s.isDirectory() ? meta.worktreePath : undefined),
 85          () => {
 86            logForDebugging(
 87              `Resumed worktree ${meta.worktreePath} no longer exists; falling back to parent cwd`,
 88            )
 89            return undefined
 90          },
 91        )
 92      : undefined
 93    if (resumedWorktreePath) {
 94      // Bump mtime so stale-worktree cleanup doesn't delete a just-resumed worktree (#22355)
 95      const now = new Date()
 96      await fsp.utimes(resumedWorktreePath, now, now)
 97    }
 98  
 99    // Skip filterDeniedAgents re-gating — original spawn already passed permission checks
100    let selectedAgent: AgentDefinition
101    let isResumedFork = false
102    if (meta?.agentType === FORK_AGENT.agentType) {
103      selectedAgent = FORK_AGENT
104      isResumedFork = true
105    } else if (meta?.agentType) {
106      const found = toolUseContext.options.agentDefinitions.activeAgents.find(
107        a => a.agentType === meta.agentType,
108      )
109      selectedAgent = found ?? GENERAL_PURPOSE_AGENT
110    } else {
111      selectedAgent = GENERAL_PURPOSE_AGENT
112    }
113  
114    const uiDescription = meta?.description ?? '(resumed)'
115  
116    let forkParentSystemPrompt: SystemPrompt | undefined
117    if (isResumedFork) {
118      if (toolUseContext.renderedSystemPrompt) {
119        forkParentSystemPrompt = toolUseContext.renderedSystemPrompt
120      } else {
121        const mainThreadAgentDefinition = appState.agent
122          ? appState.agentDefinitions.activeAgents.find(
123              a => a.agentType === appState.agent,
124            )
125          : undefined
126        const additionalWorkingDirectories = Array.from(
127          appState.toolPermissionContext.additionalWorkingDirectories.keys(),
128        )
129        const defaultSystemPrompt = await getSystemPrompt(
130          toolUseContext.options.tools,
131          toolUseContext.options.mainLoopModel,
132          additionalWorkingDirectories,
133          toolUseContext.options.mcpClients,
134        )
135        forkParentSystemPrompt = buildEffectiveSystemPrompt({
136          mainThreadAgentDefinition,
137          toolUseContext,
138          customSystemPrompt: toolUseContext.options.customSystemPrompt,
139          defaultSystemPrompt,
140          appendSystemPrompt: toolUseContext.options.appendSystemPrompt,
141        })
142      }
143      if (!forkParentSystemPrompt) {
144        throw new Error(
145          'Cannot resume fork agent: unable to reconstruct parent system prompt',
146        )
147      }
148    }
149  
150    // Resolve model for analytics metadata (runAgent resolves its own internally)
151    const resolvedAgentModel = getAgentModel(
152      selectedAgent.model,
153      toolUseContext.options.mainLoopModel,
154      undefined,
155      permissionMode,
156    )
157  
158    const workerPermissionContext = {
159      ...appState.toolPermissionContext,
160      mode: selectedAgent.permissionMode ?? 'acceptEdits',
161    }
162    const workerTools = isResumedFork
163      ? toolUseContext.options.tools
164      : assembleToolPool(workerPermissionContext, appState.mcp.tools)
165  
166    const runAgentParams: Parameters<typeof runAgent>[0] = {
167      agentDefinition: selectedAgent,
168      promptMessages: [
169        ...resumedMessages,
170        createUserMessage({ content: prompt }),
171      ],
172      toolUseContext,
173      canUseTool,
174      isAsync: true,
175      querySource: getQuerySourceForAgent(
176        selectedAgent.agentType,
177        isBuiltInAgent(selectedAgent),
178      ),
179      model: undefined,
180      // Fork resume: pass parent's system prompt (cache-identical prefix).
181      // Non-fork: undefined → runAgent recomputes under wrapWithCwd so
182      // getCwd() sees resumedWorktreePath.
183      override: isResumedFork
184        ? { systemPrompt: forkParentSystemPrompt }
185        : undefined,
186      availableTools: workerTools,
187      // Transcript already contains the parent context slice from the
188      // original fork. Re-supplying it would cause duplicate tool_use IDs.
189      forkContextMessages: undefined,
190      ...(isResumedFork && { useExactTools: true }),
191      // Re-persist so metadata survives runAgent's writeAgentMetadata overwrite
192      worktreePath: resumedWorktreePath,
193      description: meta?.description,
194      contentReplacementState: resumedReplacementState,
195    }
196  
197    // Skip name-registry write — original entry persists from the initial spawn
198    const agentBackgroundTask = registerAsyncAgent({
199      agentId,
200      description: uiDescription,
201      prompt,
202      selectedAgent,
203      setAppState: rootSetAppState,
204      toolUseId: toolUseContext.toolUseId,
205    })
206  
207    const metadata = {
208      prompt,
209      resolvedAgentModel,
210      isBuiltInAgent: isBuiltInAgent(selectedAgent),
211      startTime,
212      agentType: selectedAgent.agentType,
213      isAsync: true,
214    }
215  
216    const asyncAgentContext = {
217      agentId,
218      parentSessionId: getParentSessionId(),
219      agentType: 'subagent' as const,
220      subagentName: selectedAgent.agentType,
221      isBuiltIn: isBuiltInAgent(selectedAgent),
222      invokingRequestId,
223      invocationKind: 'resume' as const,
224      invocationEmitted: false,
225    }
226  
227    const wrapWithCwd = <T>(fn: () => T): T =>
228      resumedWorktreePath ? runWithCwdOverride(resumedWorktreePath, fn) : fn()
229  
230    void runWithAgentContext(asyncAgentContext, () =>
231      wrapWithCwd(() =>
232        runAsyncAgentLifecycle({
233          taskId: agentBackgroundTask.agentId,
234          abortController: agentBackgroundTask.abortController!,
235          makeStream: onCacheSafeParams =>
236            runAgent({
237              ...runAgentParams,
238              override: {
239                ...runAgentParams.override,
240                agentId: asAgentId(agentBackgroundTask.agentId),
241                abortController: agentBackgroundTask.abortController!,
242              },
243              onCacheSafeParams,
244            }),
245          metadata,
246          description: uiDescription,
247          toolUseContext,
248          rootSetAppState,
249          agentIdForCleanup: agentId,
250          enableSummarization:
251            isCoordinatorMode() ||
252            isForkSubagentEnabled() ||
253            getSdkAgentProgressSummariesEnabled(),
254          getWorktreeResult: async () =>
255            resumedWorktreePath ? { worktreePath: resumedWorktreePath } : {},
256        }),
257      ),
258    )
259  
260    return {
261      agentId,
262      description: uiDescription,
263      outputFile: getTaskOutputPath(agentId),
264    }
265  }