@compilr-dev/agents 0.3.11 → 0.3.12

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.
Files changed (37) hide show
  1. package/dist/agent.d.ts +23 -0
  2. package/dist/agent.js +41 -7
  3. package/dist/anchors/manager.js +3 -2
  4. package/dist/context/delegated-result-store.d.ts +67 -0
  5. package/dist/context/delegated-result-store.js +99 -0
  6. package/dist/context/delegation-types.d.ts +82 -0
  7. package/dist/context/delegation-types.js +18 -0
  8. package/dist/context/index.d.ts +6 -0
  9. package/dist/context/index.js +4 -0
  10. package/dist/context/manager.js +12 -32
  11. package/dist/context/tool-result-delegator.d.ts +63 -0
  12. package/dist/context/tool-result-delegator.js +305 -0
  13. package/dist/index.d.ts +5 -5
  14. package/dist/index.js +9 -3
  15. package/dist/memory/loader.js +2 -1
  16. package/dist/memory/types.d.ts +1 -1
  17. package/dist/providers/claude.d.ts +1 -5
  18. package/dist/providers/claude.js +3 -28
  19. package/dist/providers/gemini-native.d.ts +1 -1
  20. package/dist/providers/gemini-native.js +3 -24
  21. package/dist/providers/mock.d.ts +1 -1
  22. package/dist/providers/mock.js +3 -24
  23. package/dist/providers/openai-compatible.d.ts +1 -5
  24. package/dist/providers/openai-compatible.js +3 -28
  25. package/dist/rate-limit/provider-wrapper.d.ts +1 -1
  26. package/dist/rate-limit/provider-wrapper.js +3 -27
  27. package/dist/tools/builtin/index.d.ts +2 -0
  28. package/dist/tools/builtin/index.js +2 -0
  29. package/dist/tools/builtin/recall-result.d.ts +29 -0
  30. package/dist/tools/builtin/recall-result.js +48 -0
  31. package/dist/tools/index.d.ts +2 -2
  32. package/dist/tools/index.js +2 -0
  33. package/dist/utils/index.d.ts +1 -0
  34. package/dist/utils/index.js +2 -0
  35. package/dist/utils/tokenizer.d.ts +18 -0
  36. package/dist/utils/tokenizer.js +56 -0
  37. package/package.json +3 -2
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Tool Result Delegator
3
+ *
4
+ * Intercepts large tool results via the AfterToolHook mechanism,
5
+ * stores the full result for optional recall, and replaces it
6
+ * with a compact summary to conserve context tokens.
7
+ */
8
+ import { DEFAULT_DELEGATION_CONFIG } from './delegation-types.js';
9
+ import { DelegatedResultStore } from './delegated-result-store.js';
10
+ import { countTokens } from '../utils/tokenizer.js';
11
+ /**
12
+ * System prompt for LLM-based summarization.
13
+ */
14
+ const SUMMARIZATION_SYSTEM_PROMPT = 'You are a tool result summarizer. Compress this tool output preserving:\n' +
15
+ '- ALL errors/warnings verbatim\n' +
16
+ '- File paths, function names, line numbers, counts\n' +
17
+ '- Overall structure (sections, item counts)\n' +
18
+ '- First few items of lists verbatim\n' +
19
+ 'Drop: verbose repetition, successful routine output, function bodies.\n' +
20
+ 'Never add information not in the original.\n' +
21
+ 'Respond with ONLY the summary, no preamble.';
22
+ /**
23
+ * Core class that creates an AfterToolHook for auto-delegating large tool results.
24
+ */
25
+ export class ToolResultDelegator {
26
+ store;
27
+ config;
28
+ provider;
29
+ onEvent;
30
+ constructor(options) {
31
+ this.provider = options.provider;
32
+ this.config = { ...DEFAULT_DELEGATION_CONFIG, ...options.config };
33
+ this.onEvent = options.onEvent;
34
+ this.store = new DelegatedResultStore({
35
+ maxSize: this.config.maxStoredResults,
36
+ defaultTTL: this.config.resultTTL,
37
+ });
38
+ }
39
+ /**
40
+ * Returns an AfterToolHook to register with HooksManager.
41
+ */
42
+ createHook() {
43
+ const getToolConfig = this.getToolConfig.bind(this);
44
+ const delegateResult = this.delegateResult.bind(this);
45
+ return (context) => {
46
+ // Skip failed results — errors should always pass through
47
+ if (!context.result.success)
48
+ return undefined;
49
+ // Never re-delegate recall_full_result — that would create a loop
50
+ if (context.toolName === 'recall_full_result')
51
+ return undefined;
52
+ // Check if delegation is enabled for this tool
53
+ const toolConfig = getToolConfig(context.toolName);
54
+ if (!toolConfig.enabled)
55
+ return undefined;
56
+ // Serialize the result content
57
+ const content = typeof context.result.result === 'string'
58
+ ? context.result.result
59
+ : JSON.stringify(context.result.result);
60
+ // Count tokens
61
+ const tokens = countTokens(content);
62
+ // Below threshold — pass through unchanged
63
+ if (tokens <= toolConfig.threshold)
64
+ return undefined;
65
+ // Delegate asynchronously
66
+ return delegateResult(context, content, tokens, toolConfig);
67
+ };
68
+ }
69
+ /**
70
+ * Perform the actual delegation (async path).
71
+ */
72
+ async delegateResult(context, content, tokens, toolConfig) {
73
+ // Generate delegation ID
74
+ const id = this.store.generateId();
75
+ this.onEvent?.({
76
+ type: 'delegation:started',
77
+ toolName: context.toolName,
78
+ originalTokens: tokens,
79
+ delegationId: id,
80
+ });
81
+ try {
82
+ // Determine strategy
83
+ const strategy = toolConfig.strategy === 'auto' ? 'extractive' : toolConfig.strategy;
84
+ let summary;
85
+ let usedStrategy = strategy;
86
+ if (strategy === 'llm') {
87
+ const llmSummary = await this.summarizeLLM(content, context.toolName);
88
+ if (llmSummary !== null) {
89
+ summary = llmSummary;
90
+ usedStrategy = 'llm';
91
+ }
92
+ else {
93
+ // LLM failed, fall back to extractive
94
+ summary = this.summarizeExtractive(content);
95
+ usedStrategy = 'extractive';
96
+ }
97
+ }
98
+ else if (toolConfig.strategy === 'auto') {
99
+ // Auto: try LLM first, fall back to extractive
100
+ const llmSummary = await this.summarizeLLM(content, context.toolName);
101
+ if (llmSummary !== null) {
102
+ summary = llmSummary;
103
+ usedStrategy = 'llm';
104
+ }
105
+ else {
106
+ summary = this.summarizeExtractive(content);
107
+ usedStrategy = 'extractive';
108
+ }
109
+ }
110
+ else {
111
+ summary = this.summarizeExtractive(content);
112
+ usedStrategy = 'extractive';
113
+ }
114
+ const summaryTokens = countTokens(summary);
115
+ // Store the full result
116
+ const now = Date.now();
117
+ const stored = {
118
+ id,
119
+ toolName: context.toolName,
120
+ toolInput: context.input,
121
+ fullContent: content,
122
+ fullTokens: tokens,
123
+ summary,
124
+ summaryTokens,
125
+ storedAt: now,
126
+ expiresAt: now + this.config.resultTTL,
127
+ };
128
+ this.store.add(stored);
129
+ this.onEvent?.({
130
+ type: 'delegation:completed',
131
+ toolName: context.toolName,
132
+ originalTokens: tokens,
133
+ summaryTokens,
134
+ delegationId: id,
135
+ strategy: usedStrategy,
136
+ });
137
+ // Return modified result with summary
138
+ // The format must be unmistakable so the LLM never confuses
139
+ // a delegated summary with the actual tool output.
140
+ return {
141
+ result: {
142
+ success: true,
143
+ result: {
144
+ _delegated: true,
145
+ _delegationId: id,
146
+ _originalTokens: tokens,
147
+ _summaryTokens: summaryTokens,
148
+ _warning: 'THIS IS A SUMMARY, NOT THE FULL RESULT. You did not see the full output.',
149
+ summary,
150
+ recall: `To get the full output, call recall_full_result with id="${id}"`,
151
+ },
152
+ },
153
+ };
154
+ }
155
+ catch (error) {
156
+ this.onEvent?.({
157
+ type: 'delegation:failed',
158
+ toolName: context.toolName,
159
+ error: error instanceof Error ? error.message : String(error),
160
+ });
161
+ // On failure, return original result unchanged
162
+ return { result: context.result };
163
+ }
164
+ }
165
+ /**
166
+ * Access the store (needed by recall_full_result tool).
167
+ */
168
+ getStore() {
169
+ return this.store;
170
+ }
171
+ /**
172
+ * Get the resolved config for a specific tool.
173
+ */
174
+ getToolConfig(toolName) {
175
+ const override = this.config.toolOverrides?.[toolName];
176
+ return {
177
+ enabled: override?.enabled ?? this.config.enabled,
178
+ threshold: override?.threshold ?? this.config.delegationThreshold,
179
+ strategy: override?.strategy ?? this.config.strategy,
180
+ };
181
+ }
182
+ /**
183
+ * Extractive summarization: first/last lines + prioritized structural markers.
184
+ * No LLM cost, fast, preserves actual code signatures.
185
+ */
186
+ summarizeExtractive(content) {
187
+ const lines = content.split('\n');
188
+ const totalLines = lines.length;
189
+ if (totalLines <= 40) {
190
+ // Small enough to include mostly as-is
191
+ return content;
192
+ }
193
+ const parts = [];
194
+ // Header: makes it impossible to confuse with full content
195
+ parts.push(`[SUMMARIZED — showing first 20 + last 10 of ${String(totalLines)} lines, plus key signatures. Use recall_full_result for complete output.]`);
196
+ parts.push('');
197
+ // First 20 lines
198
+ parts.push(...lines.slice(0, 20));
199
+ // Extract structural markers from the middle, prioritized by importance
200
+ const middleLines = lines.slice(20, -10);
201
+ // High priority: exports, class/interface/function declarations with signatures
202
+ const highPriority = /^\s*export\s+(default\s+)?(async\s+)?(function|class|interface|type|const|enum)\s/;
203
+ // Medium priority: non-exported declarations, method signatures
204
+ const medPriority = /^\s*(async\s+)?(function|class|interface|type|const|enum)\s|^\s+(async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/;
205
+ // Low priority: errors, headings, section markers
206
+ const lowPriority = /^\s*(ERROR|WARN|FAIL|error|warning|#{1,6}\s|---{3,}|\*{3,}|={3,}|\/\/\s*={3,})/;
207
+ const high = [];
208
+ const med = [];
209
+ const low = [];
210
+ for (const line of middleLines) {
211
+ if (highPriority.test(line)) {
212
+ high.push(line);
213
+ }
214
+ else if (medPriority.test(line)) {
215
+ med.push(line);
216
+ }
217
+ else if (lowPriority.test(line)) {
218
+ low.push(line);
219
+ }
220
+ }
221
+ // Budget: scale markers with file size, cap at 40
222
+ const totalMarkers = high.length + med.length + low.length;
223
+ const budget = Math.min(40, Math.max(15, Math.floor(totalLines / 80)));
224
+ // Fill budget: all high, then medium, then low
225
+ const selected = [];
226
+ for (const line of high) {
227
+ if (selected.length >= budget)
228
+ break;
229
+ selected.push(line);
230
+ }
231
+ for (const line of med) {
232
+ if (selected.length >= budget)
233
+ break;
234
+ selected.push(line);
235
+ }
236
+ for (const line of low) {
237
+ if (selected.length >= budget)
238
+ break;
239
+ selected.push(line);
240
+ }
241
+ if (selected.length > 0) {
242
+ parts.push('');
243
+ parts.push(`... (${String(middleLines.length)} lines omitted, ${String(totalMarkers)} structural markers found, showing ${String(selected.length)} key signatures) ...`);
244
+ parts.push(...selected);
245
+ if (totalMarkers > selected.length) {
246
+ parts.push(`... (${String(totalMarkers - selected.length)} more markers omitted) ...`);
247
+ }
248
+ }
249
+ else {
250
+ parts.push('');
251
+ parts.push(`... (${String(middleLines.length)} lines omitted) ...`);
252
+ }
253
+ // Last 10 lines
254
+ parts.push('');
255
+ parts.push(...lines.slice(-10));
256
+ parts.push('');
257
+ parts.push(`[Total: ${String(totalLines)} lines]`);
258
+ return parts.join('\n');
259
+ }
260
+ /**
261
+ * LLM-based summarization using the provider.
262
+ * Returns null if the LLM call fails.
263
+ */
264
+ async summarizeLLM(content, toolName) {
265
+ try {
266
+ const messages = [
267
+ {
268
+ role: 'system',
269
+ content: SUMMARIZATION_SYSTEM_PROMPT,
270
+ },
271
+ {
272
+ role: 'user',
273
+ content: `Summarize this ${toolName} tool result (keep under ${String(this.config.summaryMaxTokens)} tokens):\n\n${content}`,
274
+ },
275
+ ];
276
+ let text = '';
277
+ for await (const chunk of this.provider.chat(messages, {
278
+ maxTokens: this.config.summaryMaxTokens,
279
+ temperature: 0,
280
+ model: undefined, // Use the provider's default model
281
+ })) {
282
+ if (chunk.type === 'text' && chunk.text) {
283
+ text += chunk.text;
284
+ }
285
+ }
286
+ return text.trim() || null;
287
+ }
288
+ catch {
289
+ return null;
290
+ }
291
+ }
292
+ }
293
+ /**
294
+ * System prompt addition when delegation is enabled.
295
+ * Append to the agent's system prompt.
296
+ */
297
+ export const DELEGATION_SYSTEM_PROMPT = '\n\n## Tool Result Delegation\n' +
298
+ 'Some tool results are automatically summarized to conserve context. When a result has `_delegated: true`, ' +
299
+ 'you received a SUMMARY, not the full output. You MUST acknowledge this — never claim you read the full content.\n' +
300
+ 'Delegated results include:\n' +
301
+ '- `_warning`: confirms this is a summary\n' +
302
+ '- `summary`: extracted key information\n' +
303
+ '- `recall`: how to get the full output\n' +
304
+ 'Use `recall_full_result` with the delegation ID to get the full output when the summary is insufficient.\n' +
305
+ 'Only recall when you need specific code, exact values, or the summary indicates omitted content.\n';
package/dist/index.d.ts CHANGED
@@ -31,16 +31,16 @@ export { PerplexityProvider, createPerplexityProvider } from './providers/index.
31
31
  export type { PerplexityProviderConfig } from './providers/index.js';
32
32
  export { OpenRouterProvider, createOpenRouterProvider } from './providers/index.js';
33
33
  export type { OpenRouterProviderConfig } from './providers/index.js';
34
- export type { Tool, ToolHandler, ToolRegistry, ToolInputSchema, ToolExecutionResult, ToolRegistryOptions, ToolFallbackHandler, DefineToolOptions, ReadFileInput, WriteFileInput, BashInput, BashResult, FifoDetectionResult, GrepInput, GlobInput, EditInput, TodoWriteInput, TodoReadInput, TodoItem, TodoStatus, TodoContextCleanupOptions, TaskInput, TaskResult, AgentTypeConfig, TaskToolOptions, ContextMode, ThoroughnessLevel, SubAgentEventInfo, SuggestInput, SuggestToolOptions, } from './tools/index.js';
34
+ export type { Tool, ToolHandler, ToolRegistry, ToolInputSchema, ToolExecutionResult, ToolRegistryOptions, ToolFallbackHandler, DefineToolOptions, ReadFileInput, WriteFileInput, BashInput, BashResult, FifoDetectionResult, GrepInput, GlobInput, EditInput, TodoWriteInput, TodoReadInput, TodoItem, TodoStatus, TodoContextCleanupOptions, TaskInput, TaskResult, AgentTypeConfig, TaskToolOptions, ContextMode, ThoroughnessLevel, SubAgentEventInfo, SuggestInput, SuggestToolOptions, RecallResultInput, RecallResultToolOptions, } from './tools/index.js';
35
35
  export { defineTool, createSuccessResult, createErrorResult, wrapToolExecute, DefaultToolRegistry, createToolRegistry, } from './tools/index.js';
36
- export { readFileTool, createReadFileTool, writeFileTool, createWriteFileTool, bashTool, createBashTool, execStream, detectFifoUsage, bashOutputTool, createBashOutputTool, killShellTool, createKillShellTool, ShellManager, getDefaultShellManager, setDefaultShellManager, grepTool, createGrepTool, globTool, createGlobTool, editTool, createEditTool, todoWriteTool, todoReadTool, createTodoTools, TodoStore, resetDefaultTodoStore, getDefaultTodoStore, createIsolatedTodoStore, cleanupTodoContextMessages, getTodoContextStats, webFetchTool, createWebFetchTool, createTaskTool, defaultAgentTypes, suggestTool, createSuggestTool, builtinTools, allBuiltinTools, TOOL_NAMES, TOOL_SETS, } from './tools/index.js';
36
+ export { readFileTool, createReadFileTool, writeFileTool, createWriteFileTool, bashTool, createBashTool, execStream, detectFifoUsage, bashOutputTool, createBashOutputTool, killShellTool, createKillShellTool, ShellManager, getDefaultShellManager, setDefaultShellManager, grepTool, createGrepTool, globTool, createGlobTool, editTool, createEditTool, todoWriteTool, todoReadTool, createTodoTools, TodoStore, resetDefaultTodoStore, getDefaultTodoStore, createIsolatedTodoStore, cleanupTodoContextMessages, getTodoContextStats, webFetchTool, createWebFetchTool, createTaskTool, defaultAgentTypes, suggestTool, createSuggestTool, createRecallResultTool, builtinTools, allBuiltinTools, TOOL_NAMES, TOOL_SETS, } from './tools/index.js';
37
37
  export { userMessage, assistantMessage, systemMessage, textBlock, toolUseBlock, toolResultBlock, getTextContent, getToolUses, getToolResults, hasToolUses, validateToolUseResultPairing, repairToolPairing, ensureMessageContent, normalizeMessages, } from './messages/index.js';
38
38
  export type { ToolPairingValidation } from './messages/index.js';
39
- export { generateId, sleep, retry, truncate, withRetryGenerator, calculateBackoffDelay, DEFAULT_RETRY_CONFIG, } from './utils/index.js';
39
+ export { generateId, sleep, retry, truncate, withRetryGenerator, calculateBackoffDelay, DEFAULT_RETRY_CONFIG, countTokens, countMessageTokens, } from './utils/index.js';
40
40
  export type { RetryConfig as LLMRetryConfig, WithRetryOptions } from './utils/index.js';
41
41
  export { AgentError, ProviderError, ToolError, ToolTimeoutError, ToolLoopError, ValidationError, MaxIterationsError, AbortError, ContextOverflowError, isAgentError, isProviderError, isToolError, isToolTimeoutError, isToolLoopError, isContextOverflowError, wrapError, } from './errors.js';
42
- export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, } from './context/index.js';
43
- export type { ContextManagerOptions, ContextCategory, BudgetAllocation, CategoryBudgetInfo, PreflightResult, VerbosityLevel, VerbosityConfig, ContextConfig, FilteringConfig, CompactionConfig, SummarizationConfig, CompactionResult, SummarizationResult, FilteringResult, ContextEvent, ContextEventHandler, ContextStats, FileAccessType, FileAccess, FileAccessTrackerOptions, FormatHintsOptions, FileAccessStats, } from './context/index.js';
42
+ export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG, } from './context/index.js';
43
+ export type { ContextManagerOptions, ContextCategory, BudgetAllocation, CategoryBudgetInfo, PreflightResult, VerbosityLevel, VerbosityConfig, ContextConfig, FilteringConfig, CompactionConfig, SummarizationConfig, CompactionResult, SummarizationResult, FilteringResult, ContextEvent, ContextEventHandler, ContextStats, FileAccessType, FileAccess, FileAccessTrackerOptions, FormatHintsOptions, FileAccessStats, DelegatedResultStoreStats, ToolResultDelegatorOptions, DelegationConfig, StoredResult, DelegationEvent, } from './context/index.js';
44
44
  export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
