@compilr-dev/agents 0.3.10 → 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 (38) hide show
  1. package/README.md +84 -41
  2. package/dist/agent.d.ts +23 -0
  3. package/dist/agent.js +41 -7
  4. package/dist/anchors/manager.js +3 -2
  5. package/dist/context/delegated-result-store.d.ts +67 -0
  6. package/dist/context/delegated-result-store.js +99 -0
  7. package/dist/context/delegation-types.d.ts +82 -0
  8. package/dist/context/delegation-types.js +18 -0
  9. package/dist/context/index.d.ts +6 -0
  10. package/dist/context/index.js +4 -0
  11. package/dist/context/manager.js +12 -32
  12. package/dist/context/tool-result-delegator.d.ts +63 -0
  13. package/dist/context/tool-result-delegator.js +305 -0
  14. package/dist/index.d.ts +5 -5
  15. package/dist/index.js +9 -3
  16. package/dist/memory/loader.js +2 -1
  17. package/dist/memory/types.d.ts +1 -1
  18. package/dist/providers/claude.d.ts +1 -5
  19. package/dist/providers/claude.js +3 -28
  20. package/dist/providers/gemini-native.d.ts +1 -1
  21. package/dist/providers/gemini-native.js +3 -24
  22. package/dist/providers/mock.d.ts +1 -1
  23. package/dist/providers/mock.js +3 -24
  24. package/dist/providers/openai-compatible.d.ts +1 -5
  25. package/dist/providers/openai-compatible.js +3 -28
  26. package/dist/rate-limit/provider-wrapper.d.ts +1 -1
  27. package/dist/rate-limit/provider-wrapper.js +3 -27
  28. package/dist/tools/builtin/index.d.ts +2 -0
  29. package/dist/tools/builtin/index.js +2 -0
  30. package/dist/tools/builtin/recall-result.d.ts +29 -0
  31. package/dist/tools/builtin/recall-result.js +48 -0
  32. package/dist/tools/index.d.ts +2 -2
  33. package/dist/tools/index.js +2 -0
  34. package/dist/utils/index.d.ts +1 -0
  35. package/dist/utils/index.js +2 -0
  36. package/dist/utils/tokenizer.d.ts +18 -0
  37. package/dist/utils/tokenizer.js +56 -0
  38. package/package.json +8 -7
