@probelabs/probe 0.6.0-rc100

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 (115) hide show
  1. package/README.md +583 -0
  2. package/bin/.gitkeep +0 -0
  3. package/bin/probe +158 -0
  4. package/bin/probe-binary +0 -0
  5. package/build/agent/ProbeAgent.d.ts +199 -0
  6. package/build/agent/ProbeAgent.js +1486 -0
  7. package/build/agent/acp/README.md +347 -0
  8. package/build/agent/acp/connection.js +237 -0
  9. package/build/agent/acp/connection.test.js +311 -0
  10. package/build/agent/acp/examples/simple-client.js +212 -0
  11. package/build/agent/acp/examples/tool-lifecycle.js +230 -0
  12. package/build/agent/acp/final-test.js +173 -0
  13. package/build/agent/acp/index.js +5 -0
  14. package/build/agent/acp/integration.test.js +385 -0
  15. package/build/agent/acp/manual-test.js +410 -0
  16. package/build/agent/acp/protocol-test.js +190 -0
  17. package/build/agent/acp/server.js +448 -0
  18. package/build/agent/acp/server.test.js +371 -0
  19. package/build/agent/acp/test-runner.js +216 -0
  20. package/build/agent/acp/test-utils/README.md +315 -0
  21. package/build/agent/acp/test-utils/acp-tester.js +484 -0
  22. package/build/agent/acp/test-utils/mock-acp-client.js +434 -0
  23. package/build/agent/acp/tools.js +368 -0
  24. package/build/agent/acp/tools.test.js +334 -0
  25. package/build/agent/acp/types.js +218 -0
  26. package/build/agent/acp/types.test.js +327 -0
  27. package/build/agent/appTracer.js +360 -0
  28. package/build/agent/fileSpanExporter.js +169 -0
  29. package/build/agent/index.js +7426 -0
  30. package/build/agent/mcp/client.js +338 -0
  31. package/build/agent/mcp/config.js +313 -0
  32. package/build/agent/mcp/index.js +64 -0
  33. package/build/agent/mcp/xmlBridge.js +371 -0
  34. package/build/agent/mockProvider.js +53 -0
  35. package/build/agent/probeTool.js +257 -0
  36. package/build/agent/schemaUtils.js +1726 -0
  37. package/build/agent/simpleTelemetry.js +267 -0
  38. package/build/agent/telemetry.js +225 -0
  39. package/build/agent/tokenCounter.js +395 -0
  40. package/build/agent/tools.js +163 -0
  41. package/build/cli.js +49 -0
  42. package/build/delegate.js +267 -0
  43. package/build/directory-resolver.js +237 -0
  44. package/build/downloader.js +750 -0
  45. package/build/extract.js +149 -0
  46. package/build/index.js +70 -0
  47. package/build/mcp/index.js +514 -0
  48. package/build/mcp/index.ts +608 -0
  49. package/build/query.js +116 -0
  50. package/build/search.js +247 -0
  51. package/build/tools/common.js +410 -0
  52. package/build/tools/index.js +40 -0
  53. package/build/tools/langchain.js +88 -0
  54. package/build/tools/system-message.js +121 -0
  55. package/build/tools/vercel.js +271 -0
  56. package/build/utils/file-lister.js +193 -0
  57. package/build/utils.js +128 -0
  58. package/cjs/agent/ProbeAgent.cjs +5829 -0
  59. package/cjs/index.cjs +6217 -0
  60. package/cjs/package.json +3 -0
  61. package/index.d.ts +401 -0
  62. package/package.json +114 -0
  63. package/scripts/postinstall.js +172 -0
  64. package/src/agent/ProbeAgent.d.ts +199 -0
  65. package/src/agent/ProbeAgent.js +1486 -0
  66. package/src/agent/acp/README.md +347 -0
  67. package/src/agent/acp/connection.js +237 -0
  68. package/src/agent/acp/connection.test.js +311 -0
  69. package/src/agent/acp/examples/simple-client.js +212 -0
  70. package/src/agent/acp/examples/tool-lifecycle.js +230 -0
  71. package/src/agent/acp/final-test.js +173 -0
  72. package/src/agent/acp/index.js +5 -0
  73. package/src/agent/acp/integration.test.js +385 -0
  74. package/src/agent/acp/manual-test.js +410 -0
  75. package/src/agent/acp/protocol-test.js +190 -0
  76. package/src/agent/acp/server.js +448 -0
  77. package/src/agent/acp/server.test.js +371 -0
  78. package/src/agent/acp/test-runner.js +216 -0
  79. package/src/agent/acp/test-utils/README.md +315 -0
  80. package/src/agent/acp/test-utils/acp-tester.js +484 -0
  81. package/src/agent/acp/test-utils/mock-acp-client.js +434 -0
  82. package/src/agent/acp/tools.js +368 -0
  83. package/src/agent/acp/tools.test.js +334 -0
  84. package/src/agent/acp/types.js +218 -0
  85. package/src/agent/acp/types.test.js +327 -0
  86. package/src/agent/appTracer.js +360 -0
  87. package/src/agent/fileSpanExporter.js +169 -0
  88. package/src/agent/index.js +813 -0
  89. package/src/agent/mcp/client.js +338 -0
  90. package/src/agent/mcp/config.js +313 -0
  91. package/src/agent/mcp/index.js +64 -0
  92. package/src/agent/mcp/xmlBridge.js +371 -0
  93. package/src/agent/mockProvider.js +53 -0
  94. package/src/agent/probeTool.js +257 -0
  95. package/src/agent/schemaUtils.js +1726 -0
  96. package/src/agent/simpleTelemetry.js +267 -0
  97. package/src/agent/telemetry.js +225 -0
  98. package/src/agent/tokenCounter.js +395 -0
  99. package/src/agent/tools.js +163 -0
  100. package/src/cli.js +49 -0
  101. package/src/delegate.js +267 -0
  102. package/src/directory-resolver.js +237 -0
  103. package/src/downloader.js +750 -0
  104. package/src/extract.js +149 -0
  105. package/src/index.js +70 -0
  106. package/src/mcp/index.ts +608 -0
  107. package/src/query.js +116 -0
  108. package/src/search.js +247 -0
  109. package/src/tools/common.js +410 -0
  110. package/src/tools/index.js +40 -0
  111. package/src/tools/langchain.js +88 -0
  112. package/src/tools/system-message.js +121 -0
  113. package/src/tools/vercel.js +271 -0
  114. package/src/utils/file-lister.js +193 -0
  115. package/src/utils.js +128 -0
