@compilr-dev/agents 0.1.0 → 0.2.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.
@@ -18,7 +18,8 @@
18
18
  * ```
19
19
  */
20
20
  import type { Message, LLMProvider } from '../providers/types.js';
21
- import type { ContextConfig, ContextStats, CompactionResult, SummarizationResult, ContextEventHandler, ContextCategory, CategoryBudgetInfo, BudgetAllocation, PreflightResult, VerbosityLevel } from './types.js';
21
+ import type { ContextConfig, ContextStats, CompactionResult, SummarizationResult, ContextEventHandler, ContextCategory, CategoryBudgetInfo, BudgetAllocation, PreflightResult, VerbosityLevel, CategorizedMessages, SmartCompactOptions, SmartCompactionResult } from './types.js';
22
+ import type { FileAccessTracker } from './file-tracker.js';
22
23
  /**
23
24
  * Default budget allocation
24
25
  */
@@ -43,6 +44,12 @@ export interface ContextManagerOptions {
43
44
  * Event handler for context events
44
45
  */
45
46
  onEvent?: ContextEventHandler;
47
+ /**
48
+ * File access tracker for context restoration hints.
49
+ * When provided, compaction/summarization will inject hints
50
+ * about previously accessed files.
51
+ */
52
+ fileTracker?: FileAccessTracker;
46
53
  }
47
54
  /**
48
55
  * ContextManager tracks and manages context window usage
@@ -51,6 +58,7 @@ export declare class ContextManager {
51
58
  private readonly provider;
52
59
  private readonly config;
53
60
  private readonly onEvent?;
61
+ private readonly fileTracker?;
54
62
  private cachedTokenCount;
55
63
  private turnCount;
56
64
  private compactionCount;
@@ -110,6 +118,23 @@ export declare class ContextManager {
110
118
  * Get the current configuration
111
119
  */
112
120
  getConfig(): ContextConfig;
121
+ /**
122
+ * Get the file access tracker (if configured)
123
+ */
124
+ getFileTracker(): FileAccessTracker | undefined;
125
+ /**
126
+ * Format context restoration hints based on current verbosity level.
127
+ * Returns empty string if no file tracker is configured or no files have been accessed.
128
+ */
129
+ formatRestorationHints(): string;
130
+ /**
131
+ * Create a system message with context restoration hints.
132
+ * Returns undefined if no hints are available.
133
+ *
134
+ * The caller (Agent) is responsible for injecting this message
135
+ * after compaction or summarization.
136
+ */
137
+ createRestorationHintMessage(): Message | undefined;
113
138
  /**
114
139
  * Get budget information for a specific category
115
140
  */
@@ -144,6 +169,49 @@ export declare class ContextManager {
144
169
  * Determine which category a message belongs to
145
170
  */
146
171
  private categorizeMessage;
172
+ /**
173
+ * Categorize all messages by type for smart compaction
174
+ *
175
+ * This method:
176
+ * 1. Identifies system messages
177
+ * 2. Separates recent messages (last N turns) that should never be compacted
178
+ * 3. Identifies tool results in older messages
179
+ * 4. Classifies remaining older messages as history
180
+ *
181
+ * @param messages All messages to categorize
182
+ * @param preserveRecentTurns Number of recent turns to preserve (default: config value)
183
+ */
184
+ categorizeMessages(messages: Message[], preserveRecentTurns?: number): Promise<CategorizedMessages>;
185
+ /**
186
+ * Find which categories are over their budget allocation
187
+ *
188
+ * @param categorized Categorized messages from categorizeMessages()
189
+ * @returns Array of categories that exceed their budget, sorted by how much they exceed
190
+ */
191
+ findOverBudgetCategories(categorized: CategorizedMessages): ContextCategory[];
192
+ /**
193
+ * Smart compaction that respects category budgets
194
+ *
195
+ * Strategy:
196
+ * 1. System messages: Never compacted (critical for agent behavior)
197
+ * 2. Recent messages: Never compacted (needed for conversation continuity)
198
+ * 3. Tool results: Save large results to files, replace with references
199
+ * 4. History: Summarize with LLM
200
+ *
201
+ * The method compacts categories in order of how much they exceed their budget.
202
+ */
203
+ smartCompact(messages: Message[], options: SmartCompactOptions): Promise<{
204
+ messages: Message[];
205
+ result: SmartCompactionResult;
206
+ }>;
207
+ /**
208
+ * Compact tool results by saving large ones to files
209
+ */
210
+ private compactToolResults;
211
+ /**
212
+ * Calculate utilization given a token count
213
+ */
214
+ private calculateUtilization;
147
215
  /**
148
216
  * Estimate tokens for a string content
149
217
  */
@@ -66,6 +66,7 @@ export class ContextManager {
66
66
  provider;
67
67
  config;
68
68
  onEvent;
69
+ fileTracker;
69
70
  cachedTokenCount = 0;
70
71
  turnCount = 0;
71
72
  compactionCount = 0;
@@ -82,6 +83,7 @@ export class ContextManager {
82
83
  this.provider = options.provider;
83
84
  this.config = this.mergeConfig(options.config);
84
85
  this.onEvent = options.onEvent;
86
+ this.fileTracker = options.fileTracker;
85
87
  }
86
88
  /**
87
89
  * Merge partial config with defaults
@@ -221,6 +223,41 @@ export class ContextManager {
221
223
  getConfig() {
222
224
  return { ...this.config };
223
225
  }
226
+ /**
227
+ * Get the file access tracker (if configured)
228
+ */
229
+ getFileTracker() {
230
+ return this.fileTracker;
231
+ }
232
+ /**
233
+ * Format context restoration hints based on current verbosity level.
234
+ * Returns empty string if no file tracker is configured or no files have been accessed.
235
+ */
236
+ formatRestorationHints() {
237
+ if (!this.fileTracker || this.fileTracker.size === 0) {
238
+ return '';
239
+ }
240
+ return this.fileTracker.formatRestorationHints({
241
+ verbosityLevel: this.getVerbosityLevel(),
242
+ });
243
+ }
244
+ /**
245
+ * Create a system message with context restoration hints.
246
+ * Returns undefined if no hints are available.
247
+ *
248
+ * The caller (Agent) is responsible for injecting this message
249
+ * after compaction or summarization.
250
+ */
251
+ createRestorationHintMessage() {
252
+ const hints = this.formatRestorationHints();
253
+ if (!hints) {
254
+ return undefined;
255
+ }
256
+ return {
257
+ role: 'user',
258
+ content: hints,
259
+ };
260
+ }
224
261
  // ==========================================================================
225
262
  // Budget System (Novel Technique #1)
226
263
  // ==========================================================================
@@ -371,6 +408,273 @@ export class ContextManager {
371
408
  return 'recentMessages';
372
409
  }
373
410
  // ==========================================================================
411
+ // Smart Compaction (Phase 1)
412
+ // ==========================================================================
413
+ /**
414
+ * Categorize all messages by type for smart compaction
415
+ *
416
+ * This method:
417
+ * 1. Identifies system messages
418
+ * 2. Separates recent messages (last N turns) that should never be compacted
419
+ * 3. Identifies tool results in older messages
420
+ * 4. Classifies remaining older messages as history
421
+ *
422
+ * @param messages All messages to categorize
423
+ * @param preserveRecentTurns Number of recent turns to preserve (default: config value)
424
+ */
425
+ async categorizeMessages(messages, preserveRecentTurns) {
426
+ const recentTurns = preserveRecentTurns ?? this.config.compaction.preserveRecentTurns;
427
+ const preserveCount = recentTurns * 2; // 2 messages per turn (user + assistant)
428
+ const result = {
429
+ system: [],
430
+ recentMessages: [],
431
+ toolResults: [],
432
+ history: [],
433
+ tokenCounts: {
434
+ system: 0,
435
+ recentMessages: 0,
436
+ toolResults: 0,
437
+ history: 0,
438
+ },
439
+ originalIndices: new Map(),
440
+ };
441
+ // First pass: separate system messages and track indices
442
+ const nonSystemMessages = [];
443
+ for (let i = 0; i < messages.length; i++) {
444
+ const msg = messages[i];
445
+ result.originalIndices.set(msg, i);
446
+ if (msg.role === 'system') {
447
+ result.system.push(msg);
448
+ }
449
+ else {
450
+ nonSystemMessages.push(msg);
451
+ }
452
+ }
453
+ // Second pass: separate recent vs older messages
454
+ const recentStartIndex = Math.max(0, nonSystemMessages.length - preserveCount);
455
+ const recentMessages = nonSystemMessages.slice(recentStartIndex);
456
+ const olderMessages = nonSystemMessages.slice(0, recentStartIndex);
457
+ result.recentMessages = recentMessages;
458
+ // Third pass: categorize older messages
459
+ for (const msg of olderMessages) {
460
+ const category = this.categorizeMessage(msg);
461
+ if (category === 'toolResults') {
462
+ result.toolResults.push(msg);
463
+ }
464
+ else {
465
+ result.history.push(msg);
466
+ }
467
+ }
468
+ // Count tokens for each category
469
+ if (result.system.length > 0) {
470
+ result.tokenCounts.system = await this.countTokens(result.system);
471
+ }
472
+ if (result.recentMessages.length > 0) {
473
+ result.tokenCounts.recentMessages = await this.countTokens(result.recentMessages);
474
+ }
475
+ if (result.toolResults.length > 0) {
476
+ result.tokenCounts.toolResults = await this.countTokens(result.toolResults);
477
+ }
478
+ if (result.history.length > 0) {
479
+ result.tokenCounts.history = await this.countTokens(result.history);
480
+ }
481
+ return result;
482
+ }
483
+ /**
484
+ * Find which categories are over their budget allocation
485
+ *
486
+ * @param categorized Categorized messages from categorizeMessages()
487
+ * @returns Array of categories that exceed their budget, sorted by how much they exceed
488
+ */
489
+ findOverBudgetCategories(categorized) {
490
+ const overBudget = [];
491
+ for (const category of ['system', 'toolResults', 'history']) {
492
+ // Skip recentMessages - they're never compacted
493
+ const budget = this.getCategoryBudget(category);
494
+ const used = categorized.tokenCounts[category];
495
+ const allocated = budget.allocatedTokens;
496
+ if (used > allocated) {
497
+ overBudget.push({
498
+ category,
499
+ excess: used - allocated,
500
+ });
501
+ }
502
+ }
503
+ // Sort by excess (most over-budget first)
504
+ overBudget.sort((a, b) => b.excess - a.excess);
505
+ return overBudget.map((item) => item.category);
506
+ }
507
+ /**
508
+ * Smart compaction that respects category budgets
509
+ *
510
+ * Strategy:
511
+ * 1. System messages: Never compacted (critical for agent behavior)
512
+ * 2. Recent messages: Never compacted (needed for conversation continuity)
513
+ * 3. Tool results: Save large results to files, replace with references
514
+ * 4. History: Summarize with LLM
515
+ *
516
+ * The method compacts categories in order of how much they exceed their budget.
517
+ */
518
+ async smartCompact(messages, options) {
519
+ const emergency = options.emergency ?? this.needsEmergencySummarization();
520
+ const targetUtilization = options.targetUtilization ?? this.config.summarization.targetUtilization;
521
+ const preserveRecentTurns = options.preserveRecentTurns ?? this.config.compaction.preserveRecentTurns;
522
+ // Categorize all messages
523
+ const categorized = await this.categorizeMessages(messages, preserveRecentTurns);
524
+ const tokensBefore = categorized.tokenCounts.system +
525
+ categorized.tokenCounts.recentMessages +
526
+ categorized.tokenCounts.toolResults +
527
+ categorized.tokenCounts.history;
528
+ const filesCreated = [];
529
+ let fileIndex = 0;
530
+ let summarizationRounds = 0;
531
+ let summary;
532
+ // Initialize category stats
533
+ const categoryStats = {
534
+ system: {
535
+ tokensBefore: categorized.tokenCounts.system,
536
+ tokensAfter: categorized.tokenCounts.system,
537
+ action: 'preserved',
538
+ },
539
+ recentMessages: {
540
+ tokensBefore: categorized.tokenCounts.recentMessages,
541
+ tokensAfter: categorized.tokenCounts.recentMessages,
542
+ action: 'preserved',
543
+ },
544
+ toolResults: {
545
+ tokensBefore: categorized.tokenCounts.toolResults,
546
+ tokensAfter: categorized.tokenCounts.toolResults,
547
+ action: 'preserved',
548
+ },
549
+ history: {
550
+ tokensBefore: categorized.tokenCounts.history,
551
+ tokensAfter: categorized.tokenCounts.history,
552
+ action: 'preserved',
553
+ },
554
+ };
555
+ // Find which categories need compaction
556
+ const overBudgetCategories = this.findOverBudgetCategories(categorized);
557
+ // Process tool results if over budget
558
+ let compactedToolResults = categorized.toolResults;
559
+ if (overBudgetCategories.includes('toolResults') && categorized.toolResults.length > 0) {
560
+ const { messages: compacted, files } = await this.compactToolResults(categorized.toolResults, options.saveToFile, fileIndex);
561
+ compactedToolResults = compacted;
562
+ filesCreated.push(...files);
563
+ fileIndex += files.length;
564
+ const tokensAfter = await this.countTokens(compactedToolResults);
565
+ categoryStats.toolResults.tokensAfter = tokensAfter;
566
+ categoryStats.toolResults.action = 'compacted';
567
+ }
568
+ // Process history if over budget or if we need to reach target utilization
569
+ let compactedHistory = categorized.history;
570
+ const currentUtilization = this.calculateUtilization(categoryStats.system.tokensAfter +
571
+ categoryStats.recentMessages.tokensAfter +
572
+ categoryStats.toolResults.tokensAfter +
573
+ categorized.tokenCounts.history);
574
+ if ((overBudgetCategories.includes('history') || currentUtilization > targetUtilization) &&
575
+ categorized.history.length > 0) {
576
+ // Summarize history
577
+ summary = await options.generateSummary(categorized.history);
578
+ summarizationRounds = 1;
579
+ // Create summary message
580
+ const summaryMessage = {
581
+ role: 'user',
582
+ content: `[Previous conversation summary]\n\n${summary}\n\n[End of summary]`,
583
+ };
584
+ compactedHistory = [summaryMessage];
585
+ const tokensAfter = await this.countTokens(compactedHistory);
586
+ categoryStats.history.tokensAfter = tokensAfter;
587
+ categoryStats.history.action = 'summarized';
588
+ }
589
+ // Reconstruct messages in original order
590
+ // System messages come first, then summary (if any), then tool results, then recent
591
+ const finalMessages = [
592
+ ...categorized.system,
593
+ ...compactedHistory,
594
+ ...compactedToolResults,
595
+ ...categorized.recentMessages,
596
+ ];
597
+ // If history was summarized, add assistant acknowledgment after summary
598
+ if (categoryStats.history.action === 'summarized' && compactedHistory.length > 0) {
599
+ // Insert assistant acknowledgment after the summary message
600
+ const summaryIndex = categorized.system.length + 1; // After system + summary
601
+ finalMessages.splice(summaryIndex, 0, {
602
+ role: 'assistant',
603
+ content: 'I understand. I have the context from our previous conversation.',
604
+ });
605
+ }
606
+ const tokensAfter = await this.countTokens(finalMessages);
607
+ this.cachedTokenCount = tokensAfter;
608
+ this.compactionCount++;
609
+ this.lastCompactionTurn = this.turnCount;
610
+ const result = {
611
+ tokensBefore,
612
+ tokensAfter,
613
+ categoryStats,
614
+ filesCreated,
615
+ summary,
616
+ summarizationRounds,
617
+ emergency,
618
+ };
619
+ return { messages: finalMessages, result };
620
+ }
621
+ /**
622
+ * Compact tool results by saving large ones to files
623
+ */
624
+ async compactToolResults(messages, saveToFile, startIndex) {
625
+ const compactedMessages = [];
626
+ const files = [];
627
+ let fileIndex = startIndex;
628
+ for (const msg of messages) {
629
+ if (typeof msg.content === 'string') {
630
+ // String content - check if large enough to compact
631
+ const tokens = this.estimateTokens(msg.content);
632
+ if (tokens >= this.config.compaction.minTokensToCompact) {
633
+ const filePath = await saveToFile(msg.content, fileIndex++);
634
+ files.push(filePath);
635
+ compactedMessages.push({
636
+ ...msg,
637
+ content: `[Content saved to ${filePath}]`,
638
+ });
639
+ }
640
+ else {
641
+ compactedMessages.push(msg);
642
+ }
643
+ }
644
+ else {
645
+ // Content blocks - compact large tool_results
646
+ const compactedBlocks = [];
647
+ for (const block of msg.content) {
648
+ if (block.type === 'tool_result') {
649
+ const tokens = this.estimateTokens(block.content);
650
+ if (tokens >= this.config.compaction.minTokensToCompact) {
651
+ const filePath = await saveToFile(block.content, fileIndex++);
652
+ files.push(filePath);
653
+ compactedBlocks.push({
654
+ ...block,
655
+ content: `[Content saved to ${filePath}]`,
656
+ });
657
+ }
658
+ else {
659
+ compactedBlocks.push(block);
660
+ }
661
+ }
662
+ else {
663
+ compactedBlocks.push(block);
664
+ }
665
+ }
666
+ compactedMessages.push({ ...msg, content: compactedBlocks });
667
+ }
668
+ }
669
+ return { messages: compactedMessages, files };
670
+ }
671
+ /**
672
+ * Calculate utilization given a token count
673
+ */
674
+ calculateUtilization(tokens) {
675
+ return tokens / this.config.maxContextTokens;
676
+ }
677
+ // ==========================================================================
374
678
  // Pre-flight Checks (Novel Technique #2)
375
679
  // ==========================================================================
376
680
  /**
@@ -4,6 +4,7 @@
4
4
  * Types for managing agent context windows, including token tracking,
5
5
  * compaction, summarization, and filtering.
6
6
  */
7
+ import type { Message } from '../providers/types.js';
7
8
  /**
8
9
  * Context categories for budget allocation
9
10
  *
@@ -342,6 +343,100 @@ export type ContextEvent = {
342
343
  * Context event handler
343
344
  */
344
345
  export type ContextEventHandler = (event: ContextEvent) => void;
346
+ /**
347
+ * Categorized messages for smart compaction
348
+ */
349
+ export interface CategorizedMessages {
350
+ /**
351
+ * System prompt message(s)
352
+ */
353
+ system: Message[];
354
+ /**
355
+ * Recent messages (last N turns, never compacted)
356
+ */
357
+ recentMessages: Message[];
358
+ /**
359
+ * Messages containing tool results
360
+ */
361
+ toolResults: Message[];
362
+ /**
363
+ * Older conversation history
364
+ */
365
+ history: Message[];
366
+ /**
367
+ * Token counts per category
368
+ */
369
+ tokenCounts: Record<ContextCategory, number>;
370
+ /**
371
+ * Original message indices for reconstruction
372
+ */
373
+ originalIndices: Map<Message, number>;
374
+ }
375
+ /**
376
+ * Options for smart compaction
377
+ */
378
+ export interface SmartCompactOptions {
379
+ /**
380
+ * Function to generate LLM summary for history messages
381
+ */
382
+ generateSummary: (messages: Message[]) => Promise<string>;
383
+ /**
384
+ * Function to save content to a file (for tool results)
385
+ */
386
+ saveToFile: (content: string, index: number) => Promise<string>;
387
+ /**
388
+ * Number of recent turns to preserve (not compact)
389
+ * @default from config.compaction.preserveRecentTurns
390
+ */
391
+ preserveRecentTurns?: number;
392
+ /**
393
+ * Target utilization after compaction (0-1)
394
+ * @default from config.summarization.targetUtilization
395
+ */
396
+ targetUtilization?: number;
397
+ /**
398
+ * Whether to use emergency mode (more aggressive)
399
+ * @default auto-detect based on utilization
400
+ */
401
+ emergency?: boolean;
402
+ }
403
+ /**
404
+ * Result of smart compaction
405
+ */
406
+ export interface SmartCompactionResult {
407
+ /**
408
+ * Total tokens before compaction
409
+ */
410
+ tokensBefore: number;
411
+ /**
412
+ * Total tokens after compaction
413
+ */
414
+ tokensAfter: number;
415
+ /**
416
+ * Per-category statistics
417
+ */
418
+ categoryStats: Record<ContextCategory, {
419
+ tokensBefore: number;
420
+ tokensAfter: number;
421
+ action: 'preserved' | 'compacted' | 'summarized';
422
+ }>;
423
+ /**
424
+ * Files created during tool result compaction
425
+ */
426
+ filesCreated: string[];
427
+ /**
428
+ * Generated summary (for history)
429
+ */
430
+ summary?: string;
431
+ /**
432
+ * Number of summarization rounds performed
433
+ */
434
+ summarizationRounds: number;
435
+ /**
436
+ * Whether emergency mode was used
437
+ */
438
+ emergency: boolean;
439
+ }
345
440
  /**
346
441
  * Context statistics
347
442
  */
package/dist/index.d.ts CHANGED
@@ -17,15 +17,15 @@ export { GeminiProvider, createGeminiProvider } from './providers/index.js';
17
17
  export type { GeminiProviderConfig } from './providers/index.js';
18
18
  export { OpenAICompatibleProvider } from './providers/index.js';
19
19
  export type { OpenAICompatibleConfig } from './providers/index.js';
20
- export type { Tool, ToolHandler, ToolRegistry, ToolInputSchema, ToolExecutionResult, ToolRegistryOptions, DefineToolOptions, ReadFileInput, WriteFileInput, BashInput, BashResult, FifoDetectionResult, GrepInput, GlobInput, EditInput, TodoWriteInput, TodoReadInput, TodoItem, TodoStatus, TodoContextCleanupOptions, TaskInput, TaskResult, AgentTypeConfig, TaskToolOptions, ContextMode, ThoroughnessLevel, SubAgentEventInfo, } from './tools/index.js';
20
+ export type { Tool, ToolHandler, ToolRegistry, ToolInputSchema, ToolExecutionResult, ToolRegistryOptions, 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';
21
21
  export { defineTool, createSuccessResult, createErrorResult, wrapToolExecute, DefaultToolRegistry, createToolRegistry, } from './tools/index.js';
22
- 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, builtinTools, allBuiltinTools, } from './tools/index.js';
23
- export { userMessage, assistantMessage, systemMessage, textBlock, toolUseBlock, toolResultBlock, getTextContent, getToolUses, getToolResults, hasToolUses, validateToolUseResultPairing, ensureMessageContent, normalizeMessages, } from './messages/index.js';
22
+ 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';
23
+ export { userMessage, assistantMessage, systemMessage, textBlock, toolUseBlock, toolResultBlock, getTextContent, getToolUses, getToolResults, hasToolUses, validateToolUseResultPairing, repairToolPairing, ensureMessageContent, normalizeMessages, } from './messages/index.js';
24
24
  export type { ToolPairingValidation } from './messages/index.js';
25
25
  export { generateId, sleep, retry, truncate } from './utils/index.js';
26
26
  export { AgentError, ProviderError, ToolError, ToolTimeoutError, ToolLoopError, ValidationError, MaxIterationsError, AbortError, ContextOverflowError, isAgentError, isProviderError, isToolError, isToolTimeoutError, isToolLoopError, isContextOverflowError, wrapError, } from './errors.js';
27
- export { ContextManager, DEFAULT_CONTEXT_CONFIG } from './context/index.js';
28
- export type { ContextManagerOptions, ContextCategory, BudgetAllocation, CategoryBudgetInfo, PreflightResult, VerbosityLevel, VerbosityConfig, ContextConfig, FilteringConfig, CompactionConfig, SummarizationConfig, CompactionResult, SummarizationResult, FilteringResult, ContextEvent, ContextEventHandler, ContextStats, } from './context/index.js';
27
+ export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, } from './context/index.js';
28
+ 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';
29
29
  export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
30
30
  export type { Skill, SkillInvocationResult, SkillInvokeOptions } from './skills/index.js';
31
31
  export { JsonSerializer, CompactJsonSerializer, defaultSerializer, MemoryCheckpointer, FileCheckpointer, StateError, StateErrorCode, CURRENT_STATE_VERSION, } from './state/index.js';
package/dist/index.js CHANGED
@@ -17,15 +17,19 @@ export { defineTool, createSuccessResult, createErrorResult, wrapToolExecute, De
17
17
  // Built-in tools
18
18
  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,
19
19
  // Task tool (sub-agent spawning)
20
- createTaskTool, defaultAgentTypes, builtinTools, allBuiltinTools, } from './tools/index.js';
20
+ createTaskTool, defaultAgentTypes,
21
+ // Suggest tool (next action suggestions)
22
+ suggestTool, createSuggestTool, builtinTools, allBuiltinTools,
23
+ // Tool names - single source of truth
24
+ TOOL_NAMES, TOOL_SETS, } from './tools/index.js';
21
25
  // Message utilities
22
- export { userMessage, assistantMessage, systemMessage, textBlock, toolUseBlock, toolResultBlock, getTextContent, getToolUses, getToolResults, hasToolUses, validateToolUseResultPairing, ensureMessageContent, normalizeMessages, } from './messages/index.js';
26
+ export { userMessage, assistantMessage, systemMessage, textBlock, toolUseBlock, toolResultBlock, getTextContent, getToolUses, getToolResults, hasToolUses, validateToolUseResultPairing, repairToolPairing, ensureMessageContent, normalizeMessages, } from './messages/index.js';
23
27
  // Utilities
24
28
  export { generateId, sleep, retry, truncate } from './utils/index.js';
25
29
  // Errors
26
30
  export { AgentError, ProviderError, ToolError, ToolTimeoutError, ToolLoopError, ValidationError, MaxIterationsError, AbortError, ContextOverflowError, isAgentError, isProviderError, isToolError, isToolTimeoutError, isToolLoopError, isContextOverflowError, wrapError, } from './errors.js';
27
31
  // Context management
28
- export { ContextManager, DEFAULT_CONTEXT_CONFIG } from './context/index.js';
32
+ export { ContextManager, DEFAULT_CONTEXT_CONFIG, FileAccessTracker, createFileTrackingHook, TRACKED_TOOLS, } from './context/index.js';
29
33
  // Skills system
30
34
  export { SkillRegistry, defineSkill, createSkillRegistry, builtinSkills, getDefaultSkillRegistry, resetDefaultSkillRegistry, } from './skills/index.js';
31
35
  // State management
@@ -80,3 +80,16 @@ export declare function ensureMessageContent(message: Message | {
80
80
  * @returns Normalized messages
81
81
  */
82
82
  export declare function normalizeMessages(messages: Message[]): Message[];
83
+ /**
84
+ * Repair tool use/result pairing issues in a message array.
85
+ *
86
+ * This function removes orphaned tool_result blocks (those without a matching tool_use)
87
+ * which can cause API errors like "function_response.name: Name cannot be empty" in Gemini.
88
+ *
89
+ * This is particularly useful after context compaction/summarization, which may remove
90
+ * tool_use messages while preserving tool_result messages in the "recent" window.
91
+ *
92
+ * @param messages - Array of messages to repair
93
+ * @returns New array with orphaned tool_results removed
94
+ */
95
+ export declare function repairToolPairing(messages: Message[]): Message[];
@@ -153,3 +153,54 @@ export function ensureMessageContent(message) {
153
153
  export function normalizeMessages(messages) {
154
154
  return messages.map(ensureMessageContent);
155
155
  }
156
+ /**
157
+ * Repair tool use/result pairing issues in a message array.
158
+ *
159
+ * This function removes orphaned tool_result blocks (those without a matching tool_use)
160
+ * which can cause API errors like "function_response.name: Name cannot be empty" in Gemini.
161
+ *
162
+ * This is particularly useful after context compaction/summarization, which may remove
163
+ * tool_use messages while preserving tool_result messages in the "recent" window.
164
+ *
165
+ * @param messages - Array of messages to repair
166
+ * @returns New array with orphaned tool_results removed
167
+ */
168
+ export function repairToolPairing(messages) {
169
+ // First, collect all tool_use IDs
170
+ const toolUseIds = new Set();
171
+ for (const message of messages) {
172
+ if (typeof message.content === 'string')
173
+ continue;
174
+ for (const block of message.content) {
175
+ if (block.type === 'tool_use') {
176
+ toolUseIds.add(block.id);
177
+ }
178
+ }
179
+ }
180
+ // Now filter out orphaned tool_results
181
+ const repairedMessages = [];
182
+ for (const message of messages) {
183
+ if (typeof message.content === 'string') {
184
+ repairedMessages.push(message);
185
+ continue;
186
+ }
187
+ // Filter content blocks to remove orphaned tool_results
188
+ const filteredBlocks = message.content.filter((block) => {
189
+ if (block.type === 'tool_result') {
190
+ // Only keep if there's a matching tool_use
191
+ return toolUseIds.has(block.toolUseId);
192
+ }
193
+ return true;
194
+ });
195
+ // If all blocks were removed, skip this message entirely
196
+ if (filteredBlocks.length === 0) {
197
+ continue;
198
+ }
199
+ // If some blocks remain, include the message with filtered content
200
+ repairedMessages.push({
201
+ ...message,
202
+ content: filteredBlocks,
203
+ });
204
+ }
205
+ return repairedMessages;
206
+ }
@@ -243,7 +243,12 @@ export class PermissionManager {
243
243
  };
244
244
  }
245
245
  case 'once': {
246
- // Always ask for permission
246
+ // Check if user explicitly granted session permission (via 'allow-always')
247
+ if (this.sessionGrants.has(toolName)) {
248
+ this.emit({ type: 'permission:granted', toolName, level, input, rule });
249
+ return { allowed: true, level, askedUser: false, rule };
250
+ }
251
+ // Ask for permission each time (unless session-granted above)
247
252
  const allowed = await this.askPermission(toolName, input, level, rule);
248
253
  if (allowed) {
249
254
  this.emit({ type: 'permission:granted', toolName, level, input, rule });