/ services / toolUseSummary / toolUseSummaryGenerator.ts
toolUseSummaryGenerator.ts
  1  /**
  2   * Tool Use Summary Generator
  3   *
  4   * Generates human-readable summaries of completed tool batches using Haiku.
  5   * Used by the SDK to provide high-level progress updates to clients.
  6   */
  7  
  8  import { E_TOOL_USE_SUMMARY_GENERATION_FAILED } from '../../constants/errorIds.js'
  9  import { toError } from '../../utils/errors.js'
 10  import { logError } from '../../utils/log.js'
 11  import { jsonStringify } from '../../utils/slowOperations.js'
 12  import { asSystemPrompt } from '../../utils/systemPromptType.js'
 13  import { queryHaiku } from '../api/claude.js'
 14  
 15  const TOOL_USE_SUMMARY_SYSTEM_PROMPT = `Write a short summary label describing what these tool calls accomplished. It appears as a single-line row in a mobile app and truncates around 30 characters, so think git-commit-subject, not sentence.
 16  
 17  Keep the verb in past tense and the most distinctive noun. Drop articles, connectors, and long location context first.
 18  
 19  Examples:
 20  - Searched in auth/
 21  - Fixed NPE in UserService
 22  - Created signup endpoint
 23  - Read config.json
 24  - Ran failing tests`
 25  
 26  type ToolInfo = {
 27    name: string
 28    input: unknown
 29    output: unknown
 30  }
 31  
 32  export type GenerateToolUseSummaryParams = {
 33    tools: ToolInfo[]
 34    signal: AbortSignal
 35    isNonInteractiveSession: boolean
 36    lastAssistantText?: string
 37  }
 38  
 39  /**
 40   * Generates a human-readable summary of completed tools.
 41   *
 42   * @param params - Parameters including tools executed and their results
 43   * @returns A brief summary string, or null if generation fails
 44   */
 45  export async function generateToolUseSummary({
 46    tools,
 47    signal,
 48    isNonInteractiveSession,
 49    lastAssistantText,
 50  }: GenerateToolUseSummaryParams): Promise<string | null> {
 51    if (tools.length === 0) {
 52      return null
 53    }
 54  
 55    try {
 56      // Build a concise representation of what tools did
 57      const toolSummaries = tools
 58        .map(tool => {
 59          const inputStr = truncateJson(tool.input, 300)
 60          const outputStr = truncateJson(tool.output, 300)
 61          return `Tool: ${tool.name}\nInput: ${inputStr}\nOutput: ${outputStr}`
 62        })
 63        .join('\n\n')
 64  
 65      const contextPrefix = lastAssistantText
 66        ? `User's intent (from assistant's last message): ${lastAssistantText.slice(0, 200)}\n\n`
 67        : ''
 68  
 69      const response = await queryHaiku({
 70        systemPrompt: asSystemPrompt([TOOL_USE_SUMMARY_SYSTEM_PROMPT]),
 71        userPrompt: `${contextPrefix}Tools completed:\n\n${toolSummaries}\n\nLabel:`,
 72        signal,
 73        options: {
 74          querySource: 'tool_use_summary_generation',
 75          enablePromptCaching: true,
 76          agents: [],
 77          isNonInteractiveSession,
 78          hasAppendSystemPrompt: false,
 79          mcpTools: [],
 80        },
 81      })
 82  
 83      const summary = response.message.content
 84        .filter(block => block.type === 'text')
 85        .map(block => (block.type === 'text' ? block.text : ''))
 86        .join('')
 87        .trim()
 88  
 89      return summary || null
 90    } catch (error) {
 91      // Log but don't fail - summaries are non-critical
 92      const err = toError(error)
 93      err.cause = { errorId: E_TOOL_USE_SUMMARY_GENERATION_FAILED }
 94      logError(err)
 95      return null
 96    }
 97  }
 98  
 99  /**
100   * Truncates a JSON value to a maximum length for the prompt.
101   */
102  function truncateJson(value: unknown, maxLength: number): string {
103    try {
104      const str = jsonStringify(value)
105      if (str.length <= maxLength) {
106        return str
107      }
108      return str.slice(0, maxLength - 3) + '...'
109    } catch {
110      return '[unable to serialize]'
111    }
112  }