fix: recover Work runs after failed verification

This commit is contained in:
wuyang
2026-07-26 13:37:51 +08:00
parent 2f6f46c2fa
commit 8a31a0edb6
7 changed files with 331 additions and 25 deletions
+85 -14
View File
@@ -32,6 +32,9 @@ relevant verification, and report concrete results. Use tools instead of inventi
command output. A claim that a file was created, changed, executed, or tested must be backed by a
successful tool result from this run. For scripts, reports, code, and other deliverables, create real
files under /workspace, inspect them after writing, run relevant checks, and cite their exact paths.
Write source files with real line breaks rather than literal `\\n` escape sequences. If any command
fails, inspect the failure, repair the underlying problem, and successfully rerun the relevant check;
never replace failed output with invented numbers, placeholders, or a hand-written success report.
Never use an interactive command such as bare `python3`, `bash`, or `node`; provide a complete
non-interactive command. Do not use `echo` merely to simulate progress. Keep the plan current. Use
background processes for servers or long jobs. Delegate bounded independent investigations when it
@@ -59,8 +62,15 @@ ACTION_REQUEST = re.compile(
r"\b(?:run|execute|test|inspect|analy[sz]e|compare|debug|deploy)\b)",
re.IGNORECASE,
)
EXECUTION_REQUEST = re.compile(
r"(运行|执行|测试|脚本|程序|代码|比较|对比|基准|"
r"\b(?:run|execute|test|script|program|code|compare|benchmark)\b)",
re.IGNORECASE,
)
MUTATION_TOOLS = {"write_file", "apply_patch"}
VERIFICATION_TOOLS = {"read_file", "list_files", "search_files", "git_status", "git_diff", "exec", "poll_process"}
VERIFICATION_TOOLS = {"read_file", "git_status", "git_diff", "exec", "poll_process"}
EXECUTION_TOOLS = {"exec", "start_process", "poll_process"}
MAX_CONSECUTIVE_CHECKPOINT_REJECTIONS = 3
def latest_user_text(messages: list[dict[str, Any]]) -> str:
@@ -78,19 +88,49 @@ def latest_user_text(messages: list[dict[str, Any]]) -> str:
return ""
def completion_failure(artifact_required: bool, mutation_count: int, verified_mutation: int) -> str | None:
def completion_failure(
artifact_required: bool,
mutation_count: int,
verified_mutation: int,
action_required: bool = False,
execution_required: bool = False,
successful_executions: int = 0,
unresolved_execution_failure: str | None = None,
) -> str | None:
reasons: list[str] = []
if artifact_required and mutation_count == 0:
return (
reasons.append(
"The user requested a concrete deliverable, but this run has no successful file-writing tool call. "
"Do not claim completion. Create the requested files with write_file or apply_patch, then inspect "
"and verify them with tools."
"Create the requested files with write_file or apply_patch."
)
if artifact_required and verified_mutation < mutation_count:
return (
elif artifact_required and verified_mutation < mutation_count:
reasons.append(
"Files changed, but no successful verification occurred after the latest change. Inspect the actual "
"files and run the relevant check before claiming completion."
"files and run the relevant check."
)
return None
if action_required and unresolved_execution_failure:
reasons.append(
"A command or process check failed and has not been followed by a successful execution. "
f"Repair and rerun it. Last failure: {unresolved_execution_failure}"
)
elif execution_required and successful_executions == 0:
reasons.append(
"The task requires a script, code, test, benchmark, or comparison, but no command or process "
"completed successfully. Run the relevant verification and use its real output."
)
if not reasons:
return None
return " Do not claim completion. ".join(reasons)
def recovery_message(failure: str) -> str:
return (
"Your attempted checkpoint was rejected by the execution policy. Continue the original task now. "
"You MUST call the appropriate tools in this response instead of repeating or paraphrasing a final answer. "
f"Evidence gap: {failure} "
"If a command failed, inspect and repair the real files, rerun the command successfully, then inspect the "
"resulting deliverables before finishing."
)
@dataclass(slots=True)
@@ -147,7 +187,7 @@ class AgentLoop:
"run.created",
{
"model_tier": spec.strength,
"strategy_version": "work-loop-v1",
"strategy_version": "work-loop-v2",
"scheduler_version": "safe-parallel-v1",
"context_policy": "recent-visible-v1",
},
@@ -200,9 +240,13 @@ class AgentLoop:
request_text = latest_user_text(messages)
artifact_required = depth == 0 and bool(ARTIFACT_REQUEST.search(request_text))
action_required = depth == 0 and bool(ACTION_REQUEST.search(request_text))
execution_required = depth == 0 and bool(EXECUTION_REQUEST.search(request_text))
mutation_count = 0
verified_mutation = -1
successful_tools = 0
successful_executions = 0
unresolved_execution_failure: str | None = None
consecutive_checkpoint_rejections = 0
for iteration in range(max_iterations):
await recorder.emit(
"model.requested",
@@ -228,13 +272,26 @@ class AgentLoop:
tool_calls = message.get("tool_calls") or []
if not tool_calls:
candidate = str(message.get("content") or "").strip()
failure = completion_failure(artifact_required, mutation_count, verified_mutation)
failure = completion_failure(
artifact_required,
mutation_count,
verified_mutation,
action_required,
execution_required,
successful_executions,
unresolved_execution_failure,
)
if not failure and action_required and successful_tools == 0:
failure = (
"The user requested an executed or inspected result, but no tool completed successfully. "
"Use the appropriate workspace tools and return evidence instead of an unverified answer."
)
if failure and iteration + 1 < max_iterations:
consecutive_checkpoint_rejections += 1
if (
failure
and iteration + 1 < max_iterations
and consecutive_checkpoint_rejections < MAX_CONSECUTIVE_CHECKPOINT_REJECTIONS
):
await recorder.emit(
"completion.rejected",
{"reason": failure, "iteration": iteration + 1, "depth": depth},
@@ -242,7 +299,7 @@ class AgentLoop:
messages.extend(
[
{"role": "assistant", "content": candidate},
{"role": "system", "content": failure},
{"role": "user", "content": recovery_message(failure)},
]
)
continue
@@ -257,6 +314,7 @@ class AgentLoop:
)
return candidate
consecutive_checkpoint_rejections = 0
assistant_message = {
"role": "assistant",
"content": message.get("content"),
@@ -279,6 +337,11 @@ class AgentLoop:
mutation_count += 1
elif name in VERIFICATION_TOOLS and mutation_count:
verified_mutation = mutation_count
if name in EXECUTION_TOOLS:
successful_executions += 1
unresolved_execution_failure = None
elif name in EXECUTION_TOOLS:
unresolved_execution_failure = f"{name}: {public_tool_summary(result, 1000)}"
messages.append(
{
"role": "tool",
@@ -299,7 +362,15 @@ class AgentLoop:
)
response = await self.provider.complete(model=spec.provider_model, messages=messages, tools=None)
answer = str(response["choices"][0].get("message", {}).get("content") or "").strip()
if completion_failure(artifact_required, mutation_count, verified_mutation):
if completion_failure(
artifact_required,
mutation_count,
verified_mutation,
action_required,
execution_required,
successful_executions,
unresolved_execution_failure,
):
return (
"未完成:工具迭代已用尽,但交付物没有形成完整的“写入后验证”证据链。"
"我不会声称文件已经完成;工作区中已有内容可从“工作区文件”查看。"