/ services / awaySummary.ts
awaySummary.ts
 1  import { APIUserAbortError } from '@anthropic-ai/sdk'
 2  import { getEmptyToolPermissionContext } from '../Tool.js'
 3  import type { Message } from '../types/message.js'
 4  import { logForDebugging } from '../utils/debug.js'
 5  import {
 6    createUserMessage,
 7    getAssistantMessageText,
 8  } from '../utils/messages.js'
 9  import { getSmallFastModel } from '../utils/model/model.js'
10  import { asSystemPrompt } from '../utils/systemPromptType.js'
11  import { queryModelWithoutStreaming } from './api/claude.js'
12  import { getSessionMemoryContent } from './SessionMemory/sessionMemoryUtils.js'
13  
14  // Recap only needs recent context — truncate to avoid "prompt too long" on
15  // large sessions. 30 messages ≈ ~15 exchanges, plenty for "where we left off."
16  const RECENT_MESSAGE_WINDOW = 30
17  
18  function buildAwaySummaryPrompt(memory: string | null): string {
19    const memoryBlock = memory
20      ? `Session memory (broader context):\n${memory}\n\n`
21      : ''
22    return `${memoryBlock}The user stepped away and is coming back. Write exactly 1-3 short sentences. Start by stating the high-level task — what they are building or debugging, not implementation details. Next: the concrete next step. Skip status reports and commit recaps.`
23  }
24  
25  /**
26   * Generates a short session recap for the "while you were away" card.
27   * Returns null on abort, empty transcript, or error.
28   */
29  export async function generateAwaySummary(
30    messages: readonly Message[],
31    signal: AbortSignal,
32  ): Promise<string | null> {
33    if (messages.length === 0) {
34      return null
35    }
36  
37    try {
38      const memory = await getSessionMemoryContent()
39      const recent = messages.slice(-RECENT_MESSAGE_WINDOW)
40      recent.push(createUserMessage({ content: buildAwaySummaryPrompt(memory) }))
41      const response = await queryModelWithoutStreaming({
42        messages: recent,
43        systemPrompt: asSystemPrompt([]),
44        thinkingConfig: { type: 'disabled' },
45        tools: [],
46        signal,
47        options: {
48          getToolPermissionContext: async () => getEmptyToolPermissionContext(),
49          model: getSmallFastModel(),
50          toolChoice: undefined,
51          isNonInteractiveSession: false,
52          hasAppendSystemPrompt: false,
53          agents: [],
54          querySource: 'away_summary',
55          mcpTools: [],
56          skipCacheWrite: true,
57        },
58      })
59  
60      if (response.isApiErrorMessage) {
61        logForDebugging(
62          `[awaySummary] API error: ${getAssistantMessageText(response)}`,
63        )
64        return null
65      }
66      return getAssistantMessageText(response)
67    } catch (err) {
68      if (err instanceof APIUserAbortError || signal.aborted) {
69        return null
70      }
71      logForDebugging(`[awaySummary] generation failed: ${err}`)
72      return null
73    }
74  }