wcgw 5.1.1__tar.gz → 5.1.3__tar.gz

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.

Files changed (59) hide show
  1. {wcgw-5.1.1 → wcgw-5.1.3}/PKG-INFO +1 -1
  2. {wcgw-5.1.1 → wcgw-5.1.3}/pyproject.toml +1 -1
  3. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/bash_state/bash_state.py +39 -37
  4. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/tool_prompts.py +2 -4
  5. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/tools.py +61 -56
  6. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/types_.py +12 -7
  7. {wcgw-5.1.1 → wcgw-5.1.3}/tests/test_edit.py +35 -29
  8. {wcgw-5.1.1 → wcgw-5.1.3}/tests/test_mcp_server.py +8 -4
  9. {wcgw-5.1.1 → wcgw-5.1.3}/tests/test_tools.py +171 -75
  10. {wcgw-5.1.1 → wcgw-5.1.3}/uv.lock +1 -1
  11. {wcgw-5.1.1 → wcgw-5.1.3}/.github/workflows/python-publish.yml +0 -0
  12. {wcgw-5.1.1 → wcgw-5.1.3}/.github/workflows/python-tests.yml +0 -0
  13. {wcgw-5.1.1 → wcgw-5.1.3}/.github/workflows/python-types.yml +0 -0
  14. {wcgw-5.1.1 → wcgw-5.1.3}/.gitignore +0 -0
  15. {wcgw-5.1.1 → wcgw-5.1.3}/.gitmodules +0 -0
  16. {wcgw-5.1.1 → wcgw-5.1.3}/.python-version +0 -0
  17. {wcgw-5.1.1 → wcgw-5.1.3}/.vscode/settings.json +0 -0
  18. {wcgw-5.1.1 → wcgw-5.1.3}/CLAUDE.md +0 -0
  19. {wcgw-5.1.1 → wcgw-5.1.3}/Dockerfile +0 -0
  20. {wcgw-5.1.1 → wcgw-5.1.3}/LICENSE +0 -0
  21. {wcgw-5.1.1 → wcgw-5.1.3}/README.md +0 -0
  22. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/__init__.py +0 -0
  23. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/__init__.py +0 -0
  24. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/bash_state/parser/__init__.py +0 -0
  25. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/bash_state/parser/bash_statement_parser.py +0 -0
  26. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/common.py +0 -0
  27. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/diff-instructions.txt +0 -0
  28. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/encoder/__init__.py +0 -0
  29. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/file_ops/diff_edit.py +0 -0
  30. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/file_ops/extensions.py +0 -0
  31. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/file_ops/search_replace.py +0 -0
  32. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/mcp_server/Readme.md +0 -0
  33. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/mcp_server/__init__.py +0 -0
  34. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/mcp_server/server.py +0 -0
  35. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/memory.py +0 -0
  36. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/modes.py +0 -0
  37. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/repo_ops/display_tree.py +0 -0
  38. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/repo_ops/file_stats.py +0 -0
  39. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/repo_ops/path_prob.py +0 -0
  40. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
  41. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
  42. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/client/repo_ops/repo_context.py +0 -0
  43. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw/py.typed +0 -0
  44. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw_cli/__init__.py +0 -0
  45. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw_cli/__main__.py +0 -0
  46. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw_cli/anthropic_client.py +0 -0
  47. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw_cli/cli.py +0 -0
  48. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw_cli/openai_client.py +0 -0
  49. {wcgw-5.1.1 → wcgw-5.1.3}/src/wcgw_cli/openai_utils.py +0 -0
  50. {wcgw-5.1.1 → wcgw-5.1.3}/static/claude-ss.jpg +0 -0
  51. {wcgw-5.1.1 → wcgw-5.1.3}/static/computer-use.jpg +0 -0
  52. {wcgw-5.1.1 → wcgw-5.1.3}/static/example.jpg +0 -0
  53. {wcgw-5.1.1 → wcgw-5.1.3}/static/rocket-icon.png +0 -0
  54. {wcgw-5.1.1 → wcgw-5.1.3}/static/ss1.png +0 -0
  55. {wcgw-5.1.1 → wcgw-5.1.3}/static/workflow-demo.gif +0 -0
  56. {wcgw-5.1.1 → wcgw-5.1.3}/tests/test_bash_parser.py +0 -0
  57. {wcgw-5.1.1 → wcgw-5.1.3}/tests/test_bash_parser_complex.py +0 -0
  58. {wcgw-5.1.1 → wcgw-5.1.3}/tests/test_file_range_tracking.py +0 -0
  59. {wcgw-5.1.1 → wcgw-5.1.3}/tests/test_readfiles.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 5.1.1
