source dump of claude code
at main 686 lines 23 kB view raw
1import { feature } from 'bun:bundle' 2import { z } from 'zod/v4' 3import { clearInvokedSkillsForAgent } from '../../bootstrap/state.js' 4import { 5 ALL_AGENT_DISALLOWED_TOOLS, 6 ASYNC_AGENT_ALLOWED_TOOLS, 7 CUSTOM_AGENT_DISALLOWED_TOOLS, 8 IN_PROCESS_TEAMMATE_ALLOWED_TOOLS, 9} from '../../constants/tools.js' 10import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js' 11import { 12 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 13 logEvent, 14} from '../../services/analytics/index.js' 15import { clearDumpState } from '../../services/api/dumpPrompts.js' 16import type { AppState } from '../../state/AppState.js' 17import type { 18 Tool, 19 ToolPermissionContext, 20 Tools, 21 ToolUseContext, 22} from '../../Tool.js' 23import { toolMatchesName } from '../../Tool.js' 24import { 25 completeAgentTask as completeAsyncAgent, 26 createActivityDescriptionResolver, 27 createProgressTracker, 28 enqueueAgentNotification, 29 failAgentTask as failAsyncAgent, 30 getProgressUpdate, 31 getTokenCountFromTracker, 32 isLocalAgentTask, 33 killAsyncAgent, 34 type ProgressTracker, 35 updateAgentProgress as updateAsyncAgentProgress, 36 updateProgressFromMessage, 37} from '../../tasks/LocalAgentTask/LocalAgentTask.js' 38import { asAgentId } from '../../types/ids.js' 39import type { Message as MessageType } from '../../types/message.js' 40import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' 41import { logForDebugging } from '../../utils/debug.js' 42import { isInProtectedNamespace } from '../../utils/envUtils.js' 43import { AbortError, errorMessage } from '../../utils/errors.js' 44import type { CacheSafeParams } from '../../utils/forkedAgent.js' 45import { lazySchema } from '../../utils/lazySchema.js' 46import { 47 extractTextContent, 48 getLastAssistantMessage, 49} from '../../utils/messages.js' 50import type { PermissionMode } from '../../utils/permissions/PermissionMode.js' 51import { permissionRuleValueFromString } from '../../utils/permissions/permissionRuleParser.js' 52import { 53 buildTranscriptForClassifier, 54 classifyYoloAction, 55} from '../../utils/permissions/yoloClassifier.js' 56import { emitTaskProgress as emitTaskProgressEvent } from '../../utils/task/sdkProgress.js' 57import { isInProcessTeammate } from '../../utils/teammateContext.js' 58import { getTokenCountFromUsage } from '../../utils/tokens.js' 59import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../ExitPlanModeTool/constants.js' 60import { AGENT_TOOL_NAME, LEGACY_AGENT_TOOL_NAME } from './constants.js' 61import type { AgentDefinition } from './loadAgentsDir.js' 62export type ResolvedAgentTools = { 63 hasWildcard: boolean 64 validTools: string[] 65 invalidTools: string[] 66 resolvedTools: Tools 67 allowedAgentTypes?: string[] 68} 69 70export function filterToolsForAgent({ 71 tools, 72 isBuiltIn, 73 isAsync = false, 74 permissionMode, 75}: { 76 tools: Tools 77 isBuiltIn: boolean 78 isAsync?: boolean 79 permissionMode?: PermissionMode 80}): Tools { 81 return tools.filter(tool => { 82 // Allow MCP tools for all agents 83 if (tool.name.startsWith('mcp__')) { 84 return true 85 } 86 // Allow ExitPlanMode for agents in plan mode (e.g., in-process teammates) 87 // This bypasses both the ALL_AGENT_DISALLOWED_TOOLS and async tool filters 88 if ( 89 toolMatchesName(tool, EXIT_PLAN_MODE_V2_TOOL_NAME) && 90 permissionMode === 'plan' 91 ) { 92 return true 93 } 94 if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) { 95 return false 96 } 97 if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) { 98 return false 99 } 100 if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) { 101 if (isAgentSwarmsEnabled() && isInProcessTeammate()) { 102 // Allow AgentTool for in-process teammates to spawn sync subagents. 103 // Validation in AgentTool.call() prevents background agents and teammate spawning. 104 if (toolMatchesName(tool, AGENT_TOOL_NAME)) { 105 return true 106 } 107 // Allow task tools for in-process teammates to coordinate via shared task list 108 if (IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.has(tool.name)) { 109 return true 110 } 111 } 112 return false 113 } 114 return true 115 }) 116} 117 118/** 119 * Resolves and validates agent tools against available tools 120 * Handles wildcard expansion and validation in one place 121 */ 122export function resolveAgentTools( 123 agentDefinition: Pick< 124 AgentDefinition, 125 'tools' | 'disallowedTools' | 'source' | 'permissionMode' 126 >, 127 availableTools: Tools, 128 isAsync = false, 129 isMainThread = false, 130): ResolvedAgentTools { 131 const { 132 tools: agentTools, 133 disallowedTools, 134 source, 135 permissionMode, 136 } = agentDefinition 137 // When isMainThread is true, skip filterToolsForAgent entirely — the main 138 // thread's tool pool is already properly assembled by useMergedTools(), so 139 // the sub-agent disallow lists shouldn't apply. 140 const filteredAvailableTools = isMainThread 141 ? availableTools 142 : filterToolsForAgent({ 143 tools: availableTools, 144 isBuiltIn: source === 'built-in', 145 isAsync, 146 permissionMode, 147 }) 148 149 // Create a set of disallowed tool names for quick lookup 150 const disallowedToolSet = new Set( 151 disallowedTools?.map(toolSpec => { 152 const { toolName } = permissionRuleValueFromString(toolSpec) 153 return toolName 154 }) ?? [], 155 ) 156 157 // Filter available tools based on disallowed list 158 const allowedAvailableTools = filteredAvailableTools.filter( 159 tool => !disallowedToolSet.has(tool.name), 160 ) 161 162 // If tools is undefined or ['*'], allow all tools (after filtering disallowed) 163 const hasWildcard = 164 agentTools === undefined || 165 (agentTools.length === 1 && agentTools[0] === '*') 166 if (hasWildcard) { 167 return { 168 hasWildcard: true, 169 validTools: [], 170 invalidTools: [], 171 resolvedTools: allowedAvailableTools, 172 } 173 } 174 175 const availableToolMap = new Map<string, Tool>() 176 for (const tool of allowedAvailableTools) { 177 availableToolMap.set(tool.name, tool) 178 } 179 180 const validTools: string[] = [] 181 const invalidTools: string[] = [] 182 const resolved: Tool[] = [] 183 const resolvedToolsSet = new Set<Tool>() 184 let allowedAgentTypes: string[] | undefined 185 186 for (const toolSpec of agentTools) { 187 // Parse the tool spec to extract the base tool name and any permission pattern 188 const { toolName, ruleContent } = permissionRuleValueFromString(toolSpec) 189 190 // Special case: Agent tool carries allowedAgentTypes metadata in its spec 191 if (toolName === AGENT_TOOL_NAME) { 192 if (ruleContent) { 193 // Parse comma-separated agent types: "worker, researcher" → ["worker", "researcher"] 194 allowedAgentTypes = ruleContent.split(',').map(s => s.trim()) 195 } 196 // For sub-agents, Agent is excluded by filterToolsForAgent — mark the spec 197 // valid for allowedAgentTypes tracking but skip tool resolution. 198 if (!isMainThread) { 199 validTools.push(toolSpec) 200 continue 201 } 202 // For main thread, filtering was skipped so Agent is in availableToolMap — 203 // fall through to normal resolution below. 204 } 205 206 const tool = availableToolMap.get(toolName) 207 if (tool) { 208 validTools.push(toolSpec) 209 if (!resolvedToolsSet.has(tool)) { 210 resolved.push(tool) 211 resolvedToolsSet.add(tool) 212 } 213 } else { 214 invalidTools.push(toolSpec) 215 } 216 } 217 218 return { 219 hasWildcard: false, 220 validTools, 221 invalidTools, 222 resolvedTools: resolved, 223 allowedAgentTypes, 224 } 225} 226 227export const agentToolResultSchema = lazySchema(() => 228 z.object({ 229 agentId: z.string(), 230 // Optional: older persisted sessions won't have this (resume replays 231 // results verbatim without re-validation). Used to gate the sync 232 // result trailer — one-shot built-ins skip the SendMessage hint. 233 agentType: z.string().optional(), 234 content: z.array(z.object({ type: z.literal('text'), text: z.string() })), 235 totalToolUseCount: z.number(), 236 totalDurationMs: z.number(), 237 totalTokens: z.number(), 238 usage: z.object({ 239 input_tokens: z.number(), 240 output_tokens: z.number(), 241 cache_creation_input_tokens: z.number().nullable(), 242 cache_read_input_tokens: z.number().nullable(), 243 server_tool_use: z 244 .object({ 245 web_search_requests: z.number(), 246 web_fetch_requests: z.number(), 247 }) 248 .nullable(), 249 service_tier: z.enum(['standard', 'priority', 'batch']).nullable(), 250 cache_creation: z 251 .object({ 252 ephemeral_1h_input_tokens: z.number(), 253 ephemeral_5m_input_tokens: z.number(), 254 }) 255 .nullable(), 256 }), 257 }), 258) 259 260export type AgentToolResult = z.input<ReturnType<typeof agentToolResultSchema>> 261 262export function countToolUses(messages: MessageType[]): number { 263 let count = 0 264 for (const m of messages) { 265 if (m.type === 'assistant') { 266 for (const block of m.message.content) { 267 if (block.type === 'tool_use') { 268 count++ 269 } 270 } 271 } 272 } 273 return count 274} 275 276export function finalizeAgentTool( 277 agentMessages: MessageType[], 278 agentId: string, 279 metadata: { 280 prompt: string 281 resolvedAgentModel: string 282 isBuiltInAgent: boolean 283 startTime: number 284 agentType: string 285 isAsync: boolean 286 }, 287): AgentToolResult { 288 const { 289 prompt, 290 resolvedAgentModel, 291 isBuiltInAgent, 292 startTime, 293 agentType, 294 isAsync, 295 } = metadata 296 297 const lastAssistantMessage = getLastAssistantMessage(agentMessages) 298 if (lastAssistantMessage === undefined) { 299 throw new Error('No assistant messages found') 300 } 301 // Extract text content from the agent's response. If the final assistant 302 // message is a pure tool_use block (loop exited mid-turn), fall back to 303 // the most recent assistant message that has text content. 304 let content = lastAssistantMessage.message.content.filter( 305 _ => _.type === 'text', 306 ) 307 if (content.length === 0) { 308 for (let i = agentMessages.length - 1; i >= 0; i--) { 309 const m = agentMessages[i]! 310 if (m.type !== 'assistant') continue 311 const textBlocks = m.message.content.filter(_ => _.type === 'text') 312 if (textBlocks.length > 0) { 313 content = textBlocks 314 break 315 } 316 } 317 } 318 319 const totalTokens = getTokenCountFromUsage(lastAssistantMessage.message.usage) 320 const totalToolUseCount = countToolUses(agentMessages) 321 322 logEvent('tengu_agent_tool_completed', { 323 agent_type: 324 agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 325 model: 326 resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 327 prompt_char_count: prompt.length, 328 response_char_count: content.length, 329 assistant_message_count: agentMessages.length, 330 total_tool_uses: totalToolUseCount, 331 duration_ms: Date.now() - startTime, 332 total_tokens: totalTokens, 333 is_built_in_agent: isBuiltInAgent, 334 is_async: isAsync, 335 }) 336 337 // Signal to inference that this subagent's cache chain can be evicted. 338 const lastRequestId = lastAssistantMessage.requestId 339 if (lastRequestId) { 340 logEvent('tengu_cache_eviction_hint', { 341 scope: 342 'subagent_end' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 343 last_request_id: 344 lastRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 345 }) 346 } 347 348 return { 349 agentId, 350 agentType, 351 content, 352 totalDurationMs: Date.now() - startTime, 353 totalTokens, 354 totalToolUseCount, 355 usage: lastAssistantMessage.message.usage, 356 } 357} 358 359/** 360 * Returns the name of the last tool_use block in an assistant message, 361 * or undefined if the message is not an assistant message with tool_use. 362 */ 363export function getLastToolUseName(message: MessageType): string | undefined { 364 if (message.type !== 'assistant') return undefined 365 const block = message.message.content.findLast(b => b.type === 'tool_use') 366 return block?.type === 'tool_use' ? block.name : undefined 367} 368 369export function emitTaskProgress( 370 tracker: ProgressTracker, 371 taskId: string, 372 toolUseId: string | undefined, 373 description: string, 374 startTime: number, 375 lastToolName: string, 376): void { 377 const progress = getProgressUpdate(tracker) 378 emitTaskProgressEvent({ 379 taskId, 380 toolUseId, 381 description: progress.lastActivity?.activityDescription ?? description, 382 startTime, 383 totalTokens: progress.tokenCount, 384 toolUses: progress.toolUseCount, 385 lastToolName, 386 }) 387} 388 389export async function classifyHandoffIfNeeded({ 390 agentMessages, 391 tools, 392 toolPermissionContext, 393 abortSignal, 394 subagentType, 395 totalToolUseCount, 396}: { 397 agentMessages: MessageType[] 398 tools: Tools 399 toolPermissionContext: AppState['toolPermissionContext'] 400 abortSignal: AbortSignal 401 subagentType: string 402 totalToolUseCount: number 403}): Promise<string | null> { 404 if (feature('TRANSCRIPT_CLASSIFIER')) { 405 if (toolPermissionContext.mode !== 'auto') return null 406 407 const agentTranscript = buildTranscriptForClassifier(agentMessages, tools) 408 if (!agentTranscript) return null 409 410 const classifierResult = await classifyYoloAction( 411 agentMessages, 412 { 413 role: 'user', 414 content: [ 415 { 416 type: 'text', 417 text: "Sub-agent has finished and is handing back control to the main agent. Review the sub-agent's work based on the block rules and let the main agent know if any file is dangerous (the main agent will see the reason).", 418 }, 419 ], 420 }, 421 tools, 422 toolPermissionContext as ToolPermissionContext, 423 abortSignal, 424 ) 425 426 const handoffDecision = classifierResult.unavailable 427 ? 'unavailable' 428 : classifierResult.shouldBlock 429 ? 'blocked' 430 : 'allowed' 431 logEvent('tengu_auto_mode_decision', { 432 decision: 433 handoffDecision as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 434 toolName: 435 // Use legacy name for analytics continuity across the Task→Agent rename 436 LEGACY_AGENT_TOOL_NAME as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 437 inProtectedNamespace: isInProtectedNamespace(), 438 classifierModel: 439 classifierResult.model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 440 agentType: 441 subagentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 442 toolUseCount: totalToolUseCount, 443 isHandoff: true, 444 // For handoff, the relevant agent completion is the subagent's final 445 // assistant message — the last thing the classifier transcript shows 446 // before the handoff review prompt. 447 agentMsgId: getLastAssistantMessage(agentMessages)?.message 448 .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 449 classifierStage: 450 classifierResult.stage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 451 classifierStage1RequestId: 452 classifierResult.stage1RequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 453 classifierStage1MsgId: 454 classifierResult.stage1MsgId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 455 classifierStage2RequestId: 456 classifierResult.stage2RequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 457 classifierStage2MsgId: 458 classifierResult.stage2MsgId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 459 }) 460 461 if (classifierResult.shouldBlock) { 462 // When classifier is unavailable, still propagate the sub-agent's 463 // results but with a warning so the parent agent can verify the work. 464 if (classifierResult.unavailable) { 465 logForDebugging( 466 'Handoff classifier unavailable, allowing sub-agent output with warning', 467 { level: 'warn' }, 468 ) 469 return `Note: The safety classifier was unavailable when reviewing this sub-agent's work. Please carefully verify the sub-agent's actions and output before acting on them.` 470 } 471 472 logForDebugging( 473 `Handoff classifier flagged sub-agent output: ${classifierResult.reason}`, 474 { level: 'warn' }, 475 ) 476 return `SECURITY WARNING: This sub-agent performed actions that may violate security policy. Reason: ${classifierResult.reason}. Review the sub-agent's actions carefully before acting on its output.` 477 } 478 } 479 480 return null 481} 482 483/** 484 * Extract a partial result string from an agent's accumulated messages. 485 * Used when an async agent is killed to preserve what it accomplished. 486 * Returns undefined if no text content is found. 487 */ 488export function extractPartialResult( 489 messages: MessageType[], 490): string | undefined { 491 for (let i = messages.length - 1; i >= 0; i--) { 492 const m = messages[i]! 493 if (m.type !== 'assistant') continue 494 const text = extractTextContent(m.message.content, '\n') 495 if (text) { 496 return text 497 } 498 } 499 return undefined 500} 501 502type SetAppState = (f: (prev: AppState) => AppState) => void 503 504/** 505 * Drives a background agent from spawn to terminal notification. 506 * Shared between AgentTool's async-from-start path and resumeAgentBackground. 507 */ 508export async function runAsyncAgentLifecycle({ 509 taskId, 510 abortController, 511 makeStream, 512 metadata, 513 description, 514 toolUseContext, 515 rootSetAppState, 516 agentIdForCleanup, 517 enableSummarization, 518 getWorktreeResult, 519}: { 520 taskId: string 521 abortController: AbortController 522 makeStream: ( 523 onCacheSafeParams: ((p: CacheSafeParams) => void) | undefined, 524 ) => AsyncGenerator<MessageType, void> 525 metadata: Parameters<typeof finalizeAgentTool>[2] 526 description: string 527 toolUseContext: ToolUseContext 528 rootSetAppState: SetAppState 529 agentIdForCleanup: string 530 enableSummarization: boolean 531 getWorktreeResult: () => Promise<{ 532 worktreePath?: string 533 worktreeBranch?: string 534 }> 535}): Promise<void> { 536 let stopSummarization: (() => void) | undefined 537 const agentMessages: MessageType[] = [] 538 try { 539 const tracker = createProgressTracker() 540 const resolveActivity = createActivityDescriptionResolver( 541 toolUseContext.options.tools, 542 ) 543 const onCacheSafeParams = enableSummarization 544 ? (params: CacheSafeParams) => { 545 const { stop } = startAgentSummarization( 546 taskId, 547 asAgentId(taskId), 548 params, 549 rootSetAppState, 550 ) 551 stopSummarization = stop 552 } 553 : undefined 554 for await (const message of makeStream(onCacheSafeParams)) { 555 agentMessages.push(message) 556 // Append immediately when UI holds the task (retain). Bootstrap reads 557 // disk in parallel and UUID-merges the prefix — disk-write-before-yield 558 // means live is always a suffix of disk, so merge is order-correct. 559 rootSetAppState(prev => { 560 const t = prev.tasks[taskId] 561 if (!isLocalAgentTask(t) || !t.retain) return prev 562 const base = t.messages ?? [] 563 return { 564 ...prev, 565 tasks: { 566 ...prev.tasks, 567 [taskId]: { ...t, messages: [...base, message] }, 568 }, 569 } 570 }) 571 updateProgressFromMessage( 572 tracker, 573 message, 574 resolveActivity, 575 toolUseContext.options.tools, 576 ) 577 updateAsyncAgentProgress( 578 taskId, 579 getProgressUpdate(tracker), 580 rootSetAppState, 581 ) 582 const lastToolName = getLastToolUseName(message) 583 if (lastToolName) { 584 emitTaskProgress( 585 tracker, 586 taskId, 587 toolUseContext.toolUseId, 588 description, 589 metadata.startTime, 590 lastToolName, 591 ) 592 } 593 } 594 595 stopSummarization?.() 596 597 const agentResult = finalizeAgentTool(agentMessages, taskId, metadata) 598 599 // Mark task completed FIRST so TaskOutput(block=true) unblocks 600 // immediately. classifyHandoffIfNeeded (API call) and getWorktreeResult 601 // (git exec) are notification embellishments that can hang — they must 602 // not gate the status transition (gh-20236). 603 completeAsyncAgent(agentResult, rootSetAppState) 604 605 let finalMessage = extractTextContent(agentResult.content, '\n') 606 607 if (feature('TRANSCRIPT_CLASSIFIER')) { 608 const handoffWarning = await classifyHandoffIfNeeded({ 609 agentMessages, 610 tools: toolUseContext.options.tools, 611 toolPermissionContext: 612 toolUseContext.getAppState().toolPermissionContext, 613 abortSignal: abortController.signal, 614 subagentType: metadata.agentType, 615 totalToolUseCount: agentResult.totalToolUseCount, 616 }) 617 if (handoffWarning) { 618 finalMessage = `${handoffWarning}\n\n${finalMessage}` 619 } 620 } 621 622 const worktreeResult = await getWorktreeResult() 623 624 enqueueAgentNotification({ 625 taskId, 626 description, 627 status: 'completed', 628 setAppState: rootSetAppState, 629 finalMessage, 630 usage: { 631 totalTokens: getTokenCountFromTracker(tracker), 632 toolUses: agentResult.totalToolUseCount, 633 durationMs: agentResult.totalDurationMs, 634 }, 635 toolUseId: toolUseContext.toolUseId, 636 ...worktreeResult, 637 }) 638 } catch (error) { 639 stopSummarization?.() 640 if (error instanceof AbortError) { 641 // killAsyncAgent is a no-op if TaskStop already set status='killed' — 642 // but only this catch handler has agentMessages, so the notification 643 // must fire unconditionally. Transition status BEFORE worktree cleanup 644 // so TaskOutput unblocks even if git hangs (gh-20236). 645 killAsyncAgent(taskId, rootSetAppState) 646 logEvent('tengu_agent_tool_terminated', { 647 agent_type: 648 metadata.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 649 model: 650 metadata.resolvedAgentModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 651 duration_ms: Date.now() - metadata.startTime, 652 is_async: true, 653 is_built_in_agent: metadata.isBuiltInAgent, 654 reason: 655 'user_kill_async' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 656 }) 657 const worktreeResult = await getWorktreeResult() 658 const partialResult = extractPartialResult(agentMessages) 659 enqueueAgentNotification({ 660 taskId, 661 description, 662 status: 'killed', 663 setAppState: rootSetAppState, 664 toolUseId: toolUseContext.toolUseId, 665 finalMessage: partialResult, 666 ...worktreeResult, 667 }) 668 return 669 } 670 const msg = errorMessage(error) 671 failAsyncAgent(taskId, msg, rootSetAppState) 672 const worktreeResult = await getWorktreeResult() 673 enqueueAgentNotification({ 674 taskId, 675 description, 676 status: 'failed', 677 error: msg, 678 setAppState: rootSetAppState, 679 toolUseId: toolUseContext.toolUseId, 680 ...worktreeResult, 681 }) 682 } finally { 683 clearInvokedSkillsForAgent(agentIdForCleanup) 684 clearDumpState(agentIdForCleanup) 685 } 686}