Skip to main content
Glama

FileWriteOrEdit

Write or edit files by specifying the percentage of changes needed, using either full content or search/replace blocks based on the modification scope.

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()
>>>>>>> REPLACE

SEARCH/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

TableJSON Schema
NameRequiredDescriptionDefault
file_pathYes#1: absolute file path
percentage_to_changeYes#2: predict this percentage, calculated as number of existing lines that will have some diff divided by total existing lines.
text_or_search_replace_blocksYes#3: content/edit blocks. Must be after #2 in the tool xml
thread_idYes#4: thread_id

Implementation Reference

  • Main handler function for the FileWriteOrEdit tool. Determines whether to perform a full file write or a targeted edit based on the predicted percentage_to_change, delegating to write_file or do_diff_edit accordingly.
    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
  • Pydantic BaseModel defining the input schema for FileWriteOrEdit, including fields for file_path, percentage_to_change, text_or_search_replace_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")
    
        def model_post_init(self, __context: Any) -> None:
            self.thread_id = normalize_thread_id(self.thread_id)
            return super().model_post_init(__context)
  • MCP tool registration for FileWriteOrEdit, providing the JSON schema, name, detailed description, and annotations indicating it's destructive but idempotent.
            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
            ),
        ),
  • Dispatch logic within get_tool_output that invokes the file_writing handler specifically for FileWriteOrEdit tool calls and handles the output and file tracking.
    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()
  • MCP server handler for all tool calls, including FileWriteOrEdit. Uses which_tool_name and parse_tool_by_name to instantiate the tool and delegates to get_tool_output.
    async def handle_call_tool(
        name: str, arguments: dict[str, Any] | None
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        global BASH_STATE
        if not arguments:
            raise ValueError("Missing arguments")
    
        tool_type = which_tool_name(name)
        tool_call = parse_tool_by_name(name, arguments)
    
        try:
            assert BASH_STATE
            output_or_dones, _ = get_tool_output(
                Context(BASH_STATE, BASH_STATE.console),
                tool_call,
                default_enc,
                0.0,
                lambda x, y: ("", 0),
                24000,  # coding_max_tokens
                8000,  # noncoding_max_tokens
            )
    
        except Exception as e:
            output_or_dones = [f"GOT EXCEPTION while calling tool. Error: {e}"]
    
        content: list[types.TextContent | types.ImageContent | types.EmbeddedResource] = []
        for output_or_done in output_or_dones:
            if isinstance(output_or_done, str):
                if issubclass(tool_type, Initialize):
                    # Prepare the original hardcoded message
                    original_message = """
    - Additional important note: as soon as you encounter "The user has chosen to disallow the tool call.", immediately stop doing everything and ask user for the reason.
    
    Initialize call done.
        """
    
                    # If custom instructions exist, prepend them to the original message
                    if CUSTOM_INSTRUCTIONS:
                        output_or_done += f"\n{CUSTOM_INSTRUCTIONS}\n{original_message}"
                    else:
                        output_or_done += original_message
    
                content.append(types.TextContent(type="text", text=output_or_done))
            else:
                content.append(
                    types.ImageContent(
                        type="image",
                        data=output_or_done.data,
                        mimeType=output_or_done.media_type,
                    )
                )
    
        return content

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rusiaaman/wcgw'

If you have feedback or need assistance with the MCP directory API, please join our Discord server