@vybestack/llxprt-code-core 0.2.24 → 0.3.4-nightly.250912.5e46408e

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 (194) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/code_assist/converter.js +35 -3
  5. package/dist/src/code_assist/converter.js.map +1 -1
  6. package/dist/src/code_assist/oauth2.d.ts +1 -1
  7. package/dist/src/code_assist/oauth2.js +18 -25
  8. package/dist/src/code_assist/oauth2.js.map +1 -1
  9. package/dist/src/config/config.d.ts +33 -2
  10. package/dist/src/config/config.js +73 -13
  11. package/dist/src/config/config.js.map +1 -1
  12. package/dist/src/config/storage.d.ts +33 -0
  13. package/dist/src/config/storage.js +93 -0
  14. package/dist/src/config/storage.js.map +1 -0
  15. package/dist/src/core/client.d.ts +8 -5
  16. package/dist/src/core/client.js +119 -20
  17. package/dist/src/core/client.js.map +1 -1
  18. package/dist/src/core/contentGenerator.js +11 -0
  19. package/dist/src/core/contentGenerator.js.map +1 -1
  20. package/dist/src/core/coreToolScheduler.d.ts +18 -12
  21. package/dist/src/core/coreToolScheduler.js +235 -241
  22. package/dist/src/core/coreToolScheduler.js.map +1 -1
  23. package/dist/src/core/logger.d.ts +3 -1
  24. package/dist/src/core/logger.js +5 -3
  25. package/dist/src/core/logger.js.map +1 -1
  26. package/dist/src/core/nonInteractiveToolExecutor.js +2 -2
  27. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  28. package/dist/src/core/subagent.js +2 -12
  29. package/dist/src/core/subagent.js.map +1 -1
  30. package/dist/src/core/turn.d.ts +13 -2
  31. package/dist/src/core/turn.js +15 -4
  32. package/dist/src/core/turn.js.map +1 -1
  33. package/dist/src/filters/EmojiFilter.d.ts +5 -0
  34. package/dist/src/filters/EmojiFilter.js +3 -2
  35. package/dist/src/filters/EmojiFilter.js.map +1 -1
  36. package/dist/src/ide/detect-ide.d.ts +8 -3
  37. package/dist/src/ide/detect-ide.js +29 -11
  38. package/dist/src/ide/detect-ide.js.map +1 -1
  39. package/dist/src/ide/ide-client.d.ts +5 -4
  40. package/dist/src/ide/ide-client.js +21 -17
  41. package/dist/src/ide/ide-client.js.map +1 -1
  42. package/dist/src/ide/ide-installer.d.ts +1 -1
  43. package/dist/src/ide/ide-installer.js +29 -20
  44. package/dist/src/ide/ide-installer.js.map +1 -1
  45. package/dist/src/ide/process-utils.d.ts +7 -5
  46. package/dist/src/ide/process-utils.js +80 -47
  47. package/dist/src/ide/process-utils.js.map +1 -1
  48. package/dist/src/index.d.ts +6 -0
  49. package/dist/src/index.js +7 -0
  50. package/dist/src/index.js.map +1 -1
  51. package/dist/src/mcp/file-token-store.d.ts +58 -0
  52. package/dist/src/mcp/file-token-store.js +181 -0
  53. package/dist/src/mcp/file-token-store.js.map +1 -0
  54. package/dist/src/mcp/oauth-token-storage.d.ts +13 -28
  55. package/dist/src/mcp/oauth-token-storage.js +21 -87
  56. package/dist/src/mcp/oauth-token-storage.js.map +1 -1
  57. package/dist/src/mcp/oauth-utils.d.ts +8 -0
  58. package/dist/src/mcp/oauth-utils.js +41 -27
  59. package/dist/src/mcp/oauth-utils.js.map +1 -1
  60. package/dist/src/mcp/token-store.d.ts +99 -0
  61. package/dist/src/mcp/token-store.js +77 -0
  62. package/dist/src/mcp/token-store.js.map +1 -0
  63. package/dist/src/parsers/TextToolCallParser.d.ts +5 -0
  64. package/dist/src/parsers/TextToolCallParser.js +3 -13
  65. package/dist/src/parsers/TextToolCallParser.js.map +1 -1
  66. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +5 -0
  67. package/dist/src/providers/anthropic/AnthropicProvider.js +26 -8
  68. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  69. package/dist/src/providers/gemini/GeminiProvider.js +16 -1
  70. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  71. package/dist/src/providers/openai/OpenAIProvider.d.ts +15 -0
  72. package/dist/src/providers/openai/OpenAIProvider.js +99 -48
  73. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  74. package/dist/src/services/gitService.d.ts +3 -1
  75. package/dist/src/services/gitService.js +20 -10
  76. package/dist/src/services/gitService.js.map +1 -1
  77. package/dist/src/services/history/ContentConverters.js +40 -1
  78. package/dist/src/services/history/ContentConverters.js.map +1 -1
  79. package/dist/src/services/loopDetectionService.js +8 -2
  80. package/dist/src/services/loopDetectionService.js.map +1 -1
  81. package/dist/src/services/shellExecutionService.js +2 -1
  82. package/dist/src/services/shellExecutionService.js.map +1 -1
  83. package/dist/src/settings/SettingsService.d.ts +5 -0
  84. package/dist/src/settings/SettingsService.js +5 -0
  85. package/dist/src/settings/SettingsService.js.map +1 -1
  86. package/dist/src/settings/settingsServiceInstance.d.ts +5 -0
  87. package/dist/src/settings/settingsServiceInstance.js +5 -0
  88. package/dist/src/settings/settingsServiceInstance.js.map +1 -1
  89. package/dist/src/settings/types.d.ts +5 -0
  90. package/dist/src/settings/types.js +3 -1
  91. package/dist/src/settings/types.js.map +1 -1
  92. package/dist/src/tools/IToolFormatter.d.ts +5 -0
  93. package/dist/src/tools/IToolFormatter.js +3 -13
  94. package/dist/src/tools/IToolFormatter.js.map +1 -1
  95. package/dist/src/tools/ToolFormatter.d.ts +18 -0
  96. package/dist/src/tools/ToolFormatter.js +41 -13
  97. package/dist/src/tools/ToolFormatter.js.map +1 -1
  98. package/dist/src/tools/doubleEscapeUtils.d.ts +3 -13
  99. package/dist/src/tools/doubleEscapeUtils.js +5 -0
  100. package/dist/src/tools/doubleEscapeUtils.js.map +1 -1
  101. package/dist/src/tools/edit.js +10 -1
  102. package/dist/src/tools/edit.js.map +1 -1
  103. package/dist/src/tools/glob.d.ts +0 -4
  104. package/dist/src/tools/glob.js +18 -42
  105. package/dist/src/tools/glob.js.map +1 -1
  106. package/dist/src/tools/grep.d.ts +0 -4
  107. package/dist/src/tools/grep.js +41 -77
  108. package/dist/src/tools/grep.js.map +1 -1
  109. package/dist/src/tools/ls.d.ts +2 -6
  110. package/dist/src/tools/ls.js +18 -13
  111. package/dist/src/tools/ls.js.map +1 -1
  112. package/dist/src/tools/mcp-client.d.ts +7 -0
  113. package/dist/src/tools/mcp-client.js +26 -21
  114. package/dist/src/tools/mcp-client.js.map +1 -1
  115. package/dist/src/tools/mcp-tool.js +12 -2
  116. package/dist/src/tools/mcp-tool.js.map +1 -1
  117. package/dist/src/tools/memoryTool.js +7 -2
  118. package/dist/src/tools/memoryTool.js.map +1 -1
  119. package/dist/src/tools/read-file.js +2 -30
  120. package/dist/src/tools/read-file.js.map +1 -1
  121. package/dist/src/tools/read-many-files.js +29 -52
  122. package/dist/src/tools/read-many-files.js.map +1 -1
  123. package/dist/src/tools/ripGrep.d.ts +46 -0
  124. package/dist/src/tools/ripGrep.js +368 -0
  125. package/dist/src/tools/ripGrep.js.map +1 -0
  126. package/dist/src/tools/shell.js +13 -2
  127. package/dist/src/tools/shell.js.map +1 -1
  128. package/dist/src/tools/todo-pause.js +0 -1
  129. package/dist/src/tools/todo-pause.js.map +1 -1
  130. package/dist/src/tools/tool-error.d.ts +18 -2
  131. package/dist/src/tools/tool-error.js +27 -1
  132. package/dist/src/tools/tool-error.js.map +1 -1
  133. package/dist/src/tools/tool-registry.d.ts +13 -2
  134. package/dist/src/tools/tool-registry.js +30 -2
  135. package/dist/src/tools/tool-registry.js.map +1 -1
  136. package/dist/src/tools/tools.d.ts +7 -5
  137. package/dist/src/tools/tools.js +12 -0
  138. package/dist/src/tools/tools.js.map +1 -1
  139. package/dist/src/tools/web-fetch.js +101 -76
  140. package/dist/src/tools/web-fetch.js.map +1 -1
  141. package/dist/src/tools/web-search-invocation.js +13 -1
  142. package/dist/src/tools/web-search-invocation.js.map +1 -1
  143. package/dist/src/utils/deterministicEditCorrector.d.ts +28 -0
  144. package/dist/src/utils/deterministicEditCorrector.js +295 -0
  145. package/dist/src/utils/deterministicEditCorrector.js.map +1 -0
  146. package/dist/src/utils/editCorrector.d.ts +4 -4
  147. package/dist/src/utils/editCorrector.js +15 -213
  148. package/dist/src/utils/editCorrector.js.map +1 -1
  149. package/dist/src/utils/editor.js +1 -1
  150. package/dist/src/utils/editor.js.map +1 -1
  151. package/dist/src/utils/errors.d.ts +19 -0
  152. package/dist/src/utils/errors.js +32 -0
  153. package/dist/src/utils/errors.js.map +1 -1
  154. package/dist/src/utils/fileUtils.d.ts +2 -7
  155. package/dist/src/utils/fileUtils.js +16 -47
  156. package/dist/src/utils/fileUtils.js.map +1 -1
  157. package/dist/src/utils/filesearch/fileSearch.d.ts +1 -0
  158. package/dist/src/utils/filesearch/fileSearch.js +14 -9
  159. package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
  160. package/dist/src/utils/ignorePatterns.d.ts +103 -0
  161. package/dist/src/utils/ignorePatterns.js +220 -0
  162. package/dist/src/utils/ignorePatterns.js.map +1 -0
  163. package/dist/src/utils/installationManager.d.ts +16 -0
  164. package/dist/src/utils/installationManager.js +50 -0
  165. package/dist/src/utils/installationManager.js.map +1 -0
  166. package/dist/src/utils/memoryDiscovery.d.ts +1 -1
  167. package/dist/src/utils/memoryDiscovery.js +64 -43
  168. package/dist/src/utils/memoryDiscovery.js.map +1 -1
  169. package/dist/src/utils/paths.d.ts +0 -17
  170. package/dist/src/utils/paths.js +0 -26
  171. package/dist/src/utils/paths.js.map +1 -1
  172. package/dist/src/utils/schemaValidator.js +4 -0
  173. package/dist/src/utils/schemaValidator.js.map +1 -1
  174. package/dist/src/utils/shell-utils.d.ts +1 -1
  175. package/dist/src/utils/shell-utils.js +23 -29
  176. package/dist/src/utils/shell-utils.js.map +1 -1
  177. package/dist/src/utils/tool-utils.d.ts +19 -0
  178. package/dist/src/utils/tool-utils.js +58 -0
  179. package/dist/src/utils/tool-utils.js.map +1 -0
  180. package/dist/src/utils/userAccountManager.d.ts +20 -0
  181. package/dist/src/utils/userAccountManager.js +122 -0
  182. package/dist/src/utils/userAccountManager.js.map +1 -0
  183. package/dist/src/utils/workspaceContext.js +11 -5
  184. package/dist/src/utils/workspaceContext.js.map +1 -1
  185. package/package.json +7 -3
  186. package/dist/src/utils/nextSpeakerChecker.d.ts +0 -12
  187. package/dist/src/utils/nextSpeakerChecker.js +0 -97
  188. package/dist/src/utils/nextSpeakerChecker.js.map +0 -1
  189. package/dist/src/utils/user_account.d.ts +0 -9
  190. package/dist/src/utils/user_account.js +0 -109
  191. package/dist/src/utils/user_account.js.map +0 -1
  192. package/dist/src/utils/user_id.d.ts +0 -11
  193. package/dist/src/utils/user_id.js +0 -49
  194. package/dist/src/utils/user_id.js.map +0 -1
