wcgw 3.0.1rc1__py3-none-any.whl → 3.0.1rc3__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.

@@ -6,7 +6,15 @@ import threading
6
6
  import time
7
7
  import traceback
8
8
  from dataclasses import dataclass
9
- from typing import Any, Literal, Optional
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Concatenate,
13
+ Literal,
14
+ Optional,
15
+ ParamSpec,
16
+ TypeVar,
17
+ )
10
18
 
11
19
  import pexpect
12
20
  import pyte
@@ -24,7 +32,8 @@ from ...types_ import (
24
32
  from ..encoder import EncoderDecoder
25
33
  from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
26
34
 
27
- PROMPT_CONST = "#" + "@wcgw@#"
35
+ PROMPT_CONST = "wcgw→" + " "
36
+ PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
28
37
  BASH_CLF_OUTPUT = Literal["repl", "pending"]
29
38
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
30
39
 
@@ -63,7 +72,7 @@ def get_tmpdir() -> str:
63
72
  timeout=CONFIG.timeout,
64
73
  ).strip()
65
74
  return result
66
- except subprocess.CalledProcessError:
75
+ except (subprocess.CalledProcessError, FileNotFoundError):
67
76
  return "//tmp"
68
77
  except Exception:
69
78
  return ""
@@ -71,9 +80,7 @@ def get_tmpdir() -> str:
71
80
 
72
81
  def check_if_screen_command_available() -> bool:
73
82
  try:
74
- subprocess.run(
75
- ["screen", "-v"], capture_output=True, check=True, timeout=CONFIG.timeout
76
- )
83
+ subprocess.run(["screen", "-v"], capture_output=True, check=True, timeout=0.2)
77
84
  return True
78
85
  except (subprocess.CalledProcessError, FileNotFoundError):
79
86
  return False
@@ -92,12 +99,14 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
92
99
  capture_output=True,
93
100
  text=True,
94
101
  check=True,
95
- timeout=CONFIG.timeout,
102
+ timeout=0.2,
96
103
  )
97
104
  output = result.stdout
98
105
  except subprocess.CalledProcessError as e:
99
106
  # When no screens exist, screen may return a non-zero exit code.
100
107
  output = (e.stdout or "") + (e.stderr or "")
108
+ except FileNotFoundError:
109
+ return
101
110
 
102
111
  sessions_to_kill = []
103
112
 
@@ -121,7 +130,7 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
121
130
  check=True,
122
131
  timeout=CONFIG.timeout,
123
132
  )
124
- except subprocess.CalledProcessError:
133
+ except (subprocess.CalledProcessError, FileNotFoundError):
125
134
  console.log(f"Failed to kill screen session: {session}")
126
135
 
127
136
 
@@ -136,23 +145,21 @@ def start_shell(
136
145
  **os.environ,
137
146
  "PS1": PROMPT_CONST,
138
147
  "TMPDIR": get_tmpdir(),
139
- "TERM": "vt100",
148
+ "TERM": "xterm-256color",
140
149
  }
141
150
  try:
142
151
  shell = pexpect.spawn(
143
152
  cmd,
144
153
  env=overrideenv, # type: ignore[arg-type]
145
- echo=False,
154
+ echo=True,
146
155
  encoding="utf-8",
147
156
  timeout=CONFIG.timeout,
148
157
  cwd=initial_dir,
149
158
  codec_errors="backslashreplace",
150
159
  dimensions=(500, 160),
151
160
  )
152
- shell.sendline(
153
- f"export PROMPT_COMMAND= PS1={PROMPT_CONST}"
154
- ) # Unset prompt command to avoid interfering
155
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
161
+ shell.sendline(PROMPT_STATEMENT) # Unset prompt command to avoid interfering
162
+ shell.expect(PROMPT_CONST, timeout=0.2)
156
163
  except Exception as e:
157
164
  console.print(traceback.format_exc())
158
165
  console.log(f"Error starting shell: {e}. Retrying without rc ...")
