wcgw 3.0.1rc2__py3-none-any.whl → 3.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wcgw might be problematic. Click here for more details.
- wcgw/client/bash_state/bash_state.py +84 -155
- wcgw/client/mcp_server/server.py +9 -17
- wcgw/client/modes.py +13 -12
- wcgw/client/tool_prompts.py +5 -11
- wcgw/client/tools.py +106 -48
- wcgw/relay/serve.py +0 -31
- wcgw/types_.py +24 -20
- {wcgw-3.0.1rc2.dist-info → wcgw-3.0.2.dist-info}/METADATA +24 -8
- {wcgw-3.0.1rc2.dist-info → wcgw-3.0.2.dist-info}/RECORD +14 -14
- wcgw_cli/anthropic_client.py +27 -11
- wcgw_cli/openai_client.py +1 -0
- {wcgw-3.0.1rc2.dist-info → wcgw-3.0.2.dist-info}/WHEEL +0 -0
- {wcgw-3.0.1rc2.dist-info → wcgw-3.0.2.dist-info}/entry_points.txt +0 -0
- {wcgw-3.0.1rc2.dist-info → wcgw-3.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,8 +8,6 @@ import traceback
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from typing import (
|
|
10
10
|
Any,
|
|
11
|
-
Callable,
|
|
12
|
-
Concatenate,
|
|
13
11
|
Literal,
|
|
14
12
|
Optional,
|
|
15
13
|
ParamSpec,
|
|
@@ -32,7 +30,8 @@ from ...types_ import (
|
|
|
32
30
|
from ..encoder import EncoderDecoder
|
|
33
31
|
from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
|
|
34
32
|
|
|
35
|
-
PROMPT_CONST = "
|
|
33
|
+
PROMPT_CONST = "wcgw→" + " "
|
|
34
|
+
PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
|
|
36
35
|
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
37
36
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
38
37
|
|
|
@@ -79,9 +78,7 @@ def get_tmpdir() -> str:
|
|
|
79
78
|
|
|
80
79
|
def check_if_screen_command_available() -> bool:
|
|
81
80
|
try:
|
|
82
|
-
subprocess.run(
|
|
83
|
-
["screen", "-v"], capture_output=True, check=True, timeout=CONFIG.timeout
|
|
84
|
-
)
|
|
81
|
+
subprocess.run(["screen", "-v"], capture_output=True, check=True, timeout=0.2)
|
|
85
82
|
return True
|
|
86
83
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
87
84
|
return False
|
|
@@ -100,7 +97,7 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
100
97
|
capture_output=True,
|
|
101
98
|
text=True,
|
|
102
99
|
check=True,
|
|
103
|
-
timeout=
|
|
100
|
+
timeout=0.2,
|
|
104
101
|
)
|
|
105
102
|
output = result.stdout
|
|
106
103
|
except subprocess.CalledProcessError as e:
|
|
@@ -108,6 +105,9 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
108
105
|
output = (e.stdout or "") + (e.stderr or "")
|
|
109
106
|
except FileNotFoundError:
|
|
110
107
|
return
|
|
108
|
+
except Exception as e:
|
|
109
|
+
console.log(f"{e}: exception while clearing running screens.")
|
|
110
|
+
return
|
|
111
111
|
|
|
112
112
|
sessions_to_kill = []
|
|
113
113
|
|
|
@@ -131,8 +131,8 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
131
131
|
check=True,
|
|
132
132
|
timeout=CONFIG.timeout,
|
|
133
133
|
)
|
|
134
|
-
except
|
|
135
|
-
console.log(f"Failed to kill screen session: {session}")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
console.log(f"Failed to kill screen session: {session}\n{e}")
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
def start_shell(
|
|
@@ -146,23 +146,21 @@ def start_shell(
|
|
|
146
146
|
**os.environ,
|
|
147
147
|
"PS1": PROMPT_CONST,
|
|
148
148
|
"TMPDIR": get_tmpdir(),
|
|
149
|
-
"TERM": "
|
|
149
|
+
"TERM": "xterm-256color",
|
|
150
150
|
}
|
|
151
151
|
try:
|
|
152
152
|
shell = pexpect.spawn(
|
|
153
153
|
cmd,
|
|
154
154
|
env=overrideenv, # type: ignore[arg-type]
|
|
155
|
-
echo=
|
|
155
|
+
echo=True,
|
|
156
156
|
encoding="utf-8",
|
|
157
157
|
timeout=CONFIG.timeout,
|
|
158
158
|
cwd=initial_dir,
|
|
159
159
|
codec_errors="backslashreplace",
|
|
160
160
|
dimensions=(500, 160),
|
|
161
161
|
)
|
|
162
|
-
shell.sendline(
|
|
163
|
-
|
|
164
|
-
) # Unset prompt command to avoid interfering
|
|
165
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
162
|
+
shell.sendline(PROMPT_STATEMENT) # Unset prompt command to avoid interfering
|
|
163
|
+
shell.expect(PROMPT_CONST, timeout=0.2)
|
|
166
164
|
except Exception as e:
|
|
167
165
|
console.print(traceback.format_exc())
|
|
168
166
|
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
@@ -170,13 +168,13 @@ def start_shell(
|
|
|
170
168
|
shell = pexpect.spawn(
|
|
171
169
|
"/bin/bash --noprofile --norc",
|
|
172
170
|
env=overrideenv, # type: ignore[arg-type]
|
|
173
|
-
echo=
|
|
171
|
+
echo=True,
|
|
174
172
|
encoding="utf-8",
|
|
175
173
|
timeout=CONFIG.timeout,
|
|
176
174
|
codec_errors="backslashreplace",
|
|
177
175
|
)
|
|
178
|
-
shell.sendline(
|
|
179
|
-
shell.expect(PROMPT_CONST, timeout=
|
|
176
|
+
shell.sendline(PROMPT_STATEMENT)
|
|
177
|
+
shell.expect(PROMPT_CONST, timeout=0.2)
|
|
180
178
|
|
|
181
179
|
shellid = "wcgw." + time.strftime("%H%M%S")
|
|
182
180
|
if over_screen:
|
|
@@ -184,20 +182,11 @@ def start_shell(
|
|
|
184
182
|
raise ValueError("Screen command not available")
|
|
185
183
|
# shellid is just hour, minute, second number
|
|
186
184
|
shell.sendline(f"trap 'screen -X -S {shellid} quit' EXIT")
|
|
187
|
-
shell.expect(PROMPT_CONST, timeout=
|
|
185
|
+
shell.expect(PROMPT_CONST, timeout=0.2)
|
|
188
186
|
|
|
189
187
|
shell.sendline(f"screen -q -S {shellid} /bin/bash --noprofile --norc")
|
|
190
188
|
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
191
189
|
|
|
192
|
-
shell.sendline("stty -icanon -echo")
|
|
193
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
194
|
-
|
|
195
|
-
shell.sendline("set +o pipefail")
|
|
196
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
197
|
-
|
|
198
|
-
shell.sendline("export GIT_PAGER=cat PAGER=cat")
|
|
199
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
200
|
-
|
|
201
190
|
return shell, shellid
|
|
202
191
|
|
|
203
192
|
|
|
@@ -229,31 +218,7 @@ P = ParamSpec("P")
|
|
|
229
218
|
R = TypeVar("R")
|
|
230
219
|
|
|
231
220
|
|
|
232
|
-
def requires_shell(
|
|
233
|
-
func: Callable[Concatenate["BashState", "pexpect.spawn[str]", P], R],
|
|
234
|
-
) -> Callable[Concatenate["BashState", P], R]:
|
|
235
|
-
def wrapper(self: "BashState", /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
236
|
-
if not self._shell_loading.is_set():
|
|
237
|
-
if not self._shell_loading.wait(timeout=CONFIG.timeout):
|
|
238
|
-
raise RuntimeError("Shell initialization timeout")
|
|
239
|
-
|
|
240
|
-
if self._shell_error:
|
|
241
|
-
raise RuntimeError(f"Shell failed to initialize: {self._shell_error}.")
|
|
242
|
-
|
|
243
|
-
if not self._shell:
|
|
244
|
-
raise RuntimeError("Shell not initialized")
|
|
245
|
-
|
|
246
|
-
return func(self, self._shell, *args, **kwargs)
|
|
247
|
-
|
|
248
|
-
return wrapper
|
|
249
|
-
|
|
250
|
-
|
|
251
221
|
class BashState:
|
|
252
|
-
_shell: Optional["pexpect.spawn[str]"]
|
|
253
|
-
_shell_id: Optional[str]
|
|
254
|
-
_shell_lock: threading.Lock
|
|
255
|
-
_shell_loading: threading.Event
|
|
256
|
-
_shell_error: Optional[Exception]
|
|
257
222
|
_use_screen: bool
|
|
258
223
|
|
|
259
224
|
def __init__(
|
|
@@ -276,68 +241,41 @@ class BashState:
|
|
|
276
241
|
self._write_if_empty_mode: WriteIfEmptyMode = (
|
|
277
242
|
write_if_empty_mode or WriteIfEmptyMode("all")
|
|
278
243
|
)
|
|
279
|
-
self._mode = mode or
|
|
244
|
+
self._mode = mode or "wcgw"
|
|
280
245
|
self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
|
|
281
|
-
self._prompt = PROMPT_CONST
|
|
282
246
|
self._bg_expect_thread: Optional[threading.Thread] = None
|
|
283
247
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
284
|
-
self._shell = None
|
|
285
|
-
self._shell_id = None
|
|
286
|
-
self._shell_lock = threading.Lock()
|
|
287
|
-
self._shell_loading = threading.Event()
|
|
288
|
-
self._shell_error = None
|
|
289
248
|
self._use_screen = use_screen
|
|
290
|
-
self.
|
|
249
|
+
self._init_shell()
|
|
291
250
|
|
|
292
|
-
def
|
|
293
|
-
def load_shell() -> None:
|
|
294
|
-
try:
|
|
295
|
-
with self._shell_lock:
|
|
296
|
-
if self._shell is not None:
|
|
297
|
-
return
|
|
298
|
-
self._init_shell()
|
|
299
|
-
except Exception as e:
|
|
300
|
-
self._shell_error = e
|
|
301
|
-
finally:
|
|
302
|
-
self._shell_loading.set()
|
|
303
|
-
|
|
304
|
-
threading.Thread(target=load_shell).start()
|
|
305
|
-
|
|
306
|
-
@requires_shell
|
|
307
|
-
def expect(
|
|
308
|
-
self, shell: "pexpect.spawn[str]", pattern: Any, timeout: Optional[float] = -1
|
|
309
|
-
) -> int:
|
|
251
|
+
def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
|
|
310
252
|
self.close_bg_expect_thread()
|
|
311
|
-
|
|
253
|
+
output = self._shell.expect(pattern, timeout)
|
|
254
|
+
return output
|
|
312
255
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
output =
|
|
316
|
-
self.run_bg_expect_thread()
|
|
256
|
+
def send(self, s: str | bytes) -> int:
|
|
257
|
+
self.close_bg_expect_thread()
|
|
258
|
+
output = self._shell.send(s)
|
|
317
259
|
return output
|
|
318
260
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
output =
|
|
322
|
-
self.run_bg_expect_thread()
|
|
261
|
+
def sendline(self, s: str | bytes) -> int:
|
|
262
|
+
self.close_bg_expect_thread()
|
|
263
|
+
output = self._shell.sendline(s)
|
|
323
264
|
return output
|
|
324
265
|
|
|
325
266
|
@property
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return shell.linesep
|
|
267
|
+
def linesep(self) -> Any:
|
|
268
|
+
return self._shell.linesep
|
|
329
269
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
270
|
+
def sendintr(self) -> None:
|
|
271
|
+
self.close_bg_expect_thread()
|
|
272
|
+
self._shell.sendintr()
|
|
333
273
|
|
|
334
274
|
@property
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return shell.before
|
|
275
|
+
def before(self) -> Optional[str]:
|
|
276
|
+
return self._shell.before
|
|
338
277
|
|
|
339
|
-
|
|
340
|
-
def run_bg_expect_thread(self, shell: "pexpect.spawn[str]") -> None:
|
|
278
|
+
def run_bg_expect_thread(self) -> None:
|
|
341
279
|
"""
|
|
342
280
|
Run background expect thread for handling shell interactions.
|
|
343
281
|
"""
|
|
@@ -346,7 +284,7 @@ class BashState:
|
|
|
346
284
|
while True:
|
|
347
285
|
if self._bg_expect_thread_stop_event.is_set():
|
|
348
286
|
break
|
|
349
|
-
output =
|
|
287
|
+
output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
|
|
350
288
|
if output == 0:
|
|
351
289
|
break
|
|
352
290
|
|
|
@@ -367,13 +305,8 @@ class BashState:
|
|
|
367
305
|
|
|
368
306
|
def cleanup(self) -> None:
|
|
369
307
|
self.close_bg_expect_thread()
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
self._shell.close(True)
|
|
373
|
-
if self._shell_id:
|
|
374
|
-
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
375
|
-
self._shell = None
|
|
376
|
-
self._shell_id = None
|
|
308
|
+
self._shell.close(True)
|
|
309
|
+
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
377
310
|
|
|
378
311
|
def __enter__(self) -> "BashState":
|
|
379
312
|
return self
|
|
@@ -397,59 +330,41 @@ class BashState:
|
|
|
397
330
|
def write_if_empty_mode(self) -> WriteIfEmptyMode:
|
|
398
331
|
return self._write_if_empty_mode
|
|
399
332
|
|
|
400
|
-
|
|
401
|
-
def ensure_env_and_bg_jobs(self, _: "pexpect.spawn[str]") -> Optional[int]:
|
|
402
|
-
return self._ensure_env_and_bg_jobs()
|
|
403
|
-
|
|
404
|
-
def _ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
405
|
-
# Do not add @requires_shell decorator here, as it will cause deadlock
|
|
406
|
-
|
|
407
|
-
self.close_bg_expect_thread()
|
|
408
|
-
assert self._shell is not None, "Bad state, shell is not initialized"
|
|
409
|
-
if self._prompt != PROMPT_CONST:
|
|
410
|
-
return None
|
|
333
|
+
def ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
411
334
|
quick_timeout = 0.2 if not self.over_screen else 1
|
|
412
335
|
# First reset the prompt in case venv was sourced or other reasons.
|
|
413
|
-
self.
|
|
414
|
-
self.
|
|
336
|
+
self.sendline(PROMPT_STATEMENT)
|
|
337
|
+
self.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
415
338
|
# Reset echo also if it was enabled
|
|
416
|
-
|
|
417
|
-
self.
|
|
418
|
-
self._shell.sendline("set +o pipefail")
|
|
419
|
-
self._shell.expect(self._prompt, timeout=quick_timeout)
|
|
420
|
-
self._shell.sendline("export GIT_PAGER=cat PAGER=cat")
|
|
421
|
-
self._shell.expect(self._prompt, timeout=quick_timeout)
|
|
422
|
-
self._shell.sendline("jobs | wc -l")
|
|
339
|
+
command = "jobs | wc -l"
|
|
340
|
+
self.sendline(command)
|
|
423
341
|
before = ""
|
|
424
342
|
counts = 0
|
|
425
343
|
while not _is_int(before): # Consume all previous output
|
|
426
344
|
try:
|
|
427
|
-
self.
|
|
345
|
+
self.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
428
346
|
except pexpect.TIMEOUT:
|
|
429
347
|
self.console.print(f"Couldn't get exit code, before: {before}")
|
|
430
348
|
raise
|
|
431
349
|
|
|
432
|
-
before_val = self.
|
|
350
|
+
before_val = self.before
|
|
433
351
|
if not isinstance(before_val, str):
|
|
434
352
|
before_val = str(before_val)
|
|
435
353
|
assert isinstance(before_val, str)
|
|
436
354
|
before_lines = render_terminal_output(before_val)
|
|
437
|
-
before = "\n".join(before_lines).strip()
|
|
355
|
+
before = "\n".join(before_lines).replace(command, "").strip()
|
|
438
356
|
counts += 1
|
|
439
357
|
if counts > 100:
|
|
440
358
|
raise ValueError(
|
|
441
359
|
"Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
|
|
442
360
|
)
|
|
443
|
-
|
|
444
361
|
try:
|
|
445
362
|
return int(before)
|
|
446
363
|
except ValueError:
|
|
447
364
|
raise ValueError(f"Malformed output: {before}")
|
|
448
365
|
|
|
449
366
|
def _init_shell(self) -> None:
|
|
450
|
-
self._prompt = PROMPT_CONST
|
|
451
367
|
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
452
|
-
self._is_in_docker: Optional[str] = ""
|
|
453
368
|
# Ensure self._cwd exists
|
|
454
369
|
os.makedirs(self._cwd, exist_ok=True)
|
|
455
370
|
try:
|
|
@@ -474,13 +389,17 @@ class BashState:
|
|
|
474
389
|
self.over_screen = False
|
|
475
390
|
|
|
476
391
|
self._pending_output = ""
|
|
477
|
-
|
|
392
|
+
try:
|
|
393
|
+
self.ensure_env_and_bg_jobs()
|
|
394
|
+
except ValueError as e:
|
|
395
|
+
self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
|
|
396
|
+
|
|
397
|
+
self.run_bg_expect_thread()
|
|
478
398
|
|
|
479
399
|
def set_pending(self, last_pending_output: str) -> None:
|
|
480
400
|
if not isinstance(self._state, datetime.datetime):
|
|
481
401
|
self._state = datetime.datetime.now()
|
|
482
402
|
self._pending_output = last_pending_output
|
|
483
|
-
self.run_bg_expect_thread()
|
|
484
403
|
|
|
485
404
|
def set_repl(self) -> None:
|
|
486
405
|
self._state = "repl"
|
|
@@ -492,26 +411,18 @@ class BashState:
|
|
|
492
411
|
return "repl"
|
|
493
412
|
return "pending"
|
|
494
413
|
|
|
495
|
-
@property
|
|
496
|
-
def is_in_docker(self) -> Optional[str]:
|
|
497
|
-
return self._is_in_docker
|
|
498
|
-
|
|
499
|
-
def set_in_docker(self, docker_image_id: str) -> None:
|
|
500
|
-
self._is_in_docker = docker_image_id
|
|
501
|
-
|
|
502
414
|
@property
|
|
503
415
|
def cwd(self) -> str:
|
|
504
416
|
return self._cwd
|
|
505
417
|
|
|
506
418
|
@property
|
|
507
419
|
def prompt(self) -> str:
|
|
508
|
-
return
|
|
420
|
+
return PROMPT_CONST
|
|
509
421
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
before_val = shell.before
|
|
422
|
+
def update_cwd(self) -> str:
|
|
423
|
+
self.sendline("pwd")
|
|
424
|
+
self.expect(PROMPT_CONST, timeout=0.2)
|
|
425
|
+
before_val = self.before
|
|
515
426
|
if not isinstance(before_val, str):
|
|
516
427
|
before_val = str(before_val)
|
|
517
428
|
before_lines = render_terminal_output(before_val)
|
|
@@ -521,9 +432,7 @@ class BashState:
|
|
|
521
432
|
|
|
522
433
|
def reset_shell(self) -> None:
|
|
523
434
|
self.cleanup()
|
|
524
|
-
self.
|
|
525
|
-
self._shell_error = None
|
|
526
|
-
self._start_shell_loading()
|
|
435
|
+
self._init_shell()
|
|
527
436
|
|
|
528
437
|
def serialize(self) -> dict[str, Any]:
|
|
529
438
|
"""Serialize BashState to a dictionary for saving"""
|
|
@@ -543,7 +452,7 @@ class BashState:
|
|
|
543
452
|
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
544
453
|
FileEditMode.deserialize(state["file_edit_mode"]),
|
|
545
454
|
WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
|
|
546
|
-
|
|
455
|
+
state["mode"],
|
|
547
456
|
state["whitelist_for_overwrite"],
|
|
548
457
|
)
|
|
549
458
|
|
|
@@ -607,7 +516,7 @@ WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run mul
|
|
|
607
516
|
1. Get its output using `send_ascii: [10] or send_specials: ["Enter"]`
|
|
608
517
|
2. Use `send_ascii` or `send_specials` to give inputs to the running program, don't use `BashCommand` OR
|
|
609
518
|
3. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
|
|
610
|
-
4.
|
|
519
|
+
4. Interrupt and run the process in background by re-running it using screen
|
|
611
520
|
"""
|
|
612
521
|
|
|
613
522
|
|
|
@@ -637,9 +546,8 @@ def _incremental_text(text: str, last_pending_output: str) -> str:
|
|
|
637
546
|
# text = render_terminal_output(text[-100_000:])
|
|
638
547
|
text = text[-100_000:]
|
|
639
548
|
|
|
640
|
-
|
|
641
|
-
last_pending_output_rendered = "\n".join(
|
|
642
|
-
last_rendered_lines = last_pending_output_rendered.split("\n")
|
|
549
|
+
last_rendered_lines = render_terminal_output(last_pending_output)
|
|
550
|
+
last_pending_output_rendered = "\n".join(last_rendered_lines)
|
|
643
551
|
if not last_rendered_lines:
|
|
644
552
|
return rstrip(render_terminal_output(text))
|
|
645
553
|
|
|
@@ -690,6 +598,27 @@ def execute_bash(
|
|
|
690
598
|
bash_arg: BashCommand,
|
|
691
599
|
max_tokens: Optional[int],
|
|
692
600
|
timeout_s: Optional[float],
|
|
601
|
+
) -> tuple[str, float]:
|
|
602
|
+
try:
|
|
603
|
+
output, cost = _execute_bash(bash_state, enc, bash_arg, max_tokens, timeout_s)
|
|
604
|
+
|
|
605
|
+
# Remove echo if it's a command
|
|
606
|
+
if isinstance(bash_arg.action, Command):
|
|
607
|
+
command = bash_arg.action.command.strip()
|
|
608
|
+
if output.startswith(command):
|
|
609
|
+
output = output[len(command) :]
|
|
610
|
+
|
|
611
|
+
finally:
|
|
612
|
+
bash_state.run_bg_expect_thread()
|
|
613
|
+
return output, cost
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _execute_bash(
|
|
617
|
+
bash_state: BashState,
|
|
618
|
+
enc: EncoderDecoder[int],
|
|
619
|
+
bash_arg: BashCommand,
|
|
620
|
+
max_tokens: Optional[int],
|
|
621
|
+
timeout_s: Optional[float],
|
|
693
622
|
) -> tuple[str, float]:
|
|
694
623
|
try:
|
|
695
624
|
is_interrupt = False
|
wcgw/client/mcp_server/server.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import importlib
|
|
2
|
-
import json
|
|
3
2
|
import logging
|
|
4
3
|
import os
|
|
5
4
|
from typing import Any
|
|
@@ -9,7 +8,7 @@ import mcp_wcgw.types as types
|
|
|
9
8
|
from mcp_wcgw.server import NotificationOptions, Server
|
|
10
9
|
from mcp_wcgw.server.models import InitializationOptions
|
|
11
10
|
from mcp_wcgw.types import Tool as ToolParam
|
|
12
|
-
from pydantic import AnyUrl
|
|
11
|
+
from pydantic import AnyUrl
|
|
13
12
|
|
|
14
13
|
from wcgw.client.modes import KTS
|
|
15
14
|
from wcgw.client.tool_prompts import TOOL_PROMPTS
|
|
@@ -18,7 +17,13 @@ from ...types_ import (
|
|
|
18
17
|
Initialize,
|
|
19
18
|
)
|
|
20
19
|
from ..bash_state.bash_state import CONFIG, BashState
|
|
21
|
-
from ..tools import
|
|
20
|
+
from ..tools import (
|
|
21
|
+
Context,
|
|
22
|
+
default_enc,
|
|
23
|
+
get_tool_output,
|
|
24
|
+
parse_tool_by_name,
|
|
25
|
+
which_tool_name,
|
|
26
|
+
)
|
|
22
27
|
|
|
23
28
|
server = Server("wcgw")
|
|
24
29
|
|
|
@@ -104,20 +109,7 @@ async def handle_call_tool(
|
|
|
104
109
|
raise ValueError("Missing arguments")
|
|
105
110
|
|
|
106
111
|
tool_type = which_tool_name(name)
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
tool_call = tool_type(**arguments)
|
|
110
|
-
except ValidationError:
|
|
111
|
-
|
|
112
|
-
def try_json(x: str) -> Any:
|
|
113
|
-
if not isinstance(x, str):
|
|
114
|
-
return x
|
|
115
|
-
try:
|
|
116
|
-
return json.loads(x)
|
|
117
|
-
except json.JSONDecodeError:
|
|
118
|
-
return x
|
|
119
|
-
|
|
120
|
-
tool_call = tool_type(**{k: try_json(v) for k, v in arguments.items()})
|
|
112
|
+
tool_call = parse_tool_by_name(name, arguments)
|
|
121
113
|
|
|
122
114
|
try:
|
|
123
115
|
assert BASH_STATE
|
wcgw/client/modes.py
CHANGED
|
@@ -85,7 +85,7 @@ You are now running in "code_writer" mode.
|
|
|
85
85
|
base += path_prompt
|
|
86
86
|
|
|
87
87
|
run_command_common = """
|
|
88
|
-
- Do not use Ctrl-c
|
|
88
|
+
- Do not use Ctrl-c interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
89
89
|
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
90
90
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
91
91
|
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
@@ -124,7 +124,7 @@ Instructions:
|
|
|
124
124
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
125
125
|
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
126
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
|
|
127
|
+
- Do not use Ctrl-c or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
128
128
|
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
129
129
|
|
|
130
130
|
Additional instructions:
|
|
@@ -138,10 +138,11 @@ ARCHITECT_PROMPT = """You are now running in "architect" mode. This means
|
|
|
138
138
|
- You are not allowed to edit or update any file. You are not allowed to create any file.
|
|
139
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
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
|
|
141
|
+
- Do not use Ctrl-c or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
142
142
|
- You are not allowed to change directory (bash will run in -r mode)
|
|
143
|
+
- Share only snippets when any implementation is requested.
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
Respond only after doing the following:
|
|
145
146
|
- Read as many relevant files as possible.
|
|
146
147
|
- Be comprehensive in your understanding and search of relevant files.
|
|
147
148
|
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
@@ -149,17 +150,17 @@ Your response should be in self-critique and brainstorm style.
|
|
|
149
150
|
|
|
150
151
|
|
|
151
152
|
DEFAULT_MODES: dict[Modes, ModeImpl] = {
|
|
152
|
-
|
|
153
|
+
"wcgw": ModeImpl(
|
|
153
154
|
bash_command_mode=BashCommandMode("normal_mode", "all"),
|
|
154
155
|
write_if_empty_mode=WriteIfEmptyMode("all"),
|
|
155
156
|
file_edit_mode=FileEditMode("all"),
|
|
156
157
|
),
|
|
157
|
-
|
|
158
|
+
"architect": ModeImpl(
|
|
158
159
|
bash_command_mode=BashCommandMode("restricted_mode", "all"),
|
|
159
160
|
write_if_empty_mode=WriteIfEmptyMode([]),
|
|
160
161
|
file_edit_mode=FileEditMode([]),
|
|
161
162
|
),
|
|
162
|
-
|
|
163
|
+
"code_writer": ModeImpl(
|
|
163
164
|
bash_command_mode=BashCommandMode("normal_mode", "all"),
|
|
164
165
|
write_if_empty_mode=WriteIfEmptyMode("all"),
|
|
165
166
|
file_edit_mode=FileEditMode("all"),
|
|
@@ -172,11 +173,11 @@ def modes_to_state(
|
|
|
172
173
|
) -> tuple[BashCommandMode, FileEditMode, WriteIfEmptyMode, Modes]:
|
|
173
174
|
# First get default mode config
|
|
174
175
|
if isinstance(mode, str):
|
|
175
|
-
mode_impl = DEFAULT_MODES[
|
|
176
|
-
mode_name =
|
|
176
|
+
mode_impl = DEFAULT_MODES[mode] # converts str to Modes enum
|
|
177
|
+
mode_name: Modes = mode
|
|
177
178
|
else:
|
|
178
179
|
# For CodeWriterMode, use code_writer as base and override
|
|
179
|
-
mode_impl = DEFAULT_MODES[
|
|
180
|
+
mode_impl = DEFAULT_MODES["code_writer"]
|
|
180
181
|
# Override with custom settings from CodeWriterMode
|
|
181
182
|
mode_impl = ModeImpl(
|
|
182
183
|
bash_command_mode=BashCommandMode(
|
|
@@ -186,7 +187,7 @@ def modes_to_state(
|
|
|
186
187
|
file_edit_mode=FileEditMode(mode.allowed_globs),
|
|
187
188
|
write_if_empty_mode=WriteIfEmptyMode(mode.allowed_globs),
|
|
188
189
|
)
|
|
189
|
-
mode_name =
|
|
190
|
+
mode_name = "code_writer"
|
|
190
191
|
return (
|
|
191
192
|
mode_impl.bash_command_mode,
|
|
192
193
|
mode_impl.file_edit_mode,
|
|
@@ -233,4 +234,4 @@ Provide all relevant file paths in order to understand and solve the the task. E
|
|
|
233
234
|
(Note to self: this conversation can then be resumed later asking "Resume wcgw task `<generated id>`" which should call Initialize tool)
|
|
234
235
|
"""
|
|
235
236
|
|
|
236
|
-
KTS = {
|
|
237
|
+
KTS = {"wcgw": WCGW_KT, "architect": ARCHITECT_KT, "code_writer": WCGW_KT}
|
wcgw/client/tool_prompts.py
CHANGED
|
@@ -9,7 +9,6 @@ from ..types_ import (
|
|
|
9
9
|
Initialize,
|
|
10
10
|
ReadFiles,
|
|
11
11
|
ReadImage,
|
|
12
|
-
ResetWcgw,
|
|
13
12
|
WriteIfEmpty,
|
|
14
13
|
)
|
|
15
14
|
|
|
@@ -30,15 +29,15 @@ TOOL_PROMPTS = [
|
|
|
30
29
|
name="Initialize",
|
|
31
30
|
description="""
|
|
32
31
|
- Always call this at the start of the conversation before using any of the shell tools from wcgw.
|
|
33
|
-
- This will reset the shell.
|
|
34
32
|
- Use `any_workspace_path` to initialize the shell in the appropriate project directory.
|
|
35
|
-
- If the user has mentioned a workspace or project root
|
|
36
|
-
- If the user has mentioned a folder or file with unclear project root, use the file or folder as `any_workspace_path`.
|
|
33
|
+
- If the user has mentioned a workspace or project root or any other file or folder use it to set `any_workspace_path`.
|
|
37
34
|
- If user has mentioned any files use `initial_files_to_read` to read, use absolute paths only.
|
|
38
|
-
- Leave `any_workspace_path` as empty if no file or folder is mentioned.
|
|
39
35
|
- By default use mode "wcgw"
|
|
40
36
|
- In "code-writer" mode, set the commands and globs which user asked to set, otherwise use 'all'.
|
|
41
|
-
-
|
|
37
|
+
- Use type="first_call" if it's the first call to this tool.
|
|
38
|
+
- Use type="user_asked_mode_change" if in a conversation user has asked to change mode.
|
|
39
|
+
- Use type="reset_shell" if in a conversation shell is not working after multiple tries.
|
|
40
|
+
- Use type="user_asked_change_workspace" if in a conversation user asked to change workspace
|
|
42
41
|
""",
|
|
43
42
|
),
|
|
44
43
|
Prompts(
|
|
@@ -79,11 +78,6 @@ TOOL_PROMPTS = [
|
|
|
79
78
|
name="ReadImage",
|
|
80
79
|
description="Read an image from the shell.",
|
|
81
80
|
),
|
|
82
|
-
Prompts(
|
|
83
|
-
inputSchema=ResetWcgw.model_json_schema(),
|
|
84
|
-
name="ResetWcgw",
|
|
85
|
-
description="Resets the shell. Use either when changing mode, or when all interrupts and prompt reset attempts have failed repeatedly.",
|
|
86
|
-
),
|
|
87
81
|
Prompts(
|
|
88
82
|
inputSchema=FileEdit.model_json_schema(),
|
|
89
83
|
name="FileEdit",
|
wcgw/client/tools.py
CHANGED
|
@@ -10,6 +10,7 @@ from os.path import expanduser
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from tempfile import NamedTemporaryFile
|
|
12
12
|
from typing import (
|
|
13
|
+
Any,
|
|
13
14
|
Callable,
|
|
14
15
|
Literal,
|
|
15
16
|
Optional,
|
|
@@ -21,7 +22,7 @@ from typing import (
|
|
|
21
22
|
from openai.types.chat import (
|
|
22
23
|
ChatCompletionMessageParam,
|
|
23
24
|
)
|
|
24
|
-
from pydantic import BaseModel, TypeAdapter
|
|
25
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
25
26
|
from syntax_checker import check_syntax
|
|
26
27
|
|
|
27
28
|
from wcgw.client.bash_state.bash_state import get_status
|
|
@@ -37,7 +38,6 @@ from ..types_ import (
|
|
|
37
38
|
ModesConfig,
|
|
38
39
|
ReadFiles,
|
|
39
40
|
ReadImage,
|
|
40
|
-
ResetWcgw,
|
|
41
41
|
WriteIfEmpty,
|
|
42
42
|
)
|
|
43
43
|
from .bash_state.bash_state import (
|
|
@@ -67,13 +67,13 @@ INITIALIZED = False
|
|
|
67
67
|
|
|
68
68
|
def get_mode_prompt(context: Context) -> str:
|
|
69
69
|
mode_prompt = ""
|
|
70
|
-
if context.bash_state.mode ==
|
|
70
|
+
if context.bash_state.mode == "code_writer":
|
|
71
71
|
mode_prompt = code_writer_prompt(
|
|
72
72
|
context.bash_state.file_edit_mode.allowed_globs,
|
|
73
73
|
context.bash_state.write_if_empty_mode.allowed_globs,
|
|
74
74
|
"all" if context.bash_state.bash_command_mode.allowed_commands else [],
|
|
75
75
|
)
|
|
76
|
-
elif context.bash_state.mode ==
|
|
76
|
+
elif context.bash_state.mode == "architect":
|
|
77
77
|
mode_prompt = ARCHITECT_PROMPT
|
|
78
78
|
else:
|
|
79
79
|
mode_prompt = WCGW_PROMPT
|
|
@@ -82,6 +82,7 @@ def get_mode_prompt(context: Context) -> str:
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def initialize(
|
|
85
|
+
type: Literal["user_asked_change_workspace", "first_call"],
|
|
85
86
|
context: Context,
|
|
86
87
|
any_workspace_path: str,
|
|
87
88
|
read_files_: list[str],
|
|
@@ -95,20 +96,26 @@ def initialize(
|
|
|
95
96
|
|
|
96
97
|
memory = ""
|
|
97
98
|
loaded_state = None
|
|
98
|
-
if task_id_to_resume:
|
|
99
|
-
try:
|
|
100
|
-
project_root_path, task_mem, loaded_state = load_memory(
|
|
101
|
-
task_id_to_resume,
|
|
102
|
-
max_tokens,
|
|
103
|
-
lambda x: default_enc.encoder(x),
|
|
104
|
-
lambda x: default_enc.decoder(x),
|
|
105
|
-
)
|
|
106
|
-
memory = "Following is the retrieved task:\n" + task_mem
|
|
107
|
-
if os.path.exists(project_root_path):
|
|
108
|
-
any_workspace_path = project_root_path
|
|
109
99
|
|
|
110
|
-
|
|
111
|
-
|
|
100
|
+
if type == "first_call":
|
|
101
|
+
if task_id_to_resume:
|
|
102
|
+
try:
|
|
103
|
+
project_root_path, task_mem, loaded_state = load_memory(
|
|
104
|
+
task_id_to_resume,
|
|
105
|
+
max_tokens,
|
|
106
|
+
lambda x: default_enc.encoder(x),
|
|
107
|
+
lambda x: default_enc.decoder(x),
|
|
108
|
+
)
|
|
109
|
+
memory = "Following is the retrieved task:\n" + task_mem
|
|
110
|
+
if os.path.exists(project_root_path):
|
|
111
|
+
any_workspace_path = project_root_path
|
|
112
|
+
|
|
113
|
+
except Exception:
|
|
114
|
+
memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
|
|
115
|
+
elif task_id_to_resume:
|
|
116
|
+
memory = (
|
|
117
|
+
"Warning: task can only be resumed in a new conversation. No task loaded."
|
|
118
|
+
)
|
|
112
119
|
|
|
113
120
|
folder_to_start = None
|
|
114
121
|
if any_workspace_path:
|
|
@@ -164,7 +171,9 @@ def initialize(
|
|
|
164
171
|
context.console.print(traceback.format_exc())
|
|
165
172
|
context.console.print("Error: couldn't load bash state")
|
|
166
173
|
pass
|
|
174
|
+
mode_prompt = get_mode_prompt(context)
|
|
167
175
|
else:
|
|
176
|
+
mode_changed = is_mode_change(mode, context.bash_state)
|
|
168
177
|
state = modes_to_state(mode)
|
|
169
178
|
context.bash_state.load_state(
|
|
170
179
|
state[0],
|
|
@@ -174,6 +183,11 @@ def initialize(
|
|
|
174
183
|
list(context.bash_state.whitelist_for_overwrite),
|
|
175
184
|
str(folder_to_start) if folder_to_start else "",
|
|
176
185
|
)
|
|
186
|
+
if type == "first_call" or mode_changed:
|
|
187
|
+
mode_prompt = get_mode_prompt(context)
|
|
188
|
+
else:
|
|
189
|
+
mode_prompt = ""
|
|
190
|
+
|
|
177
191
|
del mode
|
|
178
192
|
|
|
179
193
|
initial_files_context = ""
|
|
@@ -188,7 +202,7 @@ def initialize(
|
|
|
188
202
|
|
|
189
203
|
uname_sysname = os.uname().sysname
|
|
190
204
|
uname_machine = os.uname().machine
|
|
191
|
-
|
|
205
|
+
|
|
192
206
|
output = f"""
|
|
193
207
|
{mode_prompt}
|
|
194
208
|
|
|
@@ -211,20 +225,34 @@ Initialized in directory (also cwd): {context.bash_state.cwd}
|
|
|
211
225
|
return output, context
|
|
212
226
|
|
|
213
227
|
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
def is_mode_change(mode_config: ModesConfig, bash_state: BashState) -> bool:
|
|
229
|
+
allowed = modes_to_state(mode_config)
|
|
230
|
+
bash_allowed = (
|
|
231
|
+
bash_state.bash_command_mode,
|
|
232
|
+
bash_state.file_edit_mode,
|
|
233
|
+
bash_state.write_if_empty_mode,
|
|
234
|
+
bash_state.mode,
|
|
235
|
+
)
|
|
236
|
+
return allowed != bash_allowed
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def reset_wcgw(
|
|
240
|
+
context: Context,
|
|
241
|
+
starting_directory: str,
|
|
242
|
+
mode_name: Optional[Modes],
|
|
243
|
+
change_mode: ModesConfig,
|
|
244
|
+
) -> str:
|
|
245
|
+
global INITIALIZED
|
|
246
|
+
if mode_name:
|
|
247
|
+
# update modes if they're relative
|
|
248
|
+
if isinstance(change_mode, CodeWriterMode):
|
|
249
|
+
change_mode.update_relative_globs(starting_directory)
|
|
222
250
|
else:
|
|
223
|
-
|
|
251
|
+
assert isinstance(change_mode, str)
|
|
224
252
|
|
|
225
253
|
# Get new state configuration
|
|
226
254
|
bash_command_mode, file_edit_mode, write_if_empty_mode, mode = modes_to_state(
|
|
227
|
-
|
|
255
|
+
change_mode
|
|
228
256
|
)
|
|
229
257
|
|
|
230
258
|
# Reset shell with new mode
|
|
@@ -234,11 +262,12 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
|
|
|
234
262
|
write_if_empty_mode,
|
|
235
263
|
mode,
|
|
236
264
|
list(context.bash_state.whitelist_for_overwrite),
|
|
237
|
-
|
|
265
|
+
starting_directory,
|
|
238
266
|
)
|
|
239
267
|
mode_prompt = get_mode_prompt(context)
|
|
268
|
+
INITIALIZED = True
|
|
240
269
|
return (
|
|
241
|
-
f"Reset successful with mode change to {
|
|
270
|
+
f"Reset successful with mode change to {mode_name}.\n"
|
|
242
271
|
+ mode_prompt
|
|
243
272
|
+ "\n"
|
|
244
273
|
+ get_status(context.bash_state)
|
|
@@ -257,9 +286,8 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
|
|
|
257
286
|
write_if_empty_mode,
|
|
258
287
|
mode,
|
|
259
288
|
list(context.bash_state.whitelist_for_overwrite),
|
|
260
|
-
|
|
289
|
+
starting_directory,
|
|
261
290
|
)
|
|
262
|
-
global INITIALIZED
|
|
263
291
|
INITIALIZED = True
|
|
264
292
|
return "Reset successful" + get_status(context.bash_state)
|
|
265
293
|
|
|
@@ -513,7 +541,6 @@ Syntax errors:
|
|
|
513
541
|
|
|
514
542
|
TOOLS = (
|
|
515
543
|
BashCommand
|
|
516
|
-
| ResetWcgw
|
|
517
544
|
| WriteIfEmpty
|
|
518
545
|
| FileEdit
|
|
519
546
|
| ReadImage
|
|
@@ -531,8 +558,6 @@ def which_tool(args: str) -> TOOLS:
|
|
|
531
558
|
def which_tool_name(name: str) -> Type[TOOLS]:
|
|
532
559
|
if name == "BashCommand":
|
|
533
560
|
return BashCommand
|
|
534
|
-
elif name == "ResetWcgw":
|
|
535
|
-
return ResetWcgw
|
|
536
561
|
elif name == "WriteIfEmpty":
|
|
537
562
|
return WriteIfEmpty
|
|
538
563
|
elif name == "FileEdit":
|
|
@@ -549,6 +574,23 @@ def which_tool_name(name: str) -> Type[TOOLS]:
|
|
|
549
574
|
raise ValueError(f"Unknown tool name: {name}")
|
|
550
575
|
|
|
551
576
|
|
|
577
|
+
def parse_tool_by_name(name: str, arguments: dict[str, Any]) -> TOOLS:
|
|
578
|
+
tool_type = which_tool_name(name)
|
|
579
|
+
try:
|
|
580
|
+
return tool_type(**arguments)
|
|
581
|
+
except ValidationError:
|
|
582
|
+
|
|
583
|
+
def try_json(x: str) -> Any:
|
|
584
|
+
if not isinstance(x, str):
|
|
585
|
+
return x
|
|
586
|
+
try:
|
|
587
|
+
return json.loads(x)
|
|
588
|
+
except json.JSONDecodeError:
|
|
589
|
+
return x
|
|
590
|
+
|
|
591
|
+
return tool_type(**{k: try_json(v) for k, v in arguments.items()})
|
|
592
|
+
|
|
593
|
+
|
|
552
594
|
TOOL_CALLS: list[TOOLS] = []
|
|
553
595
|
|
|
554
596
|
|
|
@@ -595,21 +637,37 @@ def get_tool_output(
|
|
|
595
637
|
elif isinstance(arg, ReadFiles):
|
|
596
638
|
context.console.print("Calling read file tool")
|
|
597
639
|
output = read_files(arg.file_paths, max_tokens, context), 0.0
|
|
598
|
-
elif isinstance(arg, ResetWcgw):
|
|
599
|
-
context.console.print("Calling reset wcgw tool")
|
|
600
|
-
output = reset_wcgw(context, arg), 0.0
|
|
601
|
-
|
|
602
640
|
elif isinstance(arg, Initialize):
|
|
603
641
|
context.console.print("Calling initial info tool")
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
642
|
+
if arg.type == "user_asked_mode_change" or arg.type == "reset_shell":
|
|
643
|
+
workspace_path = (
|
|
644
|
+
arg.any_workspace_path
|
|
645
|
+
if os.path.isdir(arg.any_workspace_path)
|
|
646
|
+
else os.path.dirname(arg.any_workspace_path)
|
|
647
|
+
)
|
|
648
|
+
workspace_path = workspace_path if os.path.exists(workspace_path) else ""
|
|
649
|
+
output = (
|
|
650
|
+
reset_wcgw(
|
|
651
|
+
context,
|
|
652
|
+
workspace_path,
|
|
653
|
+
arg.mode_name
|
|
654
|
+
if is_mode_change(arg.mode, context.bash_state)
|
|
655
|
+
else None,
|
|
656
|
+
arg.mode,
|
|
657
|
+
),
|
|
658
|
+
0.0,
|
|
659
|
+
)
|
|
660
|
+
else:
|
|
661
|
+
output_, context = initialize(
|
|
662
|
+
arg.type,
|
|
663
|
+
context,
|
|
664
|
+
arg.any_workspace_path,
|
|
665
|
+
arg.initial_files_to_read,
|
|
666
|
+
arg.task_id_to_resume,
|
|
667
|
+
max_tokens,
|
|
668
|
+
arg.mode,
|
|
669
|
+
)
|
|
670
|
+
output = output_, 0.0
|
|
613
671
|
|
|
614
672
|
elif isinstance(arg, ContextSave):
|
|
615
673
|
context.console.print("Calling task memory tool")
|
wcgw/relay/serve.py
CHANGED
|
@@ -19,7 +19,6 @@ from ..types_ import (
|
|
|
19
19
|
FileEdit,
|
|
20
20
|
Initialize,
|
|
21
21
|
ReadFiles,
|
|
22
|
-
ResetWcgw,
|
|
23
22
|
WriteIfEmpty,
|
|
24
23
|
)
|
|
25
24
|
|
|
@@ -28,7 +27,6 @@ class Mdata(BaseModel):
|
|
|
28
27
|
data: (
|
|
29
28
|
BashCommand
|
|
30
29
|
| WriteIfEmpty
|
|
31
|
-
| ResetWcgw
|
|
32
30
|
| FileEdit
|
|
33
31
|
| ReadFiles
|
|
34
32
|
| Initialize
|
|
@@ -160,35 +158,6 @@ async def file_edit_find_replace(
|
|
|
160
158
|
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
161
159
|
|
|
162
160
|
|
|
163
|
-
class ResetWcgwWithUUID(ResetWcgw):
|
|
164
|
-
user_id: UUID
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@app.post("/v1/reset_wcgw")
|
|
168
|
-
async def reset_wcgw(reset_wcgw: ResetWcgwWithUUID) -> str:
|
|
169
|
-
user_id = reset_wcgw.user_id
|
|
170
|
-
if user_id not in clients:
|
|
171
|
-
return "Failure: id not found, ask the user to check it."
|
|
172
|
-
|
|
173
|
-
results: Optional[str] = None
|
|
174
|
-
|
|
175
|
-
def put_results(result: str) -> None:
|
|
176
|
-
nonlocal results
|
|
177
|
-
results = result
|
|
178
|
-
|
|
179
|
-
gpts[user_id] = put_results
|
|
180
|
-
|
|
181
|
-
await clients[user_id](Mdata(data=reset_wcgw, user_id=user_id))
|
|
182
|
-
|
|
183
|
-
start_time = time.time()
|
|
184
|
-
while time.time() - start_time < 30:
|
|
185
|
-
if results is not None:
|
|
186
|
-
return results
|
|
187
|
-
await asyncio.sleep(0.1)
|
|
188
|
-
|
|
189
|
-
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
190
|
-
|
|
191
|
-
|
|
192
161
|
class CommandWithUUID(BashCommand):
|
|
193
162
|
user_id: UUID
|
|
194
163
|
|
wcgw/types_.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from enum import Enum
|
|
3
2
|
from typing import Any, Literal, Optional, Protocol, Sequence, Union
|
|
4
3
|
|
|
5
4
|
from pydantic import BaseModel as PydanticBaseModel
|
|
@@ -13,16 +12,23 @@ class NoExtraArgs(PydanticBaseModel):
|
|
|
13
12
|
BaseModel = NoExtraArgs
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
wcgw = "wcgw"
|
|
18
|
-
architect = "architect"
|
|
19
|
-
code_writer = "code_writer"
|
|
15
|
+
Modes = Literal["wcgw", "architect", "code_writer"]
|
|
20
16
|
|
|
21
17
|
|
|
22
18
|
class CodeWriterMode(BaseModel):
|
|
23
19
|
allowed_globs: Literal["all"] | list[str]
|
|
24
20
|
allowed_commands: Literal["all"] | list[str]
|
|
25
21
|
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
# Patch frequently wrong output trading off accuracy
|
|
24
|
+
# in rare case there's a file named 'all' or a command named 'all'
|
|
25
|
+
if len(self.allowed_commands) == 1:
|
|
26
|
+
if self.allowed_commands[0] == "all":
|
|
27
|
+
self.allowed_commands = "all"
|
|
28
|
+
if len(self.allowed_globs) == 1:
|
|
29
|
+
if self.allowed_globs[0] == "all":
|
|
30
|
+
self.allowed_globs = "all"
|
|
31
|
+
|
|
26
32
|
def update_relative_globs(self, workspace_root: str) -> None:
|
|
27
33
|
"""Update globs if they're relative paths"""
|
|
28
34
|
if self.allowed_globs != "all":
|
|
@@ -36,6 +42,12 @@ ModesConfig = Union[Literal["wcgw", "architect"], CodeWriterMode]
|
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
class Initialize(BaseModel):
|
|
45
|
+
type: Literal[
|
|
46
|
+
"first_call",
|
|
47
|
+
"user_asked_mode_change",
|
|
48
|
+
"reset_shell",
|
|
49
|
+
"user_asked_change_workspace",
|
|
50
|
+
]
|
|
39
51
|
any_workspace_path: str
|
|
40
52
|
initial_files_to_read: list[str]
|
|
41
53
|
task_id_to_resume: str
|
|
@@ -44,9 +56,9 @@ class Initialize(BaseModel):
|
|
|
44
56
|
|
|
45
57
|
def model_post_init(self, __context: Any) -> None:
|
|
46
58
|
if self.mode_name == "code_writer":
|
|
47
|
-
assert
|
|
48
|
-
|
|
49
|
-
)
|
|
59
|
+
assert (
|
|
60
|
+
self.code_writer_config is not None
|
|
61
|
+
), "code_writer_config can't be null when the mode is code_writer"
|
|
50
62
|
return super().model_post_init(__context)
|
|
51
63
|
|
|
52
64
|
@property
|
|
@@ -55,9 +67,9 @@ class Initialize(BaseModel):
|
|
|
55
67
|
return "wcgw"
|
|
56
68
|
if self.mode_name == "architect":
|
|
57
69
|
return "architect"
|
|
58
|
-
assert
|
|
59
|
-
|
|
60
|
-
)
|
|
70
|
+
assert (
|
|
71
|
+
self.code_writer_config is not None
|
|
72
|
+
), "code_writer_config can't be null when the mode is code_writer"
|
|
61
73
|
return self.code_writer_config
|
|
62
74
|
|
|
63
75
|
|
|
@@ -74,7 +86,7 @@ class SendText(BaseModel):
|
|
|
74
86
|
|
|
75
87
|
|
|
76
88
|
Specials = Literal[
|
|
77
|
-
"Enter", "Key-up", "Key-down", "Key-left", "Key-right", "Ctrl-c", "Ctrl-d"
|
|
89
|
+
"Enter", "Key-up", "Key-down", "Key-left", "Key-right", "Ctrl-c", "Ctrl-d"
|
|
78
90
|
]
|
|
79
91
|
|
|
80
92
|
|
|
@@ -104,13 +116,6 @@ class ReadFiles(BaseModel):
|
|
|
104
116
|
file_paths: list[str]
|
|
105
117
|
|
|
106
118
|
|
|
107
|
-
class ResetWcgw(BaseModel):
|
|
108
|
-
should_reset: Literal[True]
|
|
109
|
-
change_mode: Optional[Literal["wcgw", "architect", "code_writer"]]
|
|
110
|
-
code_writer_config: Optional[CodeWriterMode] = None
|
|
111
|
-
starting_directory: str
|
|
112
|
-
|
|
113
|
-
|
|
114
119
|
class FileEdit(BaseModel):
|
|
115
120
|
file_path: str
|
|
116
121
|
file_edit_using_search_replace_blocks: str
|
|
@@ -133,7 +138,6 @@ class Mdata(PydanticBaseModel):
|
|
|
133
138
|
data: (
|
|
134
139
|
BashCommand
|
|
135
140
|
| WriteIfEmpty
|
|
136
|
-
| ResetWcgw
|
|
137
141
|
| FileEdit
|
|
138
142
|
| str
|
|
139
143
|
| ReadFiles
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
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>
|
|
@@ -43,6 +43,8 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
43
43
|
|
|
44
44
|
## Updates
|
|
45
45
|
|
|
46
|
+
- [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
|
|
47
|
+
|
|
46
48
|
- [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
|
|
47
49
|
|
|
48
50
|
- [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.
|
|
@@ -51,9 +53,6 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
51
53
|
|
|
52
54
|
- [9 Dec 2024] [Vscode extension to paste context on Claude app](https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw)
|
|
53
55
|
|
|
54
|
-
- [01 Dec 2024] Removed author hosted relay server for chatgpt.
|
|
55
|
-
|
|
56
|
-
- [26 Nov 2024] Introduced claude desktop support through mcp
|
|
57
56
|
|
|
58
57
|
## 🚀 Highlights
|
|
59
58
|
|
|
@@ -100,7 +99,7 @@ First install `uv` using homebrew `brew install uv`
|
|
|
100
99
|
|
|
101
100
|
(**Important:** use homebrew to install uv. Otherwise make sure `uv` is present in a global location like /usr/bin/)
|
|
102
101
|
|
|
103
|
-
Then update `claude_desktop_config.json` (~/Library/Application Support/Claude/claude_desktop_config.json)
|
|
102
|
+
Then create or update `claude_desktop_config.json` (~/Library/Application Support/Claude/claude_desktop_config.json) with following json.
|
|
104
103
|
|
|
105
104
|
```json
|
|
106
105
|
{
|
|
@@ -127,6 +126,8 @@ _If there's an error in setting up_
|
|
|
127
126
|
|
|
128
127
|
- If there's an error like "uv ENOENT", make sure `uv` is installed. Then run 'which uv' in the terminal, and use its output in place of "uv" in the configuration.
|
|
129
128
|
- If there's still an issue, check that `uv tool run --from wcgw@latest --python 3.12 wcgw_mcp` runs in your terminal. It should have no output and shouldn't exit.
|
|
129
|
+
- Try removing ~/.cache/uv folder
|
|
130
|
+
- Try using `uv` version `0.6.0` for which this tool was tested.
|
|
130
131
|
- Debug the mcp server using `npx @modelcontextprotocol/inspector@0.1.7 uv tool run --from wcgw@latest --python 3.12 wcgw_mcp`
|
|
131
132
|
|
|
132
133
|
### Alternative configuration using smithery (npx required)
|
|
@@ -139,6 +140,9 @@ Then to configure wcgw for Claude Desktop automatically via [Smithery](https://s
|
|
|
139
140
|
npx -y @smithery/cli install wcgw --client claude
|
|
140
141
|
```
|
|
141
142
|
|
|
143
|
+
_If there's an error in setting up_
|
|
144
|
+
- Try removing ~/.cache/uv folder
|
|
145
|
+
|
|
142
146
|
### Usage
|
|
143
147
|
|
|
144
148
|
Wait for a few seconds. You should be able to see this icon if everything goes right.
|
|
@@ -168,6 +172,21 @@ There are three built-in modes. You may ask Claude to run in one of the modes, l
|
|
|
168
172
|
|
|
169
173
|
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)
|
|
170
174
|
|
|
175
|
+
#### Attach to the working terminal to investigate
|
|
176
|
+
If you've `screen` command installed, wcgw runs on a screen instance automatically. If you've started wcgw mcp server, you can list the screen sessions:
|
|
177
|
+
|
|
178
|
+
`screen -ls`
|
|
179
|
+
|
|
180
|
+
And note down the wcgw screen name which will be something like `93358.wcgw.235521` where the last number is in the hour-minute-second format.
|
|
181
|
+
|
|
182
|
+
You can then attach to the session using `screen -x 93358.wcgw.235521`
|
|
183
|
+
|
|
184
|
+
You may interrupt any running command safely.
|
|
185
|
+
|
|
186
|
+
You can interact with the terminal but beware that the AI might be running in parallel and it may conflict with what you're doing. It's recommended to keep your interactions to minimum.
|
|
187
|
+
|
|
188
|
+
You shouldn't exit the session using `exit `or Ctrl-d, instead you should use `ctrl+a+d` to safely detach without destroying the screen session.
|
|
189
|
+
|
|
171
190
|
### [Optional] Vs code extension
|
|
172
191
|
|
|
173
192
|
https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw
|
|
@@ -216,7 +235,6 @@ The server provides the following MCP tools:
|
|
|
216
235
|
- Parameters: `any_workspace_path` (string), `initial_files_to_read` (string[]), `mode_name` ("wcgw"|"architect"|"code_writer"), `task_id_to_resume` (string)
|
|
217
236
|
- `BashCommand`: Execute shell commands with timeout control
|
|
218
237
|
- Parameters: `command` (string), `wait_for_seconds` (int, optional)
|
|
219
|
-
- `BashInteraction`: Send keyboard input to running programs
|
|
220
238
|
- Parameters: `send_text` (string) or `send_specials` (["Enter"|"Key-up"|...]) or `send_ascii` (int[]), `wait_for_seconds` (int, optional)
|
|
221
239
|
|
|
222
240
|
**File Operations:**
|
|
@@ -234,7 +252,5 @@ The server provides the following MCP tools:
|
|
|
234
252
|
|
|
235
253
|
- `ContextSave`: Save project context and files for Knowledge Transfer or saving task checkpoints to be resumed later
|
|
236
254
|
- Parameters: `id` (string), `project_root_path` (string), `description` (string), `relevant_file_globs` (string[])
|
|
237
|
-
- `ResetShell`: Emergency reset for shell environment
|
|
238
|
-
- Parameters: `should_reset` (boolean)
|
|
239
255
|
|
|
240
256
|
All tools support absolute paths and include built-in protections against common errors. See the [MCP specification](https://modelcontextprotocol.io/) for detailed protocol information.
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
wcgw/__init__.py,sha256=qUofQOAXCGcWr2u_B8U-MIMhhYaBUpUwNDcscvRmYfo,90
|
|
2
2
|
wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
wcgw/types_.py,sha256=
|
|
3
|
+
wcgw/types_.py,sha256=KFyfLFrctU9jr3zoQR3nlAMlP17hu91bM7ZeUTdTZ74,3624
|
|
4
4
|
wcgw/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
wcgw/client/common.py,sha256=OCH7Tx64jojz3M3iONUrGMadE07W21DiZs5sOxWX1Qc,1456
|
|
6
6
|
wcgw/client/diff-instructions.txt,sha256=tmJ9Fu9XdO_72lYXQQNY9RZyx91bjxrXJf9d_KBz57k,1611
|
|
7
7
|
wcgw/client/memory.py,sha256=8LdYsOhvCOoC1kfvDr85kNy07WnhPMvE6B2FRM2w85Y,2902
|
|
8
|
-
wcgw/client/modes.py,sha256=
|
|
9
|
-
wcgw/client/tool_prompts.py,sha256=
|
|
10
|
-
wcgw/client/tools.py,sha256=
|
|
11
|
-
wcgw/client/bash_state/bash_state.py,sha256=
|
|
8
|
+
wcgw/client/modes.py,sha256=FjIQOjT5oI7dk9VG0oRemN1A6EHBvHnuSQGKuRbguJI,10352
|
|
9
|
+
wcgw/client/tool_prompts.py,sha256=RzLO3XGMg2VJfURG1BiQzZ6IdDWXfH2syqM1N3w0770,3980
|
|
10
|
+
wcgw/client/tools.py,sha256=UtVH_Q7E7A6ertdY9yPe5ukwSO2zg1Ojlp3z6MqmURI,24402
|
|
11
|
+
wcgw/client/bash_state/bash_state.py,sha256=DND9vNIfFee-Djif3YZhIsnaXamI3TzXE-AhISnICZY,26476
|
|
12
12
|
wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
|
|
13
13
|
wcgw/client/file_ops/diff_edit.py,sha256=sIwXSSkWYff_Dp3oHfefqtSuFqLrxbhKvzkCoLuLqDE,18679
|
|
14
14
|
wcgw/client/file_ops/search_replace.py,sha256=Napa7IWaYPGMNdttunKyRDkb90elZE7r23B_o_htRxo,5585
|
|
15
15
|
wcgw/client/mcp_server/Readme.md,sha256=2Z88jj1mf9daYGW1CWaldcJ0moy8owDumhR2glBY3A8,109
|
|
16
16
|
wcgw/client/mcp_server/__init__.py,sha256=mm7xhBIPwJpRT3u-Qsj4cKVMpVyucJoKRlbMP_gRRB0,343
|
|
17
|
-
wcgw/client/mcp_server/server.py,sha256=
|
|
17
|
+
wcgw/client/mcp_server/server.py,sha256=ayK6qbzCveoQW7RO80m10cAIS3m-hvxzd15XhjiyxmE,5055
|
|
18
18
|
wcgw/client/repo_ops/display_tree.py,sha256=E5q9mrGBb57NyvudSmRIG-fj4FUqupbzjmARpX8X0XY,4166
|
|
19
19
|
wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9tZ4M,2144
|
|
20
20
|
wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4qcYFY0kqk,315087
|
|
21
21
|
wcgw/client/repo_ops/paths_tokens.model,sha256=jiwwE4ae8ADKuTZISutXuM5Wfyc_FBmN5rxTjoNnCos,1569052
|
|
22
22
|
wcgw/client/repo_ops/repo_context.py,sha256=5NqRxBY0K-SBFXJ0Ybt7llzYOBD8pRkTpruMMJHWxv4,4336
|
|
23
23
|
wcgw/relay/client.py,sha256=BUeEKUsWts8RpYxXwXcyFyjBJhOCS-CxThAlL_-VCOI,3618
|
|
24
|
-
wcgw/relay/serve.py,sha256=
|
|
24
|
+
wcgw/relay/serve.py,sha256=Vl7Nb68-F910LwHrTElfCNwajF37CfgObUIAdwhrRsI,7886
|
|
25
25
|
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
26
26
|
wcgw_cli/__init__.py,sha256=TNxXsTPgb52OhakIda9wTRh91cqoBqgQRx5TxjzQQFU,21
|
|
27
27
|
wcgw_cli/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
28
|
-
wcgw_cli/anthropic_client.py,sha256=
|
|
28
|
+
wcgw_cli/anthropic_client.py,sha256=gQc1opw-N5ecqcORbvvHGBW1Ac-Soe7c7APJD25HIfo,19887
|
|
29
29
|
wcgw_cli/cli.py,sha256=-7FBe_lahKyUOhf65iurTA1M1gXXXAiT0OVKQVcZKKo,948
|
|
30
|
-
wcgw_cli/openai_client.py,sha256=
|
|
30
|
+
wcgw_cli/openai_client.py,sha256=o4vfhcfGKqoWYWnn5YxNw4muKyDnFEiPmWpZpufDGMA,16021
|
|
31
31
|
wcgw_cli/openai_utils.py,sha256=xGOb3W5ALrIozV7oszfGYztpj0FnXdD7jAxm5lEIVKY,2439
|
|
32
32
|
mcp_wcgw/__init__.py,sha256=fKCgOdN7cn7gR3YGFaGyV5Goe8A2sEyllLcsRkN0i-g,2601
|
|
33
33
|
mcp_wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -51,8 +51,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
|
|
|
51
51
|
mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
|
|
52
52
|
mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
|
|
53
53
|
mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
|
|
54
|
-
wcgw-3.0.
|
|
55
|
-
wcgw-3.0.
|
|
56
|
-
wcgw-3.0.
|
|
57
|
-
wcgw-3.0.
|
|
58
|
-
wcgw-3.0.
|
|
54
|
+
wcgw-3.0.2.dist-info/METADATA,sha256=VNXS8-pCPvOPt_Xdgq4UIxp4aeZ69V-7DbIe0WGT7Eo,13858
|
|
55
|
+
wcgw-3.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
56
|
+
wcgw-3.0.2.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
|
|
57
|
+
wcgw-3.0.2.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
58
|
+
wcgw-3.0.2.dist-info/RECORD,,
|
wcgw_cli/anthropic_client.py
CHANGED
|
@@ -22,7 +22,7 @@ from anthropic.types import (
|
|
|
22
22
|
ToolUseBlockParam,
|
|
23
23
|
)
|
|
24
24
|
from dotenv import load_dotenv
|
|
25
|
-
from pydantic import BaseModel
|
|
25
|
+
from pydantic import BaseModel, ValidationError
|
|
26
26
|
from typer import Typer
|
|
27
27
|
|
|
28
28
|
from wcgw.client.bash_state.bash_state import BashState
|
|
@@ -35,7 +35,7 @@ from wcgw.client.tools import (
|
|
|
35
35
|
default_enc,
|
|
36
36
|
get_tool_output,
|
|
37
37
|
initialize,
|
|
38
|
-
|
|
38
|
+
parse_tool_by_name,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
@@ -218,6 +218,7 @@ def loop(
|
|
|
218
218
|
) as bash_state:
|
|
219
219
|
context = Context(bash_state, system_console)
|
|
220
220
|
system, context = initialize(
|
|
221
|
+
"first_call",
|
|
221
222
|
context,
|
|
222
223
|
os.getcwd(),
|
|
223
224
|
[],
|
|
@@ -379,14 +380,6 @@ def loop(
|
|
|
379
380
|
tool_input = str(tc["input"])
|
|
380
381
|
tool_id = str(tc["id"])
|
|
381
382
|
|
|
382
|
-
tool_parsed = which_tool_name(
|
|
383
|
-
tool_name
|
|
384
|
-
).model_validate_json(tool_input)
|
|
385
|
-
|
|
386
|
-
system_console.print(
|
|
387
|
-
f"\n---------------------------------------\n# Assistant invoked tool: {tool_parsed}"
|
|
388
|
-
)
|
|
389
|
-
|
|
390
383
|
_histories.append(
|
|
391
384
|
{
|
|
392
385
|
"role": "assistant",
|
|
@@ -394,12 +387,35 @@ def loop(
|
|
|
394
387
|
ToolUseBlockParam(
|
|
395
388
|
id=tool_id,
|
|
396
389
|
name=tool_name,
|
|
397
|
-
input=
|
|
390
|
+
input=json.loads(tool_input),
|
|
398
391
|
type="tool_use",
|
|
399
392
|
)
|
|
400
393
|
],
|
|
401
394
|
}
|
|
402
395
|
)
|
|
396
|
+
try:
|
|
397
|
+
tool_parsed = parse_tool_by_name(
|
|
398
|
+
tool_name, json.loads(tool_input)
|
|
399
|
+
)
|
|
400
|
+
except ValidationError:
|
|
401
|
+
error_msg = f"Error parsing tool {tool_name}\n{traceback.format_exc()}"
|
|
402
|
+
system_console.log(
|
|
403
|
+
f"Error parsing tool {tool_name}"
|
|
404
|
+
)
|
|
405
|
+
tool_results.append(
|
|
406
|
+
ToolResultBlockParam(
|
|
407
|
+
type="tool_result",
|
|
408
|
+
tool_use_id=str(tc["id"]),
|
|
409
|
+
content=error_msg,
|
|
410
|
+
is_error=True,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
system_console.print(
|
|
416
|
+
f"\n---------------------------------------\n# Assistant invoked tool: {tool_parsed}"
|
|
417
|
+
)
|
|
418
|
+
|
|
403
419
|
try:
|
|
404
420
|
output_or_dones, _ = get_tool_output(
|
|
405
421
|
context,
|
wcgw_cli/openai_client.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|