wcgw 3.0.7__py3-none-any.whl → 4.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of wcgw might be problematic. Click here for more details.
- wcgw/client/bash_state/bash_state.py +182 -13
- wcgw/client/diff-instructions.txt +29 -15
- wcgw/client/file_ops/diff_edit.py +2 -1
- wcgw/client/file_ops/search_replace.py +37 -21
- wcgw/client/memory.py +5 -2
- wcgw/client/modes.py +7 -7
- wcgw/client/repo_ops/display_tree.py +3 -3
- wcgw/client/repo_ops/file_stats.py +152 -0
- wcgw/client/repo_ops/repo_context.py +122 -4
- wcgw/client/tool_prompts.py +13 -16
- wcgw/client/tools.py +479 -80
- wcgw/relay/serve.py +8 -53
- wcgw/types_.py +103 -16
- {wcgw-3.0.7.dist-info → wcgw-4.0.0.dist-info}/METADATA +36 -19
- {wcgw-3.0.7.dist-info → wcgw-4.0.0.dist-info}/RECORD +20 -19
- wcgw_cli/anthropic_client.py +1 -1
- wcgw_cli/openai_client.py +1 -1
- {wcgw-3.0.7.dist-info → wcgw-4.0.0.dist-info}/WHEEL +0 -0
- {wcgw-3.0.7.dist-info → wcgw-4.0.0.dist-info}/entry_points.txt +0 -0
- {wcgw-3.0.7.dist-info → wcgw-4.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ import threading
|
|
|
6
6
|
import time
|
|
7
7
|
import traceback
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
+
from hashlib import sha256
|
|
9
10
|
from typing import (
|
|
10
11
|
Any,
|
|
11
12
|
Literal,
|
|
@@ -232,11 +233,13 @@ class BashState:
|
|
|
232
233
|
write_if_empty_mode: Optional[WriteIfEmptyMode],
|
|
233
234
|
mode: Optional[Modes],
|
|
234
235
|
use_screen: bool,
|
|
235
|
-
whitelist_for_overwrite: Optional[
|
|
236
|
+
whitelist_for_overwrite: Optional[dict[str, "FileWhitelistData"]] = None,
|
|
236
237
|
) -> None:
|
|
237
238
|
self._last_command: str = ""
|
|
238
239
|
self.console = console
|
|
239
240
|
self._cwd = working_dir or os.getcwd()
|
|
241
|
+
# Store the workspace root separately from the current working directory
|
|
242
|
+
self._workspace_root = working_dir or os.getcwd()
|
|
240
243
|
self._bash_command_mode: BashCommandMode = bash_command_mode or BashCommandMode(
|
|
241
244
|
"normal_mode", "all"
|
|
242
245
|
)
|
|
@@ -245,7 +248,9 @@ class BashState:
|
|
|
245
248
|
write_if_empty_mode or WriteIfEmptyMode("all")
|
|
246
249
|
)
|
|
247
250
|
self._mode = mode or "wcgw"
|
|
248
|
-
self._whitelist_for_overwrite:
|
|
251
|
+
self._whitelist_for_overwrite: dict[str, FileWhitelistData] = (
|
|
252
|
+
whitelist_for_overwrite or {}
|
|
253
|
+
)
|
|
249
254
|
self._bg_expect_thread: Optional[threading.Thread] = None
|
|
250
255
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
251
256
|
self._use_screen = use_screen
|
|
@@ -314,9 +319,11 @@ class BashState:
|
|
|
314
319
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
315
320
|
|
|
316
321
|
def cleanup(self) -> None:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
try:
|
|
323
|
+
self.close_bg_expect_thread()
|
|
324
|
+
self._shell.close(True)
|
|
325
|
+
finally:
|
|
326
|
+
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
320
327
|
|
|
321
328
|
def __enter__(self) -> "BashState":
|
|
322
329
|
return self
|
|
@@ -427,6 +434,15 @@ class BashState:
|
|
|
427
434
|
def cwd(self) -> str:
|
|
428
435
|
return self._cwd
|
|
429
436
|
|
|
437
|
+
@property
|
|
438
|
+
def workspace_root(self) -> str:
|
|
439
|
+
"""Return the workspace root directory."""
|
|
440
|
+
return self._workspace_root
|
|
441
|
+
|
|
442
|
+
def set_workspace_root(self, workspace_root: str) -> None:
|
|
443
|
+
"""Set the workspace root directory."""
|
|
444
|
+
self._workspace_root = workspace_root
|
|
445
|
+
|
|
430
446
|
@property
|
|
431
447
|
def prompt(self) -> str:
|
|
432
448
|
return PROMPT_CONST
|
|
@@ -454,20 +470,56 @@ class BashState:
|
|
|
454
470
|
"bash_command_mode": self._bash_command_mode.serialize(),
|
|
455
471
|
"file_edit_mode": self._file_edit_mode.serialize(),
|
|
456
472
|
"write_if_empty_mode": self._write_if_empty_mode.serialize(),
|
|
457
|
-
"whitelist_for_overwrite":
|
|
473
|
+
"whitelist_for_overwrite": {
|
|
474
|
+
k: v.serialize() for k, v in self._whitelist_for_overwrite.items()
|
|
475
|
+
},
|
|
458
476
|
"mode": self._mode,
|
|
477
|
+
"workspace_root": self._workspace_root,
|
|
459
478
|
}
|
|
460
479
|
|
|
461
480
|
@staticmethod
|
|
462
481
|
def parse_state(
|
|
463
482
|
state: dict[str, Any],
|
|
464
|
-
) -> tuple[
|
|
483
|
+
) -> tuple[
|
|
484
|
+
BashCommandMode,
|
|
485
|
+
FileEditMode,
|
|
486
|
+
WriteIfEmptyMode,
|
|
487
|
+
Modes,
|
|
488
|
+
dict[str, "FileWhitelistData"],
|
|
489
|
+
str,
|
|
490
|
+
]:
|
|
491
|
+
whitelist_state = state["whitelist_for_overwrite"]
|
|
492
|
+
# Convert serialized whitelist data back to FileWhitelistData objects
|
|
493
|
+
whitelist_dict = {}
|
|
494
|
+
if isinstance(whitelist_state, dict):
|
|
495
|
+
for file_path, data in whitelist_state.items():
|
|
496
|
+
if isinstance(data, dict) and "file_hash" in data:
|
|
497
|
+
# New format
|
|
498
|
+
whitelist_dict[file_path] = FileWhitelistData.deserialize(data)
|
|
499
|
+
else:
|
|
500
|
+
# Legacy format (just a hash string)
|
|
501
|
+
# Try to get line count from file if it exists, otherwise use a large default
|
|
502
|
+
whitelist_dict[file_path] = FileWhitelistData(
|
|
503
|
+
file_hash=data if isinstance(data, str) else "",
|
|
504
|
+
line_ranges_read=[(1, 1000000)], # Assume entire file was read
|
|
505
|
+
total_lines=1000000,
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
# Handle really old format if needed
|
|
509
|
+
whitelist_dict = {
|
|
510
|
+
k: FileWhitelistData(
|
|
511
|
+
file_hash="", line_ranges_read=[(1, 1000000)], total_lines=1000000
|
|
512
|
+
)
|
|
513
|
+
for k in whitelist_state
|
|
514
|
+
}
|
|
515
|
+
|
|
465
516
|
return (
|
|
466
517
|
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
467
518
|
FileEditMode.deserialize(state["file_edit_mode"]),
|
|
468
519
|
WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
|
|
469
520
|
state["mode"],
|
|
470
|
-
|
|
521
|
+
whitelist_dict,
|
|
522
|
+
state.get("workspace_root", ""),
|
|
471
523
|
)
|
|
472
524
|
|
|
473
525
|
def load_state(
|
|
@@ -476,15 +528,17 @@ class BashState:
|
|
|
476
528
|
file_edit_mode: FileEditMode,
|
|
477
529
|
write_if_empty_mode: WriteIfEmptyMode,
|
|
478
530
|
mode: Modes,
|
|
479
|
-
whitelist_for_overwrite:
|
|
531
|
+
whitelist_for_overwrite: dict[str, "FileWhitelistData"],
|
|
480
532
|
cwd: str,
|
|
533
|
+
workspace_root: str,
|
|
481
534
|
) -> None:
|
|
482
535
|
"""Create a new BashState instance from a serialized state dictionary"""
|
|
483
536
|
self._bash_command_mode = bash_command_mode
|
|
484
537
|
self._cwd = cwd or self._cwd
|
|
538
|
+
self._workspace_root = workspace_root or cwd or self._workspace_root
|
|
485
539
|
self._file_edit_mode = file_edit_mode
|
|
486
540
|
self._write_if_empty_mode = write_if_empty_mode
|
|
487
|
-
self._whitelist_for_overwrite =
|
|
541
|
+
self._whitelist_for_overwrite = dict(whitelist_for_overwrite)
|
|
488
542
|
self._mode = mode
|
|
489
543
|
self.reset_shell()
|
|
490
544
|
|
|
@@ -505,17 +559,132 @@ class BashState:
|
|
|
505
559
|
return "Not pending"
|
|
506
560
|
|
|
507
561
|
@property
|
|
508
|
-
def whitelist_for_overwrite(self) ->
|
|
562
|
+
def whitelist_for_overwrite(self) -> dict[str, "FileWhitelistData"]:
|
|
509
563
|
return self._whitelist_for_overwrite
|
|
510
564
|
|
|
511
|
-
def add_to_whitelist_for_overwrite(
|
|
512
|
-
self
|
|
565
|
+
def add_to_whitelist_for_overwrite(
|
|
566
|
+
self, file_paths_with_ranges: dict[str, list[tuple[int, int]]]
|
|
567
|
+
) -> None:
|
|
568
|
+
"""
|
|
569
|
+
Add files to the whitelist for overwrite.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
file_paths_with_ranges: Dictionary mapping file paths to sequences of
|
|
573
|
+
(start_line, end_line) tuples representing
|
|
574
|
+
the ranges that have been read.
|
|
575
|
+
"""
|
|
576
|
+
for file_path, ranges in file_paths_with_ranges.items():
|
|
577
|
+
# Read the file to get its hash and count lines
|
|
578
|
+
with open(file_path, "rb") as f:
|
|
579
|
+
file_content = f.read()
|
|
580
|
+
file_hash = sha256(file_content).hexdigest()
|
|
581
|
+
total_lines = file_content.count(b"\n") + 1
|
|
582
|
+
|
|
583
|
+
# Update or create whitelist entry
|
|
584
|
+
if file_path in self._whitelist_for_overwrite:
|
|
585
|
+
# Update existing entry
|
|
586
|
+
whitelist_data = self._whitelist_for_overwrite[file_path]
|
|
587
|
+
whitelist_data.file_hash = file_hash
|
|
588
|
+
whitelist_data.total_lines = total_lines
|
|
589
|
+
for range_start, range_end in ranges:
|
|
590
|
+
whitelist_data.add_range(range_start, range_end)
|
|
591
|
+
else:
|
|
592
|
+
# Create new entry
|
|
593
|
+
self._whitelist_for_overwrite[file_path] = FileWhitelistData(
|
|
594
|
+
file_hash=file_hash,
|
|
595
|
+
line_ranges_read=list(ranges),
|
|
596
|
+
total_lines=total_lines,
|
|
597
|
+
)
|
|
513
598
|
|
|
514
599
|
@property
|
|
515
600
|
def pending_output(self) -> str:
|
|
516
601
|
return self._pending_output
|
|
517
602
|
|
|
518
603
|
|
|
604
|
+
@dataclass
|
|
605
|
+
class FileWhitelistData:
|
|
606
|
+
"""Data about a file that has been read and can be modified."""
|
|
607
|
+
|
|
608
|
+
file_hash: str
|
|
609
|
+
# List of line ranges that have been read (inclusive start, inclusive end)
|
|
610
|
+
# E.g., [(1, 10), (20, 30)] means lines 1-10 and 20-30 have been read
|
|
611
|
+
line_ranges_read: list[tuple[int, int]]
|
|
612
|
+
# Total number of lines in the file
|
|
613
|
+
total_lines: int
|
|
614
|
+
|
|
615
|
+
def get_percentage_read(self) -> float:
|
|
616
|
+
"""Calculate percentage of file read based on line ranges."""
|
|
617
|
+
if self.total_lines == 0:
|
|
618
|
+
return 100.0
|
|
619
|
+
|
|
620
|
+
# Count unique lines read
|
|
621
|
+
lines_read: set[int] = set()
|
|
622
|
+
for start, end in self.line_ranges_read:
|
|
623
|
+
lines_read.update(range(start, end + 1))
|
|
624
|
+
|
|
625
|
+
return (len(lines_read) / self.total_lines) * 100.0
|
|
626
|
+
|
|
627
|
+
def is_read_enough(self) -> bool:
|
|
628
|
+
"""Check if enough of the file has been read (>=99%)"""
|
|
629
|
+
return self.get_percentage_read() >= 99
|
|
630
|
+
|
|
631
|
+
def get_unread_ranges(self) -> list[tuple[int, int]]:
|
|
632
|
+
"""Return a list of line ranges (start, end) that haven't been read yet.
|
|
633
|
+
|
|
634
|
+
Returns line ranges as tuples of (start_line, end_line) in 1-indexed format.
|
|
635
|
+
If the whole file has been read, returns an empty list.
|
|
636
|
+
"""
|
|
637
|
+
if self.total_lines == 0:
|
|
638
|
+
return []
|
|
639
|
+
|
|
640
|
+
# First collect all lines that have been read
|
|
641
|
+
lines_read: set[int] = set()
|
|
642
|
+
for start, end in self.line_ranges_read:
|
|
643
|
+
lines_read.update(range(start, end + 1))
|
|
644
|
+
|
|
645
|
+
# Generate unread ranges from the gaps
|
|
646
|
+
unread_ranges: list[tuple[int, int]] = []
|
|
647
|
+
start_range = None
|
|
648
|
+
|
|
649
|
+
for i in range(1, self.total_lines + 1):
|
|
650
|
+
if i not in lines_read:
|
|
651
|
+
if start_range is None:
|
|
652
|
+
start_range = i
|
|
653
|
+
elif start_range is not None:
|
|
654
|
+
# End of an unread range
|
|
655
|
+
unread_ranges.append((start_range, i - 1))
|
|
656
|
+
start_range = None
|
|
657
|
+
|
|
658
|
+
# Don't forget the last range if it extends to the end of the file
|
|
659
|
+
if start_range is not None:
|
|
660
|
+
unread_ranges.append((start_range, self.total_lines))
|
|
661
|
+
|
|
662
|
+
return unread_ranges
|
|
663
|
+
|
|
664
|
+
def add_range(self, start: int, end: int) -> None:
|
|
665
|
+
"""Add a new range of lines that have been read."""
|
|
666
|
+
# Merge with existing ranges if possible
|
|
667
|
+
self.line_ranges_read.append((start, end))
|
|
668
|
+
# Could add range merging logic here for optimization
|
|
669
|
+
|
|
670
|
+
def serialize(self) -> dict[str, Any]:
|
|
671
|
+
"""Convert to a serializable dictionary."""
|
|
672
|
+
return {
|
|
673
|
+
"file_hash": self.file_hash,
|
|
674
|
+
"line_ranges_read": self.line_ranges_read,
|
|
675
|
+
"total_lines": self.total_lines,
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
@classmethod
|
|
679
|
+
def deserialize(cls, data: dict[str, Any]) -> "FileWhitelistData":
|
|
680
|
+
"""Create from a serialized dictionary."""
|
|
681
|
+
return cls(
|
|
682
|
+
file_hash=data.get("file_hash", ""),
|
|
683
|
+
line_ranges_read=data.get("line_ranges_read", []),
|
|
684
|
+
total_lines=data.get("total_lines", 0),
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
|
|
519
688
|
WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
|
|
520
689
|
1. Get its output using status check.
|
|
521
690
|
2. Use `send_ascii` or `send_specials` to give inputs to the running program OR
|
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
|
|
2
2
|
Instructions for editing files.
|
|
3
|
+
# Example
|
|
4
|
+
## Input file
|
|
5
|
+
```
|
|
6
|
+
import numpy as np
|
|
7
|
+
from impls import impl1, impl2
|
|
8
|
+
|
|
9
|
+
def hello():
|
|
10
|
+
"print a greeting"
|
|
3
11
|
|
|
12
|
+
print("hello")
|
|
4
13
|
|
|
5
|
-
|
|
14
|
+
def call_hello():
|
|
15
|
+
"call hello"
|
|
16
|
+
|
|
17
|
+
hello()
|
|
18
|
+
print("Called")
|
|
19
|
+
impl1()
|
|
20
|
+
hello()
|
|
21
|
+
impl2()
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
## Edit format on the input file
|
|
6
25
|
```
|
|
7
|
-
|
|
26
|
+
<<<<<<< SEARCH
|
|
27
|
+
from impls import impl1, impl2
|
|
28
|
+
=======
|
|
29
|
+
from impls import impl1, impl2
|
|
30
|
+
from hello import hello as hello_renamed
|
|
31
|
+
>>>>>>> REPLACE
|
|
8
32
|
<<<<<<< SEARCH
|
|
9
33
|
def hello():
|
|
10
34
|
"print a greeting"
|
|
11
35
|
|
|
12
36
|
print("hello")
|
|
13
37
|
=======
|
|
14
|
-
from hello import hello as hello_renamed
|
|
15
38
|
>>>>>>> REPLACE
|
|
16
39
|
<<<<<<< SEARCH
|
|
17
40
|
def call_hello():
|
|
@@ -33,26 +56,17 @@ def call_hello_renamed():
|
|
|
33
56
|
hello_renamed()
|
|
34
57
|
impl2()
|
|
35
58
|
>>>>>>> REPLACE
|
|
36
|
-
"""
|
|
37
59
|
```
|
|
38
60
|
|
|
39
61
|
# *SEARCH/REPLACE block* Rules:
|
|
40
|
-
|
|
41
|
-
Every *SEARCH/REPLACE block* must use this format:
|
|
42
|
-
1. The start of match block: <<<<<<< SEARCH
|
|
43
|
-
2. A contiguous chunk of lines to do exact match for in the existing source code
|
|
44
|
-
3. The dividing line: =======
|
|
45
|
-
4. The lines to replace into the source code
|
|
46
|
-
5. The end of the replace block: >>>>>>> REPLACE
|
|
47
|
-
|
|
48
62
|
Every "<<<<<<< SEARCH" section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, whitespaces, etc.
|
|
49
63
|
|
|
50
64
|
Including multiple unique *SEARCH/REPLACE* blocks if needed.
|
|
51
|
-
Include enough lines in each SEARCH section to uniquely match each set of lines that need to change.
|
|
65
|
+
Include enough and only enough lines in each SEARCH section to uniquely match each set of lines that need to change.
|
|
52
66
|
|
|
53
67
|
Keep *SEARCH/REPLACE* blocks concise.
|
|
54
68
|
Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
|
|
55
|
-
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
|
|
56
|
-
|
|
69
|
+
Include just the changing lines, and a few surrounding lines (2-3 lines) if needed for uniqueness.
|
|
70
|
+
Other than for uniqueness, avoid including those lines which do not change in search (and replace) blocks. Target 2-3 non trivial extra lines per block.
|
|
57
71
|
|
|
58
72
|
Preserve leading spaces and indentations in both SEARCH and REPLACE blocks.
|
|
@@ -45,11 +45,12 @@ class FileEditOutput:
|
|
|
45
45
|
if tol.severity_cat == "WARNING":
|
|
46
46
|
warnings.add(tol.error_name)
|
|
47
47
|
elif tol.severity_cat == "ERROR":
|
|
48
|
+
search__ = "\n".join(search_)
|
|
48
49
|
errors.append(f"""
|
|
49
50
|
Got error while processing the following search block:
|
|
50
51
|
---
|
|
51
52
|
```
|
|
52
|
-
{
|
|
53
|
+
{search__}
|
|
53
54
|
```
|
|
54
55
|
---
|
|
55
56
|
Error:
|
|
@@ -5,67 +5,79 @@ from .diff_edit import FileEditInput, FileEditOutput, SearchReplaceMatchError
|
|
|
5
5
|
|
|
6
6
|
# Global regex patterns
|
|
7
7
|
SEARCH_MARKER = re.compile(r"^<<<<<<+\s*SEARCH\s*$")
|
|
8
|
-
DIVIDER_MARKER = re.compile(r"^======*\s*$")
|
|
8
|
+
DIVIDER_MARKER = re.compile(r"^======*\s*$")
|
|
9
9
|
REPLACE_MARKER = re.compile(r"^>>>>>>+\s*REPLACE\s*$")
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class SearchReplaceSyntaxError(Exception):
|
|
12
13
|
def __init__(self, message: str):
|
|
13
|
-
message =f"""Got syntax error while parsing search replace blocks:
|
|
14
|
+
message = f"""Got syntax error while parsing search replace blocks:
|
|
14
15
|
{message}
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
Make sure blocks are in correct sequence, and the markers are in separate lines:
|
|
18
19
|
|
|
19
|
-
<{
|
|
20
|
+
<{"<<<<<< SEARCH"}
|
|
20
21
|
example old
|
|
21
22
|
=======
|
|
22
23
|
example new
|
|
23
|
-
>{
|
|
24
|
+
>{">>>>>> REPLACE"}
|
|
24
25
|
|
|
25
26
|
"""
|
|
26
27
|
super().__init__(message)
|
|
27
28
|
|
|
29
|
+
|
|
28
30
|
def search_replace_edit(
|
|
29
31
|
lines: list[str], original_content: str, logger: Callable[[str], object]
|
|
30
32
|
) -> tuple[str, str]:
|
|
31
33
|
if not lines:
|
|
32
34
|
raise SearchReplaceSyntaxError("Error: No input to search replace edit")
|
|
33
|
-
|
|
35
|
+
|
|
34
36
|
original_lines = original_content.split("\n")
|
|
35
37
|
n_lines = len(lines)
|
|
36
38
|
i = 0
|
|
37
39
|
search_replace_blocks = list[tuple[list[str], list[str]]]()
|
|
38
|
-
|
|
40
|
+
|
|
39
41
|
while i < n_lines:
|
|
40
42
|
if SEARCH_MARKER.match(lines[i]):
|
|
41
43
|
line_num = i + 1
|
|
42
44
|
search_block = []
|
|
43
45
|
i += 1
|
|
44
|
-
|
|
46
|
+
|
|
45
47
|
while i < n_lines and not DIVIDER_MARKER.match(lines[i]):
|
|
46
48
|
if SEARCH_MARKER.match(lines[i]) or REPLACE_MARKER.match(lines[i]):
|
|
47
|
-
raise SearchReplaceSyntaxError(
|
|
49
|
+
raise SearchReplaceSyntaxError(
|
|
50
|
+
f"Line {i + 1}: Found stray marker in SEARCH block: {lines[i]}"
|
|
51
|
+
)
|
|
48
52
|
search_block.append(lines[i])
|
|
49
53
|
i += 1
|
|
50
|
-
|
|
54
|
+
|
|
51
55
|
if i >= n_lines:
|
|
52
|
-
raise SearchReplaceSyntaxError(
|
|
53
|
-
|
|
56
|
+
raise SearchReplaceSyntaxError(
|
|
57
|
+
f"Line {line_num}: Unclosed SEARCH block - missing ======= marker"
|
|
58
|
+
)
|
|
59
|
+
|
|
54
60
|
if not search_block:
|
|
55
|
-
raise SearchReplaceSyntaxError(
|
|
56
|
-
|
|
61
|
+
raise SearchReplaceSyntaxError(
|
|
62
|
+
f"Line {line_num}: SEARCH block cannot be empty"
|
|
63
|
+
)
|
|
64
|
+
|
|
57
65
|
i += 1
|
|
58
66
|
replace_block = []
|
|
59
|
-
|
|
67
|
+
|
|
60
68
|
while i < n_lines and not REPLACE_MARKER.match(lines[i]):
|
|
61
69
|
if SEARCH_MARKER.match(lines[i]) or DIVIDER_MARKER.match(lines[i]):
|
|
62
|
-
raise SearchReplaceSyntaxError(
|
|
70
|
+
raise SearchReplaceSyntaxError(
|
|
71
|
+
f"Line {i + 1}: Found stray marker in REPLACE block: {lines[i]}"
|
|
72
|
+
)
|
|
63
73
|
replace_block.append(lines[i])
|
|
64
74
|
i += 1
|
|
65
|
-
|
|
75
|
+
|
|
66
76
|
if i >= n_lines:
|
|
67
|
-
raise SearchReplaceSyntaxError(
|
|
68
|
-
|
|
77
|
+
raise SearchReplaceSyntaxError(
|
|
78
|
+
f"Line {line_num}: Unclosed block - missing REPLACE marker"
|
|
79
|
+
)
|
|
80
|
+
|
|
69
81
|
i += 1
|
|
70
82
|
|
|
71
83
|
for line in search_block:
|
|
@@ -78,7 +90,9 @@ def search_replace_edit(
|
|
|
78
90
|
search_replace_blocks.append((search_block, replace_block))
|
|
79
91
|
else:
|
|
80
92
|
if REPLACE_MARKER.match(lines[i]) or DIVIDER_MARKER.match(lines[i]):
|
|
81
|
-
raise SearchReplaceSyntaxError(
|
|
93
|
+
raise SearchReplaceSyntaxError(
|
|
94
|
+
f"Line {i + 1}: Found stray marker outside block: {lines[i]}"
|
|
95
|
+
)
|
|
82
96
|
i += 1
|
|
83
97
|
|
|
84
98
|
if not search_replace_blocks:
|
|
@@ -121,11 +135,12 @@ def greedy_context_replace(
|
|
|
121
135
|
if len(best_matches) > 1:
|
|
122
136
|
# Duplicate found, try to ground using previous blocks.
|
|
123
137
|
if current_block_offset == 0:
|
|
138
|
+
matches_ = "\n".join(current_blocks[-1][0])
|
|
124
139
|
raise SearchReplaceMatchError(f"""
|
|
125
140
|
The following block matched more than once:
|
|
126
141
|
---
|
|
127
142
|
```
|
|
128
|
-
{
|
|
143
|
+
{matches_}
|
|
129
144
|
```
|
|
130
145
|
""")
|
|
131
146
|
|
|
@@ -140,11 +155,12 @@ def greedy_context_replace(
|
|
|
140
155
|
original_lines, search_replace_blocks, original_lines, set(), 0
|
|
141
156
|
)
|
|
142
157
|
except Exception:
|
|
158
|
+
ma_more = "\n".join(current_blocks[-1][0])
|
|
143
159
|
raise Exception(f"""
|
|
144
160
|
The following block matched more than once:
|
|
145
161
|
---
|
|
146
162
|
```
|
|
147
|
-
{
|
|
163
|
+
{ma_more}
|
|
148
164
|
```
|
|
149
165
|
""")
|
|
150
166
|
|
wcgw/client/memory.py
CHANGED
|
@@ -2,7 +2,7 @@ import json
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
import shlex
|
|
5
|
-
from typing import Any, Callable, Optional
|
|
5
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
6
6
|
|
|
7
7
|
from ..types_ import ContextSave
|
|
8
8
|
|
|
@@ -59,7 +59,10 @@ def save_memory(
|
|
|
59
59
|
return memory_file_full
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
T = TypeVar("T")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_memory(
|
|
63
66
|
task_id: str,
|
|
64
67
|
max_tokens: Optional[int],
|
|
65
68
|
encoder: Callable[[str], list[T]],
|
wcgw/client/modes.py
CHANGED
|
@@ -55,38 +55,38 @@ You are now running in "code_writer" mode.
|
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
path_prompt = """
|
|
58
|
-
- You are allowed to
|
|
58
|
+
- You are allowed to edit files in the provided repository only.
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
61
|
if allowed_file_edit_globs != "all":
|
|
62
62
|
if allowed_file_edit_globs:
|
|
63
63
|
path_prompt = f"""
|
|
64
|
-
- You are allowed to
|
|
64
|
+
- You are allowed to edit files for files matching only the following globs: {", ".join(allowed_file_edit_globs)}
|
|
65
65
|
"""
|
|
66
66
|
else:
|
|
67
67
|
path_prompt = """
|
|
68
|
-
- You are not allowed to
|
|
68
|
+
- You are not allowed to edit files.
|
|
69
69
|
"""
|
|
70
70
|
base += path_prompt
|
|
71
71
|
|
|
72
72
|
path_prompt = """
|
|
73
|
-
- You are allowed to
|
|
73
|
+
- You are allowed to write files in the provided repository only.
|
|
74
74
|
"""
|
|
75
75
|
|
|
76
76
|
if all_write_new_globs != "all":
|
|
77
77
|
if all_write_new_globs:
|
|
78
78
|
path_prompt = f"""
|
|
79
|
-
- You are allowed to
|
|
79
|
+
- You are allowed to write files files matching only the following globs: {", ".join(allowed_file_edit_globs)}
|
|
80
80
|
"""
|
|
81
81
|
else:
|
|
82
82
|
path_prompt = """
|
|
83
|
-
- You are not allowed to
|
|
83
|
+
- You are not allowed to write files.
|
|
84
84
|
"""
|
|
85
85
|
base += path_prompt
|
|
86
86
|
|
|
87
87
|
run_command_common = """
|
|
88
88
|
- Do not use Ctrl-c interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
89
|
-
- Do not use echo to write multi-line files, always use
|
|
89
|
+
- Do not use echo to write multi-line files, always use FileWriteOrEdit tool to update a code.
|
|
90
90
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
91
91
|
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
92
92
|
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
@@ -33,13 +33,13 @@ class DirectoryTree:
|
|
|
33
33
|
abs_path = self.root / rel_path
|
|
34
34
|
|
|
35
35
|
if not abs_path.exists():
|
|
36
|
-
|
|
36
|
+
return
|
|
37
37
|
|
|
38
38
|
if not abs_path.is_file():
|
|
39
|
-
|
|
39
|
+
return
|
|
40
40
|
|
|
41
41
|
if not str(abs_path).startswith(str(self.root)):
|
|
42
|
-
|
|
42
|
+
return
|
|
43
43
|
|
|
44
44
|
self.expanded_files.add(abs_path)
|
|
45
45
|
|