已合并
feat(orchestrator): verify失败原因回灌到executor重试提示词 #29
shenmin创建于 21 天前
feat(orchestrator): verify失败原因回灌到executor重试提示词 #29
已合并
共 2 个文件变更+47-0
| @@ -49,6 +49,7 @@ from dataclasses import dataclass | |||
| 49 | import yaml | 49 | import yaml |
| 50 | 50 | ||
| 51 | import orchestrator as orch # 复用工作区快照与事件日志工具;agent 仅由 orchestrator 执行 | 51 | import orchestrator as orch # 复用工作区快照与事件日志工具;agent 仅由 orchestrator 执行 |
| 52 | +from verdict_common import verdict_file_path | ||
| 52 | 53 | ||
| 53 | TOP_KEYS = ("workflow", "max_parallel", "nodes") | 54 | TOP_KEYS = ("workflow", "max_parallel", "nodes") |
| 54 | OPTIONAL_TOP_KEYS = ("max_rollbacks", "system_prompt") # 顶层可选键 | 55 | OPTIONAL_TOP_KEYS = ("max_rollbacks", "system_prompt") # 顶层可选键 |
| @@ -510,10 +511,24 @@ class PromptContext: | |||
| 510 | """提示词可选上下文:回滚建议、全局指令与完整任务标识。""" | 511 | """提示词可选上下文:回滚建议、全局指令与完整任务标识。""" |
| 511 | 512 | ||
| 512 | advice: str = None | 513 | advice: str = None |
| 514 | + failure_reason: str = None | ||
| 513 | system_prompt: str = None | 515 | system_prompt: str = None |
| 514 | task_id: str = None | 516 | 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 | + | ||
| 517 | def build_prompt(node, phase, work_dir, user_prompt, context=None): | 532 | def 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.advice | 541 | advice = context.advice |
| 542 | + failure_reason = context.failure_reason | ||
| 527 | system_prompt = context.system_prompt | 543 | 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")) |