FileWriteOrEdit
Write or edit files with percentage-based change estimation: use search/replace blocks for small edits, or full file content for large rewrites.
Instructions
Writes or edits a file based on the percentage of changes.
Use absolute path only (~ allowed).
First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change
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.
If percentage_to_change > 50, provide full file content in text_or_search_replace_blocks
If percentage_to_change <= 50, text_or_search_replace_blocks should be search/replace blocks.
Instructions for editing files.
Example
Input file
import numpy as np
from impls import impl1, impl2
def hello():
"print a greeting"
print("hello")
def call_hello():
"call hello"
hello()
print("Called")
impl1()
hello()
impl2()
Edit format on the input file
<<<<<<< SEARCH
from impls import impl1, impl2
=======
from impls import impl1, impl2
from hello import hello as hello_renamed
>>>>>>> REPLACE
<<<<<<< SEARCH
def hello():
"print a greeting"
print("hello")
=======
>>>>>>> REPLACE
<<<<<<< SEARCH
def call_hello():
"call hello"
hello()
=======
def call_hello_renamed():
"call hello renamed"
hello_renamed()
>>>>>>> REPLACE
<<<<<<< SEARCH
impl1()
hello()
impl2()
=======
impl1()
hello_renamed()
impl2()
>>>>>>> REPLACESEARCH/REPLACE block Rules:
Every "<<<<<<< SEARCH" section must EXACTLY MATCH the existing file content, character for character, including all comments, docstrings, whitespaces, etc.
Including multiple unique SEARCH/REPLACE blocks if needed. Include enough and only enough lines in each SEARCH section to uniquely match each set of lines that need to change.
Keep SEARCH/REPLACE blocks concise. Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file. Include just the changing lines, and a few surrounding lines (0-3 lines) if needed for uniqueness. Other than for uniqueness, avoid including those lines which do not change in search (and replace) blocks. Target 0-3 non trivial extra lines per block.
Preserve leading spaces and indentations in both SEARCH and REPLACE blocks.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file_path | Yes | #1: absolute file path | |
| percentage_to_change | Yes | #2: predict this percentage, calculated as number of existing lines that will have some diff divided by total existing lines. | |
| text_or_search_replace_blocks | Yes | #3: content/edit blocks. Must be after #2 in the tool xml | |
| thread_id | Yes | #4: thread_id |
Implementation Reference
- src/wcgw/types_.py:355-368 (schema)FileWriteOrEdit Pydantic model/schema definition with fields: file_path, percentage_to_change, text_or_search_replace_blocks, thread_id
class FileWriteOrEdit(BaseModel): # Naming should be in sorted order otherwise it gets changed in LLM backend. file_path: str = Field(description="#1: absolute file path") percentage_to_change: int = Field( description="#2: predict this percentage, calculated as number of existing lines that will have some diff divided by total existing lines." ) text_or_search_replace_blocks: str = Field( description="#3: content/edit blocks. Must be after #2 in the tool xml" ) thread_id: str = Field(description="#4: thread_id") def model_post_init(self, __context: Any) -> None: self.thread_id = normalize_thread_id(self.thread_id) return super().model_post_init(__context) - src/wcgw/client/tools.py:842-901 (handler)file_writing() function - the actual handler that dispatches to write_file (full content) or do_diff_edit (search/replace) based on percentage_to_change
def file_writing( file_writing_args: FileWriteOrEdit, coding_max_tokens: Optional[int], noncoding_max_tokens: Optional[int], context: Context, ) -> tuple[ str, dict[str, list[tuple[int, int]]] ]: # Updated to return message and file paths with line ranges """ Write or edit a file based on percentage of changes. If percentage_changed > 50%, treat content as direct file content. Otherwise, treat content as search/replace blocks. """ # Check if the thread_id matches current if file_writing_args.thread_id != context.bash_state.current_thread_id: # Try to load state from the thread_id if not context.bash_state.load_state_from_thread_id( file_writing_args.thread_id ): return ( 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.", {}, ) # Expand the path before checking if it's absolute path_ = expand_user(file_writing_args.file_path) if not os.path.isabs(path_): return ( f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}", {}, # Return empty dict instead of empty list for type consistency ) # If file doesn't exist, always use direct file_content mode content = file_writing_args.text_or_search_replace_blocks if not _is_edit(content, file_writing_args.percentage_to_change): # Use direct content mode (same as WriteIfEmpty) result, paths = write_file( WriteIfEmpty( file_path=path_, file_content=file_writing_args.text_or_search_replace_blocks, ), True, coding_max_tokens, noncoding_max_tokens, context, ) return result, paths else: # File exists and percentage <= 50, use search/replace mode result, paths = do_diff_edit( FileEdit( file_path=path_, file_edit_using_search_replace_blocks=file_writing_args.text_or_search_replace_blocks, ), coding_max_tokens, noncoding_max_tokens, context, ) return result, paths - src/wcgw/client/tool_prompts.py:67-88 (registration)Tool registration as an MCP tool with name 'FileWriteOrEdit' and description explaining percentage-based write/edit logic
Tool( inputSchema=remove_titles_from_schema(ReadImage.model_json_schema()), name="ReadImage", description="Read an image from the shell.", annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False), ), Tool( inputSchema=remove_titles_from_schema(FileWriteOrEdit.model_json_schema()), name="FileWriteOrEdit", description=""" - Writes or edits a file based on the percentage of changes. - Use absolute path only (~ allowed). - First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change - 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. - If percentage_to_change > 50, provide full file content in text_or_search_replace_blocks - If percentage_to_change <= 50, text_or_search_replace_blocks should be search/replace blocks. """ + diffinstructions, annotations=ToolAnnotations( destructiveHint=True, idempotentHint=True, openWorldHint=False ), ), - src/wcgw/client/tools.py:1007-1019 (helper)Dispatch in get_tool_output() - handles FileWriteOrEdit arg by calling file_writing() and tracking file paths with ranges
elif isinstance(arg, FileWriteOrEdit): context.console.print("Calling file writing tool") result, write_edit_paths = file_writing( arg, coding_max_tokens, noncoding_max_tokens, context ) output = result, 0.0 # Add write/edit paths with their ranges to our tracking dictionary for path, ranges in write_edit_paths.items(): if path in file_paths_with_ranges: file_paths_with_ranges[path].extend(ranges) else: file_paths_with_ranges[path] = ranges.copy() - src/wcgw/client/tools.py:832-839 (helper)_is_edit() helper function that determines whether to use search/replace mode vs direct write based on content markers and percentage
def _is_edit(content: str, percentage: int) -> bool: lines = content.lstrip().split("\n") if not lines: return False line = lines[0] if SEARCH_MARKER.match(line) or (0 < percentage <= 50): return True return False