simbiot-harness/venv/lib/python3.12/site-packages/strands/agent/_concurrency.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

205 lines
8.1 KiB
Python

"""Concurrency and idempotency control for Agent invocations.
Encapsulates the per-Agent state that guards against concurrent invocations and
deduplicates retried requests via caller-supplied idempotency tokens. Designed to
be used by ``Agent.stream_async`` as a single delegate, keeping the orchestration
in ``agent.py`` and the synchronization primitives + bookkeeping here.
"""
from __future__ import annotations
import asyncio
import concurrent.futures
import threading
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
from ..types.agent import ConcurrentInvocationMode
from ..types.exceptions import IdempotencyAbortedError
if TYPE_CHECKING:
from .agent_result import AgentResult
@dataclass
class _InflightInvocation:
"""Tracks an inflight invocation for idempotency deduplication.
Duplicate callers register via ``register_waiter`` and await the returned awaitable,
then read ``result`` or ``error``. The primary calls ``settle`` on completion.
A single thread-safe ``concurrent.futures.Future`` is the broadcast signal; each
waiter turns it into a loop-local awaitable via ``asyncio.wrap_future``, which hooks
a done-callback that bridges back to the waiter's loop with ``call_soon_threadsafe``.
No waiter ever blocks a thread-pool worker, so a storm of duplicates cannot starve
the executor the primary needs to make progress. The wrapped future is ``shield``ed
so that cancelling one waiting duplicate cannot cancel the shared signal and strand
the others.
"""
result: AgentResult | None = None
error: BaseException | None = None
_done: concurrent.futures.Future = field(default_factory=concurrent.futures.Future, repr=False)
@property
def settled(self) -> bool:
"""Whether the primary has produced a result or error yet."""
return self._done.done()
def register_waiter(self) -> asyncio.Future:
"""Return a loop-local awaitable that resolves when the primary settles.
Resolves immediately if the primary has already settled (the caller then reads
``result``/``error``), so there is no register-after-settle race to handle.
"""
return asyncio.shield(asyncio.wrap_future(self._done))
def settle(self, result: AgentResult | None, error: BaseException | None) -> None:
"""Record the outcome and wake every registered waiter. Idempotent: first wins."""
if self._done.done():
return
# Publish result/error before signalling so woken waiters observe them.
self.result = result
self.error = error
self._done.set_result(None)
@dataclass
class _BeginResult:
"""Outcome of ``_ConcurrencyController.begin``.
Exactly one of the following is the actionable signal for the caller:
- ``waiting_on`` is set: this call is a duplicate of an inflight token. Await
``waiting_on.register_waiter()`` and then yield the cached result or raise the cached error.
- ``lock_acquired`` is False: a different invocation owns the lock. Raise
``ConcurrencyException``.
- Otherwise: proceed with the invocation. Pass ``registered_token`` back to
``complete()`` in the success and error paths so waiters get unblocked.
"""
waiting_on: _InflightInvocation | None
registered_token: Any
lock_acquired: bool
class _ConcurrencyController:
"""Owns the invocation lock and the inflight idempotency-token registry.
In THROW mode only one invocation can be inflight at a time, so a single
inflight slot suffices. The lock and registry use ``threading`` primitives
because ``Agent.run_async()`` may spawn separate event loops on separate threads;
waiter notification bridges back to each waiter's loop via ``call_soon_threadsafe``.
"""
def __init__(self, mode: ConcurrentInvocationMode) -> None:
self._mode = mode
self._invocation_lock = threading.Lock()
self._inflight_token: Any = None
self._inflight: _InflightInvocation | None = None
self._inflight_lock = threading.Lock()
@property
def mode(self) -> ConcurrentInvocationMode:
"""Return the configured concurrency mode."""
return self._mode
def begin(self, idempotency_token: Any) -> _BeginResult:
"""Attempt to start a new invocation.
Combines idempotency-check + lock-acquire into a single call. The returned
``_BeginResult`` tells the caller which of three paths to take.
Args:
idempotency_token: Caller-provided dedup token, or None.
Returns:
See ``_BeginResult``. If ``waiting_on`` is set, the lock is *not* held
and ``registered_token`` is None.
"""
waiting_on, registered_token = self._check_idempotency(idempotency_token)
if waiting_on is not None:
return _BeginResult(waiting_on=waiting_on, registered_token=None, lock_acquired=False)
lock_acquired = True
if self._mode == ConcurrentInvocationMode.THROW:
lock_acquired = self._invocation_lock.acquire(blocking=False)
return _BeginResult(waiting_on=None, registered_token=registered_token, lock_acquired=lock_acquired)
def complete(
self,
registered_token: Any,
*,
result: AgentResult | None = None,
error: BaseException | None = None,
) -> None:
"""Signal waiting duplicates and clear the inflight slot.
Safe to call multiple times for the same ``registered_token`` (subsequent
calls no-op once the slot has been cleared). Safe to call with
``registered_token=None`` (no-op).
If both ``result`` and ``error`` are None, waiters receive
``IdempotencyAbortedError``.
"""
if registered_token is None:
return
with self._inflight_lock:
if self._inflight_token != registered_token:
# Another invocation owns the slot (or it was already cleared).
return
inflight = self._inflight
self._inflight_token = None
self._inflight = None
if inflight is None:
return
if error is not None:
inflight.settle(None, error)
elif result is not None:
inflight.settle(result, None)
else:
inflight.settle(None, IdempotencyAbortedError("Primary invocation was aborted before producing a result."))
def try_acquire_lock(self) -> bool:
"""Non-blockingly acquire the invocation lock.
Exposed for direct tool callers that bypass the full idempotency flow but
still need to serialize against an inflight invocation.
Returns:
True if the lock was acquired, False otherwise.
"""
return self._invocation_lock.acquire(blocking=False)
def release_lock(self) -> None:
"""Release the invocation lock if it is held. Safe to call unconditionally."""
if self._invocation_lock.locked():
self._invocation_lock.release()
def _check_idempotency(self, idempotency_token: Any) -> tuple[_InflightInvocation | None, Any]:
"""Register a new inflight token, identify a duplicate, or no-op.
Returns:
``(waiting_on, registered_token)``:
- duplicate: ``(inflight_invocation, None)``
- new request: ``(None, idempotency_token)``
- different token already inflight, no token provided, or
UNSAFE_REENTRANT mode: ``(None, None)``
"""
if idempotency_token is None or self._mode != ConcurrentInvocationMode.THROW:
return None, None
with self._inflight_lock:
if self._inflight_token == idempotency_token:
return self._inflight, None
if self._inflight_token is not None:
# A different token is inflight; don't overwrite. Caller will hit the
# lock-acquire path and surface ConcurrencyException.
return None, None
self._inflight = _InflightInvocation()
self._inflight_token = idempotency_token
return None, idempotency_token