@@ -160,13 +167,13 @@ def start_shell(
160
167
  shell = pexpect.spawn(
161
168
  "/bin/bash --noprofile --norc",
162
169
  env=overrideenv, # type: ignore[arg-type]
163
- echo=False,
170
+ echo=True,
164
171
  encoding="utf-8",
165
172
  timeout=CONFIG.timeout,
166
173
  codec_errors="backslashreplace",
167
174
  )
168
- shell.sendline(f"export PS1={PROMPT_CONST}")
169
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
175
+ shell.sendline(PROMPT_STATEMENT)
176
+ shell.expect(PROMPT_CONST, timeout=0.2)
170
177
 
171
178
  shellid = "wcgw." + time.strftime("%H%M%S")
172
179
  if over_screen:
@@ -174,22 +181,11 @@ def start_shell(
174
181
  raise ValueError("Screen command not available")
175
182
  # shellid is just hour, minute, second number
176
183
  shell.sendline(f"trap 'screen -X -S {shellid} quit' EXIT")
177
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
184
+ shell.expect(PROMPT_CONST, timeout=0.2)
178
185
 
179
- shell.sendline(f"screen -q -s /bin/bash -S {shellid}")
186
+ shell.sendline(f"screen -q -S {shellid} /bin/bash --noprofile --norc")
180
187
  shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
181
188
 
182
- console.log(f"Entering screen session, name: {shellid}")
183
-
184
- shell.sendline("stty -icanon -echo")
185
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
186
-
187
- shell.sendline("set +o pipefail")
188
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
189
-
190
- shell.sendline("export GIT_PAGER=cat PAGER=cat")
191
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
192
-
193
189
  return shell, shellid
194
190
 
195
191
 
@@ -217,7 +213,39 @@ def render_terminal_output(text: str) -> list[str]:
217
213
  return lines
218
214
 
219
215
 
216
+ P = ParamSpec("P")
217
+ R = TypeVar("R")
218
+
219
+
220
+ def requires_shell(
221
+ func: Callable[Concatenate["BashState", "pexpect.spawn[str]", P], R],
222
+ ) -> Callable[Concatenate["BashState", P], R]:
223
+ def wrapper(self: "BashState", /, *args: P.args, **kwargs: P.kwargs) -> R:
224
+ if not self._shell_loading.is_set():
225
+ if not self._shell_loading.wait(
226
+ timeout=CONFIG.timeout * 2
227
+ ): # Twice in worst case if screen fails
228
+ raise RuntimeError("Shell initialization timeout")
229
+
230
+ if self._shell_error:
231
+ raise RuntimeError(f"Shell failed to initialize: {self._shell_error}.")
232
+
233
+ if not self._shell:
234
+ raise RuntimeError("Shell not initialized")
235
+
236
+ return func(self, self._shell, *args, **kwargs)
237
+
238
+ return wrapper
239
+
240
+
220
241
  class BashState:
242
+ _shell: Optional["pexpect.spawn[str]"]
243
+ _shell_id: Optional[str]
244
+ _shell_lock: threading.Lock
245
+ _shell_loading: threading.Event
246
+ _shell_error: Optional[Exception]
247
+ _use_screen: bool
248
+
221
249
  def __init__(
222
250
  self,
223
251
  console: Console,
@@ -240,35 +268,65 @@ class BashState:
240
268
  )
241
269
  self._mode = mode or Modes.wcgw
242
270
  self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
243
- self._prompt = PROMPT_CONST
244
271
  self._bg_expect_thread: Optional[threading.Thread] = None
245
272
  self._bg_expect_thread_stop_event = threading.Event()
246
- self._init_shell(use_screen)
247
-
248
- def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
273
+ self._shell = None
274
+ self._shell_id = None
275
+ self._shell_lock = threading.Lock()
276
+ self._shell_loading = threading.Event()
277
+ self._shell_error = None
278
+ self._use_screen = use_screen
279
+ self._start_shell_loading()
280
+
281
+ def _start_shell_loading(self) -> None:
282
+ def load_shell() -> None:
283
+ try:
284
+ with self._shell_lock:
285
+ if self._shell is not None:
286
+ return
287
+ self._init_shell()
288
+ self.run_bg_expect_thread()
289
+ except Exception as e:
290
+ self._shell_error = e
291
+ finally:
292
+ self._shell_loading.set()
293
+
294
+ threading.Thread(target=load_shell).start()
295
+
296
+ @requires_shell
297
+ def expect(
298
+ self, shell: "pexpect.spawn[str]", pattern: Any, timeout: Optional[float] = -1
299
+ ) -> int:
249
300
  self.close_bg_expect_thread()
250
- return self._shell.expect(pattern, timeout)
301
+ output = shell.expect(pattern, timeout)
302
+ return output
251
303
 
252
- def send(self, s: str | bytes) -> int:
253
- output = self._shell.send(s)
254
- self.run_bg_expect_thread()
304
+ @requires_shell
305
+ def send(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
306
+ self.close_bg_expect_thread()
307
+ output = shell.send(s)
255
308
  return output
256
309
 
257
- def sendline(self, s: str | bytes) -> int:
258
- output = self._shell.sendline(s)
259
- self.run_bg_expect_thread()
310
+ @requires_shell
311
+ def sendline(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
312
+ self.close_bg_expect_thread()
313
+ output = shell.sendline(s)
260
314
  return output
261
315
 
262
316
  @property
263
- def linesep(self) -> Any:
264
- return self._shell.linesep
317
+ @requires_shell
318
+ def linesep(self, shell: "pexpect.spawn[str]") -> Any:
319
+ return shell.linesep
265
320
 
266
- def sendintr(self) -> None:
267
- self._shell.sendintr()
321
+ @requires_shell
322
+ def sendintr(self, shell: "pexpect.spawn[str]") -> None:
323
+ self.close_bg_expect_thread()
324
+ shell.sendintr()
268
325
 
269
326
  @property
270
- def before(self) -> Optional[str]:
271
- return self._shell.before
327
+ @requires_shell
328
+ def before(self, shell: "pexpect.spawn[str]") -> Optional[str]:
329
+ return shell.before
272
330
 
273
331
  def run_bg_expect_thread(self) -> None:
274
332
  """
@@ -279,11 +337,16 @@ class BashState:
279
337
  while True:
280
338
  if self._bg_expect_thread_stop_event.is_set():
281
339
  break
340
+ if self._shell is None:
341
+ time.sleep(0.1)
342
+ continue
282
343
  output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
283
344
  if output == 0:
284
345
  break
285
346
 
286
- if self._bg_expect_thread:
347
+ if self._bg_expect_thread and self._bg_expect_thread.is_alive():
348
+ if not self._bg_expect_thread_stop_event.is_set():
349
+ return
287
350
  self.close_bg_expect_thread()
288
351
 
289
352
  self._bg_expect_thread = threading.Thread(
@@ -300,8 +363,13 @@ class BashState:
300
363
 
301
364
  def cleanup(self) -> None:
302
365
  self.close_bg_expect_thread()
303
- self._shell.close(True)
304
- cleanup_all_screens_with_name(self._shell_id, self.console)
366
+ with self._shell_lock:
367
+ if self._shell:
368
+ self._shell.close(True)
369
+ if self._shell_id:
370
+ cleanup_all_screens_with_name(self._shell_id, self.console)
371
+ self._shell = None
372
+ self._shell_id = None
305
373
 
306
374
  def __enter__(self) -> "BashState":
307
375
  return self
@@ -325,26 +393,26 @@ class BashState:
325
393
  def write_if_empty_mode(self) -> WriteIfEmptyMode:
326
394
  return self._write_if_empty_mode
327
395
 
328
- def ensure_env_and_bg_jobs(self) -> Optional[int]:
329
- if self._prompt != PROMPT_CONST:
330
- return None
396
+ @requires_shell
397
+ def ensure_env_and_bg_jobs(self, _: "pexpect.spawn[str]") -> Optional[int]:
398
+ return self._ensure_env_and_bg_jobs()
399
+
400
+ def _ensure_env_and_bg_jobs(self) -> Optional[int]:
401
+ # Do not add @requires_shell decorator here, as it will cause deadlock
402
+ self.close_bg_expect_thread()
403
+ assert self._shell is not None, "Bad state, shell is not initialized"
331
404
  quick_timeout = 0.2 if not self.over_screen else 1
332
405
  # First reset the prompt in case venv was sourced or other reasons.
333
- self.sendline(f"export PS1={self._prompt}")
334
- self.expect(self._prompt, timeout=quick_timeout)
406
+ self._shell.sendline(PROMPT_STATEMENT)
407
+ self._shell.expect(PROMPT_CONST, timeout=quick_timeout)
335
408
  # Reset echo also if it was enabled
336
- self.sendline("stty -icanon -echo")
337
- self.expect(self._prompt, timeout=quick_timeout)
338
- self.sendline("set +o pipefail")
339
- self.expect(self._prompt, timeout=quick_timeout)
340
- self.sendline("export GIT_PAGER=cat PAGER=cat")
341
- self.expect(self._prompt, timeout=quick_timeout)
342
- self.sendline("jobs | wc -l")
409
+ command = "jobs | wc -l"
410
+ self._shell.sendline(command)
343
411
  before = ""
344
412
  counts = 0
345
413
  while not _is_int(before): # Consume all previous output
346
414
  try:
347
- self.expect(self._prompt, timeout=quick_timeout)
415
+ self._shell.expect(PROMPT_CONST, timeout=quick_timeout)
348
416
  except pexpect.TIMEOUT:
349
417
  self.console.print(f"Couldn't get exit code, before: {before}")
350
418
  raise
@@ -354,20 +422,18 @@ class BashState:
354
422
  before_val = str(before_val)
355
423
  assert isinstance(before_val, str)
356
424
  before_lines = render_terminal_output(before_val)
357
- before = "\n".join(before_lines).strip()
425
+ before = "\n".join(before_lines).replace(command, "").strip()
358
426
  counts += 1
359
427
  if counts > 100:
360
428
  raise ValueError(
361
429
  "Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
362
430
  )
363
-
364
431
  try:
365
432
  return int(before)
366
433
  except ValueError:
367
434
  raise ValueError(f"Malformed output: {before}")
368
435
 
369
- def _init_shell(self, use_screen: bool) -> None:
370
- self._prompt = PROMPT_CONST
436
+ def _init_shell(self) -> None:
371
437
  self._state: Literal["repl"] | datetime.datetime = "repl"
372
438
  self._is_in_docker: Optional[str] = ""
373
439
  # Ensure self._cwd exists
@@ -377,9 +443,9 @@ class BashState:
377
443
  self._bash_command_mode.bash_mode == "restricted_mode",
378
444
  self._cwd,
379
445
  self.console,
380
- over_screen=use_screen,
446
+ over_screen=self._use_screen,
381
447
  )
382
- self.over_screen = use_screen
448
+ self.over_screen = self._use_screen
383
449
  except Exception as e:
384
450
  if not isinstance(e, ValueError):
385
451
  self.console.log(traceback.format_exc())
@@ -394,15 +460,15 @@ class BashState:
394
460
  self.over_screen = False
395
461
 
396
462
  self._pending_output = ""
397
-
398
- # Get exit info to ensure shell is ready
399
- self.ensure_env_and_bg_jobs()
463
+ try:
464
+ self._ensure_env_and_bg_jobs()
465
+ except ValueError as e:
466
+ self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
400
467
 
401
468
  def set_pending(self, last_pending_output: str) -> None:
402
469
  if not isinstance(self._state, datetime.datetime):
403
470
  self._state = datetime.datetime.now()
404
471
  self._pending_output = last_pending_output
405
- self.run_bg_expect_thread()
406
472
 
407
473
  def set_repl(self) -> None:
408
474
  self._state = "repl"
@@ -427,12 +493,13 @@ class BashState:
427
493
 
428
494
  @property
429
495
  def prompt(self) -> str:
430
- return self._prompt
496
+ return PROMPT_CONST
431
497
 
432
- def update_cwd(self) -> str:
433
- self.sendline("pwd")
434
- self.expect(self._prompt, timeout=0.2)
435
- before_val = self._shell.before
498
+ @requires_shell
499
+ def update_cwd(self, shell: "pexpect.spawn[str]") -> str:
500
+ shell.sendline("pwd")
501
+ shell.expect(PROMPT_CONST, timeout=0.2)
502
+ before_val = shell.before
436
503
  if not isinstance(before_val, str):
437
504
  before_val = str(before_val)
438
505
  before_lines = render_terminal_output(before_val)
@@ -442,7 +509,9 @@ class BashState:
442
509
 
443
510
  def reset_shell(self) -> None:
444
511
  self.cleanup()
445
- self._init_shell(True)
512
+ self._shell_loading.clear()
513
+ self._shell_error = None
514
+ self._start_shell_loading()
446
515
 
447
516
  def serialize(self) -> dict[str, Any]:
448
517
  """Serialize BashState to a dictionary for saving"""
@@ -476,6 +545,16 @@ class BashState:
476
545
  cwd: str,
477
546
  ) -> None:
478
547
  """Create a new BashState instance from a serialized state dictionary"""
548
+ if (
549
+ self._bash_command_mode == bash_command_mode
550
+ and ((self._cwd == cwd) or not cwd)
551
+ and (self._file_edit_mode == file_edit_mode)
552
+ and (self._write_if_empty_mode == write_if_empty_mode)
553
+ and (self._mode == mode)
554
+ and (self._whitelist_for_overwrite == set(whitelist_for_overwrite))
555
+ ):
556
+ # No need to reset shell if the state is the same
557
+ return
479
558
  self._bash_command_mode = bash_command_mode
480
559
  self._cwd = cwd or self._cwd
481
560
  self._file_edit_mode = file_edit_mode
@@ -546,9 +625,8 @@ def _incremental_text(text: str, last_pending_output: str) -> str:
546
625
  # text = render_terminal_output(text[-100_000:])
547
626
  text = text[-100_000:]
548
627
 
549
- last_pending_output_rendered_lines = render_terminal_output(last_pending_output)
550
- last_pending_output_rendered = "\n".join(last_pending_output_rendered_lines)
551
- last_rendered_lines = last_pending_output_rendered.split("\n")
628
+ last_rendered_lines = render_terminal_output(last_pending_output)
629
+ last_pending_output_rendered = "\n".join(last_rendered_lines)
552
630
  if not last_rendered_lines:
553
631
  return rstrip(render_terminal_output(text))
554
632
 
@@ -599,6 +677,27 @@ def execute_bash(
599
677
  bash_arg: BashCommand,
600
678
  max_tokens: Optional[int],
601
679
  timeout_s: Optional[float],
680
+ ) -> tuple[str, float]:
681
+ try:
682
+ output, cost = _execute_bash(bash_state, enc, bash_arg, max_tokens, timeout_s)
683
+
684
+ # Remove echo if it's a command
685
+ if isinstance(bash_arg.action, Command):
686
+ command = bash_arg.action.command.strip()
687
+ if output.startswith(command):
688
+ output = output[len(command) :]
689
+
690
+ finally:
691
+ bash_state.run_bg_expect_thread()
692
+ return output, cost
693
+
694
+
695
+ def _execute_bash(
696
+ bash_state: BashState,
697
+ enc: EncoderDecoder[int],
698
+ bash_arg: BashCommand,
699
+ max_tokens: Optional[int],
700
+ timeout_s: Optional[float],
602
701
  ) -> tuple[str, float]:
603
702
  try:
604
703
  is_interrupt = False
@@ -102,6 +102,8 @@ def line_process_max_space_tolerance(line: str) -> str:
102
102
  return re.sub(r"\s", "", line)
103
103
 
104
104
 
105
+ REMOVE_INDENTATION = "Warning: matching after removing all spaces in lines."
106
+
105
107
  DEFAULT_TOLERANCES = [
106
108
  Tolerance(
107
109
  line_process=str.rstrip,
@@ -119,11 +121,50 @@ DEFAULT_TOLERANCES = [
119
121
  line_process=line_process_max_space_tolerance,
120
122
  severity_cat="WARNING",
121
123
  score_multiplier=50,
122
- error_name="Warning: matching after removing all spaces in lines.",
124
+ error_name=REMOVE_INDENTATION,
123
125
  ),
124
126
  ]
125
127
 
126
128
 
129
+ def fix_indentation(
130
+ matched_lines: list[str], searched_lines: list[str], replaced_lines: list[str]
131
+ ) -> list[str]:
132
+ if not matched_lines or not searched_lines or not replaced_lines:
133
+ return replaced_lines
134
+
135
+ def get_indentation(line: str) -> str:
136
+ match = re.match(r"^(\s*)", line)
137
+ assert match
138
+ return match.group(0)
139
+
140
+ matched_indents = [get_indentation(line) for line in matched_lines if line.strip()]
141
+ searched_indents = [
142
+ get_indentation(line) for line in searched_lines if line.strip()
143
+ ]
144
+ if len(matched_indents) != len(searched_indents):
145
+ return replaced_lines
146
+ diffs: list[int] = [
147
+ len(searched) - len(matched)
148
+ for matched, searched in zip(matched_indents, searched_indents)
149
+ ]
150
+ if not all(diff == diffs[0] for diff in diffs):
151
+ return replaced_lines
152
+ if diffs[0] == 0:
153
+ return replaced_lines
154
+
155
+ # At this point we have same number of non-empty lines and the same indentation difference
156
+ # We can now adjust the indentation of the replaced lines
157
+ def adjust_indentation(line: str, diff: int) -> str:
158
+ if diff < 0:
159
+ return matched_indents[0][:-diff] + line
160
+ return line[diff:]
161
+
162
+ if diffs[0] > 0:
163
+ if not (all(not line[: diffs[0]].strip() for line in replaced_lines)):
164
+ return replaced_lines
165
+ return [adjust_indentation(line, diffs[0]) for line in replaced_lines]
166
+
167
+
127
168
  def remove_leading_trailing_empty_lines(lines: list[str]) -> list[str]:
128
169
  start = 0
129
170
  end = len(lines) - 1
@@ -247,6 +288,16 @@ class FileEditInput:
247
288
  ]
248
289
 
249
290
  for match, tolerances in matches_with_tolerances:
291
+ if any(
292
+ tolerance.error_name == REMOVE_INDENTATION
293
+ for tolerance in tolerances
294
+ ):
295
+ replace_by = fix_indentation(
296
+ self.file_lines[match.start : match.stop],
297
+ first_block[0],
298
+ replace_by,
299
+ )
300
+
250
301
  file_edit_input = FileEditInput(
251
302
  self.file_lines,
252
303
  match.stop,
@@ -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, ValidationError
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 Context, default_enc, get_tool_output, which_tool_name
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
@@ -165,7 +157,7 @@ async def main() -> None:
165
157
  version = str(importlib.metadata.version("wcgw"))
166
158
  home_dir = os.path.expanduser("~")
167
159
  with BashState(
168
- Console(), home_dir, None, None, None, None, False, None
160
+ Console(), home_dir, None, None, None, None, True, None
169
161
  ) as BASH_STATE:
170
162
  BASH_STATE.console.log("wcgw version: " + version)
171
163
  # Run the server using stdin/stdout streams
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
@@ -206,6 +207,8 @@ Initialized in directory (also cwd): {context.bash_state.cwd}
206
207
  {memory}
207
208
  """
208
209
 
210
+ global INITIALIZED
211
+ INITIALIZED = True
209
212
  return output, context
210
213
 
211
214
 
@@ -257,6 +260,8 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
257
260
  list(context.bash_state.whitelist_for_overwrite),
258
261
  reset_wcgw.starting_directory,
259
262
  )
263
+ global INITIALIZED
264
+ INITIALIZED = True
260
265
  return "Reset successful" + get_status(context.bash_state)
261
266
 
262
267
 
@@ -545,6 +550,23 @@ def which_tool_name(name: str) -> Type[TOOLS]:
545
550
  raise ValueError(f"Unknown tool name: {name}")
546
551
 
547
552
 
553
+ def parse_tool_by_name(name: str, arguments: dict[str, Any]) -> TOOLS:
554
+ tool_type = which_tool_name(name)
555
+ try:
556
+ return tool_type(**arguments)
557
+ except ValidationError:
558
+
559
+ def try_json(x: str) -> Any:
560
+ if not isinstance(x, str):
561
+ return x
562
+ try:
563
+ return json.loads(x)
564
+ except json.JSONDecodeError:
565
+ return x
566
+
567
+ return tool_type(**{k: try_json(v) for k, v in arguments.items()})
568
+
569
+
548
570
  TOOL_CALLS: list[TOOLS] = []
549
571
 
550
572
 
@@ -595,7 +617,6 @@ def get_tool_output(
595
617
  context.console.print("Calling reset wcgw tool")
596
618
  output = reset_wcgw(context, arg), 0.0
597
619
 
598
- INITIALIZED = True
599
620
  elif isinstance(arg, Initialize):
600
621
  context.console.print("Calling initial info tool")
601
622
  output_, context = initialize(
@@ -608,7 +629,6 @@ def get_tool_output(
608
629
  )
609
630
  output = output_, 0.0
610
631
 
611
- INITIALIZED = True
612
632
  elif isinstance(arg, ContextSave):
613
633
  context.console.print("Calling task memory tool")
614
634
  relevant_files = []
wcgw/relay/client.py CHANGED
@@ -25,7 +25,7 @@ def register_client(server_url: str, client_uuid: str = "") -> None:
25
25
  # Create the WebSocket connection and context
26
26
  the_console = rich.console.Console(style="magenta", highlight=False, markup=False)
27
27
  with BashState(
28
- the_console, os.getcwd(), None, None, None, None, False, None
28
+ the_console, os.getcwd(), None, None, None, None, True, None
29
29
  ) as bash_state:
30
30
  context = Context(bash_state=bash_state, console=the_console)
31
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 3.0.1rc1
3
+ Version: 3.0.1rc3
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>
@@ -7,27 +7,27 @@ wcgw/client/diff-instructions.txt,sha256=tmJ9Fu9XdO_72lYXQQNY9RZyx91bjxrXJf9d_KB
7
7
  wcgw/client/memory.py,sha256=8LdYsOhvCOoC1kfvDr85kNy07WnhPMvE6B2FRM2w85Y,2902
8
8
  wcgw/client/modes.py,sha256=dBkiMNQevTFNkhplrDsDuSIyeU-wLmAIfAa9Dqowvk8,10387
9
9
  wcgw/client/tool_prompts.py,sha256=j82aYnlr1pcinsOYitSrlyYz4-9K3KcqnAfGqot21NQ,4158
10
- wcgw/client/tools.py,sha256=djXlF83gp07nFl-jmYaufrxiSVCI1aoEyGLs9RBgRRs,22622
11
- wcgw/client/bash_state/bash_state.py,sha256=IG2IdGhMEYyDsH7MUhu4pvlfWtIDAeXv4yuAs-CZ1GY,26184
10
+ wcgw/client/tools.py,sha256=caj10NHeK5tZ5Gi0L_oXzPcKBycgXeNaLEbXAzYTpqo,23170
11
+ wcgw/client/bash_state/bash_state.py,sha256=2cxzYN6x-vEJ4oasrCgoRyVQKpARKONcaNDLfLPDTG8,29476
12
12
  wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
13
- wcgw/client/file_ops/diff_edit.py,sha256=OlJCpPSE_3T41q9H0yDORm6trjm3w6zh1EkuPTxik2A,16832
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=3IEZqKNCiJl8J7Ibcksy4nlwW7yNtCnRK6RyE-jPuTo,5372
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
- wcgw/relay/client.py,sha256=HU3gzVaYRRiCZedQTEtV-iN8JI--A_St2hTbeOYOFOw,3619
23
+ wcgw/relay/client.py,sha256=BUeEKUsWts8RpYxXwXcyFyjBJhOCS-CxThAlL_-VCOI,3618
24
24
  wcgw/relay/serve.py,sha256=Ofq6PjW3zVVA2-9MVviGRiUESTD3sXb-482Q4RV13q8,8664
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=sRpu9L9yhYgk-XPkiUDPkxUqeB5jX28qBQPQ6o5YSaQ,18968
28
+ wcgw_cli/anthropic_client.py,sha256=2QLFBLbMeQuixF7Pz9j_hINHTG1CF9IYQZeri7zFuF0,18964
29
29
  wcgw_cli/cli.py,sha256=-7FBe_lahKyUOhf65iurTA1M1gXXXAiT0OVKQVcZKKo,948
30
- wcgw_cli/openai_client.py,sha256=sVIJ0VEe--TTV3M2uoWMMJVZltiXCXxJaQD6AF0OraM,15996
30
+ wcgw_cli/openai_client.py,sha256=oMFAaOkvXQtOY7choylVRJfaF2SnWvRc02ygQhlhVqY,15995
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.1rc1.dist-info/METADATA,sha256=G7TKicqyb4Lo8_djHNi9Fm2PQ1w_DYBDY3BvrIazTvY,13005
55
- wcgw-3.0.1rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
- wcgw-3.0.1rc1.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
57
- wcgw-3.0.1rc1.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
58
- wcgw-3.0.1rc1.dist-info/RECORD,,
54
+ wcgw-3.0.1rc3.dist-info/METADATA,sha256=ymiydLYH56IAV_8FKq98BrGf-tt7Vwumtc8nUBvzs50,13005
55
+ wcgw-3.0.1rc3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
+ wcgw-3.0.1rc3.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
57
+ wcgw-3.0.1rc3.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
58
+ wcgw-3.0.1rc3.dist-info/RECORD,,
@@ -35,7 +35,7 @@ from wcgw.client.tools import (
35
35
  default_enc,
36
36
  get_tool_output,
37
37
  initialize,
38
- which_tool_name,
38
+ parse_tool_by_name,
39
39
  )
40
40
 
41
41
 
@@ -214,10 +214,9 @@ def loop(
214
214
  )
215
215
 
216
216
  with BashState(
217
- system_console, os.getcwd(), None, None, None, None, False, None
217
+ system_console, os.getcwd(), None, None, None, None, True, None
218
218
  ) as bash_state:
219
219
  context = Context(bash_state, system_console)
220
-
221
220
  system, context = initialize(
222
221
  context,
223
222
  os.getcwd(),
@@ -380,9 +379,9 @@ def loop(
380
379
  tool_input = str(tc["input"])
381
380
  tool_id = str(tc["id"])
382
381
 
383
- tool_parsed = which_tool_name(
384
- tool_name
385
- ).model_validate_json(tool_input)
382
+ tool_parsed = parse_tool_by_name(
383
+ tool_name, json.loads(tool_input)
384
+ )
386
385
 
387
386
  system_console.print(
388
387
  f"\n---------------------------------------\n# Assistant invoked tool: {tool_parsed}"
wcgw_cli/openai_client.py CHANGED
@@ -178,7 +178,7 @@ def loop(
178
178
  )
179
179
 
180
180
  with BashState(
181
- system_console, os.getcwd(), None, None, None, None, False, None
181
+ system_console, os.getcwd(), None, None, None, None, True, None
182
182
  ) as bash_state:
183
183
  context = Context(bash_state, system_console)
184
184
  system, context = initialize(