fix: prevent unchanged execution retry loops

This commit is contained in:
wuyang
2026-07-26 13:49:30 +08:00
parent 8a31a0edb6
commit f149379cf7
2 changed files with 170 additions and 3 deletions
+100 -2
View File
@@ -268,12 +268,23 @@ async def test_failed_execution_must_be_repaired_before_completion(settings) ->
]
}
premature = {"choices": [{"message": {"role": "assistant", "content": "已完成"}, "finish_reason": "stop"}]}
repair = {
"choices": [
{
"message": {
"role": "assistant",
"tool_calls": [tool_call("4", "write_file", {"path": "sort.py", "content": "fixed"})],
},
"finish_reason": "tool_calls",
}
]
}
repaired_exec = {
"choices": [
{
"message": {
"role": "assistant",
"tool_calls": [tool_call("4", "exec", {"command": "python3 sort.py"})],
"tool_calls": [tool_call("5", "exec", {"command": "python3 sort.py"})],
},
"finish_reason": "tool_calls",
}
@@ -282,7 +293,7 @@ async def test_failed_execution_must_be_repaired_before_completion(settings) ->
final = {
"choices": [{"message": {"role": "assistant", "content": "脚本运行和报告验证完成"}, "finish_reason": "stop"}]
}
provider = ScriptedProvider([write, read, failed_exec, premature, repaired_exec, final])
provider = ScriptedProvider([write, read, failed_exec, premature, repair, repaired_exec, final])
registry = FailingExecutionRegistry()
store = RuntimeStore(settings.database_url)
await store.initialize()
@@ -303,6 +314,9 @@ async def test_failed_execution_must_be_repaired_before_completion(settings) ->
assert answer == "脚本运行和报告验证完成"
assert registry.exec_calls == 2
assert events.count("completion.rejected") == 1
direct_recovery = provider.requests[3]["messages"][-1]
assert direct_recovery["role"] == "user"
assert "Execution recovery is required" in direct_recovery["content"]
recovery = provider.requests[4]["messages"][-1]
assert recovery["role"] == "user"
assert "SyntaxError" in recovery["content"]
@@ -310,6 +324,90 @@ async def test_failed_execution_must_be_repaired_before_completion(settings) ->
await store.close()
async def test_unchanged_failed_command_is_blocked_until_a_repair(settings) -> None:
write = {
"choices": [
{
"message": {
"role": "assistant",
"tool_calls": [tool_call("1", "write_file", {"path": "sort.py", "content": "broken"})],
},
"finish_reason": "tool_calls",
}
]
}
failed_exec = {
"choices": [
{
"message": {
"role": "assistant",
"tool_calls": [tool_call("2", "exec", {"command": "python3 sort.py"})],
},
"finish_reason": "tool_calls",
}
]
}
repeated_exec = {
"choices": [
{
"message": {
"role": "assistant",
"tool_calls": [tool_call("3", "exec", {"command": "python3 sort.py"})],
},
"finish_reason": "tool_calls",
}
]
}
repair = {
"choices": [
{
"message": {
"role": "assistant",
"tool_calls": [tool_call("4", "write_file", {"path": "sort.py", "content": "fixed"})],
},
"finish_reason": "tool_calls",
}
]
}
successful_exec = {
"choices": [
{
"message": {
"role": "assistant",
"tool_calls": [tool_call("5", "exec", {"command": "python3 sort.py"})],
},
"finish_reason": "tool_calls",
}
]
}
final = {"choices": [{"message": {"role": "assistant", "content": "验证完成"}, "finish_reason": "stop"}]}
provider = ScriptedProvider([write, failed_exec, repeated_exec, repair, successful_exec, final])
registry = FailingExecutionRegistry()
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="c1",
callback=callback,
)
assert answer == "验证完成"
assert registry.exec_calls == 2
assert [name for name, _ in registry.calls] == ["write_file", "exec", "write_file", "exec"]
blocked_result = provider.requests[3]["messages"][-2]
assert blocked_result["role"] == "tool"
assert "unchanged retry" in blocked_result["content"]
finally:
await store.close()
async def test_script_delivery_requires_successful_execution(settings) -> None:
write = {
"choices": [