source dump of claude code
at main 179 lines 6.4 kB view raw
1/** 2 * Periodic background summarization for coordinator mode sub-agents. 3 * 4 * Forks the sub-agent's conversation every ~30s using runForkedAgent() 5 * to generate a 1-2 sentence progress summary. The summary is stored 6 * on AgentProgress for UI display. 7 * 8 * Cache sharing: uses the same CacheSafeParams as the parent agent 9 * to share the prompt cache. Tools are kept in the request for cache 10 * key matching but denied via canUseTool callback. 11 */ 12 13import type { TaskContext } from '../../Task.js' 14import { updateAgentSummary } from '../../tasks/LocalAgentTask/LocalAgentTask.js' 15import { filterIncompleteToolCalls } from '../../tools/AgentTool/runAgent.js' 16import type { AgentId } from '../../types/ids.js' 17import { logForDebugging } from '../../utils/debug.js' 18import { 19 type CacheSafeParams, 20 runForkedAgent, 21} from '../../utils/forkedAgent.js' 22import { logError } from '../../utils/log.js' 23import { createUserMessage } from '../../utils/messages.js' 24import { getAgentTranscript } from '../../utils/sessionStorage.js' 25 26const SUMMARY_INTERVAL_MS = 30_000 27 28function buildSummaryPrompt(previousSummary: string | null): string { 29 const prevLine = previousSummary 30 ? `\nPrevious: "${previousSummary}" — say something NEW.\n` 31 : '' 32 33 return `Describe your most recent action in 3-5 words using present tense (-ing). Name the file or function, not the branch. Do not use tools. 34${prevLine} 35Good: "Reading runAgent.ts" 36Good: "Fixing null check in validate.ts" 37Good: "Running auth module tests" 38Good: "Adding retry logic to fetchUser" 39 40Bad (past tense): "Analyzed the branch diff" 41Bad (too vague): "Investigating the issue" 42Bad (too long): "Reviewing full branch diff and AgentTool.tsx integration" 43Bad (branch name): "Analyzed adam/background-summary branch diff"` 44} 45 46export function startAgentSummarization( 47 taskId: string, 48 agentId: AgentId, 49 cacheSafeParams: CacheSafeParams, 50 setAppState: TaskContext['setAppState'], 51): { stop: () => void } { 52 // Drop forkContextMessages from the closure — runSummary rebuilds it each 53 // tick from getAgentTranscript(). Without this, the original fork messages 54 // (passed from AgentTool.tsx) are pinned for the lifetime of the timer. 55 const { forkContextMessages: _drop, ...baseParams } = cacheSafeParams 56 let summaryAbortController: AbortController | null = null 57 let timeoutId: ReturnType<typeof setTimeout> | null = null 58 let stopped = false 59 let previousSummary: string | null = null 60 61 async function runSummary(): Promise<void> { 62 if (stopped) return 63 64 logForDebugging(`[AgentSummary] Timer fired for agent ${agentId}`) 65 66 try { 67 // Read current messages from transcript 68 const transcript = await getAgentTranscript(agentId) 69 if (!transcript || transcript.messages.length < 3) { 70 // Not enough context yet — finally block will schedule next attempt 71 logForDebugging( 72 `[AgentSummary] Skipping summary for ${taskId}: not enough messages (${transcript?.messages.length ?? 0})`, 73 ) 74 return 75 } 76 77 // Filter to clean message state 78 const cleanMessages = filterIncompleteToolCalls(transcript.messages) 79 80 // Build fork params with current messages 81 const forkParams: CacheSafeParams = { 82 ...baseParams, 83 forkContextMessages: cleanMessages, 84 } 85 86 logForDebugging( 87 `[AgentSummary] Forking for summary, ${cleanMessages.length} messages in context`, 88 ) 89 90 // Create abort controller for this summary 91 summaryAbortController = new AbortController() 92 93 // Deny tools via callback, NOT by passing tools:[] - that busts cache 94 const canUseTool = async () => ({ 95 behavior: 'deny' as const, 96 message: 'No tools needed for summary', 97 decisionReason: { type: 'other' as const, reason: 'summary only' }, 98 }) 99 100 // DO NOT set maxOutputTokens here. The fork piggybacks on the main 101 // thread's prompt cache by sending identical cache-key params (system, 102 // tools, model, messages prefix, thinking config). Setting maxOutputTokens 103 // would clamp budget_tokens, creating a thinking config mismatch that 104 // invalidates the cache. 105 // 106 // ContentReplacementState is cloned by default in createSubagentContext 107 // from forkParams.toolUseContext (the subagent's LIVE state captured at 108 // onCacheSafeParams time). No explicit override needed. 109 const result = await runForkedAgent({ 110 promptMessages: [ 111 createUserMessage({ content: buildSummaryPrompt(previousSummary) }), 112 ], 113 cacheSafeParams: forkParams, 114 canUseTool, 115 querySource: 'agent_summary', 116 forkLabel: 'agent_summary', 117 overrides: { abortController: summaryAbortController }, 118 skipTranscript: true, 119 }) 120 121 if (stopped) return 122 123 // Extract summary text from result 124 for (const msg of result.messages) { 125 if (msg.type !== 'assistant') continue 126 // Skip API error messages 127 if (msg.isApiErrorMessage) { 128 logForDebugging( 129 `[AgentSummary] Skipping API error message for ${taskId}`, 130 ) 131 continue 132 } 133 const textBlock = msg.message.content.find(b => b.type === 'text') 134 if (textBlock?.type === 'text' && textBlock.text.trim()) { 135 const summaryText = textBlock.text.trim() 136 logForDebugging( 137 `[AgentSummary] Summary result for ${taskId}: ${summaryText}`, 138 ) 139 previousSummary = summaryText 140 updateAgentSummary(taskId, summaryText, setAppState) 141 break 142 } 143 } 144 } catch (e) { 145 if (!stopped && e instanceof Error) { 146 logError(e) 147 } 148 } finally { 149 summaryAbortController = null 150 // Reset timer on completion (not initiation) to prevent overlapping summaries 151 if (!stopped) { 152 scheduleNext() 153 } 154 } 155 } 156 157 function scheduleNext(): void { 158 if (stopped) return 159 timeoutId = setTimeout(runSummary, SUMMARY_INTERVAL_MS) 160 } 161 162 function stop(): void { 163 logForDebugging(`[AgentSummary] Stopping summarization for ${taskId}`) 164 stopped = true 165 if (timeoutId) { 166 clearTimeout(timeoutId) 167 timeoutId = null 168 } 169 if (summaryAbortController) { 170 summaryAbortController.abort() 171 summaryAbortController = null 172 } 173 } 174 175 // Start the first timer 176 scheduleNext() 177 178 return { stop } 179}