@compilr-dev/agents 0.1.0 → 0.2.1
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 +168 -1
- package/dist/agent.js +268 -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 +5 -5
- package/dist/index.js +7 -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.js +1 -3
- package/dist/providers/mock.js +8 -0
- package/dist/providers/openai-compatible.js +1 -3
- package/dist/skills/index.js +691 -0
- 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 +4 -4
package/dist/agent.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { LLMProvider, Message, ChatOptions, StreamChunk } from './providers/types.js';
|
|
5
5
|
import type { Tool, ToolDefinition, ToolRegistry, ToolExecutionResult } from './tools/types.js';
|
|
6
|
-
import type { ContextStats, VerbosityLevel } from './context/types.js';
|
|
6
|
+
import type { ContextStats, VerbosityLevel, SmartCompactionResult } from './context/types.js';
|
|
7
7
|
import type { AgentState, Checkpointer, SessionMetadata } from './state/types.js';
|
|
8
8
|
import type { Anchor, AnchorInput, AnchorQueryOptions, AnchorClearOptions, AnchorManagerOptions } from './anchors/types.js';
|
|
9
9
|
import type { Guardrail, GuardrailInput, GuardrailResult, GuardrailManagerOptions } from './guardrails/types.js';
|
|
@@ -13,6 +13,7 @@ import type { UsageTrackerOptions, UsageStats, BudgetStatus, TokenUsage } from '
|
|
|
13
13
|
import type { HooksConfig } from './hooks/types.js';
|
|
14
14
|
import { PermissionManager } from './permissions/manager.js';
|
|
15
15
|
import { ContextManager } from './context/manager.js';
|
|
16
|
+
import { FileAccessTracker } from './context/file-tracker.js';
|
|
16
17
|
import { AnchorManager } from './anchors/manager.js';
|
|
17
18
|
import { GuardrailManager } from './guardrails/manager.js';
|
|
18
19
|
/**
|
|
@@ -125,6 +126,18 @@ export type AgentEvent = {
|
|
|
125
126
|
} | {
|
|
126
127
|
type: 'usage_budget_exceeded';
|
|
127
128
|
status: BudgetStatus;
|
|
129
|
+
} | {
|
|
130
|
+
type: 'suggest';
|
|
131
|
+
action: string;
|
|
132
|
+
reason?: string;
|
|
133
|
+
} | {
|
|
134
|
+
type: 'iteration_limit_reached';
|
|
135
|
+
iteration: number;
|
|
136
|
+
maxIterations: number;
|
|
137
|
+
} | {
|
|
138
|
+
type: 'iteration_limit_extended';
|
|
139
|
+
newMaxIterations: number;
|
|
140
|
+
addedIterations: number;
|
|
128
141
|
};
|
|
129
142
|
/**
|
|
130
143
|
* Event handler function type
|
|
@@ -189,8 +202,36 @@ export interface AgentConfig {
|
|
|
189
202
|
* - 'error': Throw MaxIterationsError immediately
|
|
190
203
|
* - 'summarize': Generate a final summary response before throwing
|
|
191
204
|
* - 'continue': Return partial result without throwing (response will be empty)
|
|
205
|
+
*
|
|
206
|
+
* Note: If onIterationLimitReached callback is provided, it takes precedence.
|
|
192
207
|
*/
|
|
193
208
|
iterationLimitBehavior?: 'error' | 'summarize' | 'continue';
|
|
209
|
+
/**
|
|
210
|
+
* Callback invoked when the agent reaches its iteration limit.
|
|
211
|
+
* Allows the caller to decide whether to continue or stop.
|
|
212
|
+
*
|
|
213
|
+
* @param context - Information about the current state
|
|
214
|
+
* @returns Promise resolving to:
|
|
215
|
+
* - `false` to stop the agent gracefully
|
|
216
|
+
* - A positive number to continue with that many additional iterations
|
|
217
|
+
*
|
|
218
|
+
* If this callback is provided and returns a number, the agent will continue
|
|
219
|
+
* running with the extended iteration limit. This takes precedence over
|
|
220
|
+
* iterationLimitBehavior.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* onIterationLimitReached: async ({ iteration, toolCallCount }) => {
|
|
225
|
+
* const answer = await askUser(`Agent used ${iteration} iterations. Continue?`);
|
|
226
|
+
* return answer === 'yes' ? 50 : false; // Add 50 more or stop
|
|
227
|
+
* }
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
onIterationLimitReached?: (context: {
|
|
231
|
+
iteration: number;
|
|
232
|
+
maxIterations: number;
|
|
233
|
+
toolCallCount: number;
|
|
234
|
+
}) => Promise<number | false>;
|
|
194
235
|
/**
|
|
195
236
|
* Chat options (model, temperature, etc.)
|
|
196
237
|
*/
|
|
@@ -199,6 +240,12 @@ export interface AgentConfig {
|
|
|
199
240
|
* Custom tool registry (optional, creates new one if not provided)
|
|
200
241
|
*/
|
|
201
242
|
toolRegistry?: ToolRegistry;
|
|
243
|
+
/**
|
|
244
|
+
* Default timeout for tool execution in milliseconds (default: 30000 = 30s).
|
|
245
|
+
* Set to 0 to disable timeout. Only used when toolRegistry is not provided.
|
|
246
|
+
* Sub-agents inherit this timeout from the parent agent.
|
|
247
|
+
*/
|
|
248
|
+
toolTimeoutMs?: number;
|
|
202
249
|
/**
|
|
203
250
|
* Context manager for tracking and managing context window usage.
|
|
204
251
|
* If not provided, context management is disabled.
|
|
@@ -428,6 +475,31 @@ export interface AgentConfig {
|
|
|
428
475
|
* ```
|
|
429
476
|
*/
|
|
430
477
|
hooks?: HooksConfig;
|
|
478
|
+
/**
|
|
479
|
+
* Enable file access tracking for context restoration hints.
|
|
480
|
+
*
|
|
481
|
+
* When enabled, the agent tracks which files were read, referenced (grep/glob),
|
|
482
|
+
* and modified during execution. After context compaction, these hints are
|
|
483
|
+
* injected to help the LLM understand what files it previously accessed.
|
|
484
|
+
*
|
|
485
|
+
* Requires contextManager to be set (hints are injected after compaction).
|
|
486
|
+
*
|
|
487
|
+
* @default false
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* const agent = new Agent({
|
|
492
|
+
* provider,
|
|
493
|
+
* contextManager: new ContextManager({ provider }),
|
|
494
|
+
* enableFileTracking: true, // Automatically track file accesses
|
|
495
|
+
* });
|
|
496
|
+
*
|
|
497
|
+
* // After compaction, the agent will inject hints like:
|
|
498
|
+
* // [Context compacted. Previously accessed files:]
|
|
499
|
+
* // Read (3 files): file1.ts (100 lines), file2.ts (50 lines)...
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
enableFileTracking?: boolean;
|
|
431
503
|
}
|
|
432
504
|
/**
|
|
433
505
|
* Options for a single run
|
|
@@ -631,6 +703,7 @@ export declare class Agent {
|
|
|
631
703
|
private readonly contextManager?;
|
|
632
704
|
private readonly autoContextManagement;
|
|
633
705
|
private readonly onEvent?;
|
|
706
|
+
private readonly onIterationLimitReached?;
|
|
634
707
|
private readonly checkpointer?;
|
|
635
708
|
private readonly _sessionId;
|
|
636
709
|
private readonly autoCheckpoint;
|
|
@@ -670,6 +743,10 @@ export declare class Agent {
|
|
|
670
743
|
* Hooks manager for lifecycle hooks
|
|
671
744
|
*/
|
|
672
745
|
private readonly hooksManager?;
|
|
746
|
+
/**
|
|
747
|
+
* File access tracker for context restoration hints
|
|
748
|
+
*/
|
|
749
|
+
private readonly fileTracker?;
|
|
673
750
|
constructor(config: AgentConfig);
|
|
674
751
|
/**
|
|
675
752
|
* Create an agent with project memory loaded from files.
|
|
@@ -1065,6 +1142,96 @@ export declare class Agent {
|
|
|
1065
1142
|
* Get current verbosity level based on context pressure
|
|
1066
1143
|
*/
|
|
1067
1144
|
getVerbosityLevel(): VerbosityLevel;
|
|
1145
|
+
/**
|
|
1146
|
+
* Get the file access tracker (if file tracking is enabled)
|
|
1147
|
+
*/
|
|
1148
|
+
getFileTracker(): FileAccessTracker | undefined;
|
|
1149
|
+
/**
|
|
1150
|
+
* Format context restoration hints based on tracked file accesses.
|
|
1151
|
+
* Returns empty string if no files have been accessed or file tracking is disabled.
|
|
1152
|
+
*/
|
|
1153
|
+
formatRestorationHints(): string;
|
|
1154
|
+
/**
|
|
1155
|
+
* Inject context restoration hints into messages after compaction/summarization.
|
|
1156
|
+
* Modifies messages array in place if hints are available.
|
|
1157
|
+
*
|
|
1158
|
+
* @internal
|
|
1159
|
+
*/
|
|
1160
|
+
private injectRestorationHints;
|
|
1161
|
+
/**
|
|
1162
|
+
* Compact the conversation context to reduce token usage.
|
|
1163
|
+
*
|
|
1164
|
+
* This is the recommended way to trigger context compaction externally.
|
|
1165
|
+
* It handles:
|
|
1166
|
+
* 1. Summarizing older messages
|
|
1167
|
+
* 2. Repairing tool use/result pairing (prevents API errors)
|
|
1168
|
+
* 3. Injecting context restoration hints (if file tracking is enabled)
|
|
1169
|
+
* 4. Updating the conversation history
|
|
1170
|
+
*
|
|
1171
|
+
* @param options - Compaction options
|
|
1172
|
+
* @returns Compaction result with statistics
|
|
1173
|
+
*
|
|
1174
|
+
* @example
|
|
1175
|
+
* ```typescript
|
|
1176
|
+
* // Basic compaction
|
|
1177
|
+
* const result = await agent.compact();
|
|
1178
|
+
* console.log(`Reduced from ${result.originalTokens} to ${result.summaryTokens} tokens`);
|
|
1179
|
+
*
|
|
1180
|
+
* // Compaction without restoration hints
|
|
1181
|
+
* await agent.compact({ injectRestorationHints: false });
|
|
1182
|
+
*
|
|
1183
|
+
* // Emergency compaction (more aggressive)
|
|
1184
|
+
* await agent.compact({ emergency: true });
|
|
1185
|
+
* ```
|
|
1186
|
+
*/
|
|
1187
|
+
compact(options?: {
|
|
1188
|
+
/**
|
|
1189
|
+
* Inject file restoration hints after compaction.
|
|
1190
|
+
* Only applies if file tracking is enabled.
|
|
1191
|
+
* Default: true (if file tracking is enabled)
|
|
1192
|
+
*/
|
|
1193
|
+
injectRestorationHints?: boolean;
|
|
1194
|
+
/**
|
|
1195
|
+
* Use emergency mode (more aggressive summarization).
|
|
1196
|
+
* Default: auto-detect based on context utilization
|
|
1197
|
+
*/
|
|
1198
|
+
emergency?: boolean;
|
|
1199
|
+
/**
|
|
1200
|
+
* Target utilization after compaction (0-1).
|
|
1201
|
+
* Default: from context manager config (typically 0.5)
|
|
1202
|
+
*/
|
|
1203
|
+
targetUtilization?: number;
|
|
1204
|
+
/**
|
|
1205
|
+
* Use smart category-aware compaction instead of simple summarization.
|
|
1206
|
+
* Smart compaction:
|
|
1207
|
+
* - Preserves system and recent messages completely
|
|
1208
|
+
* - Saves large tool results to files
|
|
1209
|
+
* - Summarizes history with LLM
|
|
1210
|
+
* Default: true
|
|
1211
|
+
*/
|
|
1212
|
+
useSmartCompaction?: boolean;
|
|
1213
|
+
}): Promise<{
|
|
1214
|
+
/** Whether compaction was successful */
|
|
1215
|
+
success: boolean;
|
|
1216
|
+
/** Original token count */
|
|
1217
|
+
originalTokens: number;
|
|
1218
|
+
/** Token count after compaction */
|
|
1219
|
+
summaryTokens: number;
|
|
1220
|
+
/** Number of summarization rounds performed */
|
|
1221
|
+
rounds: number;
|
|
1222
|
+
/** Number of messages preserved (not summarized) */
|
|
1223
|
+
messagesPreserved: number;
|
|
1224
|
+
/** Whether restoration hints were injected */
|
|
1225
|
+
restorationHintsInjected: boolean;
|
|
1226
|
+
/** Number of orphaned tool_results removed */
|
|
1227
|
+
toolResultsRepaired: number;
|
|
1228
|
+
/** The generated summary (for debugging/display) */
|
|
1229
|
+
summary: string;
|
|
1230
|
+
/** Files created during smart compaction (tool results saved to files) */
|
|
1231
|
+
filesCreated?: string[];
|
|
1232
|
+
/** Category-specific statistics (only for smart compaction) */
|
|
1233
|
+
categoryStats?: SmartCompactionResult['categoryStats'];
|
|
1234
|
+
}>;
|
|
1068
1235
|
/**
|
|
1069
1236
|
* Serialize the current agent state to an AgentState object.
|
|
1070
1237
|
* This can be used for manual persistence or transferring state.
|
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
|
}
|
|
@@ -713,6 +736,173 @@ export class Agent {
|
|
|
713
736
|
getVerbosityLevel() {
|
|
714
737
|
return this.contextManager?.getVerbosityLevel() ?? 'full';
|
|
715
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
|
+
}
|
|
716
906
|
// ==========================================================================
|
|
717
907
|
// State Management
|
|
718
908
|
// ==========================================================================
|
|
@@ -913,8 +1103,9 @@ export class Agent {
|
|
|
913
1103
|
maxContextTokens: subAgentMaxTokens,
|
|
914
1104
|
},
|
|
915
1105
|
});
|
|
916
|
-
// Create tool registry for sub-agent
|
|
917
|
-
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);
|
|
918
1109
|
// If tools specified, use those; otherwise inherit from parent
|
|
919
1110
|
const toolsToRegister = config.tools ??
|
|
920
1111
|
this.toolRegistry
|
|
@@ -1188,7 +1379,7 @@ export class Agent {
|
|
|
1188
1379
|
* Run the agent with a user message
|
|
1189
1380
|
*/
|
|
1190
1381
|
async run(userMessage, options) {
|
|
1191
|
-
|
|
1382
|
+
let maxIterations = options?.maxIterations ?? this.maxIterations;
|
|
1192
1383
|
const chatOptions = { ...this.chatOptions, ...options?.chatOptions };
|
|
1193
1384
|
const signal = options?.signal;
|
|
1194
1385
|
// Combined event emitter
|
|
@@ -1243,6 +1434,8 @@ export class Agent {
|
|
|
1243
1434
|
// Perform emergency summarization
|
|
1244
1435
|
const { messages: summarized, result } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs), { emergency: true });
|
|
1245
1436
|
messages = summarized;
|
|
1437
|
+
// Inject restoration hints after summarization
|
|
1438
|
+
this.injectRestorationHints(messages);
|
|
1246
1439
|
emit({
|
|
1247
1440
|
type: 'context_summarized',
|
|
1248
1441
|
tokensBefore: result.originalTokens,
|
|
@@ -1390,12 +1583,15 @@ export class Agent {
|
|
|
1390
1583
|
// If no tool uses, we're done
|
|
1391
1584
|
if (toolUses.length === 0) {
|
|
1392
1585
|
finalResponse = text;
|
|
1393
|
-
// Add final assistant response to history
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
+
}
|
|
1399
1595
|
// Run afterIteration hooks
|
|
1400
1596
|
if (this.hooksManager) {
|
|
1401
1597
|
await this.hooksManager.runAfterIteration({
|
|
@@ -1450,11 +1646,26 @@ export class Agent {
|
|
|
1450
1646
|
success: false,
|
|
1451
1647
|
error: `Permission denied: ${permResult.reason ?? 'Tool execution not allowed'}`,
|
|
1452
1648
|
};
|
|
1453
|
-
//
|
|
1649
|
+
// Emit tool_end and record the tool call
|
|
1454
1650
|
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1455
1651
|
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1456
1652
|
toolCalls.push(toolCallEntry);
|
|
1457
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);
|
|
1458
1669
|
continue;
|
|
1459
1670
|
}
|
|
1460
1671
|
emit({ type: 'permission_granted', toolName: toolUse.name, level: permResult.level });
|
|
@@ -1472,11 +1683,26 @@ export class Agent {
|
|
|
1472
1683
|
success: false,
|
|
1473
1684
|
error: `Guardrail blocked: ${message}`,
|
|
1474
1685
|
};
|
|
1475
|
-
//
|
|
1686
|
+
// Emit tool_end and record the tool call
|
|
1476
1687
|
emit({ type: 'tool_end', name: toolUse.name, result });
|
|
1477
1688
|
const toolCallEntry = { name: toolUse.name, input: toolUse.input, result };
|
|
1478
1689
|
toolCalls.push(toolCallEntry);
|
|
1479
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);
|
|
1480
1706
|
continue;
|
|
1481
1707
|
}
|
|
1482
1708
|
else if (guardrailResult.action === 'warn') {
|
|
@@ -1602,6 +1828,8 @@ export class Agent {
|
|
|
1602
1828
|
const tokensBefore = this.contextManager.getTokenCount();
|
|
1603
1829
|
const compactResult = await this.contextManager.compactCategory(messages, 'toolResults', (content, index) => Promise.resolve(`[compacted_tool_result_${String(index)}]`));
|
|
1604
1830
|
messages = compactResult.messages;
|
|
1831
|
+
// Inject restoration hints after compaction
|
|
1832
|
+
this.injectRestorationHints(messages);
|
|
1605
1833
|
emit({
|
|
1606
1834
|
type: 'context_compacted',
|
|
1607
1835
|
tokensBefore,
|
|
@@ -1612,6 +1840,8 @@ export class Agent {
|
|
|
1612
1840
|
// Approaching context limit - summarize
|
|
1613
1841
|
const { messages: summarized, result: sumResult } = await this.contextManager.summarizeWithRetry(messages, (msgs) => this.generateSummary(msgs));
|
|
1614
1842
|
messages = summarized;
|
|
1843
|
+
// Inject restoration hints after summarization
|
|
1844
|
+
this.injectRestorationHints(messages);
|
|
1615
1845
|
emit({
|
|
1616
1846
|
type: 'context_summarized',
|
|
1617
1847
|
tokensBefore: sumResult.originalTokens,
|
|
@@ -1652,6 +1882,30 @@ export class Agent {
|
|
|
1652
1882
|
});
|
|
1653
1883
|
}
|
|
1654
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
|
+
}
|
|
1655
1909
|
}
|
|
1656
1910
|
// Check if we hit max iterations without completing
|
|
1657
1911
|
if (!aborted && iterations >= maxIterations && finalResponse === '') {
|