45
45
  export type { Skill, SkillInvocationResult, SkillInvokeOptions } from './skills/index.js';
46
46
  export { JsonSerializer, CompactJsonSerializer, defaultSerializer, MemoryCheckpointer, FileCheckpointer, StateError, StateErrorCode, CURRENT_STATE_VERSION, } from './state/index.js';
package/dist/index.js CHANGED
@@ -29,7 +29,9 @@ export { readFileTool, createReadFileTool, writeFileTool, createWriteFileTool, b
29
29
  // Task tool (sub-agent spawning)
30
30
  createTaskTool, defaultAgentTypes,
31
31
  // Suggest tool (next action suggestions)
32
- suggestTool, createSuggestTool, builtinTools, allBuiltinTools,
32
+ suggestTool, createSuggestTool,
33
+ // Recall result tool (delegation)
34
+ createRecallResultTool, builtinTools, allBuiltinTools,
33
35
  // Tool names - single source of truth
34
36
  TOOL_NAMES, TOOL_SETS, } from './tools/index.js';
35
37
  // Message utilities
@@ -37,11 +39,15 @@ export { userMessage, assistantMessage, systemMessage, textBlock, toolUseBlock,
37
39
  // Utilities
38
40
  export { generateId, sleep,
39
41
  // eslint-disable-next-line @typescript-eslint/no-deprecated -- Kept for backward compatibility
40
- retry, truncate, withRetryGenerator, calculateBackoffDelay, DEFAULT_RETRY_CONFIG, } from './utils/index.js';
42
+ retry, truncate, withRetryGenerator, calculateBackoffDelay, DEFAULT_RETRY_CONFIG,
43
+ // Token counting (tiktoken-based)
44
+ countTokens, countMessageTokens, } from './utils/index.js';
41
45
  // Errors
