{"version":3,"file":"contextPressure.mjs","sources":["../../../src/utils/contextPressure.ts"],"sourcesContent":["/**\n * Context Pressure Utilities\n *\n * Pure functions for context overflow management. These handle:\n * 1. Multi-document detection — counting attached documents in messages\n * 2. Multi-document delegation hint — injected when 3+ documents detected\n * 3. Post-prune context note — injected after pruning/summarization\n *\n * DESIGN PRINCIPLE: The LLM never sees raw token numbers. Context overflow\n * is handled mechanically by pruning (Graph) + auto-continuation (client.js).\n * Only task-driven hints (multi-document) are injected — never budget-based.\n *\n * @see docs/context-overflow-architecture.md\n */\n\nimport type { BaseMessage } from '@langchain/core/messages';\nimport { MULTI_DOCUMENT_THRESHOLD } from '@/common/constants';\n\n/** Result of scanning messages for attached documents */\nexport interface DocumentDetectionResult {\n  /** Total unique documents detected */\n  count: number;\n  /** Names of detected documents */\n  names: string[];\n}\n\n/**\n * Scan messages for attached documents using known content patterns.\n *\n * Detects documents from:\n * 1. `# \"filename\"` headers in \"Attached document(s):\" blocks (text content)\n * 2. `**filename1, filename2**` in \"The user has attached:\" blocks (embedded files)\n *\n * @param messages - Conversation messages to scan\n * @returns Document count and names (deduplicated)\n */\nexport function detectDocuments(\n  messages: BaseMessage[]\n): DocumentDetectionResult {\n  const documentNames: string[] = [];\n\n  for (const msg of messages) {\n    const content = extractTextContent(msg);\n\n    // Pattern 1: # \"filename\" headers in attached document blocks\n    const docMatches = content.match(/# \"([^\"]+)\"/g);\n    if (docMatches) {\n      for (const match of docMatches) {\n        const name = match.replace(/# \"/, '').replace(/\"$/, '');\n        if (!documentNames.includes(name)) {\n          documentNames.push(name);\n        }\n      }\n    }\n\n    // Pattern 2: \"The user has attached: **file1, file2**\" (embedded files)\n    const attachedMatch = content.match(\n      /user has attached:\\s*\\*\\*([^*]+)\\*\\*/i\n    );\n    if (attachedMatch) {\n      const names = attachedMatch[1]\n        .split(',')\n        .map((n: string) => n.trim())\n        .filter(Boolean);\n      for (const name of names) {\n        if (!documentNames.includes(name)) {\n          documentNames.push(name);\n        }\n      }\n    }\n  }\n\n  return { count: documentNames.length, names: documentNames };\n}\n\n/**\n * Determine whether the multi-document delegation hint should be injected.\n *\n * Only fires on the first iteration (before any AI response) when the\n * document count meets the threshold. This ensures the agent delegates\n * upfront rather than trying to process all documents itself.\n *\n * @param documentCount - Number of detected documents\n * @param hasAiResponse - Whether the agent has already responded in this chain\n * @returns Whether to inject the delegation hint\n */\nexport function shouldInjectMultiDocHint(\n  documentCount: number,\n  hasAiResponse: boolean\n): boolean {\n  return documentCount >= MULTI_DOCUMENT_THRESHOLD && !hasAiResponse;\n}\n\n/**\n * Build the multi-document delegation hint message content.\n *\n * @param documentCount - Number of detected documents\n * @param documentNames - Names of detected documents\n * @returns Message content string for injection as HumanMessage\n */\nexport function buildMultiDocHintContent(\n  documentCount: number,\n  documentNames: string[]\n): string {\n  return (\n    `[MULTI-DOCUMENT PROCESSING — ${documentCount} documents detected]\\n` +\n    `Documents: ${documentNames.join(', ')}\\n\\n` +\n    `You have ${documentCount} documents attached. For thorough analysis, use the \"task\" tool ` +\n    'to delegate each document (or group of related documents) to a sub-agent.\\n' +\n    'Each sub-agent has its own fresh context window and can use file_search to retrieve the full document content.\\n' +\n    'After all sub-agents complete, synthesize their results into a comprehensive response.\\n\\n' +\n    'This approach ensures each document gets full attention without context limitations.'\n  );\n}\n\n/**\n * Build the post-prune context note injected after messages are pruned\n * and summarized. No token numbers — just a contextual signal that\n * earlier conversation was compressed.\n *\n * @param discardedCount - Number of messages that were pruned\n * @param hasSummary - Whether a summary was successfully generated\n * @returns Message content string for injection as SystemMessage, or null if no note needed\n */\nexport function buildPostPruneNote(\n  discardedCount: number,\n  hasSummary: boolean\n): string | null {\n  if (discardedCount <= 0) {\n    return null;\n  }\n\n  if (hasSummary) {\n    return (\n      '[Context Compressed] Earlier conversation messages have been summarized above. ' +\n      'For complex remaining work that requires deep analysis, consider delegating to ' +\n      'sub-agents using the \"task\" tool — each gets a fresh context window.'\n    );\n  }\n\n  return (\n    '[Context Compressed] Some earlier conversation messages were removed to maintain context capacity. ' +\n    'For complex remaining work, consider delegating to sub-agents using the \"task\" tool.'\n  );\n}\n\n/**\n * Check whether a tool named \"task\" exists in the agent's tool set.\n *\n * @param tools - Array of tool objects or structured tools\n * @returns Whether the task tool is available\n */\nexport function hasTaskTool(\n  tools: Array<{ name?: string } | unknown> | undefined\n): boolean {\n  if (!tools) {\n    return false;\n  }\n  return tools.some((tool) => {\n    const toolName =\n      typeof tool === 'object' && tool !== null && 'name' in tool\n        ? (tool as { name: string }).name\n        : '';\n    return toolName === 'task';\n  });\n}\n\n/**\n * Extract text content from a BaseMessage, handling both string and\n * array content formats.\n *\n * @param msg - A LangChain BaseMessage\n * @returns Flattened text content\n */\nfunction extractTextContent(msg: BaseMessage): string {\n  if (typeof msg.content === 'string') {\n    return msg.content;\n  }\n  if (Array.isArray(msg.content)) {\n    return msg.content\n      .map((p: unknown) => {\n        const part = p as Record<string, unknown>;\n        return String(part.text ?? part.content ?? '');\n      })\n      .join(' ');\n  }\n  return '';\n}\n"],"names":[],"mappings":"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"}