@@ -0,0 +1,368 @@
1
+ // ACP Tool Integration - Maps probe tools to ACP tool format
2
+ import { randomUUID } from 'crypto';
3
+ import {
4
+ ToolCallStatus,
5
+ ToolCallKind,
6
+ createTextContent,
7
+ createToolCallProgress
8
+ } from './types.js';
9
+
10
+ /**
11
+ * ACP Tool Call represents a tool execution instance
12
+ */
13
+ export class ACPToolCall {
14
+ constructor(id, name, kind, params, sessionId) {
15
+ this.id = id;
16
+ this.name = name;
17
+ this.kind = kind;
18
+ this.params = params;
19
+ this.sessionId = sessionId;
20
+ this.status = ToolCallStatus.PENDING;
21
+ this.startTime = Date.now();
22
+ this.endTime = null;
23
+ this.result = null;
24
+ this.error = null;
25
+ }
26
+
27
+ /**
28
+ * Update tool call status
29
+ */
30
+ updateStatus(status, result = null, error = null) {
31
+ this.status = status;
32
+ this.result = result;
33
+ this.error = error;
34
+
35
+ if (status === ToolCallStatus.COMPLETED || status === ToolCallStatus.FAILED) {
36
+ this.endTime = Date.now();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get execution duration in ms
42
+ */
43
+ getDuration() {
44
+ const end = this.endTime || Date.now();
45
+ return end - this.startTime;
46
+ }
47
+
48
+ /**
49
+ * Serialize to JSON
50
+ */
51
+ toJSON() {
52
+ return {
53
+ id: this.id,
54
+ name: this.name,
55
+ kind: this.kind,
56
+ params: this.params,
57
+ sessionId: this.sessionId,
58
+ status: this.status,
59
+ startTime: this.startTime,
60
+ endTime: this.endTime,
61
+ duration: this.getDuration(),
62
+ result: this.result,
63
+ error: this.error
64
+ };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * ACP Tool Manager - manages tool execution and lifecycle
70
+ */
71
+ export class ACPToolManager {
72
+ constructor(server, probeAgent) {
73
+ this.server = server;
74
+ this.probeAgent = probeAgent;
75
+ this.activeCalls = new Map();
76
+ this.debug = server.options.debug;
77
+ }
78
+
79
+ /**
80
+ * Execute a tool with ACP lifecycle tracking
81
+ */
82
+ async executeToolCall(sessionId, toolName, params) {
83
+ const toolCallId = randomUUID();
84
+ const kind = this.getToolKind(toolName);
85
+
86
+ const toolCall = new ACPToolCall(toolCallId, toolName, kind, params, sessionId);
87
+ this.activeCalls.set(toolCallId, toolCall);
88
+
89
+ if (this.debug) {
90
+ console.error(`[ACP] Starting tool call: ${toolName} (${toolCallId})`);
91
+ }
92
+
93
+ // Send pending notification
94
+ this.server.sendToolCallProgress(
95
+ sessionId,
96
+ toolCallId,
97
+ ToolCallStatus.PENDING
98
+ );
99
+
100
+ try {
101
+ // Update to in progress
102
+ toolCall.updateStatus(ToolCallStatus.IN_PROGRESS);
103
+ this.server.sendToolCallProgress(
104
+ sessionId,
105
+ toolCallId,
106
+ ToolCallStatus.IN_PROGRESS
107
+ );
108
+
109
+ // Execute the actual tool
110
+ const result = await this.executeProbeTool(toolName, params);
111
+
112
+ // Update to completed
113
+ toolCall.updateStatus(ToolCallStatus.COMPLETED, result);
114
+ this.server.sendToolCallProgress(
115
+ sessionId,
116
+ toolCallId,
117
+ ToolCallStatus.COMPLETED,
118
+ result
119
+ );
120
+
121
+ if (this.debug) {
122
+ console.error(`[ACP] Tool call completed: ${toolName} (${toolCall.getDuration()}ms)`);
123
+ }
124
+
125
+ return result;
126
+
127
+ } catch (error) {
128
+ // Update to failed
129
+ toolCall.updateStatus(ToolCallStatus.FAILED, null, error.message);
130
+ this.server.sendToolCallProgress(
131
+ sessionId,
132
+ toolCallId,
133
+ ToolCallStatus.FAILED,
134
+ null,
135
+ error.message
136
+ );
137
+
138
+ if (this.debug) {
139
+ console.error(`[ACP] Tool call failed: ${toolName}`, error);
140
+ }
141
+
142
+ throw error;
143
+
144
+ } finally {
145
+ // Clean up completed calls after a delay
146
+ setTimeout(() => {
147
+ this.activeCalls.delete(toolCallId);
148
+ }, 30000); // Keep for 30 seconds for status queries
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Get tool kind based on tool name
154
+ */
155
+ getToolKind(toolName) {
156
+ switch (toolName) {
157
+ case 'search':
158
+ return ToolCallKind.search;
159
+ case 'query':
160
+ return ToolCallKind.query;
161
+ case 'extract':
162
+ return ToolCallKind.extract;
163
+ case 'delegate':
164
+ return ToolCallKind.execute;
165
+ case 'implement':
166
+ return ToolCallKind.edit;
167
+ default:
168
+ return ToolCallKind.execute;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Execute a probe tool
174
+ */
175
+ async executeProbeTool(toolName, params) {
176
+ // Get the tool from the probe agent
177
+ const tools = this.probeAgent.wrappedTools;
178
+
179
+ switch (toolName) {
180
+ case 'search':
181
+ if (!tools.searchToolInstance) {
182
+ throw new Error('Search tool not available');
183
+ }
184
+ return await tools.searchToolInstance.execute({
185
+ ...params,
186
+ sessionId: this.probeAgent.sessionId
187
+ });
188
+
189
+ case 'query':
190
+ if (!tools.queryToolInstance) {
191
+ throw new Error('Query tool not available');
192
+ }
193
+ return await tools.queryToolInstance.execute({
194
+ ...params,
195
+ sessionId: this.probeAgent.sessionId
196
+ });
197
+
198
+ case 'extract':
199
+ if (!tools.extractToolInstance) {
200
+ throw new Error('Extract tool not available');
201
+ }
202
+ return await tools.extractToolInstance.execute({
203
+ ...params,
204
+ sessionId: this.probeAgent.sessionId
205
+ });
206
+
207
+ case 'delegate':
208
+ if (!tools.delegateToolInstance) {
209
+ throw new Error('Delegate tool not available');
210
+ }
211
+ return await tools.delegateToolInstance.execute({
212
+ ...params,
213
+ sessionId: this.probeAgent.sessionId
214
+ });
215
+
216
+ default:
217
+ throw new Error(`Unknown tool: ${toolName}`);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Get tool call status
223
+ */
224
+ getToolCallStatus(toolCallId) {
225
+ const toolCall = this.activeCalls.get(toolCallId);
226
+ return toolCall ? toolCall.toJSON() : null;
227
+ }
228
+
229
+ /**
230
+ * Get all active tool calls for a session
231
+ */
232
+ getActiveToolCalls(sessionId) {
233
+ const calls = [];
234
+ for (const toolCall of this.activeCalls.values()) {
235
+ if (toolCall.sessionId === sessionId) {
236
+ calls.push(toolCall.toJSON());
237
+ }
238
+ }
239
+ return calls;
240
+ }
241
+
242
+ /**
243
+ * Cancel all tool calls for a session
244
+ */
245
+ cancelSessionToolCalls(sessionId) {
246
+ for (const [id, toolCall] of this.activeCalls) {
247
+ if (toolCall.sessionId === sessionId &&
248
+ (toolCall.status === ToolCallStatus.PENDING ||
249
+ toolCall.status === ToolCallStatus.IN_PROGRESS)) {
250
+
251
+ toolCall.updateStatus(ToolCallStatus.FAILED, null, 'Cancelled');
252
+ this.server.sendToolCallProgress(
253
+ sessionId,
254
+ id,
255
+ ToolCallStatus.FAILED,
256
+ null,
257
+ 'Cancelled'
258
+ );
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Get tool definitions for capabilities
265
+ */
266
+ static getToolDefinitions() {
267
+ return [
268
+ {
269
+ name: 'search',
270
+ description: 'Search for code patterns and content using flexible text search with stemming and stopword removal. Supports regex patterns and elastic search query syntax.',
271
+ kind: ToolCallKind.search,
272
+ inputSchema: {
273
+ type: 'object',
274
+ properties: {
275
+ query: {
276
+ type: 'string',
277
+ description: 'Search query using elastic search syntax. Supports logical operators (AND, OR, NOT), quotes for exact matches, field specifiers, and regex patterns.'
278
+ },
279
+ path: {
280
+ type: 'string',
281
+ description: 'Directory to search in (defaults to current working directory)'
282
+ },
283
+ max_results: {
284
+ type: 'number',
285
+ description: 'Maximum number of results to return (default: 10)'
286
+ },
287
+ allow_tests: {
288
+ type: 'boolean',
289
+ description: 'Include test files in results (default: false)'
290
+ }
291
+ },
292
+ required: ['query']
293
+ }
294
+ },
295
+ {
296
+ name: 'query',
297
+ description: 'Perform structural queries using AST patterns to find specific code structures like functions, classes, or methods.',
298
+ kind: ToolCallKind.query,
299
+ inputSchema: {
300
+ type: 'object',
301
+ properties: {
302
+ pattern: {
303
+ type: 'string',
304
+ description: 'AST-grep pattern to search for. Examples: "fn $NAME($$$PARAMS) $$$BODY" for Rust functions, "def $NAME($$$PARAMS): $$$BODY" for Python functions.'
305
+ },
306
+ path: {
307
+ type: 'string',
308
+ description: 'Directory to search in (defaults to current working directory)'
309
+ },
310
+ language: {
311
+ type: 'string',
312
+ description: 'Programming language to search in (rust, javascript, python, go, etc.)'
313
+ },
314
+ max_results: {
315
+ type: 'number',
316
+ description: 'Maximum number of results to return (default: 10)'
317
+ }
318
+ },
319
+ required: ['pattern']
320
+ }
321
+ },
322
+ {
323
+ name: 'extract',
324
+ description: 'Extract specific code blocks from files based on file paths and optional line numbers.',
325
+ kind: ToolCallKind.extract,
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: {
329
+ files: {
330
+ type: 'array',
331
+ items: { type: 'string' },
332
+ description: 'Array of file paths or file:line specifications to extract from'
333
+ },
334
+ context_lines: {
335
+ type: 'number',
336
+ description: 'Number of context lines to include before and after (default: 0)'
337
+ },
338
+ allow_tests: {
339
+ type: 'boolean',
340
+ description: 'Allow test files in results (default: false)'
341
+ },
342
+ format: {
343
+ type: 'string',
344
+ enum: ['plain', 'markdown', 'json'],
345
+ description: 'Output format (default: markdown)'
346
+ }
347
+ },
348
+ required: ['files']
349
+ }
350
+ },
351
+ {
352
+ name: 'delegate',
353
+ description: 'Automatically delegate big distinct tasks to specialized probe subagents within the agentic loop. Use when complex requests can be broken into focused, parallel tasks.',
354
+ kind: ToolCallKind.execute,
355
+ inputSchema: {
356
+ type: 'object',
357
+ properties: {
358
+ task: {
359
+ type: 'string',
360
+ description: 'A complete, self-contained task that can be executed independently by a subagent. Should be specific and focused on one area of expertise.'
361
+ }
362
+ },
363
+ required: ['task']
364
+ }
365
+ }
366
+ ];
367
+ }
368
+ }
@@ -0,0 +1,334 @@
1
+ // Tests for ACP Tool Manager and Tool Calls
2
+ import { jest } from '@jest/globals';
3
+ import { ACPToolCall, ACPToolManager } from './tools.js';
4
+ import { ToolCallStatus, ToolCallKind } from './types.js';
5
+
6
+ describe('ACPToolCall', () => {
7
+ test('should create tool call with correct initial state', () => {
8
+ const toolCall = new ACPToolCall(
9
+ 'test-id',
10
+ 'search',
11
+ ToolCallKind.search,
12
+ { query: 'test' },
13
+ 'session-123'
14
+ );
15
+
16
+ expect(toolCall.id).toBe('test-id');
17
+ expect(toolCall.name).toBe('search');
18
+ expect(toolCall.kind).toBe(ToolCallKind.search);
19
+ expect(toolCall.params).toEqual({ query: 'test' });
20
+ expect(toolCall.sessionId).toBe('session-123');
21
+ expect(toolCall.status).toBe(ToolCallStatus.PENDING);
22
+ expect(toolCall.startTime).toBeLessThanOrEqual(Date.now());
23
+ expect(toolCall.endTime).toBeNull();
24
+ expect(toolCall.result).toBeNull();
25
+ expect(toolCall.error).toBeNull();
26
+ });
27
+
28
+ test('should update status correctly', async () => {
29
+ const toolCall = new ACPToolCall('id', 'test', 'kind', {}, 'session');
30
+ const result = { data: 'test result' };
31
+
32
+ toolCall.updateStatus(ToolCallStatus.IN_PROGRESS);
33
+ expect(toolCall.status).toBe(ToolCallStatus.IN_PROGRESS);
34
+ expect(toolCall.endTime).toBeNull();
35
+
36
+ // Add a small delay to ensure timing difference
37
+ await new Promise(resolve => setTimeout(resolve, 1));
38
+
39
+ toolCall.updateStatus(ToolCallStatus.COMPLETED, result);
40
+ expect(toolCall.status).toBe(ToolCallStatus.COMPLETED);
41
+ expect(toolCall.result).toBe(result);
42
+ expect(toolCall.endTime).toBeGreaterThanOrEqual(toolCall.startTime);
43
+ });
44
+
45
+ test('should calculate duration correctly', (done) => {
46
+ const toolCall = new ACPToolCall('id', 'test', 'kind', {}, 'session');
47
+
48
+ setTimeout(() => {
49
+ const duration = toolCall.getDuration();
50
+ expect(duration).toBeGreaterThan(0);
51
+ expect(duration).toBeLessThan(100); // Should be very small
52
+ done();
53
+ }, 10);
54
+ });
55
+
56
+ test('should serialize to JSON correctly', () => {
57
+ const toolCall = new ACPToolCall(
58
+ 'test-id',
59
+ 'search',
60
+ ToolCallKind.search,
61
+ { query: 'test' },
62
+ 'session-123'
63
+ );
64
+ toolCall.updateStatus(ToolCallStatus.COMPLETED, { found: 5 });
65
+
66
+ const json = toolCall.toJSON();
67
+
68
+ expect(json).toEqual({
69
+ id: 'test-id',
70
+ name: 'search',
71
+ kind: ToolCallKind.search,
72
+ params: { query: 'test' },
73
+ sessionId: 'session-123',
74
+ status: ToolCallStatus.COMPLETED,
75
+ startTime: toolCall.startTime,
76
+ endTime: toolCall.endTime,
77
+ duration: toolCall.getDuration(),
78
+ result: { found: 5 },
79
+ error: null
80
+ });
81
+ });
82
+ });
83
+
84
+ describe('ACPToolManager', () => {
85
+ let mockServer, mockProbeAgent, toolManager;
86
+
87
+ beforeEach(() => {
88
+ mockServer = {
89
+ options: { debug: true },
90
+ sendToolCallProgress: jest.fn()
91
+ };
92
+
93
+ mockProbeAgent = {
94
+ sessionId: 'test-session',
95
+ wrappedTools: {
96
+ searchToolInstance: {
97
+ execute: jest.fn().mockResolvedValue('search result')
98
+ },
99
+ queryToolInstance: {
100
+ execute: jest.fn().mockResolvedValue('query result')
101
+ },
102
+ extractToolInstance: {
103
+ execute: jest.fn().mockResolvedValue('extract result')
104
+ },
105
+ delegateToolInstance: {
106
+ execute: jest.fn().mockResolvedValue('delegate result')
107
+ }
108
+ }
109
+ };
110
+
111
+ toolManager = new ACPToolManager(mockServer, mockProbeAgent);
112
+ });
113
+
114
+ describe('tool kind mapping', () => {
115
+ test('should map tool names to correct kinds', () => {
116
+ expect(toolManager.getToolKind('search')).toBe(ToolCallKind.search);
117
+ expect(toolManager.getToolKind('query')).toBe(ToolCallKind.query);
118
+ expect(toolManager.getToolKind('extract')).toBe(ToolCallKind.extract);
119
+ expect(toolManager.getToolKind('delegate')).toBe(ToolCallKind.execute);
120
+ expect(toolManager.getToolKind('implement')).toBe(ToolCallKind.edit);
121
+ expect(toolManager.getToolKind('unknown')).toBe(ToolCallKind.execute);
122
+ });
123
+ });
124
+
125
+ describe('tool execution', () => {
126
+ test('should execute search tool successfully', async () => {
127
+ const params = { query: 'test search', path: '/test' };
128
+
129
+ const result = await toolManager.executeToolCall('session-123', 'search', params);
130
+
131
+ expect(result).toBe('search result');
132
+ expect(mockProbeAgent.wrappedTools.searchToolInstance.execute).toHaveBeenCalledWith({
133
+ ...params,
134
+ sessionId: 'test-session'
135
+ });
136
+
137
+ // Should send progress notifications
138
+ expect(mockServer.sendToolCallProgress).toHaveBeenCalledTimes(3);
139
+ expect(mockServer.sendToolCallProgress).toHaveBeenNthCalledWith(
140
+ 1, 'session-123', expect.any(String), ToolCallStatus.PENDING
141
+ );
142
+ expect(mockServer.sendToolCallProgress).toHaveBeenNthCalledWith(
143
+ 2, 'session-123', expect.any(String), ToolCallStatus.IN_PROGRESS
144
+ );
145
+ expect(mockServer.sendToolCallProgress).toHaveBeenNthCalledWith(
146
+ 3, 'session-123', expect.any(String), ToolCallStatus.COMPLETED, 'search result'
147
+ );
148
+ });
149
+
150
+ test('should execute query tool successfully', async () => {
151
+ const params = { pattern: 'fn $NAME($$$)', language: 'rust' };
152
+
153
+ const result = await toolManager.executeToolCall('session-123', 'query', params);
154
+
155
+ expect(result).toBe('query result');
156
+ expect(mockProbeAgent.wrappedTools.queryToolInstance.execute).toHaveBeenCalledWith({
157
+ ...params,
158
+ sessionId: 'test-session'
159
+ });
160
+ });
161
+
162
+ test('should execute extract tool successfully', async () => {
163
+ const params = { files: ['src/main.rs:10'], context_lines: 5 };
164
+
165
+ const result = await toolManager.executeToolCall('session-123', 'extract', params);
166
+
167
+ expect(result).toBe('extract result');
168
+ expect(mockProbeAgent.wrappedTools.extractToolInstance.execute).toHaveBeenCalledWith({
169
+ ...params,
170
+ sessionId: 'test-session'
171
+ });
172
+ });
173
+
174
+ test('should execute delegate tool successfully', async () => {
175
+ const params = { task: 'Analyze security vulnerabilities in authentication code' };
176
+
177
+ const result = await toolManager.executeToolCall('session-123', 'delegate', params);
178
+
179
+ expect(result).toBe('delegate result');
180
+ expect(mockProbeAgent.wrappedTools.delegateToolInstance.execute).toHaveBeenCalledWith({
181
+ ...params,
182
+ sessionId: 'test-session'
183
+ });
184
+ });
185
+
186
+ test('should handle tool execution errors', async () => {
187
+ const error = new Error('Tool execution failed');
188
+ mockProbeAgent.wrappedTools.searchToolInstance.execute.mockRejectedValue(error);
189
+
190
+ await expect(toolManager.executeToolCall('session-123', 'search', {})).rejects.toThrow(error);
191
+
192
+ // Should send error notification
193
+ expect(mockServer.sendToolCallProgress).toHaveBeenCalledWith(
194
+ 'session-123', expect.any(String), ToolCallStatus.FAILED, null, 'Tool execution failed'
195
+ );
196
+ });
197
+
198
+ test('should handle unknown tools', async () => {
199
+ await expect(toolManager.executeToolCall('session-123', 'unknown', {})).rejects.toThrow('Unknown tool: unknown');
200
+ });
201
+
202
+ test('should handle missing tool instances', async () => {
203
+ mockProbeAgent.wrappedTools.searchToolInstance = null;
204
+
205
+ await expect(toolManager.executeToolCall('session-123', 'search', {})).rejects.toThrow('Search tool not available');
206
+
207
+ mockProbeAgent.wrappedTools.delegateToolInstance = null;
208
+
209
+ await expect(toolManager.executeToolCall('session-123', 'delegate', {})).rejects.toThrow('Delegate tool not available');
210
+ });
211
+ });
212
+
213
+ describe('tool call tracking', () => {
214
+ test('should track active tool calls', async () => {
215
+ expect(toolManager.activeCalls.size).toBe(0);
216
+
217
+ const promise = toolManager.executeToolCall('session-123', 'search', { query: 'test' });
218
+
219
+ // Should have active call during execution
220
+ expect(toolManager.activeCalls.size).toBe(1);
221
+
222
+ await promise;
223
+
224
+ // Should still have the call (cleaned up after timeout)
225
+ expect(toolManager.activeCalls.size).toBe(1);
226
+ });
227
+
228
+ test('should get tool call status', async () => {
229
+ const promise = toolManager.executeToolCall('session-123', 'search', { query: 'test' });
230
+
231
+ // Get the tool call ID from the active calls
232
+ const toolCallId = Array.from(toolManager.activeCalls.keys())[0];
233
+
234
+ const status = toolManager.getToolCallStatus(toolCallId);
235
+ expect(status).toBeDefined();
236
+ expect(status.name).toBe('search');
237
+ expect(status.sessionId).toBe('session-123');
238
+
239
+ await promise;
240
+
241
+ const completedStatus = toolManager.getToolCallStatus(toolCallId);
242
+ expect(completedStatus.status).toBe(ToolCallStatus.COMPLETED);
243
+ });
244
+
245
+ test('should get active tool calls for session', async () => {
246
+ await toolManager.executeToolCall('session-123', 'search', { query: 'test1' });
247
+ await toolManager.executeToolCall('session-123', 'query', { pattern: 'test' });
248
+ await toolManager.executeToolCall('session-456', 'extract', { files: ['test.rs'] });
249
+
250
+ const session123Calls = toolManager.getActiveToolCalls('session-123');
251
+ const session456Calls = toolManager.getActiveToolCalls('session-456');
252
+
253
+ expect(session123Calls).toHaveLength(2);
254
+ expect(session456Calls).toHaveLength(1);
255
+
256
+ expect(session123Calls[0].name).toBe('search');
257
+ expect(session123Calls[1].name).toBe('query');
258
+ expect(session456Calls[0].name).toBe('extract');
259
+ });
260
+
261
+ test('should cancel session tool calls', () => {
262
+ // Start some tool calls without awaiting
263
+ toolManager.executeToolCall('session-123', 'search', { query: 'test1' });
264
+ toolManager.executeToolCall('session-123', 'query', { pattern: 'test' });
265
+ toolManager.executeToolCall('session-456', 'extract', { files: ['test.rs'] });
266
+
267
+ // Cancel session 123 calls
268
+ toolManager.cancelSessionToolCalls('session-123');
269
+
270
+ // Should send cancellation notifications
271
+ expect(mockServer.sendToolCallProgress).toHaveBeenCalledWith(
272
+ 'session-123', expect.any(String), ToolCallStatus.FAILED, null, 'Cancelled'
273
+ );
274
+
275
+ // Session 456 calls should not be affected
276
+ const session456Calls = toolManager.getActiveToolCalls('session-456');
277
+ expect(session456Calls).toHaveLength(1);
278
+ expect(session456Calls[0].status).not.toBe(ToolCallStatus.FAILED);
279
+ });
280
+ });
281
+
282
+ describe('tool definitions', () => {
283
+ test('should provide correct tool definitions', () => {
284
+ const definitions = ACPToolManager.getToolDefinitions();
285
+
286
+ expect(definitions).toHaveLength(4);
287
+
288
+ const searchTool = definitions.find(d => d.name === 'search');
289
+ expect(searchTool).toBeDefined();
290
+ expect(searchTool.kind).toBe(ToolCallKind.search);
291
+ expect(searchTool.inputSchema.properties.query).toBeDefined();
292
+ expect(searchTool.inputSchema.required).toContain('query');
293
+
294
+ const queryTool = definitions.find(d => d.name === 'query');
295
+ expect(queryTool).toBeDefined();
296
+ expect(queryTool.kind).toBe(ToolCallKind.query);
297
+ expect(queryTool.inputSchema.properties.pattern).toBeDefined();
298
+ expect(queryTool.inputSchema.required).toContain('pattern');
299
+
300
+ const extractTool = definitions.find(d => d.name === 'extract');
301
+ expect(extractTool).toBeDefined();
302
+ expect(extractTool.kind).toBe(ToolCallKind.extract);
303
+ expect(extractTool.inputSchema.properties.files).toBeDefined();
304
+ expect(extractTool.inputSchema.required).toContain('files');
305
+
306
+ const delegateTool = definitions.find(d => d.name === 'delegate');
307
+ expect(delegateTool).toBeDefined();
308
+ expect(delegateTool.kind).toBe(ToolCallKind.execute);
309
+ expect(delegateTool.inputSchema.properties.task).toBeDefined();
310
+ expect(delegateTool.inputSchema.required).toContain('task');
311
+ });
312
+ });
313
+
314
+ describe('cleanup', () => {
315
+ beforeEach(() => {
316
+ jest.useFakeTimers();
317
+ });
318
+
319
+ afterEach(() => {
320
+ jest.useRealTimers();
321
+ });
322
+
323
+ test('should clean up completed tool calls after timeout', async () => {
324
+ await toolManager.executeToolCall('session-123', 'search', { query: 'test' });
325
+
326
+ expect(toolManager.activeCalls.size).toBe(1);
327
+
328
+ // Fast-forward time by 30 seconds
329
+ jest.advanceTimersByTime(30000);
330
+
331
+ expect(toolManager.activeCalls.size).toBe(0);
332
+ });
333
+ });
334
+ });