@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.
- package/README.md +583 -0
- package/bin/.gitkeep +0 -0
- package/bin/probe +158 -0
- package/bin/probe-binary +0 -0
- package/build/agent/ProbeAgent.d.ts +199 -0
- package/build/agent/ProbeAgent.js +1486 -0
- package/build/agent/acp/README.md +347 -0
- package/build/agent/acp/connection.js +237 -0
- package/build/agent/acp/connection.test.js +311 -0
- package/build/agent/acp/examples/simple-client.js +212 -0
- package/build/agent/acp/examples/tool-lifecycle.js +230 -0
- package/build/agent/acp/final-test.js +173 -0
- package/build/agent/acp/index.js +5 -0
- package/build/agent/acp/integration.test.js +385 -0
- package/build/agent/acp/manual-test.js +410 -0
- package/build/agent/acp/protocol-test.js +190 -0
- package/build/agent/acp/server.js +448 -0
- package/build/agent/acp/server.test.js +371 -0
- package/build/agent/acp/test-runner.js +216 -0
- package/build/agent/acp/test-utils/README.md +315 -0
- package/build/agent/acp/test-utils/acp-tester.js +484 -0
- package/build/agent/acp/test-utils/mock-acp-client.js +434 -0
- package/build/agent/acp/tools.js +368 -0
- package/build/agent/acp/tools.test.js +334 -0
- package/build/agent/acp/types.js +218 -0
- package/build/agent/acp/types.test.js +327 -0
- package/build/agent/appTracer.js +360 -0
- package/build/agent/fileSpanExporter.js +169 -0
- package/build/agent/index.js +7426 -0
- package/build/agent/mcp/client.js +338 -0
- package/build/agent/mcp/config.js +313 -0
- package/build/agent/mcp/index.js +64 -0
- package/build/agent/mcp/xmlBridge.js +371 -0
- package/build/agent/mockProvider.js +53 -0
- package/build/agent/probeTool.js +257 -0
- package/build/agent/schemaUtils.js +1726 -0
- package/build/agent/simpleTelemetry.js +267 -0
- package/build/agent/telemetry.js +225 -0
- package/build/agent/tokenCounter.js +395 -0
- package/build/agent/tools.js +163 -0
- package/build/cli.js +49 -0
- package/build/delegate.js +267 -0
- package/build/directory-resolver.js +237 -0
- package/build/downloader.js +750 -0
- package/build/extract.js +149 -0
- package/build/index.js +70 -0
- package/build/mcp/index.js +514 -0
- package/build/mcp/index.ts +608 -0
- package/build/query.js +116 -0
- package/build/search.js +247 -0
- package/build/tools/common.js +410 -0
- package/build/tools/index.js +40 -0
- package/build/tools/langchain.js +88 -0
- package/build/tools/system-message.js +121 -0
- package/build/tools/vercel.js +271 -0
- package/build/utils/file-lister.js +193 -0
- package/build/utils.js +128 -0
- package/cjs/agent/ProbeAgent.cjs +5829 -0
- package/cjs/index.cjs +6217 -0
- package/cjs/package.json +3 -0
- package/index.d.ts +401 -0
- package/package.json +114 -0
- package/scripts/postinstall.js +172 -0
- package/src/agent/ProbeAgent.d.ts +199 -0
- package/src/agent/ProbeAgent.js +1486 -0
- package/src/agent/acp/README.md +347 -0
- package/src/agent/acp/connection.js +237 -0
- package/src/agent/acp/connection.test.js +311 -0
- package/src/agent/acp/examples/simple-client.js +212 -0
- package/src/agent/acp/examples/tool-lifecycle.js +230 -0
- package/src/agent/acp/final-test.js +173 -0
- package/src/agent/acp/index.js +5 -0
- package/src/agent/acp/integration.test.js +385 -0
- package/src/agent/acp/manual-test.js +410 -0
- package/src/agent/acp/protocol-test.js +190 -0
- package/src/agent/acp/server.js +448 -0
- package/src/agent/acp/server.test.js +371 -0
- package/src/agent/acp/test-runner.js +216 -0
- package/src/agent/acp/test-utils/README.md +315 -0
- package/src/agent/acp/test-utils/acp-tester.js +484 -0
- package/src/agent/acp/test-utils/mock-acp-client.js +434 -0
- package/src/agent/acp/tools.js +368 -0
- package/src/agent/acp/tools.test.js +334 -0
- package/src/agent/acp/types.js +218 -0
- package/src/agent/acp/types.test.js +327 -0
- package/src/agent/appTracer.js +360 -0
- package/src/agent/fileSpanExporter.js +169 -0
- package/src/agent/index.js +813 -0
- package/src/agent/mcp/client.js +338 -0
- package/src/agent/mcp/config.js +313 -0
- package/src/agent/mcp/index.js +64 -0
- package/src/agent/mcp/xmlBridge.js +371 -0
- package/src/agent/mockProvider.js +53 -0
- package/src/agent/probeTool.js +257 -0
- package/src/agent/schemaUtils.js +1726 -0
- package/src/agent/simpleTelemetry.js +267 -0
- package/src/agent/telemetry.js +225 -0
- package/src/agent/tokenCounter.js +395 -0
- package/src/agent/tools.js +163 -0
- package/src/cli.js +49 -0
- package/src/delegate.js +267 -0
- package/src/directory-resolver.js +237 -0
- package/src/downloader.js +750 -0
- package/src/extract.js +149 -0
- package/src/index.js +70 -0
- package/src/mcp/index.ts +608 -0
- package/src/query.js +116 -0
- package/src/search.js +247 -0
- package/src/tools/common.js +410 -0
- package/src/tools/index.js +40 -0
- package/src/tools/langchain.js +88 -0
- package/src/tools/system-message.js +121 -0
- package/src/tools/vercel.js +271 -0
- package/src/utils/file-lister.js +193 -0
- 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
|
+
});
|