42
46
  export { AgentError, ProviderError, ToolError, ToolTimeoutError, ToolLoopError, ValidationError, MaxIterationsError, AbortError, ContextOverflowError, isAgentError, isProviderError, isToolError, isToolTimeoutError, isToolLoopError, isContextOverflowError, wrapError, } from './errors.js';
43
47
  // Context management
44
- export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, } from './context/index.js';
48
+ export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS,
49
+ // Tool result delegation
50
+ DelegatedResultStore, ToolResultDelegator, DELEGATION_SYSTEM_PROMPT, DEFAULT_DELEGATION_CONFIG, } from './context/index.js';
45
51
  // Skills system
46
52
  export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
47
53
  // State management
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import * as fs from 'fs/promises';
27
27
  import * as path from 'path';
28
+ import { countTokens } from '../utils/tokenizer.js';
28
29
  /**
29
30
  * Built-in patterns for various LLM providers
30
31
  */
@@ -268,7 +269,7 @@ export class ProjectMemoryLoader {
268
269
  files,
269
270
  content,
270
271
  rootDir: absoluteRoot,
271
- estimatedTokens: Math.ceil(content.length / 4),
272
+ estimatedTokens: countTokens(content),
272
273
  };
273
274
  this.emit({ type: 'memory:search_complete', memory });
