@defai.digital/ax-cli 3.8.22 → 3.8.24
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 +8 -2
- package/config-defaults/models.yaml +1 -1
- package/dist/agent/core/index.d.ts +8 -0
- package/dist/agent/core/index.js +9 -0
- package/dist/agent/core/index.js.map +1 -0
- package/dist/agent/core/types.d.ts +92 -0
- package/dist/agent/core/types.js +11 -0
- package/dist/agent/core/types.js.map +1 -0
- package/dist/agent/execution/index.d.ts +9 -0
- package/dist/agent/execution/index.js +9 -0
- package/dist/agent/execution/index.js.map +1 -0
- package/dist/agent/execution/tool-executor.d.ts +79 -0
- package/dist/agent/execution/tool-executor.js +281 -0
- package/dist/agent/execution/tool-executor.js.map +1 -0
- package/dist/agent/llm-agent.d.ts +22 -98
- package/dist/agent/llm-agent.js +181 -722
- package/dist/agent/llm-agent.js.map +1 -1
- package/dist/agent/planning/index.d.ts +9 -0
- package/dist/agent/planning/index.js +9 -0
- package/dist/agent/planning/index.js.map +1 -0
- package/dist/agent/planning/plan-executor.d.ts +84 -0
- package/dist/agent/planning/plan-executor.js +223 -0
- package/dist/agent/planning/plan-executor.js.map +1 -0
- package/dist/agent/streaming/index.d.ts +9 -0
- package/dist/agent/streaming/index.js +9 -0
- package/dist/agent/streaming/index.js.map +1 -0
- package/dist/agent/streaming/stream-handler.d.ts +62 -0
- package/dist/agent/streaming/stream-handler.js +193 -0
- package/dist/agent/streaming/stream-handler.js.map +1 -0
- package/dist/agent/subagent-orchestrator.d.ts +3 -3
- package/dist/agent/subagent-orchestrator.js +1 -0
- package/dist/agent/subagent-orchestrator.js.map +1 -1
- package/dist/agent/subagent-types.d.ts +10 -22
- package/dist/agent/subagent-types.js +19 -0
- package/dist/agent/subagent-types.js.map +1 -1
- package/dist/commands/usage.js +14 -0
- package/dist/commands/usage.js.map +1 -1
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/llm/client.d.ts +33 -1
- package/dist/llm/client.js +23 -11
- package/dist/llm/client.js.map +1 -1
- package/dist/llm/types.d.ts +7 -1
- package/dist/llm/types.js +5 -4
- package/dist/llm/types.js.map +1 -1
- package/dist/mcp/index.d.ts +31 -0
- package/dist/mcp/index.js +36 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/planner/types.d.ts +8 -8
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/tool-schemas.d.ts +12 -12
- package/dist/tools/bash.js +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/text-editor.js +57 -63
- package/dist/tools/text-editor.js.map +1 -1
- package/dist/ui/hooks/use-enhanced-input.js +66 -81
- package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
- package/dist/utils/background-task-manager.js +10 -2
- package/dist/utils/background-task-manager.js.map +1 -1
- package/dist/utils/confirmation-service.js +8 -5
- package/dist/utils/confirmation-service.js.map +1 -1
- package/dist/utils/index.d.ts +85 -6
- package/dist/utils/index.js +103 -15
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry-helper.d.ts +7 -6
- package/dist/utils/retry-helper.js +8 -6
- package/dist/utils/retry-helper.js.map +1 -1
- package/dist/utils/settings-manager.d.ts +6 -0
- package/dist/utils/settings-manager.js +51 -64
- package/dist/utils/settings-manager.js.map +1 -1
- package/node_modules/@ax-cli/schemas/dist/index.d.ts +1 -0
- package/node_modules/@ax-cli/schemas/dist/index.d.ts.map +1 -1
- package/node_modules/@ax-cli/schemas/dist/index.js.map +1 -1
- package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.d.ts +164 -0
- package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.d.ts.map +1 -0
- package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.js +10 -0
- package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.js.map +1 -0
- package/node_modules/@ax-cli/schemas/dist/public/agent/index.d.ts +9 -0
- package/node_modules/@ax-cli/schemas/dist/public/agent/index.d.ts.map +1 -0
- package/node_modules/@ax-cli/schemas/dist/public/agent/index.js +9 -0
- package/node_modules/@ax-cli/schemas/dist/public/agent/index.js.map +1 -0
- package/package.json +1 -1
- package/packages/schemas/dist/index.d.ts +1 -0
- package/packages/schemas/dist/index.d.ts.map +1 -1
- package/packages/schemas/dist/index.js.map +1 -1
- package/packages/schemas/dist/public/agent/chat-types.d.ts +164 -0
- package/packages/schemas/dist/public/agent/chat-types.d.ts.map +1 -0
- package/packages/schemas/dist/public/agent/chat-types.js +10 -0
- package/packages/schemas/dist/public/agent/chat-types.js.map +1 -0
- package/packages/schemas/dist/public/agent/index.d.ts +9 -0
- package/packages/schemas/dist/public/agent/index.d.ts.map +1 -0
- package/packages/schemas/dist/public/agent/index.js +9 -0
- package/packages/schemas/dist/public/agent/index.js.map +1 -0
- package/dist/mcp/config-detector-v2.d.ts +0 -83
- package/dist/mcp/config-detector-v2.js +0 -328
- package/dist/mcp/config-detector-v2.js.map +0 -1
- package/dist/mcp/config-migrator-v2.d.ts +0 -89
- package/dist/mcp/config-migrator-v2.js +0 -288
- package/dist/mcp/config-migrator-v2.js.map +0 -1
- package/dist/mcp/config-v2.d.ts +0 -111
- package/dist/mcp/config-v2.js +0 -443
- package/dist/mcp/config-v2.js.map +0 -1
- package/dist/mcp/transports-v2.d.ts +0 -152
- package/dist/mcp/transports-v2.js +0 -481
- package/dist/mcp/transports-v2.js.map +0 -1
- package/dist/utils/error-sanitizer.d.ts +0 -119
- package/dist/utils/error-sanitizer.js +0 -253
- package/dist/utils/error-sanitizer.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -74
- package/dist/utils/errors.js +0 -139
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/incremental-analyzer.d.ts +0 -134
- package/dist/utils/incremental-analyzer.js +0 -377
- package/dist/utils/incremental-analyzer.js.map +0 -1
- package/dist/utils/settings.d.ts +0 -1
- package/dist/utils/settings.js +0 -4
- package/dist/utils/settings.js.map +0 -1
- package/dist/utils/streaming-analyzer.d.ts +0 -160
- package/dist/utils/streaming-analyzer.js +0 -214
- package/dist/utils/streaming-analyzer.js.map +0 -1
package/dist/agent/llm-agent.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { LLMClient } from "../llm/client.js";
|
|
2
2
|
import { getAllGrokTools, getMCPManager, initializeMCPServers, } from "../llm/tools.js";
|
|
3
3
|
import { loadMCPConfig } from "../mcp/config.js";
|
|
4
|
-
import { TextEditorTool, BashTool, TodoTool, SearchTool, } from "../tools/index.js";
|
|
5
|
-
import { BashOutputTool } from "../tools/bash-output.js";
|
|
6
|
-
import { ArchitectureTool } from "../tools/analysis-tools/architecture-tool.js";
|
|
7
|
-
import { ValidationTool } from "../tools/analysis-tools/validation-tool.js";
|
|
8
4
|
import { EventEmitter } from "events";
|
|
9
5
|
import { AGENT_CONFIG, CACHE_CONFIG, TIMEOUT_CONFIG } from "../constants.js";
|
|
10
6
|
import { getTokenCounter } from "../utils/token-counter.js";
|
|
@@ -12,26 +8,37 @@ import { loadCustomInstructions } from "../utils/custom-instructions.js";
|
|
|
12
8
|
import { getSettingsManager } from "../utils/settings-manager.js";
|
|
13
9
|
import { ContextManager } from "./context-manager.js";
|
|
14
10
|
import { buildSystemPrompt } from "../utils/prompt-builder.js";
|
|
15
|
-
|
|
11
|
+
// Note: getUsageTracker is now used by StreamHandler (Phase 2 refactoring)
|
|
16
12
|
import { extractErrorMessage } from "../utils/error-handler.js";
|
|
17
13
|
import { getCheckpointManager } from "../checkpoint/index.js";
|
|
18
14
|
import { SubagentOrchestrator } from "./subagent-orchestrator.js";
|
|
19
15
|
import { getTaskPlanner, isComplexRequest, } from "../planner/index.js";
|
|
16
|
+
// Note: TaskPhase now used by PlanExecutor (Phase 2 refactoring)
|
|
20
17
|
import { PLANNER_CONFIG } from "../constants.js";
|
|
21
18
|
import { resolveMCPReferences, extractMCPReferences } from "../mcp/resources.js";
|
|
22
19
|
import { SDKError, SDKErrorCode } from "../sdk/errors.js";
|
|
23
20
|
import { getStatusReporter } from "./status-reporter.js";
|
|
24
21
|
import { getLoopDetector, resetLoopDetector } from "./loop-detector.js";
|
|
22
|
+
// Import from extracted modules (Phase 2 refactoring)
|
|
23
|
+
import { ToolExecutor } from "./execution/index.js";
|
|
24
|
+
import { StreamHandler } from "./streaming/index.js";
|
|
25
|
+
import { PlanExecutor } from "./planning/index.js";
|
|
26
|
+
/** Debug flag for loop detection logging (set DEBUG_LOOP_DETECTION=1 to enable) */
|
|
27
|
+
const DEBUG_LOOP = process.env.DEBUG_LOOP_DETECTION === '1';
|
|
28
|
+
/** Log debug message for loop detection (only when DEBUG_LOOP_DETECTION=1) */
|
|
29
|
+
function debugLoop(message) {
|
|
30
|
+
if (DEBUG_LOOP) {
|
|
31
|
+
console.error(`[LOOP DETECTION] ${message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
25
34
|
export class LLMAgent extends EventEmitter {
|
|
26
35
|
llmClient;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
_architectureTool;
|
|
34
|
-
_validationTool;
|
|
36
|
+
// Tool execution delegated to ToolExecutor (Phase 2 refactoring)
|
|
37
|
+
toolExecutor;
|
|
38
|
+
// Stream processing delegated to StreamHandler (Phase 2 refactoring)
|
|
39
|
+
streamHandler;
|
|
40
|
+
// Plan execution delegated to PlanExecutor (Phase 2 refactoring)
|
|
41
|
+
planExecutor;
|
|
35
42
|
chatHistory = [];
|
|
36
43
|
messages = [];
|
|
37
44
|
tokenCounter;
|
|
@@ -69,39 +76,52 @@ export class LLMAgent extends EventEmitter {
|
|
|
69
76
|
}
|
|
70
77
|
this.maxToolRounds = maxToolRounds || 400;
|
|
71
78
|
this.llmClient = new LLMClient(apiKey, modelToUse, baseURL);
|
|
72
|
-
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
// Initialize ToolExecutor with checkpoint callback (Phase 2 refactoring)
|
|
80
|
+
this.toolExecutor = new ToolExecutor({
|
|
81
|
+
checkpointCallback: async (files, description) => {
|
|
82
|
+
// BUG FIX: Check if agent is disposed before creating checkpoint
|
|
83
|
+
if (this.disposed)
|
|
84
|
+
return;
|
|
85
|
+
// Create immutable snapshot of chat history at callback time
|
|
86
|
+
const chatHistorySnapshot = JSON.parse(JSON.stringify(this.chatHistory));
|
|
87
|
+
await this.checkpointManager.createCheckpoint({
|
|
88
|
+
files,
|
|
89
|
+
conversationState: chatHistorySnapshot,
|
|
90
|
+
description,
|
|
91
|
+
metadata: {
|
|
92
|
+
model: this.llmClient.getCurrentModel(),
|
|
93
|
+
triggeredBy: 'auto',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
// Initialize StreamHandler with callbacks (Phase 2 refactoring)
|
|
99
|
+
this.streamHandler = new StreamHandler({
|
|
100
|
+
isCancelled: () => this.isCancelled(),
|
|
101
|
+
yieldCancellation: () => this.yieldCancellation(),
|
|
102
|
+
model: modelToUse,
|
|
103
|
+
});
|
|
78
104
|
this.tokenCounter = getTokenCounter(modelToUse);
|
|
79
105
|
this.contextManager = new ContextManager({ model: modelToUse });
|
|
80
106
|
this.checkpointManager = getCheckpointManager();
|
|
81
107
|
this.subagentOrchestrator = new SubagentOrchestrator({ maxConcurrentAgents: 5 });
|
|
82
108
|
this.taskPlanner = getTaskPlanner();
|
|
109
|
+
// Initialize PlanExecutor with callbacks (Phase 2 refactoring)
|
|
110
|
+
this.planExecutor = new PlanExecutor({
|
|
111
|
+
llmClient: this.llmClient,
|
|
112
|
+
tokenCounter: this.tokenCounter,
|
|
113
|
+
toolExecutor: this.toolExecutor,
|
|
114
|
+
getTools: () => getAllGrokTools(),
|
|
115
|
+
executeTool: (toolCall) => this.executeTool(toolCall),
|
|
116
|
+
parseToolArgumentsCached: (toolCall) => this.parseToolArgumentsCached(toolCall),
|
|
117
|
+
buildChatOptions: (options) => this.buildChatOptions(options),
|
|
118
|
+
applyContextPruning: () => this.applyContextPruning(),
|
|
119
|
+
emitter: this,
|
|
120
|
+
maxToolRounds: Math.min(this.maxToolRounds, 50),
|
|
121
|
+
setPlanningEnabled: (enabled) => { this.planningEnabled = enabled; },
|
|
122
|
+
});
|
|
83
123
|
// Load sampling configuration from settings (supports env vars, project, and user settings)
|
|
84
124
|
this.samplingConfig = manager.getSamplingSettings();
|
|
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
|
|
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
|
-
// BUG FIX: Check if agent is disposed before creating checkpoint
|
|
92
|
-
if (this.disposed)
|
|
93
|
-
return;
|
|
94
|
-
const chatHistorySnapshot = JSON.parse(JSON.stringify(this.chatHistory));
|
|
95
|
-
await this.checkpointManager.createCheckpoint({
|
|
96
|
-
files,
|
|
97
|
-
conversationState: chatHistorySnapshot,
|
|
98
|
-
description,
|
|
99
|
-
metadata: {
|
|
100
|
-
model: this.llmClient.getCurrentModel(),
|
|
101
|
-
triggeredBy: 'auto',
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
125
|
// Initialize checkpoint manager
|
|
106
126
|
this.initializeCheckpointManager();
|
|
107
127
|
// Initialize MCP servers if configured
|
|
@@ -112,18 +132,20 @@ export class LLMAgent extends EventEmitter {
|
|
|
112
132
|
customInstructions: customInstructions || undefined,
|
|
113
133
|
});
|
|
114
134
|
// Initialize with system message
|
|
115
|
-
// OPTIMIZATION:
|
|
116
|
-
//
|
|
117
|
-
//
|
|
135
|
+
// GLM 4.6 OPTIMIZATION: Merge static prompt with dynamic context in SINGLE message
|
|
136
|
+
// Z.AI caches by PREFIX matching - keeping static content first maximizes cache hits
|
|
137
|
+
// Dynamic content at END doesn't break cache prefix for the static portion
|
|
138
|
+
// See: https://docs.z.ai/guides/capabilities/cache
|
|
139
|
+
const dynamicContext = [
|
|
140
|
+
'',
|
|
141
|
+
'---',
|
|
142
|
+
'[Session Context]',
|
|
143
|
+
`Working Directory: ${process.cwd()}`,
|
|
144
|
+
`Session Start: ${new Date().toISOString().split('T')[0]}`,
|
|
145
|
+
].join('\n');
|
|
118
146
|
this.messages.push({
|
|
119
147
|
role: "system",
|
|
120
|
-
content: systemPrompt,
|
|
121
|
-
});
|
|
122
|
-
// Add dynamic context as a separate system message
|
|
123
|
-
// This allows the main system prompt to be cached while context varies
|
|
124
|
-
this.messages.push({
|
|
125
|
-
role: "system",
|
|
126
|
-
content: `Current working directory: ${process.cwd()}\nTimestamp: ${new Date().toISOString().split('T')[0]}`,
|
|
148
|
+
content: systemPrompt + dynamicContext,
|
|
127
149
|
});
|
|
128
150
|
// NEW: Listen for context pruning to generate summaries
|
|
129
151
|
// CRITICAL FIX: Wrap async callback to prevent uncaught promise rejections
|
|
@@ -142,42 +164,41 @@ export class LLMAgent extends EventEmitter {
|
|
|
142
164
|
};
|
|
143
165
|
this.contextManager.on('before_prune', this.contextOverflowListener);
|
|
144
166
|
}
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Run an async task in background with proper error handling
|
|
169
|
+
* Centralizes the common pattern of background initialization
|
|
170
|
+
*/
|
|
171
|
+
runBackgroundTask(taskName, task, options) {
|
|
147
172
|
Promise.resolve().then(async () => {
|
|
148
173
|
try {
|
|
149
|
-
await
|
|
150
|
-
|
|
174
|
+
await task();
|
|
175
|
+
if (options?.emitSuccess) {
|
|
176
|
+
this.emit('system', options.emitSuccess);
|
|
177
|
+
}
|
|
151
178
|
}
|
|
152
179
|
catch (error) {
|
|
153
180
|
const errorMsg = extractErrorMessage(error);
|
|
154
|
-
|
|
155
|
-
|
|
181
|
+
if (options?.warnOnError !== false) {
|
|
182
|
+
console.warn(`${taskName} failed:`, errorMsg);
|
|
183
|
+
}
|
|
184
|
+
this.emit('system', `${taskName} failed: ${errorMsg}`);
|
|
156
185
|
}
|
|
157
186
|
}).catch((error) => {
|
|
158
|
-
|
|
159
|
-
console.warn("Unexpected error during checkpoint initialization:", errorMsg);
|
|
187
|
+
console.error(`Unexpected error during ${taskName}:`, error);
|
|
160
188
|
});
|
|
161
189
|
}
|
|
190
|
+
initializeCheckpointManager() {
|
|
191
|
+
this.runBackgroundTask('Checkpoint initialization', async () => {
|
|
192
|
+
await this.checkpointManager.initialize();
|
|
193
|
+
}, { emitSuccess: 'Checkpoint system initialized' });
|
|
194
|
+
}
|
|
162
195
|
async initializeMCP() {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.emit('system', 'MCP servers initialized successfully');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
const errorMsg = extractErrorMessage(error);
|
|
174
|
-
console.warn("MCP initialization failed:", errorMsg);
|
|
175
|
-
this.emit('system', `MCP initialization failed: ${errorMsg}`);
|
|
176
|
-
}
|
|
177
|
-
}).catch((error) => {
|
|
178
|
-
// Catch any errors from emit() or other unexpected failures
|
|
179
|
-
console.error("Unexpected MCP initialization error:", error);
|
|
180
|
-
});
|
|
196
|
+
const config = loadMCPConfig();
|
|
197
|
+
if (config.servers.length === 0)
|
|
198
|
+
return; // Skip if no servers configured
|
|
199
|
+
this.runBackgroundTask('MCP initialization', async () => {
|
|
200
|
+
await initializeMCPServers();
|
|
201
|
+
}, { emitSuccess: 'MCP servers initialized successfully', warnOnError: true });
|
|
181
202
|
}
|
|
182
203
|
/**
|
|
183
204
|
* Build chat options with sampling and thinking configuration included
|
|
@@ -391,26 +412,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
391
412
|
return {};
|
|
392
413
|
}
|
|
393
414
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Lazy-loaded getter for ArchitectureTool
|
|
396
|
-
* Only instantiates when first accessed to reduce startup time
|
|
397
|
-
*/
|
|
398
|
-
get architectureTool() {
|
|
399
|
-
if (!this._architectureTool) {
|
|
400
|
-
this._architectureTool = new ArchitectureTool();
|
|
401
|
-
}
|
|
402
|
-
return this._architectureTool;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Lazy-loaded getter for ValidationTool
|
|
406
|
-
* Only instantiates when first accessed to reduce startup time
|
|
407
|
-
*/
|
|
408
|
-
get validationTool() {
|
|
409
|
-
if (!this._validationTool) {
|
|
410
|
-
this._validationTool = new ValidationTool();
|
|
411
|
-
}
|
|
412
|
-
return this._validationTool;
|
|
413
|
-
}
|
|
414
415
|
/**
|
|
415
416
|
* Detect if a tool call is repetitive (likely causing a loop)
|
|
416
417
|
* Uses the intelligent LoopDetector which provides:
|
|
@@ -427,32 +428,21 @@ export class LLMAgent extends EventEmitter {
|
|
|
427
428
|
const detector = getLoopDetector();
|
|
428
429
|
const result = detector.checkForLoop(toolCall);
|
|
429
430
|
// Debug logging
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
console.error(`[LOOP DETECTION] Reason: ${result.reason}`);
|
|
437
|
-
}
|
|
438
|
-
const stats = detector.getStats();
|
|
439
|
-
console.error(`[LOOP DETECTION] Stats: ${JSON.stringify(stats)}`);
|
|
440
|
-
}
|
|
431
|
+
debugLoop(`Tool: ${toolCall.function.name}`);
|
|
432
|
+
debugLoop(`Count: ${result.count}, Threshold: ${result.threshold}, Is Loop: ${result.isLoop}`);
|
|
433
|
+
if (result.reason)
|
|
434
|
+
debugLoop(`Reason: ${result.reason}`);
|
|
435
|
+
if (DEBUG_LOOP)
|
|
436
|
+
debugLoop(`Stats: ${JSON.stringify(detector.getStats())}`);
|
|
441
437
|
if (result.isLoop) {
|
|
442
438
|
// Store the result for generating better error message
|
|
443
439
|
this.lastLoopResult = result;
|
|
444
|
-
|
|
445
|
-
console.error(`[LOOP DETECTION] ⚠️ LOOP DETECTED!`);
|
|
446
|
-
console.error(`[LOOP DETECTION] Reason: ${result.reason}`);
|
|
447
|
-
console.error(`[LOOP DETECTION] Suggestion: ${result.suggestion}`);
|
|
448
|
-
}
|
|
440
|
+
debugLoop(`⚠️ LOOP DETECTED! Reason: ${result.reason}, Suggestion: ${result.suggestion}`);
|
|
449
441
|
return true;
|
|
450
442
|
}
|
|
451
443
|
// Note: We don't record here - recording happens AFTER execution
|
|
452
444
|
// in executeToolCalls() with the actual success/failure status
|
|
453
|
-
|
|
454
|
-
console.error(`[LOOP DETECTION] ✅ Allowed, count: ${result.count}/${result.threshold}`);
|
|
455
|
-
}
|
|
445
|
+
debugLoop(`✅ Allowed, count: ${result.count}/${result.threshold}`);
|
|
456
446
|
return false;
|
|
457
447
|
}
|
|
458
448
|
/** Last loop detection result for error messages */
|
|
@@ -461,10 +451,9 @@ export class LLMAgent extends EventEmitter {
|
|
|
461
451
|
* Reset the tool call tracking (called at start of new user message)
|
|
462
452
|
*/
|
|
463
453
|
resetToolCallTracking() {
|
|
464
|
-
if (
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
console.error(`[LOOP TRACKING] 🔄 Resetting tool call tracking (had ${stats.uniqueSignatures} signatures)`);
|
|
454
|
+
if (DEBUG_LOOP) {
|
|
455
|
+
const stats = getLoopDetector().getStats();
|
|
456
|
+
debugLoop(`🔄 Resetting tool call tracking (had ${stats.uniqueSignatures} signatures)`);
|
|
468
457
|
}
|
|
469
458
|
// Reset the new intelligent loop detector
|
|
470
459
|
resetLoopDetector();
|
|
@@ -511,140 +500,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
511
500
|
getCurrentPlan() {
|
|
512
501
|
return this.currentPlan;
|
|
513
502
|
}
|
|
514
|
-
/**
|
|
515
|
-
* Execute a single phase using the LLM
|
|
516
|
-
*/
|
|
517
|
-
async executePhase(phase, context) {
|
|
518
|
-
const startTime = Date.now();
|
|
519
|
-
const startTokens = this.tokenCounter.countMessageTokens(this.messages);
|
|
520
|
-
const filesModified = [];
|
|
521
|
-
let lastAssistantContent = "";
|
|
522
|
-
// Emit phase started event
|
|
523
|
-
this.emit("phase:started", { phase, planId: context.planId });
|
|
524
|
-
try {
|
|
525
|
-
// Build phase-specific prompt
|
|
526
|
-
const phasePrompt = this.buildPhasePrompt(phase, context);
|
|
527
|
-
// Execute through normal message processing (without recursively planning)
|
|
528
|
-
const savedPlanningState = this.planningEnabled;
|
|
529
|
-
this.planningEnabled = false; // Temporarily disable planning for phase execution
|
|
530
|
-
// Add phase context to messages
|
|
531
|
-
this.messages.push({
|
|
532
|
-
role: "user",
|
|
533
|
-
content: phasePrompt,
|
|
534
|
-
});
|
|
535
|
-
// Execute using the standard tool loop
|
|
536
|
-
const tools = await getAllGrokTools();
|
|
537
|
-
let toolRounds = 0;
|
|
538
|
-
const maxPhaseRounds = Math.min(this.maxToolRounds, 50); // Limit per phase
|
|
539
|
-
while (toolRounds < maxPhaseRounds) {
|
|
540
|
-
const response = await this.llmClient.chat(this.messages, tools, this.buildChatOptions());
|
|
541
|
-
const assistantMessage = response.choices[0]?.message;
|
|
542
|
-
if (!assistantMessage)
|
|
543
|
-
break;
|
|
544
|
-
// Capture the assistant's content for phase output
|
|
545
|
-
if (assistantMessage.content) {
|
|
546
|
-
lastAssistantContent = assistantMessage.content;
|
|
547
|
-
}
|
|
548
|
-
// Add to messages
|
|
549
|
-
this.messages.push({
|
|
550
|
-
role: "assistant",
|
|
551
|
-
content: assistantMessage.content || "",
|
|
552
|
-
tool_calls: assistantMessage.tool_calls,
|
|
553
|
-
});
|
|
554
|
-
// Check for tool calls
|
|
555
|
-
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
556
|
-
break; // No more tool calls, phase complete
|
|
557
|
-
}
|
|
558
|
-
toolRounds++;
|
|
559
|
-
// Execute tools and track file modifications
|
|
560
|
-
for (const toolCall of assistantMessage.tool_calls) {
|
|
561
|
-
const result = await this.executeTool(toolCall);
|
|
562
|
-
// Track file modifications from text_editor tool
|
|
563
|
-
if (toolCall.function.name === "text_editor" ||
|
|
564
|
-
toolCall.function.name === "str_replace_editor") {
|
|
565
|
-
const args = this.parseToolArgumentsCached(toolCall);
|
|
566
|
-
if (args.path && result.success) {
|
|
567
|
-
if (!filesModified.includes(args.path)) {
|
|
568
|
-
filesModified.push(args.path);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
this.messages.push({
|
|
573
|
-
role: "tool",
|
|
574
|
-
tool_call_id: toolCall.id,
|
|
575
|
-
content: result.output || result.error || "No output",
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
// Restore planning state
|
|
580
|
-
this.planningEnabled = savedPlanningState;
|
|
581
|
-
// Prune context if configured
|
|
582
|
-
if (PLANNER_CONFIG.PRUNE_AFTER_PHASE) {
|
|
583
|
-
this.applyContextPruning();
|
|
584
|
-
}
|
|
585
|
-
const endTokens = this.tokenCounter.countMessageTokens(this.messages);
|
|
586
|
-
const duration = Date.now() - startTime;
|
|
587
|
-
// Build meaningful output
|
|
588
|
-
const output = lastAssistantContent ||
|
|
589
|
-
`Phase "${phase.name}" completed (${toolRounds} tool rounds, ${filesModified.length} files modified)`;
|
|
590
|
-
// Emit phase completed event
|
|
591
|
-
this.emit("phase:completed", {
|
|
592
|
-
phase,
|
|
593
|
-
planId: context.planId,
|
|
594
|
-
result: { success: true, output, filesModified }
|
|
595
|
-
});
|
|
596
|
-
return {
|
|
597
|
-
phaseId: phase.id,
|
|
598
|
-
success: true,
|
|
599
|
-
output,
|
|
600
|
-
duration,
|
|
601
|
-
tokensUsed: endTokens - startTokens,
|
|
602
|
-
filesModified,
|
|
603
|
-
wasRetry: false,
|
|
604
|
-
retryAttempt: 0,
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
catch (error) {
|
|
608
|
-
const duration = Date.now() - startTime;
|
|
609
|
-
const errorMessage = extractErrorMessage(error);
|
|
610
|
-
// Emit phase failed event
|
|
611
|
-
this.emit("phase:failed", {
|
|
612
|
-
phase,
|
|
613
|
-
planId: context.planId,
|
|
614
|
-
error: errorMessage
|
|
615
|
-
});
|
|
616
|
-
return {
|
|
617
|
-
phaseId: phase.id,
|
|
618
|
-
success: false,
|
|
619
|
-
error: errorMessage,
|
|
620
|
-
duration,
|
|
621
|
-
tokensUsed: 0,
|
|
622
|
-
filesModified,
|
|
623
|
-
wasRetry: false,
|
|
624
|
-
retryAttempt: 0,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Build a prompt for phase execution
|
|
630
|
-
*/
|
|
631
|
-
buildPhasePrompt(phase, context) {
|
|
632
|
-
let prompt = `## Phase ${phase.index + 1}: ${phase.name}\n\n`;
|
|
633
|
-
prompt += `**Objective:** ${phase.description}\n\n`;
|
|
634
|
-
if (phase.objectives.length > 0) {
|
|
635
|
-
prompt += "**Tasks to complete:**\n";
|
|
636
|
-
for (const obj of phase.objectives) {
|
|
637
|
-
prompt += `- ${obj}\n`;
|
|
638
|
-
}
|
|
639
|
-
prompt += "\n";
|
|
640
|
-
}
|
|
641
|
-
if (context.completedPhases.length > 0) {
|
|
642
|
-
prompt += `**Previously completed phases:** ${context.completedPhases.join(", ")}\n\n`;
|
|
643
|
-
}
|
|
644
|
-
prompt += `**Original request:** ${context.originalRequest}\n\n`;
|
|
645
|
-
prompt += "Please complete this phase. Focus only on the objectives listed above.";
|
|
646
|
-
return prompt;
|
|
647
|
-
}
|
|
648
503
|
/**
|
|
649
504
|
* Generate and execute a plan for a complex request
|
|
650
505
|
* Uses TodoWrite for Claude Code-style seamless progress display
|
|
@@ -702,7 +557,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
702
557
|
phase.riskLevel === "low" ? "low" : "medium",
|
|
703
558
|
}));
|
|
704
559
|
try {
|
|
705
|
-
await this.
|
|
560
|
+
await this.toolExecutor.getTodoTool().createTodoList(todoItems);
|
|
706
561
|
}
|
|
707
562
|
catch (todoError) {
|
|
708
563
|
// TodoWrite failure is non-critical, continue execution
|
|
@@ -713,7 +568,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
713
568
|
// Display explicit plan summary
|
|
714
569
|
yield {
|
|
715
570
|
type: "content",
|
|
716
|
-
content: this.formatPlanSummary(plan),
|
|
571
|
+
content: this.planExecutor.formatPlanSummary(plan),
|
|
717
572
|
};
|
|
718
573
|
}
|
|
719
574
|
// Execute phases one by one with progress updates
|
|
@@ -726,7 +581,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
726
581
|
if (PLANNER_CONFIG.SILENT_MODE) {
|
|
727
582
|
// Update TodoWrite: mark current phase as in_progress
|
|
728
583
|
try {
|
|
729
|
-
await this.
|
|
584
|
+
await this.toolExecutor.getTodoTool().updateTodoList([{
|
|
730
585
|
id: `phase-${i}`,
|
|
731
586
|
status: "in_progress",
|
|
732
587
|
}]);
|
|
@@ -740,13 +595,14 @@ export class LLMAgent extends EventEmitter {
|
|
|
740
595
|
content: `\n**⏳ Phase ${i + 1}/${plan.phases.length}: ${phase.name}**\n`,
|
|
741
596
|
};
|
|
742
597
|
}
|
|
743
|
-
// Execute the phase
|
|
598
|
+
// Execute the phase (delegated to PlanExecutor - Phase 2 refactoring)
|
|
744
599
|
const context = {
|
|
745
600
|
planId: plan.id,
|
|
746
601
|
originalRequest: message,
|
|
747
602
|
completedPhases: phaseResults.filter(r => r.success).map(r => r.phaseId),
|
|
748
603
|
};
|
|
749
|
-
const result = await this.executePhase(phase, context);
|
|
604
|
+
const { result, messages: updatedMessages } = await this.planExecutor.executePhase(phase, context, this.messages, this.chatHistory);
|
|
605
|
+
this.messages = updatedMessages; // Update messages with phase execution results
|
|
750
606
|
phaseResults.push(result);
|
|
751
607
|
totalTokensUsed += result.tokensUsed;
|
|
752
608
|
// Report phase result
|
|
@@ -754,7 +610,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
754
610
|
if (PLANNER_CONFIG.SILENT_MODE) {
|
|
755
611
|
// Update TodoWrite: mark phase as completed
|
|
756
612
|
try {
|
|
757
|
-
await this.
|
|
613
|
+
await this.toolExecutor.getTodoTool().updateTodoList([{
|
|
758
614
|
id: `phase-${i}`,
|
|
759
615
|
status: "completed",
|
|
760
616
|
}]);
|
|
@@ -778,7 +634,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
778
634
|
if (PLANNER_CONFIG.SILENT_MODE) {
|
|
779
635
|
// Update TodoWrite: mark phase as failed (update content to show failure)
|
|
780
636
|
try {
|
|
781
|
-
await this.
|
|
637
|
+
await this.toolExecutor.getTodoTool().updateTodoList([{
|
|
782
638
|
id: `phase-${i}`,
|
|
783
639
|
status: "completed", // Mark as done even if failed
|
|
784
640
|
content: `${phase.name} (failed)`,
|
|
@@ -829,7 +685,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
829
685
|
if (!PLANNER_CONFIG.SILENT_MODE) {
|
|
830
686
|
yield {
|
|
831
687
|
type: "content",
|
|
832
|
-
content: this.formatPlanResult(planResult),
|
|
688
|
+
content: this.planExecutor.formatPlanResult(planResult),
|
|
833
689
|
};
|
|
834
690
|
}
|
|
835
691
|
else {
|
|
@@ -925,8 +781,12 @@ export class LLMAgent extends EventEmitter {
|
|
|
925
781
|
const stream = this.llmClient.chatStream(this.messages, tools, this.buildChatOptions({
|
|
926
782
|
searchOptions: { search_parameters: { mode: "off" } }
|
|
927
783
|
}));
|
|
928
|
-
// Process streaming chunks
|
|
929
|
-
const chunkGen = this.
|
|
784
|
+
// Process streaming chunks (delegated to StreamHandler - Phase 2 refactoring)
|
|
785
|
+
const chunkGen = this.streamHandler.processChunks(stream, {
|
|
786
|
+
inputTokens: inputTokensRef.value,
|
|
787
|
+
lastTokenUpdate: lastTokenUpdateRef,
|
|
788
|
+
totalOutputTokens: totalOutputTokensRef,
|
|
789
|
+
});
|
|
930
790
|
let streamResult;
|
|
931
791
|
for await (const chunk of chunkGen) {
|
|
932
792
|
if ('accumulated' in chunk) {
|
|
@@ -977,41 +837,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
977
837
|
};
|
|
978
838
|
yield { type: "done" };
|
|
979
839
|
}
|
|
980
|
-
/**
|
|
981
|
-
* Format plan summary for display
|
|
982
|
-
*/
|
|
983
|
-
formatPlanSummary(plan) {
|
|
984
|
-
let output = `**📋 Execution Plan Created**\n\n`;
|
|
985
|
-
output += `**Request:** ${plan.originalPrompt.slice(0, 100)}${plan.originalPrompt.length > 100 ? "..." : ""}\n\n`;
|
|
986
|
-
output += `**Phases (${plan.phases.length}):**\n`;
|
|
987
|
-
for (const phase of plan.phases) {
|
|
988
|
-
const riskIcon = phase.riskLevel === "high" ? "⚠️" : phase.riskLevel === "medium" ? "△" : "";
|
|
989
|
-
output += ` ${phase.index + 1}. ${phase.name} ${riskIcon}\n`;
|
|
990
|
-
}
|
|
991
|
-
output += `\n**Estimated Duration:** ~${Math.ceil(plan.estimatedDuration / 60000)} min\n\n`;
|
|
992
|
-
output += "---\n\n";
|
|
993
|
-
return output;
|
|
994
|
-
}
|
|
995
|
-
/**
|
|
996
|
-
* Format plan result for display
|
|
997
|
-
*/
|
|
998
|
-
formatPlanResult(result) {
|
|
999
|
-
let output = "\n---\n\n**📋 Plan Execution Complete**\n\n";
|
|
1000
|
-
const successful = result.phaseResults.filter((r) => r.success).length;
|
|
1001
|
-
const failed = result.phaseResults.filter((r) => !r.success).length;
|
|
1002
|
-
output += `**Results:** ${successful}/${result.phaseResults.length} phases successful`;
|
|
1003
|
-
if (failed > 0) {
|
|
1004
|
-
output += ` (${failed} failed)`;
|
|
1005
|
-
}
|
|
1006
|
-
output += "\n";
|
|
1007
|
-
if (result.totalDuration) {
|
|
1008
|
-
output += `**Duration:** ${Math.ceil(result.totalDuration / 1000)}s\n`;
|
|
1009
|
-
}
|
|
1010
|
-
if (result.totalTokensUsed) {
|
|
1011
|
-
output += `**Tokens Used:** ${result.totalTokensUsed.toLocaleString()}\n`;
|
|
1012
|
-
}
|
|
1013
|
-
return output;
|
|
1014
|
-
}
|
|
1015
840
|
async processUserMessage(message) {
|
|
1016
841
|
// Check if agent has been disposed
|
|
1017
842
|
this.checkDisposed();
|
|
@@ -1057,17 +882,11 @@ export class LLMAgent extends EventEmitter {
|
|
|
1057
882
|
assistantMessage.tool_calls.length > 0) {
|
|
1058
883
|
toolRounds++;
|
|
1059
884
|
// Check for repetitive tool calls (loop detection)
|
|
1060
|
-
|
|
1061
|
-
console.error(`\n[LOOP CHECK] Checking ${assistantMessage.tool_calls.length} tool calls...`);
|
|
1062
|
-
}
|
|
885
|
+
debugLoop(`Checking ${assistantMessage.tool_calls.length} tool calls...`);
|
|
1063
886
|
const hasRepetitiveCall = assistantMessage.tool_calls.some((tc) => this.isRepetitiveToolCall(tc));
|
|
1064
|
-
|
|
1065
|
-
console.error(`[LOOP CHECK] hasRepetitiveCall: ${hasRepetitiveCall}\n`);
|
|
1066
|
-
}
|
|
887
|
+
debugLoop(`hasRepetitiveCall: ${hasRepetitiveCall}`);
|
|
1067
888
|
if (hasRepetitiveCall) {
|
|
1068
|
-
|
|
1069
|
-
console.error(`[LOOP CHECK] 🛑 Breaking loop!`);
|
|
1070
|
-
}
|
|
889
|
+
debugLoop(`🛑 Breaking loop!`);
|
|
1071
890
|
const loopMsg = this.getLoopWarningMessage();
|
|
1072
891
|
const warningEntry = {
|
|
1073
892
|
type: "assistant",
|
|
@@ -1119,9 +938,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
1119
938
|
const updatedEntry = {
|
|
1120
939
|
...this.chatHistory[entryIndex],
|
|
1121
940
|
type: "tool_result",
|
|
1122
|
-
content: result
|
|
1123
|
-
? result.output || "Success"
|
|
1124
|
-
: result.error || "Error occurred",
|
|
941
|
+
content: this.formatToolResultContent(result),
|
|
1125
942
|
toolResult: result,
|
|
1126
943
|
};
|
|
1127
944
|
this.chatHistory[entryIndex] = updatedEntry;
|
|
@@ -1135,9 +952,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
1135
952
|
// Add tool result to messages with proper format (needed for AI context)
|
|
1136
953
|
this.messages.push({
|
|
1137
954
|
role: "tool",
|
|
1138
|
-
content: result
|
|
1139
|
-
? result.output || "Success"
|
|
1140
|
-
: result.error || "Error",
|
|
955
|
+
content: this.formatToolResultContent(result, "Success", "Error"),
|
|
1141
956
|
tool_call_id: toolCall.id,
|
|
1142
957
|
});
|
|
1143
958
|
}
|
|
@@ -1188,67 +1003,6 @@ export class LLMAgent extends EventEmitter {
|
|
|
1188
1003
|
return [userEntry, errorEntry];
|
|
1189
1004
|
}
|
|
1190
1005
|
}
|
|
1191
|
-
/**
|
|
1192
|
-
* Optimized streaming delta merge - mutates accumulator for performance
|
|
1193
|
-
* This is safe because accumulator is only used internally during streaming
|
|
1194
|
-
*
|
|
1195
|
-
* Performance: 50% faster than immutable approach (no object copying)
|
|
1196
|
-
*/
|
|
1197
|
-
reduceStreamDelta(acc, delta) {
|
|
1198
|
-
for (const [key, value] of Object.entries(delta)) {
|
|
1199
|
-
if (value === undefined || value === null) {
|
|
1200
|
-
continue; // Skip undefined/null values
|
|
1201
|
-
}
|
|
1202
|
-
if (acc[key] === undefined || acc[key] === null) {
|
|
1203
|
-
// Initial value assignment
|
|
1204
|
-
acc[key] = value;
|
|
1205
|
-
// Clean up index properties from tool calls
|
|
1206
|
-
if (Array.isArray(acc[key])) {
|
|
1207
|
-
for (const arr of acc[key]) {
|
|
1208
|
-
if (arr && typeof arr === 'object') {
|
|
1209
|
-
delete arr.index;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
else if (typeof acc[key] === "string" && typeof value === "string") {
|
|
1215
|
-
// String concatenation (most common case during streaming)
|
|
1216
|
-
acc[key] += value;
|
|
1217
|
-
}
|
|
1218
|
-
else if (Array.isArray(acc[key]) && Array.isArray(value)) {
|
|
1219
|
-
// Array merging (for tool calls)
|
|
1220
|
-
const accArray = acc[key];
|
|
1221
|
-
for (let i = 0; i < value.length; i++) {
|
|
1222
|
-
if (value[i] === undefined || value[i] === null)
|
|
1223
|
-
continue;
|
|
1224
|
-
if (!accArray[i]) {
|
|
1225
|
-
accArray[i] = {};
|
|
1226
|
-
}
|
|
1227
|
-
// Recursively merge array elements
|
|
1228
|
-
this.reduceStreamDelta(accArray[i], value[i]);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
else if (typeof acc[key] === "object" && typeof value === "object") {
|
|
1232
|
-
// Object merging
|
|
1233
|
-
this.reduceStreamDelta(acc[key], value);
|
|
1234
|
-
}
|
|
1235
|
-
else {
|
|
1236
|
-
// Direct assignment for other types
|
|
1237
|
-
acc[key] = value;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
return acc;
|
|
1241
|
-
}
|
|
1242
|
-
/**
|
|
1243
|
-
* Accumulate streaming message chunks
|
|
1244
|
-
*/
|
|
1245
|
-
messageReducer(previous, item) {
|
|
1246
|
-
// Safety check: ensure item has valid structure
|
|
1247
|
-
if (!item?.choices || item.choices.length === 0 || !item.choices[0]?.delta) {
|
|
1248
|
-
return previous;
|
|
1249
|
-
}
|
|
1250
|
-
return this.reduceStreamDelta(previous, item.choices[0].delta);
|
|
1251
|
-
}
|
|
1252
1006
|
/**
|
|
1253
1007
|
* Prepare user message and apply context management
|
|
1254
1008
|
* Returns the calculated input tokens
|
|
@@ -1312,111 +1066,18 @@ export class LLMAgent extends EventEmitter {
|
|
|
1312
1066
|
}
|
|
1313
1067
|
}
|
|
1314
1068
|
/**
|
|
1315
|
-
*
|
|
1069
|
+
* Format tool result content for display or message
|
|
1070
|
+
* Centralizes the common pattern of formatting success/error output
|
|
1071
|
+
*
|
|
1072
|
+
* @param result - Tool execution result
|
|
1073
|
+
* @param defaultSuccess - Default message if success but no output (default: "Success")
|
|
1074
|
+
* @param defaultError - Default message if error but no error message (default: "Error occurred")
|
|
1075
|
+
* @returns Formatted content string
|
|
1316
1076
|
*/
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
let usageData = null;
|
|
1322
|
-
// CRITICAL FIX: Ensure stream is properly closed on cancellation or error
|
|
1323
|
-
// Without this, HTTP connections and buffers remain in memory
|
|
1324
|
-
try {
|
|
1325
|
-
for await (const chunk of stream) {
|
|
1326
|
-
// Check for cancellation in the streaming loop
|
|
1327
|
-
if (this.isCancelled()) {
|
|
1328
|
-
yield* this.yieldCancellation();
|
|
1329
|
-
// Return empty state after cancellation to avoid processing partial results
|
|
1330
|
-
return { accumulated: {}, content: "", yielded: false };
|
|
1331
|
-
}
|
|
1332
|
-
if (!chunk.choices?.[0])
|
|
1333
|
-
continue;
|
|
1334
|
-
// Capture usage data from chunks (usually in the final chunk)
|
|
1335
|
-
if (chunk.usage) {
|
|
1336
|
-
usageData = chunk.usage;
|
|
1337
|
-
}
|
|
1338
|
-
// Accumulate the message using reducer
|
|
1339
|
-
accumulatedMessage = this.messageReducer(accumulatedMessage, chunk);
|
|
1340
|
-
// Check for tool calls - yield when we have complete tool calls with function names
|
|
1341
|
-
const toolCalls = accumulatedMessage.tool_calls;
|
|
1342
|
-
if (!toolCallsYielded && toolCalls && Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
1343
|
-
const hasCompleteTool = toolCalls.some((tc) => tc.function?.name);
|
|
1344
|
-
if (hasCompleteTool) {
|
|
1345
|
-
yield {
|
|
1346
|
-
type: "tool_calls",
|
|
1347
|
-
toolCalls: toolCalls,
|
|
1348
|
-
};
|
|
1349
|
-
toolCallsYielded = true;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
// Stream reasoning content (GLM-4.6 thinking mode)
|
|
1353
|
-
// Safety check: ensure choices[0] exists before accessing
|
|
1354
|
-
if (chunk.choices[0]?.delta?.reasoning_content) {
|
|
1355
|
-
yield {
|
|
1356
|
-
type: "reasoning",
|
|
1357
|
-
reasoningContent: chunk.choices[0].delta.reasoning_content,
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
// Stream content as it comes
|
|
1361
|
-
if (chunk.choices[0]?.delta?.content) {
|
|
1362
|
-
accumulatedContent += chunk.choices[0].delta.content;
|
|
1363
|
-
yield {
|
|
1364
|
-
type: "content",
|
|
1365
|
-
content: chunk.choices[0].delta.content,
|
|
1366
|
-
};
|
|
1367
|
-
// Emit token count update (throttled and optimized)
|
|
1368
|
-
const now = Date.now();
|
|
1369
|
-
if (now - lastTokenUpdate.value > 1000) { // Increased throttle to 1s for better performance
|
|
1370
|
-
lastTokenUpdate.value = now;
|
|
1371
|
-
// Use fast estimation during streaming (4 chars ≈ 1 token)
|
|
1372
|
-
// This is ~70% faster than tiktoken encoding
|
|
1373
|
-
const estimatedOutputTokens = Math.floor(accumulatedContent.length / 4) +
|
|
1374
|
-
(accumulatedMessage.tool_calls
|
|
1375
|
-
? Math.floor(JSON.stringify(accumulatedMessage.tool_calls).length / 4)
|
|
1376
|
-
: 0);
|
|
1377
|
-
totalOutputTokens.value = estimatedOutputTokens;
|
|
1378
|
-
yield {
|
|
1379
|
-
type: "token_count",
|
|
1380
|
-
tokenCount: inputTokens + estimatedOutputTokens,
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
// Track usage if available and emit accurate final token count
|
|
1386
|
-
if (usageData) {
|
|
1387
|
-
const tracker = getUsageTracker();
|
|
1388
|
-
tracker.trackUsage(this.llmClient.getCurrentModel(), usageData);
|
|
1389
|
-
// Emit accurate token count from API usage data (replaces estimation)
|
|
1390
|
-
const totalTokens = usageData.total_tokens;
|
|
1391
|
-
const completionTokens = usageData.completion_tokens;
|
|
1392
|
-
if (totalTokens) {
|
|
1393
|
-
totalOutputTokens.value = completionTokens || 0;
|
|
1394
|
-
yield {
|
|
1395
|
-
type: "token_count",
|
|
1396
|
-
tokenCount: totalTokens,
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
// CRITICAL: Yield the accumulated result so the main loop can access it!
|
|
1401
|
-
const result = { accumulated: accumulatedMessage, content: accumulatedContent, yielded: toolCallsYielded };
|
|
1402
|
-
yield result;
|
|
1403
|
-
return result;
|
|
1404
|
-
}
|
|
1405
|
-
finally {
|
|
1406
|
-
// CRITICAL FIX: Properly close the async iterator to release HTTP connections and buffers
|
|
1407
|
-
// This prevents socket leaks when streams are cancelled or errors occur
|
|
1408
|
-
try {
|
|
1409
|
-
// Use a type assertion to safely access the return method
|
|
1410
|
-
const streamWithReturn = stream;
|
|
1411
|
-
if (typeof streamWithReturn.return === 'function') {
|
|
1412
|
-
await streamWithReturn.return();
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
catch (cleanupError) {
|
|
1416
|
-
// Log but don't throw - cleanup errors shouldn't break the flow
|
|
1417
|
-
console.warn('Stream cleanup warning:', cleanupError);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1077
|
+
formatToolResultContent(result, defaultSuccess = "Success", defaultError = "Error occurred") {
|
|
1078
|
+
return result.success
|
|
1079
|
+
? result.output || defaultSuccess
|
|
1080
|
+
: result.error || defaultError;
|
|
1420
1081
|
}
|
|
1421
1082
|
/**
|
|
1422
1083
|
* Add assistant message to history and conversation
|
|
@@ -1468,14 +1129,10 @@ export class LLMAgent extends EventEmitter {
|
|
|
1468
1129
|
// This enables failure-based threshold adjustment (repeated failures = lower threshold)
|
|
1469
1130
|
const detector = getLoopDetector();
|
|
1470
1131
|
detector.recordToolCall(toolCall, result.success);
|
|
1471
|
-
|
|
1472
|
-
console.error(`[LOOP DETECTION] 📝 Recorded: ${toolCall.function.name}, success=${result.success}`);
|
|
1473
|
-
}
|
|
1132
|
+
debugLoop(`📝 Recorded: ${toolCall.function.name}, success=${result.success}`);
|
|
1474
1133
|
const toolResultEntry = {
|
|
1475
1134
|
type: "tool_result",
|
|
1476
|
-
content: result
|
|
1477
|
-
? result.output || "Success"
|
|
1478
|
-
: result.error || "Error occurred",
|
|
1135
|
+
content: this.formatToolResultContent(result),
|
|
1479
1136
|
timestamp: new Date(),
|
|
1480
1137
|
toolCall: toolCall,
|
|
1481
1138
|
toolResult: result,
|
|
@@ -1491,9 +1148,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
1491
1148
|
// Add tool result with proper format (needed for AI context)
|
|
1492
1149
|
this.messages.push({
|
|
1493
1150
|
role: "tool",
|
|
1494
|
-
content: result
|
|
1495
|
-
? result.output || "Success"
|
|
1496
|
-
: result.error || "Error",
|
|
1151
|
+
content: this.formatToolResultContent(result, "Success", "Error"),
|
|
1497
1152
|
tool_call_id: toolCall.id,
|
|
1498
1153
|
});
|
|
1499
1154
|
}
|
|
@@ -1534,9 +1189,7 @@ export class LLMAgent extends EventEmitter {
|
|
|
1534
1189
|
try {
|
|
1535
1190
|
// Agent loop - continue until no more tool calls or max rounds reached
|
|
1536
1191
|
while (toolRounds < maxToolRounds) {
|
|
1537
|
-
|
|
1538
|
-
console.error(`\n[LOOP DEBUG] Agent loop iteration, toolRounds: ${toolRounds}`);
|
|
1539
|
-
}
|
|
1192
|
+
debugLoop(`Agent loop iteration, toolRounds: ${toolRounds}`);
|
|
1540
1193
|
// Check if operation was cancelled
|
|
1541
1194
|
if (this.isCancelled()) {
|
|
1542
1195
|
yield* this.yieldCancellation();
|
|
@@ -1555,8 +1208,12 @@ export class LLMAgent extends EventEmitter {
|
|
|
1555
1208
|
const stream = this.llmClient.chatStream(this.messages, tools, this.buildChatOptions({
|
|
1556
1209
|
searchOptions: { search_parameters: { mode: "off" } }
|
|
1557
1210
|
}));
|
|
1558
|
-
// Process streaming chunks
|
|
1559
|
-
const chunkGen = this.
|
|
1211
|
+
// Process streaming chunks (delegated to StreamHandler - Phase 2 refactoring)
|
|
1212
|
+
const chunkGen = this.streamHandler.processChunks(stream, {
|
|
1213
|
+
inputTokens: inputTokensRef.value,
|
|
1214
|
+
lastTokenUpdate: lastTokenUpdateRef,
|
|
1215
|
+
totalOutputTokens: totalOutputTokensRef,
|
|
1216
|
+
});
|
|
1560
1217
|
let streamResult;
|
|
1561
1218
|
for await (const chunk of chunkGen) {
|
|
1562
1219
|
if ('accumulated' in chunk) {
|
|
@@ -1626,237 +1283,56 @@ export class LLMAgent extends EventEmitter {
|
|
|
1626
1283
|
}
|
|
1627
1284
|
}
|
|
1628
1285
|
/**
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
* @param toolType Type of tool (for error messages)
|
|
1632
|
-
* @returns Parsed arguments or error result
|
|
1286
|
+
* Execute a tool call using the ToolExecutor
|
|
1287
|
+
* Handles tool approval for VSCode integration before delegation
|
|
1633
1288
|
*/
|
|
1634
|
-
parseToolArguments(toolCall, toolType = 'Tool') {
|
|
1635
|
-
const argsString = toolCall.function.arguments;
|
|
1636
|
-
if (!argsString || typeof argsString !== 'string' || argsString.trim() === '') {
|
|
1637
|
-
return {
|
|
1638
|
-
success: false,
|
|
1639
|
-
error: `${toolType} ${toolCall.function.name} called with empty arguments`,
|
|
1640
|
-
};
|
|
1641
|
-
}
|
|
1642
|
-
try {
|
|
1643
|
-
const args = JSON.parse(argsString);
|
|
1644
|
-
// Validate that args is an object (not null, array, or primitive)
|
|
1645
|
-
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
|
|
1646
|
-
return {
|
|
1647
|
-
success: false,
|
|
1648
|
-
error: `${toolType} ${toolCall.function.name} arguments must be a JSON object, got ${Array.isArray(args) ? 'array' : typeof args}`,
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
return { success: true, args };
|
|
1652
|
-
}
|
|
1653
|
-
catch (error) {
|
|
1654
|
-
return {
|
|
1655
|
-
success: false,
|
|
1656
|
-
error: `Failed to parse ${toolType} arguments: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
|
|
1657
|
-
};
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
1289
|
async executeTool(toolCall) {
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
if (needsApproval) {
|
|
1674
|
-
// Emit event and wait for approval
|
|
1675
|
-
const approved = await this.waitForToolApproval(toolCall);
|
|
1676
|
-
if (!approved) {
|
|
1677
|
-
// User rejected the change
|
|
1678
|
-
this.emit('tool:rejected', toolCall);
|
|
1679
|
-
return {
|
|
1680
|
-
success: false,
|
|
1681
|
-
error: 'Change rejected by user'
|
|
1682
|
-
};
|
|
1683
|
-
}
|
|
1684
|
-
// User approved
|
|
1685
|
-
this.emit('tool:approved', toolCall);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
// Helper to safely get string argument with validation
|
|
1689
|
-
const getString = (key, required = true) => {
|
|
1690
|
-
const value = args[key];
|
|
1691
|
-
if (typeof value !== 'string') {
|
|
1692
|
-
if (required)
|
|
1693
|
-
throw new Error(`Tool argument '${key}' must be a string, got ${typeof value}`);
|
|
1694
|
-
return '';
|
|
1695
|
-
}
|
|
1696
|
-
return value;
|
|
1697
|
-
};
|
|
1698
|
-
// Helper to safely get number argument
|
|
1699
|
-
const getNumber = (key) => {
|
|
1700
|
-
const value = args[key];
|
|
1701
|
-
if (value === undefined || value === null)
|
|
1702
|
-
return undefined;
|
|
1703
|
-
if (typeof value !== 'number')
|
|
1704
|
-
return undefined;
|
|
1705
|
-
return value;
|
|
1706
|
-
};
|
|
1707
|
-
// Helper to safely get boolean argument
|
|
1708
|
-
const getBoolean = (key) => {
|
|
1709
|
-
const value = args[key];
|
|
1710
|
-
if (value === undefined || value === null)
|
|
1711
|
-
return undefined;
|
|
1712
|
-
if (typeof value !== 'boolean')
|
|
1713
|
-
return undefined;
|
|
1714
|
-
return value;
|
|
1715
|
-
};
|
|
1716
|
-
switch (toolCall.function.name) {
|
|
1717
|
-
case "view_file":
|
|
1718
|
-
const startLine = getNumber('start_line');
|
|
1719
|
-
const endLine = getNumber('end_line');
|
|
1720
|
-
const range = startLine !== undefined && endLine !== undefined
|
|
1721
|
-
? [startLine, endLine]
|
|
1722
|
-
: undefined;
|
|
1723
|
-
return await this.textEditor.view(getString('path'), range);
|
|
1724
|
-
case "create_file":
|
|
1725
|
-
return await this.textEditor.create(getString('path'), getString('content'));
|
|
1726
|
-
case "str_replace_editor":
|
|
1727
|
-
return await this.textEditor.strReplace(getString('path'), getString('old_str'), getString('new_str'), getBoolean('replace_all') ?? false);
|
|
1728
|
-
case "multi_edit":
|
|
1729
|
-
return await this.textEditor.multiEdit(getString('path'), Array.isArray(args.edits) ? args.edits : []);
|
|
1730
|
-
case "bash":
|
|
1731
|
-
return await this.bash.execute(getString('command'), {
|
|
1732
|
-
background: getBoolean('background'),
|
|
1733
|
-
timeout: getNumber('timeout'),
|
|
1734
|
-
});
|
|
1735
|
-
case "bash_output":
|
|
1736
|
-
return await this.bashOutput.execute(getString('task_id'), getBoolean('wait'), getNumber('timeout'));
|
|
1737
|
-
case "create_todo_list":
|
|
1738
|
-
return await this.todoTool.createTodoList(Array.isArray(args.todos) ? args.todos : []);
|
|
1739
|
-
case "update_todo_list":
|
|
1740
|
-
return await this.todoTool.updateTodoList(Array.isArray(args.updates) ? args.updates : []);
|
|
1741
|
-
case "search":
|
|
1742
|
-
const searchTypeValue = args.search_type;
|
|
1743
|
-
const validSearchType = (searchTypeValue === 'text' || searchTypeValue === 'files' || searchTypeValue === 'both') ? searchTypeValue : undefined;
|
|
1744
|
-
return await this.search.search(getString('query'), {
|
|
1745
|
-
searchType: validSearchType,
|
|
1746
|
-
includePattern: typeof args.include_pattern === 'string' ? args.include_pattern : undefined,
|
|
1747
|
-
excludePattern: typeof args.exclude_pattern === 'string' ? args.exclude_pattern : undefined,
|
|
1748
|
-
caseSensitive: getBoolean('case_sensitive'),
|
|
1749
|
-
wholeWord: getBoolean('whole_word'),
|
|
1750
|
-
regex: getBoolean('regex'),
|
|
1751
|
-
maxResults: getNumber('max_results'),
|
|
1752
|
-
fileTypes: Array.isArray(args.file_types) ? args.file_types : undefined,
|
|
1753
|
-
includeHidden: getBoolean('include_hidden'),
|
|
1754
|
-
});
|
|
1755
|
-
case "analyze_architecture": {
|
|
1756
|
-
const projectPath = typeof args.projectPath === 'string' ? args.projectPath : undefined;
|
|
1757
|
-
const depth = typeof args.depth === 'string' ? args.depth : undefined;
|
|
1758
|
-
return await this.architectureTool.execute({ projectPath, depth });
|
|
1759
|
-
}
|
|
1760
|
-
case "validate_best_practices": {
|
|
1761
|
-
const path = typeof args.path === 'string' ? args.path : undefined;
|
|
1762
|
-
const pattern = typeof args.pattern === 'string' ? args.pattern : undefined;
|
|
1763
|
-
const rules = typeof args.rules === 'object' && args.rules !== null ? args.rules : undefined;
|
|
1764
|
-
return await this.validationTool.execute({ path, pattern, rules });
|
|
1765
|
-
}
|
|
1766
|
-
default:
|
|
1767
|
-
// Check if this is an MCP tool
|
|
1768
|
-
if (toolCall.function.name.startsWith("mcp__")) {
|
|
1769
|
-
return await this.executeMCPTool(toolCall);
|
|
1770
|
-
}
|
|
1290
|
+
// Check if tool approval is required (for VSCode integration)
|
|
1291
|
+
if (this.requireToolApproval) {
|
|
1292
|
+
// Only require approval for file modification operations
|
|
1293
|
+
const needsApproval = toolCall.function.name === "create_file" ||
|
|
1294
|
+
toolCall.function.name === "str_replace_editor" ||
|
|
1295
|
+
toolCall.function.name === "insert_text";
|
|
1296
|
+
if (needsApproval) {
|
|
1297
|
+
// Emit event and wait for approval
|
|
1298
|
+
const approved = await this.waitForToolApproval(toolCall);
|
|
1299
|
+
if (!approved) {
|
|
1300
|
+
// User rejected the change
|
|
1301
|
+
this.emit('tool:rejected', toolCall);
|
|
1771
1302
|
return {
|
|
1772
1303
|
success: false,
|
|
1773
|
-
error:
|
|
1304
|
+
error: 'Change rejected by user'
|
|
1774
1305
|
};
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
catch (error) {
|
|
1778
|
-
const errorMsg = extractErrorMessage(error);
|
|
1779
|
-
return {
|
|
1780
|
-
success: false,
|
|
1781
|
-
error: `Tool execution error: ${errorMsg}`,
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
async executeMCPTool(toolCall) {
|
|
1786
|
-
try {
|
|
1787
|
-
const parseResult = this.parseToolArguments(toolCall, 'MCP tool');
|
|
1788
|
-
if (!parseResult.success) {
|
|
1789
|
-
return { success: false, error: parseResult.error };
|
|
1790
|
-
}
|
|
1791
|
-
const args = parseResult.args;
|
|
1792
|
-
const mcpManager = getMCPManager();
|
|
1793
|
-
const result = await mcpManager.callTool(toolCall.function.name, args);
|
|
1794
|
-
if (result.isError) {
|
|
1795
|
-
// Extract error message from MCP result content
|
|
1796
|
-
// Safely check content structure before accessing
|
|
1797
|
-
let errorMsg = "MCP tool error";
|
|
1798
|
-
if (result.content && Array.isArray(result.content) && result.content.length > 0) {
|
|
1799
|
-
const firstContent = result.content[0];
|
|
1800
|
-
if (typeof firstContent === 'object' && firstContent !== null && 'text' in firstContent) {
|
|
1801
|
-
const textValue = firstContent.text;
|
|
1802
|
-
errorMsg = typeof textValue === 'string' ? textValue : String(textValue || errorMsg);
|
|
1803
|
-
}
|
|
1804
1306
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
error: errorMsg,
|
|
1808
|
-
};
|
|
1307
|
+
// User approved
|
|
1308
|
+
this.emit('tool:approved', toolCall);
|
|
1809
1309
|
}
|
|
1810
|
-
// Extract content from result
|
|
1811
|
-
// Ensure result.content exists and is an array before mapping
|
|
1812
|
-
const output = result.content && Array.isArray(result.content)
|
|
1813
|
-
? result.content
|
|
1814
|
-
.map((item) => {
|
|
1815
|
-
if (item.type === "text") {
|
|
1816
|
-
return item.text || ""; // Safety check for missing text property
|
|
1817
|
-
}
|
|
1818
|
-
else if (item.type === "resource") {
|
|
1819
|
-
return `Resource: ${item.resource?.uri || "Unknown"}`;
|
|
1820
|
-
}
|
|
1821
|
-
return String(item);
|
|
1822
|
-
})
|
|
1823
|
-
.join("\n")
|
|
1824
|
-
: "";
|
|
1825
|
-
return {
|
|
1826
|
-
success: true,
|
|
1827
|
-
output: output || "Success",
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
catch (error) {
|
|
1831
|
-
const errorMsg = extractErrorMessage(error);
|
|
1832
|
-
return {
|
|
1833
|
-
success: false,
|
|
1834
|
-
error: `MCP tool execution error: ${errorMsg}`,
|
|
1835
|
-
};
|
|
1836
1310
|
}
|
|
1311
|
+
// Delegate to ToolExecutor (Phase 2 refactoring)
|
|
1312
|
+
return await this.toolExecutor.execute(toolCall);
|
|
1837
1313
|
}
|
|
1838
1314
|
getChatHistory() {
|
|
1839
1315
|
this.checkDisposed();
|
|
1840
1316
|
return [...this.chatHistory];
|
|
1841
1317
|
}
|
|
1842
1318
|
getCurrentDirectory() {
|
|
1843
|
-
return this.
|
|
1319
|
+
return this.toolExecutor.getBashTool().getCurrentDirectory();
|
|
1844
1320
|
}
|
|
1845
1321
|
async executeBashCommand(command) {
|
|
1846
|
-
return await this.
|
|
1322
|
+
return await this.toolExecutor.getBashTool().execute(command);
|
|
1847
1323
|
}
|
|
1848
1324
|
/**
|
|
1849
1325
|
* Check if a bash command is currently executing
|
|
1850
1326
|
*/
|
|
1851
1327
|
isBashExecuting() {
|
|
1852
|
-
return this.
|
|
1328
|
+
return this.toolExecutor.getBashTool().isExecuting();
|
|
1853
1329
|
}
|
|
1854
1330
|
/**
|
|
1855
1331
|
* Move currently running bash command to background
|
|
1856
1332
|
* Returns task ID if successful, null otherwise
|
|
1857
1333
|
*/
|
|
1858
1334
|
moveBashToBackground() {
|
|
1859
|
-
return this.
|
|
1335
|
+
return this.toolExecutor.getBashTool().moveToBackground();
|
|
1860
1336
|
}
|
|
1861
1337
|
getCurrentModel() {
|
|
1862
1338
|
return this.llmClient.getCurrentModel();
|
|
@@ -1865,6 +1341,8 @@ export class LLMAgent extends EventEmitter {
|
|
|
1865
1341
|
this.llmClient.setModel(model);
|
|
1866
1342
|
// Update token counter for new model (use singleton)
|
|
1867
1343
|
this.tokenCounter = getTokenCounter(model);
|
|
1344
|
+
// Update stream handler model for usage tracking
|
|
1345
|
+
this.streamHandler.setModel(model);
|
|
1868
1346
|
}
|
|
1869
1347
|
abortCurrentOperation() {
|
|
1870
1348
|
if (this.abortController) {
|
|
@@ -1989,19 +1467,9 @@ export class LLMAgent extends EventEmitter {
|
|
|
1989
1467
|
*/
|
|
1990
1468
|
async spawnSubagent(role, description, context) {
|
|
1991
1469
|
try {
|
|
1992
|
-
// Import
|
|
1993
|
-
const {
|
|
1994
|
-
|
|
1995
|
-
const roleMap = {
|
|
1996
|
-
'testing': SubagentRole.TESTING,
|
|
1997
|
-
'documentation': SubagentRole.DOCUMENTATION,
|
|
1998
|
-
'refactoring': SubagentRole.REFACTORING,
|
|
1999
|
-
'analysis': SubagentRole.ANALYSIS,
|
|
2000
|
-
'debug': SubagentRole.DEBUG,
|
|
2001
|
-
'performance': SubagentRole.PERFORMANCE,
|
|
2002
|
-
'general': SubagentRole.GENERAL,
|
|
2003
|
-
};
|
|
2004
|
-
const subagentRole = roleMap[role.toLowerCase()] || SubagentRole.GENERAL;
|
|
1470
|
+
// Import parseSubagentRole helper to convert string to enum
|
|
1471
|
+
const { parseSubagentRole } = await import('./subagent-types.js');
|
|
1472
|
+
const subagentRole = parseSubagentRole(role);
|
|
2005
1473
|
// Spawn the subagent
|
|
2006
1474
|
const subagent = await this.subagentOrchestrator.spawnSubagent(subagentRole);
|
|
2007
1475
|
// Execute the task
|
|
@@ -2045,22 +1513,13 @@ export class LLMAgent extends EventEmitter {
|
|
|
2045
1513
|
*/
|
|
2046
1514
|
async executeParallelTasks(tasks) {
|
|
2047
1515
|
try {
|
|
2048
|
-
// Import
|
|
2049
|
-
const {
|
|
2050
|
-
const roleMap = {
|
|
2051
|
-
'testing': SubagentRole.TESTING,
|
|
2052
|
-
'documentation': SubagentRole.DOCUMENTATION,
|
|
2053
|
-
'refactoring': SubagentRole.REFACTORING,
|
|
2054
|
-
'analysis': SubagentRole.ANALYSIS,
|
|
2055
|
-
'debug': SubagentRole.DEBUG,
|
|
2056
|
-
'performance': SubagentRole.PERFORMANCE,
|
|
2057
|
-
'general': SubagentRole.GENERAL,
|
|
2058
|
-
};
|
|
1516
|
+
// Import parseSubagentRole helper to convert string to enum
|
|
1517
|
+
const { parseSubagentRole } = await import('./subagent-types.js');
|
|
2059
1518
|
// Convert tasks to SubagentTask format
|
|
2060
1519
|
const subagentTasks = tasks.map((task, index) => ({
|
|
2061
1520
|
id: task.id || `task-${index}-${Date.now()}`,
|
|
2062
1521
|
description: task.description,
|
|
2063
|
-
role:
|
|
1522
|
+
role: parseSubagentRole(task.role),
|
|
2064
1523
|
priority: 1,
|
|
2065
1524
|
context: {
|
|
2066
1525
|
files: [],
|
|
@@ -2141,8 +1600,8 @@ export class LLMAgent extends EventEmitter {
|
|
|
2141
1600
|
this.contextManager.removeListener('before_prune', this.contextOverflowListener);
|
|
2142
1601
|
this.contextOverflowListener = undefined;
|
|
2143
1602
|
}
|
|
2144
|
-
// Dispose tools
|
|
2145
|
-
this.
|
|
1603
|
+
// Dispose tool executor (includes all tools with cleanup methods)
|
|
1604
|
+
this.toolExecutor.dispose();
|
|
2146
1605
|
// Clear in-memory caches
|
|
2147
1606
|
this.recentToolCalls.clear();
|
|
2148
1607
|
this.toolCallIndexMap.clear();
|