3
+ Version: 5.1.3
4
4
  Summary: Shell and coding agent for Claude and other mcp clients
5
5
  Project-URL: Homepage, https://github.com/rusiaaman/wcgw
6
6
  Author-email: Aman Rusia <gapypi@arcfu.com>
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  authors = [{ name = "Aman Rusia", email = "gapypi@arcfu.com" }]
3
3
  name = "wcgw"
4
- version = "5.1.1"
4
+ version = "5.1.3"
5
5
  description = "Shell and coding agent for Claude and other mcp clients"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.11"
@@ -342,30 +342,30 @@ def get_bash_state_dir_xdg() -> str:
342
342
  return bash_state_dir
343
343
 
344
344
 
345
- def generate_chat_id() -> str:
346
- """Generate a random 4-digit chat ID."""
345
+ def generate_thread_id() -> str:
346
+ """Generate a random 4-digit thread_id."""
347
347
  return f"i{random.randint(1000, 9999)}"
348
348
 
349
349
 
350
- def save_bash_state_by_id(chat_id: str, bash_state_dict: dict[str, Any]) -> None:
351
- """Save bash state to XDG directory with the given chat ID."""
352
- if not chat_id:
350
+ def save_bash_state_by_id(thread_id: str, bash_state_dict: dict[str, Any]) -> None:
351
+ """Save bash state to XDG directory with the given thread_id."""
352
+ if not thread_id:
353
353
  return
354
354
 
355
355
  bash_state_dir = get_bash_state_dir_xdg()
356
- state_file = os.path.join(bash_state_dir, f"{chat_id}_bash_state.json")
356
+ state_file = os.path.join(bash_state_dir, f"{thread_id}_bash_state.json")
357
357
 
358
358
  with open(state_file, "w") as f:
359
359
  json.dump(bash_state_dict, f, indent=2)
360
360
 
361
361
 
362
- def load_bash_state_by_id(chat_id: str) -> Optional[dict[str, Any]]:
363
- """Load bash state from XDG directory with the given chat ID."""
364
- if not chat_id:
362
+ def load_bash_state_by_id(thread_id: str) -> Optional[dict[str, Any]]:
363
+ """Load bash state from XDG directory with the given thread_id."""
364
+ if not thread_id:
365
365
  return None
366
366
 
367
367
  bash_state_dir = get_bash_state_dir_xdg()
368
- state_file = os.path.join(bash_state_dir, f"{chat_id}_bash_state.json")
368
+ state_file = os.path.join(bash_state_dir, f"{thread_id}_bash_state.json")
369
369
 
370
370
  if not os.path.exists(state_file):
371
371
  return None
@@ -376,7 +376,7 @@ def load_bash_state_by_id(chat_id: str) -> Optional[dict[str, Any]]:
376
376
 
377
377
  class BashState:
378
378
  _use_screen: bool
379
- _current_chat_id: str
379
+ _current_thread_id: str
380
380
 
381
381
  def __init__(
382
382
  self,
@@ -388,7 +388,7 @@ class BashState:
388
388
  mode: Optional[Modes],
389
389
  use_screen: bool,
390
390
  whitelist_for_overwrite: Optional[dict[str, "FileWhitelistData"]] = None,
391
- chat_id: Optional[str] = None,
391
+ thread_id: Optional[str] = None,
392
392
  ) -> None:
393
393
  self._last_command: str = ""
394
394
  self.console = console
@@ -406,8 +406,10 @@ class BashState:
406
406
  self._whitelist_for_overwrite: dict[str, FileWhitelistData] = (
407
407
  whitelist_for_overwrite or {}
408
408
  )