@@ -4,10 +4,11 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { ToolConfirmationOutcome, ApprovalMode, logToolCall, ToolCallEvent, ToolErrorType, } from '../index.js';
7
- import { ToolCallTrackerService } from '../services/tool-call-tracker-service.js';
8
7
  import { getResponseTextFromParts } from '../utils/generateContentResponseUtilities.js';
9
8
  import { isModifiableDeclarativeTool, modifyWithEditor, } from '../tools/modifiable-tool.js';
10
9
  import * as Diff from 'diff';
10
+ import levenshtein from 'fast-levenshtein';
11
+ import { doesToolInvocationMatch } from '../utils/tool-utils.js';
11
12
  /**
12
13
  * Formats tool output for a Gemini FunctionResponse.
13
14
  */
@@ -25,62 +26,56 @@ export function convertToFunctionResponse(toolName, callId, llmContent) {
25
26
  ? llmContent[0]
26
27
  : llmContent;
27
28
  if (typeof contentToProcess === 'string') {
28
- return createFunctionResponsePart(callId, toolName, contentToProcess);
29
+ return [createFunctionResponsePart(callId, toolName, contentToProcess)];
29
30
  }
30
31
  if (Array.isArray(contentToProcess)) {
31
- // If the array contains string elements, join them and create a single function response
32
- const stringElements = contentToProcess.filter((item) => typeof item === 'string');
33
- if (stringElements.length === contentToProcess.length) {
34
- // All elements are strings, join them
35
- return createFunctionResponsePart(callId, toolName, stringElements.join('\n'));
36
- }
37
- // If the array contains Part objects, check if any are already function responses
32
+ // Check if any part already has a function response to avoid duplicates
38
33
  const hasFunctionResponse = contentToProcess.some((part) => typeof part === 'object' && part.functionResponse);
39
34
  if (hasFunctionResponse) {
40
- // Already has function response(s), return as-is
41
- return contentToProcess;
35
+ // Already has function response(s), return as-is without creating duplicates
36
+ return toParts(contentToProcess);
42
37
  }
43
- // Otherwise, wrap the parts in a function response
44
- return createFunctionResponsePart(callId, toolName, getResponseTextFromParts(contentToProcess) ||
45
- 'Tool execution succeeded.');
38
+ // No existing function response, create one
39
+ const functionResponse = createFunctionResponsePart(callId, toolName, 'Tool execution succeeded.');
40
+ return [functionResponse, ...toParts(contentToProcess)];
46
41
  }
47
42
  // After this point, contentToProcess is a single Part object.
48
43
  if (contentToProcess.functionResponse) {
49
- if (contentToProcess.functionResponse.response?.content) {
50
- const stringifiedOutput = getResponseTextFromParts(contentToProcess.functionResponse.response.content) || '';
51
- return createFunctionResponsePart(callId, toolName, stringifiedOutput);
44
+ if (contentToProcess.functionResponse.response?.['content']) {
45
+ const stringifiedOutput = getResponseTextFromParts(contentToProcess.functionResponse.response['content']) || '';
46
+ return [createFunctionResponsePart(callId, toolName, stringifiedOutput)];
52
47
  }
53
- // It's a functionResponse - ensure it has the correct id and name
54
- return {
55
- functionResponse: {
56
- ...contentToProcess.functionResponse,
57
- id: callId,
58
- name: toolName,
59
- },
60
- };
48
+ // It's a functionResponse that we should pass through as is.
49
+ return [contentToProcess];
61
50
  }
62
51
  if (contentToProcess.inlineData || contentToProcess.fileData) {
63
52
  const mimeType = contentToProcess.inlineData?.mimeType ||
64
53
  contentToProcess.fileData?.mimeType ||
65
54
  'unknown';
66
- // Return a special function response that includes the binary content
67
- return {
68
- functionResponse: {
69
- id: callId,
70
- name: toolName,
71
- response: {
72
- output: `Binary content of type ${mimeType} was processed.`,
73
- // Include the binary content in a special field
74
- binaryContent: contentToProcess,
75
- },
76
- },
77
- };
55
+ const functionResponse = createFunctionResponsePart(callId, toolName, `Binary content of type ${mimeType} was processed.`);
56
+ return [functionResponse, contentToProcess];
78
57
  }
79
58
  if (contentToProcess.text !== undefined) {
80
- return createFunctionResponsePart(callId, toolName, contentToProcess.text);
59
+ return [
60
+ createFunctionResponsePart(callId, toolName, contentToProcess.text),
61
+ ];
81
62
  }
82
63
  // Default case for other kinds of parts.
83
- return createFunctionResponsePart(callId, toolName, 'Tool execution succeeded.');
64
+ return [
65
+ createFunctionResponsePart(callId, toolName, 'Tool execution succeeded.'),
66
+ ];
67
+ }
68
+ function toParts(input) {
69
+ const parts = [];
70
+ for (const part of Array.isArray(input) ? input : [input]) {
71
+ if (typeof part === 'string') {
72
+ parts.push({ text: part });
73
+ }
74
+ else if (part) {
75
+ parts.push(part);
76
+ }
77
+ }
78
+ return parts;
84
79
  }
85
80
  const createErrorResponse = (request, error, errorType) => ({
86
81
  callId: request.callId,
@@ -114,12 +109,13 @@ export class CoreToolScheduler {
114
109
  onToolCallsUpdate;
115
110
  getPreferredEditor;
116
111
  config;
117
- pendingQueue = [];
118
- isProcessingBatch = false;
119
112
  onEditorClose;
113
+ isFinalizingToolCalls = false;
114
+ isScheduling = false;
115
+ requestQueue = [];
120
116
  constructor(options) {
121
117
  this.config = options.config;
122
- this.toolRegistry = options.toolRegistry;
118
+ this.toolRegistry = options.config.getToolRegistry();
123
119
  this.outputUpdateHandler = options.outputUpdateHandler;
124
120
  this.onAllToolCallsComplete = options.onAllToolCallsComplete;
125
121
  this.onToolCallsUpdate = options.onToolCallsUpdate;
@@ -138,7 +134,6 @@ export class CoreToolScheduler {
138
134
  const existingStartTime = currentCall.startTime;
139
135
  const toolInstance = currentCall.tool;
140
136
  const invocation = currentCall.invocation;
141
- const existingSignal = currentCall.signal;
142
137
  const outcome = currentCall.outcome;
143
138
  switch (newStatus) {
144
139
  case 'success': {
@@ -186,7 +181,6 @@ export class CoreToolScheduler {
186
181
  startTime: existingStartTime,
187
182
  outcome,
188
183
  invocation,
189
- signal: existingSignal,
190
184
  };
191
185
  case 'cancelled': {
192
186
  const durationMs = existingStartTime
@@ -265,7 +259,7 @@ export class CoreToolScheduler {
265
259
  }
266
260
  });
267
261
  this.notifyToolCallsUpdate();
268
- void this.checkAndNotifyCompletion();
262
+ this.checkAndNotifyCompletion();
269
263
  }
270
264
  setArgsInternal(targetCallId, args) {
271
265
  this.toolCalls = this.toolCalls.map((call) => {
@@ -291,6 +285,10 @@ export class CoreToolScheduler {
291
285
  };
292
286
  });
293
287
  }
288
+ isRunning() {
289
+ return (this.isFinalizingToolCalls ||
290
+ this.toolCalls.some((call) => call.status === 'executing' || call.status === 'awaiting_approval'));
291
+ }
294
292
  buildInvocation(tool, args) {
295
293
  try {
296
294
  return tool.build(args);
@@ -302,77 +300,124 @@ export class CoreToolScheduler {
302
300
  return new Error(String(e));
303
301
  }
304
302
  }
305
- async schedule(request, signal) {
306
- // Queue requests if we're currently processing a batch
307
- const requestsToProcess = Array.isArray(request) ? request : [request];
308
- if (this.isProcessingBatch) {
309
- // Add to queue for next batch
310
- requestsToProcess.forEach((req) => {
311
- this.pendingQueue.push({ request: req, signal });
312
- });
313
- return;
303
+ /**
304
+ * Generates a suggestion string for a tool name that was not found in the registry.
305
+ * It finds the closest matches based on Levenshtein distance.
306
+ * @param unknownToolName The tool name that was not found.
307
+ * @param topN The number of suggestions to return. Defaults to 3.
308
+ * @returns A suggestion string like " Did you mean 'tool'?" or " Did you mean one of: 'tool1', 'tool2'?", or an empty string if no suggestions are found.
309
+ */
310
+ getToolSuggestion(unknownToolName, topN = 3) {
311
+ const allToolNames = this.toolRegistry.getAllToolNames();
312
+ const matches = allToolNames.map((toolName) => ({
313
+ name: toolName,
314
+ distance: levenshtein.get(unknownToolName, toolName),
315
+ }));
316
+ matches.sort((a, b) => a.distance - b.distance);
317
+ const topNResults = matches.slice(0, topN);
318
+ if (topNResults.length === 0) {
319
+ return '';
314
320
  }
315
- const toolRegistry = await this.toolRegistry;
316
- const newToolCalls = requestsToProcess.map((reqInfo) => {
317
- // Create context from config
318
- const context = {
319
- sessionId: typeof this.config.getSessionId === 'function'
320
- ? this.config.getSessionId()
321
- : 'default-session',
322
- interactiveMode: true, // Enable interactive mode for UI updates
323
- // TODO: Add agentId when available in the request
324
- };
325
- const toolInstance = toolRegistry.getTool(reqInfo.name, context);
326
- if (!toolInstance) {
327
- return {
328
- status: 'error',
329
- request: reqInfo,
330
- response: createErrorResponse(reqInfo, new Error(`Tool "${reqInfo.name}" not found in registry.`), ToolErrorType.TOOL_NOT_REGISTERED),
331
- durationMs: 0,
321
+ const suggestedNames = topNResults
322
+ .map((match) => `"${match.name}"`)
323
+ .join(', ');
324
+ if (topNResults.length > 1) {
325
+ return ` Did you mean one of: ${suggestedNames}?`;
326
+ }
327
+ else {
328
+ return ` Did you mean ${suggestedNames}?`;
329
+ }
330
+ }
331
+ schedule(request, signal) {
332
+ if (this.isRunning() || this.isScheduling) {
333
+ return new Promise((resolve, reject) => {
334
+ const abortHandler = () => {
335
+ // Find and remove the request from the queue
336
+ const index = this.requestQueue.findIndex((item) => item.request === request);
337
+ if (index > -1) {
338
+ this.requestQueue.splice(index, 1);
339
+ reject(new Error('Tool call cancelled while in queue.'));
340
+ }
332
341
  };
342
+ signal.addEventListener('abort', abortHandler, { once: true });
343
+ this.requestQueue.push({
344
+ request,
345
+ signal,
346
+ resolve: () => {
347
+ signal.removeEventListener('abort', abortHandler);
348
+ resolve();
349
+ },
350
+ reject: (reason) => {
351
+ signal.removeEventListener('abort', abortHandler);
352
+ reject(reason);
353
+ },
354
+ });
355
+ });
356
+ }
357
+ return this._schedule(request, signal);
358
+ }
359
+ async _schedule(request, signal) {
360
+ this.isScheduling = true;
361
+ try {
362
+ if (this.isRunning()) {
363
+ throw new Error('Cannot schedule new tool calls while other tool calls are actively running (executing or awaiting approval).');
333
364
  }
334
- const invocationOrError = this.buildInvocation(toolInstance, reqInfo.args);
335
- if (invocationOrError instanceof Error) {
365
+ const requestsToProcess = Array.isArray(request) ? request : [request];
366
+ const newToolCalls = requestsToProcess.map((reqInfo) => {
367
+ const toolInstance = this.toolRegistry.getTool(reqInfo.name);
368
+ if (!toolInstance) {
369
+ const suggestion = this.getToolSuggestion(reqInfo.name);
370
+ const errorMessage = `Tool "${reqInfo.name}" not found in registry. Tools must use the exact names that are registered.${suggestion}`;
371
+ return {
372
+ status: 'error',
373
+ request: reqInfo,
374
+ response: createErrorResponse(reqInfo, new Error(errorMessage), ToolErrorType.TOOL_NOT_REGISTERED),
375
+ durationMs: 0,
376
+ };
377
+ }
378
+ const invocationOrError = this.buildInvocation(toolInstance, reqInfo.args);
379
+ if (invocationOrError instanceof Error) {
380
+ return {
381
+ status: 'error',
382
+ request: reqInfo,
383
+ tool: toolInstance,
384
+ response: createErrorResponse(reqInfo, invocationOrError, ToolErrorType.INVALID_TOOL_PARAMS),
385
+ durationMs: 0,
386
+ };
387
+ }
336
388
  return {
337
- status: 'error',
389
+ status: 'validating',
338
390
  request: reqInfo,
339
391
  tool: toolInstance,
340
- response: createErrorResponse(reqInfo, invocationOrError, ToolErrorType.INVALID_TOOL_PARAMS),
341
- durationMs: 0,
392
+ invocation: invocationOrError,
393
+ startTime: Date.now(),
342
394
  };
343
- }
344
- return {
345
- status: 'validating',
346
- request: reqInfo,
347
- tool: toolInstance,
348
- invocation: invocationOrError,
349
- startTime: Date.now(),
350
- signal,
351
- };
352
- });
353
- this.toolCalls = this.toolCalls.concat(newToolCalls);
354
- this.notifyToolCallsUpdate();
355
- for (const toolCall of newToolCalls) {
356
- if (toolCall.status !== 'validating') {
357
- continue;
358
- }
359
- const { request: reqInfo, invocation } = toolCall;
360
- try {
361
- if (signal.aborted) {
362
- this.setStatusInternal(reqInfo.callId, 'cancelled', 'Tool call cancelled by user.');
395
+ });
396
+ this.toolCalls = this.toolCalls.concat(newToolCalls);
397
+ this.notifyToolCallsUpdate();
398
+ for (const toolCall of newToolCalls) {
399
+ if (toolCall.status !== 'validating') {
363
400
  continue;
364
401
  }
365
- if (this.config.getApprovalMode() === ApprovalMode.YOLO) {
366
- this.setToolCallOutcome(reqInfo.callId, ToolConfirmationOutcome.ProceedAlways);
367
- this.setStatusInternal(reqInfo.callId, 'scheduled');
368
- }
369
- else {
402
+ const { request: reqInfo, invocation } = toolCall;
403
+ try {
404
+ if (signal.aborted) {
405
+ this.setStatusInternal(reqInfo.callId, 'cancelled', 'Tool call cancelled by user.');
406
+ continue;
407
+ }
370
408
  const confirmationDetails = await invocation.shouldConfirmExecute(signal);
371
- // Check if this command is already allowed
372
- const isAlwaysAllowed = confirmationDetails &&
373
- confirmationDetails.type === 'exec' &&
374
- this.config.isCommandAlwaysAllowed(confirmationDetails.rootCommand);
375
- if (confirmationDetails && !isAlwaysAllowed) {
409
+ if (!confirmationDetails) {
410
+ this.setToolCallOutcome(reqInfo.callId, ToolConfirmationOutcome.ProceedAlways);
411
+ this.setStatusInternal(reqInfo.callId, 'scheduled');
412
+ continue;
413
+ }
414
+ const allowedTools = this.config.getAllowedTools() || [];
415
+ if (this.config.getApprovalMode() === ApprovalMode.YOLO ||
416
+ doesToolInvocationMatch(toolCall.tool, invocation, allowedTools)) {
417
+ this.setToolCallOutcome(reqInfo.callId, ToolConfirmationOutcome.ProceedAlways);
418
+ this.setStatusInternal(reqInfo.callId, 'scheduled');
419
+ }
420
+ else {
376
421
  // Allow IDE to resolve confirmation
377
422
  if (confirmationDetails.type === 'edit' &&
378
423
  confirmationDetails.ideConfirmation) {
@@ -392,26 +437,17 @@ export class CoreToolScheduler {
392
437
  };
393
438
  this.setStatusInternal(reqInfo.callId, 'awaiting_approval', wrappedConfirmationDetails);
394
439
  }
395
- else {
396
- // Either no confirmation needed or command is always allowed
397
- this.setToolCallOutcome(reqInfo.callId, ToolConfirmationOutcome.ProceedAlways);
398
- this.setStatusInternal(reqInfo.callId, 'scheduled');
399
- }
400
440
  }
401
- }
402
- catch (error) {
403
- // Check if the error is due to an aborted signal
404
- if (signal.aborted ||
405
- (error instanceof Error && error.name === 'AbortError')) {
406
- this.setStatusInternal(reqInfo.callId, 'cancelled', 'Tool call was cancelled');
407
- }
408
- else {
441
+ catch (error) {
409
442
  this.setStatusInternal(reqInfo.callId, 'error', createErrorResponse(reqInfo, error instanceof Error ? error : new Error(String(error)), ToolErrorType.UNHANDLED_EXCEPTION));
410
443
  }
411
444
  }
445
+ this.attemptExecutionOfScheduledCalls(signal);
446
+ void this.checkAndNotifyCompletion();
447
+ }
448
+ finally {
449
+ this.isScheduling = false;
412
450
  }
413
- this.attemptExecutionOfScheduledCalls(signal);
414
- void this.checkAndNotifyCompletion();
415
451
  }
416
452
  async handleConfirmationResponse(callId, originalOnConfirm, outcome, signal, payload) {
417
453
  const toolCall = this.toolCalls.find((c) => c.request.callId === callId && c.status === 'awaiting_approval');
@@ -422,13 +458,6 @@ export class CoreToolScheduler {
422
458
  await this.autoApproveCompatiblePendingTools(signal, callId);
423
459
  }
424
460
  this.setToolCallOutcome(callId, outcome);
425
- // Store the command if user selected "always allow"
426
- if (outcome === ToolConfirmationOutcome.ProceedAlways &&
427
- toolCall &&
428
- toolCall.status === 'awaiting_approval' &&
429
- toolCall.confirmationDetails.type === 'exec') {
430
- this.config.addAlwaysAllowedCommand(toolCall.confirmationDetails.rootCommand);
431
- }
432
461
  if (outcome === ToolConfirmationOutcome.Cancel || signal.aborted) {
433
462
  this.setStatusInternal(callId, 'cancelled', 'User did not allow tool call');
434
463
  }
@@ -484,98 +513,78 @@ export class CoreToolScheduler {
484
513
  });
485
514
  }
486
515
  attemptExecutionOfScheduledCalls(signal) {
487
- // Execute all scheduled tools in the current batch
488
- const callsToExecute = this.toolCalls.filter((call) => call.status === 'scheduled');
489
- if (callsToExecute.length > 0) {
490
- this.isProcessingBatch = true;
516
+ const allCallsFinalOrScheduled = this.toolCalls.every((call) => call.status === 'scheduled' ||
517
+ call.status === 'cancelled' ||
518
+ call.status === 'success' ||
519
+ call.status === 'error');
520
+ if (allCallsFinalOrScheduled) {
521
+ const callsToExecute = this.toolCalls.filter((call) => call.status === 'scheduled');
522
+ callsToExecute.forEach((toolCall) => {
523
+ if (toolCall.status !== 'scheduled')
524
+ return;
525
+ const scheduledCall = toolCall;
526
+ const { callId, name: toolName } = scheduledCall.request;
527
+ const invocation = scheduledCall.invocation;
528
+ this.setStatusInternal(callId, 'executing');
529
+ const liveOutputCallback = scheduledCall.tool.canUpdateOutput && this.outputUpdateHandler
530
+ ? (outputChunk) => {
531
+ if (this.outputUpdateHandler) {
532
+ this.outputUpdateHandler(callId, outputChunk);
533
+ }
534
+ this.toolCalls = this.toolCalls.map((tc) => tc.request.callId === callId && tc.status === 'executing'
535
+ ? { ...tc, liveOutput: outputChunk }
536
+ : tc);
537
+ this.notifyToolCallsUpdate();
538
+ }
539
+ : undefined;
540
+ invocation
541
+ .execute(signal, liveOutputCallback)
542
+ .then(async (toolResult) => {
543
+ if (signal.aborted) {
544
+ this.setStatusInternal(callId, 'cancelled', 'User cancelled tool execution.');
545
+ return;
546
+ }
547
+ if (toolResult.error === undefined) {
548
+ const response = convertToFunctionResponse(toolName, callId, toolResult.llmContent);
549
+ // Return BOTH the tool call and response as an array
550
+ // This ensures they're always paired and added to history atomically
551
+ const responseParts = [
552
+ // First, the tool call
553
+ {
554
+ functionCall: {
555
+ id: callId,
556
+ name: toolName,
557
+ args: scheduledCall.request.args,
558
+ },
559
+ },
560
+ // Then, spread the response(s) since convertToFunctionResponse returns Part[]
561
+ ...response,
562
+ ];
563
+ const successResponse = {
564
+ callId,
565
+ responseParts,
566
+ resultDisplay: toolResult.returnDisplay,
567
+ error: undefined,
568
+ errorType: undefined,
569
+ };
570
+ this.setStatusInternal(callId, 'success', successResponse);
571
+ }
572
+ else {
573
+ // It is a failure
574
+ const error = new Error(toolResult.error.message);
575
+ const errorResponse = createErrorResponse(scheduledCall.request, error, toolResult.error.type);
576
+ this.setStatusInternal(callId, 'error', errorResponse);
577
+ }
578
+ })
579
+ .catch((executionError) => {
580
+ this.setStatusInternal(callId, 'error', createErrorResponse(scheduledCall.request, executionError instanceof Error
581
+ ? executionError
582
+ : new Error(String(executionError)), ToolErrorType.UNHANDLED_EXCEPTION));
583
+ });
584
+ });
491
585
  }
492
- callsToExecute.forEach((toolCall) => {
493
- const toolSignal = toolCall.signal ||
494
- signal ||
495
- new AbortController().signal;
496
- this.executeToolCall(toolCall, toolSignal);
497
- });
498
- }
499
- executeToolCall(toolCall, signal) {
500
- if (toolCall.status !== 'scheduled')
501
- return;
502
- const scheduledCall = toolCall;
503
- const { callId, name: toolName } = scheduledCall.request;
504
- const invocation = scheduledCall.invocation;
505
- this.setStatusInternal(callId, 'executing');
506
- // Start tracking the tool call execution
507
- const sessionId = typeof this.config.getSessionId === 'function'
508
- ? this.config.getSessionId()
509
- : 'default-session';
510
- const toolCallId = ToolCallTrackerService.startTrackingToolCall(sessionId, toolName, scheduledCall.request.args);
511
- const liveOutputCallback = scheduledCall.tool.canUpdateOutput && this.outputUpdateHandler
512
- ? (outputChunk) => {
513
- if (this.outputUpdateHandler) {
514
- this.outputUpdateHandler(callId, outputChunk);
515
- }
516
- this.toolCalls = this.toolCalls.map((tc) => tc.request.callId === callId && tc.status === 'executing'
517
- ? { ...tc, liveOutput: outputChunk }
518
- : tc);
519
- this.notifyToolCallsUpdate();
520
- }
521
- : undefined;
522
- invocation
523
- .execute(signal, liveOutputCallback)
524
- .then(async (toolResult) => {
525
- if (signal.aborted) {
526
- // Mark tool call as failed if aborted
527
- if (toolCallId) {
528
- ToolCallTrackerService.failToolCallTracking(sessionId, toolCallId);
529
- }
530
- this.setStatusInternal(callId, 'cancelled', 'User cancelled tool execution.');
531
- return;
532
- }
533
- // Mark tool call as completed
534
- if (toolCallId) {
535
- await ToolCallTrackerService.completeToolCallTracking(sessionId, toolCallId);
536
- }
537
- if (toolResult.error === undefined) {
538
- const functionResponse = convertToFunctionResponse(toolName, callId, toolResult.llmContent);
539
- // Return BOTH the tool call and response as an array
540
- const responseParts = [
541
- // First, the tool call
542
- {
543
- functionCall: {
544
- id: callId,
545
- name: toolName,
546
- args: scheduledCall.request.args,
547
- },
548
- },
549
- // Then, the response
550
- functionResponse,
551
- ];
552
- const successResponse = {
553
- callId,
554
- responseParts: responseParts,
555
- resultDisplay: toolResult.returnDisplay,
556
- error: undefined,
557
- errorType: undefined,
558
- };
559
- this.setStatusInternal(callId, 'success', successResponse);
560
- }
561
- else {
562
- // It is a failure
563
- const error = new Error(toolResult.error.message);
564
- const errorResponse = createErrorResponse(scheduledCall.request, error, toolResult.error.type);
565
- this.setStatusInternal(callId, 'error', errorResponse);
566
- }
567
- })
568
- .catch((executionError) => {
569
- // Mark tool call as failed on error
570
- if (toolCallId) {
571
- ToolCallTrackerService.failToolCallTracking(sessionId, toolCallId);
572
- }
573
- this.setStatusInternal(callId, 'error', createErrorResponse(scheduledCall.request, executionError instanceof Error
574
- ? executionError
575
- : new Error(String(executionError)), ToolErrorType.UNHANDLED_EXCEPTION));
576
- });
577
586
  }
578
- async checkAndNotifyCompletion(_toolJustCompleted = false) {
587
+ async checkAndNotifyCompletion() {
579
588
  const allCallsAreTerminal = this.toolCalls.every((call) => call.status === 'success' ||
580
589
  call.status === 'error' ||
581
590
  call.status === 'cancelled');
@@ -586,33 +595,18 @@ export class CoreToolScheduler {
586
595
  logToolCall(this.config, new ToolCallEvent(call));
587
596
  }
588
597
  if (this.onAllToolCallsComplete) {
598
+ this.isFinalizingToolCalls = true;
589
599
  await this.onAllToolCallsComplete(completedCalls);
600
+ this.isFinalizingToolCalls = false;
590
601
  }
591
602
  this.notifyToolCallsUpdate();
592
- // Batch is complete, process any queued requests
593
- this.isProcessingBatch = false;
594
- await this.processQueue();
595
- }
596
- }
597
- async processQueue() {
598
- if (this.pendingQueue.length === 0 || this.isProcessingBatch) {
599
- return;
600
- }
601
- // Process all queued requests as the next batch
602
- const queuedRequests = [...this.pendingQueue];
603
- this.pendingQueue = [];
604
- // Collect all requests and signals
605
- const allRequests = [];
606
- let commonSignal;
607
- queuedRequests.forEach((item) => {
608
- allRequests.push(item.request);
609
- if (!commonSignal && item.signal) {
610
- commonSignal = item.signal;
603
+ // After completion, process the next item in the queue.
604
+ if (this.requestQueue.length > 0) {
605
+ const next = this.requestQueue.shift();
606
+ this._schedule(next.request, next.signal)
607
+ .then(next.resolve)
608
+ .catch(next.reject);
611
609
  }
612
- });
613
- if (allRequests.length > 0) {
614
- // Schedule the entire batch at once
615
- await this.schedule(allRequests, commonSignal || new AbortController().signal);
616
610
  }
617
611
  }
618
612
  notifyToolCallsUpdate() {