wcgw 2.8.10__py3-none-any.whl → 3.0.1rc1__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/__init__.py +1 -1
- wcgw/client/bash_state/bash_state.py +768 -0
- wcgw/client/encoder/__init__.py +47 -0
- wcgw/client/mcp_server/Readme.md +2 -88
- wcgw/client/mcp_server/__init__.py +2 -2
- wcgw/client/mcp_server/server.py +46 -198
- wcgw/client/modes.py +3 -9
- wcgw/client/repo_ops/display_tree.py +1 -12
- wcgw/client/tool_prompts.py +105 -0
- wcgw/client/tools.py +230 -1084
- wcgw/py.typed +0 -0
- wcgw/relay/client.py +95 -0
- wcgw/relay/serve.py +11 -45
- wcgw/types_.py +51 -61
- {wcgw-2.8.10.dist-info → wcgw-3.0.1rc1.dist-info}/METADATA +2 -3
- {wcgw-2.8.10.dist-info → wcgw-3.0.1rc1.dist-info}/RECORD +22 -19
- wcgw_cli/anthropic_client.py +253 -365
- wcgw_cli/cli.py +0 -2
- wcgw_cli/openai_client.py +223 -280
- wcgw/client/computer_use.py +0 -435
- wcgw/client/sys_utils.py +0 -41
- {wcgw-2.8.10.dist-info → wcgw-3.0.1rc1.dist-info}/WHEEL +0 -0
- {wcgw-2.8.10.dist-info → wcgw-3.0.1rc1.dist-info}/entry_points.txt +0 -0
- {wcgw-2.8.10.dist-info → wcgw-3.0.1rc1.dist-info}/licenses/LICENSE +0 -0
wcgw/client/tools.py
CHANGED
|
@@ -1,482 +1,107 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
import datetime
|
|
3
2
|
import fnmatch
|
|
4
3
|
import glob
|
|
5
|
-
import importlib.metadata
|
|
6
4
|
import json
|
|
7
5
|
import mimetypes
|
|
8
6
|
import os
|
|
9
|
-
import platform
|
|
10
|
-
import re
|
|
11
|
-
import shlex
|
|
12
|
-
import subprocess
|
|
13
|
-
import time
|
|
14
7
|
import traceback
|
|
15
|
-
import
|
|
8
|
+
from dataclasses import dataclass
|
|
16
9
|
from os.path import expanduser
|
|
17
10
|
from pathlib import Path
|
|
18
|
-
from tempfile import NamedTemporaryFile
|
|
11
|
+
from tempfile import NamedTemporaryFile
|
|
19
12
|
from typing import (
|
|
20
|
-
Any,
|
|
21
13
|
Callable,
|
|
22
14
|
Literal,
|
|
23
15
|
Optional,
|
|
24
16
|
ParamSpec,
|
|
25
|
-
Protocol,
|
|
26
17
|
Type,
|
|
27
18
|
TypeVar,
|
|
28
19
|
)
|
|
29
20
|
|
|
30
|
-
import pexpect
|
|
31
|
-
import pyte
|
|
32
|
-
import rich
|
|
33
|
-
import tokenizers # type: ignore
|
|
34
|
-
import typer
|
|
35
|
-
import websockets
|
|
36
21
|
from openai.types.chat import (
|
|
37
22
|
ChatCompletionMessageParam,
|
|
38
23
|
)
|
|
39
24
|
from pydantic import BaseModel, TypeAdapter
|
|
40
25
|
from syntax_checker import check_syntax
|
|
41
|
-
|
|
42
|
-
from
|
|
26
|
+
|
|
27
|
+
from wcgw.client.bash_state.bash_state import get_status
|
|
43
28
|
|
|
44
29
|
from ..types_ import (
|
|
45
30
|
BashCommand,
|
|
46
|
-
BashInteraction,
|
|
47
31
|
CodeWriterMode,
|
|
32
|
+
Console,
|
|
48
33
|
ContextSave,
|
|
49
34
|
FileEdit,
|
|
50
|
-
FileEditFindReplace,
|
|
51
|
-
GetScreenInfo,
|
|
52
35
|
Initialize,
|
|
53
|
-
Keyboard,
|
|
54
36
|
Modes,
|
|
55
37
|
ModesConfig,
|
|
56
|
-
Mouse,
|
|
57
38
|
ReadFiles,
|
|
58
39
|
ReadImage,
|
|
59
|
-
|
|
60
|
-
ScreenShot,
|
|
40
|
+
ResetWcgw,
|
|
61
41
|
WriteIfEmpty,
|
|
62
42
|
)
|
|
63
|
-
from .
|
|
43
|
+
from .bash_state.bash_state import (
|
|
44
|
+
BashState,
|
|
45
|
+
execute_bash,
|
|
46
|
+
)
|
|
47
|
+
from .encoder import EncoderDecoder, get_default_encoder
|
|
64
48
|
from .file_ops.search_replace import search_replace_edit
|
|
65
49
|
from .memory import load_memory, save_memory
|
|
66
50
|
from .modes import (
|
|
67
51
|
ARCHITECT_PROMPT,
|
|
68
52
|
WCGW_PROMPT,
|
|
69
|
-
BashCommandMode,
|
|
70
|
-
FileEditMode,
|
|
71
|
-
WriteIfEmptyMode,
|
|
72
53
|
code_writer_prompt,
|
|
73
54
|
modes_to_state,
|
|
74
55
|
)
|
|
75
56
|
from .repo_ops.repo_context import get_repo_context
|
|
76
|
-
from .sys_utils import command_run
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class Console(Protocol):
|
|
80
|
-
def print(self, msg: str, *args: Any, **kwargs: Any) -> None: ...
|
|
81
|
-
|
|
82
|
-
def log(self, msg: str, *args: Any, **kwargs: Any) -> None: ...
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console: Console = rich.console.Console(style="magenta", highlight=False, markup=False)
|
|
86
|
-
|
|
87
|
-
TIMEOUT = 5
|
|
88
|
-
TIMEOUT_WHILE_OUTPUT = 20
|
|
89
|
-
OUTPUT_WAIT_PATIENCE = 3
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def render_terminal_output(text: str) -> list[str]:
|
|
93
|
-
screen = pyte.Screen(160, 500)
|
|
94
|
-
screen.set_mode(pyte.modes.LNM)
|
|
95
|
-
stream = pyte.Stream(screen)
|
|
96
|
-
stream.feed(text)
|
|
97
|
-
# Filter out empty lines
|
|
98
|
-
dsp = screen.display[::-1]
|
|
99
|
-
for i, line in enumerate(dsp):
|
|
100
|
-
if line.strip():
|
|
101
|
-
break
|
|
102
|
-
else:
|
|
103
|
-
i = len(dsp)
|
|
104
|
-
lines = screen.display[: len(dsp) - i]
|
|
105
|
-
return lines
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def get_incremental_output(old_output: list[str], new_output: list[str]) -> list[str]:
|
|
109
|
-
nold = len(old_output)
|
|
110
|
-
nnew = len(new_output)
|
|
111
|
-
if not old_output:
|
|
112
|
-
return new_output
|
|
113
|
-
for i in range(nnew - 1, -1, -1):
|
|
114
|
-
if new_output[i] != old_output[-1]:
|
|
115
|
-
continue
|
|
116
|
-
for j in range(i - 1, -1, -1):
|
|
117
|
-
if (nold - 1 + j - i) < 0:
|
|
118
|
-
break
|
|
119
|
-
if new_output[j] != old_output[-1 + j - i]:
|
|
120
|
-
break
|
|
121
|
-
else:
|
|
122
|
-
return new_output[i + 1 :]
|
|
123
|
-
return new_output
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class Confirmation(BaseModel):
|
|
127
|
-
prompt: str
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def ask_confirmation(prompt: Confirmation) -> str:
|
|
131
|
-
response = input(prompt.prompt + " [y/n] ")
|
|
132
|
-
return "Yes" if response.lower() == "y" else "No"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
PROMPT_CONST = "#" + "@wcgw@#"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def is_mac() -> bool:
|
|
139
|
-
return platform.system() == "Darwin"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def get_tmpdir() -> str:
|
|
143
|
-
current_tmpdir = os.environ.get("TMPDIR", "")
|
|
144
|
-
if current_tmpdir or not is_mac():
|
|
145
|
-
return current_tmpdir
|
|
146
|
-
try:
|
|
147
|
-
# Fix issue while running ocrmypdf -> tesseract -> leptonica, set TMPDIR
|
|
148
|
-
# https://github.com/tesseract-ocr/tesseract/issues/4333
|
|
149
|
-
result = subprocess.check_output(
|
|
150
|
-
["getconf", "DARWIN_USER_TEMP_DIR"],
|
|
151
|
-
text=True,
|
|
152
|
-
).strip()
|
|
153
|
-
return result
|
|
154
|
-
except subprocess.CalledProcessError:
|
|
155
|
-
return "//tmp"
|
|
156
|
-
except Exception:
|
|
157
|
-
return ""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def start_shell(is_restricted_mode: bool, initial_dir: str) -> pexpect.spawn: # type: ignore[type-arg]
|
|
161
|
-
cmd = "/bin/bash"
|
|
162
|
-
if is_restricted_mode:
|
|
163
|
-
cmd += " -r"
|
|
164
|
-
|
|
165
|
-
overrideenv = {**os.environ, "PS1": PROMPT_CONST, "TMPDIR": get_tmpdir()}
|
|
166
|
-
try:
|
|
167
|
-
shell = pexpect.spawn(
|
|
168
|
-
cmd,
|
|
169
|
-
env=overrideenv, # type: ignore[arg-type]
|
|
170
|
-
echo=False,
|
|
171
|
-
encoding="utf-8",
|
|
172
|
-
timeout=TIMEOUT,
|
|
173
|
-
cwd=initial_dir,
|
|
174
|
-
codec_errors="backslashreplace",
|
|
175
|
-
)
|
|
176
|
-
shell.sendline(
|
|
177
|
-
f"export PROMPT_COMMAND= PS1={PROMPT_CONST}"
|
|
178
|
-
) # Unset prompt command to avoid interfering
|
|
179
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
180
|
-
except Exception as e:
|
|
181
|
-
console.print(traceback.format_exc())
|
|
182
|
-
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
183
|
-
|
|
184
|
-
shell = pexpect.spawn(
|
|
185
|
-
"/bin/bash --noprofile --norc",
|
|
186
|
-
env=overrideenv, # type: ignore[arg-type]
|
|
187
|
-
echo=False,
|
|
188
|
-
encoding="utf-8",
|
|
189
|
-
timeout=TIMEOUT,
|
|
190
|
-
codec_errors="backslashreplace",
|
|
191
|
-
)
|
|
192
|
-
shell.sendline(f"export PS1={PROMPT_CONST}")
|
|
193
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
194
|
-
|
|
195
|
-
shell.sendline("stty -icanon -echo")
|
|
196
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
197
|
-
shell.sendline("set +o pipefail")
|
|
198
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
199
|
-
shell.sendline("export GIT_PAGER=cat PAGER=cat")
|
|
200
|
-
shell.expect(PROMPT_CONST, timeout=TIMEOUT)
|
|
201
|
-
return shell
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _is_int(mystr: str) -> bool:
|
|
205
|
-
try:
|
|
206
|
-
int(mystr)
|
|
207
|
-
return True
|
|
208
|
-
except ValueError:
|
|
209
|
-
return False
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
class BashState:
|
|
216
|
-
def __init__(
|
|
217
|
-
self,
|
|
218
|
-
working_dir: str,
|
|
219
|
-
bash_command_mode: Optional[BashCommandMode],
|
|
220
|
-
file_edit_mode: Optional[FileEditMode],
|
|
221
|
-
write_if_empty_mode: Optional[WriteIfEmptyMode],
|
|
222
|
-
mode: Optional[Modes],
|
|
223
|
-
whitelist_for_overwrite: Optional[set[str]] = None,
|
|
224
|
-
) -> None:
|
|
225
|
-
self._cwd = working_dir or os.getcwd()
|
|
226
|
-
self._bash_command_mode: BashCommandMode = bash_command_mode or BashCommandMode(
|
|
227
|
-
"normal_mode", "all"
|
|
228
|
-
)
|
|
229
|
-
self._file_edit_mode: FileEditMode = file_edit_mode or FileEditMode("all")
|
|
230
|
-
self._write_if_empty_mode: WriteIfEmptyMode = (
|
|
231
|
-
write_if_empty_mode or WriteIfEmptyMode("all")
|
|
232
|
-
)
|
|
233
|
-
self._mode = mode or Modes.wcgw
|
|
234
|
-
self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
|
|
235
|
-
self._prompt = PROMPT_CONST
|
|
236
|
-
self._init_shell()
|
|
237
|
-
|
|
238
|
-
@property
|
|
239
|
-
def mode(self) -> Modes:
|
|
240
|
-
return self._mode
|
|
241
|
-
|
|
242
|
-
@property
|
|
243
|
-
def bash_command_mode(self) -> BashCommandMode:
|
|
244
|
-
return self._bash_command_mode
|
|
245
|
-
|
|
246
|
-
@property
|
|
247
|
-
def file_edit_mode(self) -> FileEditMode:
|
|
248
|
-
return self._file_edit_mode
|
|
249
|
-
|
|
250
|
-
@property
|
|
251
|
-
def write_if_empty_mode(self) -> WriteIfEmptyMode:
|
|
252
|
-
return self._write_if_empty_mode
|
|
253
|
-
|
|
254
|
-
def ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
255
|
-
if self._prompt != PROMPT_CONST:
|
|
256
|
-
return None
|
|
257
|
-
shell = self.shell
|
|
258
|
-
# First reset the prompt in case venv was sourced or other reasons.
|
|
259
|
-
shell.sendline(f"export PS1={self._prompt}")
|
|
260
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
261
|
-
# Reset echo also if it was enabled
|
|
262
|
-
shell.sendline("stty -icanon -echo")
|
|
263
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
264
|
-
shell.sendline("set +o pipefail")
|
|
265
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
266
|
-
shell.sendline("export GIT_PAGER=cat PAGER=cat")
|
|
267
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
268
|
-
shell.sendline("jobs | wc -l")
|
|
269
|
-
before = ""
|
|
270
|
-
counts = 0
|
|
271
|
-
while not _is_int(before): # Consume all previous output
|
|
272
|
-
try:
|
|
273
|
-
shell.expect(self._prompt, timeout=0.2)
|
|
274
|
-
except pexpect.TIMEOUT:
|
|
275
|
-
console.print(f"Couldn't get exit code, before: {before}")
|
|
276
|
-
raise
|
|
277
|
-
|
|
278
|
-
before_val = shell.before
|
|
279
|
-
if not isinstance(before_val, str):
|
|
280
|
-
before_val = str(before_val)
|
|
281
|
-
assert isinstance(before_val, str)
|
|
282
|
-
before_lines = render_terminal_output(before_val)
|
|
283
|
-
before = "\n".join(before_lines).strip()
|
|
284
|
-
counts += 1
|
|
285
|
-
if counts > 100:
|
|
286
|
-
raise ValueError(
|
|
287
|
-
"Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
|
|
288
|
-
)
|
|
289
57
|
|
|
290
|
-
try:
|
|
291
|
-
return int(before)
|
|
292
|
-
except ValueError:
|
|
293
|
-
raise ValueError(f"Malformed output: {before}")
|
|
294
|
-
|
|
295
|
-
def _init_shell(self) -> None:
|
|
296
|
-
self._prompt = PROMPT_CONST
|
|
297
|
-
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
298
|
-
self._is_in_docker: Optional[str] = ""
|
|
299
|
-
# Ensure self._cwd exists
|
|
300
|
-
os.makedirs(self._cwd, exist_ok=True)
|
|
301
|
-
self._shell = start_shell(
|
|
302
|
-
self._bash_command_mode.bash_mode == "restricted_mode",
|
|
303
|
-
self._cwd,
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
self._pending_output = ""
|
|
307
|
-
|
|
308
|
-
# Get exit info to ensure shell is ready
|
|
309
|
-
self.ensure_env_and_bg_jobs()
|
|
310
|
-
|
|
311
|
-
@property
|
|
312
|
-
def shell(self) -> pexpect.spawn: # type: ignore
|
|
313
|
-
return self._shell
|
|
314
|
-
|
|
315
|
-
def set_pending(self, last_pending_output: str) -> None:
|
|
316
|
-
if not isinstance(self._state, datetime.datetime):
|
|
317
|
-
self._state = datetime.datetime.now()
|
|
318
|
-
self._pending_output = last_pending_output
|
|
319
|
-
|
|
320
|
-
def set_repl(self) -> None:
|
|
321
|
-
self._state = "repl"
|
|
322
|
-
self._pending_output = ""
|
|
323
58
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return "pending"
|
|
59
|
+
@dataclass
|
|
60
|
+
class Context:
|
|
61
|
+
bash_state: BashState
|
|
62
|
+
console: Console
|
|
329
63
|
|
|
330
|
-
@property
|
|
331
|
-
def is_in_docker(self) -> Optional[str]:
|
|
332
|
-
return self._is_in_docker
|
|
333
64
|
|
|
334
|
-
|
|
335
|
-
self._is_in_docker = docker_image_id
|
|
65
|
+
INITIALIZED = False
|
|
336
66
|
|
|
337
|
-
@property
|
|
338
|
-
def cwd(self) -> str:
|
|
339
|
-
return self._cwd
|
|
340
67
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
before_val = self.shell.before
|
|
349
|
-
if not isinstance(before_val, str):
|
|
350
|
-
before_val = str(before_val)
|
|
351
|
-
before_lines = render_terminal_output(before_val)
|
|
352
|
-
current_dir = "\n".join(before_lines).strip()
|
|
353
|
-
self._cwd = current_dir
|
|
354
|
-
return current_dir
|
|
355
|
-
|
|
356
|
-
def reset_shell(self) -> None:
|
|
357
|
-
self.shell.close(True)
|
|
358
|
-
self._init_shell()
|
|
359
|
-
|
|
360
|
-
def serialize(self) -> dict[str, Any]:
|
|
361
|
-
"""Serialize BashState to a dictionary for saving"""
|
|
362
|
-
return {
|
|
363
|
-
"bash_command_mode": self._bash_command_mode.serialize(),
|
|
364
|
-
"file_edit_mode": self._file_edit_mode.serialize(),
|
|
365
|
-
"write_if_empty_mode": self._write_if_empty_mode.serialize(),
|
|
366
|
-
"whitelist_for_overwrite": list(self._whitelist_for_overwrite),
|
|
367
|
-
"mode": self._mode,
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
@staticmethod
|
|
371
|
-
def parse_state(
|
|
372
|
-
state: dict[str, Any],
|
|
373
|
-
) -> tuple[BashCommandMode, FileEditMode, WriteIfEmptyMode, Modes, list[str]]:
|
|
374
|
-
return (
|
|
375
|
-
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
376
|
-
FileEditMode.deserialize(state["file_edit_mode"]),
|
|
377
|
-
WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
|
|
378
|
-
Modes[str(state["mode"])],
|
|
379
|
-
state["whitelist_for_overwrite"],
|
|
68
|
+
def get_mode_prompt(context: Context) -> str:
|
|
69
|
+
mode_prompt = ""
|
|
70
|
+
if context.bash_state.mode == Modes.code_writer:
|
|
71
|
+
mode_prompt = code_writer_prompt(
|
|
72
|
+
context.bash_state.file_edit_mode.allowed_globs,
|
|
73
|
+
context.bash_state.write_if_empty_mode.allowed_globs,
|
|
74
|
+
"all" if context.bash_state.bash_command_mode.allowed_commands else [],
|
|
380
75
|
)
|
|
76
|
+
elif context.bash_state.mode == Modes.architect:
|
|
77
|
+
mode_prompt = ARCHITECT_PROMPT
|
|
78
|
+
else:
|
|
79
|
+
mode_prompt = WCGW_PROMPT
|
|
381
80
|
|
|
382
|
-
|
|
383
|
-
self,
|
|
384
|
-
bash_command_mode: BashCommandMode,
|
|
385
|
-
file_edit_mode: FileEditMode,
|
|
386
|
-
write_if_empty_mode: WriteIfEmptyMode,
|
|
387
|
-
mode: Modes,
|
|
388
|
-
whitelist_for_overwrite: list[str],
|
|
389
|
-
cwd: str,
|
|
390
|
-
) -> None:
|
|
391
|
-
"""Create a new BashState instance from a serialized state dictionary"""
|
|
392
|
-
self._bash_command_mode = bash_command_mode
|
|
393
|
-
self._cwd = cwd or self._cwd
|
|
394
|
-
self._file_edit_mode = file_edit_mode
|
|
395
|
-
self._write_if_empty_mode = write_if_empty_mode
|
|
396
|
-
self._whitelist_for_overwrite = set(whitelist_for_overwrite)
|
|
397
|
-
self._mode = mode
|
|
398
|
-
self.reset_shell()
|
|
399
|
-
|
|
400
|
-
def get_pending_for(self) -> str:
|
|
401
|
-
if isinstance(self._state, datetime.datetime):
|
|
402
|
-
timedelta = datetime.datetime.now() - self._state
|
|
403
|
-
return (
|
|
404
|
-
str(
|
|
405
|
-
int(
|
|
406
|
-
(
|
|
407
|
-
timedelta + datetime.timedelta(seconds=TIMEOUT)
|
|
408
|
-
).total_seconds()
|
|
409
|
-
)
|
|
410
|
-
)
|
|
411
|
-
+ " seconds"
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
return "Not pending"
|
|
415
|
-
|
|
416
|
-
@property
|
|
417
|
-
def whitelist_for_overwrite(self) -> set[str]:
|
|
418
|
-
return self._whitelist_for_overwrite
|
|
419
|
-
|
|
420
|
-
def add_to_whitelist_for_overwrite(self, file_path: str) -> None:
|
|
421
|
-
self._whitelist_for_overwrite.add(file_path)
|
|
422
|
-
|
|
423
|
-
@property
|
|
424
|
-
def pending_output(self) -> str:
|
|
425
|
-
return self._pending_output
|
|
426
|
-
|
|
427
|
-
def update_repl_prompt(self, command: str) -> bool:
|
|
428
|
-
if re.match(r"^wcgw_update_prompt\(\)$", command.strip()):
|
|
429
|
-
self.shell.sendintr()
|
|
430
|
-
index = self.shell.expect([self._prompt, pexpect.TIMEOUT], timeout=0.2)
|
|
431
|
-
if index == 0:
|
|
432
|
-
return True
|
|
433
|
-
before = self.shell.before or ""
|
|
434
|
-
assert before, "Something went wrong updating repl prompt"
|
|
435
|
-
self._prompt = before.split("\n")[-1].strip()
|
|
436
|
-
# Escape all regex
|
|
437
|
-
self._prompt = re.escape(self._prompt)
|
|
438
|
-
console.print(f"Trying to update prompt to: {self._prompt.encode()!r}")
|
|
439
|
-
index = 0
|
|
440
|
-
counts = 0
|
|
441
|
-
while index == 0:
|
|
442
|
-
# Consume all REPL prompts till now
|
|
443
|
-
index = self.shell.expect([self._prompt, pexpect.TIMEOUT], timeout=0.2)
|
|
444
|
-
counts += 1
|
|
445
|
-
if counts > 100:
|
|
446
|
-
raise ValueError(
|
|
447
|
-
"Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
|
|
448
|
-
)
|
|
449
|
-
console.print(f"Prompt updated to: {self._prompt}")
|
|
450
|
-
return True
|
|
451
|
-
return False
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
BASH_STATE = BashState(os.getcwd(), None, None, None, None)
|
|
455
|
-
INITIALIZED = False
|
|
81
|
+
return mode_prompt
|
|
456
82
|
|
|
457
83
|
|
|
458
84
|
def initialize(
|
|
85
|
+
context: Context,
|
|
459
86
|
any_workspace_path: str,
|
|
460
87
|
read_files_: list[str],
|
|
461
88
|
task_id_to_resume: str,
|
|
462
89
|
max_tokens: Optional[int],
|
|
463
90
|
mode: ModesConfig,
|
|
464
|
-
) -> str:
|
|
465
|
-
global BASH_STATE
|
|
466
|
-
|
|
91
|
+
) -> tuple[str, Context]:
|
|
467
92
|
# Expand the workspace path
|
|
468
|
-
any_workspace_path = expand_user(any_workspace_path
|
|
93
|
+
any_workspace_path = expand_user(any_workspace_path)
|
|
469
94
|
repo_context = ""
|
|
470
95
|
|
|
471
96
|
memory = ""
|
|
472
|
-
|
|
97
|
+
loaded_state = None
|
|
473
98
|
if task_id_to_resume:
|
|
474
99
|
try:
|
|
475
|
-
project_root_path, task_mem,
|
|
100
|
+
project_root_path, task_mem, loaded_state = load_memory(
|
|
476
101
|
task_id_to_resume,
|
|
477
102
|
max_tokens,
|
|
478
|
-
lambda x: default_enc.
|
|
479
|
-
lambda x: default_enc.
|
|
103
|
+
lambda x: default_enc.encoder(x),
|
|
104
|
+
lambda x: default_enc.decoder(x),
|
|
480
105
|
)
|
|
481
106
|
memory = "Following is the retrieved task:\n" + task_mem
|
|
482
107
|
if os.path.exists(project_root_path):
|
|
@@ -494,7 +119,7 @@ def initialize(
|
|
|
494
119
|
if not read_files_:
|
|
495
120
|
read_files_ = [any_workspace_path]
|
|
496
121
|
any_workspace_path = os.path.dirname(any_workspace_path)
|
|
497
|
-
repo_context, folder_to_start = get_repo_context(any_workspace_path,
|
|
122
|
+
repo_context, folder_to_start = get_repo_context(any_workspace_path, 50)
|
|
498
123
|
|
|
499
124
|
repo_context = f"---\n# Workspace structure\n{repo_context}\n---\n"
|
|
500
125
|
|
|
@@ -513,40 +138,40 @@ def initialize(
|
|
|
513
138
|
f"\nInfo: Workspace path {any_workspace_path} does not exist."
|
|
514
139
|
)
|
|
515
140
|
# Restore bash state if available
|
|
516
|
-
if
|
|
141
|
+
if loaded_state is not None:
|
|
517
142
|
try:
|
|
518
|
-
parsed_state = BashState.parse_state(
|
|
143
|
+
parsed_state = BashState.parse_state(loaded_state)
|
|
519
144
|
if mode == "wcgw":
|
|
520
|
-
|
|
145
|
+
context.bash_state.load_state(
|
|
521
146
|
parsed_state[0],
|
|
522
147
|
parsed_state[1],
|
|
523
148
|
parsed_state[2],
|
|
524
149
|
parsed_state[3],
|
|
525
|
-
parsed_state[4] + list(
|
|
150
|
+
parsed_state[4] + list(context.bash_state.whitelist_for_overwrite),
|
|
526
151
|
str(folder_to_start) if folder_to_start else "",
|
|
527
152
|
)
|
|
528
153
|
else:
|
|
529
154
|
state = modes_to_state(mode)
|
|
530
|
-
|
|
155
|
+
context.bash_state.load_state(
|
|
531
156
|
state[0],
|
|
532
157
|
state[1],
|
|
533
158
|
state[2],
|
|
534
159
|
state[3],
|
|
535
|
-
parsed_state[4] + list(
|
|
160
|
+
parsed_state[4] + list(context.bash_state.whitelist_for_overwrite),
|
|
536
161
|
str(folder_to_start) if folder_to_start else "",
|
|
537
162
|
)
|
|
538
163
|
except ValueError:
|
|
539
|
-
console.print(traceback.format_exc())
|
|
540
|
-
console.print("Error: couldn't load bash state")
|
|
164
|
+
context.console.print(traceback.format_exc())
|
|
165
|
+
context.console.print("Error: couldn't load bash state")
|
|
541
166
|
pass
|
|
542
167
|
else:
|
|
543
168
|
state = modes_to_state(mode)
|
|
544
|
-
|
|
169
|
+
context.bash_state.load_state(
|
|
545
170
|
state[0],
|
|
546
171
|
state[1],
|
|
547
172
|
state[2],
|
|
548
173
|
state[3],
|
|
549
|
-
list(
|
|
174
|
+
list(context.bash_state.whitelist_for_overwrite),
|
|
550
175
|
str(folder_to_start) if folder_to_start else "",
|
|
551
176
|
)
|
|
552
177
|
del mode
|
|
@@ -558,31 +183,19 @@ def initialize(
|
|
|
558
183
|
os.path.join(folder_to_start, f) if not os.path.isabs(f) else f
|
|
559
184
|
for f in read_files_
|
|
560
185
|
]
|
|
561
|
-
initial_files = read_files(read_files_, max_tokens)
|
|
186
|
+
initial_files = read_files(read_files_, max_tokens, context)
|
|
562
187
|
initial_files_context = f"---\n# Requested files\n{initial_files}\n---\n"
|
|
563
188
|
|
|
564
189
|
uname_sysname = os.uname().sysname
|
|
565
190
|
uname_machine = os.uname().machine
|
|
566
|
-
|
|
567
|
-
mode_prompt = ""
|
|
568
|
-
if BASH_STATE.mode == Modes.code_writer:
|
|
569
|
-
mode_prompt = code_writer_prompt(
|
|
570
|
-
BASH_STATE.file_edit_mode.allowed_globs,
|
|
571
|
-
BASH_STATE.write_if_empty_mode.allowed_globs,
|
|
572
|
-
"all" if BASH_STATE.bash_command_mode.allowed_commands else [],
|
|
573
|
-
)
|
|
574
|
-
elif BASH_STATE.mode == Modes.architect:
|
|
575
|
-
mode_prompt = ARCHITECT_PROMPT
|
|
576
|
-
else:
|
|
577
|
-
mode_prompt = WCGW_PROMPT
|
|
578
|
-
|
|
191
|
+
mode_prompt = get_mode_prompt(context)
|
|
579
192
|
output = f"""
|
|
580
193
|
{mode_prompt}
|
|
581
194
|
|
|
582
195
|
# Environment
|
|
583
196
|
System: {uname_sysname}
|
|
584
197
|
Machine: {uname_machine}
|
|
585
|
-
Initialized in directory (also cwd): {
|
|
198
|
+
Initialized in directory (also cwd): {context.bash_state.cwd}
|
|
586
199
|
|
|
587
200
|
{repo_context}
|
|
588
201
|
|
|
@@ -593,40 +206,58 @@ Initialized in directory (also cwd): {BASH_STATE.cwd}
|
|
|
593
206
|
{memory}
|
|
594
207
|
"""
|
|
595
208
|
|
|
596
|
-
|
|
597
|
-
INITIALIZED = True
|
|
598
|
-
|
|
599
|
-
return output
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
def reset_shell() -> str:
|
|
603
|
-
BASH_STATE.reset_shell()
|
|
604
|
-
return "Reset successful" + get_status()
|
|
209
|
+
return output, context
|
|
605
210
|
|
|
606
211
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
212
|
+
def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
|
|
213
|
+
if reset_wcgw.change_mode:
|
|
214
|
+
# Convert to the type expected by modes_to_state
|
|
215
|
+
mode_config: ModesConfig
|
|
216
|
+
if reset_wcgw.change_mode == "code_writer":
|
|
217
|
+
if not reset_wcgw.code_writer_config:
|
|
218
|
+
return "Error: code_writer_config is required when changing to code_writer mode"
|
|
219
|
+
mode_config = reset_wcgw.code_writer_config
|
|
220
|
+
else:
|
|
221
|
+
mode_config = reset_wcgw.change_mode
|
|
613
222
|
|
|
223
|
+
# Get new state configuration
|
|
224
|
+
bash_command_mode, file_edit_mode, write_if_empty_mode, mode = modes_to_state(
|
|
225
|
+
mode_config
|
|
226
|
+
)
|
|
614
227
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
228
|
+
# Reset shell with new mode
|
|
229
|
+
context.bash_state.load_state(
|
|
230
|
+
bash_command_mode,
|
|
231
|
+
file_edit_mode,
|
|
232
|
+
write_if_empty_mode,
|
|
233
|
+
mode,
|
|
234
|
+
list(context.bash_state.whitelist_for_overwrite),
|
|
235
|
+
reset_wcgw.starting_directory,
|
|
236
|
+
)
|
|
237
|
+
mode_prompt = get_mode_prompt(context)
|
|
238
|
+
return (
|
|
239
|
+
f"Reset successful with mode change to {reset_wcgw.change_mode}.\n"
|
|
240
|
+
+ mode_prompt
|
|
241
|
+
+ "\n"
|
|
242
|
+
+ get_status(context.bash_state)
|
|
243
|
+
)
|
|
621
244
|
else:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
245
|
+
# Regular reset without mode change - keep same mode but update directory
|
|
246
|
+
bash_command_mode = context.bash_state.bash_command_mode
|
|
247
|
+
file_edit_mode = context.bash_state.file_edit_mode
|
|
248
|
+
write_if_empty_mode = context.bash_state.write_if_empty_mode
|
|
249
|
+
mode = context.bash_state.mode
|
|
250
|
+
|
|
251
|
+
# Reload state with new directory
|
|
252
|
+
context.bash_state.load_state(
|
|
253
|
+
bash_command_mode,
|
|
254
|
+
file_edit_mode,
|
|
255
|
+
write_if_empty_mode,
|
|
256
|
+
mode,
|
|
257
|
+
list(context.bash_state.whitelist_for_overwrite),
|
|
258
|
+
reset_wcgw.starting_directory,
|
|
259
|
+
)
|
|
260
|
+
return "Reset successful" + get_status(context.bash_state)
|
|
630
261
|
|
|
631
262
|
|
|
632
263
|
T = TypeVar("T")
|
|
@@ -639,238 +270,12 @@ def save_out_of_context(content: str, suffix: str) -> str:
|
|
|
639
270
|
return file_path
|
|
640
271
|
|
|
641
272
|
|
|
642
|
-
def
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
def expand_user(path: str, docker_id: Optional[str]) -> str:
|
|
647
|
-
if not path or not path.startswith("~") or docker_id:
|
|
273
|
+
def expand_user(path: str) -> str:
|
|
274
|
+
if not path or not path.startswith("~"):
|
|
648
275
|
return path
|
|
649
276
|
return expanduser(path)
|
|
650
277
|
|
|
651
278
|
|
|
652
|
-
def _incremental_text(text: str, last_pending_output: str) -> str:
|
|
653
|
-
# text = render_terminal_output(text[-100_000:])
|
|
654
|
-
text = text[-100_000:]
|
|
655
|
-
|
|
656
|
-
last_pending_output_rendered_lines = render_terminal_output(last_pending_output)
|
|
657
|
-
last_pending_output_rendered = "\n".join(last_pending_output_rendered_lines)
|
|
658
|
-
last_rendered_lines = last_pending_output_rendered.split("\n")
|
|
659
|
-
if not last_rendered_lines:
|
|
660
|
-
return rstrip(render_terminal_output(text))
|
|
661
|
-
|
|
662
|
-
text = text[len(last_pending_output) :]
|
|
663
|
-
old_rendered_applied = render_terminal_output(last_pending_output_rendered + text)
|
|
664
|
-
# True incremental is then
|
|
665
|
-
rendered = get_incremental_output(last_rendered_lines[:-1], old_rendered_applied)
|
|
666
|
-
|
|
667
|
-
if not rendered:
|
|
668
|
-
return ""
|
|
669
|
-
|
|
670
|
-
if rendered[0] == last_rendered_lines[-1]:
|
|
671
|
-
rendered = rendered[1:]
|
|
672
|
-
return rstrip(rendered)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
def is_status_check(arg: BashInteraction | BashCommand) -> bool:
|
|
676
|
-
return isinstance(arg, BashInteraction) and (
|
|
677
|
-
arg.send_specials == ["Enter"] or arg.send_ascii == [10]
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def execute_bash(
|
|
682
|
-
enc: tokenizers.Tokenizer,
|
|
683
|
-
bash_arg: BashCommand | BashInteraction,
|
|
684
|
-
max_tokens: Optional[int],
|
|
685
|
-
timeout_s: Optional[float],
|
|
686
|
-
) -> tuple[str, float]:
|
|
687
|
-
try:
|
|
688
|
-
is_interrupt = False
|
|
689
|
-
if isinstance(bash_arg, BashCommand):
|
|
690
|
-
if BASH_STATE.bash_command_mode.allowed_commands == "none":
|
|
691
|
-
return "Error: BashCommand not allowed in current mode", 0.0
|
|
692
|
-
updated_repl_mode = BASH_STATE.update_repl_prompt(bash_arg.command)
|
|
693
|
-
if updated_repl_mode:
|
|
694
|
-
BASH_STATE.set_repl()
|
|
695
|
-
response = (
|
|
696
|
-
"Prompt updated, you can execute REPL lines using BashCommand now"
|
|
697
|
-
)
|
|
698
|
-
console.print(response)
|
|
699
|
-
return (
|
|
700
|
-
response,
|
|
701
|
-
0,
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
console.print(f"$ {bash_arg.command}")
|
|
705
|
-
if BASH_STATE.state == "pending":
|
|
706
|
-
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
707
|
-
command = bash_arg.command.strip()
|
|
708
|
-
|
|
709
|
-
if "\n" in command:
|
|
710
|
-
raise ValueError(
|
|
711
|
-
"Command should not contain newline character in middle. Run only one command at a time."
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
for i in range(0, len(command), 128):
|
|
715
|
-
BASH_STATE.shell.send(command[i : i + 128])
|
|
716
|
-
BASH_STATE.shell.send(BASH_STATE.shell.linesep)
|
|
717
|
-
|
|
718
|
-
else:
|
|
719
|
-
if (
|
|
720
|
-
sum(
|
|
721
|
-
[
|
|
722
|
-
int(bool(bash_arg.send_text)),
|
|
723
|
-
int(bool(bash_arg.send_specials)),
|
|
724
|
-
int(bool(bash_arg.send_ascii)),
|
|
725
|
-
]
|
|
726
|
-
)
|
|
727
|
-
!= 1
|
|
728
|
-
):
|
|
729
|
-
return (
|
|
730
|
-
"Failure: exactly one of send_text, send_specials or send_ascii should be provided",
|
|
731
|
-
0.0,
|
|
732
|
-
)
|
|
733
|
-
if bash_arg.send_specials:
|
|
734
|
-
console.print(f"Sending special sequence: {bash_arg.send_specials}")
|
|
735
|
-
for char in bash_arg.send_specials:
|
|
736
|
-
if char == "Key-up":
|
|
737
|
-
BASH_STATE.shell.send("\033[A")
|
|
738
|
-
elif char == "Key-down":
|
|
739
|
-
BASH_STATE.shell.send("\033[B")
|
|
740
|
-
elif char == "Key-left":
|
|
741
|
-
BASH_STATE.shell.send("\033[D")
|
|
742
|
-
elif char == "Key-right":
|
|
743
|
-
BASH_STATE.shell.send("\033[C")
|
|
744
|
-
elif char == "Enter":
|
|
745
|
-
BASH_STATE.shell.send("\n")
|
|
746
|
-
elif char == "Ctrl-c":
|
|
747
|
-
BASH_STATE.shell.sendintr()
|
|
748
|
-
is_interrupt = True
|
|
749
|
-
elif char == "Ctrl-d":
|
|
750
|
-
BASH_STATE.shell.sendintr()
|
|
751
|
-
is_interrupt = True
|
|
752
|
-
elif char == "Ctrl-z":
|
|
753
|
-
BASH_STATE.shell.send("\x1a")
|
|
754
|
-
else:
|
|
755
|
-
raise Exception(f"Unknown special character: {char}")
|
|
756
|
-
elif bash_arg.send_ascii:
|
|
757
|
-
console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
|
|
758
|
-
for ascii_char in bash_arg.send_ascii:
|
|
759
|
-
BASH_STATE.shell.send(chr(ascii_char))
|
|
760
|
-
if ascii_char == 3:
|
|
761
|
-
is_interrupt = True
|
|
762
|
-
else:
|
|
763
|
-
if bash_arg.send_text is None:
|
|
764
|
-
return (
|
|
765
|
-
"Failure: at least one of send_text, send_specials or send_ascii should be provided",
|
|
766
|
-
0.0,
|
|
767
|
-
)
|
|
768
|
-
|
|
769
|
-
updated_repl_mode = BASH_STATE.update_repl_prompt(bash_arg.send_text)
|
|
770
|
-
if updated_repl_mode:
|
|
771
|
-
BASH_STATE.set_repl()
|
|
772
|
-
response = "Prompt updated, you can execute REPL lines using BashCommand now"
|
|
773
|
-
console.print(response)
|
|
774
|
-
return (
|
|
775
|
-
response,
|
|
776
|
-
0,
|
|
777
|
-
)
|
|
778
|
-
console.print(f"Interact text: {bash_arg.send_text}")
|
|
779
|
-
for i in range(0, len(bash_arg.send_text), 128):
|
|
780
|
-
BASH_STATE.shell.send(bash_arg.send_text[i : i + 128])
|
|
781
|
-
BASH_STATE.shell.send(BASH_STATE.shell.linesep)
|
|
782
|
-
|
|
783
|
-
except KeyboardInterrupt:
|
|
784
|
-
BASH_STATE.shell.sendintr()
|
|
785
|
-
BASH_STATE.shell.expect(BASH_STATE.prompt)
|
|
786
|
-
return "---\n\nFailure: user interrupted the execution", 0.0
|
|
787
|
-
|
|
788
|
-
wait = min(timeout_s or TIMEOUT, TIMEOUT_WHILE_OUTPUT)
|
|
789
|
-
index = BASH_STATE.shell.expect([BASH_STATE.prompt, pexpect.TIMEOUT], timeout=wait)
|
|
790
|
-
if index == 1:
|
|
791
|
-
text = BASH_STATE.shell.before or ""
|
|
792
|
-
incremental_text = _incremental_text(text, BASH_STATE.pending_output)
|
|
793
|
-
|
|
794
|
-
second_wait_success = False
|
|
795
|
-
if is_status_check(bash_arg):
|
|
796
|
-
# There's some text in BashInteraction mode wait for TIMEOUT_WHILE_OUTPUT
|
|
797
|
-
remaining = TIMEOUT_WHILE_OUTPUT - wait
|
|
798
|
-
patience = OUTPUT_WAIT_PATIENCE
|
|
799
|
-
if not incremental_text:
|
|
800
|
-
patience -= 1
|
|
801
|
-
itext = incremental_text
|
|
802
|
-
while remaining > 0 and patience > 0:
|
|
803
|
-
index = BASH_STATE.shell.expect(
|
|
804
|
-
[BASH_STATE.prompt, pexpect.TIMEOUT], timeout=wait
|
|
805
|
-
)
|
|
806
|
-
if index == 0:
|
|
807
|
-
second_wait_success = True
|
|
808
|
-
break
|
|
809
|
-
else:
|
|
810
|
-
_itext = BASH_STATE.shell.before or ""
|
|
811
|
-
_itext = _incremental_text(_itext, BASH_STATE.pending_output)
|
|
812
|
-
if _itext != itext:
|
|
813
|
-
patience = 3
|
|
814
|
-
else:
|
|
815
|
-
patience -= 1
|
|
816
|
-
itext = _itext
|
|
817
|
-
|
|
818
|
-
remaining = remaining - wait
|
|
819
|
-
|
|
820
|
-
if not second_wait_success:
|
|
821
|
-
text = BASH_STATE.shell.before or ""
|
|
822
|
-
incremental_text = _incremental_text(text, BASH_STATE.pending_output)
|
|
823
|
-
|
|
824
|
-
if not second_wait_success:
|
|
825
|
-
BASH_STATE.set_pending(text)
|
|
826
|
-
|
|
827
|
-
tokens = enc.encode(incremental_text)
|
|
828
|
-
|
|
829
|
-
if max_tokens and len(tokens) >= max_tokens:
|
|
830
|
-
incremental_text = "(...truncated)\n" + enc.decode(
|
|
831
|
-
tokens.ids[-(max_tokens - 1) :]
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
if is_interrupt:
|
|
835
|
-
incremental_text = (
|
|
836
|
-
incremental_text
|
|
837
|
-
+ """---
|
|
838
|
-
----
|
|
839
|
-
Failure interrupting.
|
|
840
|
-
If any REPL session was previously running or if bashrc was sourced, or if there is issue to other REPL related reasons:
|
|
841
|
-
Run BashCommand: "wcgw_update_prompt()" to reset the PS1 prompt.
|
|
842
|
-
Otherwise, you may want to try Ctrl-c again or program specific exit interactive commands.
|
|
843
|
-
"""
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
exit_status = get_status()
|
|
847
|
-
incremental_text += exit_status
|
|
848
|
-
|
|
849
|
-
return incremental_text, 0
|
|
850
|
-
|
|
851
|
-
if not isinstance(BASH_STATE.shell.before, str):
|
|
852
|
-
BASH_STATE.shell.before = str(BASH_STATE.shell.before)
|
|
853
|
-
|
|
854
|
-
output = _incremental_text(BASH_STATE.shell.before, BASH_STATE.pending_output)
|
|
855
|
-
BASH_STATE.set_repl()
|
|
856
|
-
|
|
857
|
-
tokens = enc.encode(output)
|
|
858
|
-
if max_tokens and len(tokens) >= max_tokens:
|
|
859
|
-
output = "(...truncated)\n" + enc.decode(tokens.ids[-(max_tokens - 1) :])
|
|
860
|
-
|
|
861
|
-
try:
|
|
862
|
-
exit_status = get_status()
|
|
863
|
-
output += exit_status
|
|
864
|
-
except ValueError:
|
|
865
|
-
console.print(output)
|
|
866
|
-
console.print(traceback.format_exc())
|
|
867
|
-
console.print("Malformed output, restarting shell", style="red")
|
|
868
|
-
# Malformed output, restart shell
|
|
869
|
-
BASH_STATE.reset_shell()
|
|
870
|
-
output = "(exit shell has restarted)"
|
|
871
|
-
return output, 0
|
|
872
|
-
|
|
873
|
-
|
|
874
279
|
MEDIA_TYPES = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
875
280
|
|
|
876
281
|
|
|
@@ -886,58 +291,34 @@ class ImageData(BaseModel):
|
|
|
886
291
|
Param = ParamSpec("Param")
|
|
887
292
|
|
|
888
293
|
|
|
889
|
-
def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
890
|
-
def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
|
|
891
|
-
if BASH_STATE.state == "pending":
|
|
892
|
-
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
893
|
-
|
|
894
|
-
return func(*args, **kwargs)
|
|
895
|
-
|
|
896
|
-
return wrapper
|
|
897
|
-
|
|
898
|
-
|
|
899
294
|
def truncate_if_over(content: str, max_tokens: Optional[int]) -> str:
|
|
900
295
|
if max_tokens and max_tokens > 0:
|
|
901
|
-
tokens = default_enc.
|
|
296
|
+
tokens = default_enc.encoder(content)
|
|
902
297
|
n_tokens = len(tokens)
|
|
903
298
|
if n_tokens > max_tokens:
|
|
904
299
|
content = (
|
|
905
|
-
default_enc.
|
|
300
|
+
default_enc.decoder(tokens[: max(0, max_tokens - 100)])
|
|
906
301
|
+ "\n(...truncated)"
|
|
907
302
|
)
|
|
908
303
|
|
|
909
304
|
return content
|
|
910
305
|
|
|
911
306
|
|
|
912
|
-
def read_image_from_shell(file_path: str) -> ImageData:
|
|
307
|
+
def read_image_from_shell(file_path: str, context: Context) -> ImageData:
|
|
913
308
|
# Expand the path
|
|
914
|
-
file_path = expand_user(file_path
|
|
309
|
+
file_path = expand_user(file_path)
|
|
915
310
|
|
|
916
311
|
if not os.path.isabs(file_path):
|
|
917
|
-
file_path = os.path.join(
|
|
312
|
+
file_path = os.path.join(context.bash_state.cwd, file_path)
|
|
918
313
|
|
|
919
|
-
if not
|
|
920
|
-
|
|
921
|
-
raise ValueError(f"File {file_path} does not exist")
|
|
314
|
+
if not os.path.exists(file_path):
|
|
315
|
+
raise ValueError(f"File {file_path} does not exist")
|
|
922
316
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
else:
|
|
929
|
-
with TemporaryDirectory() as tmpdir:
|
|
930
|
-
rcode = os.system(
|
|
931
|
-
f"docker cp {BASH_STATE.is_in_docker}:{shlex.quote(file_path)} {tmpdir}"
|
|
932
|
-
)
|
|
933
|
-
if rcode != 0:
|
|
934
|
-
raise Exception(f"Error: Read failed with code {rcode}")
|
|
935
|
-
path_ = os.path.join(tmpdir, os.path.basename(file_path))
|
|
936
|
-
with open(path_, "rb") as f:
|
|
937
|
-
image_bytes = f.read()
|
|
938
|
-
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
939
|
-
image_type = mimetypes.guess_type(file_path)[0]
|
|
940
|
-
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
317
|
+
with open(file_path, "rb") as image_file:
|
|
318
|
+
image_bytes = image_file.read()
|
|
319
|
+
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
320
|
+
image_type = mimetypes.guess_type(file_path)[0]
|
|
321
|
+
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
941
322
|
|
|
942
323
|
|
|
943
324
|
def get_context_for_errors(
|
|
@@ -950,95 +331,63 @@ def get_context_for_errors(
|
|
|
950
331
|
context = "\n".join(context_lines)
|
|
951
332
|
|
|
952
333
|
if max_tokens is not None and max_tokens > 0:
|
|
953
|
-
ntokens = len(default_enc.
|
|
334
|
+
ntokens = len(default_enc.encoder(context))
|
|
954
335
|
if ntokens > max_tokens:
|
|
955
336
|
return "Please re-read the file to understand the context"
|
|
956
337
|
return f"Here's relevant snippet from the file where the syntax errors occured:\n```\n{context}\n```"
|
|
957
338
|
|
|
958
339
|
|
|
959
340
|
def write_file(
|
|
960
|
-
writefile: WriteIfEmpty,
|
|
341
|
+
writefile: WriteIfEmpty,
|
|
342
|
+
error_on_exist: bool,
|
|
343
|
+
max_tokens: Optional[int],
|
|
344
|
+
context: Context,
|
|
961
345
|
) -> str:
|
|
962
346
|
if not os.path.isabs(writefile.file_path):
|
|
963
|
-
return f"Failure: file_path should be absolute path, current working directory is {
|
|
347
|
+
return f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}"
|
|
964
348
|
else:
|
|
965
|
-
path_ = expand_user(writefile.file_path
|
|
349
|
+
path_ = expand_user(writefile.file_path)
|
|
966
350
|
|
|
967
|
-
error_on_exist_ =
|
|
351
|
+
error_on_exist_ = (
|
|
352
|
+
error_on_exist and path_ not in context.bash_state.whitelist_for_overwrite
|
|
353
|
+
)
|
|
968
354
|
|
|
969
355
|
# Validate using write_if_empty_mode after checking whitelist
|
|
970
|
-
allowed_globs =
|
|
356
|
+
allowed_globs = context.bash_state.write_if_empty_mode.allowed_globs
|
|
971
357
|
if allowed_globs != "all" and not any(
|
|
972
358
|
fnmatch.fnmatch(path_, pattern) for pattern in allowed_globs
|
|
973
359
|
):
|
|
974
360
|
return f"Error: updating file {path_} not allowed in current mode. Doesn't match allowed globs: {allowed_globs}"
|
|
361
|
+
|
|
975
362
|
add_overwrite_warning = ""
|
|
976
|
-
if
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
content = truncate_if_over(content, max_tokens)
|
|
981
|
-
|
|
982
|
-
if error_on_exist_:
|
|
983
|
-
return (
|
|
984
|
-
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
985
|
-
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
986
|
-
)
|
|
987
|
-
else:
|
|
988
|
-
add_overwrite_warning = content
|
|
989
|
-
|
|
990
|
-
# Since we've already errored once, add this to whitelist
|
|
991
|
-
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
992
|
-
|
|
993
|
-
path = Path(path_)
|
|
994
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
if (error_on_exist or error_on_exist_) and os.path.exists(path_):
|
|
364
|
+
content = Path(path_).read_text().strip()
|
|
365
|
+
if content:
|
|
366
|
+
content = truncate_if_over(content, max_tokens)
|
|
995
367
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
return_code, content, stderr = command_run(
|
|
1004
|
-
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(path_)}",
|
|
1005
|
-
timeout=TIMEOUT,
|
|
1006
|
-
)
|
|
1007
|
-
if return_code != 0 and content.strip():
|
|
1008
|
-
content = truncate_if_over(content, max_tokens)
|
|
1009
|
-
|
|
1010
|
-
if error_on_exist_:
|
|
1011
|
-
return (
|
|
1012
|
-
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
1013
|
-
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
1014
|
-
)
|
|
1015
|
-
else:
|
|
1016
|
-
add_overwrite_warning = content
|
|
1017
|
-
|
|
1018
|
-
# Since we've already errored once, add this to whitelist
|
|
1019
|
-
BASH_STATE.add_to_whitelist_for_overwrite(path_)
|
|
1020
|
-
|
|
1021
|
-
with TemporaryDirectory() as tmpdir:
|
|
1022
|
-
tmppath = os.path.join(tmpdir, os.path.basename(path_))
|
|
1023
|
-
with open(tmppath, "w") as f:
|
|
1024
|
-
f.write(writefile.file_content)
|
|
1025
|
-
os.chmod(tmppath, 0o777)
|
|
1026
|
-
parent_dir = os.path.dirname(path_)
|
|
1027
|
-
rcode = os.system(
|
|
1028
|
-
f"docker exec {BASH_STATE.is_in_docker} mkdir -p {parent_dir}"
|
|
1029
|
-
)
|
|
1030
|
-
if rcode != 0:
|
|
1031
|
-
return f"Error: Write failed with code while creating dirs {rcode}"
|
|
368
|
+
if error_on_exist_:
|
|
369
|
+
return (
|
|
370
|
+
f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
371
|
+
+ f"\nHere's the existing content:\n```\n{content}\n```"
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
add_overwrite_warning = content
|
|
1032
375
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
376
|
+
# Since we've already errored once, add this to whitelist
|
|
377
|
+
context.bash_state.add_to_whitelist_for_overwrite(path_)
|
|
378
|
+
|
|
379
|
+
path = Path(path_)
|
|
380
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
with path.open("w") as f:
|
|
384
|
+
f.write(writefile.file_content)
|
|
385
|
+
except OSError as e:
|
|
386
|
+
return f"Error: {e}"
|
|
1038
387
|
|
|
1039
388
|
extension = Path(path_).suffix.lstrip(".")
|
|
1040
389
|
|
|
1041
|
-
console.print(f"File written to {path_}")
|
|
390
|
+
context.console.print(f"File written to {path_}")
|
|
1042
391
|
|
|
1043
392
|
warnings = []
|
|
1044
393
|
try:
|
|
@@ -1049,7 +398,7 @@ def write_file(
|
|
|
1049
398
|
context_for_errors = get_context_for_errors(
|
|
1050
399
|
check.errors, writefile.file_content, max_tokens
|
|
1051
400
|
)
|
|
1052
|
-
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
401
|
+
context.console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
1053
402
|
warnings.append(f"""
|
|
1054
403
|
---
|
|
1055
404
|
Warning: tree-sitter reported syntax errors
|
|
@@ -1075,9 +424,9 @@ Syntax errors:
|
|
|
1075
424
|
return "Success" + "".join(warnings)
|
|
1076
425
|
|
|
1077
426
|
|
|
1078
|
-
def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
427
|
+
def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int], context: Context) -> str:
|
|
1079
428
|
try:
|
|
1080
|
-
return _do_diff_edit(fedit, max_tokens)
|
|
429
|
+
return _do_diff_edit(fedit, max_tokens, context)
|
|
1081
430
|
except Exception as e:
|
|
1082
431
|
# Try replacing \"
|
|
1083
432
|
try:
|
|
@@ -1087,24 +436,24 @@ def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
|
1087
436
|
'\\"', '"'
|
|
1088
437
|
),
|
|
1089
438
|
)
|
|
1090
|
-
return _do_diff_edit(fedit, max_tokens)
|
|
439
|
+
return _do_diff_edit(fedit, max_tokens, context)
|
|
1091
440
|
except Exception:
|
|
1092
441
|
pass
|
|
1093
442
|
raise e
|
|
1094
443
|
|
|
1095
444
|
|
|
1096
|
-
def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
1097
|
-
console.log(f"Editing file: {fedit.file_path}")
|
|
445
|
+
def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int], context: Context) -> str:
|
|
446
|
+
context.console.log(f"Editing file: {fedit.file_path}")
|
|
1098
447
|
|
|
1099
448
|
if not os.path.isabs(fedit.file_path):
|
|
1100
449
|
raise Exception(
|
|
1101
|
-
f"Failure: file_path should be absolute path, current working directory is {
|
|
450
|
+
f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}"
|
|
1102
451
|
)
|
|
1103
452
|
else:
|
|
1104
|
-
path_ = expand_user(fedit.file_path
|
|
453
|
+
path_ = expand_user(fedit.file_path)
|
|
1105
454
|
|
|
1106
455
|
# Validate using file_edit_mode
|
|
1107
|
-
allowed_globs =
|
|
456
|
+
allowed_globs = context.bash_state.file_edit_mode.allowed_globs
|
|
1108
457
|
if allowed_globs != "all" and not any(
|
|
1109
458
|
fnmatch.fnmatch(path_, pattern) for pattern in allowed_globs
|
|
1110
459
|
):
|
|
@@ -1113,48 +462,25 @@ def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
|
1113
462
|
)
|
|
1114
463
|
|
|
1115
464
|
# The LLM is now aware that the file exists
|
|
1116
|
-
|
|
465
|
+
context.bash_state.add_to_whitelist_for_overwrite(path_)
|
|
1117
466
|
|
|
1118
|
-
if not
|
|
1119
|
-
|
|
1120
|
-
raise Exception(f"Error: file {path_} does not exist")
|
|
467
|
+
if not os.path.exists(path_):
|
|
468
|
+
raise Exception(f"Error: file {path_} does not exist")
|
|
1121
469
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
else:
|
|
1125
|
-
# Copy from docker
|
|
1126
|
-
with TemporaryDirectory() as tmpdir:
|
|
1127
|
-
rcode = os.system(
|
|
1128
|
-
f"docker cp {BASH_STATE.is_in_docker}:{shlex.quote(path_)} {tmpdir}"
|
|
1129
|
-
)
|
|
1130
|
-
if rcode != 0:
|
|
1131
|
-
raise Exception(f"Error: Read failed with code {rcode}")
|
|
1132
|
-
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
1133
|
-
with open(path_tmp, "r") as f:
|
|
1134
|
-
apply_diff_to = f.read()
|
|
470
|
+
with open(path_) as f:
|
|
471
|
+
apply_diff_to = f.read()
|
|
1135
472
|
|
|
1136
473
|
fedit.file_edit_using_search_replace_blocks = (
|
|
1137
474
|
fedit.file_edit_using_search_replace_blocks.strip()
|
|
1138
475
|
)
|
|
1139
476
|
lines = fedit.file_edit_using_search_replace_blocks.split("\n")
|
|
1140
477
|
|
|
1141
|
-
apply_diff_to, comments = search_replace_edit(
|
|
478
|
+
apply_diff_to, comments = search_replace_edit(
|
|
479
|
+
lines, apply_diff_to, context.console.log
|
|
480
|
+
)
|
|
1142
481
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
f.write(apply_diff_to)
|
|
1146
|
-
else:
|
|
1147
|
-
with TemporaryDirectory() as tmpdir:
|
|
1148
|
-
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
1149
|
-
with open(path_tmp, "w") as f:
|
|
1150
|
-
f.write(apply_diff_to)
|
|
1151
|
-
os.chmod(path_tmp, 0o777)
|
|
1152
|
-
# Copy to docker using docker cp
|
|
1153
|
-
rcode = os.system(
|
|
1154
|
-
f"docker cp {shlex.quote(path_tmp)} {BASH_STATE.is_in_docker}:{shlex.quote(path_)}"
|
|
1155
|
-
)
|
|
1156
|
-
if rcode != 0:
|
|
1157
|
-
raise Exception(f"Error: Write failed with code {rcode}")
|
|
482
|
+
with open(path_, "w") as f:
|
|
483
|
+
f.write(apply_diff_to)
|
|
1158
484
|
|
|
1159
485
|
syntax_errors = ""
|
|
1160
486
|
extension = Path(path_).suffix.lstrip(".")
|
|
@@ -1166,7 +492,7 @@ def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
|
|
|
1166
492
|
check.errors, apply_diff_to, max_tokens
|
|
1167
493
|
)
|
|
1168
494
|
|
|
1169
|
-
console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
495
|
+
context.console.print(f"W: Syntax errors encountered: {syntax_errors}")
|
|
1170
496
|
return f"""{comments}
|
|
1171
497
|
---
|
|
1172
498
|
Tree-sitter reported syntax errors, please re-read the file and fix if there are any errors.
|
|
@@ -1181,45 +507,14 @@ Syntax errors:
|
|
|
1181
507
|
return comments
|
|
1182
508
|
|
|
1183
509
|
|
|
1184
|
-
class DoneFlag(BaseModel):
|
|
1185
|
-
task_output: str
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
def mark_finish(done: DoneFlag) -> DoneFlag:
|
|
1189
|
-
return done
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
class AIAssistant(BaseModel):
|
|
1193
|
-
instruction: str
|
|
1194
|
-
desired_output: str
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
def take_help_of_ai_assistant(
|
|
1198
|
-
aiassistant: AIAssistant,
|
|
1199
|
-
limit: float,
|
|
1200
|
-
loop_call: Callable[[str, float], tuple[str, float]],
|
|
1201
|
-
) -> tuple[str, float]:
|
|
1202
|
-
output, cost = loop_call(aiassistant.instruction, limit)
|
|
1203
|
-
return output, cost
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
510
|
TOOLS = (
|
|
1207
|
-
|
|
1208
|
-
|
|
|
1209
|
-
| BashInteraction
|
|
1210
|
-
| ResetShell
|
|
511
|
+
BashCommand
|
|
512
|
+
| ResetWcgw
|
|
1211
513
|
| WriteIfEmpty
|
|
1212
|
-
| FileEditFindReplace
|
|
1213
514
|
| FileEdit
|
|
1214
|
-
| AIAssistant
|
|
1215
|
-
| DoneFlag
|
|
1216
515
|
| ReadImage
|
|
1217
516
|
| ReadFiles
|
|
1218
517
|
| Initialize
|
|
1219
|
-
| Mouse
|
|
1220
|
-
| Keyboard
|
|
1221
|
-
| ScreenShot
|
|
1222
|
-
| GetScreenInfo
|
|
1223
518
|
| ContextSave
|
|
1224
519
|
)
|
|
1225
520
|
|
|
@@ -1230,38 +525,20 @@ def which_tool(args: str) -> TOOLS:
|
|
|
1230
525
|
|
|
1231
526
|
|
|
1232
527
|
def which_tool_name(name: str) -> Type[TOOLS]:
|
|
1233
|
-
if name == "
|
|
1234
|
-
return Confirmation
|
|
1235
|
-
elif name == "BashCommand":
|
|
528
|
+
if name == "BashCommand":
|
|
1236
529
|
return BashCommand
|
|
1237
|
-
elif name == "
|
|
1238
|
-
return
|
|
1239
|
-
elif name == "ResetShell":
|
|
1240
|
-
return ResetShell
|
|
530
|
+
elif name == "ResetWcgw":
|
|
531
|
+
return ResetWcgw
|
|
1241
532
|
elif name == "WriteIfEmpty":
|
|
1242
533
|
return WriteIfEmpty
|
|
1243
|
-
elif name == "FileEditFindReplace":
|
|
1244
|
-
return FileEditFindReplace
|
|
1245
534
|
elif name == "FileEdit":
|
|
1246
535
|
return FileEdit
|
|
1247
|
-
elif name == "AIAssistant":
|
|
1248
|
-
return AIAssistant
|
|
1249
|
-
elif name == "DoneFlag":
|
|
1250
|
-
return DoneFlag
|
|
1251
536
|
elif name == "ReadImage":
|
|
1252
537
|
return ReadImage
|
|
1253
538
|
elif name == "ReadFiles":
|
|
1254
539
|
return ReadFiles
|
|
1255
540
|
elif name == "Initialize":
|
|
1256
541
|
return Initialize
|
|
1257
|
-
elif name == "Mouse":
|
|
1258
|
-
return Mouse
|
|
1259
|
-
elif name == "Keyboard":
|
|
1260
|
-
return Keyboard
|
|
1261
|
-
elif name == "ScreenShot":
|
|
1262
|
-
return ScreenShot
|
|
1263
|
-
elif name == "GetScreenInfo":
|
|
1264
|
-
return GetScreenInfo
|
|
1265
542
|
elif name == "ContextSave":
|
|
1266
543
|
return ContextSave
|
|
1267
544
|
else:
|
|
@@ -1272,119 +549,80 @@ TOOL_CALLS: list[TOOLS] = []
|
|
|
1272
549
|
|
|
1273
550
|
|
|
1274
551
|
def get_tool_output(
|
|
552
|
+
context: Context,
|
|
1275
553
|
args: dict[object, object] | TOOLS,
|
|
1276
|
-
enc:
|
|
554
|
+
enc: EncoderDecoder[int],
|
|
1277
555
|
limit: float,
|
|
1278
556
|
loop_call: Callable[[str, float], tuple[str, float]],
|
|
1279
557
|
max_tokens: Optional[int],
|
|
1280
|
-
) -> tuple[list[str | ImageData
|
|
1281
|
-
global
|
|
558
|
+
) -> tuple[list[str | ImageData], float]:
|
|
559
|
+
global TOOL_CALLS, INITIALIZED
|
|
1282
560
|
if isinstance(args, dict):
|
|
1283
561
|
adapter = TypeAdapter[TOOLS](TOOLS, config={"extra": "forbid"})
|
|
1284
562
|
arg = adapter.validate_python(args)
|
|
1285
563
|
else:
|
|
1286
564
|
arg = args
|
|
1287
|
-
output: tuple[str |
|
|
565
|
+
output: tuple[str | ImageData, float]
|
|
1288
566
|
TOOL_CALLS.append(arg)
|
|
1289
567
|
|
|
1290
|
-
if isinstance(arg,
|
|
1291
|
-
console.print("Calling
|
|
1292
|
-
output = ask_confirmation(arg), 0.0
|
|
1293
|
-
elif isinstance(arg, (BashCommand | BashInteraction)):
|
|
1294
|
-
console.print("Calling execute bash tool")
|
|
568
|
+
if isinstance(arg, BashCommand):
|
|
569
|
+
context.console.print("Calling execute bash tool")
|
|
1295
570
|
if not INITIALIZED:
|
|
1296
571
|
raise Exception("Initialize tool not called yet.")
|
|
1297
572
|
|
|
1298
|
-
output = execute_bash(
|
|
573
|
+
output = execute_bash(
|
|
574
|
+
context.bash_state, enc, arg, max_tokens, arg.wait_for_seconds
|
|
575
|
+
)
|
|
1299
576
|
elif isinstance(arg, WriteIfEmpty):
|
|
1300
|
-
console.print("Calling write file tool")
|
|
577
|
+
context.console.print("Calling write file tool")
|
|
1301
578
|
if not INITIALIZED:
|
|
1302
579
|
raise Exception("Initialize tool not called yet.")
|
|
1303
580
|
|
|
1304
|
-
output = write_file(arg, True, max_tokens), 0
|
|
581
|
+
output = write_file(arg, True, max_tokens, context), 0
|
|
1305
582
|
elif isinstance(arg, FileEdit):
|
|
1306
|
-
console.print("Calling full file edit tool")
|
|
583
|
+
context.console.print("Calling full file edit tool")
|
|
1307
584
|
if not INITIALIZED:
|
|
1308
585
|
raise Exception("Initialize tool not called yet.")
|
|
1309
586
|
|
|
1310
|
-
output = do_diff_edit(arg, max_tokens), 0.0
|
|
1311
|
-
elif isinstance(arg, DoneFlag):
|
|
1312
|
-
console.print("Calling mark finish tool")
|
|
1313
|
-
output = mark_finish(arg), 0.0
|
|
1314
|
-
elif isinstance(arg, AIAssistant):
|
|
1315
|
-
console.print("Calling AI assistant tool")
|
|
1316
|
-
output = take_help_of_ai_assistant(arg, limit, loop_call)
|
|
587
|
+
output = do_diff_edit(arg, max_tokens, context), 0.0
|
|
1317
588
|
elif isinstance(arg, ReadImage):
|
|
1318
|
-
console.print("Calling read image tool")
|
|
1319
|
-
output = read_image_from_shell(arg.file_path), 0.0
|
|
589
|
+
context.console.print("Calling read image tool")
|
|
590
|
+
output = read_image_from_shell(arg.file_path, context), 0.0
|
|
1320
591
|
elif isinstance(arg, ReadFiles):
|
|
1321
|
-
console.print("Calling read file tool")
|
|
1322
|
-
output = read_files(arg.file_paths, max_tokens), 0.0
|
|
1323
|
-
elif isinstance(arg,
|
|
1324
|
-
console.print("Calling reset
|
|
1325
|
-
output =
|
|
592
|
+
context.console.print("Calling read file tool")
|
|
593
|
+
output = read_files(arg.file_paths, max_tokens, context), 0.0
|
|
594
|
+
elif isinstance(arg, ResetWcgw):
|
|
595
|
+
context.console.print("Calling reset wcgw tool")
|
|
596
|
+
output = reset_wcgw(context, arg), 0.0
|
|
597
|
+
|
|
598
|
+
INITIALIZED = True
|
|
1326
599
|
elif isinstance(arg, Initialize):
|
|
1327
|
-
console.print("Calling initial info tool")
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
),
|
|
1336
|
-
0.0,
|
|
600
|
+
context.console.print("Calling initial info tool")
|
|
601
|
+
output_, context = initialize(
|
|
602
|
+
context,
|
|
603
|
+
arg.any_workspace_path,
|
|
604
|
+
arg.initial_files_to_read,
|
|
605
|
+
arg.task_id_to_resume,
|
|
606
|
+
max_tokens,
|
|
607
|
+
arg.mode,
|
|
1337
608
|
)
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
console.print(outputs_cost[0][0])
|
|
1342
|
-
outputs: list[ImageData | str | DoneFlag] = [outputs_cost[0][0]]
|
|
1343
|
-
imgBs64 = outputs_cost[0][1]
|
|
1344
|
-
if imgBs64:
|
|
1345
|
-
console.print("Captured screenshot")
|
|
1346
|
-
outputs.append(ImageData(media_type="image/png", data=imgBs64))
|
|
1347
|
-
if not BASH_STATE.is_in_docker and isinstance(arg, GetScreenInfo):
|
|
1348
|
-
try:
|
|
1349
|
-
# At this point we should go into the docker env
|
|
1350
|
-
res, _ = execute_bash(
|
|
1351
|
-
enc,
|
|
1352
|
-
BashCommand(
|
|
1353
|
-
command=f"docker exec -it {arg.docker_image_id} sh"
|
|
1354
|
-
),
|
|
1355
|
-
None,
|
|
1356
|
-
0.2,
|
|
1357
|
-
)
|
|
1358
|
-
# At this point we should go into the docker env
|
|
1359
|
-
res, _ = execute_bash(
|
|
1360
|
-
enc,
|
|
1361
|
-
BashInteraction(send_text=f"export PS1={BASH_STATE.prompt}"),
|
|
1362
|
-
None,
|
|
1363
|
-
0.2,
|
|
1364
|
-
)
|
|
1365
|
-
# Do chown of home dir
|
|
1366
|
-
except Exception as e:
|
|
1367
|
-
reset_shell()
|
|
1368
|
-
raise Exception(
|
|
1369
|
-
f"Some error happened while going inside docker. I've reset the shell. Please start again. Error {e}"
|
|
1370
|
-
)
|
|
1371
|
-
BASH_STATE.set_in_docker(arg.docker_image_id)
|
|
1372
|
-
return outputs, outputs_cost[1]
|
|
609
|
+
output = output_, 0.0
|
|
610
|
+
|
|
611
|
+
INITIALIZED = True
|
|
1373
612
|
elif isinstance(arg, ContextSave):
|
|
1374
|
-
console.print("Calling task memory tool")
|
|
1375
|
-
assert not BASH_STATE.is_in_docker, "KT not supported in docker"
|
|
613
|
+
context.console.print("Calling task memory tool")
|
|
1376
614
|
relevant_files = []
|
|
1377
615
|
warnings = ""
|
|
1378
616
|
for fglob in arg.relevant_file_globs:
|
|
1379
|
-
fglob = expand_user(fglob
|
|
617
|
+
fglob = expand_user(fglob)
|
|
1380
618
|
if not os.path.isabs(fglob) and arg.project_root_path:
|
|
1381
619
|
fglob = os.path.join(arg.project_root_path, fglob)
|
|
1382
620
|
globs = glob.glob(fglob, recursive=True)
|
|
1383
621
|
relevant_files.extend(globs[:1000])
|
|
1384
622
|
if not globs:
|
|
1385
623
|
warnings += f"Warning: No files found for the glob: {fglob}\n"
|
|
1386
|
-
relevant_files_data = read_files(relevant_files[:10_000], None)
|
|
1387
|
-
output_ = save_memory(arg, relevant_files_data,
|
|
624
|
+
relevant_files_data = read_files(relevant_files[:10_000], None, context)
|
|
625
|
+
output_ = save_memory(arg, relevant_files_data, context.bash_state.serialize())
|
|
1388
626
|
if not relevant_files and arg.relevant_file_globs:
|
|
1389
627
|
output_ = f'Error: No files found for the given globs. Context file successfully saved at "{output_}", but please fix the error.'
|
|
1390
628
|
elif warnings:
|
|
@@ -1393,108 +631,25 @@ def get_tool_output(
|
|
|
1393
631
|
else:
|
|
1394
632
|
raise ValueError(f"Unknown tool: {arg}")
|
|
1395
633
|
if isinstance(output[0], str):
|
|
1396
|
-
console.print(str(output[0]))
|
|
634
|
+
context.console.print(str(output[0]))
|
|
1397
635
|
else:
|
|
1398
|
-
console.print(f"Received {type(output[0])} from tool")
|
|
636
|
+
context.console.print(f"Received {type(output[0])} from tool")
|
|
1399
637
|
return [output[0]], output[1]
|
|
1400
638
|
|
|
1401
639
|
|
|
1402
640
|
History = list[ChatCompletionMessageParam]
|
|
1403
641
|
|
|
1404
|
-
default_enc
|
|
1405
|
-
"Xenova/claude-tokenizer"
|
|
1406
|
-
)
|
|
642
|
+
default_enc = get_default_encoder()
|
|
1407
643
|
curr_cost = 0.0
|
|
1408
644
|
|
|
1409
645
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
| BashInteraction
|
|
1414
|
-
| WriteIfEmpty
|
|
1415
|
-
| ResetShell
|
|
1416
|
-
| FileEditFindReplace
|
|
1417
|
-
| FileEdit
|
|
1418
|
-
| str
|
|
1419
|
-
| ReadFiles
|
|
1420
|
-
| Initialize
|
|
1421
|
-
| ContextSave
|
|
1422
|
-
)
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
def register_client(server_url: str, client_uuid: str = "") -> None:
|
|
1426
|
-
global default_enc, curr_cost
|
|
1427
|
-
# Generate a unique UUID for this client
|
|
1428
|
-
if not client_uuid:
|
|
1429
|
-
client_uuid = str(uuid.uuid4())
|
|
1430
|
-
|
|
1431
|
-
# Create the WebSocket connection
|
|
1432
|
-
try:
|
|
1433
|
-
with syncconnect(f"{server_url}/{client_uuid}") as websocket:
|
|
1434
|
-
server_version = str(websocket.recv())
|
|
1435
|
-
print(f"Server version: {server_version}")
|
|
1436
|
-
client_version = importlib.metadata.version("wcgw")
|
|
1437
|
-
websocket.send(client_version)
|
|
1438
|
-
|
|
1439
|
-
print(f"Connected. Share this user id with the chatbot: {client_uuid}")
|
|
1440
|
-
while True:
|
|
1441
|
-
# Wait to receive data from the server
|
|
1442
|
-
message = websocket.recv()
|
|
1443
|
-
mdata = Mdata.model_validate_json(message)
|
|
1444
|
-
if isinstance(mdata.data, str):
|
|
1445
|
-
raise Exception(mdata)
|
|
1446
|
-
try:
|
|
1447
|
-
outputs, cost = get_tool_output(
|
|
1448
|
-
mdata.data, default_enc, 0.0, lambda x, y: ("", 0), 8000
|
|
1449
|
-
)
|
|
1450
|
-
output = outputs[0]
|
|
1451
|
-
curr_cost += cost
|
|
1452
|
-
print(f"{curr_cost=}")
|
|
1453
|
-
except Exception as e:
|
|
1454
|
-
output = f"GOT EXCEPTION while calling tool. Error: {e}"
|
|
1455
|
-
console.print(traceback.format_exc())
|
|
1456
|
-
assert isinstance(output, str)
|
|
1457
|
-
websocket.send(output)
|
|
1458
|
-
|
|
1459
|
-
except (websockets.ConnectionClosed, ConnectionError, OSError):
|
|
1460
|
-
print(f"Connection closed for UUID: {client_uuid}, retrying")
|
|
1461
|
-
time.sleep(0.5)
|
|
1462
|
-
register_client(server_url, client_uuid)
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
run = Typer(pretty_exceptions_show_locals=False, no_args_is_help=True)
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
@run.command()
|
|
1469
|
-
def app(
|
|
1470
|
-
server_url: str = "",
|
|
1471
|
-
client_uuid: Optional[str] = None,
|
|
1472
|
-
version: bool = typer.Option(False, "--version", "-v"),
|
|
1473
|
-
) -> None:
|
|
1474
|
-
if version:
|
|
1475
|
-
version_ = importlib.metadata.version("wcgw")
|
|
1476
|
-
print(f"wcgw version: {version_}")
|
|
1477
|
-
exit()
|
|
1478
|
-
if not server_url:
|
|
1479
|
-
server_url = os.environ.get("WCGW_RELAY_SERVER", "")
|
|
1480
|
-
if not server_url:
|
|
1481
|
-
print(
|
|
1482
|
-
"Error: Please provide relay server url using --server_url or WCGW_RELAY_SERVER environment variable"
|
|
1483
|
-
)
|
|
1484
|
-
print(
|
|
1485
|
-
"\tNOTE: you need to run a relay server first, author doesn't host a relay server anymore."
|
|
1486
|
-
)
|
|
1487
|
-
print("\thttps://github.com/rusiaaman/wcgw/blob/main/openai.md")
|
|
1488
|
-
print("\tExample `--server-url=ws://localhost:8000/v1/register`")
|
|
1489
|
-
raise typer.Exit(1)
|
|
1490
|
-
register_client(server_url, client_uuid or "")
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
def read_files(file_paths: list[str], max_tokens: Optional[int]) -> str:
|
|
646
|
+
def read_files(
|
|
647
|
+
file_paths: list[str], max_tokens: Optional[int], context: Context
|
|
648
|
+
) -> str:
|
|
1494
649
|
message = ""
|
|
1495
650
|
for i, file in enumerate(file_paths):
|
|
1496
651
|
try:
|
|
1497
|
-
content, truncated, tokens = read_file(file, max_tokens)
|
|
652
|
+
content, truncated, tokens = read_file(file, max_tokens, context)
|
|
1498
653
|
except Exception as e:
|
|
1499
654
|
message += f"\n{file}: {str(e)}\n"
|
|
1500
655
|
continue
|
|
@@ -1515,46 +670,37 @@ def read_files(file_paths: list[str], max_tokens: Optional[int]) -> str:
|
|
|
1515
670
|
return message
|
|
1516
671
|
|
|
1517
672
|
|
|
1518
|
-
def read_file(
|
|
1519
|
-
|
|
673
|
+
def read_file(
|
|
674
|
+
file_path: str, max_tokens: Optional[int], context: Context
|
|
675
|
+
) -> tuple[str, bool, int]:
|
|
676
|
+
context.console.print(f"Reading file: {file_path}")
|
|
1520
677
|
|
|
1521
678
|
# Expand the path before checking if it's absolute
|
|
1522
|
-
file_path = expand_user(file_path
|
|
679
|
+
file_path = expand_user(file_path)
|
|
1523
680
|
|
|
1524
681
|
if not os.path.isabs(file_path):
|
|
1525
682
|
raise ValueError(
|
|
1526
|
-
f"Failure: file_path should be absolute path, current working directory is {
|
|
683
|
+
f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}"
|
|
1527
684
|
)
|
|
1528
685
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if not BASH_STATE.is_in_docker:
|
|
1532
|
-
path = Path(file_path)
|
|
1533
|
-
if not path.exists():
|
|
1534
|
-
raise ValueError(f"Error: file {file_path} does not exist")
|
|
686
|
+
context.bash_state.add_to_whitelist_for_overwrite(file_path)
|
|
1535
687
|
|
|
1536
|
-
|
|
1537
|
-
|
|
688
|
+
path = Path(file_path)
|
|
689
|
+
if not path.exists():
|
|
690
|
+
raise ValueError(f"Error: file {file_path} does not exist")
|
|
1538
691
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(file_path)}",
|
|
1542
|
-
timeout=TIMEOUT,
|
|
1543
|
-
)
|
|
1544
|
-
if return_code != 0:
|
|
1545
|
-
raise Exception(
|
|
1546
|
-
f"Error: cat {file_path} failed with code {return_code}\nstdout: {content}\nstderr: {stderr}"
|
|
1547
|
-
)
|
|
692
|
+
with path.open("r") as f:
|
|
693
|
+
content = f.read(10_000_000)
|
|
1548
694
|
|
|
1549
695
|
truncated = False
|
|
1550
696
|
tokens_counts = 0
|
|
1551
697
|
if max_tokens is not None:
|
|
1552
|
-
tokens = default_enc.
|
|
698
|
+
tokens = default_enc.encoder(content)
|
|
1553
699
|
tokens_counts = len(tokens)
|
|
1554
700
|
if len(tokens) > max_tokens:
|
|
1555
|
-
content = default_enc.
|
|
701
|
+
content = default_enc.decoder(tokens[:max_tokens])
|
|
1556
702
|
rest = save_out_of_context(
|
|
1557
|
-
default_enc.
|
|
703
|
+
default_enc.decoder(tokens[max_tokens:]), Path(file_path).suffix
|
|
1558
704
|
)
|
|
1559
705
|
content += f"\n(...truncated)\n---\nI've saved the continuation in a new file. Please read: `{rest}`"
|
|
1560
706
|
truncated = True
|