@probelabs/probe 0.6.0-rc224 → 0.6.0-rc226
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/bin/binaries/probe-v0.6.0-rc226-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc226-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +361 -36
- package/build/agent/index.js +570 -57
- package/build/agent/mcp/xmlBridge.js +10 -7
- package/build/agent/simpleTelemetry.js +198 -0
- package/build/agent/tools.js +8 -5
- package/build/tools/analyzeAll.js +6 -1
- package/build/tools/bash.js +18 -3
- package/build/tools/edit.js +19 -10
- package/build/tools/vercel.js +17 -7
- package/build/utils/path-validation.js +148 -1
- package/cjs/agent/ProbeAgent.cjs +392 -56
- package/cjs/agent/simpleTelemetry.cjs +177 -0
- package/cjs/index.cjs +569 -56
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +361 -36
- package/src/agent/mcp/xmlBridge.js +10 -7
- package/src/agent/simpleTelemetry.js +198 -0
- package/src/agent/tools.js +8 -5
- package/src/tools/analyzeAll.js +6 -1
- package/src/tools/bash.js +18 -3
- package/src/tools/edit.js +19 -10
- package/src/tools/vercel.js +17 -7
- package/src/utils/path-validation.js +148 -1
- package/bin/binaries/probe-v0.6.0-rc224-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc224-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc224-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc224-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc224-x86_64-unknown-linux-musl.tar.gz +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -71,6 +71,7 @@ import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
|
|
|
71
71
|
import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
|
|
72
72
|
import { handleContextLimitError } from './contextCompactor.js';
|
|
73
73
|
import { formatErrorForAI, ParameterError } from '../utils/error-types.js';
|
|
74
|
+
import { getCommonPrefix, toRelativePath, safeRealpath } from '../utils/path-validation.js';
|
|
74
75
|
import { truncateIfNeeded, getMaxOutputTokens } from './outputTruncator.js';
|
|
75
76
|
import { DelegationManager } from '../delegate.js';
|
|
76
77
|
import {
|
|
@@ -269,8 +270,15 @@ export class ProbeAgent {
|
|
|
269
270
|
this.allowedFolders = [process.cwd()];
|
|
270
271
|
}
|
|
271
272
|
|
|
272
|
-
//
|
|
273
|
-
|
|
273
|
+
// Compute workspace root as common prefix of all allowed folders
|
|
274
|
+
// This provides a single "root" for relative path resolution and default cwd
|
|
275
|
+
// IMPORTANT: workspaceRoot is NOT a security boundary - all security checks
|
|
276
|
+
// must be performed against this.allowedFolders, not workspaceRoot
|
|
277
|
+
this.workspaceRoot = getCommonPrefix(this.allowedFolders);
|
|
278
|
+
|
|
279
|
+
// Working directory for resolving relative paths
|
|
280
|
+
// If not explicitly provided, use workspace root for consistency
|
|
281
|
+
this.cwd = options.cwd || this.workspaceRoot;
|
|
274
282
|
|
|
275
283
|
// API configuration
|
|
276
284
|
this.clientApiProvider = options.provider || null;
|
|
@@ -289,6 +297,8 @@ export class ProbeAgent {
|
|
|
289
297
|
console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
|
|
290
298
|
console.log(`[DEBUG] Allow Edit (implement tool): ${this.allowEdit}`);
|
|
291
299
|
console.log(`[DEBUG] Search delegation enabled: ${this.searchDelegate}`);
|
|
300
|
+
console.log(`[DEBUG] Workspace root: ${this.workspaceRoot}`);
|
|
301
|
+
console.log(`[DEBUG] Working directory (cwd): ${this.cwd}`);
|
|
292
302
|
}
|
|
293
303
|
|
|
294
304
|
// Initialize tools
|
|
@@ -406,6 +416,209 @@ export class ProbeAgent {
|
|
|
406
416
|
return mcpToolNames.filter(toolName => this._isMcpToolAllowed(toolName));
|
|
407
417
|
}
|
|
408
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Check if tracer is AppTracer (expects sessionId as first param) vs SimpleAppTracer
|
|
421
|
+
* @returns {boolean} - True if tracer is AppTracer style (requires sessionId)
|
|
422
|
+
* @private
|
|
423
|
+
*/
|
|
424
|
+
_isAppTracerStyle() {
|
|
425
|
+
// AppTracer has recordThinkingContent(sessionId, iteration, content) signature
|
|
426
|
+
// SimpleAppTracer has recordThinkingContent(content, metadata) signature
|
|
427
|
+
// We detect by checking if there's a sessionSpans map (AppTracer-specific)
|
|
428
|
+
return this.tracer && typeof this.tracer.sessionSpans !== 'undefined';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Record an error classification event for telemetry
|
|
433
|
+
* Provides unified error recording across all error types
|
|
434
|
+
* @param {string} errorType - Error type (wrapped_tool, unrecognized_tool, no_tool_call, circuit_breaker)
|
|
435
|
+
* @param {string} message - Error message
|
|
436
|
+
* @param {Object} context - Additional context data
|
|
437
|
+
* @param {number} iteration - Current iteration number
|
|
438
|
+
* @private
|
|
439
|
+
*/
|
|
440
|
+
_recordErrorTelemetry(errorType, message, context, iteration) {
|
|
441
|
+
if (!this.tracer) return;
|
|
442
|
+
|
|
443
|
+
if (this._isAppTracerStyle() && typeof this.tracer.recordErrorClassification === 'function') {
|
|
444
|
+
// AppTracer style: (sessionId, iteration, errorType, details)
|
|
445
|
+
this.tracer.recordErrorClassification(this.sessionId, iteration, errorType, {
|
|
446
|
+
message,
|
|
447
|
+
context
|
|
448
|
+
});
|
|
449
|
+
} else if (typeof this.tracer.recordErrorEvent === 'function') {
|
|
450
|
+
// SimpleAppTracer style: (errorType, details)
|
|
451
|
+
this.tracer.recordErrorEvent(errorType, {
|
|
452
|
+
message,
|
|
453
|
+
context: { ...context, iteration }
|
|
454
|
+
});
|
|
455
|
+
} else {
|
|
456
|
+
this.tracer.addEvent(`error.${errorType}`, {
|
|
457
|
+
'error.type': errorType,
|
|
458
|
+
'error.message': message,
|
|
459
|
+
'error.recoverable': errorType !== 'circuit_breaker',
|
|
460
|
+
'error.context': JSON.stringify(context).substring(0, 1000),
|
|
461
|
+
'iteration': iteration
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Record AI thinking content for telemetry
|
|
468
|
+
* @param {string} thinkingContent - The thinking content
|
|
469
|
+
* @param {number} iteration - Current iteration number
|
|
470
|
+
* @private
|
|
471
|
+
*/
|
|
472
|
+
_recordThinkingTelemetry(thinkingContent, iteration) {
|
|
473
|
+
if (!this.tracer || !thinkingContent) return;
|
|
474
|
+
|
|
475
|
+
if (this._isAppTracerStyle() && typeof this.tracer.recordThinkingContent === 'function') {
|
|
476
|
+
// AppTracer style: (sessionId, iteration, content)
|
|
477
|
+
this.tracer.recordThinkingContent(this.sessionId, iteration, thinkingContent);
|
|
478
|
+
} else if (typeof this.tracer.recordThinkingContent === 'function') {
|
|
479
|
+
// SimpleAppTracer style: (content, metadata)
|
|
480
|
+
this.tracer.recordThinkingContent(thinkingContent, { iteration });
|
|
481
|
+
} else {
|
|
482
|
+
this.tracer.addEvent('ai.thinking', {
|
|
483
|
+
'ai.thinking.content': thinkingContent.substring(0, 50000),
|
|
484
|
+
'ai.thinking.length': thinkingContent.length,
|
|
485
|
+
'iteration': iteration
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Record AI tool decision for telemetry
|
|
492
|
+
* @param {string} toolName - The tool name
|
|
493
|
+
* @param {Object} params - Tool parameters
|
|
494
|
+
* @param {number} responseLength - Length of AI response
|
|
495
|
+
* @param {number} iteration - Current iteration number
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
_recordToolDecisionTelemetry(toolName, params, responseLength, iteration) {
|
|
499
|
+
if (!this.tracer) return;
|
|
500
|
+
|
|
501
|
+
if (this._isAppTracerStyle() && typeof this.tracer.recordAIToolDecision === 'function') {
|
|
502
|
+
// AppTracer style: (sessionId, iteration, toolName, params)
|
|
503
|
+
this.tracer.recordAIToolDecision(this.sessionId, iteration, toolName, params);
|
|
504
|
+
} else if (typeof this.tracer.recordToolDecision === 'function') {
|
|
505
|
+
// SimpleAppTracer style: (toolName, params, metadata)
|
|
506
|
+
this.tracer.recordToolDecision(toolName, params, {
|
|
507
|
+
iteration,
|
|
508
|
+
'ai.tool_decision.raw_response_length': responseLength
|
|
509
|
+
});
|
|
510
|
+
} else {
|
|
511
|
+
this.tracer.addEvent('ai.tool_decision', {
|
|
512
|
+
'ai.tool_decision.name': toolName,
|
|
513
|
+
'ai.tool_decision.params': JSON.stringify(params || {}).substring(0, 2000),
|
|
514
|
+
'ai.tool_decision.raw_response_length': responseLength,
|
|
515
|
+
'iteration': iteration
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Record tool result for telemetry
|
|
522
|
+
* @param {string} toolName - The tool name
|
|
523
|
+
* @param {string|Object} result - Tool result
|
|
524
|
+
* @param {boolean} success - Whether tool succeeded
|
|
525
|
+
* @param {number} durationMs - Execution duration in milliseconds
|
|
526
|
+
* @param {number} iteration - Current iteration number
|
|
527
|
+
* @private
|
|
528
|
+
*/
|
|
529
|
+
_recordToolResultTelemetry(toolName, result, success, durationMs, iteration) {
|
|
530
|
+
if (!this.tracer) return;
|
|
531
|
+
|
|
532
|
+
if (this._isAppTracerStyle() && typeof this.tracer.recordToolResult === 'function') {
|
|
533
|
+
// AppTracer style: (sessionId, iteration, toolName, result, success, durationMs)
|
|
534
|
+
this.tracer.recordToolResult(this.sessionId, iteration, toolName, result, success, durationMs);
|
|
535
|
+
} else if (typeof this.tracer.recordToolResult === 'function') {
|
|
536
|
+
// SimpleAppTracer style: (toolName, result, success, durationMs, metadata)
|
|
537
|
+
this.tracer.recordToolResult(toolName, result, success, durationMs, { iteration });
|
|
538
|
+
} else {
|
|
539
|
+
const resultStr = typeof result === 'string' ? result : JSON.stringify(result || '');
|
|
540
|
+
this.tracer.addEvent('tool.result', {
|
|
541
|
+
'tool.name': toolName,
|
|
542
|
+
'tool.result': resultStr.substring(0, 10000),
|
|
543
|
+
'tool.result.length': resultStr.length,
|
|
544
|
+
'tool.duration_ms': durationMs,
|
|
545
|
+
'tool.success': success,
|
|
546
|
+
'iteration': iteration
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Record MCP tool lifecycle event for telemetry
|
|
553
|
+
* @param {string} phase - 'start' or 'end'
|
|
554
|
+
* @param {string} toolName - MCP tool name
|
|
555
|
+
* @param {Object} params - Tool parameters (for start) or null (for end)
|
|
556
|
+
* @param {number} iteration - Current iteration number
|
|
557
|
+
* @param {Object} [endData] - Additional data for end phase (result, success, durationMs, error)
|
|
558
|
+
* @private
|
|
559
|
+
*/
|
|
560
|
+
_recordMcpToolTelemetry(phase, toolName, params, iteration, endData = null) {
|
|
561
|
+
if (!this.tracer) return;
|
|
562
|
+
|
|
563
|
+
if (phase === 'start') {
|
|
564
|
+
if (this._isAppTracerStyle() && typeof this.tracer.recordMcpToolStart === 'function') {
|
|
565
|
+
// AppTracer style: (sessionId, iteration, toolName, serverName, params)
|
|
566
|
+
this.tracer.recordMcpToolStart(this.sessionId, iteration, toolName, 'mcp', params);
|
|
567
|
+
} else if (typeof this.tracer.recordMcpToolStart === 'function') {
|
|
568
|
+
// SimpleAppTracer style: (toolName, serverName, params, metadata)
|
|
569
|
+
this.tracer.recordMcpToolStart(toolName, 'mcp', params, { iteration });
|
|
570
|
+
} else {
|
|
571
|
+
this.tracer.addEvent('mcp.tool.start', {
|
|
572
|
+
'mcp.tool.name': toolName,
|
|
573
|
+
'mcp.tool.server': 'mcp',
|
|
574
|
+
'mcp.tool.params': JSON.stringify(params || {}).substring(0, 2000),
|
|
575
|
+
'iteration': iteration
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
} else if (phase === 'end' && endData) {
|
|
579
|
+
const { result, success, durationMs, error } = endData;
|
|
580
|
+
if (this._isAppTracerStyle() && typeof this.tracer.recordMcpToolEnd === 'function') {
|
|
581
|
+
// AppTracer style: (sessionId, iteration, toolName, serverName, result, success, durationMs, error)
|
|
582
|
+
this.tracer.recordMcpToolEnd(this.sessionId, iteration, toolName, 'mcp', result, success, durationMs, error);
|
|
583
|
+
} else if (typeof this.tracer.recordMcpToolEnd === 'function') {
|
|
584
|
+
// SimpleAppTracer style: (toolName, serverName, result, success, durationMs, error, metadata)
|
|
585
|
+
this.tracer.recordMcpToolEnd(toolName, 'mcp', result, success, durationMs, error, { iteration });
|
|
586
|
+
} else {
|
|
587
|
+
const resultStr = typeof result === 'string' ? result : JSON.stringify(result || '');
|
|
588
|
+
this.tracer.addEvent('mcp.tool.end', {
|
|
589
|
+
'mcp.tool.name': toolName,
|
|
590
|
+
'mcp.tool.server': 'mcp',
|
|
591
|
+
'mcp.tool.result': resultStr.substring(0, 10000),
|
|
592
|
+
'mcp.tool.result.length': resultStr.length,
|
|
593
|
+
'mcp.tool.duration_ms': durationMs,
|
|
594
|
+
'mcp.tool.success': success,
|
|
595
|
+
'mcp.tool.error': error,
|
|
596
|
+
'iteration': iteration
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Record iteration lifecycle event for telemetry
|
|
604
|
+
* @param {string} phase - 'end' (start is already handled elsewhere)
|
|
605
|
+
* @param {number} iteration - Current iteration number
|
|
606
|
+
* @param {Object} data - Additional iteration data
|
|
607
|
+
* @private
|
|
608
|
+
*/
|
|
609
|
+
_recordIterationTelemetry(phase, iteration, data = {}) {
|
|
610
|
+
if (!this.tracer) return;
|
|
611
|
+
|
|
612
|
+
if (typeof this.tracer.recordIterationEvent === 'function') {
|
|
613
|
+
this.tracer.recordIterationEvent(phase, iteration, data);
|
|
614
|
+
} else {
|
|
615
|
+
this.tracer.addEvent(`iteration.${phase}`, {
|
|
616
|
+
'iteration': iteration,
|
|
617
|
+
...data
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
409
622
|
/**
|
|
410
623
|
* Initialize the agent asynchronously (must be called after constructor)
|
|
411
624
|
* This method initializes MCP and merges MCP tools into the tool list, and loads history from storage
|
|
@@ -529,8 +742,9 @@ export class ProbeAgent {
|
|
|
529
742
|
const configOptions = {
|
|
530
743
|
sessionId: this.sessionId,
|
|
531
744
|
debug: this.debug,
|
|
532
|
-
// Use
|
|
533
|
-
cwd: this.cwd
|
|
745
|
+
// Use cwd (which defaults to workspaceRoot in constructor)
|
|
746
|
+
cwd: this.cwd,
|
|
747
|
+
workspaceRoot: this.workspaceRoot,
|
|
534
748
|
allowedFolders: this.allowedFolders,
|
|
535
749
|
outline: this.outline,
|
|
536
750
|
searchDelegate: this.searchDelegate,
|
|
@@ -1409,7 +1623,8 @@ export class ProbeAgent {
|
|
|
1409
1623
|
}
|
|
1410
1624
|
|
|
1411
1625
|
// Security validation: check if path is within any allowed directory
|
|
1412
|
-
// Use
|
|
1626
|
+
// Use safeRealpath() to resolve symlinks and handle path traversal attempts (e.g., '/allowed/../etc/passwd')
|
|
1627
|
+
// This prevents symlink bypass attacks (e.g., /tmp -> /private/tmp on macOS)
|
|
1413
1628
|
const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
|
|
1414
1629
|
|
|
1415
1630
|
let absolutePath;
|
|
@@ -1417,20 +1632,20 @@ export class ProbeAgent {
|
|
|
1417
1632
|
|
|
1418
1633
|
// If absolute path, check if it's within any allowed directory
|
|
1419
1634
|
if (isAbsolute(imagePath)) {
|
|
1420
|
-
//
|
|
1421
|
-
absolutePath =
|
|
1635
|
+
// Use safeRealpath to resolve symlinks for security
|
|
1636
|
+
absolutePath = safeRealpath(resolve(imagePath));
|
|
1422
1637
|
isPathAllowed = allowedDirs.some(dir => {
|
|
1423
|
-
const
|
|
1638
|
+
const resolvedDir = safeRealpath(dir);
|
|
1424
1639
|
// Ensure the path is within the allowed directory (add separator to prevent prefix attacks)
|
|
1425
|
-
return absolutePath ===
|
|
1640
|
+
return absolutePath === resolvedDir || absolutePath.startsWith(resolvedDir + sep);
|
|
1426
1641
|
});
|
|
1427
1642
|
} else {
|
|
1428
1643
|
// For relative paths, try resolving against each allowed directory
|
|
1429
1644
|
for (const dir of allowedDirs) {
|
|
1430
|
-
const
|
|
1431
|
-
const resolvedPath =
|
|
1645
|
+
const resolvedDir = safeRealpath(dir);
|
|
1646
|
+
const resolvedPath = safeRealpath(resolve(dir, imagePath));
|
|
1432
1647
|
// Ensure the resolved path is within the allowed directory
|
|
1433
|
-
if (resolvedPath ===
|
|
1648
|
+
if (resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep)) {
|
|
1434
1649
|
absolutePath = resolvedPath;
|
|
1435
1650
|
isPathAllowed = true;
|
|
1436
1651
|
break;
|
|
@@ -1667,7 +1882,8 @@ export class ProbeAgent {
|
|
|
1667
1882
|
return this.architectureContext;
|
|
1668
1883
|
}
|
|
1669
1884
|
|
|
1670
|
-
|
|
1885
|
+
// Use workspaceRoot for consistent path handling
|
|
1886
|
+
const rootDirectory = this.workspaceRoot || (this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd());
|
|
1671
1887
|
const configuredName =
|
|
1672
1888
|
typeof this.architectureFileName === 'string' ? this.architectureFileName.trim() : '';
|
|
1673
1889
|
const hasConfiguredName = !!configuredName;
|
|
@@ -1825,6 +2041,10 @@ export class ProbeAgent {
|
|
|
1825
2041
|
}
|
|
1826
2042
|
|
|
1827
2043
|
_getSkillsRepoRoot() {
|
|
2044
|
+
// Use workspaceRoot for consistent path handling
|
|
2045
|
+
if (this.workspaceRoot) {
|
|
2046
|
+
return resolve(this.workspaceRoot);
|
|
2047
|
+
}
|
|
1828
2048
|
if (this.allowedFolders && this.allowedFolders.length > 0) {
|
|
1829
2049
|
return resolve(this.allowedFolders[0]);
|
|
1830
2050
|
}
|
|
@@ -1905,7 +2125,7 @@ ${extractGuidance}
|
|
|
1905
2125
|
// Add repository structure if available
|
|
1906
2126
|
if (this.fileList) {
|
|
1907
2127
|
systemPrompt += `\n\n# Repository Structure\n`;
|
|
1908
|
-
systemPrompt += `You are working with a repository located at: ${this.
|
|
2128
|
+
systemPrompt += `You are working with a repository located at: ${this.workspaceRoot}\n\n`;
|
|
1909
2129
|
systemPrompt += `Here's an overview of the repository structure (showing up to 100 most relevant files):\n\n`;
|
|
1910
2130
|
systemPrompt += '```\n' + this.fileList + '\n```\n';
|
|
1911
2131
|
}
|
|
@@ -1967,7 +2187,7 @@ ${extractGuidance}
|
|
|
1967
2187
|
// Add repository structure if available
|
|
1968
2188
|
if (this.fileList) {
|
|
1969
2189
|
systemPrompt += `\n\n# Repository Structure\n`;
|
|
1970
|
-
systemPrompt += `You are working with a repository located at: ${this.
|
|
2190
|
+
systemPrompt += `You are working with a repository located at: ${this.workspaceRoot}\n\n`;
|
|
1971
2191
|
systemPrompt += `Here's an overview of the repository structure (showing up to 100 most relevant files):\n\n`;
|
|
1972
2192
|
systemPrompt += '```\n' + this.fileList + '\n```\n';
|
|
1973
2193
|
}
|
|
@@ -2281,10 +2501,29 @@ Follow these instructions carefully:
|
|
|
2281
2501
|
}
|
|
2282
2502
|
}
|
|
2283
2503
|
|
|
2284
|
-
// Add folder information
|
|
2285
|
-
const searchDirectory = this.
|
|
2504
|
+
// Add folder information using workspace root and relative paths
|
|
2505
|
+
const searchDirectory = this.workspaceRoot;
|
|
2286
2506
|
if (this.debug) {
|
|
2287
|
-
console.log(`[DEBUG] Generating file list for
|
|
2507
|
+
console.log(`[DEBUG] Generating file list for workspace root: ${searchDirectory}...`);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
// Convert allowed folders to relative paths for cleaner AI context
|
|
2511
|
+
// Add ./ prefix to make it clear these are relative paths
|
|
2512
|
+
const relativeWorkspaces = this.allowedFolders.map(f => {
|
|
2513
|
+
const rel = toRelativePath(f, this.workspaceRoot);
|
|
2514
|
+
// Add ./ prefix if not already starting with . and not an absolute path
|
|
2515
|
+
if (rel && rel !== '.' && !rel.startsWith('.') && !rel.startsWith('/')) {
|
|
2516
|
+
return './' + rel;
|
|
2517
|
+
}
|
|
2518
|
+
return rel;
|
|
2519
|
+
}).filter(f => f && f !== '.');
|
|
2520
|
+
|
|
2521
|
+
// Describe available paths in a user-friendly way
|
|
2522
|
+
let workspaceDesc;
|
|
2523
|
+
if (relativeWorkspaces.length === 0) {
|
|
2524
|
+
workspaceDesc = '. (current directory)';
|
|
2525
|
+
} else {
|
|
2526
|
+
workspaceDesc = relativeWorkspaces.join(', ');
|
|
2288
2527
|
}
|
|
2289
2528
|
|
|
2290
2529
|
try {
|
|
@@ -2292,15 +2531,15 @@ Follow these instructions carefully:
|
|
|
2292
2531
|
directory: searchDirectory,
|
|
2293
2532
|
maxFiles: 100,
|
|
2294
2533
|
respectGitignore: !process.env.PROBE_NO_GITIGNORE || process.env.PROBE_NO_GITIGNORE === '',
|
|
2295
|
-
cwd:
|
|
2534
|
+
cwd: this.workspaceRoot
|
|
2296
2535
|
});
|
|
2297
2536
|
|
|
2298
|
-
systemMessage += `\n# Repository Structure\n\nYou are working with a
|
|
2537
|
+
systemMessage += `\n# Repository Structure\n\nYou are working with a workspace. Available paths: ${workspaceDesc}\n\nHere's an overview of the repository structure (showing up to 100 most relevant files):\n\n\`\`\`\n${files}\n\`\`\`\n\n`;
|
|
2299
2538
|
} catch (error) {
|
|
2300
2539
|
if (this.debug) {
|
|
2301
2540
|
console.log(`[DEBUG] Could not generate file list: ${error.message}`);
|
|
2302
2541
|
}
|
|
2303
|
-
systemMessage += `\n# Repository Structure\n\nYou are working with a
|
|
2542
|
+
systemMessage += `\n# Repository Structure\n\nYou are working with a workspace. Available paths: ${workspaceDesc}\n\n`;
|
|
2304
2543
|
}
|
|
2305
2544
|
|
|
2306
2545
|
// Add architecture context if available
|
|
@@ -2308,7 +2547,15 @@ Follow these instructions carefully:
|
|
|
2308
2547
|
systemMessage += this.getArchitectureSection();
|
|
2309
2548
|
|
|
2310
2549
|
if (this.allowedFolders.length > 0) {
|
|
2311
|
-
|
|
2550
|
+
const relativeAllowed = this.allowedFolders.map(f => {
|
|
2551
|
+
const rel = toRelativePath(f, this.workspaceRoot);
|
|
2552
|
+
// Add ./ prefix if not already starting with . and not an absolute path
|
|
2553
|
+
if (rel && rel !== '.' && !rel.startsWith('.') && !rel.startsWith('/')) {
|
|
2554
|
+
return './' + rel;
|
|
2555
|
+
}
|
|
2556
|
+
return rel;
|
|
2557
|
+
});
|
|
2558
|
+
systemMessage += `\n**Important**: For security reasons, you can only access these paths: ${relativeAllowed.join(', ')}\n\n`;
|
|
2312
2559
|
}
|
|
2313
2560
|
|
|
2314
2561
|
return systemMessage;
|
|
@@ -2854,8 +3101,18 @@ Follow these instructions carefully:
|
|
|
2854
3101
|
const parsedTool = (this.mcpBridge && !options._disableTools)
|
|
2855
3102
|
? parseHybridXmlToolCall(assistantResponseContent, nativeTools, this.mcpBridge)
|
|
2856
3103
|
: parseXmlToolCallWithThinking(assistantResponseContent, validTools);
|
|
3104
|
+
|
|
3105
|
+
// Capture AI thinking content if present (for debugging and telemetry)
|
|
3106
|
+
if (parsedTool?.thinkingContent) {
|
|
3107
|
+
this._recordThinkingTelemetry(parsedTool.thinkingContent, currentIteration);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
2857
3110
|
if (parsedTool) {
|
|
2858
3111
|
const { toolName, params } = parsedTool;
|
|
3112
|
+
|
|
3113
|
+
// Record AI tool decision for telemetry
|
|
3114
|
+
this._recordToolDecisionTelemetry(toolName, params, assistantResponseContent.length, currentIteration);
|
|
3115
|
+
|
|
2859
3116
|
if (this.debug) console.log(`[DEBUG] Parsed tool call: ${toolName} with params:`, params);
|
|
2860
3117
|
|
|
2861
3118
|
if (toolName === 'attempt_completion') {
|
|
@@ -2962,6 +3219,9 @@ Follow these instructions carefully:
|
|
|
2962
3219
|
|
|
2963
3220
|
if (type === 'mcp' && this.mcpBridge && this.mcpBridge.isMcpTool(toolName)) {
|
|
2964
3221
|
// Execute MCP tool
|
|
3222
|
+
const mcpStartTime = Date.now();
|
|
3223
|
+
this._recordMcpToolTelemetry('start', toolName, params, currentIteration);
|
|
3224
|
+
|
|
2965
3225
|
try {
|
|
2966
3226
|
// Log MCP tool execution in debug mode
|
|
2967
3227
|
if (this.debug) {
|
|
@@ -2999,6 +3259,15 @@ Follow these instructions carefully:
|
|
|
2999
3259
|
console.error(`[WARN] Tool output truncation failed: ${truncateError.message}`);
|
|
3000
3260
|
}
|
|
3001
3261
|
|
|
3262
|
+
// Record MCP tool end event (success)
|
|
3263
|
+
const mcpDurationMs = Date.now() - mcpStartTime;
|
|
3264
|
+
this._recordMcpToolTelemetry('end', toolName, null, currentIteration, {
|
|
3265
|
+
result: toolResultContent,
|
|
3266
|
+
success: true,
|
|
3267
|
+
durationMs: mcpDurationMs,
|
|
3268
|
+
error: null
|
|
3269
|
+
});
|
|
3270
|
+
|
|
3002
3271
|
// Log MCP tool result in debug mode
|
|
3003
3272
|
if (this.debug) {
|
|
3004
3273
|
const preview = toolResultContent.length > 500 ? toolResultContent.substring(0, 500) + '...' : toolResultContent;
|
|
@@ -3009,8 +3278,19 @@ Follow these instructions carefully:
|
|
|
3009
3278
|
console.error(`[DEBUG] ========================================\n`);
|
|
3010
3279
|
}
|
|
3011
3280
|
|
|
3281
|
+
// Add assistant message with tool call (matching native tool pattern)
|
|
3282
|
+
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
3012
3283
|
currentMessages.push({ role: 'user', content: `<tool_result>\n${toolResultContent}\n</tool_result>` });
|
|
3013
3284
|
} catch (error) {
|
|
3285
|
+
// Record MCP tool end event (failure)
|
|
3286
|
+
const mcpDurationMs = Date.now() - mcpStartTime;
|
|
3287
|
+
this._recordMcpToolTelemetry('end', toolName, null, currentIteration, {
|
|
3288
|
+
result: null,
|
|
3289
|
+
success: false,
|
|
3290
|
+
durationMs: mcpDurationMs,
|
|
3291
|
+
error: error.message
|
|
3292
|
+
});
|
|
3293
|
+
|
|
3014
3294
|
console.error(`Error executing MCP tool ${toolName}:`, error);
|
|
3015
3295
|
|
|
3016
3296
|
// Log MCP tool error in debug mode
|
|
@@ -3023,24 +3303,27 @@ Follow these instructions carefully:
|
|
|
3023
3303
|
|
|
3024
3304
|
// Format error with structured information for AI
|
|
3025
3305
|
const errorXml = formatErrorForAI(error);
|
|
3306
|
+
// Add assistant message with tool call (matching native tool pattern)
|
|
3307
|
+
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
3026
3308
|
currentMessages.push({ role: 'user', content: `<tool_result>\n${errorXml}\n</tool_result>` });
|
|
3027
3309
|
}
|
|
3028
3310
|
} else if (this.toolImplementations[toolName]) {
|
|
3029
3311
|
// Execute native tool
|
|
3030
3312
|
try {
|
|
3031
3313
|
// Add sessionId and workingDirectory to params for tool execution
|
|
3032
|
-
// Validate and resolve workingDirectory
|
|
3033
|
-
//
|
|
3034
|
-
let resolvedWorkingDirectory = this.cwd || (this.allowedFolders && this.allowedFolders[0]) || process.cwd();
|
|
3314
|
+
// Validate and resolve workingDirectory using safeRealpath for symlink security
|
|
3315
|
+
// Consistent fallback chain: workspaceRoot > cwd > allowedFolders[0] > process.cwd()
|
|
3316
|
+
let resolvedWorkingDirectory = this.workspaceRoot || this.cwd || (this.allowedFolders && this.allowedFolders[0]) || process.cwd();
|
|
3035
3317
|
if (params.workingDirectory) {
|
|
3036
3318
|
// Resolve relative paths against the current working directory context, not process.cwd()
|
|
3037
|
-
|
|
3319
|
+
// Use safeRealpath to resolve symlinks and prevent bypass attacks
|
|
3320
|
+
const requestedDir = safeRealpath(isAbsolute(params.workingDirectory)
|
|
3038
3321
|
? resolve(params.workingDirectory)
|
|
3039
|
-
: resolve(resolvedWorkingDirectory, params.workingDirectory);
|
|
3322
|
+
: resolve(resolvedWorkingDirectory, params.workingDirectory));
|
|
3040
3323
|
// Check if the requested directory is within allowed folders
|
|
3041
3324
|
const isWithinAllowed = !this.allowedFolders || this.allowedFolders.length === 0 ||
|
|
3042
3325
|
this.allowedFolders.some(folder => {
|
|
3043
|
-
const resolvedFolder =
|
|
3326
|
+
const resolvedFolder = safeRealpath(folder);
|
|
3044
3327
|
return requestedDir === resolvedFolder || requestedDir.startsWith(resolvedFolder + sep);
|
|
3045
3328
|
});
|
|
3046
3329
|
if (isWithinAllowed) {
|
|
@@ -3118,6 +3401,7 @@ Follow these instructions carefully:
|
|
|
3118
3401
|
};
|
|
3119
3402
|
|
|
3120
3403
|
let toolResult;
|
|
3404
|
+
const toolStartTime = Date.now();
|
|
3121
3405
|
try {
|
|
3122
3406
|
if (this.tracer) {
|
|
3123
3407
|
toolResult = await this.tracer.withSpan('tool.call', executeToolCall, {
|
|
@@ -3128,7 +3412,11 @@ Follow these instructions carefully:
|
|
|
3128
3412
|
} else {
|
|
3129
3413
|
toolResult = await executeToolCall();
|
|
3130
3414
|
}
|
|
3131
|
-
|
|
3415
|
+
|
|
3416
|
+
// Record tool result in telemetry
|
|
3417
|
+
const toolDurationMs = Date.now() - toolStartTime;
|
|
3418
|
+
this._recordToolResultTelemetry(toolName, toolResult, true, toolDurationMs, currentIteration);
|
|
3419
|
+
|
|
3132
3420
|
// Log tool result in debug mode
|
|
3133
3421
|
if (this.debug) {
|
|
3134
3422
|
const resultPreview = typeof toolResult === 'string'
|
|
@@ -3201,6 +3489,22 @@ Follow these instructions carefully:
|
|
|
3201
3489
|
content: toolResultMessage
|
|
3202
3490
|
});
|
|
3203
3491
|
|
|
3492
|
+
// Record conversation turns in telemetry
|
|
3493
|
+
if (this.tracer) {
|
|
3494
|
+
if (typeof this.tracer.recordConversationTurn === 'function') {
|
|
3495
|
+
this.tracer.recordConversationTurn('assistant', assistantResponseContent, {
|
|
3496
|
+
iteration: currentIteration,
|
|
3497
|
+
has_tool_call: true,
|
|
3498
|
+
tool_name: toolName
|
|
3499
|
+
});
|
|
3500
|
+
this.tracer.recordConversationTurn('tool_result', toolResultContent, {
|
|
3501
|
+
iteration: currentIteration,
|
|
3502
|
+
tool_name: toolName,
|
|
3503
|
+
tool_success: true
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3204
3508
|
// NOTE: Automatic image processing removed (GitHub issue #305)
|
|
3205
3509
|
// Images are now only loaded when the AI explicitly calls the readImage tool
|
|
3206
3510
|
// This prevents: 1) implicit behavior that users don't expect
|
|
@@ -3294,6 +3598,10 @@ Follow these instructions carefully:
|
|
|
3294
3598
|
if (this.debug) {
|
|
3295
3599
|
console.log(`[DEBUG] Detected wrapped tool '${wrappedToolName}' in assistant response - wrong XML format.`);
|
|
3296
3600
|
}
|
|
3601
|
+
|
|
3602
|
+
// Record wrapped tool error in telemetry
|
|
3603
|
+
this._recordErrorTelemetry('wrapped_tool', 'Tool call wrapped in markdown', { toolName: wrappedToolName }, currentIteration);
|
|
3604
|
+
|
|
3297
3605
|
const toolError = new ParameterError(
|
|
3298
3606
|
`Tool '${wrappedToolName}' found but in WRONG FORMAT - do not wrap tools in other XML tags.`,
|
|
3299
3607
|
{
|
|
@@ -3318,12 +3626,19 @@ Remove ALL wrapper tags and use <${wrappedToolName}> directly as the outermost t
|
|
|
3318
3626
|
if (this.debug) {
|
|
3319
3627
|
console.log(`[DEBUG] Detected unrecognized tool '${unrecognizedTool}' in assistant response.`);
|
|
3320
3628
|
}
|
|
3629
|
+
|
|
3630
|
+
// Record unrecognized tool error in telemetry
|
|
3631
|
+
this._recordErrorTelemetry('unrecognized_tool', `Unknown tool: ${unrecognizedTool}`, { toolName: unrecognizedTool, validTools }, currentIteration);
|
|
3632
|
+
|
|
3321
3633
|
const toolError = new ParameterError(`Tool '${unrecognizedTool}' is not available in this context.`, {
|
|
3322
3634
|
suggestion: `Available tools: ${validTools.join(', ')}. Please use one of these tools instead.`
|
|
3323
3635
|
});
|
|
3324
3636
|
reminderContent = `<tool_result>\n${formatErrorForAI(toolError)}\n</tool_result>`;
|
|
3325
3637
|
} else {
|
|
3326
|
-
// No tool call detected at all -
|
|
3638
|
+
// No tool call detected at all - record in telemetry
|
|
3639
|
+
this._recordErrorTelemetry('no_tool_call', 'AI response did not contain tool call', { responsePreview: assistantResponseContent.substring(0, 500) }, currentIteration);
|
|
3640
|
+
|
|
3641
|
+
// Check if this is the last iteration
|
|
3327
3642
|
// On the last iteration, if the AI gave a substantive response without using
|
|
3328
3643
|
// attempt_completion, accept it as the final answer rather than losing the content
|
|
3329
3644
|
if (currentIteration >= maxIterations) {
|
|
@@ -3439,6 +3754,10 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
|
|
|
3439
3754
|
sameFormatErrorCount++;
|
|
3440
3755
|
if (sameFormatErrorCount >= MAX_REPEATED_FORMAT_ERRORS) {
|
|
3441
3756
|
const errorDesc = isWrapped ? 'wrapped tool format' : unrecognizedTool;
|
|
3757
|
+
|
|
3758
|
+
// Record circuit breaker error in telemetry
|
|
3759
|
+
this._recordErrorTelemetry('circuit_breaker', 'Format error limit exceeded', { formatErrorCount: sameFormatErrorCount, errorCategory }, currentIteration);
|
|
3760
|
+
|
|
3442
3761
|
console.error(`[ERROR] Format error category '${errorCategory}' repeated ${sameFormatErrorCount} times. Breaking loop early to prevent infinite iteration.`);
|
|
3443
3762
|
finalResult = `Error: Unable to complete request. The AI model repeatedly used incorrect tool call format (${errorDesc}). Please try rephrasing your question or using a different model.`;
|
|
3444
3763
|
break;
|
|
@@ -3454,13 +3773,19 @@ Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant messa
|
|
|
3454
3773
|
}
|
|
3455
3774
|
}
|
|
3456
3775
|
|
|
3776
|
+
// Record iteration end event
|
|
3777
|
+
this._recordIterationTelemetry('end', currentIteration, {
|
|
3778
|
+
'iteration.completed': completionAttempted,
|
|
3779
|
+
'iteration.message_count': currentMessages.length
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3457
3782
|
// Keep message history manageable
|
|
3458
3783
|
if (currentMessages.length > MAX_HISTORY_MESSAGES) {
|
|
3459
3784
|
const messagesBefore = currentMessages.length;
|
|
3460
3785
|
const systemMsg = currentMessages[0]; // Keep system message
|
|
3461
3786
|
const recentMessages = currentMessages.slice(-MAX_HISTORY_MESSAGES + 1);
|
|
3462
3787
|
currentMessages = [systemMsg, ...recentMessages];
|
|
3463
|
-
|
|
3788
|
+
|
|
3464
3789
|
if (this.debug) {
|
|
3465
3790
|
console.log(`[DEBUG] Trimmed message history from ${messagesBefore} to ${currentMessages.length} messages`);
|
|
3466
3791
|
}
|
|
@@ -3611,7 +3936,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3611
3936
|
|
|
3612
3937
|
const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
|
|
3613
3938
|
debug: this.debug,
|
|
3614
|
-
path: this.allowedFolders[0],
|
|
3939
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
3615
3940
|
provider: this.clientApiProvider,
|
|
3616
3941
|
model: this.model,
|
|
3617
3942
|
tracer: this.tracer
|
|
@@ -3701,7 +4026,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3701
4026
|
|
|
3702
4027
|
const { JsonFixingAgent } = await import('./schemaUtils.js');
|
|
3703
4028
|
const jsonFixer = new JsonFixingAgent({
|
|
3704
|
-
path: this.allowedFolders[0],
|
|
4029
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
3705
4030
|
provider: this.clientApiProvider,
|
|
3706
4031
|
model: this.model,
|
|
3707
4032
|
debug: this.debug,
|
|
@@ -3789,7 +4114,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3789
4114
|
|
|
3790
4115
|
const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
|
|
3791
4116
|
debug: this.debug,
|
|
3792
|
-
path: this.allowedFolders[0],
|
|
4117
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
3793
4118
|
provider: this.clientApiProvider,
|
|
3794
4119
|
model: this.model,
|
|
3795
4120
|
tracer: this.tracer
|
|
@@ -3945,7 +4270,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3945
4270
|
|
|
3946
4271
|
const finalMermaidValidation = await validateAndFixMermaidResponse(finalResult, {
|
|
3947
4272
|
debug: this.debug,
|
|
3948
|
-
path: this.allowedFolders[0],
|
|
4273
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
3949
4274
|
provider: this.clientApiProvider,
|
|
3950
4275
|
model: this.model,
|
|
3951
4276
|
tracer: this.tracer
|
|
@@ -4143,7 +4468,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
4143
4468
|
allowEdit: this.allowEdit,
|
|
4144
4469
|
enableDelegate: this.enableDelegate,
|
|
4145
4470
|
architectureFileName: this.architectureFileName,
|
|
4146
|
-
|
|
4471
|
+
// Pass allowedFolders which will recompute workspaceRoot correctly
|
|
4147
4472
|
allowedFolders: [...this.allowedFolders],
|
|
4148
4473
|
cwd: this.cwd, // Preserve explicit working directory
|
|
4149
4474
|
provider: this.clientApiProvider,
|