feat(file_tools): write_file says when it just re-sent a large file that was already on disk
In the 1,393-agent refactor run write_file carried 92.8M chars of content (23.2M output tokens, ~$580, 20% of all output tokens). 661 of those calls were read -> whole-file rewrite of an existing file >20k chars (24.9M chars, ~$155): 130k-char agent/model_metadata.py, 109k model_setup_flows.py, 107k google_chat/adapter.py, re-sent in full to change a few lines. Meanwhile patch succeeded 4,602/4,617 (99.7%) at 1.3k chars per call. The tool result is the only place the model reads, so the cost signal goes there. When an overwrite targets an existing file and both old and new content are >=20k chars, write_file diffs them by line (difflib, autojunk off) and, if >=80% of lines are unchanged, adds a `hint` naming the unchanged count, the changed count, the size, and "use patch". The write itself is unchanged and still happens; new files, small files and genuine rewrites get no hint. 300 ms on the 119k-char model_metadata.py. Tests (2): a 40k-char file with 2 lines changed gets the hint and is written; a new file, a small file and a wholly rewritten file get none.
This commit is contained in:
34
tests/tools/test_write_file_rewrite_hint.py
Normal file
34
tests/tools/test_write_file_rewrite_hint.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""write_file tells the caller when it just re-sent a large file that was already on disk.
|
||||
|
||||
In one 1,393-agent run 661 read->whole-file-rewrites of >20k-char files cost ~25M output chars (~$155)
|
||||
while `patch` (1.3k chars/call) succeeded 99.7% of the time; the tool result is the only place to say so.
|
||||
"""
|
||||
import json
|
||||
|
||||
from tools.file_tools import write_file_tool
|
||||
|
||||
|
||||
def _big(n_lines=800):
|
||||
return "\n".join(f"line {i}: " + "x" * 40 for i in range(n_lines)) + "\n" # ~40k chars
|
||||
|
||||
|
||||
def test_rewrite_of_a_large_file_with_few_changes_gets_a_patch_hint(tmp_path):
|
||||
f = tmp_path / "mod.py"
|
||||
old = _big()
|
||||
f.write_text(old, encoding="utf-8")
|
||||
new = old.replace("line 400:", "line 400 (edited):").replace("line 401:", "line 401 (edited):")
|
||||
r = json.loads(write_file_tool(str(f), new, task_id="t"))
|
||||
assert r.get("error") is None and f.read_text(encoding="utf-8") == new # the write still happens
|
||||
assert "use patch" in r["hint"] and "798 of 800 lines were already on disk" in r["hint"]
|
||||
|
||||
|
||||
def test_new_files_small_files_and_real_rewrites_get_no_hint(tmp_path):
|
||||
new_file = tmp_path / "new.py"
|
||||
assert "hint" not in json.loads(write_file_tool(str(new_file), _big(), task_id="t"))
|
||||
small = tmp_path / "small.py"
|
||||
small.write_text("a\nb\n", encoding="utf-8")
|
||||
assert "hint" not in json.loads(write_file_tool(str(small), "a\nc\n", task_id="t"))
|
||||
big = tmp_path / "big.py"
|
||||
big.write_text(_big(), encoding="utf-8")
|
||||
rewritten = "\n".join(f"other {i}: " + "y" * 40 for i in range(800)) + "\n"
|
||||
assert "hint" not in json.loads(write_file_tool(str(big), rewritten, task_id="t"))
|
||||
@@ -709,6 +709,40 @@ def _note_edited(task_id: str, paths: list[str], path_to_resolved: dict, session
|
||||
file_state.note_write(task_id, path_to_resolved[p])
|
||||
|
||||
|
||||
# Whole-file rewrite hint: an overwrite of an existing file this large whose new content keeps at least
|
||||
# this fraction of the old lines is a patch written the expensive way. In one 1,393-agent run 661 such
|
||||
# rewrites of >20k-char files cost ~25M output chars (~$155) where `patch` averaged 1.3k chars/call.
|
||||
_REWRITE_HINT_MIN_CHARS = 20_000
|
||||
_REWRITE_HINT_MIN_UNCHANGED = 0.80
|
||||
|
||||
|
||||
def _whole_file_rewrite_hint(resolved: str | None, new_content: str) -> str | None:
|
||||
"""Return a hint when ``new_content`` mostly re-sends what is already on disk at ``resolved``."""
|
||||
if not resolved or len(new_content) < _REWRITE_HINT_MIN_CHARS:
|
||||
return None
|
||||
try:
|
||||
old = Path(resolved).read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return None
|
||||
if len(old) < _REWRITE_HINT_MIN_CHARS:
|
||||
return None
|
||||
old_lines, new_lines = old.splitlines(), new_content.splitlines()
|
||||
if not old_lines:
|
||||
return None
|
||||
import difflib
|
||||
matcher = difflib.SequenceMatcher(None, old_lines, new_lines, autojunk=False)
|
||||
unchanged = sum(size for _, _, size in matcher.get_matching_blocks())
|
||||
ratio = unchanged / max(len(old_lines), len(new_lines))
|
||||
if ratio < _REWRITE_HINT_MIN_UNCHANGED:
|
||||
return None
|
||||
changed = max(len(old_lines), len(new_lines)) - unchanged
|
||||
return (
|
||||
f"{unchanged:,} of {len(new_lines):,} lines were already on disk ({ratio:.0%} unchanged); ~{changed:,} "
|
||||
f"line(s) actually changed. Re-sending a {len(new_content):,}-char file costs output tokens for every "
|
||||
"unchanged line; for edits like this use patch (old_string/new_string), which sends only the changed region."
|
||||
)
|
||||
|
||||
|
||||
def write_file_tool(path: str, content: str, task_id: str = "default",
|
||||
cross_profile: bool = False,
|
||||
session_id: str | None = None) -> str:
|
||||
@@ -741,9 +775,12 @@ def write_file_tool(path: str, content: str, task_id: str = "default",
|
||||
# subagents; different paths stay fully parallel.
|
||||
_lock.enter_context(file_state.lock_path(_resolved))
|
||||
warnings = _edit_warnings([path], path_to_resolved, task_id)
|
||||
rewrite_hint = _whole_file_rewrite_hint(_resolved, content)
|
||||
result_dict = _get_file_ops(task_id).write_file(_resolved or path, content).to_dict()
|
||||
if warnings:
|
||||
result_dict["_warning"] = warnings[0]
|
||||
if rewrite_hint and not result_dict.get("error"):
|
||||
result_dict["hint"] = rewrite_hint
|
||||
if _resolved:
|
||||
# Always report the ABSOLUTE path written so a wrong-cwd mismatch
|
||||
# is visible in the response instead of silently landing elsewhere.
|
||||
|
||||
Reference in New Issue
Block a user