agent-checkpoint-framework 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_checkpoint_framework-0.1.0/PKG-INFO +37 -0
- agent_checkpoint_framework-0.1.0/README.md +25 -0
- agent_checkpoint_framework-0.1.0/pyproject.toml +36 -0
- agent_checkpoint_framework-0.1.0/setup.cfg +4 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint/__init__.py +24 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint/codec.py +109 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint/factory.py +43 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint/manager.py +55 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint/models.py +72 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint/storage.py +39 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint/validator.py +112 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint_framework.egg-info/PKG-INFO +37 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint_framework.egg-info/SOURCES.txt +17 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint_framework.egg-info/dependency_links.txt +1 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint_framework.egg-info/requires.txt +6 -0
- agent_checkpoint_framework-0.1.0/src/agent_checkpoint_framework.egg-info/top_level.txt +1 -0
- agent_checkpoint_framework-0.1.0/tests/test_codec.py +108 -0
- agent_checkpoint_framework-0.1.0/tests/test_factory.py +66 -0
- agent_checkpoint_framework-0.1.0/tests/test_manager.py +35 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-checkpoint-framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Never lose work when an agent fails.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: pytest; extra == "dev"
|
|
9
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
10
|
+
Requires-Dist: ruff; extra == "dev"
|
|
11
|
+
Requires-Dist: mypy; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# Agent Checkpoint
|
|
14
|
+
|
|
15
|
+
Never lose work when an agent fails.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Industrialized OOP Structure**: Clean separation of concerns between models, storage, and management.
|
|
20
|
+
- **Flexible Storage**: Abstract base class allows for multiple storage backends (In-Memory, File-system, Database).
|
|
21
|
+
- **Type Safe**: Fully type-hinted and compatible with `mypy`.
|
|
22
|
+
- **CI Ready**: Pre-configured with GitHub Actions, `ruff`, and `pytest`.
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
- `Checkpoint`: Immutable data model for agent states.
|
|
27
|
+
- `Storage`: Interface for persisting and retrieving checkpoints.
|
|
28
|
+
- `CheckpointManager`: Central API for creating and managing checkpoints.
|
|
29
|
+
|
|
30
|
+
## Development
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .[dev]
|
|
34
|
+
ruff check .
|
|
35
|
+
mypy src
|
|
36
|
+
pytest
|
|
37
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Agent Checkpoint
|
|
2
|
+
|
|
3
|
+
Never lose work when an agent fails.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Industrialized OOP Structure**: Clean separation of concerns between models, storage, and management.
|
|
8
|
+
- **Flexible Storage**: Abstract base class allows for multiple storage backends (In-Memory, File-system, Database).
|
|
9
|
+
- **Type Safe**: Fully type-hinted and compatible with `mypy`.
|
|
10
|
+
- **CI Ready**: Pre-configured with GitHub Actions, `ruff`, and `pytest`.
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
- `Checkpoint`: Immutable data model for agent states.
|
|
15
|
+
- `Storage`: Interface for persisting and retrieving checkpoints.
|
|
16
|
+
- `CheckpointManager`: Central API for creating and managing checkpoints.
|
|
17
|
+
|
|
18
|
+
## Development
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e .[dev]
|
|
22
|
+
ruff check .
|
|
23
|
+
mypy src
|
|
24
|
+
pytest
|
|
25
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-checkpoint-framework"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Never lose work when an agent fails."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = [
|
|
15
|
+
"pytest",
|
|
16
|
+
"pytest-cov",
|
|
17
|
+
"ruff",
|
|
18
|
+
"mypy",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.ruff]
|
|
22
|
+
line-length = 100
|
|
23
|
+
|
|
24
|
+
[tool.ruff.lint]
|
|
25
|
+
select = ["E", "F", "I"]
|
|
26
|
+
ignore = ["E501"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff.format]
|
|
29
|
+
quote-style = "double"
|
|
30
|
+
|
|
31
|
+
[tool.mypy]
|
|
32
|
+
strict = true
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
addopts = "--cov=src --cov-report=term-missing"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .codec import ParseResult, emit, emit_json, parse
|
|
2
|
+
from .factory import new_checkpoint, resume_from
|
|
3
|
+
from .manager import CheckpointManager
|
|
4
|
+
from .models import Checkpoint, CheckpointStatus, RetryStrategy
|
|
5
|
+
from .storage import InMemoryStorage, Storage
|
|
6
|
+
from .validator import ValidationError
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Checkpoint",
|
|
12
|
+
"CheckpointStatus",
|
|
13
|
+
"RetryStrategy",
|
|
14
|
+
"ValidationError",
|
|
15
|
+
"ParseResult",
|
|
16
|
+
"parse",
|
|
17
|
+
"emit",
|
|
18
|
+
"emit_json",
|
|
19
|
+
"new_checkpoint",
|
|
20
|
+
"resume_from",
|
|
21
|
+
"CheckpointManager",
|
|
22
|
+
"Storage",
|
|
23
|
+
"InMemoryStorage",
|
|
24
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from .models import Checkpoint, CheckpointStatus, RetryStrategy
|
|
8
|
+
from .validator import ValidationError, validate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ParseResult:
|
|
13
|
+
"""The result of a parse operation."""
|
|
14
|
+
|
|
15
|
+
ok: bool
|
|
16
|
+
checkpoint: Optional[Checkpoint]
|
|
17
|
+
errors: List[ValidationError]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse(data: Union[str, Dict[str, Any]]) -> ParseResult:
|
|
21
|
+
"""
|
|
22
|
+
Parses a JSON string or dictionary into a Checkpoint object.
|
|
23
|
+
Performs validation and returns a ParseResult.
|
|
24
|
+
"""
|
|
25
|
+
if isinstance(data, str):
|
|
26
|
+
try:
|
|
27
|
+
raw_dict = json.loads(data)
|
|
28
|
+
except json.JSONDecodeError as e:
|
|
29
|
+
return ParseResult(
|
|
30
|
+
ok=False,
|
|
31
|
+
checkpoint=None,
|
|
32
|
+
errors=[ValidationError("json", f"Invalid JSON: {str(e)}")],
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
raw_dict = data
|
|
36
|
+
|
|
37
|
+
# 1. Validate the dictionary
|
|
38
|
+
errors = validate(raw_dict)
|
|
39
|
+
if errors:
|
|
40
|
+
return ParseResult(ok=False, checkpoint=None, errors=errors)
|
|
41
|
+
|
|
42
|
+
# 2. Build Checkpoint from dict
|
|
43
|
+
try:
|
|
44
|
+
# Convert enum strings to actual enum members for the constructor
|
|
45
|
+
status = CheckpointStatus(raw_dict["status"])
|
|
46
|
+
|
|
47
|
+
retry_strategy = None
|
|
48
|
+
if raw_dict.get("retry_strategy"):
|
|
49
|
+
retry_strategy = RetryStrategy(raw_dict["retry_strategy"])
|
|
50
|
+
|
|
51
|
+
checkpoint = Checkpoint(
|
|
52
|
+
# Required fields
|
|
53
|
+
ac_version=raw_dict["ac_version"],
|
|
54
|
+
checkpoint_id=raw_dict["checkpoint_id"],
|
|
55
|
+
task_id=raw_dict["task_id"],
|
|
56
|
+
agent_id=raw_dict["agent_id"],
|
|
57
|
+
emitted_at=raw_dict["emitted_at"],
|
|
58
|
+
status=status,
|
|
59
|
+
task_summary=raw_dict["task_summary"],
|
|
60
|
+
# Optional fields
|
|
61
|
+
completed_steps=raw_dict.get("completed_steps", []),
|
|
62
|
+
remaining_steps=raw_dict.get("remaining_steps", []),
|
|
63
|
+
partial_output=raw_dict.get("partial_output"),
|
|
64
|
+
failure_reason=raw_dict.get("failure_reason"),
|
|
65
|
+
confidence=raw_dict.get("confidence"),
|
|
66
|
+
retry_strategy=retry_strategy,
|
|
67
|
+
executor_hint=raw_dict.get("executor_hint"),
|
|
68
|
+
context_snapshot=raw_dict.get("context_snapshot"),
|
|
69
|
+
parent_checkpoint_id=raw_dict.get("parent_checkpoint_id"),
|
|
70
|
+
)
|
|
71
|
+
return ParseResult(ok=True, checkpoint=checkpoint, errors=[])
|
|
72
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
73
|
+
# This catch-all handles unexpected type mismatches not caught by validate()
|
|
74
|
+
return ParseResult(
|
|
75
|
+
ok=False,
|
|
76
|
+
checkpoint=None,
|
|
77
|
+
errors=[ValidationError("internal", f"Failed to instantiate Checkpoint: {str(e)}")],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def emit(checkpoint: Checkpoint) -> Dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Converts a Checkpoint to a dictionary for serialization.
|
|
84
|
+
Omits fields with None values entirely.
|
|
85
|
+
"""
|
|
86
|
+
# Use __dict__ to avoid complex serialization of nested objects if any,
|
|
87
|
+
# but since Checkpoint is simple dataclass, this works well.
|
|
88
|
+
raw = checkpoint.__dict__.copy()
|
|
89
|
+
|
|
90
|
+
# Clean up enums and omit None values
|
|
91
|
+
clean: Dict[str, Any] = {}
|
|
92
|
+
for k, v in raw.items():
|
|
93
|
+
if v is None:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Handle enums
|
|
97
|
+
if isinstance(v, (CheckpointStatus, RetryStrategy)):
|
|
98
|
+
clean[k] = v.value
|
|
99
|
+
else:
|
|
100
|
+
clean[k] = v
|
|
101
|
+
|
|
102
|
+
return clean
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def emit_json(checkpoint: Checkpoint, indent: int = 2) -> str:
|
|
106
|
+
"""
|
|
107
|
+
Converts a Checkpoint to a JSON string.
|
|
108
|
+
"""
|
|
109
|
+
return json.dumps(emit(checkpoint), indent=indent, default=str)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from .models import Checkpoint, CheckpointStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def new_checkpoint(
|
|
10
|
+
task_id: str,
|
|
11
|
+
agent_id: str,
|
|
12
|
+
task_summary: str,
|
|
13
|
+
status: CheckpointStatus = CheckpointStatus.PARTIAL,
|
|
14
|
+
) -> Checkpoint:
|
|
15
|
+
"""
|
|
16
|
+
Creates a new Checkpoint for a fresh task.
|
|
17
|
+
"""
|
|
18
|
+
return Checkpoint(
|
|
19
|
+
ac_version="0.1.0",
|
|
20
|
+
checkpoint_id=str(uuid.uuid4()),
|
|
21
|
+
task_id=task_id,
|
|
22
|
+
agent_id=agent_id,
|
|
23
|
+
emitted_at=datetime.now(timezone.utc).isoformat(),
|
|
24
|
+
status=status,
|
|
25
|
+
task_summary=task_summary,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resume_from(previous: Checkpoint, agent_id: str) -> Checkpoint:
|
|
30
|
+
"""
|
|
31
|
+
Creates a new Checkpoint linked to a previous one, preserving task context.
|
|
32
|
+
"""
|
|
33
|
+
return Checkpoint(
|
|
34
|
+
ac_version="0.1.0",
|
|
35
|
+
checkpoint_id=str(uuid.uuid4()),
|
|
36
|
+
task_id=previous.task_id,
|
|
37
|
+
agent_id=agent_id,
|
|
38
|
+
emitted_at=datetime.now(timezone.utc).isoformat(),
|
|
39
|
+
status=CheckpointStatus.PARTIAL,
|
|
40
|
+
task_summary=previous.task_summary,
|
|
41
|
+
completed_steps=list(previous.completed_steps), # copy the list
|
|
42
|
+
parent_checkpoint_id=previous.checkpoint_id,
|
|
43
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from .models import Checkpoint, CheckpointStatus, RetryStrategy
|
|
6
|
+
from .storage import Storage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CheckpointManager:
|
|
10
|
+
"""Coordinates checkpoint creation and retrieval based on SPEC.md."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, storage: Storage):
|
|
13
|
+
self.storage = storage
|
|
14
|
+
|
|
15
|
+
def create_checkpoint(
|
|
16
|
+
self,
|
|
17
|
+
task_id: str,
|
|
18
|
+
agent_id: str,
|
|
19
|
+
task_summary: str,
|
|
20
|
+
status: CheckpointStatus = CheckpointStatus.PARTIAL,
|
|
21
|
+
completed_steps: Optional[List[str]] = None,
|
|
22
|
+
remaining_steps: Optional[List[str]] = None,
|
|
23
|
+
partial_output: Optional[Dict[str, Any]] = None,
|
|
24
|
+
failure_reason: Optional[str] = None,
|
|
25
|
+
confidence: Optional[float] = None,
|
|
26
|
+
retry_strategy: Optional[RetryStrategy] = None,
|
|
27
|
+
executor_hint: Optional[str] = None,
|
|
28
|
+
context_snapshot: Optional[Dict[str, Any]] = None,
|
|
29
|
+
parent_checkpoint_id: Optional[str] = None,
|
|
30
|
+
) -> Checkpoint:
|
|
31
|
+
"""Creates and saves a new checkpoint following the spec."""
|
|
32
|
+
checkpoint = Checkpoint(
|
|
33
|
+
ac_version="0.1.0",
|
|
34
|
+
checkpoint_id=str(uuid.uuid4()),
|
|
35
|
+
task_id=task_id,
|
|
36
|
+
agent_id=agent_id,
|
|
37
|
+
emitted_at=datetime.now(timezone.utc).isoformat(),
|
|
38
|
+
status=status,
|
|
39
|
+
task_summary=task_summary,
|
|
40
|
+
completed_steps=completed_steps or [],
|
|
41
|
+
remaining_steps=remaining_steps or [],
|
|
42
|
+
partial_output=partial_output or {},
|
|
43
|
+
failure_reason=failure_reason,
|
|
44
|
+
confidence=confidence,
|
|
45
|
+
retry_strategy=retry_strategy,
|
|
46
|
+
executor_hint=executor_hint,
|
|
47
|
+
context_snapshot=context_snapshot or {},
|
|
48
|
+
parent_checkpoint_id=parent_checkpoint_id,
|
|
49
|
+
)
|
|
50
|
+
self.storage.save(checkpoint)
|
|
51
|
+
return checkpoint
|
|
52
|
+
|
|
53
|
+
def get_checkpoint(self, checkpoint_id: str) -> Optional[Checkpoint]:
|
|
54
|
+
"""Retrieves a checkpoint by ID."""
|
|
55
|
+
return self.storage.load(checkpoint_id)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CheckpointStatus(StrEnum):
|
|
9
|
+
"""Status of the checkpoint as defined in SPEC.md."""
|
|
10
|
+
|
|
11
|
+
PARTIAL = "partial"
|
|
12
|
+
FAILED = "failed"
|
|
13
|
+
PAUSED = "paused"
|
|
14
|
+
COMPLETED = "completed"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RetryStrategy(StrEnum):
|
|
18
|
+
"""Strategy for continuation as defined in SPEC.md."""
|
|
19
|
+
|
|
20
|
+
RESUME = "resume"
|
|
21
|
+
RESTART = "restart"
|
|
22
|
+
ESCALATE = "escalate"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Checkpoint:
|
|
27
|
+
"""
|
|
28
|
+
Represents a single point in time for an agent's state, following SPEC.md.
|
|
29
|
+
|
|
30
|
+
Validation:
|
|
31
|
+
- confidence: None or between 0.0 and 1.0 (inclusive)
|
|
32
|
+
- executor_hint: None, 'same', 'any', or starts with 'capability:'
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Required fields (no defaults)
|
|
36
|
+
ac_version: str
|
|
37
|
+
checkpoint_id: str
|
|
38
|
+
task_id: str
|
|
39
|
+
agent_id: str
|
|
40
|
+
emitted_at: str
|
|
41
|
+
status: CheckpointStatus
|
|
42
|
+
task_summary: str
|
|
43
|
+
|
|
44
|
+
# Optional fields
|
|
45
|
+
completed_steps: List[str] = field(default_factory=list)
|
|
46
|
+
remaining_steps: List[str] = field(default_factory=list)
|
|
47
|
+
partial_output: Optional[Dict[str, object]] = None
|
|
48
|
+
failure_reason: Optional[str] = None
|
|
49
|
+
confidence: Optional[float] = None
|
|
50
|
+
retry_strategy: Optional[RetryStrategy] = None
|
|
51
|
+
executor_hint: Optional[str] = None
|
|
52
|
+
context_snapshot: Optional[Dict[str, object]] = None
|
|
53
|
+
parent_checkpoint_id: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
"""Validate input constraints."""
|
|
57
|
+
if self.confidence is not None:
|
|
58
|
+
if not (0.0 <= self.confidence <= 1.0):
|
|
59
|
+
raise ValueError(f"Confidence must be between 0.0 and 1.0; got {self.confidence}")
|
|
60
|
+
|
|
61
|
+
if self.executor_hint is not None:
|
|
62
|
+
if not (
|
|
63
|
+
self.executor_hint in ("same", "any")
|
|
64
|
+
or self.executor_hint.startswith("capability:")
|
|
65
|
+
):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"executor_hint must be 'same', 'any', or start with 'capability:'; "
|
|
68
|
+
f"got {self.executor_hint}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ["CheckpointStatus", "RetryStrategy", "Checkpoint"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from .models import Checkpoint
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Storage(ABC):
|
|
8
|
+
"""Abstract base class for checkpoint storage."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def save(self, checkpoint: Checkpoint) -> None:
|
|
12
|
+
"""Persist a checkpoint."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def load(self, checkpoint_id: str) -> Optional[Checkpoint]:
|
|
17
|
+
"""Retrieve a checkpoint by ID."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def list_all(self) -> List[Checkpoint]:
|
|
22
|
+
"""List all stored checkpoints."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InMemoryStorage(Storage):
|
|
27
|
+
"""Simple in-memory implementation of checkpoint storage."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._checkpoints: Dict[str, Checkpoint] = {}
|
|
31
|
+
|
|
32
|
+
def save(self, checkpoint: Checkpoint) -> None:
|
|
33
|
+
self._checkpoints[checkpoint.checkpoint_id] = checkpoint
|
|
34
|
+
|
|
35
|
+
def load(self, checkpoint_id: str) -> Optional[Checkpoint]:
|
|
36
|
+
return self._checkpoints.get(checkpoint_id)
|
|
37
|
+
|
|
38
|
+
def list_all(self) -> List[Checkpoint]:
|
|
39
|
+
return list(self._checkpoints.values())
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
from .models import CheckpointStatus, RetryStrategy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ValidationError:
|
|
13
|
+
"""Represents a single validation failure."""
|
|
14
|
+
|
|
15
|
+
field: str
|
|
16
|
+
message: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# UUID v4 pattern (simplified but strict enough for spec compliance)
|
|
20
|
+
UUID_V4_PATTERN = re.compile(
|
|
21
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", re.IGNORECASE
|
|
22
|
+
)
|
|
23
|
+
SEMVER_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate(data: Dict[str, Any]) -> List[ValidationError]:
|
|
27
|
+
"""
|
|
28
|
+
Validates a dictionary against the agent-checkpoint specification.
|
|
29
|
+
Returns a list of ValidationError objects; an empty list indicates success.
|
|
30
|
+
"""
|
|
31
|
+
errors: List[ValidationError] = []
|
|
32
|
+
|
|
33
|
+
# 1. Required fields presence and basic type check
|
|
34
|
+
required_fields = [
|
|
35
|
+
"ac_version",
|
|
36
|
+
"checkpoint_id",
|
|
37
|
+
"task_id",
|
|
38
|
+
"agent_id",
|
|
39
|
+
"emitted_at",
|
|
40
|
+
"status",
|
|
41
|
+
"task_summary",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
for field in required_fields:
|
|
45
|
+
if field not in data:
|
|
46
|
+
errors.append(ValidationError(field, "Field is missing"))
|
|
47
|
+
elif not isinstance(data[field], str) or not data[field].strip():
|
|
48
|
+
errors.append(ValidationError(field, "Field must be a non-empty string"))
|
|
49
|
+
|
|
50
|
+
# If basic requirements are missing, we still continue to check patterns for existing fields
|
|
51
|
+
|
|
52
|
+
# 2. Pattern and Logic Validations
|
|
53
|
+
if "ac_version" in data and isinstance(data["ac_version"], str):
|
|
54
|
+
if not SEMVER_PATTERN.match(data["ac_version"]):
|
|
55
|
+
errors.append(
|
|
56
|
+
ValidationError("ac_version", "Must match semver pattern \\d+\\.\\d+\\.\\d+")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# UUID checks
|
|
60
|
+
for uuid_field in ["checkpoint_id", "task_id", "parent_checkpoint_id"]:
|
|
61
|
+
if uuid_field in data and data[uuid_field] is not None:
|
|
62
|
+
if not isinstance(data[uuid_field], str):
|
|
63
|
+
errors.append(ValidationError(uuid_field, "Must be a string"))
|
|
64
|
+
elif not UUID_V4_PATTERN.match(data[uuid_field]):
|
|
65
|
+
errors.append(ValidationError(uuid_field, "Must be a valid UUID v4"))
|
|
66
|
+
|
|
67
|
+
# Timestamp check
|
|
68
|
+
if "emitted_at" in data and isinstance(data["emitted_at"], str):
|
|
69
|
+
try:
|
|
70
|
+
# Handle the 'Z' suffix which fromisoformat might not like in older 3.11 patches
|
|
71
|
+
ts = data["emitted_at"].replace("Z", "+00:00")
|
|
72
|
+
datetime.fromisoformat(ts)
|
|
73
|
+
except ValueError:
|
|
74
|
+
errors.append(ValidationError("emitted_at", "Must be a valid ISO 8601 timestamp"))
|
|
75
|
+
|
|
76
|
+
# Enum checks
|
|
77
|
+
if "status" in data and isinstance(data["status"], str):
|
|
78
|
+
if data["status"] not in [s.value for s in CheckpointStatus]:
|
|
79
|
+
errors.append(
|
|
80
|
+
ValidationError(
|
|
81
|
+
"status", f"Must be one of: {', '.join(s.value for s in CheckpointStatus)}"
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if "retry_strategy" in data and data["retry_strategy"] is not None:
|
|
86
|
+
if data["retry_strategy"] not in [r.value for r in RetryStrategy]:
|
|
87
|
+
errors.append(
|
|
88
|
+
ValidationError(
|
|
89
|
+
"retry_strategy", f"Must be one of: {', '.join(r.value for r in RetryStrategy)}"
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Confidence check
|
|
94
|
+
if "confidence" in data and data["confidence"] is not None:
|
|
95
|
+
if not isinstance(data["confidence"], (int, float)):
|
|
96
|
+
errors.append(ValidationError("confidence", "Must be a number"))
|
|
97
|
+
elif not (0.0 <= float(data["confidence"]) <= 1.0):
|
|
98
|
+
errors.append(ValidationError("confidence", "Must be between 0.0 and 1.0"))
|
|
99
|
+
|
|
100
|
+
# Executor hint check
|
|
101
|
+
if "executor_hint" in data and data["executor_hint"] is not None:
|
|
102
|
+
val = data["executor_hint"]
|
|
103
|
+
if not isinstance(val, str):
|
|
104
|
+
errors.append(ValidationError("executor_hint", "Must be a string"))
|
|
105
|
+
elif not (val in ("same", "any") or val.startswith("capability:")):
|
|
106
|
+
errors.append(
|
|
107
|
+
ValidationError(
|
|
108
|
+
"executor_hint", "Must be 'same', 'any', or start with 'capability:'"
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return errors
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-checkpoint-framework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Never lose work when an agent fails.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: pytest; extra == "dev"
|
|
9
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
10
|
+
Requires-Dist: ruff; extra == "dev"
|
|
11
|
+
Requires-Dist: mypy; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# Agent Checkpoint
|
|
14
|
+
|
|
15
|
+
Never lose work when an agent fails.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Industrialized OOP Structure**: Clean separation of concerns between models, storage, and management.
|
|
20
|
+
- **Flexible Storage**: Abstract base class allows for multiple storage backends (In-Memory, File-system, Database).
|
|
21
|
+
- **Type Safe**: Fully type-hinted and compatible with `mypy`.
|
|
22
|
+
- **CI Ready**: Pre-configured with GitHub Actions, `ruff`, and `pytest`.
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
- `Checkpoint`: Immutable data model for agent states.
|
|
27
|
+
- `Storage`: Interface for persisting and retrieving checkpoints.
|
|
28
|
+
- `CheckpointManager`: Central API for creating and managing checkpoints.
|
|
29
|
+
|
|
30
|
+
## Development
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .[dev]
|
|
34
|
+
ruff check .
|
|
35
|
+
mypy src
|
|
36
|
+
pytest
|
|
37
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/agent_checkpoint/__init__.py
|
|
4
|
+
src/agent_checkpoint/codec.py
|
|
5
|
+
src/agent_checkpoint/factory.py
|
|
6
|
+
src/agent_checkpoint/manager.py
|
|
7
|
+
src/agent_checkpoint/models.py
|
|
8
|
+
src/agent_checkpoint/storage.py
|
|
9
|
+
src/agent_checkpoint/validator.py
|
|
10
|
+
src/agent_checkpoint_framework.egg-info/PKG-INFO
|
|
11
|
+
src/agent_checkpoint_framework.egg-info/SOURCES.txt
|
|
12
|
+
src/agent_checkpoint_framework.egg-info/dependency_links.txt
|
|
13
|
+
src/agent_checkpoint_framework.egg-info/requires.txt
|
|
14
|
+
src/agent_checkpoint_framework.egg-info/top_level.txt
|
|
15
|
+
tests/test_codec.py
|
|
16
|
+
tests/test_factory.py
|
|
17
|
+
tests/test_manager.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent_checkpoint
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from agent_checkpoint.codec import emit, emit_json, parse
|
|
6
|
+
from agent_checkpoint.models import Checkpoint, CheckpointStatus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def VALID_DICT():
|
|
11
|
+
return {
|
|
12
|
+
"ac_version": "0.1.0",
|
|
13
|
+
"checkpoint_id": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
|
|
14
|
+
"task_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
|
|
15
|
+
"agent_id": "coder-gpt-4",
|
|
16
|
+
"emitted_at": "2026-06-16T14:30:00Z",
|
|
17
|
+
"status": "partial",
|
|
18
|
+
"task_summary": "Implement auth module",
|
|
19
|
+
"confidence": 0.75,
|
|
20
|
+
"executor_hint": "same",
|
|
21
|
+
"retry_strategy": "resume",
|
|
22
|
+
"completed_steps": ["step1"],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_parse_valid_dict(VALID_DICT):
|
|
27
|
+
result = parse(VALID_DICT)
|
|
28
|
+
assert result.ok is True
|
|
29
|
+
assert isinstance(result.checkpoint, Checkpoint)
|
|
30
|
+
assert result.checkpoint.status == CheckpointStatus.PARTIAL
|
|
31
|
+
assert result.checkpoint.confidence == 0.75
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_parse_valid_json_string(VALID_DICT):
|
|
35
|
+
json_str = json.dumps(VALID_DICT)
|
|
36
|
+
result = parse(json_str)
|
|
37
|
+
assert result.ok is True
|
|
38
|
+
assert result.checkpoint.checkpoint_id == VALID_DICT["checkpoint_id"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_parse_empty_dict():
|
|
42
|
+
result = parse({})
|
|
43
|
+
assert result.ok is False
|
|
44
|
+
# Errors should list missing required fields
|
|
45
|
+
required_fields = [
|
|
46
|
+
"ac_version",
|
|
47
|
+
"checkpoint_id",
|
|
48
|
+
"task_id",
|
|
49
|
+
"agent_id",
|
|
50
|
+
"emitted_at",
|
|
51
|
+
"status",
|
|
52
|
+
"task_summary",
|
|
53
|
+
]
|
|
54
|
+
error_fields = [e.field for e in result.errors]
|
|
55
|
+
for field in required_fields:
|
|
56
|
+
assert field in error_fields
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize(
|
|
60
|
+
"field, invalid_value",
|
|
61
|
+
[
|
|
62
|
+
("confidence", 1.5),
|
|
63
|
+
("checkpoint_id", "not-a-uuid"),
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
def test_parse_invalid_values(VALID_DICT, field, invalid_value):
|
|
67
|
+
invalid_dict = VALID_DICT.copy()
|
|
68
|
+
invalid_dict[field] = invalid_value
|
|
69
|
+
result = parse(invalid_dict)
|
|
70
|
+
assert result.ok is False
|
|
71
|
+
assert any(e.field == field for e in result.errors)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_parse_unknown_extra_field(VALID_DICT):
|
|
75
|
+
extra_dict = VALID_DICT.copy()
|
|
76
|
+
extra_dict["extra_field"] = "some value"
|
|
77
|
+
result = parse(extra_dict)
|
|
78
|
+
assert result.ok is True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_emit_no_none_values(VALID_DICT):
|
|
82
|
+
checkpoint = parse(VALID_DICT).checkpoint
|
|
83
|
+
# Manually ensure some fields are None if they weren't already
|
|
84
|
+
# but the valid_dict has some Nones by omission anyway.
|
|
85
|
+
emitted = emit(checkpoint)
|
|
86
|
+
for value in emitted.values():
|
|
87
|
+
assert value is not None
|
|
88
|
+
assert "failure_reason" not in emitted
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_emit_json_valid_json(VALID_DICT):
|
|
92
|
+
checkpoint = parse(VALID_DICT).checkpoint
|
|
93
|
+
json_str = emit_json(checkpoint)
|
|
94
|
+
# Should be a valid JSON string
|
|
95
|
+
data = json.loads(json_str)
|
|
96
|
+
assert data["checkpoint_id"] == VALID_DICT["checkpoint_id"]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_round_trip(VALID_DICT):
|
|
100
|
+
# parse(emit_json(parse(VALID_DICT).checkpoint)) returns ok=True
|
|
101
|
+
first_parse = parse(VALID_DICT)
|
|
102
|
+
assert first_parse.ok is True
|
|
103
|
+
|
|
104
|
+
json_output = emit_json(first_parse.checkpoint)
|
|
105
|
+
second_parse = parse(json_output)
|
|
106
|
+
|
|
107
|
+
assert second_parse.ok is True
|
|
108
|
+
assert second_parse.checkpoint == first_parse.checkpoint
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from agent_checkpoint.factory import new_checkpoint, resume_from
|
|
4
|
+
from agent_checkpoint.models import Checkpoint, CheckpointStatus
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_new_checkpoint_ac_version():
|
|
8
|
+
cp = new_checkpoint("task-1", "agent-1", "summary")
|
|
9
|
+
assert cp.ac_version == "0.1.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_new_checkpoint_id_is_uuid():
|
|
13
|
+
cp = new_checkpoint("task-1", "agent-1", "summary")
|
|
14
|
+
# Basic UUID v4 format check (8-4-4-4-12)
|
|
15
|
+
parts = cp.checkpoint_id.split("-")
|
|
16
|
+
assert len(parts) == 5
|
|
17
|
+
assert len(parts[0]) == 8
|
|
18
|
+
assert len(parts[1]) == 4
|
|
19
|
+
assert len(parts[2]) == 4
|
|
20
|
+
assert len(parts[3]) == 4
|
|
21
|
+
assert len(parts[4]) == 12
|
|
22
|
+
# UUID v4 marker
|
|
23
|
+
assert parts[2][0] == "4"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_new_checkpoint_emitted_at_iso8601():
|
|
27
|
+
cp = new_checkpoint("task-1", "agent-1", "summary")
|
|
28
|
+
# Should not raise ValueError
|
|
29
|
+
datetime.fromisoformat(cp.emitted_at.replace("Z", "+00:00"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_resume_from_parent_id():
|
|
33
|
+
previous = new_checkpoint("task-1", "agent-1", "summary")
|
|
34
|
+
resumed = resume_from(previous, "agent-2")
|
|
35
|
+
assert resumed.parent_checkpoint_id == previous.checkpoint_id
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_resume_from_preserves_task_id():
|
|
39
|
+
previous = new_checkpoint("task-1", "agent-1", "summary")
|
|
40
|
+
resumed = resume_from(previous, "agent-2")
|
|
41
|
+
assert resumed.task_id == previous.task_id
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_resume_from_copies_completed_steps():
|
|
45
|
+
# Actually, let's use the constructor to make a previous with steps
|
|
46
|
+
previous_with_steps = Checkpoint(
|
|
47
|
+
ac_version="0.1.0",
|
|
48
|
+
checkpoint_id="uuid-1",
|
|
49
|
+
task_id="task-1",
|
|
50
|
+
agent_id="agent-1",
|
|
51
|
+
emitted_at="2026-06-16T14:30:00Z",
|
|
52
|
+
status=CheckpointStatus.PARTIAL,
|
|
53
|
+
task_summary="summary",
|
|
54
|
+
completed_steps=["step1", "step2"],
|
|
55
|
+
)
|
|
56
|
+
resumed = resume_from(previous_with_steps, "agent-2")
|
|
57
|
+
assert resumed.completed_steps == ["step1", "step2"]
|
|
58
|
+
# Ensure it's a copy
|
|
59
|
+
assert resumed.completed_steps is not previous_with_steps.completed_steps
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_resume_from_generates_new_id():
|
|
63
|
+
previous = new_checkpoint("task-1", "agent-1", "summary")
|
|
64
|
+
resumed = resume_from(previous, "agent-2")
|
|
65
|
+
assert resumed.checkpoint_id != previous.checkpoint_id
|
|
66
|
+
assert len(resumed.checkpoint_id) == 36
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from agent_checkpoint import CheckpointManager, InMemoryStorage, emit
|
|
2
|
+
from agent_checkpoint.models import CheckpointStatus, RetryStrategy
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_checkpoint_lifecycle():
|
|
6
|
+
storage = InMemoryStorage()
|
|
7
|
+
manager = CheckpointManager(storage)
|
|
8
|
+
|
|
9
|
+
task_id = "f9e8d7c6-b5a4-4d3c-2b1a-0f9e8d7c6b5a"
|
|
10
|
+
agent_id = "coder-gpt-4"
|
|
11
|
+
summary = "Implement auth"
|
|
12
|
+
|
|
13
|
+
checkpoint = manager.create_checkpoint(
|
|
14
|
+
task_id=task_id,
|
|
15
|
+
agent_id=agent_id,
|
|
16
|
+
task_summary=summary,
|
|
17
|
+
status=CheckpointStatus.PARTIAL,
|
|
18
|
+
partial_output={"file.py": "print('hello')"},
|
|
19
|
+
retry_strategy=RetryStrategy.RESUME,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
assert checkpoint.checkpoint_id is not None
|
|
23
|
+
assert checkpoint.task_id == task_id
|
|
24
|
+
assert checkpoint.agent_id == agent_id
|
|
25
|
+
assert checkpoint.status == CheckpointStatus.PARTIAL
|
|
26
|
+
assert checkpoint.partial_output == {"file.py": "print('hello')"}
|
|
27
|
+
|
|
28
|
+
retrieved = manager.get_checkpoint(checkpoint.checkpoint_id)
|
|
29
|
+
assert retrieved == checkpoint
|
|
30
|
+
|
|
31
|
+
# Test dictionary conversion for JSON compatibility
|
|
32
|
+
d = emit(checkpoint)
|
|
33
|
+
assert d["ac_version"] == "0.1.0"
|
|
34
|
+
assert d["status"] == "partial"
|
|
35
|
+
assert d["retry_strategy"] == "resume"
|