| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- """TaskProtocol 命令的单进程原子执行边界。"""
- from __future__ import annotations
- import asyncio
- from contextlib import asynccontextmanager
- from copy import deepcopy
- from collections.abc import AsyncIterator, Callable
- from typing import Any
- from weakref import WeakValueDictionary
- from cyber_agent.core.agent_mode import require_mutable_trace_policy
- from cyber_agent.core.context_policy import get_authorized_context_snapshot
- from cyber_agent.core.task_protocol import (
- TaskProgress,
- TaskProgressRevision,
- append_task_progress_revision,
- ensure_task_protocol,
- task_contract_ref,
- )
- from cyber_agent.trace.models import Trace
- from cyber_agent.trace.protocols import TraceStore
- _TRACE_LOCKS: WeakValueDictionary[
- tuple[asyncio.AbstractEventLoop, int, str], asyncio.Lock
- ] = WeakValueDictionary()
- class TaskProtocolStateError(RuntimeError):
- """TaskProtocol 命令不能在当前持久化状态执行。"""
- class TaskProtocolService:
- """将协议领域纯函数与 TraceStore 持久化组合起来。"""
- def __init__(self, trace_store: TraceStore, event_service=None) -> None:
- self.trace_store = trace_store
- self.event_service = event_service
- def _lock(self, trace_id: str) -> asyncio.Lock:
- key = (asyncio.get_running_loop(), id(self.trace_store), trace_id)
- lock = _TRACE_LOCKS.get(key)
- if lock is None:
- lock = asyncio.Lock()
- _TRACE_LOCKS[key] = lock
- return lock
- @asynccontextmanager
- async def transaction(self, trace_id: str) -> AsyncIterator[None]:
- """Serialize one complete TaskProtocol read-modify-write command."""
- async with self._lock(trace_id):
- yield
- @asynccontextmanager
- async def transaction_many(
- self,
- *trace_ids: str,
- ) -> AsyncIterator[None]:
- """Lock multiple protocol records in stable order to avoid deadlocks."""
- locks = [self._lock(item) for item in sorted(set(trace_ids))]
- for lock in locks:
- await lock.acquire()
- try:
- yield
- finally:
- for lock in reversed(locks):
- lock.release()
- @asynccontextmanager
- async def locked_trace(self, trace_id: str) -> AsyncIterator[Trace]:
- """Load one fresh Trace while holding its protocol command lock."""
- async with self.transaction(trace_id):
- trace = await self.trace_store.get_trace(trace_id)
- if trace is None:
- raise TaskProtocolStateError(f"Trace not found: {trace_id}")
- yield trace
- async def mutate_state(
- self,
- trace_id: str,
- mutation: Callable[[Trace, dict[str, Any]], Any],
- ) -> Any:
- """Apply one synchronous protocol mutation to a fresh persisted Trace."""
- async with self.locked_trace(trace_id) as trace:
- state = ensure_task_protocol(trace.context)
- result = mutation(trace, state)
- await self.trace_store.update_trace(trace_id, context=trace.context)
- return result
- async def update_progress(
- self,
- trace_id: str,
- *,
- expected_revision: int,
- progress: TaskProgress | dict,
- effective_at_sequence: int,
- ) -> TaskProgressRevision:
- async with self.transaction(trace_id):
- trace = await self.trace_store.get_trace(trace_id)
- if trace is None:
- raise TaskProtocolStateError(f"Trace not found: {trace_id}")
- policy = require_mutable_trace_policy(trace.context)
- if not policy.requires_task_progress:
- raise TaskProtocolStateError(
- "update_task_progress requires Recursive revision 3"
- )
- if (
- trace.agent_type == "validator"
- or trace.context.get("created_by_tool") == "validator"
- ):
- raise TaskProtocolStateError(
- "Validator traces cannot update TaskProgress"
- )
- parsed = TaskProgress.model_validate(progress)
- root_trace_id = trace.context.get("root_trace_id") or trace.trace_id
- for item in (
- *parsed.questions,
- *parsed.blockers,
- *parsed.findings,
- *parsed.hypotheses,
- *parsed.work_items,
- ):
- for ref in item.context_refs:
- get_authorized_context_snapshot(
- trace.context,
- ref_id=ref.ref_id,
- version=ref.version,
- root_trace_id=root_trace_id,
- uid=trace.uid,
- )
- next_context = deepcopy(trace.context)
- state = ensure_task_protocol(next_context)
- if state["pending_reviews"]:
- raise TaskProtocolStateError(
- "Review all child TaskReports before updating TaskProgress"
- )
- if state["next_actions"]:
- raise TaskProtocolStateError(
- "Execute every approved next action before updating TaskProgress"
- )
- if state["task_report"] is not None:
- raise TaskProtocolStateError(
- "TaskProgress cannot change after TaskReport submission"
- )
- revision = append_task_progress_revision(
- state,
- parsed,
- expected_revision=expected_revision,
- effective_at_sequence=effective_at_sequence,
- contract_ref=task_contract_ref(
- state,
- root_task_anchor_hash=next_context.get(
- "root_task_anchor_hash"
- ),
- ),
- )
- await self.trace_store.update_trace(trace_id, context=next_context)
- if self.event_service is not None:
- await self.event_service.emit_after_commit(
- source_trace_id=trace_id,
- event_type="task.progress.revised",
- event_key=(
- f"task.progress.revised:{trace_id}:{revision.revision}"
- ),
- effective_at_sequence=revision.effective_at_sequence,
- payload={"revision": revision.model_dump(mode="json")},
- )
- return revision
|