274
275
  return memory;
@@ -41,7 +41,7 @@ export interface ProjectMemory {
41
41
  content: string;
42
42
  /** The root directory where search started */
43
43
  rootDir: string;
44
- /** Total token estimate (rough: chars / 4) */
44
+ /** Total token estimate (tiktoken cl100k_base) */
45
45
  estimatedTokens: number;
46
46
  }
47
47
  /**
@@ -61,11 +61,7 @@ export declare class ClaudeProvider implements LLMProvider {
61
61
  */
62
62
  chat(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk>;
63
63
  /**
64
- * Count tokens in messages (not yet available in SDK v0.30)
65
- *
66
- * Note: Token counting API is available via Anthropic's beta endpoints
67
- * but not yet exposed in the stable SDK. For now, this provides an
68
- * approximation based on character count.
64
+ * Count tokens in messages using tiktoken (cl100k_base encoding)
69
65
  */
70
66
  countTokens(messages: Message[]): Promise<number>;
71
67
  /**
@@ -11,6 +11,7 @@
11
11
  * ```
12
12
  */
13
13
  import Anthropic from '@anthropic-ai/sdk';
14
+ import { countMessageTokens } from '../utils/tokenizer.js';
14
15
  import { ProviderError } from '../errors.js';
15
16
  /**
16
17
  * Default model for Claude API
@@ -122,36 +123,10 @@ export class ClaudeProvider {
122
123
  }
123
124
  }
124
125
  /**
125
- * Count tokens in messages (not yet available in SDK v0.30)
126
- *
127
- * Note: Token counting API is available via Anthropic's beta endpoints
128
- * but not yet exposed in the stable SDK. For now, this provides an
129
- * approximation based on character count.
126
+ * Count tokens in messages using tiktoken (cl100k_base encoding)
130
127
  */
131
128
  countTokens(messages) {
132
- // Approximation: ~4 characters per token (rough estimate)
133
- let charCount = 0;
134
- for (const msg of messages) {
135
- if (typeof msg.content === 'string') {
136
- charCount += msg.content.length;
137
- }
138
- else {
139
- for (const block of msg.content) {
140
- switch (block.type) {
141
- case 'text':
142
- charCount += block.text.length;
143
- break;
144
- case 'tool_use':
145
- charCount += JSON.stringify(block.input).length;
146
- break;
147
- case 'tool_result':
148
- charCount += block.content.length;
149
- break;
150
- }
151
- }
152
- }
153
- }
154
- return Promise.resolve(Math.ceil(charCount / 4));
129
+ return Promise.resolve(countMessageTokens(messages));
155
130
  }
156
131
  /**
157
132
  * Convert our Message format to Anthropic's format
@@ -52,7 +52,7 @@ export declare class GeminiNativeProvider implements LLMProvider {
52
52
  */
53
53
  chat(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk>;
54
54
  /**
55
- * Count tokens in messages (approximation)
55
+ * Count tokens in messages using tiktoken (cl100k_base encoding)
56
56
  */
57
57
  countTokens(messages: Message[]): Promise<number>;
58
58
  /**
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { GoogleGenAI } from '@google/genai';
17
17
  import { ProviderError } from '../errors.js';
18
+ import { countMessageTokens } from '../utils/tokenizer.js';
18
19
  /**
19
20
  * Default model for Gemini API
20
21
  */
@@ -137,32 +138,10 @@ export class GeminiNativeProvider {
137
138
  }
138
139
  }
139
140
  /**
140
- * Count tokens in messages (approximation)
141
+ * Count tokens in messages using tiktoken (cl100k_base encoding)
141
142
  */
142
143
  countTokens(messages) {
143
- // Approximation: ~4 characters per token
144
- let charCount = 0;
145
- for (const msg of messages) {
146
- if (typeof msg.content === 'string') {
147
- charCount += msg.content.length;
148
- }
149
- else {
150
- for (const block of msg.content) {
151
- switch (block.type) {
152
- case 'text':
153
- charCount += block.text.length;
154
- break;
155
- case 'tool_use':
156
- charCount += JSON.stringify(block.input).length;
157
- break;
158
- case 'tool_result':
159
- charCount += block.content.length;
160
- break;
161
- }
162
- }
163
- }
164
- }
165
- return Promise.resolve(Math.ceil(charCount / 4));
144
+ return Promise.resolve(countMessageTokens(messages));
166
145
  }
167
146
  /**
168
147
  * Convert our Message format to Google's format
@@ -122,7 +122,7 @@ export declare class MockProvider implements LLMProvider {
122
122
  */
123
123
  chat(messages: Message[], options?: ChatOptions): AsyncIterable<StreamChunk>;
124
124
  /**
125
- * Count tokens (mock implementation)
125
+ * Count tokens using tiktoken (cl100k_base encoding)
126
126
  */
127
127
  countTokens(messages: Message[]): Promise<number>;
128
128
  private sleep;
@@ -25,6 +25,7 @@
25
25
  * ```
26
26
  */
27
27
  import { ProviderError } from '../errors.js';
28
+ import { countMessageTokens } from '../utils/tokenizer.js';
28
29
  /**
29
30
  * MockProvider for testing agents without API calls.
30
31
  *
@@ -171,32 +172,10 @@ export class MockProvider {
171
172
  yield { type: 'done' };
172
173
  }
173
174
  /**
174
- * Count tokens (mock implementation)
175
+ * Count tokens using tiktoken (cl100k_base encoding)
175
176
  */
176
177
  countTokens(messages) {
177
- // Simple approximation: ~4 chars per token
178
- let charCount = 0;
179
- for (const msg of messages) {
180
- if (typeof msg.content === 'string') {
181
- charCount += msg.content.length;
182
- }
183
- else {
184
- for (const block of msg.content) {
185
- if (block.type === 'text') {
186
- charCount += block.text.length;
187
- }
188
- else if (block.type === 'tool_result') {
189
- // Count tool result content (always a string per our type definition)
190
- charCount += block.content.length;
191
- }
192
- else if (block.type === 'tool_use') {
193
- // Count tool use input
194
- charCount += JSON.stringify(block.input).length;
195
- }
196
- }
197
- }
198
- }
199
- return Promise.resolve(Math.ceil(charCount / 4));
178
+ return Promise.resolve(countMessageTokens(messages));
200
179
  }
201
180
  sleep(ms) {
202
181
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -186,11 +186,7 @@ export declare abstract class OpenAICompatibleProvider implements LLMProvider {
186
186
  arguments: string;
187
187
  }>): StreamChunk[];
188
188
  /**
189
- * Estimate token count (rough approximation)
190
- *
191
- * @remarks
192
- * Most providers don't have a native token counting endpoint.
193
- * This uses a rough approximation of ~4 characters per token.
189
+ * Count tokens in messages using tiktoken (cl100k_base encoding)
194
190
  */
195
191
  countTokens(messages: Message[]): Promise<number>;
196
192
  }
@@ -22,6 +22,7 @@
22
22
  * ```
23
23
  */
24
24
  import { ProviderError } from '../errors.js';
25
+ import { countMessageTokens } from '../utils/tokenizer.js';
25
26
  // Default configuration
26
27
  const DEFAULT_MAX_TOKENS = 4096;
27
28
  const DEFAULT_TIMEOUT = 120000;
@@ -349,35 +350,9 @@ export class OpenAICompatibleProvider {
349
350
  return results;
350
351
  }
351
352
  /**
352
- * Estimate token count (rough approximation)
353
- *
354
- * @remarks
355
- * Most providers don't have a native token counting endpoint.
356
- * This uses a rough approximation of ~4 characters per token.
353
+ * Count tokens in messages using tiktoken (cl100k_base encoding)
357
354
  */
358
355
  countTokens(messages) {
359
- let charCount = 0;
360
- for (const msg of messages) {
361
- if (typeof msg.content === 'string') {
362
- charCount += msg.content.length;
363
- }
364
- else if (Array.isArray(msg.content)) {
365
- for (const block of msg.content) {
366
- if (block.type === 'text') {
367
- charCount += block.text.length;
368
- }
369
- else if (block.type === 'tool_use') {
370
- charCount += JSON.stringify(block.input).length;
371
- }
372
- else if (block.type === 'tool_result') {
373
- charCount +=
374
- typeof block.content === 'string'
375
- ? block.content.length
376
- : JSON.stringify(block.content).length;
377
- }
378
- }
379
- }
380
- }
381
- return Promise.resolve(Math.ceil(charCount / 4));
356
+ return Promise.resolve(countMessageTokens(messages));
382
357
  }
383
358
  }
@@ -45,7 +45,7 @@ export declare class RateLimitedProvider implements LLMProvider {
45
45
  */
46
46
  countTokens(messages: Message[]): Promise<number>;
47
47
  /**
48
- * Estimate tokens from messages (rough approximation)
48
+ * Estimate tokens from messages using tiktoken
49
49
  */
50
50
  private estimateTokens;
51
51
  }