@defai.digital/ax-cli 3.8.6 → 3.8.8
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/README.md +28 -393
- package/config-defaults/models.yaml +0 -24
- package/config-defaults/settings.yaml +16 -16
- package/dist/agent/dependency-resolver.js +22 -1
- package/dist/agent/dependency-resolver.js.map +1 -1
- package/dist/agent/llm-agent.d.ts +23 -2
- package/dist/agent/llm-agent.js +126 -122
- package/dist/agent/llm-agent.js.map +1 -1
- package/dist/agent/loop-detector.d.ts +70 -0
- package/dist/agent/loop-detector.js +339 -0
- package/dist/agent/loop-detector.js.map +1 -0
- package/dist/agent/progress-tracker.d.ts +94 -0
- package/dist/agent/progress-tracker.js +222 -0
- package/dist/agent/progress-tracker.js.map +1 -0
- package/dist/agent/status-reporter.js +2 -2
- package/dist/agent/status-reporter.js.map +1 -1
- package/dist/agent/subagent.js +7 -3
- package/dist/agent/subagent.js.map +1 -1
- package/dist/analyzers/architecture/project-structure-scanner.js +6 -2
- package/dist/analyzers/architecture/project-structure-scanner.js.map +1 -1
- package/dist/analyzers/git/churn-calculator.js +2 -1
- package/dist/analyzers/git/churn-calculator.js.map +1 -1
- package/dist/checkpoint/manager.js +18 -4
- package/dist/checkpoint/manager.js.map +1 -1
- package/dist/checkpoint/storage.d.ts +6 -0
- package/dist/checkpoint/storage.js +96 -49
- package/dist/checkpoint/storage.js.map +1 -1
- package/dist/commands/cache.js +8 -6
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/doctor.js +19 -27
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/mcp-migrate.js +6 -5
- package/dist/commands/mcp-migrate.js.map +1 -1
- package/dist/commands/mcp.js +14 -2
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/models.js +8 -12
- package/dist/commands/models.js.map +1 -1
- package/dist/commands/plan.js +1 -10
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/setup.js +4 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.js +40 -14
- package/dist/commands/status.js.map +1 -1
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +16 -4
- package/dist/constants.js.map +1 -1
- package/dist/hooks/hook-runner.d.ts +138 -0
- package/dist/hooks/hook-runner.js +429 -0
- package/dist/hooks/hook-runner.js.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.js +3 -21
- package/dist/index.js.map +1 -1
- package/dist/llm/client.d.ts +9 -0
- package/dist/llm/client.js +306 -45
- package/dist/llm/client.js.map +1 -1
- package/dist/llm/tools.js +2 -39
- package/dist/llm/tools.js.map +1 -1
- package/dist/llm/types.d.ts +1 -47
- package/dist/llm/types.js +0 -18
- package/dist/llm/types.js.map +1 -1
- package/dist/mcp/automatosx-loader.js +2 -1
- package/dist/mcp/automatosx-loader.js.map +1 -1
- package/dist/mcp/client-v2.d.ts +3 -0
- package/dist/mcp/client-v2.js +85 -19
- package/dist/mcp/client-v2.js.map +1 -1
- package/dist/mcp/config-migrator.js +3 -2
- package/dist/mcp/config-migrator.js.map +1 -1
- package/dist/mcp/config-v2.d.ts +5 -0
- package/dist/mcp/config-v2.js +26 -0
- package/dist/mcp/config-v2.js.map +1 -1
- package/dist/mcp/error-formatter.js +4 -1
- package/dist/mcp/error-formatter.js.map +1 -1
- package/dist/mcp/health.js +1 -1
- package/dist/mcp/health.js.map +1 -1
- package/dist/mcp/reconnection.js +2 -1
- package/dist/mcp/reconnection.js.map +1 -1
- package/dist/mcp/registry.js +3 -2
- package/dist/mcp/registry.js.map +1 -1
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/resources.js.map +1 -1
- package/dist/mcp/validation.js +9 -0
- package/dist/mcp/validation.js.map +1 -1
- package/dist/memory/context-store.js +4 -6
- package/dist/memory/context-store.js.map +1 -1
- package/dist/memory/types.d.ts +2 -0
- package/dist/memory/types.js +4 -1
- package/dist/memory/types.js.map +1 -1
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +7 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/permissions/permission-manager.d.ts +145 -0
- package/dist/permissions/permission-manager.js +401 -0
- package/dist/permissions/permission-manager.js.map +1 -0
- package/dist/planner/plan-storage.js +3 -2
- package/dist/planner/plan-storage.js.map +1 -1
- package/dist/planner/task-planner.js +2 -1
- package/dist/planner/task-planner.js.map +1 -1
- package/dist/planner/types.d.ts +6 -6
- package/dist/schemas/settings-schemas.d.ts +0 -14
- package/dist/schemas/settings-schemas.js +0 -10
- package/dist/schemas/settings-schemas.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +2 -2
- package/dist/schemas/yaml-schemas.d.ts +15 -0
- package/dist/schemas/yaml-schemas.js +3 -0
- package/dist/schemas/yaml-schemas.js.map +1 -1
- package/dist/tools/bash.js +35 -10
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/confirmation-tool.js +3 -2
- package/dist/tools/confirmation-tool.js.map +1 -1
- package/dist/tools/registry.d.ts +1 -1
- package/dist/tools/registry.js +2 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/search.js +12 -13
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/text-editor.d.ts +46 -0
- package/dist/tools/text-editor.js +455 -11
- package/dist/tools/text-editor.js.map +1 -1
- package/dist/tools/todo-tool.js +5 -4
- package/dist/tools/todo-tool.js.map +1 -1
- package/dist/ui/components/chat-input.js +10 -1
- package/dist/ui/components/chat-input.js.map +1 -1
- package/dist/ui/components/chat-interface.js +1 -0
- package/dist/ui/components/chat-interface.js.map +1 -1
- package/dist/ui/components/tool-group-display.js +0 -6
- package/dist/ui/components/tool-group-display.js.map +1 -1
- package/dist/ui/hooks/use-input-handler.js +7 -6
- package/dist/ui/hooks/use-input-handler.js.map +1 -1
- package/dist/ui/hooks/use-input-history.js +21 -13
- package/dist/ui/hooks/use-input-history.js.map +1 -1
- package/dist/ui/utils/tool-grouper.d.ts +1 -2
- package/dist/ui/utils/tool-grouper.js +4 -15
- package/dist/ui/utils/tool-grouper.js.map +1 -1
- package/dist/utils/api-error.d.ts +61 -0
- package/dist/utils/api-error.js +176 -0
- package/dist/utils/api-error.js.map +1 -0
- package/dist/utils/audit-logger.js +2 -1
- package/dist/utils/audit-logger.js.map +1 -1
- package/dist/utils/auto-accept-logger.js +3 -3
- package/dist/utils/auto-accept-logger.js.map +1 -1
- package/dist/utils/config-loader.d.ts +3 -0
- package/dist/utils/config-loader.js +27 -2
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/encryption.js +2 -1
- package/dist/utils/encryption.js.map +1 -1
- package/dist/utils/file-cache.js +4 -2
- package/dist/utils/file-cache.js.map +1 -1
- package/dist/utils/history-migration.js +5 -4
- package/dist/utils/history-migration.js.map +1 -1
- package/dist/utils/onboarding-manager.js +2 -1
- package/dist/utils/onboarding-manager.js.map +1 -1
- package/dist/utils/path-helpers.d.ts +8 -0
- package/dist/utils/path-helpers.js +35 -0
- package/dist/utils/path-helpers.js.map +1 -0
- package/dist/utils/path-security.js +3 -2
- package/dist/utils/path-security.js.map +1 -1
- package/dist/utils/retry-helper.d.ts +61 -0
- package/dist/utils/retry-helper.js +206 -0
- package/dist/utils/retry-helper.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +1 -21
- package/dist/utils/settings-manager.js +2 -82
- package/dist/utils/settings-manager.js.map +1 -1
- package/dist/utils/streaming-analyzer.d.ts +2 -13
- package/dist/utils/streaming-analyzer.js +3 -25
- package/dist/utils/streaming-analyzer.js.map +1 -1
- package/dist/utils/token-counter.d.ts +13 -1
- package/dist/utils/token-counter.js +31 -6
- package/dist/utils/token-counter.js.map +1 -1
- package/package.json +3 -2
- package/packages/schemas/README.md +1 -1
- package/packages/schemas/package.json +1 -1
- package/dist/tools/web-search/cache.d.ts +0 -62
- package/dist/tools/web-search/cache.js +0 -105
- package/dist/tools/web-search/cache.js.map +0 -1
- package/dist/tools/web-search/engines/crates.d.ts +0 -19
- package/dist/tools/web-search/engines/crates.js +0 -87
- package/dist/tools/web-search/engines/crates.js.map +0 -1
- package/dist/tools/web-search/engines/npm.d.ts +0 -18
- package/dist/tools/web-search/engines/npm.js +0 -86
- package/dist/tools/web-search/engines/npm.js.map +0 -1
- package/dist/tools/web-search/engines/pypi.d.ts +0 -18
- package/dist/tools/web-search/engines/pypi.js +0 -75
- package/dist/tools/web-search/engines/pypi.js.map +0 -1
- package/dist/tools/web-search/index.d.ts +0 -11
- package/dist/tools/web-search/index.js +0 -11
- package/dist/tools/web-search/index.js.map +0 -1
- package/dist/tools/web-search/router.d.ts +0 -34
- package/dist/tools/web-search/router.js +0 -245
- package/dist/tools/web-search/router.js.map +0 -1
- package/dist/tools/web-search/types.d.ts +0 -45
- package/dist/tools/web-search/types.js +0 -6
- package/dist/tools/web-search/types.js.map +0 -1
- package/dist/tools/web-search/web-search-tool.d.ts +0 -51
- package/dist/tools/web-search/web-search-tool.js +0 -246
- package/dist/tools/web-search/web-search-tool.js.map +0 -1
|
@@ -40,6 +40,16 @@ export interface StreamingChunk {
|
|
|
40
40
|
/** Tool execution duration in milliseconds (for tool_result type) */
|
|
41
41
|
executionDurationMs?: number;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Accumulated message from streaming response
|
|
45
|
+
* Contains the full message content and any tool calls
|
|
46
|
+
*/
|
|
47
|
+
export interface AccumulatedMessage {
|
|
48
|
+
role?: string;
|
|
49
|
+
content?: string;
|
|
50
|
+
tool_calls?: LLMToolCall[];
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
}
|
|
43
53
|
export declare class LLMAgent extends EventEmitter {
|
|
44
54
|
private llmClient;
|
|
45
55
|
private textEditor;
|
|
@@ -47,7 +57,6 @@ export declare class LLMAgent extends EventEmitter {
|
|
|
47
57
|
private bashOutput;
|
|
48
58
|
private todoTool;
|
|
49
59
|
private search;
|
|
50
|
-
private webSearch;
|
|
51
60
|
private _architectureTool?;
|
|
52
61
|
private _validationTool?;
|
|
53
62
|
private chatHistory;
|
|
@@ -68,6 +77,8 @@ export declare class LLMAgent extends EventEmitter {
|
|
|
68
77
|
private samplingConfig;
|
|
69
78
|
/** Thinking/reasoning mode configuration */
|
|
70
79
|
private thinkingConfig;
|
|
80
|
+
/** Stored reference to context overflow listener for proper cleanup */
|
|
81
|
+
private contextOverflowListener;
|
|
71
82
|
/** Track if agent has been disposed */
|
|
72
83
|
private disposed;
|
|
73
84
|
/** Tool approval system for VSCode integration */
|
|
@@ -160,13 +171,23 @@ export declare class LLMAgent extends EventEmitter {
|
|
|
160
171
|
private get validationTool();
|
|
161
172
|
/**
|
|
162
173
|
* Detect if a tool call is repetitive (likely causing a loop)
|
|
163
|
-
*
|
|
174
|
+
* Uses the intelligent LoopDetector which provides:
|
|
175
|
+
* - Tool-specific thresholds (file ops get higher limits)
|
|
176
|
+
* - Progress-based detection (tracks success/failure)
|
|
177
|
+
* - Cycle pattern detection (A→B→A→B loops)
|
|
164
178
|
*/
|
|
165
179
|
private isRepetitiveToolCall;
|
|
180
|
+
/** Last loop detection result for error messages */
|
|
181
|
+
private lastLoopResult?;
|
|
166
182
|
/**
|
|
167
183
|
* Reset the tool call tracking (called at start of new user message)
|
|
168
184
|
*/
|
|
169
185
|
private resetToolCallTracking;
|
|
186
|
+
/**
|
|
187
|
+
* Generate a helpful warning message when a loop is detected
|
|
188
|
+
* Uses the lastLoopResult for context-aware suggestions
|
|
189
|
+
*/
|
|
190
|
+
private getLoopWarningMessage;
|
|
170
191
|
/**
|
|
171
192
|
* Check if a request should trigger multi-phase planning
|
|
172
193
|
*/
|
package/dist/agent/llm-agent.js
CHANGED
|
@@ -3,11 +3,10 @@ import { getAllGrokTools, getMCPManager, initializeMCPServers, } from "../llm/to
|
|
|
3
3
|
import { loadMCPConfig } from "../mcp/config.js";
|
|
4
4
|
import { TextEditorTool, BashTool, TodoTool, SearchTool, } from "../tools/index.js";
|
|
5
5
|
import { BashOutputTool } from "../tools/bash-output.js";
|
|
6
|
-
import { WebSearchTool } from "../tools/web-search/index.js";
|
|
7
6
|
import { ArchitectureTool } from "../tools/analysis-tools/architecture-tool.js";
|
|
8
7
|
import { ValidationTool } from "../tools/analysis-tools/validation-tool.js";
|
|
9
8
|
import { EventEmitter } from "events";
|
|
10
|
-
import { AGENT_CONFIG } from "../constants.js";
|
|
9
|
+
import { AGENT_CONFIG, CACHE_CONFIG } from "../constants.js";
|
|
11
10
|
import { getTokenCounter } from "../utils/token-counter.js";
|
|
12
11
|
import { loadCustomInstructions } from "../utils/custom-instructions.js";
|
|
13
12
|
import { getSettingsManager } from "../utils/settings-manager.js";
|
|
@@ -22,6 +21,7 @@ import { PLANNER_CONFIG } from "../constants.js";
|
|
|
22
21
|
import { resolveMCPReferences, extractMCPReferences } from "../mcp/resources.js";
|
|
23
22
|
import { SDKError, SDKErrorCode } from "../sdk/errors.js";
|
|
24
23
|
import { getStatusReporter } from "./status-reporter.js";
|
|
24
|
+
import { getLoopDetector, resetLoopDetector } from "./loop-detector.js";
|
|
25
25
|
export class LLMAgent extends EventEmitter {
|
|
26
26
|
llmClient;
|
|
27
27
|
textEditor;
|
|
@@ -29,7 +29,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
29
29
|
bashOutput;
|
|
30
30
|
todoTool;
|
|
31
31
|
search;
|
|
32
|
-
webSearch;
|
|
33
32
|
// Lazy-loaded tools (rarely used)
|
|
34
33
|
_architectureTool;
|
|
35
34
|
_validationTool;
|
|
@@ -51,6 +50,8 @@ export class LLMAgent extends EventEmitter {
|
|
|
51
50
|
samplingConfig;
|
|
52
51
|
/** Thinking/reasoning mode configuration */
|
|
53
52
|
thinkingConfig;
|
|
53
|
+
/** Stored reference to context overflow listener for proper cleanup */
|
|
54
|
+
contextOverflowListener;
|
|
54
55
|
/** Track if agent has been disposed */
|
|
55
56
|
disposed = false;
|
|
56
57
|
/** Tool approval system for VSCode integration */
|
|
@@ -73,7 +74,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
73
74
|
this.bashOutput = new BashOutputTool();
|
|
74
75
|
this.todoTool = new TodoTool();
|
|
75
76
|
this.search = new SearchTool();
|
|
76
|
-
this.webSearch = new WebSearchTool();
|
|
77
77
|
// architectureTool and validationTool are lazy-loaded (see getters below)
|
|
78
78
|
this.tokenCounter = getTokenCounter(modelToUse);
|
|
79
79
|
this.contextManager = new ContextManager({ model: modelToUse });
|
|
@@ -88,6 +88,9 @@ export class LLMAgent extends EventEmitter {
|
|
|
88
88
|
this.textEditor.setCheckpointCallback(async (files, description) => {
|
|
89
89
|
// Create immutable snapshot of chat history at callback time
|
|
90
90
|
// This prevents inconsistencies if messages are added during checkpoint creation
|
|
91
|
+
// BUG FIX: Check if agent is disposed before creating checkpoint
|
|
92
|
+
if (this.disposed)
|
|
93
|
+
return;
|
|
91
94
|
const chatHistorySnapshot = JSON.parse(JSON.stringify(this.chatHistory));
|
|
92
95
|
await this.checkpointManager.createCheckpoint({
|
|
93
96
|
files,
|
|
@@ -125,14 +128,19 @@ export class LLMAgent extends EventEmitter {
|
|
|
125
128
|
// NEW: Listen for context pruning to generate summaries
|
|
126
129
|
// CRITICAL FIX: Wrap async callback to prevent uncaught promise rejections
|
|
127
130
|
// Event listeners don't handle async errors automatically, so we must catch them
|
|
128
|
-
|
|
131
|
+
// Store listener reference for proper cleanup in dispose()
|
|
132
|
+
this.contextOverflowListener = (data) => {
|
|
133
|
+
// Skip if agent is disposed to prevent operations on disposed resources
|
|
134
|
+
if (this.disposed)
|
|
135
|
+
return;
|
|
129
136
|
this.handleContextOverflow(data).catch((error) => {
|
|
130
137
|
const errorMsg = extractErrorMessage(error);
|
|
131
138
|
console.error('Error handling context overflow:', errorMsg);
|
|
132
139
|
// Emit error event for monitoring
|
|
133
140
|
this.emit('error', error);
|
|
134
141
|
});
|
|
135
|
-
}
|
|
142
|
+
};
|
|
143
|
+
this.contextManager.on('before_prune', this.contextOverflowListener);
|
|
136
144
|
}
|
|
137
145
|
initializeCheckpointManager() {
|
|
138
146
|
// Initialize checkpoint manager in the background
|
|
@@ -254,6 +262,10 @@ export class LLMAgent extends EventEmitter {
|
|
|
254
262
|
* @returns Promise<boolean> - true if approved, false if rejected or timeout
|
|
255
263
|
*/
|
|
256
264
|
waitForToolApproval(toolCall) {
|
|
265
|
+
// If agent is already disposed, immediately reject approval wait to avoid dangling promises
|
|
266
|
+
if (this.disposed) {
|
|
267
|
+
return Promise.resolve(false);
|
|
268
|
+
}
|
|
257
269
|
return new Promise((resolve) => {
|
|
258
270
|
// Emit event so external integrations can show diff preview
|
|
259
271
|
this.emit('tool:approval_required', toolCall);
|
|
@@ -331,12 +343,11 @@ export class LLMAgent extends EventEmitter {
|
|
|
331
343
|
}
|
|
332
344
|
// CRITICAL FIX: Add hard limit for messages array as safety backstop
|
|
333
345
|
// In case contextManager.shouldPrune() always returns false
|
|
334
|
-
|
|
335
|
-
if (this.messages.length > MAX_MESSAGES) {
|
|
346
|
+
if (this.messages.length > AGENT_CONFIG.MAX_MESSAGES) {
|
|
336
347
|
// Keep system message (if exists) + last N messages
|
|
337
348
|
const systemMessages = this.messages.filter(m => m.role === 'system');
|
|
338
349
|
const nonSystemMessages = this.messages.filter(m => m.role !== 'system');
|
|
339
|
-
const keepMessages = Math.min(nonSystemMessages.length, MAX_MESSAGES - systemMessages.length);
|
|
350
|
+
const keepMessages = Math.min(nonSystemMessages.length, AGENT_CONFIG.MAX_MESSAGES - systemMessages.length);
|
|
340
351
|
this.messages = [
|
|
341
352
|
...systemMessages,
|
|
342
353
|
...nonSystemMessages.slice(-keepMessages)
|
|
@@ -363,16 +374,14 @@ export class LLMAgent extends EventEmitter {
|
|
|
363
374
|
const args = JSON.parse(toolCall.function.arguments || '{}');
|
|
364
375
|
this.toolCallArgsCache.set(toolCall.id, args);
|
|
365
376
|
// CRITICAL FIX: Prevent unbounded memory growth with proper cache eviction
|
|
366
|
-
// When cache exceeds limit, reduce to 80% capacity (not just remove
|
|
367
|
-
if (this.toolCallArgsCache.size >
|
|
368
|
-
const targetSize =
|
|
377
|
+
// When cache exceeds limit, reduce to 80% capacity (not just remove fixed entries)
|
|
378
|
+
if (this.toolCallArgsCache.size > CACHE_CONFIG.TOOL_ARGS_CACHE_MAX_SIZE) {
|
|
379
|
+
const targetSize = Math.floor(CACHE_CONFIG.TOOL_ARGS_CACHE_MAX_SIZE * 0.8);
|
|
369
380
|
const toRemove = this.toolCallArgsCache.size - targetSize;
|
|
370
|
-
|
|
371
|
-
|
|
381
|
+
// BUG FIX: Don't modify Map while iterating - create array of keys first
|
|
382
|
+
const keysToDelete = Array.from(this.toolCallArgsCache.keys()).slice(0, toRemove);
|
|
383
|
+
for (const key of keysToDelete) {
|
|
372
384
|
this.toolCallArgsCache.delete(key);
|
|
373
|
-
deleted++;
|
|
374
|
-
if (deleted >= toRemove)
|
|
375
|
-
break;
|
|
376
385
|
}
|
|
377
386
|
}
|
|
378
387
|
return args;
|
|
@@ -404,102 +413,87 @@ export class LLMAgent extends EventEmitter {
|
|
|
404
413
|
}
|
|
405
414
|
/**
|
|
406
415
|
* Detect if a tool call is repetitive (likely causing a loop)
|
|
407
|
-
*
|
|
416
|
+
* Uses the intelligent LoopDetector which provides:
|
|
417
|
+
* - Tool-specific thresholds (file ops get higher limits)
|
|
418
|
+
* - Progress-based detection (tracks success/failure)
|
|
419
|
+
* - Cycle pattern detection (A→B→A→B loops)
|
|
408
420
|
*/
|
|
409
421
|
isRepetitiveToolCall(toolCall) {
|
|
410
422
|
// Check if loop detection is disabled globally
|
|
411
423
|
if (!AGENT_CONFIG.ENABLE_LOOP_DETECTION) {
|
|
412
424
|
return false;
|
|
413
425
|
}
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
const normalizedCommand = args.command.trim().replace(/\s+/g, ' ');
|
|
426
|
-
// Use full command for exact matching (catches true duplicates)
|
|
427
|
-
signature = `bash:${normalizedCommand}`;
|
|
428
|
-
}
|
|
429
|
-
else if (toolCall.function.name === 'search' && args.query && typeof args.query === 'string') {
|
|
430
|
-
// For search, include the normalized query
|
|
431
|
-
const normalizedQuery = args.query.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
432
|
-
signature = `search:${normalizedQuery}`;
|
|
433
|
-
}
|
|
434
|
-
else if (toolCall.function.name === 'view_file' && args.path && typeof args.path === 'string') {
|
|
435
|
-
// For file reads, include the path
|
|
436
|
-
signature = `view:${args.path}`;
|
|
437
|
-
}
|
|
438
|
-
else if (toolCall.function.name === 'create_file' && args.path && typeof args.path === 'string') {
|
|
439
|
-
// For file writes, include the path
|
|
440
|
-
signature = `create:${args.path}`;
|
|
441
|
-
}
|
|
442
|
-
else if (toolCall.function.name === 'str_replace_editor' && args.path && typeof args.path === 'string') {
|
|
443
|
-
// For text editor, include the path
|
|
444
|
-
signature = `edit:${args.path}`;
|
|
445
|
-
}
|
|
446
|
-
// Track by detailed signature
|
|
447
|
-
const count = this.recentToolCalls.get(signature) || 0;
|
|
448
|
-
// Debug logging
|
|
449
|
-
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
450
|
-
console.error(`[LOOP DETECTION] Tool: ${toolCall.function.name}`);
|
|
451
|
-
console.error(`[LOOP DETECTION] Signature: ${signature}`);
|
|
452
|
-
console.error(`[LOOP DETECTION] Count: ${count}`);
|
|
453
|
-
console.error(`[LOOP DETECTION] Threshold: ${AGENT_CONFIG.LOOP_DETECTION_THRESHOLD}`);
|
|
454
|
-
console.error(`[LOOP DETECTION] Map size: ${this.recentToolCalls.size}`);
|
|
455
|
-
}
|
|
456
|
-
// Increment the count first
|
|
457
|
-
const newCount = count + 1;
|
|
458
|
-
this.recentToolCalls.set(signature, newCount);
|
|
459
|
-
// Check if we've exceeded the configured threshold
|
|
460
|
-
// newCount > threshold means we've seen it threshold+1 times
|
|
461
|
-
if (newCount > AGENT_CONFIG.LOOP_DETECTION_THRESHOLD) {
|
|
462
|
-
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
463
|
-
console.error(`[LOOP DETECTION] ⚠️ LOOP DETECTED! Signature: ${signature} (count: ${newCount}, threshold: ${AGENT_CONFIG.LOOP_DETECTION_THRESHOLD})`);
|
|
464
|
-
}
|
|
465
|
-
return true;
|
|
466
|
-
}
|
|
467
|
-
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
468
|
-
console.error(`[LOOP DETECTION] ✅ Allowed, count now: ${newCount}`);
|
|
469
|
-
console.error(`[LOOP DETECTION] Current map:`, Array.from(this.recentToolCalls.entries()));
|
|
470
|
-
}
|
|
471
|
-
// Clean up old entries (keep only last N unique calls)
|
|
472
|
-
// Batch cleanup when exceeding threshold to prevent unbounded growth
|
|
473
|
-
if (this.recentToolCalls.size > AGENT_CONFIG.MAX_RECENT_TOOL_CALLS) {
|
|
474
|
-
const excessCount = this.recentToolCalls.size - AGENT_CONFIG.MAX_RECENT_TOOL_CALLS + 10;
|
|
475
|
-
let removed = 0;
|
|
476
|
-
for (const key of this.recentToolCalls.keys()) {
|
|
477
|
-
if (removed >= excessCount)
|
|
478
|
-
break;
|
|
479
|
-
this.recentToolCalls.delete(key);
|
|
480
|
-
removed++;
|
|
481
|
-
}
|
|
426
|
+
// Use the new intelligent loop detector
|
|
427
|
+
const detector = getLoopDetector();
|
|
428
|
+
const result = detector.checkForLoop(toolCall);
|
|
429
|
+
// Debug logging
|
|
430
|
+
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
431
|
+
console.error(`[LOOP DETECTION] Tool: ${toolCall.function.name}`);
|
|
432
|
+
console.error(`[LOOP DETECTION] Count: ${result.count}`);
|
|
433
|
+
console.error(`[LOOP DETECTION] Threshold: ${result.threshold}`);
|
|
434
|
+
console.error(`[LOOP DETECTION] Is Loop: ${result.isLoop}`);
|
|
435
|
+
if (result.reason) {
|
|
436
|
+
console.error(`[LOOP DETECTION] Reason: ${result.reason}`);
|
|
482
437
|
}
|
|
483
|
-
|
|
438
|
+
const stats = detector.getStats();
|
|
439
|
+
console.error(`[LOOP DETECTION] Stats: ${JSON.stringify(stats)}`);
|
|
484
440
|
}
|
|
485
|
-
|
|
486
|
-
//
|
|
441
|
+
if (result.isLoop) {
|
|
442
|
+
// Store the result for generating better error message
|
|
443
|
+
this.lastLoopResult = result;
|
|
487
444
|
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
488
|
-
console.error(`[LOOP DETECTION]
|
|
445
|
+
console.error(`[LOOP DETECTION] ⚠️ LOOP DETECTED!`);
|
|
446
|
+
console.error(`[LOOP DETECTION] Reason: ${result.reason}`);
|
|
447
|
+
console.error(`[LOOP DETECTION] Suggestion: ${result.suggestion}`);
|
|
489
448
|
}
|
|
490
|
-
return
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
// Record the tool call (will be marked success/failure after execution)
|
|
452
|
+
// For now, pre-record with success=true, we update if it fails
|
|
453
|
+
detector.recordToolCall(toolCall, true);
|
|
454
|
+
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
455
|
+
console.error(`[LOOP DETECTION] ✅ Allowed, count: ${result.count}/${result.threshold}`);
|
|
491
456
|
}
|
|
457
|
+
return false;
|
|
492
458
|
}
|
|
459
|
+
/** Last loop detection result for error messages */
|
|
460
|
+
lastLoopResult;
|
|
493
461
|
/**
|
|
494
462
|
* Reset the tool call tracking (called at start of new user message)
|
|
495
463
|
*/
|
|
496
464
|
resetToolCallTracking() {
|
|
497
465
|
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
498
|
-
|
|
466
|
+
const detector = getLoopDetector();
|
|
467
|
+
const stats = detector.getStats();
|
|
468
|
+
console.error(`[LOOP TRACKING] 🔄 Resetting tool call tracking (had ${stats.uniqueSignatures} signatures)`);
|
|
499
469
|
}
|
|
470
|
+
// Reset the new intelligent loop detector
|
|
471
|
+
resetLoopDetector();
|
|
472
|
+
// Also reset the legacy tracking (keep for backward compatibility during transition)
|
|
500
473
|
this.recentToolCalls.clear();
|
|
501
474
|
// Also clear the args cache to prevent memory leak
|
|
502
475
|
this.toolCallArgsCache.clear();
|
|
476
|
+
// Clear last loop result
|
|
477
|
+
this.lastLoopResult = undefined;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Generate a helpful warning message when a loop is detected
|
|
481
|
+
* Uses the lastLoopResult for context-aware suggestions
|
|
482
|
+
*/
|
|
483
|
+
getLoopWarningMessage() {
|
|
484
|
+
const base = "\n\n⚠️ Detected repetitive operations. Stopping to prevent infinite loop.";
|
|
485
|
+
if (this.lastLoopResult) {
|
|
486
|
+
const parts = [base];
|
|
487
|
+
if (this.lastLoopResult.reason) {
|
|
488
|
+
parts.push(`\n\nReason: ${this.lastLoopResult.reason}`);
|
|
489
|
+
}
|
|
490
|
+
if (this.lastLoopResult.suggestion) {
|
|
491
|
+
parts.push(`\n\n💡 Suggestion: ${this.lastLoopResult.suggestion}`);
|
|
492
|
+
}
|
|
493
|
+
parts.push("\n\nI'll stop here and provide what I've accomplished so far. You can ask me to continue with a different approach.");
|
|
494
|
+
return parts.join('');
|
|
495
|
+
}
|
|
496
|
+
return base + "\n\nI apologize, but I seem to be stuck in a loop. Let me provide what I can without further tool use.";
|
|
503
497
|
}
|
|
504
498
|
// ============================================================================
|
|
505
499
|
// Multi-Phase Planning Integration
|
|
@@ -949,14 +943,15 @@ export class LLMAgent extends EventEmitter {
|
|
|
949
943
|
// Add assistant message to history
|
|
950
944
|
this.addAssistantMessage(streamResult.accumulated);
|
|
951
945
|
// Handle tool calls if present
|
|
952
|
-
if (streamResult.accumulated.tool_calls
|
|
946
|
+
if (streamResult.accumulated.tool_calls && streamResult.accumulated.tool_calls.length > 0) {
|
|
953
947
|
toolRounds++;
|
|
954
948
|
// Check for repetitive tool calls (loop detection)
|
|
955
949
|
const hasRepetitiveCall = streamResult.accumulated.tool_calls.some((tc) => this.isRepetitiveToolCall(tc));
|
|
956
950
|
if (hasRepetitiveCall) {
|
|
951
|
+
const loopMsg = this.getLoopWarningMessage();
|
|
957
952
|
yield {
|
|
958
953
|
type: "content",
|
|
959
|
-
content:
|
|
954
|
+
content: loopMsg,
|
|
960
955
|
};
|
|
961
956
|
break;
|
|
962
957
|
}
|
|
@@ -1074,9 +1069,10 @@ export class LLMAgent extends EventEmitter {
|
|
|
1074
1069
|
if (process.env.DEBUG_LOOP_DETECTION === '1') {
|
|
1075
1070
|
console.error(`[LOOP CHECK] 🛑 Breaking loop!`);
|
|
1076
1071
|
}
|
|
1072
|
+
const loopMsg = this.getLoopWarningMessage();
|
|
1077
1073
|
const warningEntry = {
|
|
1078
1074
|
type: "assistant",
|
|
1079
|
-
content:
|
|
1075
|
+
content: loopMsg,
|
|
1080
1076
|
timestamp: new Date(),
|
|
1081
1077
|
};
|
|
1082
1078
|
this.chatHistory.push(warningEntry);
|
|
@@ -1116,7 +1112,11 @@ export class LLMAgent extends EventEmitter {
|
|
|
1116
1112
|
const result = await this.executeTool(toolCall);
|
|
1117
1113
|
// Update the existing tool_call entry with the result (O(1) lookup)
|
|
1118
1114
|
const entryIndex = this.toolCallIndexMap.get(toolCall.id);
|
|
1119
|
-
|
|
1115
|
+
// Validate entryIndex is still valid after potential context pruning
|
|
1116
|
+
// The index could become stale if chatHistory was modified between store and access
|
|
1117
|
+
if (entryIndex !== undefined &&
|
|
1118
|
+
entryIndex < this.chatHistory.length &&
|
|
1119
|
+
this.chatHistory[entryIndex]?.toolCall?.id === toolCall.id) {
|
|
1120
1120
|
const updatedEntry = {
|
|
1121
1121
|
...this.chatHistory[entryIndex],
|
|
1122
1122
|
type: "tool_result",
|
|
@@ -1406,15 +1406,17 @@ export class LLMAgent extends EventEmitter {
|
|
|
1406
1406
|
finally {
|
|
1407
1407
|
// CRITICAL FIX: Properly close the async iterator to release HTTP connections and buffers
|
|
1408
1408
|
// This prevents socket leaks when streams are cancelled or errors occur
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
// Log but don't throw - cleanup errors shouldn't break the flow
|
|
1415
|
-
console.warn('Stream cleanup warning:', cleanupError);
|
|
1409
|
+
try {
|
|
1410
|
+
// Use a type assertion to safely access the return method
|
|
1411
|
+
const streamWithReturn = stream;
|
|
1412
|
+
if (typeof streamWithReturn.return === 'function') {
|
|
1413
|
+
await streamWithReturn.return();
|
|
1416
1414
|
}
|
|
1417
1415
|
}
|
|
1416
|
+
catch (cleanupError) {
|
|
1417
|
+
// Log but don't throw - cleanup errors shouldn't break the flow
|
|
1418
|
+
console.warn('Stream cleanup warning:', cleanupError);
|
|
1419
|
+
}
|
|
1418
1420
|
}
|
|
1419
1421
|
}
|
|
1420
1422
|
/**
|
|
@@ -1564,14 +1566,15 @@ export class LLMAgent extends EventEmitter {
|
|
|
1564
1566
|
// Add assistant message to history
|
|
1565
1567
|
this.addAssistantMessage(streamResult.accumulated);
|
|
1566
1568
|
// Handle tool calls if present
|
|
1567
|
-
if (streamResult.accumulated.tool_calls
|
|
1569
|
+
if (streamResult.accumulated.tool_calls && streamResult.accumulated.tool_calls.length > 0) {
|
|
1568
1570
|
toolRounds++;
|
|
1569
1571
|
// Check for repetitive tool calls (loop detection)
|
|
1570
1572
|
const hasRepetitiveCall = streamResult.accumulated.tool_calls.some((tc) => this.isRepetitiveToolCall(tc));
|
|
1571
1573
|
if (hasRepetitiveCall) {
|
|
1574
|
+
const loopMsg = this.getLoopWarningMessage();
|
|
1572
1575
|
yield {
|
|
1573
1576
|
type: "content",
|
|
1574
|
-
content:
|
|
1577
|
+
content: loopMsg,
|
|
1575
1578
|
};
|
|
1576
1579
|
break;
|
|
1577
1580
|
}
|
|
@@ -1741,18 +1744,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
1741
1744
|
fileTypes: Array.isArray(args.file_types) ? args.file_types : undefined,
|
|
1742
1745
|
includeHidden: getBoolean('include_hidden'),
|
|
1743
1746
|
});
|
|
1744
|
-
case "web_search": {
|
|
1745
|
-
const freshnessValue = args.freshness;
|
|
1746
|
-
const validFreshness = (freshnessValue === 'day' || freshnessValue === 'week' || freshnessValue === 'month' || freshnessValue === 'year') ? freshnessValue : undefined;
|
|
1747
|
-
const searchDepthValue = args.searchDepth;
|
|
1748
|
-
const validSearchDepth = (searchDepthValue === 'basic' || searchDepthValue === 'advanced') ? searchDepthValue : 'basic';
|
|
1749
|
-
return await this.webSearch.search(getString('query'), {
|
|
1750
|
-
maxResults: getNumber('maxResults'),
|
|
1751
|
-
includeAnswer: getBoolean('includeAnswer'),
|
|
1752
|
-
searchDepth: validSearchDepth,
|
|
1753
|
-
freshness: validFreshness,
|
|
1754
|
-
});
|
|
1755
|
-
}
|
|
1756
1747
|
case "analyze_architecture": {
|
|
1757
1748
|
const projectPath = typeof args.projectPath === 'string' ? args.projectPath : undefined;
|
|
1758
1749
|
const depth = typeof args.depth === 'string' ? args.depth : undefined;
|
|
@@ -1796,10 +1787,11 @@ export class LLMAgent extends EventEmitter {
|
|
|
1796
1787
|
// Extract error message from MCP result content
|
|
1797
1788
|
// Safely check content structure before accessing
|
|
1798
1789
|
let errorMsg = "MCP tool error";
|
|
1799
|
-
if (result.content && result.content.length > 0) {
|
|
1790
|
+
if (result.content && Array.isArray(result.content) && result.content.length > 0) {
|
|
1800
1791
|
const firstContent = result.content[0];
|
|
1801
1792
|
if (typeof firstContent === 'object' && firstContent !== null && 'text' in firstContent) {
|
|
1802
|
-
|
|
1793
|
+
const textValue = firstContent.text;
|
|
1794
|
+
errorMsg = typeof textValue === 'string' ? textValue : String(textValue || errorMsg);
|
|
1803
1795
|
}
|
|
1804
1796
|
}
|
|
1805
1797
|
return {
|
|
@@ -2135,9 +2127,12 @@ export class LLMAgent extends EventEmitter {
|
|
|
2135
2127
|
this.disposed = true;
|
|
2136
2128
|
// Remove all event listeners to prevent memory leaks
|
|
2137
2129
|
this.removeAllListeners();
|
|
2138
|
-
// CRITICAL FIX: Remove event
|
|
2139
|
-
//
|
|
2140
|
-
this.
|
|
2130
|
+
// CRITICAL FIX: Remove event listener from contextManager to prevent memory leak
|
|
2131
|
+
// Only remove the specific listener we registered, not all listeners for this event
|
|
2132
|
+
if (this.contextOverflowListener) {
|
|
2133
|
+
this.contextManager.removeListener('before_prune', this.contextOverflowListener);
|
|
2134
|
+
this.contextOverflowListener = undefined;
|
|
2135
|
+
}
|
|
2141
2136
|
// Dispose tools that have cleanup methods
|
|
2142
2137
|
this.bash.dispose();
|
|
2143
2138
|
// Clear in-memory caches
|
|
@@ -2150,6 +2145,15 @@ export class LLMAgent extends EventEmitter {
|
|
|
2150
2145
|
clearTimeout(timeout);
|
|
2151
2146
|
}
|
|
2152
2147
|
this.toolApprovalTimeouts.clear();
|
|
2148
|
+
// Resolve any pending approval callbacks so awaiting promises don't hang forever
|
|
2149
|
+
for (const [, callback] of this.toolApprovalCallbacks) {
|
|
2150
|
+
try {
|
|
2151
|
+
callback(false);
|
|
2152
|
+
}
|
|
2153
|
+
catch {
|
|
2154
|
+
// Ignore callback errors during teardown
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2153
2157
|
this.toolApprovalCallbacks.clear();
|
|
2154
2158
|
// Clear conversation history to free memory
|
|
2155
2159
|
this.chatHistory = [];
|