wcgw 2.7.2__py3-none-any.whl → 2.8.1__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/anthropic_client.py +8 -16
- wcgw/client/mcp_server/server.py +29 -51
- wcgw/client/memory.py +23 -4
- wcgw/client/modes.py +242 -0
- wcgw/client/openai_client.py +8 -17
- wcgw/client/tools.py +228 -50
- wcgw/types_.py +51 -7
- {wcgw-2.7.2.dist-info → wcgw-2.8.1.dist-info}/METADATA +19 -2
- {wcgw-2.7.2.dist-info → wcgw-2.8.1.dist-info}/RECORD +12 -11
- {wcgw-2.7.2.dist-info → wcgw-2.8.1.dist-info}/WHEEL +0 -0
- {wcgw-2.7.2.dist-info → wcgw-2.8.1.dist-info}/entry_points.txt +0 -0
- {wcgw-2.7.2.dist-info → wcgw-2.8.1.dist-info}/licenses/LICENSE +0 -0
wcgw/client/anthropic_client.py
CHANGED
|
@@ -130,7 +130,7 @@ def loop(
|
|
|
130
130
|
memory = None
|
|
131
131
|
if resume:
|
|
132
132
|
try:
|
|
133
|
-
_, memory = load_memory(
|
|
133
|
+
_, memory, _ = load_memory(
|
|
134
134
|
resume,
|
|
135
135
|
8000,
|
|
136
136
|
lambda x: default_enc.encode(x).ids,
|
|
@@ -166,6 +166,7 @@ def loop(
|
|
|
166
166
|
- The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
|
|
167
167
|
- Run long running commands in background using screen instead of "&".
|
|
168
168
|
- Use longer wait_for_seconds if the command is expected to run for a long time.
|
|
169
|
+
- Do not use 'cat' to read files, use ReadFiles tool instead.
|
|
169
170
|
""",
|
|
170
171
|
),
|
|
171
172
|
ToolParam(
|
|
@@ -281,22 +282,13 @@ Saves provided description and file contents of all the relevant file paths or g
|
|
|
281
282
|
),
|
|
282
283
|
]
|
|
283
284
|
|
|
284
|
-
|
|
285
|
-
os.getcwd(),
|
|
285
|
+
system = initialize(
|
|
286
|
+
os.getcwd(),
|
|
287
|
+
[],
|
|
288
|
+
resume if (memory and resume) else "",
|
|
289
|
+
max_tokens=8000,
|
|
290
|
+
mode="wcgw",
|
|
286
291
|
)
|
|
287
|
-
system = f"""
|
|
288
|
-
You're an expert software engineer with shell and code knowledge.
|
|
289
|
-
|
|
290
|
-
Instructions:
|
|
291
|
-
|
|
292
|
-
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
293
|
-
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
294
|
-
- Always read relevant files before editing.
|
|
295
|
-
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
296
|
-
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
297
|
-
|
|
298
|
-
{initial_info}
|
|
299
|
-
"""
|
|
300
292
|
|
|
301
293
|
with open(os.path.join(os.path.dirname(__file__), "diff-instructions.txt")) as f:
|
|
302
294
|
system += f.read()
|
wcgw/client/mcp_server/server.py
CHANGED
|
@@ -28,6 +28,7 @@ from ...types_ import (
|
|
|
28
28
|
)
|
|
29
29
|
from .. import tools
|
|
30
30
|
from ..computer_use import SLEEP_TIME_MAX_S
|
|
31
|
+
from ..modes import get_kt_prompt
|
|
31
32
|
from ..tools import DoneFlag, default_enc, get_tool_output, which_tool_name
|
|
32
33
|
|
|
33
34
|
COMPUTER_USE_ON_DOCKER_ENABLED = False
|
|
@@ -45,48 +46,35 @@ async def handle_read_resource(uri: AnyUrl) -> str:
|
|
|
45
46
|
raise ValueError("No resources available")
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return [
|
|
49
|
+
PROMPTS = {
|
|
50
|
+
"KnowledgeTransfer": (
|
|
51
51
|
types.Prompt(
|
|
52
52
|
name="KnowledgeTransfer",
|
|
53
53
|
description="Prompt for invoking ContextSave tool in order to do a comprehensive knowledge transfer of a coding task. Prompts to save detailed error log and instructions.",
|
|
54
|
-
)
|
|
55
|
-
|
|
54
|
+
),
|
|
55
|
+
get_kt_prompt,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@server.list_prompts() # type: ignore
|
|
61
|
+
async def handle_list_prompts() -> list[types.Prompt]:
|
|
62
|
+
return [x[0] for x in PROMPTS.values()]
|
|
56
63
|
|
|
57
64
|
|
|
58
65
|
@server.get_prompt() # type: ignore
|
|
59
66
|
async def handle_get_prompt(
|
|
60
67
|
name: str, arguments: dict[str, str] | None
|
|
61
68
|
) -> types.GetPromptResult:
|
|
62
|
-
messages = [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
types.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Save all information necessary for a person to understand the task and the problems.
|
|
72
|
-
|
|
73
|
-
Format the `description` field using Markdown with the following sections.
|
|
74
|
-
- "# Objective" section containing project and task objective.
|
|
75
|
-
- "# All user instructions" section should be provided containing all instructions user shared in the conversation.
|
|
76
|
-
- "# Current status of the task" should be provided containing only what is already achieved, not what's remaining.
|
|
77
|
-
- "# All issues with snippets" section containing snippets of error, traceback, file snippets, commands, etc. But no comments or solutions.
|
|
78
|
-
- Be very verbose in the all issues with snippets section providing as much error context as possible.
|
|
79
|
-
- "# Build and development instructions" section containing instructions to build or run project or run tests, or envrionment related information. Only include what's known. Leave empty if unknown.
|
|
80
|
-
- After the tool completes succesfully, tell me the task id and the file path the tool generated (important!)
|
|
81
|
-
- This tool marks end of your conversation, do not run any further tools after calling this.
|
|
82
|
-
|
|
83
|
-
Provide all relevant file paths in order to understand and solve the the task. Err towards providing more file paths than fewer.
|
|
84
|
-
|
|
85
|
-
(Note to self: this conversation can then be resumed later asking "Resume `<generated id>`" which should call Initialize tool)
|
|
86
|
-
""",
|
|
87
|
-
),
|
|
88
|
-
)
|
|
89
|
-
]
|
|
69
|
+
messages = [
|
|
70
|
+
types.PromptMessage(
|
|
71
|
+
role="user",
|
|
72
|
+
content=types.TextContent(
|
|
73
|
+
type="text",
|
|
74
|
+
text=PROMPTS[name][1](),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
]
|
|
90
78
|
return types.GetPromptResult(messages=messages)
|
|
91
79
|
|
|
92
80
|
|
|
@@ -117,6 +105,9 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
|
117
105
|
- If user has mentioned any files use `initial_files_to_read` to read, use absolute paths only.
|
|
118
106
|
- If `any_workspace_path` is provided, a tree structure of the workspace will be shown.
|
|
119
107
|
- Leave `any_workspace_path` as empty if no file or folder is mentioned.
|
|
108
|
+
- By default use mode "wcgw"
|
|
109
|
+
- In "code-writer" mode, set the commands and globs which user asked to set, otherwise use 'all'.
|
|
110
|
+
- In order to change the mode later, call this tool again but be sure to not provide any other argument like task_id_to_resume unnecessarily.
|
|
120
111
|
""",
|
|
121
112
|
),
|
|
122
113
|
ToolParam(
|
|
@@ -263,6 +254,8 @@ async def handle_call_tool(
|
|
|
263
254
|
except ValidationError:
|
|
264
255
|
|
|
265
256
|
def try_json(x: str) -> Any:
|
|
257
|
+
if not isinstance(x, str):
|
|
258
|
+
return x
|
|
266
259
|
try:
|
|
267
260
|
return json.loads(x)
|
|
268
261
|
except json.JSONDecodeError:
|
|
@@ -284,24 +277,9 @@ async def handle_call_tool(
|
|
|
284
277
|
if isinstance(output_or_done, str):
|
|
285
278
|
if issubclass(tool_type, Initialize):
|
|
286
279
|
output_or_done += """
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
Instructions:
|
|
291
|
-
|
|
292
|
-
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
293
|
-
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
294
|
-
- Always read relevant files before editing.
|
|
295
|
-
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
296
|
-
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
297
|
-
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using shell tools.
|
|
298
|
-
- Do not use Ctrl-c or Ctrl-z or interrupt commands without asking the user, because often the program don't show any update but they still are running.
|
|
299
|
-
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
300
|
-
|
|
301
|
-
Additional instructions:
|
|
302
|
-
Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
|
|
303
|
-
|
|
304
|
-
Always write production ready, syntactically correct code.
|
|
280
|
+
- Additional important note: as soon as you encounter "The user has chosen to disallow the tool call.", immediately stop doing everything and ask user for the reason.
|
|
281
|
+
|
|
282
|
+
Initialize call done.
|
|
305
283
|
"""
|
|
306
284
|
|
|
307
285
|
content.append(types.TextContent(type="text", text=output_or_done))
|
wcgw/client/memory.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import re
|
|
3
4
|
import shlex
|
|
4
|
-
from typing import Callable, Optional
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
5
6
|
|
|
6
7
|
from ..types_ import ContextSave
|
|
7
8
|
|
|
@@ -30,7 +31,11 @@ def format_memory(task_memory: ContextSave, relevant_files: str) -> str:
|
|
|
30
31
|
return memory_data
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
def save_memory(
|
|
34
|
+
def save_memory(
|
|
35
|
+
task_memory: ContextSave,
|
|
36
|
+
relevant_files: str,
|
|
37
|
+
bash_state_dict: Optional[dict[str, Any]] = None,
|
|
38
|
+
) -> str:
|
|
34
39
|
app_dir = get_app_dir_xdg()
|
|
35
40
|
memory_dir = os.path.join(app_dir, "memory")
|
|
36
41
|
os.makedirs(memory_dir, exist_ok=True)
|
|
@@ -45,6 +50,12 @@ def save_memory(task_memory: ContextSave, relevant_files: str) -> str:
|
|
|
45
50
|
with open(memory_file_full, "w") as f:
|
|
46
51
|
f.write(memory_data)
|
|
47
52
|
|
|
53
|
+
# Save bash state if provided
|
|
54
|
+
if bash_state_dict is not None:
|
|
55
|
+
state_file = os.path.join(memory_dir, f"{task_id}_bash_state.json")
|
|
56
|
+
with open(state_file, "w") as f:
|
|
57
|
+
json.dump(bash_state_dict, f, indent=2)
|
|
58
|
+
|
|
48
59
|
return memory_file_full
|
|
49
60
|
|
|
50
61
|
|
|
@@ -53,7 +64,7 @@ def load_memory[T](
|
|
|
53
64
|
max_tokens: Optional[int],
|
|
54
65
|
encoder: Callable[[str], list[T]],
|
|
55
66
|
decoder: Callable[[list[T]], str],
|
|
56
|
-
) -> tuple[str, str]:
|
|
67
|
+
) -> tuple[str, str, Optional[dict[str, Any]]]:
|
|
57
68
|
app_dir = get_app_dir_xdg()
|
|
58
69
|
memory_dir = os.path.join(app_dir, "memory")
|
|
59
70
|
memory_file = os.path.join(memory_dir, f"{task_id}.txt")
|
|
@@ -75,4 +86,12 @@ def load_memory[T](
|
|
|
75
86
|
parsed_ = shlex.split(matched_path)
|
|
76
87
|
if parsed_ and len(parsed_) == 1:
|
|
77
88
|
project_root_path = parsed_[0]
|
|
78
|
-
|
|
89
|
+
|
|
90
|
+
# Try to load bash state if exists
|
|
91
|
+
state_file = os.path.join(memory_dir, f"{task_id}_bash_state.json")
|
|
92
|
+
bash_state: Optional[dict[str, Any]] = None
|
|
93
|
+
if os.path.exists(state_file):
|
|
94
|
+
with open(state_file) as f:
|
|
95
|
+
bash_state = json.load(f)
|
|
96
|
+
|
|
97
|
+
return project_root_path, data, bash_state
|
wcgw/client/modes.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Literal, NamedTuple
|
|
3
|
+
|
|
4
|
+
from ..types_ import Modes, ModesConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BashCommandMode(NamedTuple):
|
|
8
|
+
bash_mode: Literal["normal_mode", "restricted_mode"]
|
|
9
|
+
allowed_commands: Literal["all", "none"]
|
|
10
|
+
|
|
11
|
+
def serialize(self) -> dict[str, Any]:
|
|
12
|
+
return {"bash_mode": self.bash_mode, "allowed_commands": self.allowed_commands}
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def deserialize(cls, data: dict[str, Any]) -> "BashCommandMode":
|
|
16
|
+
return cls(data["bash_mode"], data["allowed_commands"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FileEditMode(NamedTuple):
|
|
20
|
+
allowed_globs: Literal["all"] | list[str]
|
|
21
|
+
|
|
22
|
+
def serialize(self) -> dict[str, Any]:
|
|
23
|
+
return {"allowed_globs": self.allowed_globs}
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def deserialize(cls, data: dict[str, Any]) -> "FileEditMode":
|
|
27
|
+
return cls(data["allowed_globs"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WriteIfEmptyMode(NamedTuple):
|
|
31
|
+
allowed_globs: Literal["all"] | list[str]
|
|
32
|
+
|
|
33
|
+
def serialize(self) -> dict[str, Any]:
|
|
34
|
+
return {"allowed_globs": self.allowed_globs}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def deserialize(cls, data: dict[str, Any]) -> "WriteIfEmptyMode":
|
|
38
|
+
return cls(data["allowed_globs"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ModeImpl:
|
|
43
|
+
bash_command_mode: BashCommandMode
|
|
44
|
+
file_edit_mode: FileEditMode
|
|
45
|
+
write_if_empty_mode: WriteIfEmptyMode
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def code_writer_prompt(
|
|
49
|
+
allowed_file_edit_globs: Literal["all"] | list[str],
|
|
50
|
+
all_write_new_globs: Literal["all"] | list[str],
|
|
51
|
+
allowed_commands: Literal["all"] | list[str],
|
|
52
|
+
) -> str:
|
|
53
|
+
base = """
|
|
54
|
+
You have to run in "code_writer" mode.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
path_prompt = """
|
|
58
|
+
- You are allowed to run FileEdit in the provided repository only.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
if allowed_file_edit_globs != "all":
|
|
62
|
+
if allowed_file_edit_globs:
|
|
63
|
+
path_prompt = f"""
|
|
64
|
+
- You are allowed to run FileEdit for files matching only the following globs: {', '.join(allowed_file_edit_globs)}
|
|
65
|
+
"""
|
|
66
|
+
else:
|
|
67
|
+
path_prompt = """
|
|
68
|
+
- You are not allowed to run FileEdit.
|
|
69
|
+
"""
|
|
70
|
+
base += path_prompt
|
|
71
|
+
|
|
72
|
+
path_prompt = """
|
|
73
|
+
- You are allowed to run WriteIfEmpty in the provided repository only.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if all_write_new_globs != "all":
|
|
77
|
+
if all_write_new_globs:
|
|
78
|
+
path_prompt = f"""
|
|
79
|
+
- You are allowed to run WriteIfEmpty files matching only the following globs: {', '.join(allowed_file_edit_globs)}
|
|
80
|
+
"""
|
|
81
|
+
else:
|
|
82
|
+
path_prompt = """
|
|
83
|
+
- You are not allowed to run WriteIfEmpty.
|
|
84
|
+
"""
|
|
85
|
+
base += path_prompt
|
|
86
|
+
|
|
87
|
+
run_command_common = """
|
|
88
|
+
- Do not use Ctrl-c or Ctrl-z or 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 FileEdit tool to update a code.
|
|
90
|
+
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
91
|
+
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
92
|
+
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
93
|
+
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using wcgw tools.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
command_prompt = f"""
|
|
97
|
+
- You are only allowed to run commands for project setup, code writing, editing, updating, testing, running and debugging related to the project.
|
|
98
|
+
- Do not run anything that adds or removes packages, changes system configuration or environment.
|
|
99
|
+
{run_command_common}
|
|
100
|
+
"""
|
|
101
|
+
if allowed_commands != "all":
|
|
102
|
+
if allowed_commands:
|
|
103
|
+
command_prompt = f"""
|
|
104
|
+
- You are only allowed to run the following commands: {', '.join(allowed_commands)}
|
|
105
|
+
{run_command_common}
|
|
106
|
+
"""
|
|
107
|
+
else:
|
|
108
|
+
command_prompt = """
|
|
109
|
+
- You are not allowed to run any commands.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
base += command_prompt
|
|
113
|
+
return base
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
WCGW_PROMPT = """
|
|
117
|
+
---
|
|
118
|
+
You're an expert software engineer with shell and code knowledge.
|
|
119
|
+
|
|
120
|
+
Instructions:
|
|
121
|
+
|
|
122
|
+
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
123
|
+
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
124
|
+
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
125
|
+
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
126
|
+
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using wcgw tools
|
|
127
|
+
- Do not use Ctrl-c or Ctrl-z or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
128
|
+
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
129
|
+
|
|
130
|
+
Additional instructions:
|
|
131
|
+
Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
|
|
132
|
+
|
|
133
|
+
Always write production ready, syntactically correct code.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
ARCHITECT_PROMPT = """You have to run in "architect" mode. This means
|
|
138
|
+
- You are not allowed to edit or update any file. You are not allowed to create any file.
|
|
139
|
+
- You are not allowed to run any commands that may change disk, system configuration, packages or environment. Only read-only commands are allowed.
|
|
140
|
+
- Only run commands that allows you to explore the repository, understand the system or read anything of relevance.
|
|
141
|
+
- Do not use Ctrl-c or Ctrl-z or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
142
|
+
- You are not allowed to change directory (bash will run in -r mode)
|
|
143
|
+
|
|
144
|
+
Your response should be in self-critique and brainstorm style.
|
|
145
|
+
- Read as many relevant files as possible.
|
|
146
|
+
- Be comprehensive in your understanding and search of relevant files.
|
|
147
|
+
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
DEFAULT_MODES: dict[Modes, ModeImpl] = {
|
|
152
|
+
Modes.wcgw: ModeImpl(
|
|
153
|
+
bash_command_mode=BashCommandMode("normal_mode", "all"),
|
|
154
|
+
write_if_empty_mode=WriteIfEmptyMode("all"),
|
|
155
|
+
file_edit_mode=FileEditMode("all"),
|
|
156
|
+
),
|
|
157
|
+
Modes.architect: ModeImpl(
|
|
158
|
+
bash_command_mode=BashCommandMode("restricted_mode", "all"),
|
|
159
|
+
write_if_empty_mode=WriteIfEmptyMode([]),
|
|
160
|
+
file_edit_mode=FileEditMode([]),
|
|
161
|
+
),
|
|
162
|
+
Modes.code_writer: ModeImpl(
|
|
163
|
+
bash_command_mode=BashCommandMode("normal_mode", "all"),
|
|
164
|
+
write_if_empty_mode=WriteIfEmptyMode("all"),
|
|
165
|
+
file_edit_mode=FileEditMode("all"),
|
|
166
|
+
),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def modes_to_state(
|
|
171
|
+
mode: ModesConfig,
|
|
172
|
+
) -> tuple[BashCommandMode, FileEditMode, WriteIfEmptyMode, Modes]:
|
|
173
|
+
# First get default mode config
|
|
174
|
+
if isinstance(mode, str):
|
|
175
|
+
mode_impl = DEFAULT_MODES[Modes[mode]] # converts str to Modes enum
|
|
176
|
+
mode_name = Modes[mode]
|
|
177
|
+
else:
|
|
178
|
+
# For CodeWriterMode, use code_writer as base and override
|
|
179
|
+
mode_impl = DEFAULT_MODES[Modes.code_writer]
|
|
180
|
+
# Override with custom settings from CodeWriterMode
|
|
181
|
+
mode_impl = ModeImpl(
|
|
182
|
+
bash_command_mode=BashCommandMode(
|
|
183
|
+
mode_impl.bash_command_mode.bash_mode,
|
|
184
|
+
"all" if mode.allowed_commands else "none",
|
|
185
|
+
),
|
|
186
|
+
file_edit_mode=FileEditMode(mode.allowed_globs),
|
|
187
|
+
write_if_empty_mode=WriteIfEmptyMode(mode.allowed_globs),
|
|
188
|
+
)
|
|
189
|
+
mode_name = Modes.code_writer
|
|
190
|
+
return (
|
|
191
|
+
mode_impl.bash_command_mode,
|
|
192
|
+
mode_impl.file_edit_mode,
|
|
193
|
+
mode_impl.write_if_empty_mode,
|
|
194
|
+
mode_name,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
WCGW_KT = """Use `ContextSave` tool to do a knowledge transfer of the task in hand.
|
|
199
|
+
Write detailed description in order to do a KT.
|
|
200
|
+
Save all information necessary for a person to understand the task and the problems.
|
|
201
|
+
|
|
202
|
+
Format the `description` field using Markdown with the following sections.
|
|
203
|
+
- "# Objective" section containing project and task objective.
|
|
204
|
+
- "# All user instructions" section should be provided containing all instructions user shared in the conversation.
|
|
205
|
+
- "# Current status of the task" should be provided containing only what is already achieved, not what's remaining.
|
|
206
|
+
- "# All issues with snippets" section containing snippets of error, traceback, file snippets, commands, etc. But no comments or solutions.
|
|
207
|
+
- Be very verbose in the all issues with snippets section providing as much error context as possible.
|
|
208
|
+
- "# Build and development instructions" section containing instructions to build or run project or run tests, or envrionment related information. Only include what's known. Leave empty if unknown.
|
|
209
|
+
- Any other relevant sections following the above.
|
|
210
|
+
- After the tool completes succesfully, tell me the task id and the file path the tool generated (important!)
|
|
211
|
+
- This tool marks end of your conversation, do not run any further tools after calling this.
|
|
212
|
+
|
|
213
|
+
Provide all relevant file paths in order to understand and solve the the task. Err towards providing more file paths than fewer.
|
|
214
|
+
|
|
215
|
+
(Note to self: this conversation can then be resumed later asking "Resume wcgw task `<generated id>`" which should call Initialize tool)
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
ARCHITECT_KT = """Use `ContextSave` tool to do a knowledge transfer of the task in hand.
|
|
220
|
+
Write detailed description in order to do a KT.
|
|
221
|
+
Save all information necessary for a person to understand the task and the problems.
|
|
222
|
+
|
|
223
|
+
Format the `description` field using Markdown with the following sections.
|
|
224
|
+
- "# Objective" section containing project and task objective.
|
|
225
|
+
- "# All user instructions" section should be provided containing all instructions user shared in the conversation.
|
|
226
|
+
- "# Designed plan" should be provided containing the designed plan as discussed.
|
|
227
|
+
- Any other relevant sections following the above.
|
|
228
|
+
- After the tool completes succesfully, tell me the task id and the file path the tool generated (important!)
|
|
229
|
+
- This tool marks end of your conversation, do not run any further tools after calling this.
|
|
230
|
+
|
|
231
|
+
Provide all relevant file paths in order to understand and solve the the task. Err towards providing more file paths than fewer.
|
|
232
|
+
|
|
233
|
+
(Note to self: this conversation can then be resumed later asking "Resume wcgw task `<generated id>`" which should call Initialize tool)
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
KTS = {Modes.wcgw: WCGW_KT, Modes.architect: ARCHITECT_KT, Modes.code_writer: WCGW_KT}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_kt_prompt() -> str:
|
|
240
|
+
from .tools import BASH_STATE
|
|
241
|
+
|
|
242
|
+
return KTS[BASH_STATE.mode]
|
wcgw/client/openai_client.py
CHANGED
|
@@ -125,7 +125,7 @@ def loop(
|
|
|
125
125
|
memory = None
|
|
126
126
|
if resume:
|
|
127
127
|
try:
|
|
128
|
-
_, memory = load_memory(
|
|
128
|
+
_, memory, _ = load_memory(
|
|
129
129
|
resume,
|
|
130
130
|
8000,
|
|
131
131
|
lambda x: default_enc.encode(x).ids,
|
|
@@ -176,6 +176,7 @@ def loop(
|
|
|
176
176
|
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
177
177
|
- The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
|
|
178
178
|
- Run long running commands in background using screen instead of "&".
|
|
179
|
+
- Do not use 'cat' to read files, use ReadFiles tool instead.
|
|
179
180
|
""",
|
|
180
181
|
),
|
|
181
182
|
openai.pydantic_function_tool(
|
|
@@ -226,23 +227,13 @@ Saves provided description and file contents of all the relevant file paths or g
|
|
|
226
227
|
),
|
|
227
228
|
]
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
os.getcwd(),
|
|
230
|
+
system = initialize(
|
|
231
|
+
os.getcwd(),
|
|
232
|
+
[],
|
|
233
|
+
resume if (memory and resume) else "",
|
|
234
|
+
max_tokens=8000,
|
|
235
|
+
mode="wcgw",
|
|
231
236
|
)
|
|
232
|
-
system = f"""
|
|
233
|
-
You're an expert software engineer with shell and code knowledge.
|
|
234
|
-
|
|
235
|
-
Instructions:
|
|
236
|
-
|
|
237
|
-
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
238
|
-
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
239
|
-
- Always read relevant files before editing.
|
|
240
|
-
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
241
|
-
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
242
|
-
|
|
243
|
-
{initial_info}
|
|
244
|
-
|
|
245
|
-
"""
|
|
246
237
|
|
|
247
238
|
with open(os.path.join(os.path.dirname(__file__), "diff-instructions.txt")) as f:
|
|
248
239
|
system += f.read()
|
wcgw/client/tools.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import datetime
|
|
3
|
+
import fnmatch
|
|
3
4
|
import glob
|
|
4
5
|
import importlib.metadata
|
|
5
6
|
import json
|
|
@@ -14,6 +15,7 @@ from os.path import expanduser
|
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
16
17
|
from typing import (
|
|
18
|
+
Any,
|
|
17
19
|
Callable,
|
|
18
20
|
Literal,
|
|
19
21
|
Optional,
|
|
@@ -39,12 +41,15 @@ from websockets.sync.client import connect as syncconnect
|
|
|
39
41
|
from ..types_ import (
|
|
40
42
|
BashCommand,
|
|
41
43
|
BashInteraction,
|
|
44
|
+
CodeWriterMode,
|
|
42
45
|
ContextSave,
|
|
43
46
|
FileEdit,
|
|
44
47
|
FileEditFindReplace,
|
|
45
48
|
GetScreenInfo,
|
|
46
49
|
Initialize,
|
|
47
50
|
Keyboard,
|
|
51
|
+
Modes,
|
|
52
|
+
ModesConfig,
|
|
48
53
|
Mouse,
|
|
49
54
|
ReadFiles,
|
|
50
55
|
ReadImage,
|
|
@@ -55,6 +60,15 @@ from ..types_ import (
|
|
|
55
60
|
from .computer_use import run_computer_tool
|
|
56
61
|
from .file_ops.search_replace import search_replace_edit
|
|
57
62
|
from .memory import load_memory, save_memory
|
|
63
|
+
from .modes import (
|
|
64
|
+
ARCHITECT_PROMPT,
|
|
65
|
+
WCGW_PROMPT,
|
|
66
|
+
BashCommandMode,
|
|
67
|
+
FileEditMode,
|
|
68
|
+
WriteIfEmptyMode,
|
|
69
|
+
code_writer_prompt,
|
|
70
|
+
modes_to_state,
|
|
71
|
+
)
|
|
58
72
|
from .repo_ops.repo_context import get_repo_context
|
|
59
73
|
from .sys_utils import command_run
|
|
60
74
|
|
|
@@ -123,16 +137,22 @@ PROMPT_CONST = "#@wcgw@#"
|
|
|
123
137
|
PROMPT = PROMPT_CONST
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def start_shell() -> pexpect.spawn: # type: ignore
|
|
140
|
+
def start_shell(is_restricted_mode: bool, initial_dir: str) -> pexpect.spawn: # type: ignore
|
|
141
|
+
cmd = "/bin/bash"
|
|
142
|
+
if is_restricted_mode:
|
|
143
|
+
cmd += " -r"
|
|
144
|
+
|
|
127
145
|
try:
|
|
128
146
|
shell = pexpect.spawn(
|
|
129
|
-
|
|
147
|
+
cmd,
|
|
130
148
|
env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
|
|
131
149
|
echo=False,
|
|
132
150
|
encoding="utf-8",
|
|
133
151
|
timeout=TIMEOUT,
|
|
152
|
+
cwd=initial_dir,
|
|
134
153
|
)
|
|
135
154
|
shell.sendline(f"export PS1={PROMPT}")
|
|
155
|
+
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
136
156
|
except Exception as e:
|
|
137
157
|
console.print(traceback.format_exc())
|
|
138
158
|
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
@@ -144,8 +164,9 @@ def start_shell() -> pexpect.spawn: # type: ignore
|
|
|
144
164
|
encoding="utf-8",
|
|
145
165
|
timeout=TIMEOUT,
|
|
146
166
|
)
|
|
167
|
+
shell.sendline(f"export PS1={PROMPT}")
|
|
168
|
+
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
147
169
|
|
|
148
|
-
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
149
170
|
shell.sendline("stty -icanon -echo")
|
|
150
171
|
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
151
172
|
shell.sendline("set +o pipefail")
|
|
@@ -203,15 +224,54 @@ BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
|
203
224
|
|
|
204
225
|
|
|
205
226
|
class BashState:
|
|
206
|
-
def __init__(
|
|
207
|
-
self
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
working_dir: str,
|
|
230
|
+
bash_command_mode: Optional[BashCommandMode],
|
|
231
|
+
file_edit_mode: Optional[FileEditMode],
|
|
232
|
+
write_if_empty_mode: Optional[WriteIfEmptyMode],
|
|
233
|
+
mode: Optional[Modes],
|
|
234
|
+
whitelist_for_overwrite: Optional[set[str]] = None,
|
|
235
|
+
) -> None:
|
|
236
|
+
self._cwd = working_dir or os.getcwd()
|
|
237
|
+
self._bash_command_mode: BashCommandMode = bash_command_mode or BashCommandMode(
|
|
238
|
+
"normal_mode", "all"
|
|
239
|
+
)
|
|
240
|
+
self._file_edit_mode: FileEditMode = file_edit_mode or FileEditMode("all")
|
|
241
|
+
self._write_if_empty_mode: WriteIfEmptyMode = (
|
|
242
|
+
write_if_empty_mode or WriteIfEmptyMode("all")
|
|
243
|
+
)
|
|
244
|
+
self._mode = mode or Modes.wcgw
|
|
245
|
+
self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
|
|
246
|
+
|
|
247
|
+
self._init_shell()
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def mode(self) -> Modes:
|
|
251
|
+
return self._mode
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def bash_command_mode(self) -> BashCommandMode:
|
|
255
|
+
return self._bash_command_mode
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def file_edit_mode(self) -> FileEditMode:
|
|
259
|
+
return self._file_edit_mode
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def write_if_empty_mode(self) -> WriteIfEmptyMode:
|
|
263
|
+
return self._write_if_empty_mode
|
|
208
264
|
|
|
209
|
-
def
|
|
265
|
+
def _init_shell(self) -> None:
|
|
210
266
|
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
211
267
|
self._is_in_docker: Optional[str] = ""
|
|
212
|
-
self._cwd
|
|
213
|
-
self.
|
|
214
|
-
self.
|
|
268
|
+
# Ensure self._cwd exists
|
|
269
|
+
os.makedirs(self._cwd, exist_ok=True)
|
|
270
|
+
self._shell = start_shell(
|
|
271
|
+
self._bash_command_mode.bash_mode == "restricted_mode",
|
|
272
|
+
self._cwd,
|
|
273
|
+
)
|
|
274
|
+
|
|
215
275
|
self._pending_output = ""
|
|
216
276
|
|
|
217
277
|
# Get exit info to ensure shell is ready
|
|
@@ -258,9 +318,49 @@ class BashState:
|
|
|
258
318
|
self._cwd = current_dir
|
|
259
319
|
return current_dir
|
|
260
320
|
|
|
261
|
-
def
|
|
321
|
+
def reset_shell(self) -> None:
|
|
262
322
|
self.shell.close(True)
|
|
263
|
-
self.
|
|
323
|
+
self._init_shell()
|
|
324
|
+
|
|
325
|
+
def serialize(self) -> dict[str, Any]:
|
|
326
|
+
"""Serialize BashState to a dictionary for saving"""
|
|
327
|
+
return {
|
|
328
|
+
"bash_command_mode": self._bash_command_mode.serialize(),
|
|
329
|
+
"file_edit_mode": self._file_edit_mode.serialize(),
|
|
330
|
+
"write_if_empty_mode": self._write_if_empty_mode.serialize(),
|
|
331
|
+
"whitelist_for_overwrite": list(self._whitelist_for_overwrite),
|
|
332
|
+
"mode": self._mode,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def parse_state(
|
|
337
|
+
state: dict[str, Any],
|
|
338
|
+
) -> tuple[BashCommandMode, FileEditMode, WriteIfEmptyMode, Modes, list[str]]:
|
|
339
|
+
return (
|
|
340
|
+
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
341
|
+
FileEditMode.deserialize(state["file_edit_mode"]),
|
|
342
|
+
WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
|
|
343
|
+
Modes[str(state["mode"])],
|
|
344
|
+
state["whitelist_for_overwrite"],
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def load_state(
|
|
348
|
+
self,
|
|
349
|
+
bash_command_mode: BashCommandMode,
|
|
350
|
+
file_edit_mode: FileEditMode,
|
|
351
|
+
write_if_empty_mode: WriteIfEmptyMode,
|
|
352
|
+
mode: Modes,
|
|
353
|
+
whitelist_for_overwrite: list[str],
|
|
354
|
+
cwd: str,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Create a new BashState instance from a serialized state dictionary"""
|
|
357
|
+
self._bash_command_mode = bash_command_mode
|
|
358
|
+
self._cwd = cwd or self._cwd
|
|
359
|
+
self._file_edit_mode = file_edit_mode
|
|
360
|
+
self._write_if_empty_mode = write_if_empty_mode
|
|
361
|
+
self._whitelist_for_overwrite = set(whitelist_for_overwrite)
|
|
362
|
+
self._mode = mode
|
|
363
|
+
self.reset_shell()
|
|
264
364
|
|
|
265
365
|
def get_pending_for(self) -> str:
|
|
266
366
|
if isinstance(self._state, datetime.datetime):
|
|
@@ -290,7 +390,8 @@ class BashState:
|
|
|
290
390
|
return self._pending_output
|
|
291
391
|
|
|
292
392
|
|
|
293
|
-
BASH_STATE = BashState()
|
|
393
|
+
BASH_STATE = BashState(os.getcwd(), None, None, None, None)
|
|
394
|
+
INITIALIZED = False
|
|
294
395
|
|
|
295
396
|
|
|
296
397
|
def initialize(
|
|
@@ -298,57 +399,122 @@ def initialize(
|
|
|
298
399
|
read_files_: list[str],
|
|
299
400
|
task_id_to_resume: str,
|
|
300
401
|
max_tokens: Optional[int],
|
|
402
|
+
mode: ModesConfig,
|
|
301
403
|
) -> str:
|
|
302
|
-
|
|
404
|
+
global BASH_STATE
|
|
303
405
|
|
|
304
406
|
# Expand the workspace path
|
|
305
|
-
any_workspace_path = expand_user(any_workspace_path,
|
|
407
|
+
any_workspace_path = expand_user(any_workspace_path, None)
|
|
306
408
|
repo_context = ""
|
|
307
409
|
|
|
308
410
|
memory = ""
|
|
411
|
+
bash_state = None
|
|
309
412
|
if task_id_to_resume:
|
|
310
413
|
try:
|
|
311
|
-
project_root_path, task_mem = load_memory(
|
|
414
|
+
project_root_path, task_mem, bash_state = load_memory(
|
|
312
415
|
task_id_to_resume,
|
|
313
416
|
max_tokens,
|
|
314
417
|
lambda x: default_enc.encode(x).ids,
|
|
315
418
|
lambda x: default_enc.decode(x),
|
|
316
419
|
)
|
|
317
420
|
memory = "Following is the retrieved task:\n" + task_mem
|
|
318
|
-
if (
|
|
319
|
-
not any_workspace_path or not os.path.exists(any_workspace_path)
|
|
320
|
-
) and os.path.exists(project_root_path):
|
|
421
|
+
if os.path.exists(project_root_path):
|
|
321
422
|
any_workspace_path = project_root_path
|
|
423
|
+
|
|
322
424
|
except Exception:
|
|
323
425
|
memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
|
|
324
426
|
|
|
427
|
+
folder_to_start = None
|
|
325
428
|
if any_workspace_path:
|
|
326
429
|
if os.path.exists(any_workspace_path):
|
|
327
430
|
repo_context, folder_to_start = get_repo_context(any_workspace_path, 200)
|
|
328
431
|
|
|
329
|
-
BASH_STATE.shell.sendline(f"cd {shlex.quote(str(folder_to_start))}")
|
|
330
|
-
BASH_STATE.shell.expect(PROMPT, timeout=0.2)
|
|
331
|
-
BASH_STATE.update_cwd()
|
|
332
|
-
|
|
333
432
|
repo_context = f"---\n# Workspace structure\n{repo_context}\n---\n"
|
|
433
|
+
|
|
434
|
+
# update modes if they're relative
|
|
435
|
+
if isinstance(mode, CodeWriterMode):
|
|
436
|
+
mode.update_relative_globs(any_workspace_path)
|
|
437
|
+
else:
|
|
438
|
+
assert isinstance(mode, str)
|
|
334
439
|
else:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
440
|
+
if os.path.abspath(any_workspace_path):
|
|
441
|
+
os.makedirs(any_workspace_path, exist_ok=True)
|
|
442
|
+
repo_context = f"\nInfo: Workspace path {any_workspace_path} did not exist. I've created it for you.\n"
|
|
443
|
+
else:
|
|
444
|
+
repo_context = (
|
|
445
|
+
f"\nInfo: Workspace path {any_workspace_path} does not exist."
|
|
446
|
+
)
|
|
447
|
+
# Restore bash state if available
|
|
448
|
+
if bash_state is not None:
|
|
449
|
+
try:
|
|
450
|
+
parsed_state = BashState.parse_state(bash_state)
|
|
451
|
+
if mode == "wcgw":
|
|
452
|
+
BASH_STATE.load_state(
|
|
453
|
+
parsed_state[0],
|
|
454
|
+
parsed_state[1],
|
|
455
|
+
parsed_state[2],
|
|
456
|
+
parsed_state[3],
|
|
457
|
+
parsed_state[4] + list(BASH_STATE.whitelist_for_overwrite),
|
|
458
|
+
str(folder_to_start) if folder_to_start else "",
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
state = modes_to_state(mode)
|
|
462
|
+
BASH_STATE.load_state(
|
|
463
|
+
state[0],
|
|
464
|
+
state[1],
|
|
465
|
+
state[2],
|
|
466
|
+
state[3],
|
|
467
|
+
parsed_state[4] + list(BASH_STATE.whitelist_for_overwrite),
|
|
468
|
+
str(folder_to_start) if folder_to_start else "",
|
|
469
|
+
)
|
|
470
|
+
except ValueError:
|
|
471
|
+
console.print(traceback.format_exc())
|
|
472
|
+
console.print("Error: couldn't load bash state")
|
|
473
|
+
pass
|
|
474
|
+
else:
|
|
475
|
+
state = modes_to_state(mode)
|
|
476
|
+
BASH_STATE.load_state(
|
|
477
|
+
state[0],
|
|
478
|
+
state[1],
|
|
479
|
+
state[2],
|
|
480
|
+
state[3],
|
|
481
|
+
list(BASH_STATE.whitelist_for_overwrite),
|
|
482
|
+
str(folder_to_start) if folder_to_start else "",
|
|
483
|
+
)
|
|
484
|
+
del mode
|
|
338
485
|
|
|
339
486
|
initial_files_context = ""
|
|
340
487
|
if read_files_:
|
|
488
|
+
if folder_to_start:
|
|
489
|
+
read_files_ = [
|
|
490
|
+
os.path.join(folder_to_start, f) if not os.path.isabs(f) else f
|
|
491
|
+
for f in read_files_
|
|
492
|
+
]
|
|
341
493
|
initial_files = read_files(read_files_, max_tokens)
|
|
342
494
|
initial_files_context = f"---\n# Requested files\n{initial_files}\n---\n"
|
|
343
495
|
|
|
344
496
|
uname_sysname = os.uname().sysname
|
|
345
497
|
uname_machine = os.uname().machine
|
|
346
498
|
|
|
499
|
+
mode_prompt = ""
|
|
500
|
+
if BASH_STATE.mode == Modes.code_writer:
|
|
501
|
+
mode_prompt = code_writer_prompt(
|
|
502
|
+
BASH_STATE.file_edit_mode.allowed_globs,
|
|
503
|
+
BASH_STATE.write_if_empty_mode.allowed_globs,
|
|
504
|
+
"all" if BASH_STATE.bash_command_mode.allowed_commands else [],
|
|
505
|
+
)
|
|
506
|
+
elif BASH_STATE.mode == Modes.architect:
|
|
507
|
+
mode_prompt = ARCHITECT_PROMPT
|
|
508
|
+
else:
|
|
509
|
+
mode_prompt = WCGW_PROMPT
|
|
510
|
+
|
|
347
511
|
output = f"""
|
|
512
|
+
{mode_prompt}
|
|
513
|
+
|
|
348
514
|
# Environment
|
|
349
515
|
System: {uname_sysname}
|
|
350
516
|
Machine: {uname_machine}
|
|
351
|
-
|
|
517
|
+
Initialized in directory (also cwd): {BASH_STATE.cwd}
|
|
352
518
|
|
|
353
519
|
{repo_context}
|
|
354
520
|
|
|
@@ -359,11 +525,14 @@ Current working directory: {BASH_STATE.cwd}
|
|
|
359
525
|
{memory}
|
|
360
526
|
"""
|
|
361
527
|
|
|
528
|
+
global INITIALIZED
|
|
529
|
+
INITIALIZED = True
|
|
530
|
+
|
|
362
531
|
return output
|
|
363
532
|
|
|
364
533
|
|
|
365
534
|
def reset_shell() -> str:
|
|
366
|
-
BASH_STATE.
|
|
535
|
+
BASH_STATE.reset_shell()
|
|
367
536
|
return "Reset successful" + get_status()
|
|
368
537
|
|
|
369
538
|
|
|
@@ -472,6 +641,8 @@ def execute_bash(
|
|
|
472
641
|
try:
|
|
473
642
|
is_interrupt = False
|
|
474
643
|
if isinstance(bash_arg, BashCommand):
|
|
644
|
+
if BASH_STATE.bash_command_mode.allowed_commands == "none":
|
|
645
|
+
return "Error: BashCommand not allowed in current mode", 0.0
|
|
475
646
|
updated_repl_mode = update_repl_prompt(bash_arg.command)
|
|
476
647
|
if updated_repl_mode:
|
|
477
648
|
BASH_STATE.set_repl()
|
|
@@ -647,31 +818,11 @@ def execute_bash(
|
|
|
647
818
|
console.print(traceback.format_exc())
|
|
648
819
|
console.print("Malformed output, restarting shell", style="red")
|
|
649
820
|
# Malformed output, restart shell
|
|
650
|
-
BASH_STATE.
|
|
821
|
+
BASH_STATE.reset_shell()
|
|
651
822
|
output = "(exit shell has restarted)"
|
|
652
823
|
return output, 0
|
|
653
824
|
|
|
654
825
|
|
|
655
|
-
def serve_image_in_bg(file_path: str, client_uuid: str, name: str) -> None:
|
|
656
|
-
if not client_uuid:
|
|
657
|
-
client_uuid = str(uuid.uuid4())
|
|
658
|
-
|
|
659
|
-
server_url = "wss://wcgw.arcfu.com/register_serve_image"
|
|
660
|
-
|
|
661
|
-
with open(file_path, "rb") as image_file:
|
|
662
|
-
image_bytes = image_file.read()
|
|
663
|
-
media_type = mimetypes.guess_type(file_path)[0]
|
|
664
|
-
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
665
|
-
uu = {"name": name, "image_b64": image_b64, "media_type": media_type}
|
|
666
|
-
|
|
667
|
-
with syncconnect(f"{server_url}/{client_uuid}") as websocket:
|
|
668
|
-
try:
|
|
669
|
-
websocket.send(json.dumps(uu))
|
|
670
|
-
except websockets.ConnectionClosed:
|
|
671
|
-
console.print(f"Connection closed for UUID: {client_uuid}, retrying")
|
|
672
|
-
serve_image_in_bg(file_path, client_uuid, name)
|
|
673
|
-
|
|
674
|
-
|
|
675
826
|
MEDIA_TYPES = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
676
827
|
|
|
677
828
|
|
|
@@ -766,6 +917,13 @@ def write_file(
|
|
|
766
917
|
path_ = expand_user(writefile.file_path, BASH_STATE.is_in_docker)
|
|
767
918
|
|
|
768
919
|
error_on_exist_ = error_on_exist and path_ not in BASH_STATE.whitelist_for_overwrite
|
|
920
|
+
|
|
921
|
+
# Validate using write_if_empty_mode after checking whitelist
|
|
922
|
+
allowed_globs = BASH_STATE.write_if_empty_mode.allowed_globs
|
|
923
|
+
if allowed_globs != "all" and not any(
|
|
924
|
+
fnmatch.fnmatch(path_, pattern) for pattern in allowed_globs
|
|
925
|
+
):
|
|
926
|
+
return f"Error: updating file {path_} not allowed in current mode. Doesn't match allowed globs: {allowed_globs}"
|
|
769
927
|
add_overwrite_warning = ""
|
|
770
928
|
if not BASH_STATE.is_in_docker:
|
|
771
929
|
if (error_on_exist or error_on_exist_) and os.path.exists(path_):
|
|
@@ -897,6 +1055,15 @@ def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
|
897
1055
|
else:
|
|
898
1056
|
path_ = expand_user(fedit.file_path, BASH_STATE.is_in_docker)
|
|
899
1057
|
|
|
1058
|
+
# Validate using file_edit_mode
|
|
1059
|
+
allowed_globs = BASH_STATE.file_edit_mode.allowed_globs
|
|
1060
|
+
if allowed_globs != "all" and not any(
|
|
1061
|
+
fnmatch.fnmatch(path_, pattern) for pattern in allowed_globs
|
|
1062
|
+
):
|
|
1063
|
+
raise Exception(
|
|
1064
|
+
f"Error: updating file {path_} not allowed in current mode. Doesn't match allowed globs: {allowed_globs}"
|
|
1065
|
+
)
|
|
1066
|
+
|
|
900
1067
|
# The LLM is now aware that the file exists
|
|
901
1068
|
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
902
1069
|
|
|
@@ -1063,7 +1230,7 @@ def get_tool_output(
|
|
|
1063
1230
|
loop_call: Callable[[str, float], tuple[str, float]],
|
|
1064
1231
|
max_tokens: Optional[int],
|
|
1065
1232
|
) -> tuple[list[str | ImageData | DoneFlag], float]:
|
|
1066
|
-
global IS_IN_DOCKER, TOOL_CALLS
|
|
1233
|
+
global IS_IN_DOCKER, TOOL_CALLS, INITIALIZED
|
|
1067
1234
|
if isinstance(args, dict):
|
|
1068
1235
|
adapter = TypeAdapter[TOOLS](TOOLS, config={"extra": "forbid"})
|
|
1069
1236
|
arg = adapter.validate_python(args)
|
|
@@ -1071,17 +1238,27 @@ def get_tool_output(
|
|
|
1071
1238
|
arg = args
|
|
1072
1239
|
output: tuple[str | DoneFlag | ImageData, float]
|
|
1073
1240
|
TOOL_CALLS.append(arg)
|
|
1241
|
+
|
|
1074
1242
|
if isinstance(arg, Confirmation):
|
|
1075
1243
|
console.print("Calling ask confirmation tool")
|
|
1076
1244
|
output = ask_confirmation(arg), 0.0
|
|
1077
1245
|
elif isinstance(arg, (BashCommand | BashInteraction)):
|
|
1078
1246
|
console.print("Calling execute bash tool")
|
|
1247
|
+
if not INITIALIZED:
|
|
1248
|
+
raise Exception("Initialize tool not called yet.")
|
|
1249
|
+
|
|
1079
1250
|
output = execute_bash(enc, arg, max_tokens, arg.wait_for_seconds)
|
|
1080
1251
|
elif isinstance(arg, WriteIfEmpty):
|
|
1081
1252
|
console.print("Calling write file tool")
|
|
1253
|
+
if not INITIALIZED:
|
|
1254
|
+
raise Exception("Initialize tool not called yet.")
|
|
1255
|
+
|
|
1082
1256
|
output = write_file(arg, True, max_tokens), 0
|
|
1083
1257
|
elif isinstance(arg, FileEdit):
|
|
1084
1258
|
console.print("Calling full file edit tool")
|
|
1259
|
+
if not INITIALIZED:
|
|
1260
|
+
raise Exception("Initialize tool not called yet.")
|
|
1261
|
+
|
|
1085
1262
|
output = do_diff_edit(arg, max_tokens), 0.0
|
|
1086
1263
|
elif isinstance(arg, DoneFlag):
|
|
1087
1264
|
console.print("Calling mark finish tool")
|
|
@@ -1106,6 +1283,7 @@ def get_tool_output(
|
|
|
1106
1283
|
arg.initial_files_to_read,
|
|
1107
1284
|
arg.task_id_to_resume,
|
|
1108
1285
|
max_tokens,
|
|
1286
|
+
arg.mode,
|
|
1109
1287
|
),
|
|
1110
1288
|
0.0,
|
|
1111
1289
|
)
|
|
@@ -1158,7 +1336,7 @@ def get_tool_output(
|
|
|
1158
1336
|
if not globs:
|
|
1159
1337
|
warnings += f"Warning: No files found for the glob: {fglob}\n"
|
|
1160
1338
|
relevant_files_data = read_files(relevant_files[:10_000], None)
|
|
1161
|
-
output_ = save_memory(arg, relevant_files_data)
|
|
1339
|
+
output_ = save_memory(arg, relevant_files_data, BASH_STATE.serialize())
|
|
1162
1340
|
if not relevant_files and arg.relevant_file_globs:
|
|
1163
1341
|
output_ = f'Error: No files found for the given globs. Context file successfully saved at "{output_}", but please fix the error.'
|
|
1164
1342
|
elif warnings:
|
wcgw/types_.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Literal, Optional, Sequence, Union
|
|
2
4
|
|
|
3
5
|
from pydantic import BaseModel as PydanticBaseModel
|
|
4
6
|
|
|
@@ -11,6 +13,54 @@ class NoExtraArgs(PydanticBaseModel):
|
|
|
11
13
|
BaseModel = NoExtraArgs
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
class Modes(str, Enum):
|
|
17
|
+
wcgw = "wcgw"
|
|
18
|
+
architect = "architect"
|
|
19
|
+
code_writer = "code_writer"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CodeWriterMode(BaseModel):
|
|
23
|
+
allowed_globs: Literal["all"] | list[str]
|
|
24
|
+
allowed_commands: Literal["all"] | list[str]
|
|
25
|
+
|
|
26
|
+
def update_relative_globs(self, workspace_root: str) -> None:
|
|
27
|
+
"""Update globs if they're relative paths"""
|
|
28
|
+
if self.allowed_globs != "all":
|
|
29
|
+
self.allowed_globs = [
|
|
30
|
+
glob if os.path.isabs(glob) else os.path.join(workspace_root, glob)
|
|
31
|
+
for glob in self.allowed_globs
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
ModesConfig = Union[Literal["wcgw", "architect"], CodeWriterMode]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Initialize(BaseModel):
|
|
39
|
+
any_workspace_path: str
|
|
40
|
+
initial_files_to_read: list[str]
|
|
41
|
+
task_id_to_resume: str
|
|
42
|
+
mode_name: Literal["wcgw", "architect", "code_writer"]
|
|
43
|
+
code_writer_config: Optional[CodeWriterMode] = None
|
|
44
|
+
|
|
45
|
+
def model_post_init(self, __context: Any) -> None:
|
|
46
|
+
if self.mode_name == "code_writer":
|
|
47
|
+
assert (
|
|
48
|
+
self.code_writer_config is not None
|
|
49
|
+
), "code_writer_config can't be null when the mode is code_writer"
|
|
50
|
+
return super().model_post_init(__context)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def mode(self) -> ModesConfig:
|
|
54
|
+
if self.mode_name == "wcgw":
|
|
55
|
+
return "wcgw"
|
|
56
|
+
if self.mode_name == "architect":
|
|
57
|
+
return "architect"
|
|
58
|
+
assert (
|
|
59
|
+
self.code_writer_config is not None
|
|
60
|
+
), "code_writer_config can't be null when the mode is code_writer"
|
|
61
|
+
return self.code_writer_config
|
|
62
|
+
|
|
63
|
+
|
|
14
64
|
class BashCommand(BaseModel):
|
|
15
65
|
command: str
|
|
16
66
|
wait_for_seconds: Optional[int] = None
|
|
@@ -56,12 +106,6 @@ class FileEdit(BaseModel):
|
|
|
56
106
|
file_edit_using_search_replace_blocks: str
|
|
57
107
|
|
|
58
108
|
|
|
59
|
-
class Initialize(BaseModel):
|
|
60
|
-
any_workspace_path: str
|
|
61
|
-
initial_files_to_read: list[str]
|
|
62
|
-
task_id_to_resume: str
|
|
63
|
-
|
|
64
|
-
|
|
65
109
|
class GetScreenInfo(BaseModel):
|
|
66
110
|
docker_image_id: str
|
|
67
111
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.1
|
|
4
4
|
Summary: Shell and coding agent on claude and chatgpt
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
@@ -41,6 +41,8 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
[](https://codecov.io/gh/rusiaaman/wcgw)
|
|
42
42
|
|
|
43
43
|
## Updates
|
|
44
|
+
- [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
|
|
45
|
+
|
|
44
46
|
- [8 Jan 2025] Context saving tool for saving relevant file paths along with a description in a single file. Can be used as a task checkpoint or for knowledge transfer.
|
|
45
47
|
|
|
46
48
|
- [29 Dec 2024] Syntax checking on file writing and edits is now stable. Made `initialize` tool call useful; sending smart repo structure to claude if any repo is referenced. Large file handling is also now improved.
|
|
@@ -64,11 +66,16 @@ Description-Content-Type: text/markdown
|
|
|
64
66
|
- File edit based on search-replace tries to find correct search block if it has multiple matches based on previous search blocks. Fails otherwise (for correctness).
|
|
65
67
|
- File edit has spacing tolerant matching, with warning on issues like indentation mismatch. If there's no match, the closest match is returned to the AI to fix its mistakes.
|
|
66
68
|
- Using Aider-like search and replace, which has better performance than tool call based search and replace.
|
|
67
|
-
- ⚡ **Shell
|
|
69
|
+
- ⚡ **Shell optimizations**:
|
|
68
70
|
- Only one command is allowed to be run at a time, simplifying management and avoiding rogue processes. There's only single shell instance at any point of time.
|
|
69
71
|
- Current working directory is always returned after any shell command to prevent AI from getting lost.
|
|
70
72
|
- Command polling exits after a quick timeout to avoid slow feedback. However, status checking has wait tolerance based on fresh output streaming from a command. Both of these approach combined provides a good shell interaction experience.
|
|
71
73
|
- ⚡ **Saving repo context in a single file**: Task checkpointing using "ContextSave" tool saves detailed context in a single file. Tasks can later be resumed in a new chat asking "Resume `task id`". The saved file can be used to do other kinds of knowledge transfer, such as taking help from another AI.
|
|
74
|
+
- ⚡ **Easily switch between various modes**:
|
|
75
|
+
- Ask it to run in 'architect' mode for planning. Inspired by adier's architect mode, work with Claude to come up with a plan first. Leads to better accuracy and prevents premature file editing.
|
|
76
|
+
- Ask it to run in 'code-writer' mode for code editing and project building. You can provide specific paths with wild card support to prevent other files getting edited.
|
|
77
|
+
- By default it runs in 'wcgw' mode that has no restrictions and full authorisation.
|
|
78
|
+
- More details in [Modes section](#modes)
|
|
72
79
|
|
|
73
80
|
## Top use cases examples
|
|
74
81
|
|
|
@@ -148,6 +155,16 @@ Then ask claude to execute shell commands, read files, edit files, run your code
|
|
|
148
155
|
- You can in a new chat say "Resume '<task id>'", the AI should then call "Initialize" with the task id and load the context from there.
|
|
149
156
|
- Or you can directly open the file generated and share it with another AI for help.
|
|
150
157
|
|
|
158
|
+
#### Modes
|
|
159
|
+
There are three built-in modes. You may ask Claude to run in one of the modes, like "Use 'architect' mode"
|
|
160
|
+
| **Mode** | **Description** | **Allows** | **Denies** | **Invoke prompt** |
|
|
161
|
+
|-----------------|-----------------------------------------------------------------------------|---------------------------------------------------------|----------------------------------------------|----------------------------------------------------------------------------------------------------|
|
|
162
|
+
| **Architect** | Designed for you to work with Claude to investigate and understand your repo. | Read-only commands | FileEdit and Write tool | Run in mode='architect' |
|
|
163
|
+
| **Code-writer** | For code writing and development | Specified path globs for editing or writing, specified commands | FileEdit for paths not matching specified glob, Write for paths not matching specified glob | Run in code writer mode, only 'tests/**' allowed, only uv command allowed |
|
|
164
|
+
| **wcgw** | Default mode with everything allowed | Everything | Nothing | No prompt, or "Run in wcgw mode" |
|
|
165
|
+
|
|
166
|
+
Note: in code-writer mode either all commands are allowed or none are allowed for now. If you give a list of allowed commands, Claude is instructed to run only those commands, but no actual check happens. (WIP)
|
|
167
|
+
|
|
151
168
|
### [Optional] Vs code extension
|
|
152
169
|
https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw
|
|
153
170
|
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
wcgw/__init__.py,sha256=9K2QW7QuSLhMTVbKbBYd9UUp-ZyrfBrxcjuD_xk458k,118
|
|
2
|
-
wcgw/types_.py,sha256=
|
|
2
|
+
wcgw/types_.py,sha256=D3518a2azSKeW3D-ACYWJwsaqo7Oj-8BRRR2IhCUtNU,3414
|
|
3
3
|
wcgw/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
wcgw/client/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
5
|
-
wcgw/client/anthropic_client.py,sha256=
|
|
5
|
+
wcgw/client/anthropic_client.py,sha256=TLPQCc96cxLGGMHg1sYqX3_IxTm_-WQ1e-XPQD6i6xU,20958
|
|
6
6
|
wcgw/client/cli.py,sha256=-z0kpDAW3mzfQrQeZfaVJhBCAQY3HXnt9GdgQ8s-u0Y,1003
|
|
7
7
|
wcgw/client/common.py,sha256=OCH7Tx64jojz3M3iONUrGMadE07W21DiZs5sOxWX1Qc,1456
|
|
8
8
|
wcgw/client/computer_use.py,sha256=35NKAlMrxwD0TBlMMRnbCwz4g8TBRGOlcy-cmS-yJ_A,15247
|
|
9
9
|
wcgw/client/diff-instructions.txt,sha256=tmJ9Fu9XdO_72lYXQQNY9RZyx91bjxrXJf9d_KBz57k,1611
|
|
10
|
-
wcgw/client/memory.py,sha256=
|
|
11
|
-
wcgw/client/
|
|
10
|
+
wcgw/client/memory.py,sha256=8LdYsOhvCOoC1kfvDr85kNy07WnhPMvE6B2FRM2w85Y,2902
|
|
11
|
+
wcgw/client/modes.py,sha256=ZhCY-Orbk2JtbxbrPz-YNqLO6PA_zNvE1uNCD3JmnYA,10463
|
|
12
|
+
wcgw/client/openai_client.py,sha256=WvCnTij5QOPhe0VCdsk5Qo1Ob6yBdxFSvsCmnF0Jozw,17858
|
|
12
13
|
wcgw/client/openai_utils.py,sha256=KfMB1-p2zDiA7pPWwAVarochf7-qeL1UMgtlDV9DtKA,2662
|
|
13
14
|
wcgw/client/sys_utils.py,sha256=GajPntKhaTUMn6EOmopENWZNR2G_BJyuVbuot0x6veI,1376
|
|
14
|
-
wcgw/client/tools.py,sha256=
|
|
15
|
+
wcgw/client/tools.py,sha256=XjZ1UA7lxpopFmKWlOrAR4tlvEDVyyO8fJyC8-uwRW4,51119
|
|
15
16
|
wcgw/client/file_ops/diff_edit.py,sha256=o0ucArVwn5p6QTDgYsjLfMy4TJXxUG3qcppFBNF3bbQ,16751
|
|
16
17
|
wcgw/client/file_ops/search_replace.py,sha256=89ieDC9fTsIKPDx7CJwnwpX32dRdSlMKoBtKVXc7VWI,3971
|
|
17
18
|
wcgw/client/mcp_server/Readme.md,sha256=I8N4dHkTUVGNQ63BQkBMBhCCBTgqGOSF_pUR6iOEiUk,2495
|
|
18
19
|
wcgw/client/mcp_server/__init__.py,sha256=hyPPwO9cabAJsOMWhKyat9yl7OlSmIobaoAZKHu3DMc,381
|
|
19
|
-
wcgw/client/mcp_server/server.py,sha256=
|
|
20
|
+
wcgw/client/mcp_server/server.py,sha256=C-wqaoS7sGWhQQW0pKS3HLjsUwjo2zzDJZm8d6znozU,12812
|
|
20
21
|
wcgw/client/repo_ops/display_tree.py,sha256=5FD4hfMkM2cIZnXlu7WfJswJLthj0SkuHlkGH6dpWQU,4632
|
|
21
22
|
wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9tZ4M,2144
|
|
22
23
|
wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4qcYFY0kqk,315087
|
|
@@ -46,8 +47,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
|
|
|
46
47
|
mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
|
|
47
48
|
mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
|
|
48
49
|
mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
|
|
49
|
-
wcgw-2.
|
|
50
|
-
wcgw-2.
|
|
51
|
-
wcgw-2.
|
|
52
|
-
wcgw-2.
|
|
53
|
-
wcgw-2.
|
|
50
|
+
wcgw-2.8.1.dist-info/METADATA,sha256=hYCgmhpq1DszvWaj4tIH5BIoACRPAqxFbDhywRyocAs,11945
|
|
51
|
+
wcgw-2.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
52
|
+
wcgw-2.8.1.dist-info/entry_points.txt,sha256=eKo1omwbAggWlQ0l7GKoR7uV1-j16nk9tK0BhC2Oz_E,120
|
|
53
|
+
wcgw-2.8.1.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
54
|
+
wcgw-2.8.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|