409
- # Always ensure we have a chat ID
410
- self._current_chat_id = chat_id if chat_id is not None else generate_chat_id()
409
+ # Always ensure we have a thread_id
410
+ self._current_thread_id = (
411
+ thread_id if thread_id is not None else generate_thread_id()
412
+ )
411
413
  self._bg_expect_thread: Optional[threading.Thread] = None
412
414
  self._bg_expect_thread_stop_event = threading.Event()
413
415
  self._use_screen = use_screen
@@ -632,22 +634,22 @@ class BashState:
632
634
  self._init_shell()
633
635
 
634
636
  @property
635
- def current_chat_id(self) -> str:
636
- """Get the current chat ID."""
637
- return self._current_chat_id
637
+ def current_thread_id(self) -> str:
638
+ """Get the current thread_id."""
639
+ return self._current_thread_id
638
640
 
639
- def load_state_from_chat_id(self, chat_id: str) -> bool:
641
+ def load_state_from_thread_id(self, thread_id: str) -> bool:
640
642
  """
641
- Load bash state from a chat ID.
643
+ Load bash state from a thread_id.
642
644
 
643
645
  Args:
644
- chat_id: The chat ID to load state from
646
+ thread_id: The thread_id to load state from
645
647
 
646
648
  Returns:
647
649
  bool: True if state was successfully loaded, False otherwise
648
650
  """
649
651
  # Try to load state from disk
650
- loaded_state = load_bash_state_by_id(chat_id)
652
+ loaded_state = load_bash_state_by_id(thread_id)
651
653
  if not loaded_state:
652
654
  return False
653
655
 
@@ -661,7 +663,7 @@ class BashState:
661
663
  parsed_state[4],
662
664
  parsed_state[5],
663
665
  parsed_state[5],
664
- chat_id,
666
+ thread_id,
665
667
  )
666
668
  return True
667
669
 
@@ -676,13 +678,13 @@ class BashState:
676
678
  },
677
679
  "mode": self._mode,
678
680
  "workspace_root": self._workspace_root,
679
- "chat_id": self._current_chat_id,
681
+ "chat_id": self._current_thread_id,
680
682
  }
681
683
 
682
684
  def save_state_to_disk(self) -> None:
683
- """Save the current bash state to disk using the chat ID."""
685
+ """Save the current bash state to disk using the thread_id."""
684
686
  state_dict = self.serialize()
685
- save_bash_state_by_id(self._current_chat_id, state_dict)
687
+ save_bash_state_by_id(self._current_thread_id, state_dict)
686
688
 
687
689
  @staticmethod
688
690
  def parse_state(
@@ -721,10 +723,10 @@ class BashState:
721
723
  for k in whitelist_state
722
724
  }
723
725
 
724
- # Get the chat_id from state, or generate a new one if not present
725
- chat_id = state.get("chat_id")
726
- if chat_id is None:
727
- chat_id = generate_chat_id()
726
+ # Get the thread_id from state, or generate a new one if not present
727
+ thread_id = state.get("chat_id")
728
+ if thread_id is None:
729
+ thread_id = generate_thread_id()
728
730
 
729
731
  return (
730
732
  BashCommandMode.deserialize(state["bash_command_mode"]),
@@ -733,7 +735,7 @@ class BashState:
733
735
  state["mode"],
734
736
  whitelist_dict,
735
737
  state.get("workspace_root", ""),
736
- chat_id,
738
+ thread_id,
737
739
  )
738
740
 
739
741
  def load_state(
@@ -745,7 +747,7 @@ class BashState:
745
747
  whitelist_for_overwrite: dict[str, "FileWhitelistData"],
746
748
  cwd: str,
747
749
  workspace_root: str,
748
- chat_id: str,
750
+ thread_id: str,
749
751
  ) -> None:
750
752
  """Create a new BashState instance from a serialized state dictionary"""
751
753
  self._bash_command_mode = bash_command_mode
@@ -755,7 +757,7 @@ class BashState:
755
757
  self._write_if_empty_mode = write_if_empty_mode
