context_policy.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. """Minimal, explicit context inheritance for Recursive task delegation."""
  2. from __future__ import annotations
  3. import json
  4. from typing import Any
  5. from pydantic import ValidationError
  6. from cyber_agent.core.task_protocol import TaskBrief
  7. MAX_TASK_BRIEF_CHARS = 16_000
  8. class ContextPolicyError(ValueError):
  9. """A TaskBrief cannot cross the Recursive delegation boundary safely."""
  10. def _stable_unique(items: list[str]) -> list[str]:
  11. return list(dict.fromkeys(items))
  12. def normalize_task_brief(
  13. task_brief: TaskBrief | dict[str, Any] | str,
  14. *,
  15. parent_task_brief: TaskBrief | dict[str, Any] | None = None,
  16. max_chars: int = MAX_TASK_BRIEF_CHARS,
  17. ) -> TaskBrief:
  18. """Validate a TaskBrief and inherit only the parent's hard constraints."""
  19. if max_chars <= 0:
  20. raise ContextPolicyError("max_chars must be greater than zero")
  21. try:
  22. raw_brief = (
  23. json.loads(task_brief)
  24. if isinstance(task_brief, str)
  25. else task_brief
  26. )
  27. brief = TaskBrief.model_validate(raw_brief)
  28. parent = (
  29. TaskBrief.model_validate(parent_task_brief)
  30. if parent_task_brief is not None
  31. else None
  32. )
  33. except (json.JSONDecodeError, TypeError, ValidationError) as exc:
  34. raise ContextPolicyError(f"Invalid TaskBrief: {exc}") from exc
  35. normalized = brief.model_copy(update={
  36. "completion_criteria": _stable_unique(brief.completion_criteria),
  37. "expected_outputs": _stable_unique(brief.expected_outputs),
  38. "parent_findings": _stable_unique(brief.parent_findings),
  39. "constraints": _stable_unique([
  40. *(parent.constraints if parent else []),
  41. *brief.constraints,
  42. ]),
  43. })
  44. try:
  45. encoded = json.dumps(
  46. normalized.model_dump(mode="json"),
  47. ensure_ascii=False,
  48. sort_keys=True,
  49. separators=(",", ":"),
  50. allow_nan=False,
  51. )
  52. except (TypeError, ValueError) as exc:
  53. raise ContextPolicyError(
  54. f"TaskBrief must be JSON serializable: {exc}"
  55. ) from exc
  56. if len(encoded) > max_chars:
  57. raise ContextPolicyError(
  58. f"TaskBrief exceeds the {max_chars}-character context limit"
  59. )
  60. return normalized
  61. def task_briefs_match(
  62. actual: TaskBrief | dict[str, Any] | str,
  63. approved: TaskBrief | dict[str, Any] | str,
  64. *,
  65. parent_task_brief: TaskBrief | dict[str, Any] | None = None,
  66. ) -> bool:
  67. """Compare actual and approved briefs after applying the same policy."""
  68. actual_brief = normalize_task_brief(
  69. actual,
  70. parent_task_brief=parent_task_brief,
  71. )
  72. approved_brief = normalize_task_brief(
  73. approved,
  74. parent_task_brief=parent_task_brief,
  75. )
  76. return actual_brief.model_dump(mode="json") == approved_brief.model_dump(
  77. mode="json"
  78. )