fix: make invalid source retries actionable
This commit is contained in:
@@ -409,7 +409,15 @@ def python_source_error(arguments: dict[str, Any]) -> str | None:
|
|||||||
ast.parse(content, filename=path)
|
ast.parse(content, filename=path)
|
||||||
except SyntaxError as exc:
|
except SyntaxError as exc:
|
||||||
location = f"line {exc.lineno}" if exc.lineno else "unknown line"
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -580,6 +588,7 @@ class AgentLoop:
|
|||||||
unresolved_execution_failure: str | None = None
|
unresolved_execution_failure: str | None = None
|
||||||
failed_execution_revisions: dict[str, int] = {}
|
failed_execution_revisions: dict[str, int] = {}
|
||||||
successful_write_fingerprints: set[str] = set()
|
successful_write_fingerprints: set[str] = set()
|
||||||
|
failed_write_fingerprints: set[str] = set()
|
||||||
consecutive_checkpoint_rejections = 0
|
consecutive_checkpoint_rejections = 0
|
||||||
for iteration in range(max_iterations):
|
for iteration in range(max_iterations):
|
||||||
await recorder.emit(
|
await recorder.emit(
|
||||||
@@ -697,6 +706,9 @@ class AgentLoop:
|
|||||||
):
|
):
|
||||||
blocked_call_reasons[call_id] = "duplicate_write"
|
blocked_call_reasons[call_id] = "duplicate_write"
|
||||||
continue
|
continue
|
||||||
|
if fingerprint in failed_write_fingerprints:
|
||||||
|
blocked_call_reasons[call_id] = "failed_write_retry"
|
||||||
|
continue
|
||||||
if fingerprint:
|
if fingerprint:
|
||||||
batch_write_fingerprints.add(fingerprint)
|
batch_write_fingerprints.add(fingerprint)
|
||||||
key = execution_call_key(name, arguments) if isinstance(arguments, dict) else None
|
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
|
key = execution_call_key(name, arguments) if isinstance(arguments, dict) else None
|
||||||
if key:
|
if key:
|
||||||
failed_execution_revisions[key] = mutation_count
|
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(
|
messages.append(
|
||||||
{
|
{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
@@ -913,6 +929,15 @@ class AgentLoop:
|
|||||||
"already requested it without an intervening repair."
|
"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":
|
elif blocked_reason == "failed_retry":
|
||||||
result = {
|
result = {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
|
|||||||
@@ -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"})
|
error = python_source_error({"path": "benchmark.py", "content": "# Report\n- O(n²)\n"})
|
||||||
assert error is not None
|
assert error is not None
|
||||||
assert "invalid character" in error
|
assert "invalid character" in error
|
||||||
|
assert "2: - O(n²)" in error
|
||||||
assert python_source_error({"path": "benchmark.py", "content": "print('ok')\n"}) is None
|
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()
|
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:
|
def test_tool_events_render_one_friendly_completed_card() -> None:
|
||||||
assert tool_event_details("tool.started", {"name": "exec"}) is None
|
assert tool_event_details("tool.started", {"name": "exec"}) is None
|
||||||
rendered = tool_event_details(
|
rendered = tool_event_details(
|
||||||
|
|||||||
Reference in New Issue
Block a user