756
758
  self._whitelist_for_overwrite = dict(whitelist_for_overwrite)
757
759
  self._mode = mode
758
- self._current_chat_id = chat_id
760
+ self._current_thread_id = thread_id
759
761
  self.reset_shell()
760
762
 
761
763
  # Save state to disk after loading
@@ -1000,12 +1002,12 @@ def execute_bash(
1000
1002
  timeout_s: Optional[float],
1001
1003
  ) -> tuple[str, float]:
1002
1004
  try:
1003
- # Check if the chat ID matches current
1004
- if bash_arg.chat_id != bash_state.current_chat_id:
1005
- # Try to load state from the chat ID
1006
- if not bash_state.load_state_from_chat_id(bash_arg.chat_id):
1005
+ # Check if the thread_id matches current
1006
+ if bash_arg.thread_id != bash_state.current_thread_id:
1007
+ # Try to load state from the thread_id
1008
+ if not bash_state.load_state_from_thread_id(bash_arg.thread_id):
1007
1009
  return (
1008
- f"Error: No saved bash state found for chat ID {bash_arg.chat_id}. Please initialize first with this ID.",
1010
+ f"Error: No saved bash state found for thread_id {bash_arg.thread_id}. Please initialize first with this ID.",
1009
1011
  0.0,
1010
1012
  )
1011
1013
 
