task_protocol_service.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """TaskProtocol 命令的单进程原子执行边界。"""
  2. from __future__ import annotations
  3. import asyncio
  4. from copy import deepcopy
  5. from weakref import WeakValueDictionary
  6. from cyber_agent.core.agent_mode import require_mutable_trace_policy
  7. from cyber_agent.core.context_policy import get_authorized_context_snapshot
  8. from cyber_agent.core.task_protocol import (
  9. TaskProgress,
  10. TaskProgressRevision,
  11. append_task_progress_revision,
  12. ensure_task_protocol,
  13. task_contract_ref,
  14. )
  15. from cyber_agent.trace.protocols import TraceStore
  16. _TRACE_LOCKS: WeakValueDictionary[
  17. tuple[asyncio.AbstractEventLoop, str], asyncio.Lock
  18. ] = WeakValueDictionary()
  19. class TaskProtocolStateError(RuntimeError):
  20. """TaskProtocol 命令不能在当前持久化状态执行。"""
  21. class TaskProtocolService:
  22. """将协议领域纯函数与 TraceStore 持久化组合起来。"""
  23. def __init__(self, trace_store: TraceStore, event_service=None) -> None:
  24. self.trace_store = trace_store
  25. self.event_service = event_service
  26. def _lock(self, trace_id: str) -> asyncio.Lock:
  27. key = (asyncio.get_running_loop(), trace_id)
  28. lock = _TRACE_LOCKS.get(key)
  29. if lock is None:
  30. lock = asyncio.Lock()
  31. _TRACE_LOCKS[key] = lock
  32. return lock
  33. async def update_progress(
  34. self,
  35. trace_id: str,
  36. *,
  37. expected_revision: int,
  38. progress: TaskProgress | dict,
  39. effective_at_sequence: int,
  40. ) -> TaskProgressRevision:
  41. async with self._lock(trace_id):
  42. trace = await self.trace_store.get_trace(trace_id)
  43. if trace is None:
  44. raise TaskProtocolStateError(f"Trace not found: {trace_id}")
  45. policy = require_mutable_trace_policy(trace.context)
  46. if not policy.requires_task_progress:
  47. raise TaskProtocolStateError(
  48. "update_task_progress requires Recursive revision 3"
  49. )
  50. if (
  51. trace.agent_type == "validator"
  52. or trace.context.get("created_by_tool") == "validator"
  53. ):
  54. raise TaskProtocolStateError(
  55. "Validator traces cannot update TaskProgress"
  56. )
  57. parsed = TaskProgress.model_validate(progress)
  58. root_trace_id = trace.context.get("root_trace_id") or trace.trace_id
  59. for item in (
  60. *parsed.questions,
  61. *parsed.blockers,
  62. *parsed.findings,
  63. *parsed.hypotheses,
  64. *parsed.work_items,
  65. ):
  66. for ref in item.context_refs:
  67. get_authorized_context_snapshot(
  68. trace.context,
  69. ref_id=ref.ref_id,
  70. version=ref.version,
  71. root_trace_id=root_trace_id,
  72. uid=trace.uid,
  73. )
  74. next_context = deepcopy(trace.context)
  75. state = ensure_task_protocol(next_context)
  76. if state["pending_reviews"]:
  77. raise TaskProtocolStateError(
  78. "Review all child TaskReports before updating TaskProgress"
  79. )
  80. if state["next_actions"]:
  81. raise TaskProtocolStateError(
  82. "Execute every approved next action before updating TaskProgress"
  83. )
  84. if state["task_report"] is not None:
  85. raise TaskProtocolStateError(
  86. "TaskProgress cannot change after TaskReport submission"
  87. )
  88. revision = append_task_progress_revision(
  89. state,
  90. parsed,
  91. expected_revision=expected_revision,
  92. effective_at_sequence=effective_at_sequence,
  93. contract_ref=task_contract_ref(
  94. state,
  95. root_task_anchor_hash=next_context.get(
  96. "root_task_anchor_hash"
  97. ),
  98. ),
  99. )
  100. await self.trace_store.update_trace(trace_id, context=next_context)
  101. if self.event_service is not None:
  102. await self.event_service.emit_after_commit(
  103. source_trace_id=trace_id,
  104. event_type="task.progress.revised",
  105. event_key=(
  106. f"task.progress.revised:{trace_id}:{revision.revision}"
  107. ),
  108. effective_at_sequence=revision.effective_at_sequence,
  109. payload={"revision": revision.model_dump(mode="json")},
  110. )
  111. return revision