/** * Context Analytics Utility * * Provides context analytics data for observability/traces. * No console logging - just data structures for event emission. */ import type { BaseMessage } from '@langchain/core/messages'; import type { TokenCounter } from '@/types/run'; /** Context analytics data for traces */ export interface ContextAnalytics { /** Total messages in context */ messageCount: number; /** Total tokens in context */ totalTokens: number; /** Maximum allowed context tokens */ maxContextTokens?: number; /** Instruction/system tokens */ instructionTokens?: number; /** Context utilization percentage (0-100) */ utilizationPercent?: number; /** Breakdown by message type */ breakdown?: Record; } /** * Resolve the message type string from a BaseMessage or message-like object. * * Uses the same duck-typing contract as LangChain's `isBaseMessage`: * prefer `_getType()` (internal), fallback to `getType()` (public). * This handles cross-package BaseMessage instances where `instanceof` * checks may fail but prototype methods are still present. */ function resolveMessageType(msg: BaseMessage): string { if (typeof msg._getType === 'function') { return msg._getType(); } if (typeof msg.getType === 'function') { return msg.getType(); } // Last resort: infer from known LangChain serialization fields return ( ((msg as unknown as Record).type as string) ?? 'unknown' ); } /** * Build context analytics for traces (no logging). * * @param messages - Array of BaseMessage (may originate from different @langchain/core installations) * @param options - Token counting and context budget info */ export function buildContextAnalytics( messages: BaseMessage[], options: { tokenCounter?: TokenCounter; maxContextTokens?: number; instructionTokens?: number; indexTokenCountMap?: Record; } ): ContextAnalytics { const { tokenCounter, maxContextTokens, instructionTokens, indexTokenCountMap, } = options; let totalTokens = 0; const breakdown: Record = {}; for (let i = 0; i < messages.length; i++) { const msg = messages[i]; const type = resolveMessageType(msg); let tokens = 0; if (indexTokenCountMap && indexTokenCountMap[i] != null) { tokens = indexTokenCountMap[i]!; } else if (tokenCounter) { try { tokens = tokenCounter(msg); } catch { const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content); tokens = Math.ceil(content.length / 4); } } totalTokens += tokens; if (!breakdown[type]) { breakdown[type] = { tokens: 0, percent: 0 }; } breakdown[type].tokens += tokens; } for (const type of Object.keys(breakdown)) { breakdown[type].percent = totalTokens > 0 ? Math.round((breakdown[type].tokens / totalTokens) * 1000) / 10 : 0; } let utilizationPercent: number | undefined; if (maxContextTokens && maxContextTokens > 0) { utilizationPercent = Math.round((totalTokens / maxContextTokens) * 1000) / 10; } return { messageCount: messages.length, totalTokens, maxContextTokens, instructionTokens, utilizationPercent, breakdown, }; }