init: add source code from src.zip
This commit is contained in:
179
src/services/AgentSummary/agentSummary.ts
Normal file
179
src/services/AgentSummary/agentSummary.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user