From 44fe0b3dd79b93b7e8638c13eede1a4a5216f007 Mon Sep 17 00:00:00 2001 From: wuyang <5700876+banisherwy@user.noreply.gitee.com> Date: Sun, 26 Jul 2026 14:03:01 +0800 Subject: [PATCH] fix: normalize escaped Work artifacts --- agent_platform/runtime/loop.py | 149 ++++++++++++++++++++++++++++++++- tests/test_agent_loop.py | 27 +++++- 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/agent_platform/runtime/loop.py b/agent_platform/runtime/loop.py index 04b4f8a..d677bcc 100644 --- a/agent_platform/runtime/loop.py +++ b/agent_platform/runtime/loop.py @@ -7,6 +7,7 @@ import re import uuid from collections.abc import Awaitable, Callable from dataclasses import dataclass +from pathlib import PurePosixPath from typing import Any from agent_platform.auth import UserIdentity @@ -72,6 +73,46 @@ VERIFICATION_TOOLS = {"read_file", "git_status", "git_diff", "exec", "poll_proce EXECUTION_TOOLS = {"exec", "start_process", "poll_process"} REPEAT_GUARDED_EXECUTION_TOOLS = {"exec", "start_process"} MAX_CONSECUTIVE_CHECKPOINT_REJECTIONS = 3 +CODE_FILE_SUFFIXES = { + ".c", + ".cc", + ".cpp", + ".css", + ".go", + ".h", + ".hpp", + ".html", + ".java", + ".js", + ".jsx", + ".php", + ".py", + ".rb", + ".rs", + ".sh", + ".sql", + ".svelte", + ".ts", + ".tsx", + ".vue", +} +PLAIN_TEXT_FILE_SUFFIXES = { + ".csv", + ".json", + ".jsonl", + ".md", + ".rst", + ".toml", + ".tsv", + ".txt", + ".xml", + ".yaml", + ".yml", +} +READ_ONLY_EXEC_COMMAND = re.compile( + r"^(?:cat|cut|diff|find|git\s+(?:diff|show|status)|grep|head|jq|ls|pwd|rg|sed\s+-n|stat|tail|wc)\b", + re.IGNORECASE, +) def latest_user_text(messages: list[dict[str, Any]]) -> str: @@ -156,6 +197,109 @@ def execution_call_key(name: str, arguments: dict[str, Any]) -> str | None: return f"{name}\0{cwd}\0{command}" +def decode_code_layout_escapes(content: str) -> str: + output: list[str] = [] + index = 0 + quote = "" + triple = False + escaped = False + line_comment = False + while index < len(content): + if quote: + if escaped: + output.append(content[index]) + escaped = False + index += 1 + continue + if content[index] == "\\": + output.append(content[index]) + escaped = True + index += 1 + continue + delimiter = quote * (3 if triple else 1) + if content.startswith(delimiter, index): + output.append(delimiter) + index += len(delimiter) + quote = "" + triple = False + continue + output.append(content[index]) + index += 1 + continue + + if content.startswith("\\r\\n", index): + output.append("\n") + index += 4 + line_comment = False + continue + if content.startswith("\\n", index): + output.append("\n") + index += 2 + line_comment = False + continue + if content.startswith("\\t", index): + output.append("\t") + index += 2 + continue + if line_comment: + if content[index] == "\n": + line_comment = False + output.append(content[index]) + index += 1 + continue + if content.startswith("//", index) or content[index] == "#": + line_comment = True + delimiter = "//" if content.startswith("//", index) else "#" + output.append(delimiter) + index += len(delimiter) + continue + if content[index] in {'"', "'"}: + quote = content[index] + triple = content.startswith(quote * 3, index) + delimiter = quote * (3 if triple else 1) + output.append(delimiter) + index += len(delimiter) + continue + output.append(content[index]) + index += 1 + return "".join(output) + + +def normalize_write_file_content(path: str, content: str) -> str: + literal_newlines = content.count("\\n") + content.count("\\r\\n") + actual_newlines = content.count("\n") + if literal_newlines < 2 or literal_newlines <= max(2, actual_newlines * 2): + return content + suffix = PurePosixPath(path).suffix.lower() + if suffix in CODE_FILE_SUFFIXES: + return decode_code_layout_escapes(content) + if suffix in PLAIN_TEXT_FILE_SUFFIXES: + return content.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\t", "\t") + return content + + +def normalize_tool_arguments(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if name != "write_file": + return arguments + path = arguments.get("path") + content = arguments.get("content") + if not isinstance(path, str) or not isinstance(content, str): + return arguments + normalized = normalize_write_file_content(path, content) + if normalized == content: + return arguments + return {**arguments, "content": normalized} + + +def is_substantive_execution(name: str, arguments: dict[str, Any]) -> bool: + if name == "start_process": + return True + if name != "exec": + return False + command = str(arguments.get("command", "")).strip() + return bool(command and not READ_ONLY_EXEC_COMMAND.match(command)) + + @dataclass(slots=True) class RunRecorder: store: RuntimeStore @@ -382,10 +526,10 @@ class AgentLoop: mutation_count += 1 elif name in VERIFICATION_TOOLS and mutation_count: verified_mutation = mutation_count - if name in EXECUTION_TOOLS: + if is_substantive_execution(name, arguments): successful_executions += 1 unresolved_execution_failure = None - elif name in EXECUTION_TOOLS: + elif is_substantive_execution(name, arguments): unresolved_execution_failure = f"{name}: {public_tool_summary(result, 1000)}" execution_failures.append(unresolved_execution_failure) key = execution_call_key(name, arguments) if isinstance(arguments, dict) else None @@ -458,6 +602,7 @@ class AgentLoop: except (json.JSONDecodeError, ValueError) as exc: parsed.append((index, call, name, {"__parse_error__": str(exc)}, False)) continue + arguments = normalize_tool_arguments(name, arguments) metadata = TOOL_METADATA.get(name) parallel = bool(metadata and metadata.parallel_safe) if name == "delegate_task": diff --git a/tests/test_agent_loop.py b/tests/test_agent_loop.py index 5e34d8c..9ba8b4e 100644 --- a/tests/test_agent_loop.py +++ b/tests/test_agent_loop.py @@ -7,7 +7,12 @@ from typing import Any from agent_platform.auth import UserIdentity from agent_platform.models import get_model_spec -from agent_platform.runtime.loop import AgentLoop, tool_event_details +from agent_platform.runtime.loop import ( + AgentLoop, + is_substantive_execution, + normalize_write_file_content, + tool_event_details, +) from agent_platform.runtime.tools import TOOL_METADATA from agent_platform.store import RuntimeStore @@ -20,6 +25,26 @@ def tool_call(call_id: str, name: str, arguments: dict[str, Any]) -> dict[str, A } +def test_write_file_normalizes_double_escaped_code_layout() -> None: + escaped = r'import time\n\n# benchmark\ndef main():\n\tprint("keep\ninside")\n\nmain()\n' + assert normalize_write_file_content("benchmark.py", escaped) == ( + 'import time\n\n# benchmark\ndef main():\n\tprint("keep\\ninside")\n\nmain()\n' + ) + + +def test_write_file_normalizes_double_escaped_plain_text() -> None: + assert normalize_write_file_content("report.md", r"# Report\n\nMeasured: 1.2 s\n") == ( + "# Report\n\nMeasured: 1.2 s\n" + ) + + +def test_read_only_shell_commands_do_not_satisfy_execution_evidence() -> None: + assert not is_substantive_execution("exec", {"command": "cat report.txt"}) + assert not is_substantive_execution("exec", {"command": "sed -n '1,20p' script.py"}) + assert is_substantive_execution("exec", {"command": "python3 script.py"}) + assert is_substantive_execution("start_process", {"command": "python3 server.py"}) + + class ScriptedProvider: def __init__(self, responses: list[dict[str, Any]]) -> None: self.responses = responses