task_protocol.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. """Recursive revision 2 的父子任务协议与持久化状态。
  2. Sub-Agent 使用 TaskBrief 下发任务,子 Agent 通过 TaskReport 上报,父 Agent 使用
  3. TaskReview 审核。Runner 和两个协议工具共同把待审、后续动作和回退状态保存在 Trace context。
  4. """
  5. from __future__ import annotations
  6. from copy import deepcopy
  7. from datetime import datetime
  8. from typing import Any, Literal
  9. from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
  10. from cyber_agent.core.validation import RetryFrom
  11. TaskOutcome = Literal["satisfied", "partial", "failed", "protocol_error"]
  12. NextStepDirection = Literal["DESCEND", "REFINE", "ASCEND", "NONE"]
  13. ReviewDecision = Literal[
  14. "ACCEPT_REFINE",
  15. "REJECT_REFINE",
  16. "REVISE_CHILD",
  17. "DESCEND_AGAIN",
  18. "REPLAN_CURRENT",
  19. "ASCEND",
  20. "FAIL",
  21. ]
  22. class StrictProtocolModel(BaseModel):
  23. """协议模型的统一严格基类,拒绝未定义字段并清理字符串空白。"""
  24. model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
  25. class TaskBrief(StrictProtocolModel):
  26. """父 Agent 下发给直接孩子的结构化任务说明。
  27. `agent()` 在创建/续跑子 Trace 前规范化,Runner 再将格式化内容作为子 Agent 的任务上下文。
  28. """
  29. objective: str = Field(min_length=1)
  30. reason: str = Field(min_length=1)
  31. completion_criteria: list[str] = Field(min_length=1)
  32. expected_outputs: list[str] = Field(min_length=1)
  33. parent_findings: list[str] = Field(default_factory=list)
  34. context: dict[str, Any] = Field(default_factory=dict)
  35. constraints: list[str] = Field(default_factory=list)
  36. @field_validator(
  37. "completion_criteria",
  38. "expected_outputs",
  39. "parent_findings",
  40. "constraints",
  41. )
  42. @classmethod
  43. def validate_non_empty_items(cls, items: list[str]) -> list[str]:
  44. if any(not item.strip() for item in items):
  45. raise ValueError("items must be non-empty strings")
  46. return items
  47. class Validation(StrictProtocolModel):
  48. """子 Agent 在 TaskReport 中提交的执行者自检。
  49. `submit_task_report` 校验并持久化它;它不等于框架独立 Validator 产生的权威 ValidationResult。
  50. """
  51. hard_passed: bool
  52. open_issues: list[str] = Field(default_factory=list)
  53. @field_validator("open_issues")
  54. @classmethod
  55. def validate_open_issues(cls, items: list[str]) -> list[str]:
  56. if any(not item.strip() for item in items):
  57. raise ValueError("open_issues must contain non-empty strings")
  58. return items
  59. class NextStepSuggestion(StrictProtocolModel):
  60. """子 Agent 对直接父级的下一步建议,本身不会创建同级任务。
  61. 父 Agent 在 `review_task_result` 中结合独立验收决定是否采纳、修订或上行。
  62. """
  63. direction: NextStepDirection
  64. reason: str = Field(min_length=1)
  65. suggested_next_task: TaskBrief | None = None
  66. @model_validator(mode="after")
  67. def validate_suggested_task(self) -> "NextStepSuggestion":
  68. if self.direction in {"DESCEND", "REFINE"} and not self.suggested_next_task:
  69. raise ValueError("DESCEND/REFINE requires suggested_next_task")
  70. if self.direction in {"ASCEND", "NONE"} and self.suggested_next_task:
  71. raise ValueError("ASCEND/NONE does not accept suggested_next_task")
  72. return self
  73. class TaskReportSubmission(StrictProtocolModel):
  74. """子 Agent 可提交的 TaskReport 负载,不允许模型填写 Trace ID。
  75. `submit_task_report` 接收该结构,检查结果与自检是否一致后由框架注入子 Trace ID。
  76. """
  77. summary: str = Field(min_length=1)
  78. outcome: TaskOutcome
  79. validation: Validation
  80. next_step_suggestion: NextStepSuggestion
  81. outputs: list[dict[str, Any]] = Field(default_factory=list)
  82. evidence: list[dict[str, Any]] = Field(default_factory=list)
  83. remaining_issues: list[str] = Field(default_factory=list)
  84. @field_validator("remaining_issues")
  85. @classmethod
  86. def validate_remaining_issues(cls, items: list[str]) -> list[str]:
  87. if any(not item.strip() for item in items):
  88. raise ValueError("remaining_issues must contain non-empty strings")
  89. return items
  90. @model_validator(mode="after")
  91. def validate_outcome_consistency(self) -> "TaskReportSubmission":
  92. if self.outcome == "satisfied" and (
  93. not self.validation.hard_passed
  94. or self.validation.open_issues
  95. or self.remaining_issues
  96. ):
  97. raise ValueError(
  98. "satisfied requires hard_passed with no open or remaining issues"
  99. )
  100. if self.outcome in {"failed", "protocol_error"} and self.validation.hard_passed:
  101. raise ValueError(f"{self.outcome} cannot have hard_passed=true")
  102. return self
  103. class TaskReport(TaskReportSubmission):
  104. """框架补全 `child_trace_id` 后的权威子任务报告。
  105. Sub-Agent 汇合孩子结果后将其与 ValidationResult 一起写入父 Trace 的待审队列。
  106. """
  107. child_trace_id: str = Field(min_length=1)
  108. class TaskReview(StrictProtocolModel):
  109. """父 Agent 对一份直接孩子报告的结构化审核结果。
  110. `review_task_result` 根据 ValidationResult 的决策矩阵构建它,然后持久化修订、新任务或回退动作。
  111. """
  112. parent_trace_id: str = Field(min_length=1)
  113. child_trace_id: str = Field(min_length=1)
  114. decision: ReviewDecision
  115. reason: str = Field(min_length=1)
  116. approved_next_task: TaskBrief | None = None
  117. retry_from: RetryFrom | None = None
  118. @model_validator(mode="after")
  119. def validate_approved_next_task(self) -> "TaskReview":
  120. if self.decision in {"ACCEPT_REFINE", "DESCEND_AGAIN"}:
  121. if not self.approved_next_task:
  122. raise ValueError(f"{self.decision} requires approved_next_task")
  123. elif self.decision != "REVISE_CHILD" and self.approved_next_task:
  124. raise ValueError(
  125. f"{self.decision} does not accept approved_next_task"
  126. )
  127. if self.decision == "REPLAN_CURRENT":
  128. if self.retry_from is None:
  129. raise ValueError("REPLAN_CURRENT requires retry_from")
  130. elif self.retry_from is not None:
  131. raise ValueError(f"{self.decision} does not accept retry_from")
  132. return self
  133. def new_task_protocol(task_brief: TaskBrief | dict | None = None) -> dict[str, Any]:
  134. """创建一份完整的 Recursive 任务协议初始状态。
  135. Runner 初始化根 Trace,Sub-Agent 创建子 Trace 时调用,状态随 Trace context 持久化。
  136. """
  137. brief = TaskBrief.model_validate(task_brief).model_dump() if task_brief else None
  138. return {
  139. "task_brief": brief,
  140. "task_report": None,
  141. "task_report_submitted_at_sequence": None,
  142. "task_report_validation": None,
  143. "report_history": [],
  144. "pending_reviews": {},
  145. "reviews": [],
  146. "next_actions": [],
  147. "pending_replans": [],
  148. "root_validation_attempts": 0,
  149. "root_validation_history": [],
  150. "root_validation_passed": False,
  151. "protocol_correction_attempts": 0,
  152. }
  153. def ensure_task_protocol(context: dict[str, Any]) -> dict[str, Any]:
  154. """获取 Trace context 中的协议状态,并幂等补齐缺失字段。
  155. Runner、Sub-Agent 和报告/审核工具在读写任务状态前统一调用此入口。
  156. """
  157. state = context.get("task_protocol")
  158. if not isinstance(state, dict):
  159. state = new_task_protocol()
  160. context["task_protocol"] = state
  161. defaults = new_task_protocol()
  162. for key, value in defaults.items():
  163. state.setdefault(key, deepcopy(value))
  164. return state
  165. def rebuild_pending_replans(state: dict[str, Any]) -> list[dict[str, Any]]:
  166. """在 Trace 回溯截断审核历史后,重建尚未解决的重规划请求。
  167. `AgentRunner._rewind` 清理截断点之后的协议记录时调用,避免丢失同批次的 `REPLAN_CURRENT`。
  168. """
  169. pending_batches = {
  170. entry.get("received_at_sequence")
  171. for entry in state.get("pending_reviews", {}).values()
  172. }
  173. pending_batches.discard(None)
  174. if not pending_batches:
  175. return []
  176. replans = []
  177. for review in state.get("reviews", []):
  178. pending_entry = review.get("pending_review", {})
  179. if (
  180. review.get("decision") == "REPLAN_CURRENT"
  181. and pending_entry.get("received_at_sequence") in pending_batches
  182. ):
  183. replans.append({
  184. "child_trace_id": review.get("child_trace_id"),
  185. "goal_id": pending_entry.get("goal_id"),
  186. "retry_from": review.get("retry_from"),
  187. "requested_at_sequence": review.get("reviewed_at_sequence", 0),
  188. "received_at_sequence": pending_entry.get(
  189. "received_at_sequence"
  190. ),
  191. })
  192. return replans
  193. def protocol_error_report(child_trace_id: str, reason: str) -> TaskReport:
  194. """在子 Agent 未能提交合法报告时生成框架级失败报告。
  195. Runner 的完成门禁和 Sub-Agent 结果汇合阶段调用,使父级仍能审核并恢复。
  196. """
  197. return TaskReport(
  198. child_trace_id=child_trace_id,
  199. summary="Task protocol failed before a valid report was submitted.",
  200. outcome="protocol_error",
  201. validation=Validation(hard_passed=False, open_issues=[reason]),
  202. next_step_suggestion=NextStepSuggestion(
  203. direction="ASCEND",
  204. reason="Parent review is required to revise or fail this task.",
  205. ),
  206. remaining_issues=[reason],
  207. )
  208. def stopped_task_report(child_trace_id: str, reason: str) -> TaskReport:
  209. """为被协作式停止的子 Agent 生成 `failed` TaskReport。
  210. Sub-Agent 的停止汇合路径调用,然后交由父 Agent 选择修订孩子或失败结束。
  211. """
  212. return TaskReport(
  213. child_trace_id=child_trace_id,
  214. summary="Child Agent execution was stopped before completion.",
  215. outcome="failed",
  216. validation=Validation(hard_passed=False, open_issues=[reason]),
  217. next_step_suggestion=NextStepSuggestion(
  218. direction="NONE",
  219. reason="Parent review must revise the stopped child or fail the task.",
  220. ),
  221. remaining_issues=[reason],
  222. )
  223. def pending_review_entry(
  224. *,
  225. goal_id: str | None,
  226. report: TaskReport,
  227. validation_result: dict[str, Any],
  228. received_at_sequence: int,
  229. ) -> dict[str, Any]:
  230. """组装父 Trace `pending_reviews` 中的一份待审记录。
  231. Sub-Agent 完成子 Trace 验收后调用,将 TaskReport、ValidationResult、原 Goal 和序列绑定。
  232. """
  233. return {
  234. "goal_id": goal_id,
  235. "task_report": report.model_dump(),
  236. "validation_result": validation_result,
  237. "received_at_sequence": received_at_sequence,
  238. "received_at": datetime.now().isoformat(),
  239. }
  240. def format_task_brief(task_brief: TaskBrief) -> str:
  241. """将结构化 TaskBrief 格式化为子 Agent 可执行的任务文本。
  242. ``agent`` 完成规范化后调用,生成 Runner 作为 user message 传给子 Agent 的任务说明。
  243. """
  244. criteria = "\n".join(
  245. f"{index}. {item}"
  246. for index, item in enumerate(task_brief.completion_criteria, start=1)
  247. )
  248. constraints = "\n".join(f"- {item}" for item in task_brief.constraints)
  249. expected_outputs = "\n".join(
  250. f"- {item}" for item in task_brief.expected_outputs
  251. )
  252. parent_findings = "\n".join(
  253. f"- {item}" for item in task_brief.parent_findings
  254. )
  255. parts = [
  256. "# Task Brief",
  257. f"\n## Objective\n{task_brief.objective}",
  258. f"\n## Reason\n{task_brief.reason}",
  259. f"\n## Completion Criteria\n{criteria}",
  260. f"\n## Expected Outputs\n{expected_outputs}",
  261. ]
  262. if parent_findings:
  263. parts.append(f"\n## Parent Findings\n{parent_findings}")
  264. if task_brief.context:
  265. parts.append(f"\n## Context\n{task_brief.context}")
  266. if constraints:
  267. parts.append(f"\n## Constraints\n{constraints}")
  268. parts.append(
  269. "\nSubmit a validated TaskReport with submit_task_report before finishing."
  270. )
  271. return "\n".join(parts)