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