context_policy.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """Recursive 任务委托的最小上下文继承策略。
  2. `agent()` 创建子任务和父 Agent 批准后续任务时共用本模块,只规范化 TaskBrief
  3. 并继承父级硬约束,不复制父级完整消息、GoalTree 或内部运行对象。
  4. """
  5. from __future__ import annotations
  6. import json
  7. from typing import Any
  8. from pydantic import ValidationError
  9. from cyber_agent.core.task_protocol import TaskBrief
  10. MAX_TASK_BRIEF_CHARS = 16_000
  11. class ContextPolicyError(ValueError):
  12. """TaskBrief 无法安全穿过 Recursive 委托边界。
  13. `agent()` 或 `review_task_result` 捕获该异常后拒绝创建/批准不受控的子任务。
  14. """
  15. def _stable_unique(items: list[str]) -> list[str]:
  16. return list(dict.fromkeys(items))
  17. def normalize_task_brief(
  18. task_brief: TaskBrief | dict[str, Any] | str,
  19. *,
  20. parent_task_brief: TaskBrief | dict[str, Any] | None = None,
  21. max_chars: int = MAX_TASK_BRIEF_CHARS,
  22. ) -> TaskBrief:
  23. """校验并规范化 TaskBrief,仅向下继承父级硬约束。
  24. `agent()` 解析任务、`review_task_result` 批准下一步和续跑任务时调用,同时执行 JSON 与 16K 边界检查。
  25. """
  26. if max_chars <= 0:
  27. raise ContextPolicyError("max_chars must be greater than zero")
  28. try:
  29. raw_brief = (
  30. json.loads(task_brief)
  31. if isinstance(task_brief, str)
  32. else task_brief
  33. )
  34. brief = TaskBrief.model_validate(raw_brief)
  35. parent = (
  36. TaskBrief.model_validate(parent_task_brief)
  37. if parent_task_brief is not None
  38. else None
  39. )
  40. except (json.JSONDecodeError, TypeError, ValidationError) as exc:
  41. raise ContextPolicyError(f"Invalid TaskBrief: {exc}") from exc
  42. normalized = brief.model_copy(update={
  43. "completion_criteria": _stable_unique(brief.completion_criteria),
  44. "expected_outputs": _stable_unique(brief.expected_outputs),
  45. "parent_findings": _stable_unique(brief.parent_findings),
  46. "constraints": _stable_unique([
  47. *(parent.constraints if parent else []),
  48. *brief.constraints,
  49. ]),
  50. })
  51. try:
  52. encoded = json.dumps(
  53. normalized.model_dump(mode="json"),
  54. ensure_ascii=False,
  55. sort_keys=True,
  56. separators=(",", ":"),
  57. allow_nan=False,
  58. )
  59. except (TypeError, ValueError) as exc:
  60. raise ContextPolicyError(
  61. f"TaskBrief must be JSON serializable: {exc}"
  62. ) from exc
  63. if len(encoded) > max_chars:
  64. raise ContextPolicyError(
  65. f"TaskBrief exceeds the {max_chars}-character context limit"
  66. )
  67. return normalized
  68. def task_briefs_match(
  69. actual: TaskBrief | dict[str, Any] | str,
  70. approved: TaskBrief | dict[str, Any] | str,
  71. *,
  72. parent_task_brief: TaskBrief | dict[str, Any] | None = None,
  73. ) -> bool:
  74. """用同一继承策略规范化后,精确比较实际与已批准 TaskBrief。
  75. Sub-Agent 在消费 `next_actions` 或执行 `REVISE_CHILD` 前调用,防止模型篡改父级审批内容。
  76. """
  77. actual_brief = normalize_task_brief(
  78. actual,
  79. parent_task_brief=parent_task_brief,
  80. )
  81. approved_brief = normalize_task_brief(
  82. approved,
  83. parent_task_brief=parent_task_brief,
  84. )
  85. return actual_brief.model_dump(mode="json") == approved_brief.model_dump(
  86. mode="json"
  87. )