@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.
Files changed (120) hide show
  1. package/README.md +8 -2
  2. package/config-defaults/models.yaml +1 -1
  3. package/dist/agent/core/index.d.ts +8 -0
  4. package/dist/agent/core/index.js +9 -0
  5. package/dist/agent/core/index.js.map +1 -0
  6. package/dist/agent/core/types.d.ts +92 -0
  7. package/dist/agent/core/types.js +11 -0
  8. package/dist/agent/core/types.js.map +1 -0
  9. package/dist/agent/execution/index.d.ts +9 -0
  10. package/dist/agent/execution/index.js +9 -0
  11. package/dist/agent/execution/index.js.map +1 -0
  12. package/dist/agent/execution/tool-executor.d.ts +79 -0
  13. package/dist/agent/execution/tool-executor.js +281 -0
  14. package/dist/agent/execution/tool-executor.js.map +1 -0
  15. package/dist/agent/llm-agent.d.ts +22 -98
  16. package/dist/agent/llm-agent.js +181 -722
  17. package/dist/agent/llm-agent.js.map +1 -1
  18. package/dist/agent/planning/index.d.ts +9 -0
  19. package/dist/agent/planning/index.js +9 -0
  20. package/dist/agent/planning/index.js.map +1 -0
  21. package/dist/agent/planning/plan-executor.d.ts +84 -0
  22. package/dist/agent/planning/plan-executor.js +223 -0
  23. package/dist/agent/planning/plan-executor.js.map +1 -0
  24. package/dist/agent/streaming/index.d.ts +9 -0
  25. package/dist/agent/streaming/index.js +9 -0
  26. package/dist/agent/streaming/index.js.map +1 -0
  27. package/dist/agent/streaming/stream-handler.d.ts +62 -0
  28. package/dist/agent/streaming/stream-handler.js +193 -0
  29. package/dist/agent/streaming/stream-handler.js.map +1 -0
  30. package/dist/agent/subagent-orchestrator.d.ts +3 -3
  31. package/dist/agent/subagent-orchestrator.js +1 -0
  32. package/dist/agent/subagent-orchestrator.js.map +1 -1
  33. package/dist/agent/subagent-types.d.ts +10 -22
  34. package/dist/agent/subagent-types.js +19 -0
  35. package/dist/agent/subagent-types.js.map +1 -1
  36. package/dist/commands/usage.js +14 -0
  37. package/dist/commands/usage.js.map +1 -1
  38. package/dist/index.js +9 -7
  39. package/dist/index.js.map +1 -1
  40. package/dist/llm/client.d.ts +33 -1
  41. package/dist/llm/client.js +23 -11
  42. package/dist/llm/client.js.map +1 -1
  43. package/dist/llm/types.d.ts +7 -1
  44. package/dist/llm/types.js +5 -4
  45. package/dist/llm/types.js.map +1 -1
  46. package/dist/mcp/index.d.ts +31 -0
  47. package/dist/mcp/index.js +36 -0
  48. package/dist/mcp/index.js.map +1 -0
  49. package/dist/planner/types.d.ts +8 -8
  50. package/dist/schemas/index.d.ts +4 -4
  51. package/dist/schemas/tool-schemas.d.ts +12 -12
  52. package/dist/tools/bash.js +1 -1
  53. package/dist/tools/bash.js.map +1 -1
  54. package/dist/tools/text-editor.js +57 -63
  55. package/dist/tools/text-editor.js.map +1 -1
  56. package/dist/ui/hooks/use-enhanced-input.js +66 -81
  57. package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
  58. package/dist/utils/background-task-manager.js +10 -2
  59. package/dist/utils/background-task-manager.js.map +1 -1
  60. package/dist/utils/confirmation-service.js +8 -5
  61. package/dist/utils/confirmation-service.js.map +1 -1
  62. package/dist/utils/index.d.ts +85 -6
  63. package/dist/utils/index.js +103 -15
  64. package/dist/utils/index.js.map +1 -1
  65. package/dist/utils/retry-helper.d.ts +7 -6
  66. package/dist/utils/retry-helper.js +8 -6
  67. package/dist/utils/retry-helper.js.map +1 -1
  68. package/dist/utils/settings-manager.d.ts +6 -0
  69. package/dist/utils/settings-manager.js +51 -64
  70. package/dist/utils/settings-manager.js.map +1 -1
  71. package/node_modules/@ax-cli/schemas/dist/index.d.ts +1 -0
  72. package/node_modules/@ax-cli/schemas/dist/index.d.ts.map +1 -1
  73. package/node_modules/@ax-cli/schemas/dist/index.js.map +1 -1
  74. package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.d.ts +164 -0
  75. package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.d.ts.map +1 -0
  76. package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.js +10 -0
  77. package/node_modules/@ax-cli/schemas/dist/public/agent/chat-types.js.map +1 -0
  78. package/node_modules/@ax-cli/schemas/dist/public/agent/index.d.ts +9 -0
  79. package/node_modules/@ax-cli/schemas/dist/public/agent/index.d.ts.map +1 -0
  80. package/node_modules/@ax-cli/schemas/dist/public/agent/index.js +9 -0
  81. package/node_modules/@ax-cli/schemas/dist/public/agent/index.js.map +1 -0
  82. package/package.json +1 -1
  83. package/packages/schemas/dist/index.d.ts +1 -0
  84. package/packages/schemas/dist/index.d.ts.map +1 -1
  85. package/packages/schemas/dist/index.js.map +1 -1
  86. package/packages/schemas/dist/public/agent/chat-types.d.ts +164 -0
  87. package/packages/schemas/dist/public/agent/chat-types.d.ts.map +1 -0
  88. package/packages/schemas/dist/public/agent/chat-types.js +10 -0
  89. package/packages/schemas/dist/public/agent/chat-types.js.map +1 -0
  90. package/packages/schemas/dist/public/agent/index.d.ts +9 -0
  91. package/packages/schemas/dist/public/agent/index.d.ts.map +1 -0
  92. package/packages/schemas/dist/public/agent/index.js +9 -0
  93. package/packages/schemas/dist/public/agent/index.js.map +1 -0
  94. package/dist/mcp/config-detector-v2.d.ts +0 -83
  95. package/dist/mcp/config-detector-v2.js +0 -328
  96. package/dist/mcp/config-detector-v2.js.map +0 -1
  97. package/dist/mcp/config-migrator-v2.d.ts +0 -89
  98. package/dist/mcp/config-migrator-v2.js +0 -288
  99. package/dist/mcp/config-migrator-v2.js.map +0 -1
  100. package/dist/mcp/config-v2.d.ts +0 -111
  101. package/dist/mcp/config-v2.js +0 -443
  102. package/dist/mcp/config-v2.js.map +0 -1
  103. package/dist/mcp/transports-v2.d.ts +0 -152
  104. package/dist/mcp/transports-v2.js +0 -481
  105. package/dist/mcp/transports-v2.js.map +0 -1
  106. package/dist/utils/error-sanitizer.d.ts +0 -119
  107. package/dist/utils/error-sanitizer.js +0 -253
  108. package/dist/utils/error-sanitizer.js.map +0 -1
  109. package/dist/utils/errors.d.ts +0 -74
  110. package/dist/utils/errors.js +0 -139
  111. package/dist/utils/errors.js.map +0 -1
  112. package/dist/utils/incremental-analyzer.d.ts +0 -134
  113. package/dist/utils/incremental-analyzer.js +0 -377
  114. package/dist/utils/incremental-analyzer.js.map +0 -1
  115. package/dist/utils/settings.d.ts +0 -1
  116. package/dist/utils/settings.js +0 -4
  117. package/dist/utils/settings.js.map +0 -1
  118. package/dist/utils/streaming-analyzer.d.ts +0 -160
  119. package/dist/utils/streaming-analyzer.js +0 -214
  120. package/dist/utils/streaming-analyzer.js.map +0 -1