@@ -74,12 +74,10 @@ TOOL_PROMPTS = [
74
74
  description="""
75
75
  - Writes or edits a file based on the percentage of changes.
76
76
  - Use absolute path only (~ allowed).
77
- - percentage_to_change is calculated as number of existing lines that will have some diff divided by total existing lines.
78
77
  - First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change
79
78
  - percentage_to_change should be low if mostly new code is to be added. It should be high if a lot of things are to be replaced.
80
- - If percentage_to_change > 50, provide full file content in file_content_or_search_replace_blocks
81
- - If percentage_to_change <= 50, file_content_or_search_replace_blocks should be search/replace blocks.
82
-
79
+ - If percentage_to_change > 50, provide full file content in text_or_search_replace_blocks
80
+ - If percentage_to_change <= 50, text_or_search_replace_blocks should be search/replace blocks.
83
81
  """
84
82
  + diffinstructions,
85
83
  annotations=ToolAnnotations(
@@ -31,7 +31,7 @@ from syntax_checker import check_syntax
31
31
  from ..client.bash_state.bash_state import (
32
32
  BashState,
33
33
  execute_bash,
34
- generate_chat_id,
34
+ generate_thread_id,
35
35
  get_status,
36
36
  )
37
37
  from ..client.repo_ops.file_stats import (
@@ -103,7 +103,7 @@ def initialize(
103
103
  coding_max_tokens: Optional[int],
104
104
  noncoding_max_tokens: Optional[int],
105
105
  mode: ModesConfig,
106
- chat_id: str,
106
+ thread_id: str,
107
107
  ) -> tuple[str, Context, dict[str, list[tuple[int, int]]]]:
108
108
  # Expand the workspace path
109
109
  any_workspace_path = expand_user(any_workspace_path)
@@ -113,16 +113,16 @@ def initialize(
113
113
  loaded_state = None
114
114
 
115
115
  # For workspace/mode changes, ensure we're using an existing state if possible
116
- if type != "first_call" and chat_id != context.bash_state.current_chat_id:
117
- # Try to load state from the chat ID
118
- if not context.bash_state.load_state_from_chat_id(chat_id):
116
+ if type != "first_call" and thread_id != context.bash_state.current_thread_id:
117
+ # Try to load state from the thread_id
118
+ if not context.bash_state.load_state_from_thread_id(thread_id):
119
119
  return (
120
- f"Error: No saved bash state found for chat ID {chat_id}. Please re-initialize to get a new id or use correct id.",
120
+ f"Error: No saved bash state found for thread_id {thread_id}. Please re-initialize to get a new id or use correct id.",
121
121
  context,
122
122
  {},
123
123
  )
124
124
  del (
125
- chat_id
125
+ thread_id
126
126
  ) # No use other than loading correct state before doing actual tool related stuff
127
127
 
128
128
  # Handle task resumption - this applies only to first_call
@@ -181,10 +181,10 @@ def initialize(
181
181
  workspace_root = (
182
182
  str(folder_to_start) if folder_to_start else parsed_state[5]
183
183
  )
184
- loaded_chat_id = parsed_state[6] if len(parsed_state) > 6 else None
184
+ loaded_thread_id = parsed_state[6] if len(parsed_state) > 6 else None
185
185
 
186
- if not loaded_chat_id:
187
- loaded_chat_id = context.bash_state.current_chat_id
186
+ if not loaded_thread_id:
187
+ loaded_thread_id = context.bash_state.current_thread_id
188
188
 
189
189
  if mode == "wcgw":
190
190
  context.bash_state.load_state(
@@ -195,7 +195,7 @@ def initialize(
195
195
  {**parsed_state[4], **context.bash_state.whitelist_for_overwrite},
196
196
  str(folder_to_start) if folder_to_start else workspace_root,
197
197
  workspace_root,
198
- loaded_chat_id,
198
+ loaded_thread_id,
199
199
  )
200
200
  else:
201
201
  state = modes_to_state(mode)
@@ -207,7 +207,7 @@ def initialize(
207
207
  {**parsed_state[4], **context.bash_state.whitelist_for_overwrite},
208
208
  str(folder_to_start) if folder_to_start else workspace_root,
209
209
  workspace_root,
210
- loaded_chat_id,
210
+ loaded_thread_id,
211
211
  )
212
212
  except ValueError:
213
213
  context.console.print(traceback.format_exc())
@@ -217,10 +217,10 @@ def initialize(
217
217
  else:
218
218
  mode_changed = is_mode_change(mode, context.bash_state)
219
219
  state = modes_to_state(mode)
220
- new_chat_id = context.bash_state.current_chat_id
220
+ new_thread_id = context.bash_state.current_thread_id
221
221
  if type == "first_call":
222
- # Recreate chat id
223
- new_chat_id = generate_chat_id()
222
+ # Recreate thread_id
223
+ new_thread_id = generate_thread_id()
224
224
  # Use the provided workspace path as the workspace root
225
225
  context.bash_state.load_state(
226
226
  state[0],
@@ -230,7 +230,7 @@ def initialize(
230
230
  dict(context.bash_state.whitelist_for_overwrite),
231
231
  str(folder_to_start) if folder_to_start else "",
232
232
  str(folder_to_start) if folder_to_start else "",
233
- new_chat_id,
233
+ new_thread_id,
234
234
  )
235
235
  if type == "first_call" or mode_changed:
236
236
  mode_prompt = get_mode_prompt(context)
@@ -273,6 +273,8 @@ def initialize(
273
273
  uname_machine = os.uname().machine
274
274
 
275
275
  output = f"""
276
+ Use thread_id={context.bash_state.current_thread_id} for all wcgw tool calls which take that.
277
+ ---
276
278
  {mode_prompt}
277
279
 
278
280
  # Environment
@@ -281,17 +283,16 @@ Machine: {uname_machine}
281
283
  Initialized in directory (also cwd): {context.bash_state.cwd}
282
284
  User home directory: {expanduser("~")}
283
285
 
284
- {repo_context}
285
-
286
286
  {alignment_context}
287
- {initial_files_context}
287
+ {repo_context}
288
288
 
289
289
  ---
290
290
 
291
291
  {memory}
292
-
293
292
  ---
294
- Use chat_id={context.bash_state.current_chat_id} for all wcgw tool calls which take that.
293
+
294
+ {initial_files_context}
295
+
295
296
  """
296
297
 
297
298
  return output, context, initial_paths_with_ranges
@@ -313,13 +314,13 @@ def reset_wcgw(
313
314
  starting_directory: str,
314
315
  mode_name: Optional[Modes],
315
316
  change_mode: ModesConfig,
316
- chat_id: str,
317
+ thread_id: str,
317
318
  ) -> str:
318
- # Load state for this chat_id before proceeding with mode/directory changes
319
- if chat_id != context.bash_state.current_chat_id:
320
- # Try to load state from the chat ID
321
- if not context.bash_state.load_state_from_chat_id(chat_id):
322
- return f"Error: No saved bash state found for chat ID {chat_id}. Please re-initialize to get a new id or use correct id."
319
+ # Load state for this thread_id before proceeding with mode/directory changes
320
+ if thread_id != context.bash_state.current_thread_id:
321
+ # Try to load state from the thread_id
322
+ if not context.bash_state.load_state_from_thread_id(thread_id):
323
+ return f"Error: No saved bash state found for thread_id {thread_id}. Please re-initialize to get a new id or use correct id."
323
324
  if mode_name:
324
325
  # update modes if they're relative
325
326
  if isinstance(change_mode, CodeWriterMode):
@@ -332,7 +333,7 @@ def reset_wcgw(
332
333
  change_mode
333
334
  )
334
335
 
335
- # Reset shell with new mode, using the provided chat ID
336
+ # Reset shell with new mode, using the provided thread_id
336
337
  context.bash_state.load_state(
337
338
  bash_command_mode,
338
339
  file_edit_mode,
@@ -341,7 +342,7 @@ def reset_wcgw(
341
342
  dict(context.bash_state.whitelist_for_overwrite),
342
343
  starting_directory,
343
344
  starting_directory,
344
- chat_id,
345
+ thread_id,
345
346
  )
346
347
  mode_prompt = get_mode_prompt(context)
347
348
  return (
@@ -357,7 +358,7 @@ def reset_wcgw(
357
358
  write_if_empty_mode = context.bash_state.write_if_empty_mode
358
359
  mode = context.bash_state.mode
359
360
 
360
- # Reload state with new directory, using the provided chat ID
361
+ # Reload state with new directory, using the provided thread_id
361
362
  context.bash_state.load_state(
362
363
  bash_command_mode,
363
364
  file_edit_mode,
@@ -366,7 +367,7 @@ def reset_wcgw(
366
367
  dict(context.bash_state.whitelist_for_overwrite),
367
368
  starting_directory,
368
369
  starting_directory,
369
- chat_id,
370
+ thread_id,
370
371
  )
371
372
  return "Reset successful" + get_status(context.bash_state)
372
373
 
@@ -475,7 +476,7 @@ def get_context_for_errors(
475
476
  ntokens = len(default_enc.encoder(context))
476
477
  if ntokens > max_tokens:
477
478
  return "Please re-read the file to understand the context"
478
- return f"Here's relevant snippet from the file where the syntax errors occured:\n```\n{context}\n```"
479
+ return f"Here's relevant snippet from the file where the syntax errors occured:\n<snippet>\n{context}\n</snippet>"
479
480
 
480
481
 
481
482
  def write_file(
@@ -556,7 +557,7 @@ def write_file(
556
557
  return (
557
558
  (
558
559
  msg
559
- + f"Here's the existing file:\n```\n{file_content_str}\n{final_message}\n```"
560
+ + f"Here's the existing file:\n<wcgw:file>\n{file_content_str}\n{final_message}\n</wcgw:file>"
560
561
  ),
561
562
  {path_: file_ranges},
562
563
  )
@@ -578,7 +579,7 @@ def write_file(
578
579
  return (
579
580
  (
580
581
  msg
581
- + f"Here's the existing file:\n```\n{file_content_str}\n```\n{final_message}"
582
+ + f"Here's the existing file:\n<wcgw:file>\n{file_content_str}\n</wcgw:file>\n{final_message}"
582
583
  ),
583
584
  {path_: file_ranges},
584
585
  )
@@ -819,12 +820,14 @@ def file_writing(
819
820
  If percentage_changed > 50%, treat content as direct file content.
820
821
  Otherwise, treat content as search/replace blocks.
821
822
  """
822
- # Check if the chat ID matches current
823
- if file_writing_args.chat_id != context.bash_state.current_chat_id:
824
- # Try to load state from the chat ID
825
- if not context.bash_state.load_state_from_chat_id(file_writing_args.chat_id):
823
+ # Check if the thread_id matches current
824
+ if file_writing_args.thread_id != context.bash_state.current_thread_id:
825
+ # Try to load state from the thread_id
826
+ if not context.bash_state.load_state_from_thread_id(
827
+ file_writing_args.thread_id
828
+ ):
826
829
  return (
827
- f"Error: No saved bash state found for chat ID {file_writing_args.chat_id}. Please re-initialize to get a new id or use correct id.",
830
+ f"Error: No saved bash state found for thread_id {file_writing_args.thread_id}. Please re-initialize to get a new id or use correct id.",
828
831
  {},
829
832
  )
830
833
 
@@ -837,14 +840,14 @@ def file_writing(
837
840
  )
838
841
 
839
842
  # If file doesn't exist, always use direct file_content mode
840
- content = file_writing_args.file_content_or_search_replace_blocks
843
+ content = file_writing_args.text_or_search_replace_blocks
841
844
 
842
845
  if not _is_edit(content, file_writing_args.percentage_to_change):
843
846
  # Use direct content mode (same as WriteIfEmpty)
844
847
  result, paths = write_file(
845
848
  WriteIfEmpty(
846
849
  file_path=path_,
847
- file_content=file_writing_args.file_content_or_search_replace_blocks,
850
+ file_content=file_writing_args.text_or_search_replace_blocks,
848
851
  ),
849
852
  True,
850
853
  coding_max_tokens,
@@ -857,7 +860,7 @@ def file_writing(
857
860
  result, paths = do_diff_edit(
858
861
  FileEdit(
859
862
  file_path=path_,
860
- file_edit_using_search_replace_blocks=file_writing_args.file_content_or_search_replace_blocks,
863
+ file_edit_using_search_replace_blocks=file_writing_args.text_or_search_replace_blocks,
861
864
  ),
862
865
  coding_max_tokens,
863
866
  noncoding_max_tokens,
@@ -1012,7 +1015,7 @@ def get_tool_output(
1012
1015
  )
1013
1016
  workspace_path = workspace_path if os.path.exists(workspace_path) else ""
1014
1017
 
1015
- # For these specific operations, chat_id is required
1018
+ # For these specific operations, thread_id is required
1016
1019
  output = (
1017
1020
  reset_wcgw(
1018
1021
  context,
@@ -1021,7 +1024,7 @@ def get_tool_output(
1021
1024
  if is_mode_change(arg.mode, context.bash_state)
1022
1025
  else None,
1023
1026
  arg.mode,
1024
- arg.chat_id,
1027
+ arg.thread_id,
1025
1028
  ),
1026
1029
  0.0,
1027
1030
  )
@@ -1035,7 +1038,7 @@ def get_tool_output(
1035
1038
  coding_max_tokens,
1036
1039
  noncoding_max_tokens,
1037
1040
  arg.mode,
1038
- arg.chat_id,
1041
+ arg.thread_id,
1039
1042
  )
1040
1043
  output = output_, 0.0
1041
1044
  # Since init_paths is already a dictionary mapping file paths to line ranges,
@@ -1164,25 +1167,27 @@ def read_files(
1164
1167
  continue
1165
1168
 
1166
1169
  if coding_max_tokens:
1167
- coding_max_tokens = coding_max_tokens - tokens
1170
+ coding_max_tokens = max(0, coding_max_tokens - tokens)
1168
1171
  if noncoding_max_tokens:
1169
- noncoding_max_tokens = noncoding_max_tokens - tokens
1172
+ noncoding_max_tokens = max(0, noncoding_max_tokens - tokens)
1170
1173
 
1171
1174
  range_formatted = range_format(start_line_num, end_line_num)
1172
- message += f"\n{file}{range_formatted}\n```\n{content}\n"
1175
+ message += f'\n<wcgw:file path="{file}{range_formatted}">\n{content}\n'
1173
1176
 
1174
- # Check if we've hit either token limit
1177
+ if not truncated:
1178
+ message += "</wcgw:file>"
1179
+
1180
+ # Check if we've hit both token limit
1175
1181
  if (
1176
1182
  truncated
1177
1183
  or (coding_max_tokens is not None and coding_max_tokens <= 0)
1178
- or (noncoding_max_tokens is not None and noncoding_max_tokens <= 0)
1184
+ and (noncoding_max_tokens is not None and noncoding_max_tokens <= 0)
1179
1185
  ):
1180
1186
  not_reading = file_paths[i + 1 :]
1181
1187
  if not_reading:
1182
1188
  message += f"\nNot reading the rest of the files: {', '.join(not_reading)} due to token limit, please call again"
1183
1189
  break
1184
- else:
1185
- message += "```"
1190
+
1186
1191
  return message, file_ranges_dict, truncated
1187
1192
 
1188
1193
 
@@ -1313,7 +1318,7 @@ if __name__ == "__main__":
1313
1318
  task_id_to_resume="",
1314
1319
  mode_name="wcgw",
1315
1320
  code_writer_config=None,
1316
- chat_id="",
1321
+ thread_id="",
1317
1322
  ),
1318
1323
  default_enc,
1319
1324
  0,
@@ -1327,7 +1332,7 @@ if __name__ == "__main__":
1327
1332
  Context(BASH_STATE, BASH_STATE.console),
1328
1333
  BashCommand(
1329
1334
  action_json=Command(command="pwd"),
1330
- chat_id=BASH_STATE.current_chat_id,
1335
+ thread_id=BASH_STATE.current_thread_id,
1331
1336
  ),
1332
1337
  default_enc,
1333
1338
  0,
@@ -1357,9 +1362,9 @@ if __name__ == "__main__":
1357
1362
  Context(BASH_STATE, BASH_STATE.console),
1358
1363
  FileWriteOrEdit(
1359
1364
  file_path="/Users/arusia/repos/wcgw/src/wcgw/client/tools.py",
1360
- file_content_or_search_replace_blocks="""test""",
1365
+ text_or_search_replace_blocks="""test""",
1361
1366
  percentage_to_change=100,
1362
- chat_id=BASH_STATE.current_chat_id,
1367
+ thread_id=BASH_STATE.current_thread_id,
1363
1368
  ),
1364
1369
  default_enc,
1365
1370
  0,
@@ -53,8 +53,8 @@ class Initialize(BaseModel):
53
53
  initial_files_to_read: list[str]
54
54
  task_id_to_resume: str
55
55
  mode_name: Literal["wcgw", "architect", "code_writer"]
56
- chat_id: str = Field(
57
- description="Use the chat id created in first_call, leave it as empty string if first_call"
56
+ thread_id: str = Field(
57
+ description="Use the thread_id created in first_call, leave it as empty string if first_call"
58
58
  )
59
59
  code_writer_config: Optional[CodeWriterMode] = None
60
60
 
@@ -105,7 +105,7 @@ class SendAscii(BaseModel):
105
105
  class BashCommand(BaseModel):
106
106
  action_json: Command | StatusCheck | SendText | SendSpecials | SendAscii
107
107
  wait_for_seconds: Optional[float] = None
108
- chat_id: str
108
+ thread_id: str
109
109
 
110
110
 
111
111
  class ReadImage(BaseModel):
@@ -215,10 +215,15 @@ class FileEdit(BaseModel):
215
215
 
216
216
 
217
217
  class FileWriteOrEdit(BaseModel):
218
- file_path: str
219
- percentage_to_change: int # 0.0 to 100.0
220
- file_content_or_search_replace_blocks: str
221
- chat_id: str
218
+ # Naming should be in sorted order otherwise it gets changed in LLM backend.
219
+ file_path: str = Field(description="#1: absolute file path")
220
+ percentage_to_change: int = Field(
221
+ description="#2: predict this percentage, calculated as number of existing lines that will have some diff divided by total existing lines."
222
+ )
223
+ text_or_search_replace_blocks: str = Field(
224
+ description="#3: content/edit blocks. Must be after #2 in the tool xml"
225
+ )
226
+ thread_id: str = Field(description="#4: thread_id")
222
227
 
223
228
 
224
229
  class ContextSave(BaseModel):