Download src/utils/analyzeContext.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 42.9 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/analyzeContext.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/analyzeContext.ts
-
curl -L -o analyzeContext.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/analyzeContext.ts
42.9 kB
| import { feature } from 'bun:bundle' | |
| import type { Anthropic } from '@anthropic-ai/sdk' | |
| import { | |
| getSystemPrompt, | |
| SYSTEM_PROMPT_DYNAMIC_BOUNDARY, | |
| } from 'src/constants/prompts.js' | |
| import { microcompactMessages } from 'src/services/compact/microCompact.js' | |
| import { getSdkBetas } from '../bootstrap/state.js' | |
| import { getCommandName } from '../commands.js' | |
| import { getSystemContext } from '../context.js' | |
| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' | |
| import { | |
| AUTOCOMPACT_BUFFER_TOKENS, | |
| getEffectiveContextWindowSize, | |
| isAutoCompactEnabled, | |
| MANUAL_COMPACT_BUFFER_TOKENS, | |
| } from '../services/compact/autoCompact.js' | |
| import { | |
| countMessagesTokensWithAPI, | |
| countTokensViaHaikuFallback, | |
| roughTokenCountEstimation, | |
| } from '../services/tokenEstimation.js' | |
| import { estimateSkillFrontmatterTokens } from '../skills/loadSkillsDir.js' | |
| import { | |
| findToolByName, | |
| type Tool, | |
| type ToolPermissionContext, | |
| type Tools, | |
| type ToolUseContext, | |
| toolMatchesName, | |
| } from '../Tool.js' | |
| import type { | |
| AgentDefinition, | |
| AgentDefinitionsResult, | |
| } from '../tools/AgentTool/loadAgentsDir.js' | |
| import { SKILL_TOOL_NAME } from '../tools/SkillTool/constants.js' | |
| import { | |
| getLimitedSkillToolCommands, | |
| getSkillToolInfo as getSlashCommandInfo, | |
| } from '../tools/SkillTool/prompt.js' | |
| import type { | |
| AssistantMessage, | |
| AttachmentMessage, | |
| Message, | |
| NormalizedAssistantMessage, | |
| NormalizedUserMessage, | |
| UserMessage, | |
| } from '../types/message.js' | |
| import { toolToAPISchema } from './api.js' | |
| import { filterInjectedMemoryFiles, getMemoryFiles } from './claudemd.js' | |
| import { getContextWindowForModel } from './context.js' | |
| import { getCwd } from './cwd.js' | |
| import { logForDebugging } from './debug.js' | |
| import { isEnvTruthy } from './envUtils.js' | |
| import { errorMessage, toError } from './errors.js' | |
| import { logError } from './log.js' | |
| import { normalizeMessagesForAPI } from './messages.js' | |
| import { getRuntimeMainLoopModel } from './model/model.js' | |
| import type { SettingSource } from './settings/constants.js' | |
| import { jsonStringify } from './slowOperations.js' | |
| import { buildEffectiveSystemPrompt } from './systemPrompt.js' | |
| import type { Theme } from './theme.js' | |
| import { getCurrentUsage } from './tokens.js' | |
| const RESERVED_CATEGORY_NAME = 'Autocompact buffer' | |
| const MANUAL_COMPACT_BUFFER_NAME = 'Compact buffer' | |
| /** | |
| * Fixed token overhead added by the API when tools are present. | |
| * The API adds a tool prompt preamble (~500 tokens) once per API call when tools are present. | |
| * When we count tools individually via the token counting API, each call includes this overhead, | |
| * leading to N × overhead instead of 1 × overhead for N tools. | |
| * We subtract this overhead from per-tool counts to show accurate tool content sizes. | |
| */ | |
| export const TOOL_TOKEN_COUNT_OVERHEAD = 500 | |
| async function countTokensWithFallback( | |
| messages: Anthropic.Beta.Messages.BetaMessageParam[], | |
| tools: Anthropic.Beta.Messages.BetaToolUnion[], | |
| ): Promise<number | null> { | |
| try { | |
| const result = await countMessagesTokensWithAPI(messages, tools) | |
| if (result !== null) { | |
| return result | |
| } | |
| logForDebugging( | |
| `countTokensWithFallback: API returned null, trying haiku fallback (${tools.length} tools)`, | |
| ) | |
| } catch (err) { | |
| logForDebugging(`countTokensWithFallback: API failed: ${errorMessage(err)}`) | |
| logError(err) | |
| } | |
| try { | |
| const fallbackResult = await countTokensViaHaikuFallback(messages, tools) | |
| if (fallbackResult === null) { | |
| logForDebugging( | |
| `countTokensWithFallback: haiku fallback also returned null (${tools.length} tools)`, | |
| ) | |
| } | |
| return fallbackResult | |
| } catch (err) { | |
| logForDebugging( | |
| `countTokensWithFallback: haiku fallback failed: ${errorMessage(err)}`, | |
| ) | |
| logError(err) | |
| return null | |
| } | |
| } | |
| interface ContextCategory { | |
| name: string | |
| tokens: number | |
| color: keyof Theme | |
| /** When true, these tokens are deferred and don't count toward context usage */ | |
| isDeferred?: boolean | |
| } | |
| interface GridSquare { | |
| color: keyof Theme | |
| isFilled: boolean | |
| categoryName: string | |
| tokens: number | |
| percentage: number | |
| squareFullness: number // 0-1 representing how full this individual square is | |
| } | |
| interface MemoryFile { | |
| path: string | |
| type: string | |
| tokens: number | |
| } | |
| interface McpTool { | |
| name: string | |
| serverName: string | |
| tokens: number | |
| isLoaded?: boolean | |
| } | |
| export interface DeferredBuiltinTool { | |
| name: string | |
| tokens: number | |
| isLoaded: boolean | |
| } | |
| export interface SystemToolDetail { | |
| name: string | |
| tokens: number | |
| } | |
| export interface SystemPromptSectionDetail { | |
| name: string | |
| tokens: number | |
| } | |
| interface Agent { | |
| agentType: string | |
| source: SettingSource | 'built-in' | 'plugin' | |
| tokens: number | |
| } | |
| interface SlashCommandInfo { | |
| readonly totalCommands: number | |
| readonly includedCommands: number | |
| readonly tokens: number | |
| } | |
| /** Individual skill detail for context display */ | |
| interface SkillFrontmatter { | |
| name: string | |
| source: SettingSource | 'plugin' | |
| tokens: number | |
| } | |
| /** | |
| * Information about skills included in the context window. | |
| */ | |
| interface SkillInfo { | |
| /** Total number of available skills */ | |
| readonly totalSkills: number | |
| /** Number of skills included within token budget */ | |
| readonly includedSkills: number | |
| /** Total tokens consumed by skills */ | |
| readonly tokens: number | |
| /** Individual skill details */ | |
| readonly skillFrontmatter: SkillFrontmatter[] | |
| } | |
| export interface ContextData { | |
| readonly categories: ContextCategory[] | |
| readonly totalTokens: number | |
| readonly maxTokens: number | |
| readonly rawMaxTokens: number | |
| readonly percentage: number | |
| readonly gridRows: GridSquare[][] | |
| readonly model: string | |
| readonly memoryFiles: MemoryFile[] | |
| readonly mcpTools: McpTool[] | |
| /** Ant-only: per-tool breakdown of deferred built-in tools */ | |
| readonly deferredBuiltinTools?: DeferredBuiltinTool[] | |
| /** Ant-only: per-tool breakdown of always-loaded built-in tools */ | |
| readonly systemTools?: SystemToolDetail[] | |
| /** Ant-only: per-section breakdown of system prompt */ | |
| readonly systemPromptSections?: SystemPromptSectionDetail[] | |
| readonly agents: Agent[] | |
| readonly slashCommands?: SlashCommandInfo | |
| /** Skill statistics */ | |
| readonly skills?: SkillInfo | |
| readonly autoCompactThreshold?: number | |
| readonly isAutoCompactEnabled: boolean | |
| messageBreakdown?: { | |
| toolCallTokens: number | |
| toolResultTokens: number | |
| attachmentTokens: number | |
| assistantMessageTokens: number | |
| userMessageTokens: number | |
| toolCallsByType: Array<{ | |
| name: string | |
| callTokens: number | |
| resultTokens: number | |
| }> | |
| attachmentsByType: Array<{ name: string; tokens: number }> | |
| } | |
| /** Actual token usage from last API response (if available) */ | |
| readonly apiUsage: { | |
| input_tokens: number | |
| output_tokens: number | |
| cache_creation_input_tokens: number | |
| cache_read_input_tokens: number | |
| } | null | |
| } | |
| export async function countToolDefinitionTokens( | |
| tools: Tools, | |
| getToolPermissionContext: () => Promise<ToolPermissionContext>, | |
| agentInfo: AgentDefinitionsResult | null, | |
| model?: string, | |
| ): Promise<number> { | |
| const toolSchemas = await Promise.all( | |
| tools.map(tool => | |
| toolToAPISchema(tool, { | |
| getToolPermissionContext, | |
| tools, | |
| agents: agentInfo?.activeAgents ?? [], | |
| model, | |
| }), | |
| ), | |
| ) | |
| const result = await countTokensWithFallback([], toolSchemas) | |
| if (result === null || result === 0) { | |
| const toolNames = tools.map(t => t.name).join(', ') | |
| logForDebugging( | |
| `countToolDefinitionTokens returned ${result} for ${tools.length} tools: ${toolNames.slice(0, 100)}${toolNames.length > 100 ? '...' : ''}`, | |
| ) | |
| } | |
| return result ?? 0 | |
| } | |
| /** Extract a human-readable name from a system prompt section's content */ | |
| function extractSectionName(content: string): string { | |
| // Try to find first markdown heading | |
| const headingMatch = content.match(/^#+\s+(.+)$/m) | |
| if (headingMatch) { | |
| return headingMatch[1]!.trim() | |
| } | |
| // Fall back to a truncated preview of the first non-empty line | |
| const firstLine = content.split('\n').find(l => l.trim().length > 0) ?? '' | |
| return firstLine.length > 40 ? firstLine.slice(0, 40) + '…' : firstLine | |
| } | |
| async function countSystemTokens( | |
| effectiveSystemPrompt: readonly string[], | |
| ): Promise<{ | |
| systemPromptTokens: number | |
| systemPromptSections: SystemPromptSectionDetail[] | |
| }> { | |
| // Get system context (gitStatus, etc.) which is always included | |
| const systemContext = await getSystemContext() | |
| // Build named entries: system prompt parts + system context values | |
| // Skip empty strings and the global-cache boundary marker | |
| const namedEntries: Array<{ name: string; content: string }> = [ | |
| ...effectiveSystemPrompt | |
| .filter( | |
| content => | |
| content.length > 0 && content !== SYSTEM_PROMPT_DYNAMIC_BOUNDARY, | |
| ) | |
| .map(content => ({ name: extractSectionName(content), content })), | |
| ...Object.entries(systemContext) | |
| .filter(([, content]) => content.length > 0) | |
| .map(([name, content]) => ({ name, content })), | |
| ] | |
| if (namedEntries.length < 1) { | |
| return { systemPromptTokens: 0, systemPromptSections: [] } | |
| } | |
| const systemTokenCounts = await Promise.all( | |
| namedEntries.map(({ content }) => | |
| countTokensWithFallback([{ role: 'user', content }], []), | |
| ), | |
| ) | |
| const systemPromptSections: SystemPromptSectionDetail[] = namedEntries.map( | |
| (entry, i) => ({ | |
| name: entry.name, | |
| tokens: systemTokenCounts[i] || 0, | |
| }), | |
| ) | |
| const systemPromptTokens = systemTokenCounts.reduce( | |
| (sum: number, tokens) => sum + (tokens || 0), | |
| 0, | |
| ) | |
| return { systemPromptTokens, systemPromptSections } | |
| } | |
| async function countMemoryFileTokens(): Promise<{ | |
| memoryFileDetails: MemoryFile[] | |
| claudeMdTokens: number | |
| }> { | |
| // Simple mode disables CLAUDE.md loading, so don't report tokens for them | |
| if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { | |
| return { memoryFileDetails: [], claudeMdTokens: 0 } | |
| } | |
| const memoryFilesData = filterInjectedMemoryFiles(await getMemoryFiles()) | |
| const memoryFileDetails: MemoryFile[] = [] | |
| let claudeMdTokens = 0 | |
| if (memoryFilesData.length < 1) { | |
| return { | |
| memoryFileDetails: [], | |
| claudeMdTokens: 0, | |
| } | |
| } | |
| const claudeMdTokenCounts = await Promise.all( | |
| memoryFilesData.map(async file => { | |
| const tokens = await countTokensWithFallback( | |
| [{ role: 'user', content: file.content }], | |
| [], | |
| ) | |
| return { file, tokens: tokens || 0 } | |
| }), | |
| ) | |
| for (const { file, tokens } of claudeMdTokenCounts) { | |
| claudeMdTokens += tokens | |
| memoryFileDetails.push({ | |
| path: file.path, | |
| type: file.type, | |
| tokens, | |
| }) | |
| } | |
| return { claudeMdTokens, memoryFileDetails } | |
| } | |
| async function countBuiltInToolTokens( | |
| tools: Tools, | |
| getToolPermissionContext: () => Promise<ToolPermissionContext>, | |
| agentInfo: AgentDefinitionsResult | null, | |
| model?: string, | |
| messages?: Message[], | |
| ): Promise<{ | |
| builtInToolTokens: number | |
| deferredBuiltinDetails: DeferredBuiltinTool[] | |
| deferredBuiltinTokens: number | |
| systemToolDetails: SystemToolDetail[] | |
| }> { | |
| const builtInTools = tools.filter(tool => !tool.isMcp) | |
| if (builtInTools.length < 1) { | |
| return { | |
| builtInToolTokens: 0, | |
| deferredBuiltinDetails: [], | |
| deferredBuiltinTokens: 0, | |
| systemToolDetails: [], | |
| } | |
| } | |
| // Check if tool search is enabled | |
| const { isToolSearchEnabled } = await import('./toolSearch.js') | |
| const { isDeferredTool } = await import('../tools/ToolSearchTool/prompt.js') | |
| const isDeferred = await isToolSearchEnabled( | |
| model ?? '', | |
| tools, | |
| getToolPermissionContext, | |
| agentInfo?.activeAgents ?? [], | |
| 'analyzeBuiltIn', | |
| ) | |
| // Separate always-loaded and deferred builtin tools using dynamic isDeferredTool check | |
| const alwaysLoadedTools = builtInTools.filter(t => !isDeferredTool(t)) | |
| const deferredBuiltinTools = builtInTools.filter(t => isDeferredTool(t)) | |
| // Count always-loaded tools | |
| const alwaysLoadedTokens = | |
| alwaysLoadedTools.length > 0 | |
| ? await countToolDefinitionTokens( | |
| alwaysLoadedTools, | |
| getToolPermissionContext, | |
| agentInfo, | |
| model, | |
| ) | |
| : 0 | |
| // Build per-tool breakdown for always-loaded tools (ant-only, proportional | |
| // split of the bulk count based on rough schema size estimation). Excludes | |
| // SkillTool since its tokens are shown in the separate Skills category. | |
| let systemToolDetails: SystemToolDetail[] = [] | |
| if (process.env.USER_TYPE === 'ant') { | |
| const toolsForBreakdown = alwaysLoadedTools.filter( | |
| t => !toolMatchesName(t, SKILL_TOOL_NAME), | |
| ) | |
| if (toolsForBreakdown.length > 0) { | |
| const estimates = toolsForBreakdown.map(t => | |
| roughTokenCountEstimation(jsonStringify(t.inputSchema ?? {})), | |
| ) | |
| const estimateTotal = estimates.reduce((s, e) => s + e, 0) || 1 | |
| const distributable = Math.max( | |
| 0, | |
| alwaysLoadedTokens - TOOL_TOKEN_COUNT_OVERHEAD, | |
| ) | |
| systemToolDetails = toolsForBreakdown | |
| .map((t, i) => ({ | |
| name: t.name, | |
| tokens: Math.round((estimates[i]! / estimateTotal) * distributable), | |
| })) | |
| .sort((a, b) => b.tokens - a.tokens) | |
| } | |
| } | |
| // Count deferred builtin tools individually for details | |
| const deferredBuiltinDetails: DeferredBuiltinTool[] = [] | |
| let loadedDeferredTokens = 0 | |
| let totalDeferredTokens = 0 | |
| if (deferredBuiltinTools.length > 0 && isDeferred) { | |
| // Find which deferred tools have been used in messages | |
| const loadedToolNames = new Set<string>() | |
| if (messages) { | |
| const deferredToolNameSet = new Set(deferredBuiltinTools.map(t => t.name)) | |
| for (const msg of messages) { | |
| if (msg.type === 'assistant') { | |
| for (const block of msg.message.content) { | |
| if ( | |
| 'type' in block && | |
| block.type === 'tool_use' && | |
| 'name' in block && | |
| typeof block.name === 'string' && | |
| deferredToolNameSet.has(block.name) | |
| ) { | |
| loadedToolNames.add(block.name) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Count each deferred tool | |
| const tokensByTool = await Promise.all( | |
| deferredBuiltinTools.map(t => | |
| countToolDefinitionTokens( | |
| [t], | |
| getToolPermissionContext, | |
| agentInfo, | |
| model, | |
| ), | |
| ), | |
| ) | |
| for (const [i, tool] of deferredBuiltinTools.entries()) { | |
| const tokens = Math.max( | |
| 0, | |
| (tokensByTool[i] || 0) - TOOL_TOKEN_COUNT_OVERHEAD, | |
| ) | |
| const isLoaded = loadedToolNames.has(tool.name) | |
| deferredBuiltinDetails.push({ | |
| name: tool.name, | |
| tokens, | |
| isLoaded, | |
| }) | |
| totalDeferredTokens += tokens | |
| if (isLoaded) { | |
| loadedDeferredTokens += tokens | |
| } | |
| } | |
| } else if (deferredBuiltinTools.length > 0) { | |
| // Tool search not enabled - count deferred tools as regular | |
| const deferredTokens = await countToolDefinitionTokens( | |
| deferredBuiltinTools, | |
| getToolPermissionContext, | |
| agentInfo, | |
| model, | |
| ) | |
| return { | |
| builtInToolTokens: alwaysLoadedTokens + deferredTokens, | |
| deferredBuiltinDetails: [], | |
| deferredBuiltinTokens: 0, | |
| systemToolDetails, | |
| } | |
| } | |
| return { | |
| // When deferred, only count always-loaded tools + any loaded deferred tools | |
| builtInToolTokens: alwaysLoadedTokens + loadedDeferredTokens, | |
| deferredBuiltinDetails, | |
| deferredBuiltinTokens: totalDeferredTokens - loadedDeferredTokens, | |
| systemToolDetails, | |
| } | |
| } | |
| function findSkillTool(tools: Tools): Tool | undefined { | |
| return findToolByName(tools, SKILL_TOOL_NAME) | |
| } | |
| async function countSlashCommandTokens( | |
| tools: Tools, | |
| getToolPermissionContext: () => Promise<ToolPermissionContext>, | |
| agentInfo: AgentDefinitionsResult | null, | |
| ): Promise<{ | |
| slashCommandTokens: number | |
| commandInfo: { totalCommands: number; includedCommands: number } | |
| }> { | |
| const info = await getSlashCommandInfo(getCwd()) | |
| const slashCommandTool = findSkillTool(tools) | |
| if (!slashCommandTool) { | |
| return { | |
| slashCommandTokens: 0, | |
| commandInfo: { totalCommands: 0, includedCommands: 0 }, | |
| } | |
| } | |
| const slashCommandTokens = await countToolDefinitionTokens( | |
| [slashCommandTool], | |
| getToolPermissionContext, | |
| agentInfo, | |
| ) | |
| return { | |
| slashCommandTokens, | |
| commandInfo: { | |
| totalCommands: info.totalCommands, | |
| includedCommands: info.includedCommands, | |
| }, | |
| } | |
| } | |
| async function countSkillTokens( | |
| tools: Tools, | |
| getToolPermissionContext: () => Promise<ToolPermissionContext>, | |
| agentInfo: AgentDefinitionsResult | null, | |
| ): Promise<{ | |
| skillTokens: number | |
| skillInfo: { | |
| totalSkills: number | |
| includedSkills: number | |
| skillFrontmatter: SkillFrontmatter[] | |
| } | |
| }> { | |
| try { | |
| const skills = await getLimitedSkillToolCommands(getCwd()) | |
| const slashCommandTool = findSkillTool(tools) | |
| if (!slashCommandTool) { | |
| return { | |
| skillTokens: 0, | |
| skillInfo: { totalSkills: 0, includedSkills: 0, skillFrontmatter: [] }, | |
| } | |
| } | |
| // NOTE: This counts the entire SlashCommandTool (which includes both commands AND skills). | |
| // This is the same tool counted by countSlashCommandTokens(), but we track it separately | |
| // here for display purposes. These tokens should NOT be added to context categories | |
| // to avoid double-counting. | |
| const skillTokens = await countToolDefinitionTokens( | |
| [slashCommandTool], | |
| getToolPermissionContext, | |
| agentInfo, | |
| ) | |
| // Calculate per-skill token estimates based on frontmatter only | |
| // (name, description, whenToUse) since full content is only loaded on invocation | |
| const skillFrontmatter: SkillFrontmatter[] = skills.map(skill => ({ | |
| name: getCommandName(skill), | |
| source: (skill.type === 'prompt' ? skill.source : 'plugin') as | |
| | SettingSource | |
| | 'plugin', | |
| tokens: estimateSkillFrontmatterTokens(skill), | |
| })) | |
| return { | |
| skillTokens, | |
| skillInfo: { | |
| totalSkills: skills.length, | |
| includedSkills: skills.length, | |
| skillFrontmatter, | |
| }, | |
| } | |
| } catch (error) { | |
| logError(toError(error)) | |
| // Return zero values rather than failing the entire context analysis | |
| return { | |
| skillTokens: 0, | |
| skillInfo: { totalSkills: 0, includedSkills: 0, skillFrontmatter: [] }, | |
| } | |
| } | |
| } | |
| export async function countMcpToolTokens( | |
| tools: Tools, | |
| getToolPermissionContext: () => Promise<ToolPermissionContext>, | |
| agentInfo: AgentDefinitionsResult | null, | |
| model: string, | |
| messages?: Message[], | |
| ): Promise<{ | |
| mcpToolTokens: number | |
| mcpToolDetails: McpTool[] | |
| deferredToolTokens: number | |
| loadedMcpToolNames: Set<string> | |
| }> { | |
| const mcpTools = tools.filter(tool => tool.isMcp) | |
| const mcpToolDetails: McpTool[] = [] | |
| // Single bulk API call for all MCP tools (instead of N individual calls) | |
| const totalTokensRaw = await countToolDefinitionTokens( | |
| mcpTools, | |
| getToolPermissionContext, | |
| agentInfo, | |
| model, | |
| ) | |
| // Subtract the single overhead since we made one bulk call | |
| const totalTokens = Math.max( | |
| 0, | |
| (totalTokensRaw || 0) - TOOL_TOKEN_COUNT_OVERHEAD, | |
| ) | |
| // Estimate per-tool proportions for display using local estimation. | |
| // Include name + description + input schema to match what toolToAPISchema | |
| // sends — otherwise tools with similar schemas but different descriptions | |
| // get identical counts (MCP tools share the same base Zod inputSchema). | |
| const estimates = await Promise.all( | |
| mcpTools.map(async t => | |
| roughTokenCountEstimation( | |
| jsonStringify({ | |
| name: t.name, | |
| description: await t.prompt({ | |
| getToolPermissionContext, | |
| tools, | |
| agents: agentInfo?.activeAgents ?? [], | |
| }), | |
| input_schema: t.inputJSONSchema ?? {}, | |
| }), | |
| ), | |
| ), | |
| ) | |
| const estimateTotal = estimates.reduce((s, e) => s + e, 0) || 1 | |
| const mcpToolTokensByTool = estimates.map(e => | |
| Math.round((e / estimateTotal) * totalTokens), | |
| ) | |
| // Check if tool search is enabled - if so, MCP tools are deferred | |
| // isToolSearchEnabled handles threshold calculation internally for TstAuto mode | |
| const { isToolSearchEnabled } = await import('./toolSearch.js') | |
| const { isDeferredTool } = await import('../tools/ToolSearchTool/prompt.js') | |
| const isDeferred = await isToolSearchEnabled( | |
| model, | |
| tools, | |
| getToolPermissionContext, | |
| agentInfo?.activeAgents ?? [], | |
| 'analyzeMcp', | |
| ) | |
| // Find MCP tools that have been used in messages (loaded via ToolSearchTool) | |
| const loadedMcpToolNames = new Set<string>() | |
| if (isDeferred && messages) { | |
| const mcpToolNameSet = new Set(mcpTools.map(t => t.name)) | |
| for (const msg of messages) { | |
| if (msg.type === 'assistant') { | |
| for (const block of msg.message.content) { | |
| if ( | |
| 'type' in block && | |
| block.type === 'tool_use' && | |
| 'name' in block && | |
| typeof block.name === 'string' && | |
| mcpToolNameSet.has(block.name) | |
| ) { | |
| loadedMcpToolNames.add(block.name) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Build tool details with isLoaded flag | |
| for (const [i, tool] of mcpTools.entries()) { | |
| mcpToolDetails.push({ | |
| name: tool.name, | |
| serverName: tool.name.split('__')[1] || 'unknown', | |
| tokens: mcpToolTokensByTool[i]!, | |
| isLoaded: loadedMcpToolNames.has(tool.name) || !isDeferredTool(tool), | |
| }) | |
| } | |
| // Calculate loaded vs deferred tokens | |
| let loadedTokens = 0 | |
| let deferredTokens = 0 | |
| for (const detail of mcpToolDetails) { | |
| if (detail.isLoaded) { | |
| loadedTokens += detail.tokens | |
| } else if (isDeferred) { | |
| deferredTokens += detail.tokens | |
| } | |
| } | |
| return { | |
| // When deferred but some tools are loaded, count loaded tokens | |
| mcpToolTokens: isDeferred ? loadedTokens : totalTokens, | |
| mcpToolDetails, | |
| // Track deferred tokens separately for display | |
| deferredToolTokens: deferredTokens, | |
| loadedMcpToolNames, | |
| } | |
| } | |
| async function countCustomAgentTokens(agentDefinitions: { | |
| activeAgents: AgentDefinition[] | |
| }): Promise<{ | |
| agentTokens: number | |
| agentDetails: Agent[] | |
| }> { | |
| const customAgents = agentDefinitions.activeAgents.filter( | |
| a => a.source !== 'built-in', | |
| ) | |
| const agentDetails: Agent[] = [] | |
| let agentTokens = 0 | |
| const tokenCounts = await Promise.all( | |
| customAgents.map(agent => | |
| countTokensWithFallback( | |
| [ | |
| { | |
| role: 'user', | |
| content: [agent.agentType, agent.whenToUse].join(' '), | |
| }, | |
| ], | |
| [], | |
| ), | |
| ), | |
| ) | |
| for (const [i, agent] of customAgents.entries()) { | |
| const tokens = tokenCounts[i] || 0 | |
| agentTokens += tokens || 0 | |
| agentDetails.push({ | |
| agentType: agent.agentType, | |
| source: agent.source, | |
| tokens: tokens || 0, | |
| }) | |
| } | |
| return { agentTokens, agentDetails } | |
| } | |
| type MessageBreakdown = { | |
| totalTokens: number | |
| toolCallTokens: number | |
| toolResultTokens: number | |
| attachmentTokens: number | |
| assistantMessageTokens: number | |
| userMessageTokens: number | |
| toolCallsByType: Map<string, number> | |
| toolResultsByType: Map<string, number> | |
| attachmentsByType: Map<string, number> | |
| } | |
| function processAssistantMessage( | |
| msg: AssistantMessage | NormalizedAssistantMessage, | |
| breakdown: MessageBreakdown, | |
| ): void { | |
| // Process each content block individually | |
| for (const block of msg.message.content) { | |
| const blockStr = jsonStringify(block) | |
| const blockTokens = roughTokenCountEstimation(blockStr) | |
| if ('type' in block && block.type === 'tool_use') { | |
| breakdown.toolCallTokens += blockTokens | |
| const toolName = ('name' in block ? block.name : undefined) || 'unknown' | |
| breakdown.toolCallsByType.set( | |
| toolName, | |
| (breakdown.toolCallsByType.get(toolName) || 0) + blockTokens, | |
| ) | |
| } else { | |
| // Text blocks or other non-tool content | |
| breakdown.assistantMessageTokens += blockTokens | |
| } | |
| } | |
| } | |
| function processUserMessage( | |
| msg: UserMessage | NormalizedUserMessage, | |
| breakdown: MessageBreakdown, | |
| toolUseIdToName: Map<string, string>, | |
| ): void { | |
| // Handle both string and array content | |
| if (typeof msg.message.content === 'string') { | |
| // Simple string content | |
| const tokens = roughTokenCountEstimation(msg.message.content) | |
| breakdown.userMessageTokens += tokens | |
| return | |
| } | |
| // Process each content block individually | |
| for (const block of msg.message.content) { | |
| const blockStr = jsonStringify(block) | |
| const blockTokens = roughTokenCountEstimation(blockStr) | |
| if ('type' in block && block.type === 'tool_result') { | |
| breakdown.toolResultTokens += blockTokens | |
| const toolUseId = 'tool_use_id' in block ? block.tool_use_id : undefined | |
| const toolName = | |
| (toolUseId ? toolUseIdToName.get(toolUseId) : undefined) || 'unknown' | |
| breakdown.toolResultsByType.set( | |
| toolName, | |
| (breakdown.toolResultsByType.get(toolName) || 0) + blockTokens, | |
| ) | |
| } else { | |
| // Text blocks or other non-tool content | |
| breakdown.userMessageTokens += blockTokens | |
| } | |
| } | |
| } | |
| function processAttachment( | |
| msg: AttachmentMessage, | |
| breakdown: MessageBreakdown, | |
| ): void { | |
| const contentStr = jsonStringify(msg.attachment) | |
| const tokens = roughTokenCountEstimation(contentStr) | |
| breakdown.attachmentTokens += tokens | |
| const attachType = msg.attachment.type || 'unknown' | |
| breakdown.attachmentsByType.set( | |
| attachType, | |
| (breakdown.attachmentsByType.get(attachType) || 0) + tokens, | |
| ) | |
| } | |
| async function approximateMessageTokens( | |
| messages: Message[], | |
| ): Promise<MessageBreakdown> { | |
| const microcompactResult = await microcompactMessages(messages) | |
| // Initialize tracking | |
| const breakdown: MessageBreakdown = { | |
| totalTokens: 0, | |
| toolCallTokens: 0, | |
| toolResultTokens: 0, | |
| attachmentTokens: 0, | |
| assistantMessageTokens: 0, | |
| userMessageTokens: 0, | |
| toolCallsByType: new Map<string, number>(), | |
| toolResultsByType: new Map<string, number>(), | |
| attachmentsByType: new Map<string, number>(), | |
| } | |
| // Build a map of tool_use_id to tool_name for easier lookup | |
| const toolUseIdToName = new Map<string, string>() | |
| for (const msg of microcompactResult.messages) { | |
| if (msg.type === 'assistant') { | |
| for (const block of msg.message.content) { | |
| if ('type' in block && block.type === 'tool_use') { | |
| const toolUseId = 'id' in block ? block.id : undefined | |
| const toolName = | |
| ('name' in block ? block.name : undefined) || 'unknown' | |
| if (toolUseId) { | |
| toolUseIdToName.set(toolUseId, toolName) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Process each message for detailed breakdown | |
| for (const msg of microcompactResult.messages) { | |
| if (msg.type === 'assistant') { | |
| processAssistantMessage(msg, breakdown) | |
| } else if (msg.type === 'user') { | |
| processUserMessage(msg, breakdown, toolUseIdToName) | |
| } else if (msg.type === 'attachment') { | |
| processAttachment(msg, breakdown) | |
| } | |
| } | |
| // Calculate total tokens using the API for accuracy | |
| const approximateMessageTokens = await countTokensWithFallback( | |
| normalizeMessagesForAPI(microcompactResult.messages).map(_ => { | |
| if (_.type === 'assistant') { | |
| return { | |
| // Important: strip out fields like id, etc. -- the counting API errors if they're present | |
| role: 'assistant', | |
| content: _.message.content, | |
| } | |
| } | |
| return _.message | |
| }), | |
| [], | |
| ) | |
| breakdown.totalTokens = approximateMessageTokens ?? 0 | |
| return breakdown | |
| } | |
| export async function analyzeContextUsage( | |
| messages: Message[], | |
| model: string, | |
| getToolPermissionContext: () => Promise<ToolPermissionContext>, | |
| tools: Tools, | |
| agentDefinitions: AgentDefinitionsResult, | |
| terminalWidth?: number, | |
| toolUseContext?: Pick<ToolUseContext, 'options'>, | |
| mainThreadAgentDefinition?: AgentDefinition, | |
| /** Original messages before microcompact, used to extract API usage */ | |
| originalMessages?: Message[], | |
| ): Promise<ContextData> { | |
| const runtimeModel = getRuntimeMainLoopModel({ | |
| permissionMode: (await getToolPermissionContext()).mode, | |
| mainLoopModel: model, | |
| }) | |
| // Get context window size | |
| const contextWindow = getContextWindowForModel(runtimeModel, getSdkBetas()) | |
| // Build the effective system prompt using the shared utility | |
| const defaultSystemPrompt = await getSystemPrompt(tools, runtimeModel) | |
| const effectiveSystemPrompt = buildEffectiveSystemPrompt({ | |
| mainThreadAgentDefinition, | |
| toolUseContext: toolUseContext ?? { | |
| options: {} as ToolUseContext['options'], | |
| }, | |
| customSystemPrompt: toolUseContext?.options.customSystemPrompt, | |
| defaultSystemPrompt, | |
| appendSystemPrompt: toolUseContext?.options.appendSystemPrompt, | |
| }) | |
| // Critical operations that should not fail due to skills | |
| const [ | |
| { systemPromptTokens, systemPromptSections }, | |
| { claudeMdTokens, memoryFileDetails }, | |
| { | |
| builtInToolTokens, | |
| deferredBuiltinDetails, | |
| deferredBuiltinTokens, | |
| systemToolDetails, | |
| }, | |
| { mcpToolTokens, mcpToolDetails, deferredToolTokens }, | |
| { agentTokens, agentDetails }, | |
| { slashCommandTokens, commandInfo }, | |
| messageBreakdown, | |
| ] = await Promise.all([ | |
| countSystemTokens(effectiveSystemPrompt), | |
| countMemoryFileTokens(), | |
| countBuiltInToolTokens( | |
| tools, | |
| getToolPermissionContext, | |
| agentDefinitions, | |
| runtimeModel, | |
| messages, | |
| ), | |
| countMcpToolTokens( | |
| tools, | |
| getToolPermissionContext, | |
| agentDefinitions, | |
| runtimeModel, | |
| messages, | |
| ), | |
| countCustomAgentTokens(agentDefinitions), | |
| countSlashCommandTokens(tools, getToolPermissionContext, agentDefinitions), | |
| approximateMessageTokens(messages), | |
| ]) | |
| // Count skills separately with error isolation | |
| const skillResult = await countSkillTokens( | |
| tools, | |
| getToolPermissionContext, | |
| agentDefinitions, | |
| ) | |
| const skillInfo = skillResult.skillInfo | |
| // Use sum of individual skill token estimates (matches what's shown in details) | |
| // rather than skillResult.skillTokens which includes tool schema overhead | |
| const skillFrontmatterTokens = skillInfo.skillFrontmatter.reduce( | |
| (sum, skill) => sum + skill.tokens, | |
| 0, | |
| ) | |
| const messageTokens = messageBreakdown.totalTokens | |
| // Check if autocompact is enabled and calculate threshold | |
| const isAutoCompact = isAutoCompactEnabled() | |
| const autoCompactThreshold = isAutoCompact | |
| ? getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS | |
| : undefined | |
| // Create categories | |
| const cats: ContextCategory[] = [] | |
| // System prompt is always shown first (fixed overhead) | |
| if (systemPromptTokens > 0) { | |
| cats.push({ | |
| name: 'System prompt', | |
| tokens: systemPromptTokens, | |
| color: 'promptBorder', | |
| }) | |
| } | |
| // Built-in tools right after system prompt (skills shown separately below) | |
| // Ant users get a per-tool breakdown via systemToolDetails | |
| const systemToolsTokens = builtInToolTokens - skillFrontmatterTokens | |
| if (systemToolsTokens > 0) { | |
| cats.push({ | |
| name: | |
| process.env.USER_TYPE === 'ant' | |
| ? '[ANT-ONLY] System tools' | |
| : 'System tools', | |
| tokens: systemToolsTokens, | |
| color: 'inactive', | |
| }) | |
| } | |
| // MCP tools after system tools | |
| if (mcpToolTokens > 0) { | |
| cats.push({ | |
| name: 'MCP tools', | |
| tokens: mcpToolTokens, | |
| color: 'cyan_FOR_SUBAGENTS_ONLY', | |
| }) | |
| } | |
| // Show deferred MCP tools (when tool search is enabled) | |
| // These don't count toward context usage but we show them for visibility | |
| if (deferredToolTokens > 0) { | |
| cats.push({ | |
| name: 'MCP tools (deferred)', | |
| tokens: deferredToolTokens, | |
| color: 'inactive', | |
| isDeferred: true, | |
| }) | |
| } | |
| // Show deferred builtin tools (when tool search is enabled) | |
| if (deferredBuiltinTokens > 0) { | |
| cats.push({ | |
| name: 'System tools (deferred)', | |
| tokens: deferredBuiltinTokens, | |
| color: 'inactive', | |
| isDeferred: true, | |
| }) | |
| } | |
| // Custom agents after MCP tools | |
| if (agentTokens > 0) { | |
| cats.push({ | |
| name: 'Custom agents', | |
| tokens: agentTokens, | |
| color: 'permission', | |
| }) | |
| } | |
| // Memory files after custom agents | |
| if (claudeMdTokens > 0) { | |
| cats.push({ | |
| name: 'Memory files', | |
| tokens: claudeMdTokens, | |
| color: 'claude', | |
| }) | |
| } | |
| // Skills after memory files | |
| if (skillFrontmatterTokens > 0) { | |
| cats.push({ | |
| name: 'Skills', | |
| tokens: skillFrontmatterTokens, | |
| color: 'warning', | |
| }) | |
| } | |
| if (messageTokens !== null && messageTokens > 0) { | |
| cats.push({ | |
| name: 'Messages', | |
| tokens: messageTokens, | |
| color: 'purple_FOR_SUBAGENTS_ONLY', | |
| }) | |
| } | |
| // Calculate actual content usage (before adding reserved buffers) | |
| // Exclude deferred categories from the usage calculation | |
| const actualUsage = cats.reduce( | |
| (sum, cat) => sum + (cat.isDeferred ? 0 : cat.tokens), | |
| 0, | |
| ) | |
| // Reserved space after messages (not counted in actualUsage shown to user). | |
| // Under reactive-only mode (cobalt_raccoon), proactive autocompact never | |
| // fires and the reserved buffer is a lie — skip it entirely and let Free | |
| // space fill the grid. feature() guard keeps the flag string out of | |
| // external builds. Same for context-collapse (marble_origami) — collapse | |
| // owns the threshold ladder and autocompact is suppressed in | |
| // shouldAutoCompact, so the 33k buffer shown here would be a lie too. | |
| let reservedTokens = 0 | |
| let skipReservedBuffer = false | |
| if (feature('REACTIVE_COMPACT')) { | |
| if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) { | |
| skipReservedBuffer = true | |
| } | |
| } | |
| if (feature('CONTEXT_COLLAPSE')) { | |
| /* eslint-disable @typescript-eslint/no-require-imports */ | |
| const { isContextCollapseEnabled } = | |
| require('../services/contextCollapse/index.js') as typeof import('../services/contextCollapse/index.js') | |
| /* eslint-enable @typescript-eslint/no-require-imports */ | |
| if (isContextCollapseEnabled()) { | |
| skipReservedBuffer = true | |
| } | |
| } | |
| if (skipReservedBuffer) { | |
| // No buffer category pushed — reactive compaction is transparent and | |
| // doesn't need a visible reservation in the grid. | |
| } else if (isAutoCompact && autoCompactThreshold !== undefined) { | |
| // Autocompact buffer (from effective context) | |
| reservedTokens = contextWindow - autoCompactThreshold | |
| cats.push({ | |
| name: RESERVED_CATEGORY_NAME, | |
| tokens: reservedTokens, | |
| color: 'inactive', | |
| }) | |
| } else if (!isAutoCompact) { | |
| // Compact buffer reserve (3k from actual context limit) | |
| reservedTokens = MANUAL_COMPACT_BUFFER_TOKENS | |
| cats.push({ | |
| name: MANUAL_COMPACT_BUFFER_NAME, | |
| tokens: reservedTokens, | |
| color: 'inactive', | |
| }) | |
| } | |
| // Calculate free space (subtract both actual usage and reserved buffer) | |
| const freeTokens = Math.max(0, contextWindow - actualUsage - reservedTokens) | |
| cats.push({ | |
| name: 'Free space', | |
| tokens: freeTokens, | |
| color: 'promptBorder', | |
| }) | |
| // Total for display (everything except free space) | |
| const totalIncludingReserved = actualUsage | |
| // Extract API usage from original messages (if provided) to match status line | |
| // This uses the same source of truth as the status line for consistency | |
| const apiUsage = getCurrentUsage(originalMessages ?? messages) | |
| // When API usage is available, use it for total to match status line calculation | |
| // Status line uses: input_tokens + cache_creation_input_tokens + cache_read_input_tokens | |
| const totalFromAPI = apiUsage | |
| ? apiUsage.input_tokens + | |
| apiUsage.cache_creation_input_tokens + | |
| apiUsage.cache_read_input_tokens | |
| : null | |
| // Use API total if available, otherwise fall back to estimated total | |
| const finalTotalTokens = totalFromAPI ?? totalIncludingReserved | |
| // Pre-calculate grid based on model context window and terminal width | |
| // For narrow screens (< 80 cols), use 5x5 for 200k models, 5x10 for 1M+ models | |
| // For normal screens, use 10x10 for 200k models, 20x10 for 1M+ models | |
| const isNarrowScreen = terminalWidth && terminalWidth < 80 | |
| const GRID_WIDTH = | |
| contextWindow >= 1000000 | |
| ? isNarrowScreen | |
| ? 5 | |
| : 20 | |
| : isNarrowScreen | |
| ? 5 | |
| : 10 | |
| const GRID_HEIGHT = contextWindow >= 1000000 ? 10 : isNarrowScreen ? 5 : 10 | |
| const TOTAL_SQUARES = GRID_WIDTH * GRID_HEIGHT | |
| // Filter out deferred categories - they don't take up actual context space | |
| // (e.g., MCP tools when tool search is enabled) | |
| const nonDeferredCats = cats.filter(cat => !cat.isDeferred) | |
| // Calculate squares per category (use rawEffectiveMax for visualization to show full context) | |
| const categorySquares = nonDeferredCats.map(cat => ({ | |
| ...cat, | |
| squares: | |
| cat.name === 'Free space' | |
| ? Math.round((cat.tokens / contextWindow) * TOTAL_SQUARES) | |
| : Math.max(1, Math.round((cat.tokens / contextWindow) * TOTAL_SQUARES)), | |
| percentageOfTotal: Math.round((cat.tokens / contextWindow) * 100), | |
| })) | |
| // Helper function to create grid squares for a category | |
| function createCategorySquares( | |
| category: (typeof categorySquares)[0], | |
| ): GridSquare[] { | |
| const squares: GridSquare[] = [] | |
| const exactSquares = (category.tokens / contextWindow) * TOTAL_SQUARES | |
| const wholeSquares = Math.floor(exactSquares) | |
| const fractionalPart = exactSquares - wholeSquares | |
| for (let i = 0; i < category.squares; i++) { | |
| // Determine fullness: full squares get 1.0, partial square gets fractional amount | |
| let squareFullness = 1.0 | |
| if (i === wholeSquares && fractionalPart > 0) { | |
| // This is the partial square | |
| squareFullness = fractionalPart | |
| } | |
| squares.push({ | |
| color: category.color, | |
| isFilled: true, | |
| categoryName: category.name, | |
| tokens: category.tokens, | |
| percentage: category.percentageOfTotal, | |
| squareFullness, | |
| }) | |
| } | |
| return squares | |
| } | |
| // Build the grid as an array of squares with full metadata | |
| const gridSquares: GridSquare[] = [] | |
| // Separate reserved category for end placement (either autocompact or manual compact buffer) | |
| const reservedCategory = categorySquares.find( | |
| cat => | |
| cat.name === RESERVED_CATEGORY_NAME || | |
| cat.name === MANUAL_COMPACT_BUFFER_NAME, | |
| ) | |
| const nonReservedCategories = categorySquares.filter( | |
| cat => | |
| cat.name !== RESERVED_CATEGORY_NAME && | |
| cat.name !== MANUAL_COMPACT_BUFFER_NAME && | |
| cat.name !== 'Free space', | |
| ) | |
| // Add all non-reserved, non-free-space squares first | |
| for (const cat of nonReservedCategories) { | |
| const squares = createCategorySquares(cat) | |
| for (const square of squares) { | |
| if (gridSquares.length < TOTAL_SQUARES) { | |
| gridSquares.push(square) | |
| } | |
| } | |
| } | |
| // Calculate how many squares are needed for reserved | |
| const reservedSquareCount = reservedCategory ? reservedCategory.squares : 0 | |
| // Fill with free space, leaving room for reserved at the end | |
| const freeSpaceCat = cats.find(c => c.name === 'Free space') | |
| const freeSpaceTarget = TOTAL_SQUARES - reservedSquareCount | |
| while (gridSquares.length < freeSpaceTarget) { | |
| gridSquares.push({ | |
| color: 'promptBorder', | |
| isFilled: true, | |
| categoryName: 'Free space', | |
| tokens: freeSpaceCat?.tokens || 0, | |
| percentage: freeSpaceCat | |
| ? Math.round((freeSpaceCat.tokens / contextWindow) * 100) | |
| : 0, | |
| squareFullness: 1.0, // Free space is always "full" | |
| }) | |
| } | |
| // Add reserved squares at the end | |
| if (reservedCategory) { | |
| const squares = createCategorySquares(reservedCategory) | |
| for (const square of squares) { | |
| if (gridSquares.length < TOTAL_SQUARES) { | |
| gridSquares.push(square) | |
| } | |
| } | |
| } | |
| // Convert to rows for rendering | |
| const gridRows: GridSquare[][] = [] | |
| for (let i = 0; i < GRID_HEIGHT; i++) { | |
| gridRows.push(gridSquares.slice(i * GRID_WIDTH, (i + 1) * GRID_WIDTH)) | |
| } | |
| // Format message breakdown (used by context suggestions for all users) | |
| // Combine tool calls and results, then get top 5 | |
| const toolsMap = new Map< | |
| string, | |
| { callTokens: number; resultTokens: number } | |
| >() | |
| // Add call tokens | |
| for (const [name, tokens] of messageBreakdown.toolCallsByType.entries()) { | |
| const existing = toolsMap.get(name) || { callTokens: 0, resultTokens: 0 } | |
| toolsMap.set(name, { ...existing, callTokens: tokens }) | |
| } | |
| // Add result tokens | |
| for (const [name, tokens] of messageBreakdown.toolResultsByType.entries()) { | |
| const existing = toolsMap.get(name) || { callTokens: 0, resultTokens: 0 } | |
| toolsMap.set(name, { ...existing, resultTokens: tokens }) | |
| } | |
| // Convert to array and sort by total tokens (calls + results) | |
| const toolsByTypeArray = Array.from(toolsMap.entries()) | |
| .map(([name, { callTokens, resultTokens }]) => ({ | |
| name, | |
| callTokens, | |
| resultTokens, | |
| })) | |
| .sort( | |
| (a, b) => b.callTokens + b.resultTokens - (a.callTokens + a.resultTokens), | |
| ) | |
| const attachmentsByTypeArray = Array.from( | |
| messageBreakdown.attachmentsByType.entries(), | |
| ) | |
| .map(([name, tokens]) => ({ name, tokens })) | |
| .sort((a, b) => b.tokens - a.tokens) | |
| const formattedMessageBreakdown = { | |
| toolCallTokens: messageBreakdown.toolCallTokens, | |
| toolResultTokens: messageBreakdown.toolResultTokens, | |
| attachmentTokens: messageBreakdown.attachmentTokens, | |
| assistantMessageTokens: messageBreakdown.assistantMessageTokens, | |
| userMessageTokens: messageBreakdown.userMessageTokens, | |
| toolCallsByType: toolsByTypeArray, | |
| attachmentsByType: attachmentsByTypeArray, | |
| } | |
| return { | |
| categories: cats, | |
| totalTokens: finalTotalTokens, | |
| maxTokens: contextWindow, | |
| rawMaxTokens: contextWindow, | |
| percentage: Math.round((finalTotalTokens / contextWindow) * 100), | |
| gridRows, | |
| model: runtimeModel, | |
| memoryFiles: memoryFileDetails, | |
| mcpTools: mcpToolDetails, | |
| deferredBuiltinTools: | |
| process.env.USER_TYPE === 'ant' ? deferredBuiltinDetails : undefined, | |
| systemTools: | |
| process.env.USER_TYPE === 'ant' ? systemToolDetails : undefined, | |
| systemPromptSections: | |
| process.env.USER_TYPE === 'ant' ? systemPromptSections : undefined, | |
| agents: agentDetails, | |
| slashCommands: | |
| slashCommandTokens > 0 | |
| ? { | |
| totalCommands: commandInfo.totalCommands, | |
| includedCommands: commandInfo.includedCommands, | |
| tokens: slashCommandTokens, | |
| } | |
| : undefined, | |
| skills: | |
| skillFrontmatterTokens > 0 | |
| ? { | |
| totalSkills: skillInfo.totalSkills, | |
| includedSkills: skillInfo.includedSkills, | |
| tokens: skillFrontmatterTokens, | |
| skillFrontmatter: skillInfo.skillFrontmatter, | |
| } | |
| : undefined, | |
| autoCompactThreshold, | |
| isAutoCompactEnabled: isAutoCompact, | |
| messageBreakdown: formattedMessageBreakdown, | |
| apiUsage, | |
| } | |
| } | |