Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions codewiki/src/be/agent_tools/str_replace_editor.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
#!/usr/bin/env python3

Check failure on line 1 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (EXE001)

codewiki/src/be/agent_tools/str_replace_editor.py:1:1: EXE001 Shebang is present but file is not executable

"""Source: https://github.com/SWE-agent/SWE-agent/blob/main/tools/edit_anthropic/bin/str_replace_editor
This tool is used to view the given source code and view/edit the documentation files in the separate docs directory.
"""

import json
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Annotated, List, Literal, Optional, Tuple

Check failure on line 13 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (UP035)

codewiki/src/be/agent_tools/str_replace_editor.py:13:1: UP035 `typing.Tuple` is deprecated, use `tuple` instead

Check failure on line 13 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (UP035)

codewiki/src/be/agent_tools/str_replace_editor.py:13:1: UP035 `typing.List` is deprecated, use `list` instead
import io

import logging

Check failure on line 16 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (I001)

codewiki/src/be/agent_tools/str_replace_editor.py:7:1: I001 Import block is un-sorted or un-formatted help: Organize imports
Expand All @@ -19,11 +19,11 @@

logger = logging.getLogger(__name__)

from pydantic import BeforeValidator
from pydantic_ai import RunContext, Tool

from .deps import CodeWikiDeps
from ..utils import validate_mermaid_diagrams

Check failure on line 26 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (I001)

codewiki/src/be/agent_tools/str_replace_editor.py:22:1: I001 Import block is un-sorted or un-formatted help: Organize imports


def _coerce_json_string(value):
Expand All @@ -45,8 +45,8 @@
return value


ViewRange = Annotated[Optional[List[int]], BeforeValidator(_coerce_json_string)]

Check failure on line 48 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (UP006)

codewiki/src/be/agent_tools/str_replace_editor.py:48:32: UP006 Use `list` instead of `List` for type annotation help: Replace with `list`

Check failure on line 48 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (UP045)

codewiki/src/be/agent_tools/str_replace_editor.py:48:23: UP045 Use `X | None` for type annotations help: Convert to `X | None`
InsertLine = Annotated[Optional[int], BeforeValidator(_coerce_json_string)]

Check failure on line 49 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (UP045)

codewiki/src/be/agent_tools/str_replace_editor.py:49:24: UP045 Use `X | None` for type annotations help: Convert to `X | None`


# There are some super strange "ascii can't decode x" errors,
Expand Down Expand Up @@ -84,7 +84,7 @@
"""


def maybe_truncate(content: str, truncate_after: Optional[int] = MAX_RESPONSE_LEN):

Check failure on line 87 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (UP045)

codewiki/src/be/agent_tools/str_replace_editor.py:87:50: UP045 Use `X | None` for type annotations help: Convert to `X | None`
"""Truncate content and append a notice if content exceeds the specified length."""
return (
content
Expand Down Expand Up @@ -127,7 +127,7 @@


def _update_previous_errors(
previous_errors: List[Flake8Error], replacement_window: Tuple[int, int], replacement_n_lines: int

Check failure on line 130 in codewiki/src/be/agent_tools/str_replace_editor.py

View workflow job for this annotation

GitHub Actions / Lint

ruff (UP006)

codewiki/src/be/agent_tools/str_replace_editor.py:130:22: UP006 Use `list` instead of `List` for type annotation help: Replace with `list`
) -> List[Flake8Error]:
"""Update the line numbers of the previous errors to what they would be after the edit window.
This is a helper function for `_filter_previous_errors`.
Expand Down Expand Up @@ -734,15 +734,24 @@

async def str_replace_editor(
ctx: RunContext[CodeWikiDeps],
working_dir: Literal["repo", "docs"],
command: Literal["view", "create", "str_replace", "insert", "undo_edit"],
working_dir: Literal["repo", "docs"] = "docs",
path: Optional[str] = None,
file: Optional[str] = None,
file_text: Optional[str] = None,
view_range: ViewRange = None,
old_str: Optional[str] = None,
new_str: Optional[str] = None,
insert_line: InsertLine = None,
# Aliases accepted for cross-model compatibility. Weaker/local models (e.g.
# Qwen3) often emit the OpenAI `str_replace_based_edit_tool` field names
# (old_string/new_string) or `content` for the created file body. Declaring
# them here means pydantic-ai accepts those calls instead of rejecting them
# as unknown arguments (which no number of retries can fix). They are mapped
# onto the canonical parameters below.
old_string: Optional[str] = None,
new_string: Optional[str] = None,
content: Optional[str] = None,
) -> str:
"""
Custom editing tool for viewing, creating and editing files
Expand All @@ -754,16 +763,28 @@
* Only `view` command is allowed when `working_dir` is `repo`.

Args:
working_dir: The working directory to use. Choose `repo` to work with the repository files, or `docs` to work with the generated documentation files.
command: The command to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.
working_dir: The working directory to use. Choose `repo` to work with the repository files, or `docs` to work with the generated documentation files. Defaults to `docs`.
path: Path to file or directory, e.g. `./chat_core.md` or `./agents/`
file: Alias for `path` parameter (for compatibility with some models)
file_text: Required parameter of `create` command, with the content of the file to be created.
view_range: Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.
old_str: Required parameter of `str_replace` command containing the string in `path` to replace.
new_str: Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.
old_string: Alias for `old_str` (some models emit this name).
new_string: Alias for `new_str` (some models emit this name).
content: Alias for `file_text` (some models emit this name for `create`).
"""

# Map cross-model aliases onto the canonical parameters. Prefer the
# canonical value when both are supplied.
if old_str is None and old_string is not None:
old_str = old_string
if new_str is None and new_string is not None:
new_str = new_string
Comment on lines +783 to +784

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve an explicitly null canonical replacement

When a caller supplies both spellings with new_str: null and a non-null new_string, this condition replaces the canonical value with the alias despite the stated canonical-wins behavior. An explicit null is valid and meaningful for str_replace because EditTool.str_replace converts it to an empty string to delete the matched text; the current mapping instead inserts the alias text and can silently produce the wrong document.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged, but I'm leaving the mapping as-is — the failure mode is effectively unreachable and the strict fix has real downsides.

  1. It requires a single call carrying both spellings with contradictory intent: new_str: null (delete the matched text) and a non-null new_string (replace with that text). A model uses one spelling or the other; it doesn't emit a self-contradicting call, so there's no real (Qwen or otherwise) behavior that reaches this branch.
  2. For str_replace, null and "" are already equivalent — EditTool.str_replace coerces None → "" — so the "wrong document" outcome only exists inside that contradictory call.
  3. Honoring an explicit null over the alias isn't cleanly expressible here: at the pydantic-ai tool signature the default is None, so is None can't distinguish "explicitly null" from "omitted". Doing it properly needs a sentinel default (_UNSET) on an Optional[str] param, which muddies the auto-generated JSON schema and validation for a case that can't occur.

Net: the alias only kicks in when the canonical value is absent, which is the intended behavior for every real call. Happy to revisit if a concrete tool call is observed that actually sends both keys.

if file_text is None and content is not None:
file_text = content

# Handle both `path` and `file` parameters for model compatibility
if path is None and file is None:
return "Error: Either `path` or `file` parameter must be provided."
Expand Down
Loading