π File detail
services/AgentSummary/agentSummary.ts
π― Use case
This file lives under βservices/β, which covers long-lived services (LSP, MCP, OAuth, tool execution, memory, compaction, voice, settings sync, β¦). On the API surface it exposes startAgentSummarization β mainly functions, hooks, or classes. It composes internal code from Task, tasks, tools, types, and utils (relative imports). What the file header says: 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.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
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.
π€ Exports (heuristic)
startAgentSummarization
π₯οΈ Source preview
/**
* 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 }
}