@@ -18,6 +18,7 @@
18
18
  * ```
19
19
  */
20
20
  import { repairToolPairing } from '../messages/index.js';
21
+ import { countTokens, countMessageTokens } from '../utils/tokenizer.js';
21
22
  /**
22
23
  * Default budget allocation
23
24
  */
@@ -122,29 +123,8 @@ export class ContextManager {
122
123
  if (this.provider.countTokens) {
123
124
  return this.provider.countTokens(messages);
124
125
  }
125
- // Fallback: rough estimate based on character count
126
- let charCount = 0;
127
- for (const msg of messages) {
128
- if (typeof msg.content === 'string') {
129
- charCount += msg.content.length;
130
- }
131
- else {
132
- for (const block of msg.content) {
133
- switch (block.type) {
134
- case 'text':
135
- charCount += block.text.length;
136
- break;
137
- case 'tool_use':
138
- charCount += JSON.stringify(block.input).length;
139
- break;
140
- case 'tool_result':
141
- charCount += block.content.length;
142
- break;
143
- }
144
- }
145
- }
146
- }
147
- return Math.ceil(charCount / 4);
126
+ // Fallback: count tokens using tiktoken
127
+ return countMessageTokens(messages);
148
128
  }
149
129
  /**
150
130
  * Update token count for messages
@@ -351,7 +331,7 @@ export class ContextManager {
351
331
  }
352
332
  // Compact this message
353
333
  if (typeof msg.content === 'string') {
354
- const tokens = Math.ceil(msg.content.length / 4);
334
+ const tokens = countTokens(msg.content);
355
335
  if (tokens >= this.config.compaction.minTokensToCompact) {
356
336
  const filePath = await saveToFile(msg.content, compactedIndex++);
357
337
  filesCreated.push(filePath);
@@ -369,7 +349,7 @@ export class ContextManager {
369
349
  const compactedBlocks = [];
370
350
  for (const block of msg.content) {
371
351
  if (block.type === 'tool_result' && category === 'toolResults') {
372
- const tokens = Math.ceil(block.content.length / 4);
352
+ const tokens = countTokens(block.content);
373
353
  if (tokens >= this.config.compaction.minTokensToCompact) {
374
354
  const filePath = await saveToFile(block.content, compactedIndex++);
375
355
  filesCreated.push(filePath);
@@ -702,7 +682,7 @@ export class ContextManager {
702
682
  * Estimate tokens for a string content
703
683
  */
704
684
  estimateTokens(content) {
705
- return Math.ceil(content.length / 4);
685
+ return countTokens(content);
706
686
  }
707
687
  /**
708
688
  * Check if content can be added to a category
@@ -822,13 +802,13 @@ export class ContextManager {
822
802
  maxLines = this.config.filtering.maxErrorLines;
823
803
  break;
824
804
  default: {
825
- // For tool results, check token count estimate
826
- const estimatedTokens = Math.ceil(content.length / 4);
805
+ // For tool results, check token count
806
+ const estimatedTokens = countTokens(content);
827
807
  if (estimatedTokens <= this.config.filtering.maxToolResultTokens) {
828
808
  return { content, filtered: false, originalLength };
829
809
  }
830
- // Truncate to roughly maxToolResultTokens
831
- const maxChars = this.config.filtering.maxToolResultTokens * 4;
810
+ // Truncate to roughly maxToolResultTokens (~3 chars per token)
811
+ const maxChars = this.config.filtering.maxToolResultTokens * 3;
832
812
  const truncated = content.slice(0, maxChars);
833
813
  return {
834
814
  content: truncated + '\n\n[Content truncated - see file for full output]',
@@ -877,7 +857,7 @@ export class ContextManager {
877
857
  for (const msg of oldMessages) {
878
858
  if (typeof msg.content === 'string') {
879
859
  // Check if string content is large enough to compact
880
- const tokens = Math.ceil(msg.content.length / 4);
860
+ const tokens = countTokens(msg.content);
881
861
  if (tokens >= this.config.compaction.minTokensToCompact) {
882
862
  const filePath = await saveToFile(msg.content, compactedIndex++);
883
863
  filesCreated.push(filePath);
@@ -895,7 +875,7 @@ export class ContextManager {
895
875
  const compactedBlocks = [];
896
876
  for (const block of msg.content) {
897
877
  if (block.type === 'tool_result') {
898
- const tokens = Math.ceil(block.content.length / 4);
878
+ const tokens = countTokens(block.content);
899
879
  if (tokens >= this.config.compaction.minTokensToCompact) {
900
880
  const filePath = await saveToFile(block.content, compactedIndex++);
901
881
  filesCreated.push(filePath);
@@ -0,0 +1,63 @@
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 type { LLMProvider } from '../providers/types.js';
9
+ import type { AfterToolHook } from '../hooks/types.js';
10
+ import type { DelegationConfig, DelegationEvent } from './delegation-types.js';
11
+ import { DelegatedResultStore } from './delegated-result-store.js';
12
+ /**
13
+ * Options for creating a ToolResultDelegator.
14
+ */
15
+ export interface ToolResultDelegatorOptions {
16
+ /** LLM provider for summarization (small/medium tier preferred) */
17
+ provider: LLMProvider;
18
+ /** Delegation configuration (merged with defaults) */
19
+ config?: Partial<DelegationConfig>;
20
+ /** Event callback for delegation lifecycle events */
21
+ onEvent?: (event: DelegationEvent) => void;
22
+ }
23
+ /**
24
+ * Core class that creates an AfterToolHook for auto-delegating large tool results.
25
+ */
26
+ export declare class ToolResultDelegator {
27
+ private readonly store;
28
+ private readonly config;
29
+ private readonly provider;
30
+ private readonly onEvent?;
31
+ constructor(options: ToolResultDelegatorOptions);
32
+ /**
33
+ * Returns an AfterToolHook to register with HooksManager.
34
+ */
35
+ createHook(): AfterToolHook;
36
+ /**
37
+ * Perform the actual delegation (async path).
38
+ */
39
+ private delegateResult;
40
+ /**
41
+ * Access the store (needed by recall_full_result tool).
42
+ */
43
+ getStore(): DelegatedResultStore;
44
+ /**
45
+ * Get the resolved config for a specific tool.
46
+ */
47
+ private getToolConfig;
48
+ /**
49
+ * Extractive summarization: first/last lines + prioritized structural markers.
50
+ * No LLM cost, fast, preserves actual code signatures.
51
+ */
52
+ private summarizeExtractive;
53
+ /**
54
+ * LLM-based summarization using the provider.
55
+ * Returns null if the LLM call fails.
56
+ */
57
+ private summarizeLLM;
58
+ }
59
+ /**
60
+ * System prompt addition when delegation is enabled.
61
+ * Append to the agent's system prompt.
62
+ */
63
+ export declare const DELEGATION_SYSTEM_PROMPT: string;
@@ -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