@defai.digital/ax-cli 3.7.2 → 3.8.2
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 +148 -53
- package/dist/agent/context-manager.d.ts +15 -1
- package/dist/agent/context-manager.js +50 -19
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/dependency-resolver.js +13 -7
- package/dist/agent/dependency-resolver.js.map +1 -1
- package/dist/agent/llm-agent.d.ts +37 -0
- package/dist/agent/llm-agent.js +318 -98
- package/dist/agent/llm-agent.js.map +1 -1
- package/dist/agent/status-reporter.d.ts +114 -0
- package/dist/agent/status-reporter.js +335 -0
- package/dist/agent/status-reporter.js.map +1 -0
- package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js +8 -2
- package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-const.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-const.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js.map +1 -1
- package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js +9 -3
- package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js.map +1 -1
- package/dist/analyzers/git/churn-calculator.d.ts +2 -0
- package/dist/analyzers/git/churn-calculator.js +42 -8
- package/dist/analyzers/git/churn-calculator.js.map +1 -1
- package/dist/analyzers/git/hotspot-detector.js +2 -2
- package/dist/analyzers/git/hotspot-detector.js.map +1 -1
- package/dist/analyzers/metrics/metrics-analyzer.js +1 -1
- package/dist/analyzers/metrics/metrics-analyzer.js.map +1 -1
- package/dist/analyzers/security/security-analyzer.js +1 -1
- package/dist/analyzers/security/security-analyzer.js.map +1 -1
- package/dist/checkpoint/manager.d.ts +1 -0
- package/dist/checkpoint/manager.js +49 -9
- package/dist/checkpoint/manager.js.map +1 -1
- package/dist/checkpoint/storage.js +2 -2
- package/dist/checkpoint/storage.js.map +1 -1
- package/dist/commands/mcp-migrate.d.ts +9 -0
- package/dist/commands/mcp-migrate.js +172 -0
- package/dist/commands/mcp-migrate.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +211 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/vscode.d.ts +7 -0
- package/dist/commands/vscode.js +363 -0
- package/dist/commands/vscode.js.map +1 -0
- package/dist/index.js +79 -30
- package/dist/index.js.map +1 -1
- package/dist/llm/client.js +33 -4
- package/dist/llm/client.js.map +1 -1
- package/dist/mcp/automatosx-loader.d.ts +84 -0
- package/dist/mcp/automatosx-loader.js +238 -0
- package/dist/mcp/automatosx-loader.js.map +1 -0
- package/dist/mcp/client-mutex-patch.d.ts +36 -0
- package/dist/mcp/client-mutex-patch.js +75 -0
- package/dist/mcp/client-mutex-patch.js.map +1 -0
- package/dist/mcp/client-v2.d.ts +229 -0
- package/dist/mcp/client-v2.js +740 -0
- package/dist/mcp/client-v2.js.map +1 -0
- package/dist/mcp/client.d.ts +111 -13
- package/dist/mcp/client.js +168 -253
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/config-detector-v2.d.ts +83 -0
- package/dist/mcp/config-detector-v2.js +328 -0
- package/dist/mcp/config-detector-v2.js.map +1 -0
- package/dist/mcp/config-detector.d.ts +90 -0
- package/dist/mcp/config-detector.js +242 -0
- package/dist/mcp/config-detector.js.map +1 -0
- package/dist/mcp/config-migrator-v2.d.ts +89 -0
- package/dist/mcp/config-migrator-v2.js +288 -0
- package/dist/mcp/config-migrator-v2.js.map +1 -0
- package/dist/mcp/config-migrator.d.ts +63 -0
- package/dist/mcp/config-migrator.js +269 -0
- package/dist/mcp/config-migrator.js.map +1 -0
- package/dist/mcp/config-v2.d.ts +106 -0
- package/dist/mcp/config-v2.js +417 -0
- package/dist/mcp/config-v2.js.map +1 -0
- package/dist/mcp/config.d.ts +12 -1
- package/dist/mcp/config.js +95 -10
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/error-formatter.d.ts +46 -0
- package/dist/mcp/error-formatter.js +244 -0
- package/dist/mcp/error-formatter.js.map +1 -0
- package/dist/mcp/health.d.ts +5 -0
- package/dist/mcp/health.js +22 -2
- package/dist/mcp/health.js.map +1 -1
- package/dist/mcp/invariants.d.ts +141 -0
- package/dist/mcp/invariants.js +243 -0
- package/dist/mcp/invariants.js.map +1 -0
- package/dist/mcp/mutex-safe.d.ts +153 -0
- package/dist/mcp/mutex-safe.js +260 -0
- package/dist/mcp/mutex-safe.js.map +1 -0
- package/dist/mcp/mutex.d.ts +73 -0
- package/dist/mcp/mutex.js +137 -0
- package/dist/mcp/mutex.js.map +1 -0
- package/dist/mcp/reconnection.d.ts +4 -0
- package/dist/mcp/reconnection.js +25 -1
- package/dist/mcp/reconnection.js.map +1 -1
- package/dist/mcp/transports-v2.d.ts +152 -0
- package/dist/mcp/transports-v2.js +481 -0
- package/dist/mcp/transports-v2.js.map +1 -0
- package/dist/mcp/type-safety.d.ts +231 -0
- package/dist/mcp/type-safety.js +273 -0
- package/dist/mcp/type-safety.js.map +1 -0
- package/dist/planner/task-planner.js +13 -0
- package/dist/planner/task-planner.js.map +1 -1
- package/dist/planner/types.d.ts +6 -6
- package/dist/schemas/confirmation-schemas.d.ts +2 -2
- package/dist/schemas/settings-schemas.d.ts +196 -0
- package/dist/schemas/settings-schemas.js +146 -5
- package/dist/schemas/settings-schemas.js.map +1 -1
- package/dist/sdk/index.d.ts +118 -2
- package/dist/sdk/index.js +146 -4
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/testing.d.ts +182 -0
- package/dist/sdk/testing.js +231 -0
- package/dist/sdk/testing.js.map +1 -1
- package/dist/sdk/version.d.ts +114 -15
- package/dist/sdk/version.js +137 -15
- package/dist/sdk/version.js.map +1 -1
- package/dist/tools/bash.js +54 -9
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +170 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search.js +12 -2
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/text-editor.js +84 -26
- package/dist/tools/text-editor.js.map +1 -1
- package/dist/ui/components/chat-history.js +6 -1
- package/dist/ui/components/chat-history.js.map +1 -1
- package/dist/ui/components/chat-input.d.ts +2 -1
- package/dist/ui/components/chat-input.js +5 -2
- package/dist/ui/components/chat-input.js.map +1 -1
- package/dist/ui/components/chat-interface.js +187 -5
- package/dist/ui/components/chat-interface.js.map +1 -1
- package/dist/ui/components/context-breakdown.d.ts +23 -0
- package/dist/ui/components/context-breakdown.js +124 -0
- package/dist/ui/components/context-breakdown.js.map +1 -0
- package/dist/ui/components/keyboard-help.d.ts +17 -0
- package/dist/ui/components/keyboard-help.js +116 -0
- package/dist/ui/components/keyboard-help.js.map +1 -0
- package/dist/ui/components/keyboard-hints.js +2 -2
- package/dist/ui/components/keyboard-hints.js.map +1 -1
- package/dist/ui/components/quick-actions.js +43 -7
- package/dist/ui/components/quick-actions.js.map +1 -1
- package/dist/ui/components/status-bar.d.ts +3 -0
- package/dist/ui/components/status-bar.js +25 -16
- package/dist/ui/components/status-bar.js.map +1 -1
- package/dist/ui/components/toast-notification.d.ts +42 -0
- package/dist/ui/components/toast-notification.js +30 -2
- package/dist/ui/components/toast-notification.js.map +1 -1
- package/dist/ui/components/tool-group-display.js +34 -4
- package/dist/ui/components/tool-group-display.js.map +1 -1
- package/dist/ui/components/welcome-panel.js +2 -2
- package/dist/ui/components/welcome-panel.js.map +1 -1
- package/dist/ui/hooks/use-enhanced-input.d.ts +9 -1
- package/dist/ui/hooks/use-enhanced-input.js +901 -90
- package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
- package/dist/ui/hooks/use-input-handler.d.ts +11 -1
- package/dist/ui/hooks/use-input-handler.js +67 -3
- package/dist/ui/hooks/use-input-handler.js.map +1 -1
- package/dist/ui/hooks/use-input-history.d.ts +1 -1
- package/dist/ui/hooks/use-input-history.js +50 -14
- package/dist/ui/hooks/use-input-history.js.map +1 -1
- package/dist/ui/utils/bracketed-paste-handler.d.ts +97 -0
- package/dist/ui/utils/bracketed-paste-handler.js +322 -0
- package/dist/ui/utils/bracketed-paste-handler.js.map +1 -0
- package/dist/ui/utils/change-summarizer.js +16 -6
- package/dist/ui/utils/change-summarizer.js.map +1 -1
- package/dist/ui/utils/tool-grouper.d.ts +10 -1
- package/dist/ui/utils/tool-grouper.js +143 -30
- package/dist/ui/utils/tool-grouper.js.map +1 -1
- package/dist/utils/auto-accept-logger.d.ts +173 -0
- package/dist/utils/auto-accept-logger.js +420 -0
- package/dist/utils/auto-accept-logger.js.map +1 -0
- package/dist/utils/background-task-manager.d.ts +11 -0
- package/dist/utils/background-task-manager.js +124 -38
- package/dist/utils/background-task-manager.js.map +1 -1
- package/dist/utils/confirmation-service.d.ts +1 -0
- package/dist/utils/confirmation-service.js +6 -1
- package/dist/utils/confirmation-service.js.map +1 -1
- package/dist/utils/encryption.d.ts +8 -0
- package/dist/utils/encryption.js +44 -27
- package/dist/utils/encryption.js.map +1 -1
- package/dist/utils/enhanced-error-messages.d.ts +33 -0
- package/dist/utils/enhanced-error-messages.js +420 -0
- package/dist/utils/enhanced-error-messages.js.map +1 -0
- package/dist/utils/error-handler.d.ts +13 -3
- package/dist/utils/error-handler.js +16 -4
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/external-editor.d.ts +47 -0
- package/dist/utils/external-editor.js +179 -0
- package/dist/utils/external-editor.js.map +1 -0
- package/dist/utils/history-migration.d.ts +9 -0
- package/dist/utils/history-migration.js +36 -0
- package/dist/utils/history-migration.js.map +1 -0
- package/dist/utils/paste-utils.js +12 -11
- package/dist/utils/paste-utils.js.map +1 -1
- package/dist/utils/rate-limiter.js +20 -1
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/safety-rules.d.ts +64 -0
- package/dist/utils/safety-rules.js +225 -0
- package/dist/utils/safety-rules.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +89 -1
- package/dist/utils/settings-manager.js +359 -3
- package/dist/utils/settings-manager.js.map +1 -1
- package/dist/utils/token-counter.d.ts +2 -0
- package/dist/utils/token-counter.js +32 -9
- package/dist/utils/token-counter.js.map +1 -1
- package/dist/utils/version.d.ts +11 -2
- package/dist/utils/version.js +54 -21
- package/dist/utils/version.js.map +1 -1
- package/package.json +2 -1
package/dist/agent/llm-agent.js
CHANGED
|
@@ -20,6 +20,8 @@ import { SubagentOrchestrator } from "./subagent-orchestrator.js";
|
|
|
20
20
|
import { getTaskPlanner, isComplexRequest, } from "../planner/index.js";
|
|
21
21
|
import { PLANNER_CONFIG } from "../constants.js";
|
|
22
22
|
import { resolveMCPReferences, extractMCPReferences } from "../mcp/resources.js";
|
|
23
|
+
import { SDKError, SDKErrorCode } from "../sdk/errors.js";
|
|
24
|
+
import { getStatusReporter } from "./status-reporter.js";
|
|
23
25
|
export class LLMAgent extends EventEmitter {
|
|
24
26
|
llmClient;
|
|
25
27
|
textEditor;
|
|
@@ -51,6 +53,11 @@ export class LLMAgent extends EventEmitter {
|
|
|
51
53
|
thinkingConfig;
|
|
52
54
|
/** Track if agent has been disposed */
|
|
53
55
|
disposed = false;
|
|
56
|
+
/** Tool approval system for VSCode integration */
|
|
57
|
+
requireToolApproval = false;
|
|
58
|
+
toolApprovalCallbacks = new Map();
|
|
59
|
+
/** BUG FIX: Track approval timeouts for cleanup to prevent memory leaks */
|
|
60
|
+
toolApprovalTimeouts = new Map();
|
|
54
61
|
constructor(apiKey, baseURL, model, maxToolRounds) {
|
|
55
62
|
super();
|
|
56
63
|
const manager = getSettingsManager();
|
|
@@ -76,10 +83,15 @@ export class LLMAgent extends EventEmitter {
|
|
|
76
83
|
// Load sampling configuration from settings (supports env vars, project, and user settings)
|
|
77
84
|
this.samplingConfig = manager.getSamplingSettings();
|
|
78
85
|
// Wire up checkpoint callback for automatic checkpoint creation
|
|
86
|
+
// CRITICAL FIX: Deep clone chatHistory to prevent race conditions
|
|
87
|
+
// The checkpoint creation is async and chatHistory can be modified during the operation
|
|
79
88
|
this.textEditor.setCheckpointCallback(async (files, description) => {
|
|
89
|
+
// Create immutable snapshot of chat history at callback time
|
|
90
|
+
// This prevents inconsistencies if messages are added during checkpoint creation
|
|
91
|
+
const chatHistorySnapshot = JSON.parse(JSON.stringify(this.chatHistory));
|
|
80
92
|
await this.checkpointManager.createCheckpoint({
|
|
81
93
|
files,
|
|
82
|
-
conversationState:
|
|
94
|
+
conversationState: chatHistorySnapshot,
|
|
83
95
|
description,
|
|
84
96
|
metadata: {
|
|
85
97
|
model: this.llmClient.getCurrentModel(),
|
|
@@ -110,6 +122,17 @@ export class LLMAgent extends EventEmitter {
|
|
|
110
122
|
role: "system",
|
|
111
123
|
content: `Current working directory: ${process.cwd()}\nTimestamp: ${new Date().toISOString().split('T')[0]}`,
|
|
112
124
|
});
|
|
125
|
+
// NEW: Listen for context pruning to generate summaries
|
|
126
|
+
// CRITICAL FIX: Wrap async callback to prevent uncaught promise rejections
|
|
127
|
+
// Event listeners don't handle async errors automatically, so we must catch them
|
|
128
|
+
this.contextManager.on('before_prune', (data) => {
|
|
129
|
+
this.handleContextOverflow(data).catch((error) => {
|
|
130
|
+
const errorMsg = extractErrorMessage(error);
|
|
131
|
+
console.error('Error handling context overflow:', errorMsg);
|
|
132
|
+
// Emit error event for monitoring
|
|
133
|
+
this.emit('error', error);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
113
136
|
}
|
|
114
137
|
initializeCheckpointManager() {
|
|
115
138
|
// Initialize checkpoint manager in the background
|
|
@@ -130,7 +153,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
130
153
|
}
|
|
131
154
|
async initializeMCP() {
|
|
132
155
|
// Initialize MCP in the background without blocking
|
|
133
|
-
// Single error handler - no redundant catch needed since inner try-catch handles all errors
|
|
134
156
|
Promise.resolve().then(async () => {
|
|
135
157
|
try {
|
|
136
158
|
const config = loadMCPConfig();
|
|
@@ -144,6 +166,9 @@ export class LLMAgent extends EventEmitter {
|
|
|
144
166
|
console.warn("MCP initialization failed:", errorMsg);
|
|
145
167
|
this.emit('system', `MCP initialization failed: ${errorMsg}`);
|
|
146
168
|
}
|
|
169
|
+
}).catch((error) => {
|
|
170
|
+
// Catch any errors from emit() or other unexpected failures
|
|
171
|
+
console.error("Unexpected MCP initialization error:", error);
|
|
147
172
|
});
|
|
148
173
|
}
|
|
149
174
|
/**
|
|
@@ -188,32 +213,134 @@ export class LLMAgent extends EventEmitter {
|
|
|
188
213
|
getSamplingConfig() {
|
|
189
214
|
return this.samplingConfig;
|
|
190
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Enable or disable tool approval requirement
|
|
218
|
+
* When enabled, text_editor operations will emit 'tool:approval_required' events
|
|
219
|
+
* and wait for approval before executing
|
|
220
|
+
*
|
|
221
|
+
* This is used by VSCode extension to show diff previews
|
|
222
|
+
*
|
|
223
|
+
* @param enabled - Whether to require approval for text_editor operations
|
|
224
|
+
*/
|
|
225
|
+
setRequireToolApproval(enabled) {
|
|
226
|
+
this.requireToolApproval = enabled;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Approve or reject a pending tool call
|
|
230
|
+
* Called by external integrations (e.g., VSCode extension) in response to
|
|
231
|
+
* 'tool:approval_required' events
|
|
232
|
+
*
|
|
233
|
+
* @param toolCallId - The ID of the tool call to approve/reject
|
|
234
|
+
* @param approved - true to execute the tool, false to reject it
|
|
235
|
+
*/
|
|
236
|
+
approveToolCall(toolCallId, approved) {
|
|
237
|
+
const callback = this.toolApprovalCallbacks.get(toolCallId);
|
|
238
|
+
if (callback) {
|
|
239
|
+
// BUG FIX: Clear the timeout when approval is received (prevents memory leak)
|
|
240
|
+
const timeout = this.toolApprovalTimeouts.get(toolCallId);
|
|
241
|
+
if (timeout) {
|
|
242
|
+
clearTimeout(timeout);
|
|
243
|
+
this.toolApprovalTimeouts.delete(toolCallId);
|
|
244
|
+
}
|
|
245
|
+
callback(approved);
|
|
246
|
+
this.toolApprovalCallbacks.delete(toolCallId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Wait for external approval of a tool call
|
|
251
|
+
* Emits 'tool:approval_required' event and waits for approveToolCall() to be called
|
|
252
|
+
*
|
|
253
|
+
* @param toolCall - The tool call awaiting approval
|
|
254
|
+
* @returns Promise<boolean> - true if approved, false if rejected or timeout
|
|
255
|
+
*/
|
|
256
|
+
waitForToolApproval(toolCall) {
|
|
257
|
+
return new Promise((resolve) => {
|
|
258
|
+
// Emit event so external integrations can show diff preview
|
|
259
|
+
this.emit('tool:approval_required', toolCall);
|
|
260
|
+
// Store callback
|
|
261
|
+
this.toolApprovalCallbacks.set(toolCall.id, resolve);
|
|
262
|
+
// BUG FIX: Track the timeout so it can be cleared on approval/disposal
|
|
263
|
+
// This prevents memory leaks from dangling timers
|
|
264
|
+
const timeoutId = setTimeout(() => {
|
|
265
|
+
// Clean up both the callback and timeout tracking
|
|
266
|
+
this.toolApprovalTimeouts.delete(toolCall.id);
|
|
267
|
+
if (this.toolApprovalCallbacks.has(toolCall.id)) {
|
|
268
|
+
this.toolApprovalCallbacks.delete(toolCall.id);
|
|
269
|
+
resolve(false); // Auto-reject on timeout
|
|
270
|
+
}
|
|
271
|
+
}, 5 * 60 * 1000);
|
|
272
|
+
// Track the timeout for cleanup
|
|
273
|
+
this.toolApprovalTimeouts.set(toolCall.id, timeoutId);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Handle context overflow by generating a summary
|
|
278
|
+
* Called when context manager is about to prune messages
|
|
279
|
+
*/
|
|
280
|
+
async handleContextOverflow(data) {
|
|
281
|
+
try {
|
|
282
|
+
const reporter = getStatusReporter();
|
|
283
|
+
const summary = await reporter.generateContextSummary(data.messages, this.chatHistory, 'context_overflow', data.tokenCount);
|
|
284
|
+
// Log for debugging
|
|
285
|
+
if (process.env.DEBUG) {
|
|
286
|
+
console.log(`[Context Overflow] Summary generated: ${summary.path}`);
|
|
287
|
+
}
|
|
288
|
+
// Add a chat entry to inform user (non-blocking)
|
|
289
|
+
const summaryEntry = {
|
|
290
|
+
type: 'assistant',
|
|
291
|
+
content: `⚠️ Context window approaching limit (${data.tokenCount.toLocaleString()} tokens). Summary saved to:\n\`${summary.path}\``,
|
|
292
|
+
timestamp: new Date(),
|
|
293
|
+
};
|
|
294
|
+
this.chatHistory.push(summaryEntry);
|
|
295
|
+
// Emit event for UI/logging
|
|
296
|
+
this.emit('context:summary', summary);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// Summary generation failure should not block execution
|
|
300
|
+
const errorMsg = extractErrorMessage(error);
|
|
301
|
+
console.warn('Failed to generate context summary:', errorMsg);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
191
304
|
/**
|
|
192
305
|
* Apply context pruning to both messages and chatHistory
|
|
193
306
|
* BUGFIX: Prevents chatHistory from growing unbounded
|
|
194
307
|
*/
|
|
195
308
|
applyContextPruning() {
|
|
309
|
+
// Prune LLM messages if needed
|
|
196
310
|
if (this.contextManager.shouldPrune(this.messages, this.tokenCounter)) {
|
|
197
|
-
// Prune LLM messages
|
|
198
311
|
this.messages = this.contextManager.pruneMessages(this.messages, this.tokenCounter);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
312
|
+
}
|
|
313
|
+
// CRITICAL FIX: Always check and prune chatHistory to prevent unbounded growth
|
|
314
|
+
// This must happen UNCONDITIONALLY, even if context pruning is disabled
|
|
315
|
+
// Keep last 200 entries which is more than enough for UI display
|
|
316
|
+
const MAX_CHAT_HISTORY_ENTRIES = 200;
|
|
317
|
+
if (this.chatHistory.length > MAX_CHAT_HISTORY_ENTRIES) {
|
|
318
|
+
const entriesToRemove = this.chatHistory.length - MAX_CHAT_HISTORY_ENTRIES;
|
|
319
|
+
this.chatHistory = this.chatHistory.slice(entriesToRemove);
|
|
320
|
+
// Update tool call index map after pruning
|
|
321
|
+
// Clear and rebuild only for remaining entries
|
|
322
|
+
this.toolCallIndexMap.clear();
|
|
323
|
+
this.chatHistory.forEach((entry, index) => {
|
|
324
|
+
if (entry.type === "tool_call" && entry.toolCall?.id) {
|
|
325
|
+
this.toolCallIndexMap.set(entry.toolCall.id, index);
|
|
326
|
+
}
|
|
327
|
+
else if (entry.type === "tool_result" && entry.toolCall?.id) {
|
|
328
|
+
this.toolCallIndexMap.set(entry.toolCall.id, index);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// CRITICAL FIX: Add hard limit for messages array as safety backstop
|
|
333
|
+
// In case contextManager.shouldPrune() always returns false
|
|
334
|
+
const MAX_MESSAGES = 500;
|
|
335
|
+
if (this.messages.length > MAX_MESSAGES) {
|
|
336
|
+
// Keep system message (if exists) + last N messages
|
|
337
|
+
const systemMessages = this.messages.filter(m => m.role === 'system');
|
|
338
|
+
const nonSystemMessages = this.messages.filter(m => m.role !== 'system');
|
|
339
|
+
const keepMessages = Math.min(nonSystemMessages.length, MAX_MESSAGES - systemMessages.length);
|
|
340
|
+
this.messages = [
|
|
341
|
+
...systemMessages,
|
|
342
|
+
...nonSystemMessages.slice(-keepMessages)
|
|
343
|
+
];
|
|
217
344
|
}
|
|
218
345
|
}
|
|
219
346
|
/**
|
|
@@ -235,13 +362,16 @@ export class LLMAgent extends EventEmitter {
|
|
|
235
362
|
try {
|
|
236
363
|
const args = JSON.parse(toolCall.function.arguments || '{}');
|
|
237
364
|
this.toolCallArgsCache.set(toolCall.id, args);
|
|
238
|
-
// Prevent unbounded memory growth
|
|
365
|
+
// CRITICAL FIX: Prevent unbounded memory growth with proper cache eviction
|
|
366
|
+
// When cache exceeds limit, reduce to 80% capacity (not just remove 100 entries)
|
|
239
367
|
if (this.toolCallArgsCache.size > 500) {
|
|
368
|
+
const targetSize = 400; // 80% of max capacity
|
|
369
|
+
const toRemove = this.toolCallArgsCache.size - targetSize;
|
|
240
370
|
let deleted = 0;
|
|
241
371
|
for (const key of this.toolCallArgsCache.keys()) {
|
|
242
372
|
this.toolCallArgsCache.delete(key);
|
|
243
373
|
deleted++;
|
|
244
|
-
if (deleted >=
|
|
374
|
+
if (deleted >= toRemove)
|
|
245
375
|
break;
|
|
246
376
|
}
|
|
247
377
|
}
|
|
@@ -727,6 +857,29 @@ export class LLMAgent extends EventEmitter {
|
|
|
727
857
|
}
|
|
728
858
|
// Emit plan completed event
|
|
729
859
|
this.emit("plan:completed", { plan, result: planResult });
|
|
860
|
+
// Generate status report on plan completion
|
|
861
|
+
try {
|
|
862
|
+
const reporter = getStatusReporter();
|
|
863
|
+
const tokenCount = this.tokenCounter.countMessageTokens(this.messages);
|
|
864
|
+
const statusReport = await reporter.generateStatusReport({
|
|
865
|
+
messages: this.messages,
|
|
866
|
+
chatHistory: this.chatHistory,
|
|
867
|
+
tokenCount,
|
|
868
|
+
plan,
|
|
869
|
+
});
|
|
870
|
+
// Notify user of status report
|
|
871
|
+
yield {
|
|
872
|
+
type: "content",
|
|
873
|
+
content: `\n📊 Status report saved to: \`${statusReport.path}\`\n`,
|
|
874
|
+
};
|
|
875
|
+
// Emit event for UI/logging
|
|
876
|
+
this.emit("plan:report", statusReport);
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
// Status report generation failure should not block execution
|
|
880
|
+
const errorMsg = extractErrorMessage(error);
|
|
881
|
+
console.warn("Failed to generate status report:", errorMsg);
|
|
882
|
+
}
|
|
730
883
|
this.currentPlan = null;
|
|
731
884
|
}
|
|
732
885
|
catch (error) {
|
|
@@ -1167,85 +1320,102 @@ export class LLMAgent extends EventEmitter {
|
|
|
1167
1320
|
let accumulatedContent = "";
|
|
1168
1321
|
let toolCallsYielded = false;
|
|
1169
1322
|
let usageData = null;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1323
|
+
// CRITICAL FIX: Ensure stream is properly closed on cancellation or error
|
|
1324
|
+
// Without this, HTTP connections and buffers remain in memory
|
|
1325
|
+
try {
|
|
1326
|
+
for await (const chunk of stream) {
|
|
1327
|
+
// Check for cancellation in the streaming loop
|
|
1328
|
+
if (this.isCancelled()) {
|
|
1329
|
+
yield* this.yieldCancellation();
|
|
1330
|
+
// Return empty state after cancellation to avoid processing partial results
|
|
1331
|
+
return { accumulated: {}, content: "", yielded: false };
|
|
1332
|
+
}
|
|
1333
|
+
if (!chunk.choices?.[0])
|
|
1334
|
+
continue;
|
|
1335
|
+
// Capture usage data from chunks (usually in the final chunk)
|
|
1336
|
+
if (chunk.usage) {
|
|
1337
|
+
usageData = chunk.usage;
|
|
1338
|
+
}
|
|
1339
|
+
// Accumulate the message using reducer
|
|
1340
|
+
accumulatedMessage = this.messageReducer(accumulatedMessage, chunk);
|
|
1341
|
+
// Check for tool calls - yield when we have complete tool calls with function names
|
|
1342
|
+
const toolCalls = accumulatedMessage.tool_calls;
|
|
1343
|
+
if (!toolCallsYielded && toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
1344
|
+
const hasCompleteTool = toolCalls.some((tc) => tc.function?.name);
|
|
1345
|
+
if (hasCompleteTool) {
|
|
1346
|
+
yield {
|
|
1347
|
+
type: "tool_calls",
|
|
1348
|
+
toolCalls: toolCalls,
|
|
1349
|
+
};
|
|
1350
|
+
toolCallsYielded = true;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
// Stream reasoning content (GLM-4.6 thinking mode)
|
|
1354
|
+
// Safety check: ensure choices[0] exists before accessing
|
|
1355
|
+
if (chunk.choices[0]?.delta?.reasoning_content) {
|
|
1190
1356
|
yield {
|
|
1191
|
-
type: "
|
|
1192
|
-
|
|
1357
|
+
type: "reasoning",
|
|
1358
|
+
reasoningContent: chunk.choices[0].delta.reasoning_content,
|
|
1193
1359
|
};
|
|
1194
|
-
|
|
1360
|
+
}
|
|
1361
|
+
// Stream content as it comes
|
|
1362
|
+
if (chunk.choices[0]?.delta?.content) {
|
|
1363
|
+
accumulatedContent += chunk.choices[0].delta.content;
|
|
1364
|
+
yield {
|
|
1365
|
+
type: "content",
|
|
1366
|
+
content: chunk.choices[0].delta.content,
|
|
1367
|
+
};
|
|
1368
|
+
// Emit token count update (throttled and optimized)
|
|
1369
|
+
const now = Date.now();
|
|
1370
|
+
if (now - lastTokenUpdate.value > 1000) { // Increased throttle to 1s for better performance
|
|
1371
|
+
lastTokenUpdate.value = now;
|
|
1372
|
+
// Use fast estimation during streaming (4 chars ≈ 1 token)
|
|
1373
|
+
// This is ~70% faster than tiktoken encoding
|
|
1374
|
+
const estimatedOutputTokens = Math.floor(accumulatedContent.length / 4) +
|
|
1375
|
+
(accumulatedMessage.tool_calls
|
|
1376
|
+
? Math.floor(JSON.stringify(accumulatedMessage.tool_calls).length / 4)
|
|
1377
|
+
: 0);
|
|
1378
|
+
totalOutputTokens.value = estimatedOutputTokens;
|
|
1379
|
+
yield {
|
|
1380
|
+
type: "token_count",
|
|
1381
|
+
tokenCount: inputTokens + estimatedOutputTokens,
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1195
1384
|
}
|
|
1196
1385
|
}
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (chunk.choices[0]?.delta?.content) {
|
|
1207
|
-
accumulatedContent += chunk.choices[0].delta.content;
|
|
1208
|
-
yield {
|
|
1209
|
-
type: "content",
|
|
1210
|
-
content: chunk.choices[0].delta.content,
|
|
1211
|
-
};
|
|
1212
|
-
// Emit token count update (throttled and optimized)
|
|
1213
|
-
const now = Date.now();
|
|
1214
|
-
if (now - lastTokenUpdate.value > 1000) { // Increased throttle to 1s for better performance
|
|
1215
|
-
lastTokenUpdate.value = now;
|
|
1216
|
-
// Use fast estimation during streaming (4 chars ≈ 1 token)
|
|
1217
|
-
// This is ~70% faster than tiktoken encoding
|
|
1218
|
-
const estimatedOutputTokens = Math.floor(accumulatedContent.length / 4) +
|
|
1219
|
-
(accumulatedMessage.tool_calls
|
|
1220
|
-
? Math.floor(JSON.stringify(accumulatedMessage.tool_calls).length / 4)
|
|
1221
|
-
: 0);
|
|
1222
|
-
totalOutputTokens.value = estimatedOutputTokens;
|
|
1386
|
+
// Track usage if available and emit accurate final token count
|
|
1387
|
+
if (usageData) {
|
|
1388
|
+
const tracker = getUsageTracker();
|
|
1389
|
+
tracker.trackUsage(this.llmClient.getCurrentModel(), usageData);
|
|
1390
|
+
// Emit accurate token count from API usage data (replaces estimation)
|
|
1391
|
+
const totalTokens = usageData.total_tokens;
|
|
1392
|
+
const completionTokens = usageData.completion_tokens;
|
|
1393
|
+
if (totalTokens) {
|
|
1394
|
+
totalOutputTokens.value = completionTokens || 0;
|
|
1223
1395
|
yield {
|
|
1224
1396
|
type: "token_count",
|
|
1225
|
-
tokenCount:
|
|
1397
|
+
tokenCount: totalTokens,
|
|
1226
1398
|
};
|
|
1227
1399
|
}
|
|
1228
1400
|
}
|
|
1401
|
+
// CRITICAL: Yield the accumulated result so the main loop can access it!
|
|
1402
|
+
const result = { accumulated: accumulatedMessage, content: accumulatedContent, yielded: toolCallsYielded };
|
|
1403
|
+
yield result;
|
|
1404
|
+
return result;
|
|
1229
1405
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
tokenCount: totalTokens,
|
|
1242
|
-
};
|
|
1406
|
+
finally {
|
|
1407
|
+
// CRITICAL FIX: Properly close the async iterator to release HTTP connections and buffers
|
|
1408
|
+
// This prevents socket leaks when streams are cancelled or errors occur
|
|
1409
|
+
if (typeof stream.return === 'function') {
|
|
1410
|
+
try {
|
|
1411
|
+
await stream.return();
|
|
1412
|
+
}
|
|
1413
|
+
catch (cleanupError) {
|
|
1414
|
+
// Log but don't throw - cleanup errors shouldn't break the flow
|
|
1415
|
+
console.warn('Stream cleanup warning:', cleanupError);
|
|
1416
|
+
}
|
|
1243
1417
|
}
|
|
1244
1418
|
}
|
|
1245
|
-
// CRITICAL: Yield the accumulated result so the main loop can access it!
|
|
1246
|
-
const result = { accumulated: accumulatedMessage, content: accumulatedContent, yielded: toolCallsYielded };
|
|
1247
|
-
yield result;
|
|
1248
|
-
return result;
|
|
1249
1419
|
}
|
|
1250
1420
|
/**
|
|
1251
1421
|
* Add assistant message to history and conversation
|
|
@@ -1485,6 +1655,27 @@ export class LLMAgent extends EventEmitter {
|
|
|
1485
1655
|
return { success: false, error: parseResult.error };
|
|
1486
1656
|
}
|
|
1487
1657
|
const args = parseResult.args;
|
|
1658
|
+
// Check if tool approval is required (for VSCode integration)
|
|
1659
|
+
if (this.requireToolApproval) {
|
|
1660
|
+
// Only require approval for file modification operations
|
|
1661
|
+
const needsApproval = toolCall.function.name === "create_file" ||
|
|
1662
|
+
toolCall.function.name === "str_replace_editor" ||
|
|
1663
|
+
toolCall.function.name === "insert_text";
|
|
1664
|
+
if (needsApproval) {
|
|
1665
|
+
// Emit event and wait for approval
|
|
1666
|
+
const approved = await this.waitForToolApproval(toolCall);
|
|
1667
|
+
if (!approved) {
|
|
1668
|
+
// User rejected the change
|
|
1669
|
+
this.emit('tool:rejected', toolCall);
|
|
1670
|
+
return {
|
|
1671
|
+
success: false,
|
|
1672
|
+
error: 'Change rejected by user'
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
// User approved
|
|
1676
|
+
this.emit('tool:approved', toolCall);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1488
1679
|
// Helper to safely get string argument with validation
|
|
1489
1680
|
const getString = (key, required = true) => {
|
|
1490
1681
|
const value = args[key];
|
|
@@ -1724,6 +1915,9 @@ export class LLMAgent extends EventEmitter {
|
|
|
1724
1915
|
// Safely preserve system message if it exists
|
|
1725
1916
|
const systemMessage = this.messages.length > 0 ? this.messages[0] : null;
|
|
1726
1917
|
this.messages = systemMessage ? [systemMessage] : [];
|
|
1918
|
+
// CRITICAL FIX: Track tool calls to validate tool results
|
|
1919
|
+
// Prevents API errors from orphaned tool results without corresponding tool calls
|
|
1920
|
+
const toolCallIds = new Set();
|
|
1727
1921
|
for (const entry of conversationState) {
|
|
1728
1922
|
if (entry.type === 'user') {
|
|
1729
1923
|
this.messages.push({
|
|
@@ -1732,6 +1926,14 @@ export class LLMAgent extends EventEmitter {
|
|
|
1732
1926
|
});
|
|
1733
1927
|
}
|
|
1734
1928
|
else if (entry.type === 'assistant') {
|
|
1929
|
+
// Track tool call IDs from assistant messages
|
|
1930
|
+
if (entry.toolCalls && Array.isArray(entry.toolCalls)) {
|
|
1931
|
+
for (const toolCall of entry.toolCalls) {
|
|
1932
|
+
if (toolCall?.id) {
|
|
1933
|
+
toolCallIds.add(toolCall.id);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1735
1937
|
this.messages.push({
|
|
1736
1938
|
role: 'assistant',
|
|
1737
1939
|
content: entry.content,
|
|
@@ -1739,11 +1941,18 @@ export class LLMAgent extends EventEmitter {
|
|
|
1739
1941
|
});
|
|
1740
1942
|
}
|
|
1741
1943
|
else if (entry.type === 'tool_result' && entry.toolCall) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1944
|
+
// CRITICAL FIX: Only add tool result if corresponding tool call exists
|
|
1945
|
+
// This prevents "tool message without corresponding tool call" API errors
|
|
1946
|
+
if (toolCallIds.has(entry.toolCall.id)) {
|
|
1947
|
+
this.messages.push({
|
|
1948
|
+
role: 'tool',
|
|
1949
|
+
content: entry.content,
|
|
1950
|
+
tool_call_id: entry.toolCall.id,
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
else {
|
|
1954
|
+
console.warn(`Skipping orphaned tool result for tool_call_id: ${entry.toolCall.id}`);
|
|
1955
|
+
}
|
|
1747
1956
|
}
|
|
1748
1957
|
}
|
|
1749
1958
|
this.emit('system', `Conversation rewound to checkpoint ${checkpointId}`);
|
|
@@ -1890,7 +2099,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
1890
2099
|
*/
|
|
1891
2100
|
checkDisposed() {
|
|
1892
2101
|
if (this.disposed) {
|
|
1893
|
-
const { SDKError, SDKErrorCode } = require('../sdk/errors.js');
|
|
1894
2102
|
throw new SDKError(SDKErrorCode.AGENT_DISPOSED, 'Agent has been disposed and cannot be used. Create a new agent instance.');
|
|
1895
2103
|
}
|
|
1896
2104
|
}
|
|
@@ -1927,17 +2135,29 @@ export class LLMAgent extends EventEmitter {
|
|
|
1927
2135
|
this.disposed = true;
|
|
1928
2136
|
// Remove all event listeners to prevent memory leaks
|
|
1929
2137
|
this.removeAllListeners();
|
|
2138
|
+
// CRITICAL FIX: Remove event listeners from contextManager to prevent memory leak
|
|
2139
|
+
// The 'before_prune' listener was registered in constructor (line 188) but never removed
|
|
2140
|
+
this.contextManager.removeAllListeners('before_prune');
|
|
1930
2141
|
// Dispose tools that have cleanup methods
|
|
1931
2142
|
this.bash.dispose();
|
|
1932
2143
|
// Clear in-memory caches
|
|
1933
2144
|
this.recentToolCalls.clear();
|
|
1934
2145
|
this.toolCallIndexMap.clear();
|
|
1935
2146
|
this.toolCallArgsCache.clear();
|
|
2147
|
+
// BUG FIX: Clear all pending tool approval timeouts to prevent memory leaks
|
|
2148
|
+
// These timers would otherwise keep running for up to 5 minutes after dispose
|
|
2149
|
+
for (const timeout of this.toolApprovalTimeouts.values()) {
|
|
2150
|
+
clearTimeout(timeout);
|
|
2151
|
+
}
|
|
2152
|
+
this.toolApprovalTimeouts.clear();
|
|
2153
|
+
this.toolApprovalCallbacks.clear();
|
|
1936
2154
|
// Clear conversation history to free memory
|
|
1937
2155
|
this.chatHistory = [];
|
|
1938
2156
|
this.messages = [];
|
|
1939
|
-
// Dispose
|
|
1940
|
-
|
|
2157
|
+
// Dispose context manager (tokenCounter is a singleton, don't dispose)
|
|
2158
|
+
// CRITICAL FIX: tokenCounter is obtained via getTokenCounter() which returns
|
|
2159
|
+
// a shared singleton instance. Disposing it would break other agent instances
|
|
2160
|
+
// using the same model. The singleton manages its own lifecycle.
|
|
1941
2161
|
this.contextManager.dispose();
|
|
1942
2162
|
// Abort any in-flight requests
|
|
1943
2163
|
if (this.abortController) {
|