From ade5ea1210fcf0fa51222290f47f6f0819ef16d9 Mon Sep 17 00:00:00 2001 From: wuyang <5700876+banisherwy@user.noreply.gitee.com> Date: Sun, 26 Jul 2026 17:08:27 +0800 Subject: [PATCH] fix: make invalid source retries actionable --- agent_platform/runtime/loop.py | 27 +++++++++++++++++++- tests/test_agent_loop.py | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/agent_platform/runtime/loop.py b/agent_platform/runtime/loop.py index c908ff8..5f09f65 100644 --- a/agent_platform/runtime/loop.py +++ b/agent_platform/runtime/loop.py @@ -409,7 +409,15 @@ def python_source_error(arguments: dict[str, Any]) -> str | None: ast.parse(content, filename=path) except SyntaxError as exc: location = f"line {exc.lineno}" if exc.lineno else "unknown line" - return f"{exc.msg} ({location})" + source_lines = content.splitlines() + snippet = "" + if exc.lineno: + first = max(1, exc.lineno - 1) + last = min(len(source_lines), exc.lineno + 1) + snippet = "\n" + "\n".join( + f"{line_number}: {source_lines[line_number - 1][:240]}" for line_number in range(first, last + 1) + ) + return f"{exc.msg} ({location}). Source around the error:{snippet}" return None @@ -580,6 +588,7 @@ class AgentLoop: unresolved_execution_failure: str | None = None failed_execution_revisions: dict[str, int] = {} successful_write_fingerprints: set[str] = set() + failed_write_fingerprints: set[str] = set() consecutive_checkpoint_rejections = 0 for iteration in range(max_iterations): await recorder.emit( @@ -697,6 +706,9 @@ class AgentLoop: ): blocked_call_reasons[call_id] = "duplicate_write" continue + if fingerprint in failed_write_fingerprints: + blocked_call_reasons[call_id] = "failed_write_retry" + continue if fingerprint: batch_write_fingerprints.add(fingerprint) key = execution_call_key(name, arguments) if isinstance(arguments, dict) else None @@ -760,6 +772,10 @@ class AgentLoop: key = execution_call_key(name, arguments) if isinstance(arguments, dict) else None if key: failed_execution_revisions[key] = mutation_count + if not result.get("ok", False): + fingerprint = write_call_fingerprint(name, arguments) if isinstance(arguments, dict) else None + if fingerprint and not result.get("policy_duplicate", False): + failed_write_fingerprints.add(fingerprint) messages.append( { "role": "tool", @@ -913,6 +929,15 @@ class AgentLoop: "already requested it without an intervening repair." ), } + elif blocked_reason == "failed_write_retry": + result = { + "ok": False, + "policy_duplicate": True, + "error": ( + "Execution policy skipped this unchanged write because identical content already failed " + "validation in this run. Inspect the prior validation error and make a real source change." + ), + } elif blocked_reason == "failed_retry": result = { "ok": False, diff --git a/tests/test_agent_loop.py b/tests/test_agent_loop.py index 3fe8109..0cd2909 100644 --- a/tests/test_agent_loop.py +++ b/tests/test_agent_loop.py @@ -59,6 +59,7 @@ def test_invalid_complete_python_source_is_detected_before_write() -> None: error = python_source_error({"path": "benchmark.py", "content": "# Report\n- O(n²)\n"}) assert error is not None assert "invalid character" in error + assert "2: - O(n²)" in error assert python_source_error({"path": "benchmark.py", "content": "print('ok')\n"}) is None @@ -900,6 +901,51 @@ async def test_bare_interactive_exec_is_blocked_before_registry(settings) -> Non await store.close() +async def test_unchanged_invalid_python_write_is_not_revalidated(settings) -> None: + invalid_call = { + "choices": [ + { + "message": { + "role": "assistant", + "tool_calls": [tool_call("1", "write_file", {"path": "broken.py", "content": "print('x'\n"})], + }, + "finish_reason": "tool_calls", + } + ] + } + repeated_call = copy.deepcopy(invalid_call) + repeated_call["choices"][0]["message"]["tool_calls"][0]["id"] = "2" + final = {"choices": [{"message": {"role": "assistant", "content": "无法完成"}, "finish_reason": "stop"}]} + provider = ScriptedProvider([invalid_call, repeated_call, final]) + registry = RecordingRegistry() + store = RuntimeStore(settings.database_url) + await store.initialize() + + async def callback(event_type, payload): + return None + + try: + answer = await AgentLoop(provider, registry, store, max_tool_output_chars=10_000).run( + spec=get_model_spec("work-light"), + messages=[{"role": "user", "content": "你好"}], + identity=UserIdentity("u1", "", "", "user"), + raw_user_jwt="jwt", + chat_id="invalid-repeat", + callback=callback, + ) + finally: + await store.close() + + assert answer == "无法完成" + assert registry.calls == [] + repeated_result = next( + message["content"] + for message in provider.requests[2]["messages"] + if message.get("role") == "tool" and "unchanged write" in message.get("content", "") + ) + assert "already failed validation" in repeated_result + + def test_tool_events_render_one_friendly_completed_card() -> None: assert tool_event_details("tool.started", {"name": "exec"}) is None rendered = tool_event_details(