@probelabs/probe 0.6.0-rc253 → 0.6.0-rc255
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 +166 -3
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.d.ts +1 -1
- package/build/agent/ProbeAgent.js +51 -16
- package/build/agent/acp/tools.js +2 -1
- package/build/agent/acp/tools.test.js +2 -1
- package/build/agent/dsl/environment.js +19 -0
- package/build/agent/index.js +1512 -413
- package/build/agent/schemaUtils.js +91 -2
- package/build/agent/tools.js +0 -28
- package/build/delegate.js +3 -0
- package/build/index.js +2 -0
- package/build/tools/common.js +6 -5
- package/build/tools/edit.js +457 -65
- package/build/tools/executePlan.js +3 -1
- package/build/tools/fileTracker.js +318 -0
- package/build/tools/fuzzyMatch.js +271 -0
- package/build/tools/hashline.js +131 -0
- package/build/tools/lineEditHeuristics.js +138 -0
- package/build/tools/symbolEdit.js +119 -0
- package/build/tools/vercel.js +40 -9
- package/cjs/agent/ProbeAgent.cjs +1615 -517
- package/cjs/index.cjs +1643 -543
- package/index.d.ts +189 -1
- package/package.json +1 -1
- package/src/agent/ProbeAgent.d.ts +1 -1
- package/src/agent/ProbeAgent.js +51 -16
- package/src/agent/acp/tools.js +2 -1
- package/src/agent/acp/tools.test.js +2 -1
- package/src/agent/dsl/environment.js +19 -0
- package/src/agent/index.js +14 -3
- package/src/agent/schemaUtils.js +91 -2
- package/src/agent/tools.js +0 -28
- package/src/delegate.js +3 -0
- package/src/index.js +2 -0
- package/src/tools/common.js +6 -5
- package/src/tools/edit.js +457 -65
- package/src/tools/executePlan.js +3 -1
- package/src/tools/fileTracker.js +318 -0
- package/src/tools/fuzzyMatch.js +271 -0
- package/src/tools/hashline.js +131 -0
- package/src/tools/lineEditHeuristics.js +138 -0
- package/src/tools/symbolEdit.js +119 -0
- package/src/tools/vercel.js +40 -9
- package/bin/binaries/probe-v0.6.0-rc253-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc253-x86_64-unknown-linux-musl.tar.gz +0 -0
package/index.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ export interface ProbeAgentOptions {
|
|
|
13
13
|
systemPrompt?: string;
|
|
14
14
|
/** Predefined prompt type (persona) */
|
|
15
15
|
promptType?: 'code-explorer' | 'engineer' | 'code-review' | 'support' | 'architect';
|
|
16
|
-
/** Allow the use of the '
|
|
16
|
+
/** Allow the use of the 'edit' and 'create' tools for code editing */
|
|
17
17
|
allowEdit?: boolean;
|
|
18
|
+
/** Annotate search/extract output with line hashes for integrity verification */
|
|
19
|
+
hashLines?: boolean;
|
|
18
20
|
/** Architecture context filename to embed from repo root (defaults to AGENTS.md with CLAUDE.md fallback; ARCHITECTURE.md is always included when present) */
|
|
19
21
|
architectureFileName?: string;
|
|
20
22
|
/** Search directory path */
|
|
@@ -562,12 +564,196 @@ export declare function listFilesByLevel(
|
|
|
562
564
|
*/
|
|
563
565
|
export declare const DEFAULT_SYSTEM_MESSAGE: string;
|
|
564
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Content record for a tracked symbol
|
|
569
|
+
*/
|
|
570
|
+
export interface ContentRecord {
|
|
571
|
+
/** SHA-256 content hash (first 16 hex chars) */
|
|
572
|
+
contentHash: string;
|
|
573
|
+
/** 1-indexed start line */
|
|
574
|
+
startLine: number;
|
|
575
|
+
/** 1-indexed end line */
|
|
576
|
+
endLine: number;
|
|
577
|
+
/** Symbol name, if from a symbol extract */
|
|
578
|
+
symbolName: string | null;
|
|
579
|
+
/** How the content was obtained: 'extract' or 'edit' */
|
|
580
|
+
source: string;
|
|
581
|
+
/** When the record was created (ms since epoch) */
|
|
582
|
+
timestamp: number;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Result of a staleness check
|
|
587
|
+
*/
|
|
588
|
+
export interface FileCheckResult {
|
|
589
|
+
/** Whether the file is safe to edit */
|
|
590
|
+
ok: boolean;
|
|
591
|
+
/** Reason for rejection: 'untracked' or 'stale' */
|
|
592
|
+
reason?: 'untracked' | 'stale';
|
|
593
|
+
/** Human-readable message explaining the rejection */
|
|
594
|
+
message?: string;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Compute a SHA-256 content hash for a code block.
|
|
599
|
+
* Normalizes trailing whitespace per line for robustness.
|
|
600
|
+
*/
|
|
601
|
+
export declare function computeContentHash(content: string): string;
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Per-session content-aware file state tracker for safe multi-edit workflows.
|
|
605
|
+
* Two-tier tracking: file-level "seen" flag + symbol-level content hashes.
|
|
606
|
+
* Edits proceed when the target symbol hasn't changed, even if other parts of the file changed.
|
|
607
|
+
*/
|
|
608
|
+
export declare class FileTracker {
|
|
609
|
+
constructor(options?: { debug?: boolean });
|
|
610
|
+
|
|
611
|
+
/** Mark a file as seen (read via search/extract) */
|
|
612
|
+
markFileSeen(resolvedPath: string): void;
|
|
613
|
+
|
|
614
|
+
/** Check if a file has been seen in this session */
|
|
615
|
+
isFileSeen(resolvedPath: string): boolean;
|
|
616
|
+
|
|
617
|
+
/** Store a content hash for a symbol */
|
|
618
|
+
trackSymbolContent(resolvedPath: string, symbolName: string, code: string, startLine: number, endLine: number, source?: string): void;
|
|
619
|
+
|
|
620
|
+
/** Look up a stored content record for a symbol */
|
|
621
|
+
getSymbolRecord(resolvedPath: string, symbolName: string): ContentRecord | null;
|
|
622
|
+
|
|
623
|
+
/** Check if a symbol's current content matches what was stored */
|
|
624
|
+
checkSymbolContent(resolvedPath: string, symbolName: string, currentCode: string): FileCheckResult;
|
|
625
|
+
|
|
626
|
+
/** Track files from extract target strings (marks as seen, hashes symbol targets) */
|
|
627
|
+
trackFilesFromExtract(targets: string[], cwd: string): Promise<void>;
|
|
628
|
+
|
|
629
|
+
/** Track files from probe search/extract output text (marks as seen) */
|
|
630
|
+
trackFilesFromOutput(output: string, cwd: string): Promise<void>;
|
|
631
|
+
|
|
632
|
+
/** Check if a file is safe to edit (seen-check only) */
|
|
633
|
+
checkBeforeEdit(resolvedPath: string): FileCheckResult;
|
|
634
|
+
|
|
635
|
+
/** Mark file as seen after write, invalidate content records */
|
|
636
|
+
trackFileAfterWrite(resolvedPath: string): Promise<void>;
|
|
637
|
+
|
|
638
|
+
/** Update stored hash after successful symbol write */
|
|
639
|
+
trackSymbolAfterWrite(resolvedPath: string, symbolName: string, code: string, startLine: number, endLine: number): void;
|
|
640
|
+
|
|
641
|
+
/** Remove all content records for a file */
|
|
642
|
+
invalidateFileRecords(resolvedPath: string): void;
|
|
643
|
+
|
|
644
|
+
/** Quick sync check if a file is being tracked (alias for isFileSeen) */
|
|
645
|
+
isTracked(resolvedPath: string): boolean;
|
|
646
|
+
|
|
647
|
+
/** Clear all tracking state */
|
|
648
|
+
clear(): void;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Edit tool configuration options
|
|
653
|
+
*/
|
|
654
|
+
export interface EditToolOptions {
|
|
655
|
+
/** Debug mode */
|
|
656
|
+
debug?: boolean;
|
|
657
|
+
/** Allowed directories for file operations */
|
|
658
|
+
allowedFolders?: string[];
|
|
659
|
+
/** Working directory for resolving relative paths */
|
|
660
|
+
cwd?: string;
|
|
661
|
+
/** Workspace root for relative path display */
|
|
662
|
+
workspaceRoot?: string;
|
|
663
|
+
/** File tracker for staleness detection (created automatically by ProbeAgent) */
|
|
664
|
+
fileTracker?: FileTracker;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Edit tool parameters (text mode)
|
|
669
|
+
*/
|
|
670
|
+
export interface EditTextParams {
|
|
671
|
+
/** Path to the file to edit */
|
|
672
|
+
file_path: string;
|
|
673
|
+
/** Text to find in the file (copy verbatim) */
|
|
674
|
+
old_string: string;
|
|
675
|
+
/** Replacement text */
|
|
676
|
+
new_string: string;
|
|
677
|
+
/** Replace all occurrences (default: false) */
|
|
678
|
+
replace_all?: boolean;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Edit tool parameters (symbol replace mode)
|
|
683
|
+
*/
|
|
684
|
+
export interface EditSymbolReplaceParams {
|
|
685
|
+
/** Path to the file to edit */
|
|
686
|
+
file_path: string;
|
|
687
|
+
/** Symbol name to replace (e.g. "myFunction", "MyClass.myMethod") */
|
|
688
|
+
symbol: string;
|
|
689
|
+
/** New code to replace the symbol with */
|
|
690
|
+
new_string: string;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Edit tool parameters (symbol insert mode)
|
|
695
|
+
*/
|
|
696
|
+
export interface EditSymbolInsertParams {
|
|
697
|
+
/** Path to the file to edit */
|
|
698
|
+
file_path: string;
|
|
699
|
+
/** Symbol name to insert near */
|
|
700
|
+
symbol: string;
|
|
701
|
+
/** New code to insert */
|
|
702
|
+
new_string: string;
|
|
703
|
+
/** Insert before or after the symbol */
|
|
704
|
+
position: 'before' | 'after';
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Line-targeted edit parameters
|
|
709
|
+
*/
|
|
710
|
+
export interface EditLineTargetedParams {
|
|
711
|
+
/** Path to the file to edit */
|
|
712
|
+
file_path: string;
|
|
713
|
+
/** New code content */
|
|
714
|
+
new_string: string;
|
|
715
|
+
/** Line reference (e.g. "42" or "42:ab" with hash) */
|
|
716
|
+
start_line: string;
|
|
717
|
+
/** End of line range, inclusive (e.g. "55" or "55:cd"). Defaults to start_line. */
|
|
718
|
+
end_line?: string;
|
|
719
|
+
/** Insert before or after the line instead of replacing */
|
|
720
|
+
position?: 'before' | 'after';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Create tool parameters
|
|
725
|
+
*/
|
|
726
|
+
export interface CreateParams {
|
|
727
|
+
/** Path where the file should be created */
|
|
728
|
+
file_path: string;
|
|
729
|
+
/** Content to write to the file */
|
|
730
|
+
content: string;
|
|
731
|
+
/** Overwrite if file exists (default: false) */
|
|
732
|
+
overwrite?: boolean;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Create edit tool instance
|
|
737
|
+
*/
|
|
738
|
+
export declare function editTool(options?: EditToolOptions): {
|
|
739
|
+
execute(params: EditTextParams | EditSymbolReplaceParams | EditSymbolInsertParams | EditLineTargetedParams): Promise<string>;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Create create tool instance
|
|
744
|
+
*/
|
|
745
|
+
export declare function createTool(options?: EditToolOptions): {
|
|
746
|
+
execute(params: CreateParams): Promise<string>;
|
|
747
|
+
};
|
|
748
|
+
|
|
565
749
|
/**
|
|
566
750
|
* Schema definitions
|
|
567
751
|
*/
|
|
568
752
|
export declare const searchSchema: any;
|
|
569
753
|
export declare const querySchema: any;
|
|
570
754
|
export declare const extractSchema: any;
|
|
755
|
+
export declare const editSchema: any;
|
|
756
|
+
export declare const createSchema: any;
|
|
571
757
|
export declare const attemptCompletionSchema: any;
|
|
572
758
|
|
|
573
759
|
/**
|
|
@@ -576,6 +762,8 @@ export declare const attemptCompletionSchema: any;
|
|
|
576
762
|
export declare const searchToolDefinition: any;
|
|
577
763
|
export declare const queryToolDefinition: any;
|
|
578
764
|
export declare const extractToolDefinition: any;
|
|
765
|
+
export declare const editToolDefinition: string;
|
|
766
|
+
export declare const createToolDefinition: string;
|
|
579
767
|
export declare const attemptCompletionToolDefinition: any;
|
|
580
768
|
|
|
581
769
|
/**
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ export interface ProbeAgentOptions {
|
|
|
35
35
|
systemPrompt?: string;
|
|
36
36
|
/** Predefined prompt type (persona) */
|
|
37
37
|
promptType?: 'code-explorer' | 'code-searcher' | 'engineer' | 'code-review' | 'support' | 'architect';
|
|
38
|
-
/** Allow the use of the '
|
|
38
|
+
/** Allow the use of the 'edit' and 'create' tools for code editing */
|
|
39
39
|
allowEdit?: boolean;
|
|
40
40
|
/** Enable the delegate tool for task distribution to subagents */
|
|
41
41
|
enableDelegate?: boolean;
|
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -57,7 +57,6 @@ import {
|
|
|
57
57
|
useSkillToolDefinition,
|
|
58
58
|
readImageToolDefinition,
|
|
59
59
|
attemptCompletionToolDefinition,
|
|
60
|
-
implementToolDefinition,
|
|
61
60
|
editToolDefinition,
|
|
62
61
|
createToolDefinition,
|
|
63
62
|
googleSearchToolDefinition,
|
|
@@ -66,6 +65,7 @@ import {
|
|
|
66
65
|
parseXmlToolCallWithThinking
|
|
67
66
|
} from './tools.js';
|
|
68
67
|
import { createMessagePreview, detectUnrecognizedToolCall, detectStuckResponse, areBothStuckResponses } from '../tools/common.js';
|
|
68
|
+
import { FileTracker } from '../tools/fileTracker.js';
|
|
69
69
|
import {
|
|
70
70
|
createWrappedTools,
|
|
71
71
|
listFilesToolInstance,
|
|
@@ -178,7 +178,7 @@ export class ProbeAgent {
|
|
|
178
178
|
* @param {string} [options.customPrompt] - Custom prompt to replace the default system message
|
|
179
179
|
* @param {string} [options.systemPrompt] - Alias for customPrompt; takes precedence when both are provided
|
|
180
180
|
* @param {string} [options.promptType] - Predefined prompt type (code-explorer, code-searcher, architect, code-review, support)
|
|
181
|
-
* @param {boolean} [options.allowEdit=false] - Allow the use of the '
|
|
181
|
+
* @param {boolean} [options.allowEdit=false] - Allow the use of the 'edit' and 'create' tools
|
|
182
182
|
* @param {boolean} [options.enableDelegate=false] - Enable the delegate tool for task distribution to subagents
|
|
183
183
|
* @param {boolean} [options.enableExecutePlan=false] - Enable the execute_plan DSL orchestration tool
|
|
184
184
|
* @param {string} [options.architectureFileName] - Architecture context filename to embed from repo root (defaults to AGENTS.md with CLAUDE.md fallback; ARCHITECTURE.md is always included when present)
|
|
@@ -229,6 +229,7 @@ export class ProbeAgent {
|
|
|
229
229
|
this.customPrompt = options.systemPrompt || options.customPrompt || null;
|
|
230
230
|
this.promptType = options.promptType || 'code-explorer';
|
|
231
231
|
this.allowEdit = !!options.allowEdit;
|
|
232
|
+
this.hashLines = options.hashLines !== undefined ? !!options.hashLines : this.allowEdit;
|
|
232
233
|
this.enableDelegate = !!options.enableDelegate;
|
|
233
234
|
this.enableExecutePlan = !!options.enableExecutePlan;
|
|
234
235
|
this.debug = options.debug || process.env.DEBUG === '1';
|
|
@@ -328,7 +329,8 @@ export class ProbeAgent {
|
|
|
328
329
|
if (this.debug) {
|
|
329
330
|
console.log(`[DEBUG] Generated session ID for agent: ${this.sessionId}`);
|
|
330
331
|
console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
|
|
331
|
-
console.log(`[DEBUG] Allow Edit
|
|
332
|
+
console.log(`[DEBUG] Allow Edit: ${this.allowEdit}`);
|
|
333
|
+
console.log(`[DEBUG] Hash Lines: ${this.hashLines}`);
|
|
332
334
|
console.log(`[DEBUG] Search delegation enabled: ${this.searchDelegate}`);
|
|
333
335
|
console.log(`[DEBUG] Workspace root: ${this.workspaceRoot}`);
|
|
334
336
|
console.log(`[DEBUG] Working directory (cwd): ${this.cwd}`);
|
|
@@ -831,9 +833,12 @@ export class ProbeAgent {
|
|
|
831
833
|
cwd: this.cwd,
|
|
832
834
|
workspaceRoot: this.workspaceRoot,
|
|
833
835
|
allowedFolders: this.allowedFolders,
|
|
836
|
+
// File state tracking for safe multi-edit workflows (only when editing is enabled)
|
|
837
|
+
fileTracker: this.allowEdit ? new FileTracker({ debug: this.debug }) : null,
|
|
834
838
|
outline: this.outline,
|
|
835
839
|
searchDelegate: this.searchDelegate,
|
|
836
840
|
allowEdit: this.allowEdit,
|
|
841
|
+
hashLines: this.hashLines,
|
|
837
842
|
enableDelegate: this.enableDelegate,
|
|
838
843
|
enableExecutePlan: this.enableExecutePlan,
|
|
839
844
|
enableBash: this.enableBash,
|
|
@@ -2553,16 +2558,12 @@ ${extractGuidance}
|
|
|
2553
2558
|
}
|
|
2554
2559
|
|
|
2555
2560
|
// Edit tools (require both allowEdit flag AND allowedTools permission)
|
|
2556
|
-
if (this.allowEdit && isToolAllowed('implement')) {
|
|
2557
|
-
toolDefinitions += `${implementToolDefinition}\n`;
|
|
2558
|
-
}
|
|
2559
2561
|
if (this.allowEdit && isToolAllowed('edit')) {
|
|
2560
2562
|
toolDefinitions += `${editToolDefinition}\n`;
|
|
2561
2563
|
}
|
|
2562
2564
|
if (this.allowEdit && isToolAllowed('create')) {
|
|
2563
2565
|
toolDefinitions += `${createToolDefinition}\n`;
|
|
2564
2566
|
}
|
|
2565
|
-
|
|
2566
2567
|
// Bash tool (require both enableBash flag AND allowedTools permission)
|
|
2567
2568
|
if (this.enableBash && isToolAllowed('bash')) {
|
|
2568
2569
|
toolDefinitions += `${bashToolDefinition}\n`;
|
|
@@ -2645,7 +2646,7 @@ The configuration is loaded from src/config.js lines 15-25 which contains the da
|
|
|
2645
2646
|
availableToolsList += '- query: Search code using structural AST patterns.\n';
|
|
2646
2647
|
}
|
|
2647
2648
|
if (isToolAllowed('extract')) {
|
|
2648
|
-
availableToolsList += '- extract: Extract specific code blocks or lines from files.\n';
|
|
2649
|
+
availableToolsList += '- extract: Extract specific code blocks or lines from files. Use with symbol targets (e.g. "file.js#funcName") to get line numbers for line-targeted editing.\n';
|
|
2649
2650
|
}
|
|
2650
2651
|
if (isToolAllowed('listFiles')) {
|
|
2651
2652
|
availableToolsList += '- listFiles: List files and directories in a specified location.\n';
|
|
@@ -2662,11 +2663,8 @@ The configuration is loaded from src/config.js lines 15-25 which contains the da
|
|
|
2662
2663
|
if (isToolAllowed('readImage')) {
|
|
2663
2664
|
availableToolsList += '- readImage: Read and load an image file for AI analysis.\n';
|
|
2664
2665
|
}
|
|
2665
|
-
if (this.allowEdit && isToolAllowed('implement')) {
|
|
2666
|
-
availableToolsList += '- implement: Implement a feature or fix a bug using aider.\n';
|
|
2667
|
-
}
|
|
2668
2666
|
if (this.allowEdit && isToolAllowed('edit')) {
|
|
2669
|
-
availableToolsList += '- edit: Edit files using
|
|
2667
|
+
availableToolsList += '- edit: Edit files using text replacement, AST-aware symbol operations, or line-targeted editing.\n';
|
|
2670
2668
|
}
|
|
2671
2669
|
if (this.allowEdit && isToolAllowed('create')) {
|
|
2672
2670
|
availableToolsList += '- create: Create new files with specified content.\n';
|
|
@@ -2757,8 +2755,14 @@ Follow these instructions carefully:
|
|
|
2757
2755
|
8. Once the task is fully completed, use the '<attempt_completion>' tool to provide the final result. This is the ONLY way to signal completion.
|
|
2758
2756
|
9. Prefer concise and focused search queries. Use specific keywords and phrases to narrow down results.${this.allowEdit ? `
|
|
2759
2757
|
10. When modifying files, choose the appropriate tool:
|
|
2760
|
-
- Use 'edit' for
|
|
2761
|
-
|
|
2758
|
+
- Use 'edit' for all code modifications:
|
|
2759
|
+
* For small changes (a line or a few lines), use old_string + new_string — copy old_string verbatim from the file.
|
|
2760
|
+
* For rewriting entire functions/classes/methods, use the symbol parameter instead (no exact text matching needed).
|
|
2761
|
+
* For editing specific lines from search/extract output, use start_line (and optionally end_line) with the line numbers shown in the output.${this.hashLines ? ' Line references include content hashes (e.g. "42:ab") for integrity verification.' : ''}
|
|
2762
|
+
* For editing inside large functions: first use extract with the symbol target (e.g. "file.js#myFunction") to see the function with line numbers${this.hashLines ? ' and hashes' : ''}, then use start_line/end_line to surgically edit specific lines within it.
|
|
2763
|
+
- Use 'create' for new files or complete file rewrites.
|
|
2764
|
+
- If an edit fails, read the error message — it tells you exactly how to fix the call and retry.
|
|
2765
|
+
- The system tracks which files you've seen via search/extract. If you try to edit a file you haven't read, or one that changed since you last read it, the edit will fail with instructions to re-read first. Always use extract before editing to ensure you have current file content.` : ''}
|
|
2762
2766
|
</instructions>
|
|
2763
2767
|
`;
|
|
2764
2768
|
|
|
@@ -3420,8 +3424,11 @@ Follow these instructions carefully:
|
|
|
3420
3424
|
validTools.push('attempt_completion');
|
|
3421
3425
|
|
|
3422
3426
|
// Edit tools (require both allowEdit flag AND allowedTools permission)
|
|
3423
|
-
if (this.allowEdit && this.allowedTools.isEnabled('
|
|
3424
|
-
validTools.push('
|
|
3427
|
+
if (this.allowEdit && this.allowedTools.isEnabled('edit')) {
|
|
3428
|
+
validTools.push('edit');
|
|
3429
|
+
}
|
|
3430
|
+
if (this.allowEdit && this.allowedTools.isEnabled('create')) {
|
|
3431
|
+
validTools.push('create');
|
|
3425
3432
|
}
|
|
3426
3433
|
// Bash tool (require both enableBash flag AND allowedTools permission)
|
|
3427
3434
|
if (this.enableBash && this.allowedTools.isEnabled('bash')) {
|
|
@@ -3804,6 +3811,7 @@ Follow these instructions carefully:
|
|
|
3804
3811
|
mcpConfigPath: this.mcpConfigPath, // Inherit MCP config path
|
|
3805
3812
|
enableBash: this.enableBash, // Inherit bash enablement
|
|
3806
3813
|
bashConfig: this.bashConfig, // Inherit bash configuration
|
|
3814
|
+
allowEdit: this.allowEdit, // Inherit edit/create permission
|
|
3807
3815
|
allowedTools: allowedToolsForDelegate, // Inherit allowed tools from parent
|
|
3808
3816
|
debug: this.debug,
|
|
3809
3817
|
tracer: this.tracer
|
|
@@ -4005,6 +4013,33 @@ Follow these instructions carefully:
|
|
|
4005
4013
|
break;
|
|
4006
4014
|
}
|
|
4007
4015
|
|
|
4016
|
+
// Issue #443: Check if response contains valid schema-matching JSON
|
|
4017
|
+
// Before triggering error.no_tool_call, strip markdown fences and validate
|
|
4018
|
+
// This handles cases where AI returns valid JSON without using attempt_completion
|
|
4019
|
+
if (options.schema) {
|
|
4020
|
+
// Remove thinking tags first
|
|
4021
|
+
let contentToCheck = assistantResponseContent;
|
|
4022
|
+
contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
|
|
4023
|
+
contentToCheck = contentToCheck.replace(/<thinking>[\s\S]*$/gi, '').trim();
|
|
4024
|
+
|
|
4025
|
+
// Try to extract and validate JSON
|
|
4026
|
+
const cleanedJson = cleanSchemaResponse(contentToCheck);
|
|
4027
|
+
try {
|
|
4028
|
+
JSON.parse(cleanedJson);
|
|
4029
|
+
const validation = validateJsonResponse(cleanedJson, { debug: this.debug, schema: options.schema });
|
|
4030
|
+
if (validation.isValid) {
|
|
4031
|
+
if (this.debug) {
|
|
4032
|
+
console.log(`[DEBUG] Issue #443: Accepting valid JSON response without attempt_completion (${cleanedJson.length} chars)`);
|
|
4033
|
+
}
|
|
4034
|
+
finalResult = cleanedJson;
|
|
4035
|
+
completionAttempted = true;
|
|
4036
|
+
break;
|
|
4037
|
+
}
|
|
4038
|
+
} catch {
|
|
4039
|
+
// Not valid JSON - continue to standard no_tool_call handling
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4008
4043
|
// Increment consecutive no-tool counter (catches alternating stuck responses)
|
|
4009
4044
|
consecutiveNoToolCount++;
|
|
4010
4045
|
|
package/src/agent/acp/tools.js
CHANGED
|
@@ -162,7 +162,8 @@ export class ACPToolManager {
|
|
|
162
162
|
return ToolCallKind.extract;
|
|
163
163
|
case 'delegate':
|
|
164
164
|
return ToolCallKind.execute;
|
|
165
|
-
case '
|
|
165
|
+
case 'edit':
|
|
166
|
+
case 'create':
|
|
166
167
|
return ToolCallKind.edit;
|
|
167
168
|
default:
|
|
168
169
|
return ToolCallKind.execute;
|
|
@@ -117,7 +117,8 @@ describe('ACPToolManager', () => {
|
|
|
117
117
|
expect(toolManager.getToolKind('query')).toBe(ToolCallKind.query);
|
|
118
118
|
expect(toolManager.getToolKind('extract')).toBe(ToolCallKind.extract);
|
|
119
119
|
expect(toolManager.getToolKind('delegate')).toBe(ToolCallKind.execute);
|
|
120
|
-
expect(toolManager.getToolKind('
|
|
120
|
+
expect(toolManager.getToolKind('edit')).toBe(ToolCallKind.edit);
|
|
121
|
+
expect(toolManager.getToolKind('create')).toBe(ToolCallKind.edit);
|
|
121
122
|
expect(toolManager.getToolKind('unknown')).toBe(ToolCallKind.execute);
|
|
122
123
|
});
|
|
123
124
|
});
|
|
@@ -183,6 +183,16 @@ export function generateSandboxGlobals(options) {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// Issue #444: Auto-coerce object paths to strings for search()
|
|
187
|
+
// AI-generated DSL sometimes passes field objects instead of field.file_path strings
|
|
188
|
+
if (params.path && typeof params.path === 'object') {
|
|
189
|
+
const coercedPath = params.path.file_path || params.path.path || params.path.directory || params.path.filename;
|
|
190
|
+
if (coercedPath && typeof coercedPath === 'string') {
|
|
191
|
+
logFn?.(`[${name}] Warning: Coerced object path to string "${coercedPath}" (issue #444)`);
|
|
192
|
+
params.path = coercedPath;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
const validated = schema.safeParse(params);
|
|
187
197
|
if (!validated.success) {
|
|
188
198
|
throw new Error(`Invalid parameters for ${name}: ${validated.error.message}`);
|
|
@@ -232,6 +242,15 @@ export function generateSandboxGlobals(options) {
|
|
|
232
242
|
// When schema is provided, auto-parse the JSON result for easier downstream processing
|
|
233
243
|
if (llmCall) {
|
|
234
244
|
const rawLLM = async (instruction, data, opts = {}) => {
|
|
245
|
+
// Issue #444: Guard against error strings being passed as data
|
|
246
|
+
// When previous tool calls fail, they return "ERROR: ..." strings
|
|
247
|
+
// Passing these to LLM() spawns useless delegates that can't help
|
|
248
|
+
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
|
|
249
|
+
if (dataStr && dataStr.startsWith('ERROR:')) {
|
|
250
|
+
logFn?.('[LLM] Blocked: data contains error from previous tool call');
|
|
251
|
+
return 'ERROR: Previous tool call failed - ' + dataStr.substring(0, 200);
|
|
252
|
+
}
|
|
253
|
+
|
|
235
254
|
const result = await llmCall(instruction, data, opts);
|
|
236
255
|
// Auto-parse JSON when schema is provided and result is a string
|
|
237
256
|
if (opts.schema && typeof result === 'string') {
|
package/src/agent/index.js
CHANGED
|
@@ -124,7 +124,8 @@ function parseArgs() {
|
|
|
124
124
|
schema: null,
|
|
125
125
|
provider: null,
|
|
126
126
|
model: null,
|
|
127
|
-
allowEdit: false,
|
|
127
|
+
allowEdit: process.env.ALLOW_EDIT === '1' || false,
|
|
128
|
+
hashLines: process.env.HASH_LINES !== undefined ? process.env.HASH_LINES === '1' : undefined,
|
|
128
129
|
enableDelegate: false,
|
|
129
130
|
verbose: false,
|
|
130
131
|
help: false,
|
|
@@ -167,6 +168,10 @@ function parseArgs() {
|
|
|
167
168
|
config.verbose = true;
|
|
168
169
|
} else if (arg === '--allow-edit') {
|
|
169
170
|
config.allowEdit = true;
|
|
171
|
+
} else if (arg === '--hash-lines') {
|
|
172
|
+
config.hashLines = true;
|
|
173
|
+
} else if (arg === '--no-hash-lines') {
|
|
174
|
+
config.hashLines = false;
|
|
170
175
|
} else if (arg === '--enable-delegate') {
|
|
171
176
|
config.enableDelegate = true;
|
|
172
177
|
} else if (arg === '--no-delegate') {
|
|
@@ -275,12 +280,14 @@ Options:
|
|
|
275
280
|
--schema <schema|file> Output schema (JSON, XML, any format - text or file path)
|
|
276
281
|
--provider <name> Force AI provider: anthropic, openai, google
|
|
277
282
|
--model <name> Override model name
|
|
278
|
-
--allow-edit Enable code modification capabilities
|
|
283
|
+
--allow-edit Enable code modification capabilities (edit + create tools)
|
|
284
|
+
--hash-lines Annotate search/extract output with line hashes (default: on when --allow-edit)
|
|
285
|
+
--no-hash-lines Disable line hash annotations even with --allow-edit
|
|
279
286
|
--enable-delegate Enable delegate tool for task distribution to subagents
|
|
280
287
|
--allowed-tools <tools> Filter available tools (comma-separated list)
|
|
281
288
|
Use '*' or 'all' for all tools (default)
|
|
282
289
|
Use 'none' or '' for no tools (raw AI mode)
|
|
283
|
-
Specific tools: search,query,extract,listFiles,searchFiles,listSkills,useSkill
|
|
290
|
+
Specific tools: search,query,extract,edit,create,listFiles,searchFiles,listSkills,useSkill
|
|
284
291
|
Supports exclusion: '*,!bash' (all except bash)
|
|
285
292
|
--disable-tools Disable all tools (raw AI mode, no code analysis)
|
|
286
293
|
Convenience flag equivalent to --allowed-tools none
|
|
@@ -318,6 +325,8 @@ Environment Variables:
|
|
|
318
325
|
FORCE_PROVIDER Force specific provider (anthropic, openai, google)
|
|
319
326
|
MODEL_NAME Override model name
|
|
320
327
|
MAX_RESPONSE_TOKENS Maximum tokens for AI response
|
|
328
|
+
ALLOW_EDIT Enable code modification (set to '1')
|
|
329
|
+
HASH_LINES Annotate output with line hashes (set to '1'; default: on with ALLOW_EDIT)
|
|
321
330
|
DEBUG Enable verbose mode (set to '1')
|
|
322
331
|
|
|
323
332
|
Examples:
|
|
@@ -334,6 +343,8 @@ Examples:
|
|
|
334
343
|
probe agent "Explain this code" --allowed-tools search,extract # Only search and extract
|
|
335
344
|
probe agent "What is this project about?" --allowed-tools none # Raw AI mode (no tools)
|
|
336
345
|
probe agent "Tell me about this project" --disable-tools # Raw AI mode (convenience flag)
|
|
346
|
+
probe agent "Fix the off-by-one error" --allow-edit --path ./src # Enable code editing
|
|
347
|
+
ALLOW_EDIT=1 probe agent "Refactor the login flow" # Edit via env var
|
|
337
348
|
probe agent --mcp # Start MCP server mode
|
|
338
349
|
probe agent --acp # Start ACP server mode
|
|
339
350
|
|
package/src/agent/schemaUtils.js
CHANGED
|
@@ -165,6 +165,39 @@ export function decodeHtmlEntities(text) {
|
|
|
165
165
|
return decoded;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Sanitize Markdown escape sequences in JSON strings
|
|
170
|
+
*
|
|
171
|
+
* Markdown uses backslash escapes like \*, \_, \#, \~ etc. which are NOT valid
|
|
172
|
+
* JSON escape sequences. When AI models produce JSON with Markdown content,
|
|
173
|
+
* these escapes cause JSON.parse() to fail with "Invalid \escape" errors.
|
|
174
|
+
*
|
|
175
|
+
* This function removes the backslash from invalid escape sequences while
|
|
176
|
+
* preserving valid JSON escapes: \\, \", \/, \b, \f, \n, \r, \t, \uXXXX
|
|
177
|
+
*
|
|
178
|
+
* @param {string} jsonString - JSON string that may contain Markdown escapes
|
|
179
|
+
* @returns {string} - JSON string with invalid escapes sanitized
|
|
180
|
+
*/
|
|
181
|
+
export function sanitizeMarkdownEscapesInJson(jsonString) {
|
|
182
|
+
if (!jsonString || typeof jsonString !== 'string') {
|
|
183
|
+
return jsonString;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Strategy: Match either:
|
|
187
|
+
// 1. \\\\ (escaped backslash) - preserve as-is
|
|
188
|
+
// 2. \\X where X is NOT a valid JSON escape char - remove the backslash
|
|
189
|
+
//
|
|
190
|
+
// Valid JSON escape chars: " \ / b f n r t u
|
|
191
|
+
// This converts: \* → *, \_ → _, \# → #, \~ → ~, etc.
|
|
192
|
+
// But preserves: \\, \", \n, \t, \r, \b, \f, \/, \uXXXX
|
|
193
|
+
return jsonString.replace(/\\\\|\\([^"\\\/bfnrtu])/g, (match, captured) => {
|
|
194
|
+
if (match === '\\\\') {
|
|
195
|
+
return '\\\\'; // Preserve escaped backslash
|
|
196
|
+
}
|
|
197
|
+
return captured; // Remove backslash from invalid escape
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
168
201
|
/**
|
|
169
202
|
* Normalize JavaScript syntax to valid JSON syntax
|
|
170
203
|
* Converts single quotes to double quotes for strings in JSON-like structures
|
|
@@ -261,6 +294,22 @@ export function cleanSchemaResponse(response) {
|
|
|
261
294
|
return cleanSchemaResponse(resultWrapperMatch[1]);
|
|
262
295
|
}
|
|
263
296
|
|
|
297
|
+
// Strip <tool_code>...</tool_code> wrapper (Gemini-style code execution format)
|
|
298
|
+
// Issue #443: Gemini sometimes wraps responses in <plan> + <tool_code> tags
|
|
299
|
+
// e.g., <tool_code>print(attempt_completion({"projects": ["repo1"]}))</tool_code>
|
|
300
|
+
const toolCodeMatch = trimmed.match(/<tool_code>\s*([\s\S]*?)\s*<\/tool_code>/);
|
|
301
|
+
if (toolCodeMatch) {
|
|
302
|
+
let innerContent = toolCodeMatch[1].trim();
|
|
303
|
+
// Extract JSON from print() or attempt_completion() wrappers
|
|
304
|
+
// e.g., print({"key": "value"}) or attempt_completion({"key": "value"})
|
|
305
|
+
const funcCallMatch = innerContent.match(/(?:print|attempt_completion)\s*\(\s*([{\[][\s\S]*[}\]])\s*\)/);
|
|
306
|
+
if (funcCallMatch) {
|
|
307
|
+
return cleanSchemaResponse(funcCallMatch[1]);
|
|
308
|
+
}
|
|
309
|
+
// Try cleaning the inner content directly
|
|
310
|
+
return cleanSchemaResponse(innerContent);
|
|
311
|
+
}
|
|
312
|
+
|
|
264
313
|
// First, look for JSON after code block markers - similar to mermaid extraction
|
|
265
314
|
// Try with json language specifier
|
|
266
315
|
const jsonBlockMatch = trimmed.match(/```json\s*\n([\s\S]*?)\n```/);
|
|
@@ -370,9 +419,30 @@ export function validateJsonResponse(response, options = {}) {
|
|
|
370
419
|
}
|
|
371
420
|
}
|
|
372
421
|
|
|
422
|
+
// Try to parse the response, with fallback to sanitizing Markdown escapes (issue #441)
|
|
423
|
+
let responseToValidate = response;
|
|
424
|
+
try {
|
|
425
|
+
JSON.parse(response);
|
|
426
|
+
} catch (initialError) {
|
|
427
|
+
// Check if the error is due to invalid escape sequences (Markdown escapes like \*, \_)
|
|
428
|
+
if (initialError.message && initialError.message.includes('escape')) {
|
|
429
|
+
const sanitized = sanitizeMarkdownEscapesInJson(response);
|
|
430
|
+
try {
|
|
431
|
+
JSON.parse(sanitized);
|
|
432
|
+
// Sanitized version parses - use it instead
|
|
433
|
+
responseToValidate = sanitized;
|
|
434
|
+
if (debug) {
|
|
435
|
+
console.log(`[DEBUG] JSON validation: Fixed Markdown escapes in JSON (issue #441)`);
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
// Sanitization didn't help, continue with original (will fail below with proper error)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
373
443
|
try {
|
|
374
444
|
const parseStart = Date.now();
|
|
375
|
-
const parsed = JSON.parse(
|
|
445
|
+
const parsed = JSON.parse(responseToValidate);
|
|
376
446
|
const parseTime = Date.now() - parseStart;
|
|
377
447
|
|
|
378
448
|
if (debug) {
|
|
@@ -853,7 +923,26 @@ export function tryAutoWrapForSimpleSchema(response, schema, options = {}) {
|
|
|
853
923
|
console.log(`[DEBUG] Auto-wrap: Response is already valid JSON, skipping`);
|
|
854
924
|
}
|
|
855
925
|
return null;
|
|
856
|
-
} catch {
|
|
926
|
+
} catch (initialError) {
|
|
927
|
+
// Not valid JSON - check if it's due to Markdown escapes (issue #441)
|
|
928
|
+
// AI models sometimes produce JSON with Markdown escapes like \* or \_
|
|
929
|
+
// which are valid Markdown but NOT valid JSON escape sequences
|
|
930
|
+
if (initialError.message && initialError.message.includes('escape')) {
|
|
931
|
+
try {
|
|
932
|
+
const sanitized = sanitizeMarkdownEscapesInJson(response);
|
|
933
|
+
JSON.parse(sanitized);
|
|
934
|
+
// Sanitized JSON is valid! Return it instead of wrapping
|
|
935
|
+
if (debug) {
|
|
936
|
+
console.log(`[DEBUG] Auto-wrap: Fixed Markdown escapes in JSON (issue #441), returning sanitized JSON`);
|
|
937
|
+
}
|
|
938
|
+
return sanitized;
|
|
939
|
+
} catch {
|
|
940
|
+
// Sanitization didn't help, proceed with wrapping
|
|
941
|
+
if (debug) {
|
|
942
|
+
console.log(`[DEBUG] Auto-wrap: Markdown escape sanitization didn't fix JSON, proceeding with wrapping`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
857
946
|
// Not valid JSON, proceed with wrapping
|
|
858
947
|
}
|
|
859
948
|
|