task_protocol_service.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """TaskProtocol 命令的单进程原子执行边界。"""
  2. from __future__ import annotations
  3. import asyncio
  4. from contextlib import asynccontextmanager
  5. from copy import deepcopy
  6. from collections.abc import AsyncIterator, Callable
  7. from typing import Any
  8. from weakref import WeakValueDictionary
  9. from cyber_agent.core.agent_mode import require_mutable_trace_policy
  10. from cyber_agent.core.context_policy import get_authorized_context_snapshot
  11. from cyber_agent.core.task_protocol import (
  12. TaskProgress,
  13. TaskProgressRevision,
  14. append_task_progress_revision,
  15. ensure_task_protocol,
  16. task_contract_ref,
  17. )
  18. from cyber_agent.trace.models import Trace
  19. from cyber_agent.trace.protocols import TraceStore
  20. _TRACE_LOCKS: WeakValueDictionary[
  21. tuple[asyncio.AbstractEventLoop, int, str], asyncio.Lock
  22. ] = WeakValueDictionary()
  23. class TaskProtocolStateError(RuntimeError):
  24. """TaskProtocol 命令不能在当前持久化状态执行。"""
  25. class TaskProtocolService:
  26. """将协议领域纯函数与 TraceStore 持久化组合起来。"""
  27. def __init__(self, trace_store: TraceStore, event_service=None) -> None:
  28. self.trace_store = trace_store
  29. self.event_service = event_service
  30. def _lock(self, trace_id: str) -> asyncio.Lock:
  31. key = (asyncio.get_running_loop(), id(self.trace_store), trace_id)
  32. lock = _TRACE_LOCKS.get(key)
  33. if lock is None:
  34. lock = asyncio.Lock()
  35. _TRACE_LOCKS[key] = lock
  36. return lock
  37. @asynccontextmanager
  38. async def transaction(self, trace_id: str) -> AsyncIterator[None]:
  39. """Serialize one complete TaskProtocol read-modify-write command."""
  40. async with self._lock(trace_id):
  41. yield
  42. @asynccontextmanager
  43. async def transaction_many(
  44. self,
  45. *trace_ids: str,
  46. ) -> AsyncIterator[None]:
  47. """Lock multiple protocol records in stable order to avoid deadlocks."""
  48. locks = [self._lock(item) for item in sorted(set(trace_ids))]
  49. for lock in locks:
  50. await lock.acquire()
  51. try:
  52. yield
  53. finally:
  54. for lock in reversed(locks):
  55. lock.release()
  56. @asynccontextmanager
  57. async def locked_trace(self, trace_id: str) -> AsyncIterator[Trace]:
  58. """Load one fresh Trace while holding its protocol command lock."""
  59. async with self.transaction(trace_id):
  60. trace = await self.trace_store.get_trace(trace_id)
  61. if trace is None:
  62. raise TaskProtocolStateError(f"Trace not found: {trace_id}")
  63. yield trace
  64. async def mutate_state(
  65. self,
  66. trace_id: str,
  67. mutation: Callable[[Trace, dict[str, Any]], Any],
  68. ) -> Any:
  69. """Apply one synchronous protocol mutation to a fresh persisted Trace."""
  70. async with self.locked_trace(trace_id) as trace:
  71. state = ensure_task_protocol(trace.context)
  72. result = mutation(trace, state)
  73. await self.trace_store.update_trace(trace_id, context=trace.context)
  74. return result
  75. async def update_progress(
  76. self,
  77. trace_id: str,
  78. *,
  79. expected_revision: int,
  80. progress: TaskProgress | dict,
  81. effective_at_sequence: int,
  82. ) -> TaskProgressRevision:
  83. async with self.transaction(trace_id):
  84. trace = await self.trace_store.get_trace(trace_id)
  85. if trace is None:
  86. raise TaskProtocolStateError(f"Trace not found: {trace_id}")
  87. policy = require_mutable_trace_policy(trace.context)
  88. if not policy.requires_task_progress:
  89. raise TaskProtocolStateError(
  90. "update_task_progress requires Recursive revision 3"
  91. )
  92. if (
  93. trace.agent_type == "validator"
  94. or trace.context.get("created_by_tool") == "validator"
  95. ):
  96. raise TaskProtocolStateError(
  97. "Validator traces cannot update TaskProgress"
  98. )
  99. parsed = TaskProgress.model_validate(progress)
  100. root_trace_id = trace.context.get("root_trace_id") or trace.trace_id
  101. for item in (
  102. *parsed.questions,
  103. *parsed.blockers,
  104. *parsed.findings,
  105. *parsed.hypotheses,
  106. *parsed.work_items,
  107. ):
  108. for ref in item.context_refs:
  109. get_authorized_context_snapshot(
  110. trace.context,
  111. ref_id=ref.ref_id,
  112. version=ref.version,
  113. root_trace_id=root_trace_id,
  114. uid=trace.uid,
  115. )
  116. next_context = deepcopy(trace.context)
  117. state = ensure_task_protocol(next_context)
  118. if state["pending_reviews"]:
  119. raise TaskProtocolStateError(
  120. "Review all child TaskReports before updating TaskProgress"
  121. )
  122. if state["next_actions"]:
  123. raise TaskProtocolStateError(
  124. "Execute every approved next action before updating TaskProgress"
  125. )
  126. if state["task_report"] is not None:
  127. raise TaskProtocolStateError(
  128. "TaskProgress cannot change after TaskReport submission"
  129. )
  130. revision = append_task_progress_revision(
  131. state,
  132. parsed,
  133. expected_revision=expected_revision,
  134. effective_at_sequence=effective_at_sequence,
  135. contract_ref=task_contract_ref(
  136. state,
  137. root_task_anchor_hash=next_context.get(
  138. "root_task_anchor_hash"
  139. ),
  140. ),
  141. )
  142. await self.trace_store.update_trace(trace_id, context=next_context)
  143. if self.event_service is not None:
  144. await self.event_service.emit_after_commit(
  145. source_trace_id=trace_id,
  146. event_type="task.progress.revised",
  147. event_key=(
  148. f"task.progress.revised:{trace_id}:{revision.revision}"
  149. ),
  150. effective_at_sequence=revision.effective_at_sequence,
  151. payload={"revision": revision.model_dump(mode="json")},
  152. )
  153. return revision