simbiot-harness/venv/lib/python3.12/site-packages/strands/experimental/checkpoint/checkpoint.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

83 lines
3.1 KiB
Python

"""Checkpoint system for durable agent execution.
A ``Checkpoint`` is a pause-point marker emitted at agent cycle boundaries.
It captures the position (which boundary fired) and the cycle index. It does
**not** capture conversation state — pair with a ``SessionManager`` for
cross-process state continuity.
Positions per ReAct cycle:
- ``after_model``: model returned tool_use; tools have not run yet.
- ``after_tools``: tools finished; the next model call has not happened yet.
Per-tool granularity within a cycle is the ``ToolExecutor``'s responsibility.
Usage (mirrors interrupts):
- Pause: ``AgentResult`` with ``stop_reason="checkpoint"`` and ``checkpoint`` populated.
- Resume: pass back ``{"checkpointResume": {"checkpoint": ckpt.to_dict()}}``.
Precedence:
- Interrupt > checkpoint: an interrupt during a checkpointing cycle returns
``stop_reason="interrupt"`` and skips ``after_tools``.
- Cancel > checkpoint: a cancel signal at either boundary returns
``stop_reason="cancelled"``.
Notes:
- Checkpoints are only emitted on tool_use cycles. A turn with no tool calls
emits no checkpoint; use a ``SessionManager`` for durability of every turn.
- ``EventLoopMetrics`` resets per invocation; aggregate yourself if needed.
- ``BeforeInvocationEvent`` / ``AfterInvocationEvent`` fire on every resume,
same as interrupts.
"""
import logging
from dataclasses import asdict, dataclass, field
from typing import Any, Literal
from ...types.exceptions import CheckpointException
logger = logging.getLogger(__name__)
CHECKPOINT_SCHEMA_VERSION = "1.0"
CheckpointPosition = Literal["after_model", "after_tools"]
@dataclass(frozen=True)
class Checkpoint:
"""Pause-point marker. Treat as opaque — pass back to resume.
Attributes:
position: Which boundary fired (``after_model`` or ``after_tools``).
cycle_index: ReAct loop cycle (0-based).
schema_version: Rejects incompatible checkpoints on resume.
"""
position: CheckpointPosition
cycle_index: int = 0
schema_version: str = field(init=False, default=CHECKPOINT_SCHEMA_VERSION)
def to_dict(self) -> dict[str, Any]:
"""Serialize for persistence."""
return asdict(self)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Checkpoint":
"""Reconstruct from a dict produced by to_dict().
Args:
data: Serialized checkpoint data.
Raises:
CheckpointException: If schema_version doesn't match the current version.
"""
version = data.get("schema_version", "")
if version != CHECKPOINT_SCHEMA_VERSION:
raise CheckpointException(
f"Checkpoints with schema version {version!r} are not compatible "
f"with current version {CHECKPOINT_SCHEMA_VERSION}."
)
known_keys = {k for k in cls.__dataclass_fields__ if k != "schema_version"}
unknown_keys = set(data.keys()) - known_keys - {"schema_version"}
if unknown_keys:
logger.warning("unknown_keys=<%s> | ignoring unknown fields in checkpoint data", unknown_keys)
return cls(**{k: v for k, v in data.items() if k in known_keys})