@compilr-dev/agents 0.0.1 → 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.
- package/dist/agent.d.ts +188 -1
- package/dist/agent.js +284 -14
- package/dist/context/file-tracker.d.ts +156 -0
- package/dist/context/file-tracker.js +358 -0
- package/dist/context/file-tracking-hook.d.ts +29 -0
- package/dist/context/file-tracking-hook.js +103 -0
- package/dist/context/index.d.ts +5 -1
- package/dist/context/index.js +3 -0
- package/dist/context/manager.d.ts +69 -1
- package/dist/context/manager.js +304 -0
- package/dist/context/types.d.ts +95 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.js +11 -3
- package/dist/messages/index.d.ts +13 -0
- package/dist/messages/index.js +51 -0
- package/dist/permissions/manager.js +6 -1
- package/dist/providers/gemini.d.ts +91 -0
- package/dist/providers/gemini.js +138 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +7 -3
- package/dist/providers/mock.js +8 -0
- package/dist/providers/ollama.d.ts +87 -0
- package/dist/providers/ollama.js +133 -0
- package/dist/providers/openai-compatible.d.ts +182 -0
- package/dist/providers/openai-compatible.js +357 -0
- package/dist/providers/openai.d.ts +93 -0
- package/dist/providers/openai.js +133 -0
- package/dist/skills/index.js +691 -0
- package/dist/tools/builtin/glob.d.ts +11 -0
- package/dist/tools/builtin/glob.js +44 -2
- package/dist/tools/builtin/grep.d.ts +11 -1
- package/dist/tools/builtin/grep.js +38 -2
- package/dist/tools/builtin/index.d.ts +6 -1
- package/dist/tools/builtin/index.js +7 -0
- package/dist/tools/builtin/suggest.d.ts +57 -0
- package/dist/tools/builtin/suggest.js +99 -0
- package/dist/tools/builtin/task.js +13 -8
- package/dist/tools/builtin/tool-names.d.ts +44 -0
- package/dist/tools/builtin/tool-names.js +51 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +5 -1
- package/dist/tools/registry.d.ts +4 -0
- package/dist/tools/registry.js +9 -0
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -7,11 +7,14 @@ import { ProjectMemoryLoader } from './memory/loader.js';
|
|
|
7
7
|
import { UsageTracker } from './costs/tracker.js';
|
|
8
8
|
import { DefaultToolRegistry } from './tools/registry.js';
|
|
9
9
|
import { ContextManager } from './context/manager.js';
|
|
10
|
+
import { FileAccessTracker } from './context/file-tracker.js';
|
|
11
|
+
import { createFileTrackingHook } from './context/file-tracking-hook.js';
|
|
10
12
|
import { AnchorManager } from './anchors/manager.js';
|
|
11
13
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
12
14
|
import { MaxIterationsError, ToolLoopError } from './errors.js';
|
|
13
15
|
import { generateSessionId, createAgentState, deserializeTodos } from './state/agent-state.js';
|
|
14
16
|
import { getDefaultTodoStore, createIsolatedTodoStore } from './tools/builtin/todo.js';
|
|
17
|
+
import { repairToolPairing } from './messages/index.js';
|
|
15
18
|
/**
|
|
16
19
|
* Agent class - orchestrates LLM interactions with tool use
|
|
17
20
|
*
|
|
@@ -40,6 +43,7 @@ export class Agent {
|
|
|
40
43
|
contextManager;
|
|
41
44
|
autoContextManagement;
|
|
42
45
|
onEvent;
|
|
46
|
+
onIterationLimitReached;
|
|
43
47
|
// State management
|
|
44
48
|
checkpointer;
|
|
45
49
|
_sessionId;
|
|
@@ -80,6 +84,10 @@ export class Agent {
|
|
|
80
84
|
* Hooks manager for lifecycle hooks
|
|
81
85
|
*/
|
|
82
86
|
hooksManager;
|
|
87
|
+
/**
|
|
88
|
+
* File access tracker for context restoration hints
|
|
89
|
+
*/
|
|
90
|
+
fileTracker;
|
|
83
91
|
constructor(config) {
|
|
84
92
|
this.provider = config.provider;
|
|
85
93
|
this.systemPrompt = config.systemPrompt ?? '';
|
|
@@ -87,11 +95,16 @@ export class Agent {
|
|
|
87
95
|
this.maxConsecutiveToolCalls = config.maxConsecutiveToolCalls ?? 3;
|
|
88
96
|
this.iterationLimitBehavior = config.iterationLimitBehavior ?? 'error';
|
|
89
97
|
this.chatOptions = config.chatOptions ?? {};
|
|
90
|
-
this.toolRegistry =
|
|
98
|
+
this.toolRegistry =
|
|
99
|
+
config.toolRegistry ??
|
|
100
|
+
new DefaultToolRegistry({
|
|
101
|
+
defaultTimeoutMs: config.toolTimeoutMs,
|
|
102
|
+
});
|
|
91
103
|
this.contextManager = config.contextManager;
|
|
92
104
|
this.autoContextManagement =
|
|
93
105
|
config.autoContextManagement ?? config.contextManager !== undefined;
|
|
94
106
|
this.onEvent = config.onEvent;
|
|
107
|
+
this.onIterationLimitReached = config.onIterationLimitReached;
|
|
95
108
|
// State management
|
|
96
109
|
this.checkpointer = config.checkpointer;
|
|
97
110
|
this._sessionId = config.sessionId ?? generateSessionId();
|
|
@@ -156,8 +169,18 @@ export class Agent {
|
|
|
156
169
|
}
|
|
157
170
|
});
|
|
158
171
|
}
|
|
159
|
-
//
|
|
160
|
-
if (config.
|
|
172
|
+
// File tracking for context restoration hints
|
|
173
|
+
if (config.enableFileTracking && config.contextManager) {
|
|
174
|
+
this.fileTracker = new FileAccessTracker();
|
|
175
|
+
const trackingHook = createFileTrackingHook(this.fileTracker);
|
|
176
|
+
// Merge with existing hooks or create new hooks config
|
|
177
|
+
const hooksConfig = config.hooks ?? {};
|
|
178
|
+
hooksConfig.afterTool = hooksConfig.afterTool ?? [];
|
|
179
|
+
hooksConfig.afterTool.push(trackingHook);
|
|
180
|
+
this.hooksManager = new HooksManager({ hooks: hooksConfig });
|
|
181
|
+
}
|
|
182
|
+
else if (config.hooks !== undefined) {
|
|
183
|
+
// Hooks manager without file tracking
|
|
161
184
|
this.hooksManager = new HooksManager({ hooks: config.hooks });
|
|
162
185
|
}
|
|
163
186
|
}
|
|
@@ -684,6 +707,17 @@ export class Agent {
|
|
|
684
707
|
getHistory() {
|
|
685
708
|
return [...this.conversationHistory];
|
|
686
709
|
}
|
|
710
|
+
/**
|
|
711
|
+
* Set the conversation history (for manual compaction/restoration)
|
|
712
|
+
* Also updates the context manager's token count if configured.
|
|
713
|
+
*/
|
|
714
|
+
async setHistory(messages) {
|
|
715
|
+
this.conversationHistory = [...messages];
|
|
716
|
+
if (this.contextManager) {
|
|
717
|
+
await this.contextManager.updateTokenCount(messages);
|
|
718
|
+
}
|
|
719
|
+
return this;
|
|
720
|
+
}
|
|
687
721
|
/**
|
|
688
722
|
* Get the context manager (if configured)
|
|
689
723
|
*/
|
|
@@ -702,6 +736,173 @@ export class Agent {
|
|
|
702
736
|
getVerbosityLevel() {
|
|
703
737
|
return this.contextManager?.getVerbosityLevel() ?? 'full';
|
|
704
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Get the file access tracker (if file tracking is enabled)
|
|
741
|
+
*/
|
|
742
|
+
getFileTracker() {
|
|
743
|
+
return this.fileTracker;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Format context restoration hints based on tracked file accesses.
|
|
747
|
+
* Returns empty string if no files have been accessed or file tracking is disabled.
|
|
748
|
+
*/
|
|
749
|
+
formatRestorationHints() {
|
|
750
|
+
return (this.fileTracker?.formatRestorationHints({
|
|
751
|
+
verbosityLevel: this.getVerbosityLevel(),
|
|
752
|
+
}) ?? '');
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Inject context restoration hints into messages after compaction/summarization.
|
|
756
|
+
* Modifies messages array in place if hints are available.
|
|
757
|
+
*
|
|
758
|
+
* @internal
|
|
759
|
+
*/
|
|
760
|
+
injectRestorationHints(messages) {
|
|
761
|
+
if (!this.fileTracker || this.fileTracker.size === 0) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const hints = this.formatRestorationHints();
|
|
765
|
+
if (!hints) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
// Inject as a user message after the last system message
|
|
769
|
+
const systemIndex = messages.findIndex((m) => m.role === 'system');
|
|
770
|
+
const insertIndex = systemIndex >= 0 ? systemIndex + 1 : 0;
|
|
771
|
+
messages.splice(insertIndex, 0, {
|
|
772
|
+
role: 'user',
|
|
773
|
+
content: hints,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
// ==========================================================================
|
|
777
|
+
// Context Compaction
|
|
778
|
+
// ==========================================================================
|
|
779
|
+
/**
|
|
780
|
+
* Compact the conversation context to reduce token usage.
|
|
781
|
+
*
|
|
782
|
+
* This is the recommended way to trigger context compaction externally.
|
|
783
|
+
* It handles:
|
|
784
|
+
* 1. Summarizing older messages
|
|
785
|
+
* 2. Repairing tool use/result pairing (prevents API errors)
|
|
786
|
+
* 3. Injecting context restoration hints (if file tracking is enabled)
|
|
787
|
+
* 4. Updating the conversation history
|
|
788
|
+
*
|
|
789
|
+
* @param options - Compaction options
|
|
790
|
+
* @returns Compaction result with statistics
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```typescript
|
|
794
|
+
* // Basic compaction
|
|
795
|
+
* const result = await agent.compact();
|
|
796
|
+
* console.log(`Reduced from ${result.originalTokens} to ${result.summaryTokens} tokens`);
|
|
797
|
+
*
|
|
798
|
+
* // Compaction without restoration hints
|
|
799
|
+
* await agent.compact({ injectRestorationHints: false });
|
|
800
|
+
*
|
|
801
|
+
* // Emergency compaction (more aggressive)
|
|
802
|
+
* await agent.compact({ emergency: true });
|
|
803
|
+
* ```
|
|
804
|
+
*/
|
|
805
|
+
async compact(options) {
|
|
806
|
+
// Check if context manager is available
|
|
807
|
+
if (!this.contextManager) {
|
|
808
|
+
return {
|
|
809
|
+
success: false,
|
|
810
|
+
originalTokens: 0,
|
|
811
|
+
summaryTokens: 0,
|
|
812
|
+
rounds: 0,
|
|
813
|
+
messagesPreserved: this.conversationHistory.length,
|
|
814
|
+
restorationHintsInjected: false,
|
|
815
|
+
toolResultsRepaired: 0,
|
|
816
|
+
summary: '',
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const shouldInjectHints = options?.injectRestorationHints !== false && this.fileTracker;
|
|
820
|
+
const useSmartCompaction = options?.useSmartCompaction !== false; // Default to true
|
|
821
|
+
// Build full message list including system prompt
|
|
822
|
+
const messages = this.systemPrompt
|
|
823
|
+
? [{ role: 'system', content: this.systemPrompt }, ...this.conversationHistory]
|
|
824
|
+
: [...this.conversationHistory];
|
|
825
|
+
let summarized;
|
|
826
|
+
let originalTokens;
|
|
827
|
+
let summaryTokens;
|
|
828
|
+
let rounds;
|
|
829
|
+
let messagesPreserved;
|
|
830
|
+
let summary;
|
|
831
|
+
let filesCreated;
|
|
832
|
+
let categoryStats;
|
|
833
|
+
if (useSmartCompaction) {
|
|
834
|
+
// Use smart category-aware compaction
|
|
835
|
+
const { messages: compactedMessages, result: smartResult } = await this.contextManager.smartCompact(messages, {
|
|
836
|
+
generateSummary: (msgs) => this.generateSummary(msgs),
|
|
837
|
+
saveToFile: async (content, index) => {
|
|
838
|
+
// Save to a temp file and return the path
|
|
839
|
+
const path = `/tmp/compacted-tool-result-${String(Date.now())}-${String(index)}.txt`;
|
|
840
|
+
const { writeFile } = await import('node:fs/promises');
|
|
841
|
+
await writeFile(path, content, 'utf-8');
|
|
842
|
+
return path;
|
|
843
|
+
},
|
|
844
|
+
emergency: options?.emergency,
|
|
845
|
+
targetUtilization: options?.targetUtilization,
|
|
846
|
+
});
|
|
847
|
+
summarized = compactedMessages;
|
|
848
|
+
originalTokens = smartResult.tokensBefore;
|
|
849
|
+
summaryTokens = smartResult.tokensAfter;
|
|
850
|
+
rounds = smartResult.summarizationRounds;
|
|
851
|
+
summary = smartResult.summary || '';
|
|
852
|
+
filesCreated = smartResult.filesCreated;
|
|
853
|
+
categoryStats = smartResult.categoryStats;
|
|
854
|
+
// Count preserved messages (system + recent)
|
|
855
|
+
messagesPreserved = compactedMessages.filter((m) => m.role === 'system' || categoryStats?.recentMessages.action === 'preserved').length;
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
// Use simple summarization (legacy behavior)
|
|
859
|
+
const { messages: summarizedMessages, result } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs), {
|
|
860
|
+
emergency: options.emergency,
|
|
861
|
+
targetUtilization: options.targetUtilization,
|
|
862
|
+
});
|
|
863
|
+
summarized = summarizedMessages;
|
|
864
|
+
originalTokens = result.originalTokens;
|
|
865
|
+
summaryTokens = result.summaryTokens;
|
|
866
|
+
rounds = result.rounds;
|
|
867
|
+
messagesPreserved = result.messagesPreserved;
|
|
868
|
+
summary = result.summary;
|
|
869
|
+
}
|
|
870
|
+
// Repair tool pairing issues (fixes Gemini API errors)
|
|
871
|
+
const repaired = repairToolPairing(summarized);
|
|
872
|
+
const toolResultsRepaired = summarized.length - repaired.length;
|
|
873
|
+
// Inject restoration hints if enabled
|
|
874
|
+
let restorationHintsInjected = false;
|
|
875
|
+
if (shouldInjectHints) {
|
|
876
|
+
const sizeBefore = repaired.length;
|
|
877
|
+
this.injectRestorationHints(repaired);
|
|
878
|
+
restorationHintsInjected = repaired.length > sizeBefore;
|
|
879
|
+
}
|
|
880
|
+
// Extract new history (skip system message)
|
|
881
|
+
const newHistory = repaired.filter((m) => m.role !== 'system');
|
|
882
|
+
// Update conversation history
|
|
883
|
+
this.conversationHistory = newHistory;
|
|
884
|
+
// Update context manager's token count
|
|
885
|
+
await this.contextManager.updateTokenCount(repaired);
|
|
886
|
+
// Emit event
|
|
887
|
+
this.onEvent?.({
|
|
888
|
+
type: 'context_summarized',
|
|
889
|
+
tokensBefore: originalTokens,
|
|
890
|
+
tokensAfter: summaryTokens,
|
|
891
|
+
rounds,
|
|
892
|
+
});
|
|
893
|
+
return {
|
|
894
|
+
success: true,
|
|
895
|
+
originalTokens,
|
|
896
|
+
summaryTokens,
|
|
897
|
+
rounds,
|
|
898
|
+
messagesPreserved,
|
|
899
|
+
restorationHintsInjected,
|
|
900
|
+
toolResultsRepaired,
|
|
901
|
+
summary,
|
|
902
|
+
filesCreated,
|
|
903
|
+
categoryStats,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
705
906
|
// ==========================================================================
|
|
706
907
|
// State Management
|
|
707
908
|
// ==========================================================================
|
|
@@ -902,8 +1103,9 @@ export class Agent {
|
|
|
902
1103
|
maxContextTokens: subAgentMaxTokens,
|
|
903
1104
|
},
|
|
904
1105
|
});
|
|
905
|
-
// Create tool registry for sub-agent
|
|
906
|
-
const
|
|
1106
|
+
// Create tool registry for sub-agent, inheriting timeout settings from parent
|
|
1107
|
+
const parentRegistryOptions = this.toolRegistry.getOptions();
|
|
1108
|
+
const subAgentToolRegistry = new DefaultToolRegistry(parentRegistryOptions);
|
|
907
1109
|
// If tools specified, use those; otherwise inherit from parent
|
|
908
1110
|
const toolsToRegister = config.tools ??
|
|
909
1111
|
this.toolRegistry
|
|
@@ -1177,7 +1379,7 @@ export class Agent {
|
|
|
1177
1379
|
* Run the agent with a user message
|
|
1178
1380
|
*/
|
|
1179
1381
|
async run(userMessage, options) {
|
|
1180
|
-
|
|
1382
|
+
let maxIterations = options?.maxIterations ?? this.maxIterations;
|
|
1181
1383
|
const chatOptions = { ...this.chatOptions, ...options?.chatOptions };
|
|
1182
1384
|
const signal = options?.signal;
|
|
1183
1385
|
// Combined event emitter
|
|
@@ -1232,6 +1434,8 @@ export class Agent {
|
|
|
1232
1434
|
// Perform emergency summarization
|
|
1233
1435
|
const { messages: summarized, result } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs), { emergency: true });
|
|
1234
1436
|
messages = summarized;
|
|
1437
|
+
// Inject restoration hints after summarization
|
|
1438
|
+
this.injectRestorationHints(messages);
|
|
1235
1439
|
emit({
|
|
1236
1440
|
type: 'context_summarized',
|
|
1237
1441
|
tokensBefore: result.originalTokens,
|
|
@@ -1290,6 +1494,11 @@ export class Agent {
|
|
|
1290
1494
|
}
|
|
1291
1495
|
// Get tool definitions
|
|
1292
1496
|
let tools = this.toolRegistry.getDefinitions();
|
|
1497
|
+
// Apply tool filter if specified (reduces token usage)
|
|
1498
|
+
if (options?.toolFilter && options.toolFilter.length > 0) {
|
|
1499
|
+
const filterSet = new Set(options.toolFilter);
|
|
1500
|
+
tools = tools.filter((tool) => filterSet.has(tool.name));
|
|
1501
|
+
}
|
|
1293
1502
|
// Run beforeLLM hooks (can modify messages and tools)
|
|
1294
1503
|
if (this.hooksManager) {
|
|
1295
1504
|
const llmHookResult = await this.hooksManager.runBeforeLLM({
|
|
@@ -1374,12 +1583,15 @@ export class Agent {
|
|
|
1374
1583
|
// If no tool uses, we're done
|
|
1375
1584
|
if (toolUses.length === 0) {
|
|
1376
1585
|
finalResponse = text;
|
|
1377
|
-
// Add final assistant response to history
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1586
|
+
// Add final assistant response to history (only if non-empty)
|
|
1587
|
+
// Empty responses can occur after silent tools like 'suggest'
|
|
1588
|
+
if (text) {
|
|
1589
|
+
const finalAssistantMsg = {
|
|
1590
|
+
role: 'assistant',
|
|
1591
|
+
content: text,
|
|
1592
|
+
};
|
|
1593
|
+
newMessages.push(finalAssistantMsg);
|
|
1594
|
+
}
|
|
1383
1595
|
// Run afterIteration hooks
|
|
1384
1596
|
if (this.hooksManager) {
|
|
1385
1597
|
await this.hooksManager.runAfterIteration({
|
|
@@ -1434,11 +1646,26 @@ export class Agent {
|
|
|
1434
1646
|
success: false,
|
|
1435
1647
|
error: `Permission denied: ${permResult.reason ?? 'Tool execution not allowed'}`,
|
|
1436
1648
|
};
|
|
1437
|
-
//
|
|
1649
|
+
// Emit tool_end and record the tool call
|
|
1438
1650
|
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1439
1651
|
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1440
1652
|
toolCalls.push(toolCallEntry);
|
|
1441
1653
|
iterationToolCalls.push(toolCallEntry);
|
|
1654
|
+
// CRITICAL: Add tool_result message to messages array
|
|
1655
|
+
// Claude API requires every tool_use to have a corresponding tool_result
|
|
1656
|
+
const toolResultMsg = {
|
|
1657
|
+
role: 'user',
|
|
1658
|
+
content: [
|
|
1659
|
+
{
|
|
1660
|
+
type: 'tool_result',
|
|
1661
|
+
toolUseId: toolUse.id,
|
|
1662
|
+
content: `Error: ${result.error ?? 'Permission denied'}`,
|
|
1663
|
+
isError: true,
|
|
1664
|
+
},
|
|
1665
|
+
],
|
|
1666
|
+
};
|
|
1667
|
+
messages.push(toolResultMsg);
|
|
1668
|
+
newMessages.push(toolResultMsg);
|
|
1442
1669
|
continue;
|
|
1443
1670
|
}
|
|
1444
1671
|
emit({ type: 'permission_granted', toolName: toolUse.name, level: permResult.level });
|
|
@@ -1456,11 +1683,26 @@ export class Agent {
|
|
|
1456
1683
|
success: false,
|
|
1457
1684
|
error: `Guardrail blocked: ${message}`,
|
|
1458
1685
|
};
|
|
1459
|
-
//
|
|
1686
|
+
// Emit tool_end and record the tool call
|
|
1460
1687
|
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1461
1688
|
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1462
1689
|
toolCalls.push(toolCallEntry);
|
|
1463
1690
|
iterationToolCalls.push(toolCallEntry);
|
|
1691
|
+
// CRITICAL: Add tool_result message to messages array
|
|
1692
|
+
// Claude API requires every tool_use to have a corresponding tool_result
|
|
1693
|
+
const toolResultMsg = {
|
|
1694
|
+
role: 'user',
|
|
1695
|
+
content: [
|
|
1696
|
+
{
|
|
1697
|
+
type: 'tool_result',
|
|
1698
|
+
toolUseId: toolUse.id,
|
|
1699
|
+
content: `Error: ${result.error ?? 'Blocked by guardrail'}`,
|
|
1700
|
+
isError: true,
|
|
1701
|
+
},
|
|
1702
|
+
],
|
|
1703
|
+
};
|
|
1704
|
+
messages.push(toolResultMsg);
|
|
1705
|
+
newMessages.push(toolResultMsg);
|
|
1464
1706
|
continue;
|
|
1465
1707
|
}
|
|
1466
1708
|
else if (guardrailResult.action === 'warn') {
|
|
@@ -1586,6 +1828,8 @@ export class Agent {
|
|
|
1586
1828
|
const tokensBefore = this.contextManager.getTokenCount();
|
|
1587
1829
|
const compactResult = await this.contextManager.compactCategory(messages, 'toolResults', (content, index) => Promise.resolve(`[compacted_tool_result_${String(index)}]`));
|
|
1588
1830
|
messages = compactResult.messages;
|
|
1831
|
+
// Inject restoration hints after compaction
|
|
1832
|
+
this.injectRestorationHints(messages);
|
|
1589
1833
|
emit({
|
|
1590
1834
|
type: 'context_compacted',
|
|
1591
1835
|
tokensBefore,
|
|
@@ -1596,6 +1840,8 @@ export class Agent {
|
|
|
1596
1840
|
// Approaching context limit - summarize
|
|
1597
1841
|
const { messages: summarized, result: sumResult } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs));
|
|
1598
1842
|
messages = summarized;
|
|
1843
|
+
// Inject restoration hints after summarization
|
|
1844
|
+
this.injectRestorationHints(messages);
|
|
1599
1845
|
emit({
|
|
1600
1846
|
type: 'context_summarized',
|
|
1601
1847
|
tokensBefore: sumResult.originalTokens,
|
|
@@ -1636,6 +1882,30 @@ export class Agent {
|
|
|
1636
1882
|
});
|
|
1637
1883
|
}
|
|
1638
1884
|
emit({ type: 'iteration_end', iteration: iterations });
|
|
1885
|
+
// Check if we're about to hit the iteration limit
|
|
1886
|
+
// If callback is defined, ask if we should continue
|
|
1887
|
+
if (iterations >= maxIterations && this.onIterationLimitReached) {
|
|
1888
|
+
emit({
|
|
1889
|
+
type: 'iteration_limit_reached',
|
|
1890
|
+
iteration: iterations,
|
|
1891
|
+
maxIterations,
|
|
1892
|
+
});
|
|
1893
|
+
const result = await this.onIterationLimitReached({
|
|
1894
|
+
iteration: iterations,
|
|
1895
|
+
maxIterations,
|
|
1896
|
+
toolCallCount: toolCalls.length,
|
|
1897
|
+
});
|
|
1898
|
+
if (typeof result === 'number' && result > 0) {
|
|
1899
|
+
// Extend the limit and continue
|
|
1900
|
+
maxIterations += result;
|
|
1901
|
+
emit({
|
|
1902
|
+
type: 'iteration_limit_extended',
|
|
1903
|
+
newMaxIterations: maxIterations,
|
|
1904
|
+
addedIterations: result,
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
// If false or 0, loop will exit naturally on next condition check
|
|
1908
|
+
}
|
|
1639
1909
|
}
|
|
1640
1910
|
// Check if we hit max iterations without completing
|
|
1641
1911
|
if (!aborted && iterations >= maxIterations && finalResponse === '') {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Access Tracker
|
|
3
|
+
*
|
|
4
|
+
* Tracks file accesses during agent sessions to provide context restoration
|
|
5
|
+
* hints after compaction. Inspired by Claude Code's post-compaction file
|
|
6
|
+
* reference display.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Track read, referenced, and modified files
|
|
10
|
+
* - Deduplication (read supersedes reference)
|
|
11
|
+
* - Max entries with LRU eviction
|
|
12
|
+
* - Verbosity-aware formatting
|
|
13
|
+
*
|
|
14
|
+
* @see /workspace/project-docs/00-requirements/compilr-dev-agents/context-restoration-hints.md
|
|
15
|
+
*/
|
|
16
|
+
import type { VerbosityLevel } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Type of file access
|
|
19
|
+
*/
|
|
20
|
+
export type FileAccessType = 'read' | 'referenced' | 'modified';
|
|
21
|
+
/**
|
|
22
|
+
* Record of a file access
|
|
23
|
+
*/
|
|
24
|
+
export interface FileAccess {
|
|
25
|
+
/** Absolute file path */
|
|
26
|
+
path: string;
|
|
27
|
+
/** Type of access */
|
|
28
|
+
type: FileAccessType;
|
|
29
|
+
/** When accessed (timestamp) */
|
|
30
|
+
timestamp: number;
|
|
31
|
+
/** Number of lines (for 'read' type) */
|
|
32
|
+
lineCount?: number;
|
|
33
|
+
/** Optional summary of what was found/changed */
|
|
34
|
+
summary?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Options for FileAccessTracker constructor
|
|
38
|
+
*/
|
|
39
|
+
export interface FileAccessTrackerOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Maximum number of entries to track
|
|
42
|
+
* @default 100
|
|
43
|
+
*/
|
|
44
|
+
maxEntries?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Deduplicate references (don't track same file twice as 'referenced')
|
|
47
|
+
* @default true
|
|
48
|
+
*/
|
|
49
|
+
deduplicateReferences?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Options for formatting restoration hints
|
|
53
|
+
*/
|
|
54
|
+
export interface FormatHintsOptions {
|
|
55
|
+
/** Include line counts for read files @default true */
|
|
56
|
+
includeLineCount?: boolean;
|
|
57
|
+
/** Include summaries @default false */
|
|
58
|
+
includeSummary?: boolean;
|
|
59
|
+
/** Include timestamps @default false */
|
|
60
|
+
includeTimestamp?: boolean;
|
|
61
|
+
/** Group by access type @default true */
|
|
62
|
+
groupByType?: boolean;
|
|
63
|
+
/** Maximum files to include @default 20 */
|
|
64
|
+
maxFiles?: number;
|
|
65
|
+
/** Verbosity level (adjusts format automatically) */
|
|
66
|
+
verbosityLevel?: VerbosityLevel;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Statistics about file accesses
|
|
70
|
+
*/
|
|
71
|
+
export interface FileAccessStats {
|
|
72
|
+
read: number;
|
|
73
|
+
referenced: number;
|
|
74
|
+
modified: number;
|
|
75
|
+
total: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Tracks file accesses during agent sessions for context restoration hints.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const tracker = new FileAccessTracker();
|
|
83
|
+
*
|
|
84
|
+
* // Track file accesses
|
|
85
|
+
* tracker.trackRead('/path/to/file.ts', 597);
|
|
86
|
+
* tracker.trackReference('/path/to/other.ts');
|
|
87
|
+
* tracker.trackModification('/path/to/file.ts', 'Added function');
|
|
88
|
+
*
|
|
89
|
+
* // Get restoration hints after compaction
|
|
90
|
+
* const hints = tracker.formatRestorationHints();
|
|
91
|
+
* console.log(hints);
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare class FileAccessTracker {
|
|
95
|
+
private readonly accesses;
|
|
96
|
+
private readonly maxEntries;
|
|
97
|
+
private readonly deduplicateReferences;
|
|
98
|
+
constructor(options?: FileAccessTrackerOptions);
|
|
99
|
+
/**
|
|
100
|
+
* Track a file that was fully read
|
|
101
|
+
*/
|
|
102
|
+
trackRead(filePath: string, lineCount: number, summary?: string): void;
|
|
103
|
+
/**
|
|
104
|
+
* Track a file that was referenced (e.g., appeared in grep/glob results)
|
|
105
|
+
*/
|
|
106
|
+
trackReference(filePath: string): void;
|
|
107
|
+
/**
|
|
108
|
+
* Track a file that was modified (written or edited)
|
|
109
|
+
*/
|
|
110
|
+
trackModification(filePath: string, summary?: string): void;
|
|
111
|
+
/**
|
|
112
|
+
* Get all file accesses, optionally filtered
|
|
113
|
+
*/
|
|
114
|
+
getAccesses(options?: {
|
|
115
|
+
type?: FileAccessType;
|
|
116
|
+
since?: number;
|
|
117
|
+
}): FileAccess[];
|
|
118
|
+
/**
|
|
119
|
+
* Get most recent file accesses
|
|
120
|
+
*/
|
|
121
|
+
getRecentAccesses(limit?: number): FileAccess[];
|
|
122
|
+
/**
|
|
123
|
+
* Check if a file has been accessed
|
|
124
|
+
*/
|
|
125
|
+
hasAccessed(filePath: string): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Get access record for a specific file
|
|
128
|
+
*/
|
|
129
|
+
getAccess(filePath: string): FileAccess | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* Get statistics about file accesses
|
|
132
|
+
*/
|
|
133
|
+
getStats(): FileAccessStats;
|
|
134
|
+
/**
|
|
135
|
+
* Format restoration hints for injection after compaction
|
|
136
|
+
*/
|
|
137
|
+
formatRestorationHints(options?: FormatHintsOptions): string;
|
|
138
|
+
/**
|
|
139
|
+
* Clear all tracked accesses
|
|
140
|
+
*/
|
|
141
|
+
clear(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Get the number of tracked files
|
|
144
|
+
*/
|
|
145
|
+
get size(): number;
|
|
146
|
+
private normalizePath;
|
|
147
|
+
private enforceMaxEntries;
|
|
148
|
+
private getEffectiveMaxFiles;
|
|
149
|
+
private formatCompact;
|
|
150
|
+
private formatGrouped;
|
|
151
|
+
private formatFlat;
|
|
152
|
+
private formatAccessLine;
|
|
153
|
+
private groupByType;
|
|
154
|
+
private getDisplayName;
|
|
155
|
+
private getDisplayPath;
|
|
156
|
+
}
|