nova-terminal-assistant 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// TokenCounter - Precise token counting using tiktoken
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { Message, ContentBlock } from '../types/session.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Model encoding mappings for tiktoken
|
|
9
|
+
* Maps model names to their corresponding tiktoken encoding
|
|
10
|
+
*/
|
|
11
|
+
const MODEL_ENCODING_MAP: Record<string, string> = {
|
|
12
|
+
// Anthropic Claude models (use cl100k_base, similar to GPT-4)
|
|
13
|
+
'claude-3': 'cl100k_base',
|
|
14
|
+
'claude-sonnet': 'cl100k_base',
|
|
15
|
+
'claude-opus': 'cl100k_base',
|
|
16
|
+
'claude-haiku': 'cl100k_base',
|
|
17
|
+
|
|
18
|
+
// OpenAI models
|
|
19
|
+
'gpt-4': 'cl100k_base',
|
|
20
|
+
'gpt-4o': 'o200k_base',
|
|
21
|
+
'gpt-4-turbo': 'cl100k_base',
|
|
22
|
+
'gpt-3.5-turbo': 'cl100k_base',
|
|
23
|
+
'o1': 'o200k_base',
|
|
24
|
+
'o1-mini': 'o200k_base',
|
|
25
|
+
'o3-mini': 'o200k_base',
|
|
26
|
+
|
|
27
|
+
// Google models (approximate with cl100k_base)
|
|
28
|
+
'gemini': 'cl100k_base',
|
|
29
|
+
|
|
30
|
+
// DeepSeek (use cl100k_base)
|
|
31
|
+
'deepseek': 'cl100k_base',
|
|
32
|
+
|
|
33
|
+
// Qwen (use cl100k_base)
|
|
34
|
+
'qwen': 'cl100k_base',
|
|
35
|
+
|
|
36
|
+
// GLM (use cl100k_base)
|
|
37
|
+
'glm': 'cl100k_base',
|
|
38
|
+
|
|
39
|
+
// Llama models
|
|
40
|
+
'llama': 'cl100k_base',
|
|
41
|
+
|
|
42
|
+
// Default fallback
|
|
43
|
+
'default': 'cl100k_base',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Special tokens added per message for different providers
|
|
48
|
+
* Based on official documentation
|
|
49
|
+
*/
|
|
50
|
+
const MESSAGE_TOKEN_OVERHEAD: Record<string, { perMessage: number; perName: number }> = {
|
|
51
|
+
anthropic: { perMessage: 4, perName: 0 }, // Approximate
|
|
52
|
+
openai: { perMessage: 3, perName: 1 }, // Official: every message follows <im_start>{role/name}\n{content}<im_end>\n
|
|
53
|
+
default: { perMessage: 4, perName: 0 },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Simple tokenizer implementation without external dependencies
|
|
58
|
+
* Uses character-based estimation with model-specific adjustments
|
|
59
|
+
*/
|
|
60
|
+
class SimpleTokenizer {
|
|
61
|
+
private encoding: string;
|
|
62
|
+
private avgCharsPerToken: number;
|
|
63
|
+
|
|
64
|
+
constructor(encoding: string) {
|
|
65
|
+
this.encoding = encoding;
|
|
66
|
+
// Different encodings have different average chars per token
|
|
67
|
+
this.avgCharsPerToken = encoding === 'o200k_base' ? 3.5 : 4;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Encode text to tokens (simplified implementation)
|
|
72
|
+
* Returns array of token strings for counting
|
|
73
|
+
*/
|
|
74
|
+
encode(text: string): string[] {
|
|
75
|
+
// Simple word-based tokenization with punctuation handling
|
|
76
|
+
const tokens: string[] = [];
|
|
77
|
+
let current = '';
|
|
78
|
+
let inWord = false;
|
|
79
|
+
|
|
80
|
+
for (const char of text) {
|
|
81
|
+
const isWordChar = /[a-zA-Z0-9]/.test(char);
|
|
82
|
+
|
|
83
|
+
if (isWordChar !== inWord && current) {
|
|
84
|
+
tokens.push(current);
|
|
85
|
+
current = '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
current += char;
|
|
89
|
+
inWord = isWordChar;
|
|
90
|
+
|
|
91
|
+
// Break long sequences
|
|
92
|
+
if (current.length >= Math.ceil(this.avgCharsPerToken * 2)) {
|
|
93
|
+
tokens.push(current);
|
|
94
|
+
current = '';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (current) {
|
|
99
|
+
tokens.push(current);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return tokens;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Count tokens in text
|
|
107
|
+
*/
|
|
108
|
+
count(text: string): number {
|
|
109
|
+
if (!text) return 0;
|
|
110
|
+
|
|
111
|
+
// For more accurate counting, use word-based estimation
|
|
112
|
+
// English: ~1.3 tokens per word
|
|
113
|
+
// Chinese: ~0.5-0.8 tokens per character (due to subword tokenization)
|
|
114
|
+
// Code: ~0.5-0.7 tokens per word
|
|
115
|
+
|
|
116
|
+
const words = text.split(/\s+/).filter(w => w.length > 0);
|
|
117
|
+
const wordCount = words.length;
|
|
118
|
+
|
|
119
|
+
// Count CJK characters separately (they typically tokenize as 1-2 chars per token)
|
|
120
|
+
const cjkChars = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
|
|
121
|
+
const nonCjkLength = text.length - cjkChars;
|
|
122
|
+
|
|
123
|
+
// Estimate: CJK ~0.6 tokens per char, non-CJK ~4 chars per token
|
|
124
|
+
const cjkTokens = Math.ceil(cjkChars * 0.6);
|
|
125
|
+
const nonCjkTokens = Math.ceil(nonCjkLength / this.avgCharsPerToken);
|
|
126
|
+
|
|
127
|
+
// Add overhead for special tokens and structure
|
|
128
|
+
const overhead = Math.ceil(wordCount * 0.1);
|
|
129
|
+
|
|
130
|
+
return cjkTokens + nonCjkTokens + overhead;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Token counter with caching for performance
|
|
136
|
+
*/
|
|
137
|
+
export class TokenCounter {
|
|
138
|
+
private static instance: TokenCounter;
|
|
139
|
+
private tokenizers: Map<string, SimpleTokenizer> = new Map();
|
|
140
|
+
private cache: Map<string, number> = new Map();
|
|
141
|
+
private cacheMaxSize = 1000;
|
|
142
|
+
|
|
143
|
+
private constructor() {}
|
|
144
|
+
|
|
145
|
+
static getInstance(): TokenCounter {
|
|
146
|
+
if (!TokenCounter.instance) {
|
|
147
|
+
TokenCounter.instance = new TokenCounter();
|
|
148
|
+
}
|
|
149
|
+
return TokenCounter.instance;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get tokenizer for a model
|
|
154
|
+
*/
|
|
155
|
+
private getTokenizer(model: string): SimpleTokenizer {
|
|
156
|
+
// Normalize model name
|
|
157
|
+
const normalizedModel = this.normalizeModelName(model);
|
|
158
|
+
const encoding = MODEL_ENCODING_MAP[normalizedModel] ?? 'cl100k_base';
|
|
159
|
+
|
|
160
|
+
if (!this.tokenizers.has(encoding)) {
|
|
161
|
+
this.tokenizers.set(encoding, new SimpleTokenizer(encoding));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return this.tokenizers.get(encoding)!;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Normalize model name to match encoding map
|
|
169
|
+
*/
|
|
170
|
+
private normalizeModelName(model: string): string {
|
|
171
|
+
const lower = model.toLowerCase();
|
|
172
|
+
|
|
173
|
+
// Check direct matches first
|
|
174
|
+
for (const key of Object.keys(MODEL_ENCODING_MAP)) {
|
|
175
|
+
if (lower.includes(key)) {
|
|
176
|
+
return key;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return 'default';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get provider type from model name
|
|
185
|
+
*/
|
|
186
|
+
private getProviderType(model: string): string {
|
|
187
|
+
const lower = model.toLowerCase();
|
|
188
|
+
if (lower.includes('claude') || lower.includes('anthropic')) return 'anthropic';
|
|
189
|
+
if (lower.includes('gpt') || lower.includes('o1') || lower.includes('o3')) return 'openai';
|
|
190
|
+
return 'default';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Count tokens in a text string
|
|
195
|
+
*/
|
|
196
|
+
countTokens(text: string, model: string = 'default'): number {
|
|
197
|
+
if (!text) return 0;
|
|
198
|
+
|
|
199
|
+
// Check cache
|
|
200
|
+
const cacheKey = `${model}:${text.slice(0, 100)}:${text.length}`;
|
|
201
|
+
if (this.cache.has(cacheKey)) {
|
|
202
|
+
return this.cache.get(cacheKey)!;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const tokenizer = this.getTokenizer(model);
|
|
206
|
+
const count = tokenizer.count(text);
|
|
207
|
+
|
|
208
|
+
// Cache result
|
|
209
|
+
if (this.cache.size >= this.cacheMaxSize) {
|
|
210
|
+
// Remove oldest entries
|
|
211
|
+
const keys = Array.from(this.cache.keys()).slice(0, Math.floor(this.cacheMaxSize / 2));
|
|
212
|
+
for (const key of keys) {
|
|
213
|
+
this.cache.delete(key);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.cache.set(cacheKey, count);
|
|
217
|
+
|
|
218
|
+
return count;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Count tokens in a message
|
|
223
|
+
*/
|
|
224
|
+
countMessageTokens(message: Message, model: string = 'default'): number {
|
|
225
|
+
const provider = this.getProviderType(model);
|
|
226
|
+
const overhead = MESSAGE_TOKEN_OVERHEAD[provider] ?? { perMessage: 4, perName: 0 };
|
|
227
|
+
|
|
228
|
+
let count = overhead.perMessage;
|
|
229
|
+
|
|
230
|
+
// Count role tokens
|
|
231
|
+
count += this.countTokens(message.role, model);
|
|
232
|
+
|
|
233
|
+
// Count content tokens
|
|
234
|
+
for (const block of message.content) {
|
|
235
|
+
count += this.countContentBlockTokens(block, model);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Add name overhead if present
|
|
239
|
+
if ((message as any).name) {
|
|
240
|
+
count += overhead.perName + this.countTokens((message as any).name, model);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return count;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Count tokens in a content block
|
|
248
|
+
*/
|
|
249
|
+
countContentBlockTokens(block: ContentBlock, model: string = 'default'): number {
|
|
250
|
+
switch (block.type) {
|
|
251
|
+
case 'text':
|
|
252
|
+
return this.countTokens(block.text, model);
|
|
253
|
+
|
|
254
|
+
case 'tool_use':
|
|
255
|
+
// Tool name + input JSON
|
|
256
|
+
return this.countTokens(block.name, model) +
|
|
257
|
+
this.countTokens(JSON.stringify(block.input), model) + 4; // overhead
|
|
258
|
+
|
|
259
|
+
case 'tool_result':
|
|
260
|
+
// Tool ID + content
|
|
261
|
+
const contentTokens = typeof block.content === 'string'
|
|
262
|
+
? this.countTokens(block.content, model)
|
|
263
|
+
: block.content.reduce((sum, c) => {
|
|
264
|
+
const text = 'text' in c ? c.text : '';
|
|
265
|
+
return sum + this.countTokens(text, model);
|
|
266
|
+
}, 0);
|
|
267
|
+
return this.countTokens(block.tool_use_id, model) + contentTokens + 4;
|
|
268
|
+
|
|
269
|
+
case 'image':
|
|
270
|
+
// Images are counted differently by different providers
|
|
271
|
+
// Approximate based on image size if available
|
|
272
|
+
if (block.source.type === 'base64' && block.source.data) {
|
|
273
|
+
// Rough estimate: 85 tokens for a 512x512 image, scales with pixels
|
|
274
|
+
const base64Length = block.source.data.length;
|
|
275
|
+
return Math.ceil(base64Length / 1000) * 85; // Very rough estimate
|
|
276
|
+
}
|
|
277
|
+
return 85; // Default image token cost
|
|
278
|
+
|
|
279
|
+
case 'thinking':
|
|
280
|
+
return this.countTokens(block.thinking, model);
|
|
281
|
+
|
|
282
|
+
default:
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Count tokens in an array of messages
|
|
289
|
+
*/
|
|
290
|
+
countMessagesTokens(messages: Message[], model: string = 'default'): number {
|
|
291
|
+
let total = 0;
|
|
292
|
+
|
|
293
|
+
for (const message of messages) {
|
|
294
|
+
total += this.countMessageTokens(message, model);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Add message overhead (every reply is primed with <im_start>assistant)
|
|
298
|
+
const provider = this.getProviderType(model);
|
|
299
|
+
total += MESSAGE_TOKEN_OVERHEAD[provider]?.perMessage || 4;
|
|
300
|
+
|
|
301
|
+
return total;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Estimate tokens for a system prompt
|
|
306
|
+
*/
|
|
307
|
+
countSystemPromptTokens(systemPrompt: string, model: string = 'default'): number {
|
|
308
|
+
// System prompts have slightly higher overhead
|
|
309
|
+
return this.countTokens(systemPrompt, model) + 4;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Fit content within a token budget
|
|
314
|
+
* Intelligently truncates while preserving structure
|
|
315
|
+
*/
|
|
316
|
+
fitWithinBudget(
|
|
317
|
+
content: string,
|
|
318
|
+
budget: number,
|
|
319
|
+
model: string = 'default',
|
|
320
|
+
options: {
|
|
321
|
+
preserveHead?: number; // Percentage of budget for head (default 60%)
|
|
322
|
+
preserveTail?: number; // Percentage of budget for tail (default 30%)
|
|
323
|
+
ellipsis?: string; // Ellipsis string (default "...")
|
|
324
|
+
} = {}
|
|
325
|
+
): { content: string; tokens: number; truncated: boolean } {
|
|
326
|
+
const currentTokens = this.countTokens(content, model);
|
|
327
|
+
|
|
328
|
+
if (currentTokens <= budget) {
|
|
329
|
+
return { content, tokens: currentTokens, truncated: false };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const {
|
|
333
|
+
preserveHead = 0.6,
|
|
334
|
+
preserveTail = 0.3,
|
|
335
|
+
ellipsis = '\n... [truncated] ...\n'
|
|
336
|
+
} = options;
|
|
337
|
+
|
|
338
|
+
const headBudget = Math.floor(budget * preserveHead);
|
|
339
|
+
const tailBudget = Math.floor(budget * preserveTail);
|
|
340
|
+
const ellipsisTokens = this.countTokens(ellipsis, model);
|
|
341
|
+
|
|
342
|
+
// Binary search for head cutoff
|
|
343
|
+
let headEnd = 0;
|
|
344
|
+
let low = 0, high = content.length;
|
|
345
|
+
while (low < high) {
|
|
346
|
+
const mid = Math.floor((low + high) / 2);
|
|
347
|
+
if (this.countTokens(content.slice(0, mid), model) <= headBudget) {
|
|
348
|
+
headEnd = mid;
|
|
349
|
+
low = mid + 1;
|
|
350
|
+
} else {
|
|
351
|
+
high = mid;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Binary search for tail start
|
|
356
|
+
let tailStart = content.length;
|
|
357
|
+
low = 0, high = content.length;
|
|
358
|
+
while (low < high) {
|
|
359
|
+
const mid = Math.floor((low + high) / 2);
|
|
360
|
+
if (this.countTokens(content.slice(mid), model) <= tailBudget) {
|
|
361
|
+
tailStart = mid;
|
|
362
|
+
high = mid;
|
|
363
|
+
} else {
|
|
364
|
+
low = mid + 1;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Construct truncated content
|
|
369
|
+
const truncatedContent = content.slice(0, headEnd) + ellipsis + content.slice(tailStart);
|
|
370
|
+
const finalTokens = this.countTokens(truncatedContent, model);
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
content: truncatedContent,
|
|
374
|
+
tokens: finalTokens,
|
|
375
|
+
truncated: true
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Calculate context window usage percentage
|
|
381
|
+
*/
|
|
382
|
+
calculateUsage(current: number, max: number): number {
|
|
383
|
+
return Math.min(100, Math.round((current / max) * 100));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get usage level indicator
|
|
388
|
+
*/
|
|
389
|
+
getUsageLevel(percentage: number): 'low' | 'medium' | 'high' | 'critical' {
|
|
390
|
+
if (percentage < 50) return 'low';
|
|
391
|
+
if (percentage < 70) return 'medium';
|
|
392
|
+
if (percentage < 90) return 'high';
|
|
393
|
+
return 'critical';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Clear cache
|
|
398
|
+
*/
|
|
399
|
+
clearCache(): void {
|
|
400
|
+
this.cache.clear();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Export singleton instance
|
|
405
|
+
export const tokenCounter = TokenCounter.getInstance();
|
|
406
|
+
|
|
407
|
+
// Export convenience functions
|
|
408
|
+
export function countTokens(text: string, model?: string): number {
|
|
409
|
+
return tokenCounter.countTokens(text, model);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function countMessagesTokens(messages: Message[], model?: string): number {
|
|
413
|
+
return tokenCounter.countMessagesTokens(messages, model);
|
|
414
|
+
}
|