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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,6 @@
1
+
2
+ [dev]
3
+ pytest
4
+ pytest-cov
5
+ ruff
6
+ mypy
@@ -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"