@@ -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
- import { getUsageTracker } from "../utils/usage-tracker.js";
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
- textEditor;
28
- bash;
29
- bashOutput;
30
- todoTool;
31
- search;
32
- // Lazy-loaded tools (rarely used)
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
- this.textEditor = new TextEditorTool();
73
- this.bash = new BashTool();
74
- this.bashOutput = new BashOutputTool();
75
- this.todoTool = new TodoTool();
76
- this.search = new SearchTool();
77
- // architectureTool and validationTool are lazy-loaded (see getters below)
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: Keep static system prompt separate from dynamic context
116
- // This maximizes cache hit rates on the xAI API (cached tokens = 50% cost savings)
117
- // The API automatically caches identical content across requests
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
- initializeCheckpointManager() {
146
- // Initialize checkpoint manager in the background
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 this.checkpointManager.initialize();
150
- this.emit('system', 'Checkpoint system initialized');
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
- console.warn("Checkpoint initialization failed:", errorMsg);
155
- this.emit('system', `Checkpoint initialization failed: ${errorMsg}`);
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
- const errorMsg = extractErrorMessage(error);
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
- // Initialize MCP in the background without blocking
164
- Promise.resolve().then(async () => {
165
- try {
166
- const config = loadMCPConfig();
167
- if (config.servers.length > 0) {
168
- await initializeMCPServers();
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
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
431
- console.error(`[LOOP DETECTION] Tool: ${toolCall.function.name}`);
432
- console.error(`[LOOP DETECTION] Count: ${result.count}`);
433
- console.error(`[LOOP DETECTION] Threshold: ${result.threshold}`);
434
- console.error(`[LOOP DETECTION] Is Loop: ${result.isLoop}`);
435
- if (result.reason) {
436
- console.error(`[LOOP DETECTION] Reason: ${result.reason}`);
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
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
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
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
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 (process.env.DEBUG_LOOP_DETECTION === '1') {
465
- const detector = getLoopDetector();
466
- const stats = detector.getStats();
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.todoTool.createTodoList(todoItems);
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.todoTool.updateTodoList([{
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.todoTool.updateTodoList([{
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.todoTool.updateTodoList([{
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.processStreamingChunks(stream, inputTokensRef.value, lastTokenUpdateRef, totalOutputTokensRef);
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
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
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
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
1065
- console.error(`[LOOP CHECK] hasRepetitiveCall: ${hasRepetitiveCall}\n`);
1066
- }
887
+ debugLoop(`hasRepetitiveCall: ${hasRepetitiveCall}`);
1067
888
  if (hasRepetitiveCall) {
1068
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
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.success
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.success
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
- * Process streaming chunks and accumulate message
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
- async *processStreamingChunks(stream, inputTokens, lastTokenUpdate, totalOutputTokens) {
1318
- let accumulatedMessage = {};
1319
- let accumulatedContent = "";
1320
- let toolCallsYielded = false;
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
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
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.success
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.success
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
- if (process.env.DEBUG_LOOP_DETECTION === '1') {
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.processStreamingChunks(stream, inputTokensRef.value, lastTokenUpdateRef, totalOutputTokensRef);
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
- * Parse and validate tool call arguments
1630
- * @param toolCall The tool call to parse arguments from
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
- try {
1662
- const parseResult = this.parseToolArguments(toolCall, 'Tool');
1663
- if (!parseResult.success) {
1664
- return { success: false, error: parseResult.error };
1665
- }
1666
- const args = parseResult.args;
1667
- // Check if tool approval is required (for VSCode integration)
1668
- if (this.requireToolApproval) {
1669
- // Only require approval for file modification operations
1670
- const needsApproval = toolCall.function.name === "create_file" ||
1671
- toolCall.function.name === "str_replace_editor" ||
1672
- toolCall.function.name === "insert_text";
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: `Unknown tool: ${toolCall.function.name}`,
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
- return {
1806
- success: false,
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.bash.getCurrentDirectory();
1319
+ return this.toolExecutor.getBashTool().getCurrentDirectory();
1844
1320
  }
1845
1321
  async executeBashCommand(command) {
1846
- return await this.bash.execute(command);
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.bash.isExecuting();
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.bash.moveToBackground();
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 SubagentRole from subagent-types
1993
- const { SubagentRole } = await import('./subagent-types.js');
1994
- // Convert string role to SubagentRole enum
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 SubagentRole and SubagentTask
2049
- const { SubagentRole } = await import('./subagent-types.js');
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: roleMap[task.role.toLowerCase()] ?? SubagentRole.GENERAL,
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 that have cleanup methods
2145
- this.bash.dispose();
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();