已合并
feat(orchestrator): verify失败原因回灌到executor重试提示词 #29
shenmin创建于 21 天前
feat(orchestrator): verify失败原因回灌到executor重试提示词 #29
已合并
shenmin创建于 21 天前
共 2 个文件变更+47-0
@@ -49,6 +49,7 @@ from dataclasses import dataclass
49import yaml49import yaml
50 50 
51import orchestrator as orch # 复用工作区快照与事件日志工具;agent 仅由 orchestrator 执行51import orchestrator as orch # 复用工作区快照与事件日志工具;agent 仅由 orchestrator 执行
52+from verdict_common import verdict_file_path
52 53 
53TOP_KEYS = ("workflow", "max_parallel", "nodes")54TOP_KEYS = ("workflow", "max_parallel", "nodes")
54OPTIONAL_TOP_KEYS = ("max_rollbacks", "system_prompt") # 顶层可选键55OPTIONAL_TOP_KEYS = ("max_rollbacks", "system_prompt") # 顶层可选键
@@ -510,10 +511,24 @@ class PromptContext:
510 """提示词可选上下文:回滚建议、全局指令与完整任务标识。"""511 """提示词可选上下文:回滚建议、全局指令与完整任务标识。"""
511 512 
512 advice: str = None513 advice: str = None
514+ failure_reason: str = None
513 system_prompt: str = None515 system_prompt: str = None
514 task_id: str = None516 task_id: str = None
515 517 
516 518 
519+def read_verdict_failure_reason(work_dir, task_id):
520+ """读取任务最近保留的失败裁决原因;无有效原因时返回 None。"""
521+ try:
522+ with open(verdict_file_path(work_dir, task_id), encoding="utf-8") as f:
523+ verdict = json.load(f)
524+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
525+ return None
526+ if not isinstance(verdict, dict) or verdict.get("verdict") != "fail":
527+ return None
528+ reason = verdict.get("reason")
529+ return reason.strip() if isinstance(reason, str) and reason.strip() else None
530+ 
531+ 
517def build_prompt(node, phase, work_dir, user_prompt, context=None):532def build_prompt(node, phase, work_dir, user_prompt, context=None):
518 """构建 execute 或 verify 阶段提示词。533 """构建 execute 或 verify 阶段提示词。
519 534 
@@ -524,6 +539,7 @@ def build_prompt(node, phase, work_dir, user_prompt, context=None):
524 """539 """
525 context = context or PromptContext()540 context = context or PromptContext()
526 advice = context.advice541 advice = context.advice
542+ failure_reason = context.failure_reason
527 system_prompt = context.system_prompt543 system_prompt = context.system_prompt
528 544 
529 def block(name, items):545 def block(name, items):
@@ -535,6 +551,8 @@ def build_prompt(node, phase, work_dir, user_prompt, context=None):
535 parts += ["[%s] %s" % (node["id"], node["title"]), block("Goal", node["goal"])]551 parts += ["[%s] %s" % (node["id"], node["title"]), block("Goal", node["goal"])]
536 if phase == "execute":552 if phase == "execute":
537 parts.append(block("Approach", node["approach"]))553 parts.append(block("Approach", node["approach"]))
554+ if failure_reason:
555+ parts.append("Previous-Verification-Failure: %s" % failure_reason)
538 if advice:556 if advice:
539 with open(advice, encoding="utf-8") as f:557 with open(advice, encoding="utf-8") as f:
540 advice_content = f.read()558 advice_content = f.read()
@@ -764,6 +782,7 @@ def main():
764 "agent": node["executor"] if phase == "execute" else node["verifier"],782 "agent": node["executor"] if phase == "execute" else node["verifier"],
765 "prompt": build_prompt(node, phase, work_dir, up_path, PromptContext(783 "prompt": build_prompt(node, phase, work_dir, up_path, PromptContext(
766 advice=tasks[tid].get("advice"),784 advice=tasks[tid].get("advice"),
785+ failure_reason=read_verdict_failure_reason(work_dir, tid),
767 system_prompt=config.get("system_prompt"), task_id=tid)),786 system_prompt=config.get("system_prompt"), task_id=tid)),
768 })787 })
769 if phase == "execute":788 if phase == "execute":
@@ -822,6 +822,34 @@ class BuildPromptTest(unittest.TestCase):
822 self.assertIn("Rollback-Advice:", p)822 self.assertIn("Rollback-Advice:", p)
823 self.assertIn(advice.read_text(), p)823 self.assertIn(advice.read_text(), p)
824 824 
825+ def test_execute_prompt_includes_latest_verdict_failure_reason(self):
826+ with tempfile.TemporaryDirectory() as wd:
827+ verdict = Path(verdict_file_path(wd, "a"))
828+ verdict.parent.mkdir(parents=True)
829+ verdict.write_text(json.dumps({"verdict": "fail", "reason": "报告未生成"}))
830+ reason = get_task.read_verdict_failure_reason(wd, "a")
831+ p = get_task.build_prompt(
832+ self.NODE, "execute", wd, "/up",
833+ get_task.PromptContext(failure_reason=reason))
834+ self.assertIn("Previous-Verification-Failure:", p)
835+ self.assertIn("报告未生成", p)
836+ 
837+ def test_verify_prompt_omits_verdict_failure_reason(self):
838+ p = get_task.build_prompt(
839+ self.NODE, "verify", "/wd", "/up",
840+ get_task.PromptContext(failure_reason="报告未生成"))
841+ self.assertNotIn("Previous-Verification-Failure", p)
842+ 
843+ def test_missing_or_non_fail_verdict_has_no_failure_reason(self):
844+ with tempfile.TemporaryDirectory() as wd:
845+ self.assertIsNone(get_task.read_verdict_failure_reason(wd, "a"))
846+ verdict = Path(verdict_file_path(wd, "a"))
847+ verdict.parent.mkdir(parents=True)
848+ verdict.write_text(json.dumps({"verdict": "pass"}))
849+ self.assertIsNone(get_task.read_verdict_failure_reason(wd, "a"))
850+ verdict.write_text("not json")
851+ self.assertIsNone(get_task.read_verdict_failure_reason(wd, "a"))
852+ 
825 def test_verify_prompt_omits_advice(self):853 def test_verify_prompt_omits_advice(self):
826 p = get_task.build_prompt(self.NODE, "verify", "/wd", "/up",854 p = get_task.build_prompt(self.NODE, "verify", "/wd", "/up",
827 get_task.PromptContext(advice="/wd/.workflow/advice/x.md"))855 get_task.PromptContext(advice="/wd/.workflow/advice/x.md"))