FileWriteOrEdit
Edit or write files by specifying the percentage of changes needed. Use search/replace blocks for minor edits or provide full content for >50% changes. Supports precise, multi-location edits in a single call.
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
Edit format on the input file
SEARCH/REPLACE block Rules:
Every "SEARCH" section must EXACTLY MATCH the existing file content, character for character, including all comments, docstrings, whitespaces, etc.
Use multiple search/replace blocks in a single FileWriteOrEdit tool call to edit in a single file in multiple places from top to bottom (separate calls are slower).
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/client/tools.py:842-902 (handler)Primary handler function that implements FileWriteOrEdit logic: decides between full file write (if low changes or new file) or search-replace edit (if moderate changes), dispatching to `write_file` or `do_diff_edit`.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/types_.py:275-285 (schema)Pydantic BaseModel defining the input schema for the FileWriteOrEdit tool, including fields for file path, predicted change percentage, content/edit blocks, and 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")
- src/wcgw/client/tool_prompts.py:73-88 (registration)MCP Tool registration definition including schema (from FileWriteOrEdit.model_json_schema()), name, description with usage instructions, and annotations.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/mcp_server/server.py:84-92 (registration)MCP server endpoint that lists all tools, including FileWriteOrEdit via returning TOOL_PROMPTS.@server.list_tools() # type: ignore async def handle_list_tools() -> list[types.Tool]: """ List available tools. Each tool specifies its arguments using JSON Schema validation. """ return TOOL_PROMPTS
- Helper function that parses search/replace blocks from the tool input string and applies edits using diff_edit logic, returning updated file content and comments.def search_replace_edit( lines: list[str], original_content: str, logger: Callable[[str], object] ) -> tuple[str, str]: if not lines: raise SearchReplaceSyntaxError("Error: No input to search replace edit") original_lines = original_content.split("\n") n_lines = len(lines) i = 0 search_replace_blocks = list[tuple[list[str], list[str]]]() while i < n_lines: if SEARCH_MARKER.match(lines[i]): line_num = i + 1 search_block = [] i += 1 while i < n_lines and not DIVIDER_MARKER.match(lines[i]): if SEARCH_MARKER.match(lines[i]) or REPLACE_MARKER.match(lines[i]): raise SearchReplaceSyntaxError( f"Line {i + 1}: Found stray marker in SEARCH block: {lines[i]}" ) search_block.append(lines[i]) i += 1 if i >= n_lines: raise SearchReplaceSyntaxError( f"Line {line_num}: Unclosed SEARCH block - missing ======= marker" ) if not search_block: raise SearchReplaceSyntaxError( f"Line {line_num}: SEARCH block cannot be empty" ) i += 1 replace_block = [] while i < n_lines and not REPLACE_MARKER.match(lines[i]): if SEARCH_MARKER.match(lines[i]) or DIVIDER_MARKER.match(lines[i]): raise SearchReplaceSyntaxError( f"Line {i + 1}: Found stray marker in REPLACE block: {lines[i]}" ) replace_block.append(lines[i]) i += 1 if i >= n_lines: raise SearchReplaceSyntaxError( f"Line {line_num}: Unclosed block - missing REPLACE marker" ) i += 1 for line in search_block: logger("> " + line) logger("=======") for line in replace_block: logger("< " + line) logger("\n\n\n\n") search_replace_blocks.append((search_block, replace_block)) else: if REPLACE_MARKER.match(lines[i]) or DIVIDER_MARKER.match(lines[i]): raise SearchReplaceSyntaxError( f"Line {i + 1}: Found stray marker outside block: {lines[i]}" ) i += 1 if not search_replace_blocks: raise SearchReplaceSyntaxError( "No valid search replace blocks found, ensure your SEARCH/REPLACE blocks are formatted correctly" ) edited_content, comments_ = edit_with_individual_fallback( original_lines, search_replace_blocks ) edited_file = "\n".join(edited_content) if not comments_: comments = "Edited successfully" else: comments = ( "Edited successfully. However, following warnings were generated while matching search blocks.\n" + "\n".join(comments_) ) return edited_file, comments