atomicguard 1.2.0__tar.gz → 2.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.
- {atomicguard-1.2.0/src/atomicguard.egg-info → atomicguard-2.1.0}/PKG-INFO +6 -1
- {atomicguard-1.2.0 → atomicguard-2.1.0}/pyproject.toml +9 -2
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/__init__.py +4 -3
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/application/__init__.py +5 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/application/action_pair.py +2 -2
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/application/agent.py +2 -3
- atomicguard-2.1.0/src/atomicguard/application/checkpoint_service.py +130 -0
- atomicguard-2.1.0/src/atomicguard/application/resume_service.py +242 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/application/workflow.py +53 -3
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/domain/__init__.py +2 -0
- atomicguard-2.1.0/src/atomicguard/domain/extraction.py +257 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/domain/interfaces.py +17 -2
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/domain/models.py +91 -13
- atomicguard-2.1.0/src/atomicguard/domain/multiagent.py +279 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/domain/prompts.py +12 -1
- atomicguard-2.1.0/src/atomicguard/domain/workflow.py +642 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/llm/huggingface.py +4 -22
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/llm/mock.py +5 -4
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/llm/ollama.py +5 -23
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/persistence/filesystem.py +133 -46
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/persistence/memory.py +10 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0/src/atomicguard.egg-info}/PKG-INFO +6 -1
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard.egg-info/SOURCES.txt +5 -0
- atomicguard-2.1.0/src/atomicguard.egg-info/requires.txt +10 -0
- atomicguard-1.2.0/src/atomicguard.egg-info/requires.txt +0 -4
- {atomicguard-1.2.0 → atomicguard-2.1.0}/LICENSE +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/README.md +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/setup.cfg +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/domain/exceptions.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/composite/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/composite/base.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/dynamic/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/dynamic/test_runner.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/interactive/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/interactive/human.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/static/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/static/imports.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/guards/static/syntax.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/llm/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/persistence/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/persistence/checkpoint.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/infrastructure/registry.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard/schemas/__init__.py +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard.egg-info/dependency_links.txt +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard.egg-info/entry_points.txt +0 -0
- {atomicguard-1.2.0 → atomicguard-2.1.0}/src/atomicguard.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atomicguard
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A Dual-State Agent Framework for reliable LLM code generation with guard-validated loops
|
|
5
5
|
Author-email: Matthew Thompson <thompsonson@gmail.com>
|
|
6
6
|
Maintainer-email: Matthew Thompson <thompsonson@gmail.com>
|
|
@@ -30,6 +30,11 @@ Requires-Dist: matplotlib>=3.10.0
|
|
|
30
30
|
Requires-Dist: openhands-ai>=0.27.0
|
|
31
31
|
Requires-Dist: pydantic-ai>=1.0.0
|
|
32
32
|
Requires-Dist: pytestarch>=4.0.1
|
|
33
|
+
Provides-Extra: experiment
|
|
34
|
+
Requires-Dist: datasets>=2.0.0; extra == "experiment"
|
|
35
|
+
Requires-Dist: huggingface_hub>=0.20; extra == "experiment"
|
|
36
|
+
Requires-Dist: swebench>=2.0.0; extra == "experiment"
|
|
37
|
+
Requires-Dist: docker>=7.0.0; extra == "experiment"
|
|
33
38
|
Dynamic: license-file
|
|
34
39
|
|
|
35
40
|
# AtomicGuard
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "atomicguard"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "2.1.0"
|
|
4
4
|
description = "A Dual-State Agent Framework for reliable LLM code generation with guard-validated loops"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -54,8 +54,15 @@ OllamaGenerator = "atomicguard.infrastructure.llm:OllamaGenerator"
|
|
|
54
54
|
HuggingFaceGenerator = "atomicguard.infrastructure.llm:HuggingFaceGenerator"
|
|
55
55
|
MockGenerator = "atomicguard.infrastructure.llm:MockGenerator"
|
|
56
56
|
|
|
57
|
+
[project.optional-dependencies]
|
|
58
|
+
experiment = [
|
|
59
|
+
"datasets>=2.0.0",
|
|
60
|
+
"huggingface_hub>=0.20",
|
|
61
|
+
"swebench>=2.0.0",
|
|
62
|
+
"docker>=7.0.0",
|
|
63
|
+
]
|
|
64
|
+
|
|
57
65
|
# Note: Dev dependencies are in [dependency-groups] below, not here.
|
|
58
|
-
# [project.optional-dependencies] is for end-user extras only.
|
|
59
66
|
|
|
60
67
|
[build-system]
|
|
61
68
|
requires = ["setuptools>=75.0.0", "wheel"]
|
|
@@ -5,13 +5,14 @@ A framework for managing stochastic LLM outputs through deterministic
|
|
|
5
5
|
control flow, implementing the formal model from Thompson (2025).
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
from atomicguard import Workflow, ActionPair
|
|
8
|
+
from atomicguard import Workflow, ActionPair, PromptTemplate
|
|
9
9
|
from atomicguard.guards import SyntaxGuard
|
|
10
10
|
from atomicguard.infrastructure import OllamaGenerator
|
|
11
11
|
|
|
12
12
|
generator = OllamaGenerator(model="qwen2.5-coder:7b")
|
|
13
13
|
guard = SyntaxGuard()
|
|
14
|
-
|
|
14
|
+
template = PromptTemplate(role="code generator", constraints="write clean Python code", task="generate code")
|
|
15
|
+
action_pair = ActionPair(generator=generator, guard=guard, prompt_template=template)
|
|
15
16
|
|
|
16
17
|
workflow = Workflow(rmax=3)
|
|
17
18
|
workflow.add_step('g_code', action_pair=action_pair)
|
|
@@ -73,7 +74,7 @@ from atomicguard.infrastructure.persistence import (
|
|
|
73
74
|
InMemoryArtifactDAG,
|
|
74
75
|
)
|
|
75
76
|
|
|
76
|
-
__version__ = "1.
|
|
77
|
+
__version__ = "2.1.0"
|
|
77
78
|
|
|
78
79
|
__all__ = [
|
|
79
80
|
# Version
|
|
@@ -6,11 +6,16 @@ Contains use cases and orchestration logic that coordinates domain objects.
|
|
|
6
6
|
|
|
7
7
|
from atomicguard.application.action_pair import ActionPair
|
|
8
8
|
from atomicguard.application.agent import DualStateAgent
|
|
9
|
+
from atomicguard.application.checkpoint_service import CheckpointService
|
|
10
|
+
from atomicguard.application.resume_service import ResumeResult, WorkflowResumeService
|
|
9
11
|
from atomicguard.application.workflow import Workflow, WorkflowStep
|
|
10
12
|
|
|
11
13
|
__all__ = [
|
|
12
14
|
"ActionPair",
|
|
15
|
+
"CheckpointService",
|
|
13
16
|
"DualStateAgent",
|
|
17
|
+
"ResumeResult",
|
|
14
18
|
"Workflow",
|
|
19
|
+
"WorkflowResumeService",
|
|
15
20
|
"WorkflowStep",
|
|
16
21
|
]
|
|
@@ -22,13 +22,13 @@ class ActionPair:
|
|
|
22
22
|
self,
|
|
23
23
|
generator: GeneratorInterface,
|
|
24
24
|
guard: GuardInterface,
|
|
25
|
-
prompt_template: PromptTemplate
|
|
25
|
+
prompt_template: PromptTemplate,
|
|
26
26
|
):
|
|
27
27
|
"""
|
|
28
28
|
Args:
|
|
29
29
|
generator: The artifact generator
|
|
30
30
|
guard: The validator for generated artifacts
|
|
31
|
-
prompt_template:
|
|
31
|
+
prompt_template: Structured prompt template for generation
|
|
32
32
|
"""
|
|
33
33
|
self._generator = generator
|
|
34
34
|
self._guard = guard
|
|
@@ -92,15 +92,14 @@ class DualStateAgent:
|
|
|
92
92
|
for a, f in feedback_history
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
-
# Update artifact with
|
|
95
|
+
# Update artifact with full GuardResult (Extension 08: Composite Guards)
|
|
96
96
|
artifact = replace(
|
|
97
97
|
artifact,
|
|
98
98
|
previous_attempt_id=previous_id,
|
|
99
99
|
status=ArtifactStatus.ACCEPTED
|
|
100
100
|
if result.passed
|
|
101
101
|
else ArtifactStatus.REJECTED,
|
|
102
|
-
guard_result=result
|
|
103
|
-
feedback=result.feedback,
|
|
102
|
+
guard_result=result, # Store full GuardResult, not just bool
|
|
104
103
|
context=replace(
|
|
105
104
|
artifact.context,
|
|
106
105
|
feedback_history=fb_entries,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Application service for checkpoint operations.
|
|
2
|
+
|
|
3
|
+
Provides clean separation of checkpoint creation from workflow execution,
|
|
4
|
+
with Extension 01 W_ref support for workflow integrity verification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from atomicguard.domain.models import FailureType, WorkflowCheckpoint
|
|
14
|
+
from atomicguard.domain.workflow import compute_workflow_ref
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from atomicguard.domain.interfaces import (
|
|
18
|
+
ArtifactDAGInterface,
|
|
19
|
+
CheckpointDAGInterface,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CheckpointService:
|
|
24
|
+
"""Application service for creating and managing checkpoints.
|
|
25
|
+
|
|
26
|
+
Separates checkpoint creation concerns from Workflow execution,
|
|
27
|
+
enabling cleaner DDD architecture with proper layering.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
checkpoint_dag: CheckpointDAGInterface,
|
|
33
|
+
artifact_dag: ArtifactDAGInterface | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Initialize checkpoint service.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
checkpoint_dag: DAG for storing checkpoints.
|
|
39
|
+
artifact_dag: DAG for storing artifacts (optional).
|
|
40
|
+
"""
|
|
41
|
+
self._checkpoint_dag = checkpoint_dag
|
|
42
|
+
self._artifact_dag = artifact_dag
|
|
43
|
+
|
|
44
|
+
def create_checkpoint(
|
|
45
|
+
self,
|
|
46
|
+
workflow_definition: dict,
|
|
47
|
+
workflow_id: str,
|
|
48
|
+
specification: str,
|
|
49
|
+
constraints: str,
|
|
50
|
+
rmax: int,
|
|
51
|
+
completed_steps: tuple[str, ...],
|
|
52
|
+
artifact_ids: tuple[tuple[str, str], ...],
|
|
53
|
+
failure_type: FailureType,
|
|
54
|
+
failed_step: str,
|
|
55
|
+
failed_artifact_id: str | None,
|
|
56
|
+
failure_feedback: str,
|
|
57
|
+
provenance_ids: tuple[str, ...],
|
|
58
|
+
) -> WorkflowCheckpoint:
|
|
59
|
+
"""Create checkpoint with W_ref for integrity verification.
|
|
60
|
+
|
|
61
|
+
Computes the W_ref (content-addressed workflow hash) from the
|
|
62
|
+
workflow definition and stores it with the checkpoint. This
|
|
63
|
+
enables resume-time verification that the workflow hasn't changed.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
workflow_definition: Dict representing workflow structure for W_ref computation.
|
|
67
|
+
workflow_id: UUID of the workflow execution instance.
|
|
68
|
+
specification: Original task specification (Ψ).
|
|
69
|
+
constraints: Global constraints (Ω).
|
|
70
|
+
rmax: Original retry budget.
|
|
71
|
+
completed_steps: Guard IDs that completed successfully.
|
|
72
|
+
artifact_ids: (guard_id, artifact_id) pairs for completed artifacts.
|
|
73
|
+
failure_type: Type of failure (ESCALATION or RMAX_EXHAUSTED).
|
|
74
|
+
failed_step: Guard ID where failure occurred.
|
|
75
|
+
failed_artifact_id: ID of the last artifact before failure.
|
|
76
|
+
failure_feedback: Error/feedback message from the guard.
|
|
77
|
+
provenance_ids: Artifact IDs of all failed attempts.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
WorkflowCheckpoint with W_ref for integrity verification.
|
|
81
|
+
"""
|
|
82
|
+
# Compute W_ref using domain layer (stores in registry for later resolution)
|
|
83
|
+
w_ref = compute_workflow_ref(workflow_definition, store=True)
|
|
84
|
+
|
|
85
|
+
checkpoint = WorkflowCheckpoint(
|
|
86
|
+
checkpoint_id=str(uuid.uuid4()),
|
|
87
|
+
workflow_id=workflow_id,
|
|
88
|
+
created_at=datetime.now(UTC).isoformat(),
|
|
89
|
+
specification=specification,
|
|
90
|
+
constraints=constraints,
|
|
91
|
+
rmax=rmax,
|
|
92
|
+
completed_steps=completed_steps,
|
|
93
|
+
artifact_ids=artifact_ids,
|
|
94
|
+
failure_type=failure_type,
|
|
95
|
+
failed_step=failed_step,
|
|
96
|
+
failed_artifact_id=failed_artifact_id,
|
|
97
|
+
failure_feedback=failure_feedback,
|
|
98
|
+
provenance_ids=provenance_ids,
|
|
99
|
+
workflow_ref=w_ref, # Extension 01: W_ref for integrity
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self._checkpoint_dag.store_checkpoint(checkpoint)
|
|
103
|
+
return checkpoint
|
|
104
|
+
|
|
105
|
+
def get_checkpoint(self, checkpoint_id: str) -> WorkflowCheckpoint:
|
|
106
|
+
"""Retrieve a checkpoint by ID.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
checkpoint_id: The checkpoint identifier.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The WorkflowCheckpoint.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
KeyError: If checkpoint not found.
|
|
116
|
+
"""
|
|
117
|
+
return self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
118
|
+
|
|
119
|
+
def list_checkpoints(
|
|
120
|
+
self, workflow_id: str | None = None
|
|
121
|
+
) -> list[WorkflowCheckpoint]:
|
|
122
|
+
"""List checkpoints, optionally filtered by workflow_id.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
workflow_id: Optional filter by workflow.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of matching checkpoints, newest first.
|
|
129
|
+
"""
|
|
130
|
+
return self._checkpoint_dag.list_checkpoints(workflow_id)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Application service for workflow resume operations.
|
|
2
|
+
|
|
3
|
+
Provides clean separation of resume logic from workflow execution,
|
|
4
|
+
with Extension 01 W_ref support for workflow integrity verification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import replace
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from atomicguard.domain.models import (
|
|
13
|
+
AmendmentType,
|
|
14
|
+
Artifact,
|
|
15
|
+
ArtifactStatus,
|
|
16
|
+
GuardResult,
|
|
17
|
+
HumanAmendment,
|
|
18
|
+
)
|
|
19
|
+
from atomicguard.domain.workflow import (
|
|
20
|
+
HumanAmendmentProcessor,
|
|
21
|
+
WorkflowResumer,
|
|
22
|
+
compute_workflow_ref,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from atomicguard.domain.interfaces import (
|
|
27
|
+
ArtifactDAGInterface,
|
|
28
|
+
CheckpointDAGInterface,
|
|
29
|
+
GuardInterface,
|
|
30
|
+
)
|
|
31
|
+
from atomicguard.domain.models import Context
|
|
32
|
+
from atomicguard.domain.workflow import RestoredWorkflowState
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ResumeResult:
|
|
36
|
+
"""Result of resume operation.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
success: Whether the resume completed without errors.
|
|
40
|
+
artifact: The artifact created from the amendment (if any).
|
|
41
|
+
guard_result: The guard validation result (if validation occurred).
|
|
42
|
+
needs_retry: Whether the workflow needs a retry (feedback amendment or guard failure).
|
|
43
|
+
error: Error message if success is False.
|
|
44
|
+
amended_context: The amended context for retry (feedback amendment only).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
success: bool,
|
|
50
|
+
artifact: Artifact | None = None,
|
|
51
|
+
guard_result: GuardResult | None = None,
|
|
52
|
+
needs_retry: bool = False,
|
|
53
|
+
error: str | None = None,
|
|
54
|
+
amended_context: object | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.success = success
|
|
57
|
+
self.artifact = artifact
|
|
58
|
+
self.guard_result = guard_result
|
|
59
|
+
self.needs_retry = needs_retry
|
|
60
|
+
self.error = error
|
|
61
|
+
self.amended_context = amended_context
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class WorkflowResumeService:
|
|
65
|
+
"""Application service for resuming workflows from checkpoints.
|
|
66
|
+
|
|
67
|
+
Delegates to domain layer components (WorkflowResumer, HumanAmendmentProcessor)
|
|
68
|
+
while providing a clean interface for the presentation layer.
|
|
69
|
+
|
|
70
|
+
Extension 01 compliant: Verifies W_ref integrity on resume.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
checkpoint_dag: CheckpointDAGInterface,
|
|
76
|
+
artifact_dag: ArtifactDAGInterface,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Initialize resume service.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
checkpoint_dag: DAG for storing checkpoints and amendments.
|
|
82
|
+
artifact_dag: DAG for storing artifacts.
|
|
83
|
+
"""
|
|
84
|
+
self._checkpoint_dag = checkpoint_dag
|
|
85
|
+
self._artifact_dag = artifact_dag
|
|
86
|
+
|
|
87
|
+
# Initialize domain layer components
|
|
88
|
+
self._resumer = WorkflowResumer(checkpoint_dag, artifact_dag)
|
|
89
|
+
self._processor = HumanAmendmentProcessor(checkpoint_dag, artifact_dag)
|
|
90
|
+
|
|
91
|
+
def resume(
|
|
92
|
+
self,
|
|
93
|
+
checkpoint_id: str,
|
|
94
|
+
amendment: HumanAmendment,
|
|
95
|
+
current_workflow_definition: dict,
|
|
96
|
+
guard: GuardInterface,
|
|
97
|
+
dependencies: dict[str, Artifact] | None = None,
|
|
98
|
+
) -> ResumeResult:
|
|
99
|
+
"""Resume workflow from checkpoint with W_ref verification.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
checkpoint_id: ID of checkpoint to resume from.
|
|
103
|
+
amendment: Human-provided amendment.
|
|
104
|
+
current_workflow_definition: Current workflow dict for W_ref verification.
|
|
105
|
+
guard: Guard to validate the human artifact.
|
|
106
|
+
dependencies: Artifact dependencies for guard validation.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
ResumeResult with success status and artifact.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
WorkflowIntegrityError: If workflow changed since checkpoint.
|
|
113
|
+
"""
|
|
114
|
+
dependencies = dependencies or {}
|
|
115
|
+
|
|
116
|
+
# 1. Store amendment (required by HumanAmendmentProcessor)
|
|
117
|
+
self._checkpoint_dag.store_amendment(amendment)
|
|
118
|
+
|
|
119
|
+
# 2. Verify W_ref integrity using domain layer
|
|
120
|
+
current_w_ref = compute_workflow_ref(current_workflow_definition, store=False)
|
|
121
|
+
resume_result = self._resumer.resume(checkpoint_id, current_w_ref)
|
|
122
|
+
|
|
123
|
+
if not resume_result.success:
|
|
124
|
+
return ResumeResult(success=False, error=resume_result.error)
|
|
125
|
+
|
|
126
|
+
# 3. Process amendment based on type
|
|
127
|
+
if amendment.amendment_type == AmendmentType.ARTIFACT:
|
|
128
|
+
return self._handle_artifact_amendment(amendment, guard, dependencies)
|
|
129
|
+
|
|
130
|
+
elif amendment.amendment_type == AmendmentType.FEEDBACK:
|
|
131
|
+
# Return amended context for agent retry
|
|
132
|
+
amended_context = self._processor.apply_amendment_to_context(
|
|
133
|
+
amendment.amendment_id
|
|
134
|
+
)
|
|
135
|
+
return ResumeResult(
|
|
136
|
+
success=True,
|
|
137
|
+
needs_retry=True,
|
|
138
|
+
amended_context=amended_context,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return ResumeResult(
|
|
142
|
+
success=False, error=f"Unknown amendment type: {amendment.amendment_type}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _handle_artifact_amendment(
|
|
146
|
+
self,
|
|
147
|
+
amendment: HumanAmendment,
|
|
148
|
+
guard: GuardInterface,
|
|
149
|
+
dependencies: dict[str, Artifact],
|
|
150
|
+
) -> ResumeResult:
|
|
151
|
+
"""Process artifact amendment using domain layer.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
amendment: The human amendment with artifact content.
|
|
155
|
+
guard: Guard to validate the artifact.
|
|
156
|
+
dependencies: Artifact dependencies for guard validation.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
ResumeResult with artifact and guard result.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
# Create artifact from amendment using domain layer
|
|
163
|
+
human_artifact = self._processor.create_artifact_from_amendment(
|
|
164
|
+
amendment.amendment_id
|
|
165
|
+
)
|
|
166
|
+
except KeyError as e:
|
|
167
|
+
return ResumeResult(
|
|
168
|
+
success=False,
|
|
169
|
+
error=f"Failed to create artifact from amendment: {e}",
|
|
170
|
+
)
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
return ResumeResult(
|
|
173
|
+
success=False,
|
|
174
|
+
error=f"Invalid amendment: {e}",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Validate with guard
|
|
178
|
+
guard_result = guard.validate(human_artifact, **dependencies)
|
|
179
|
+
|
|
180
|
+
if guard_result.passed:
|
|
181
|
+
# Update artifact status
|
|
182
|
+
accepted_artifact = replace(
|
|
183
|
+
human_artifact,
|
|
184
|
+
status=ArtifactStatus.ACCEPTED,
|
|
185
|
+
guard_result=guard_result, # Store full GuardResult
|
|
186
|
+
)
|
|
187
|
+
self._artifact_dag.store(accepted_artifact)
|
|
188
|
+
|
|
189
|
+
return ResumeResult(
|
|
190
|
+
success=True,
|
|
191
|
+
artifact=accepted_artifact,
|
|
192
|
+
guard_result=guard_result,
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
# Guard failed - return for new checkpoint
|
|
196
|
+
return ResumeResult(
|
|
197
|
+
success=True,
|
|
198
|
+
artifact=human_artifact,
|
|
199
|
+
guard_result=guard_result,
|
|
200
|
+
needs_retry=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def get_restored_state(self, checkpoint_id: str) -> RestoredWorkflowState:
|
|
204
|
+
"""Get restored workflow state from checkpoint.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
checkpoint_id: ID of checkpoint to restore from.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
RestoredWorkflowState with completed steps and artifact IDs.
|
|
211
|
+
"""
|
|
212
|
+
return self._resumer.restore_state(checkpoint_id)
|
|
213
|
+
|
|
214
|
+
def get_reconstructed_context(self, checkpoint_id: str) -> Context:
|
|
215
|
+
"""Get reconstructed context from checkpoint.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
checkpoint_id: ID of checkpoint.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Reconstructed Context.
|
|
222
|
+
"""
|
|
223
|
+
return self._resumer.reconstruct_context(checkpoint_id)
|
|
224
|
+
|
|
225
|
+
def verify_workflow_integrity(
|
|
226
|
+
self, checkpoint_id: str, current_workflow_definition: dict
|
|
227
|
+
) -> bool:
|
|
228
|
+
"""Verify workflow integrity without resuming.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
checkpoint_id: ID of checkpoint to verify against.
|
|
232
|
+
current_workflow_definition: Current workflow dict.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if workflow matches checkpoint, False otherwise.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
WorkflowIntegrityError: If workflow changed since checkpoint.
|
|
239
|
+
"""
|
|
240
|
+
current_w_ref = compute_workflow_ref(current_workflow_definition, store=False)
|
|
241
|
+
result = self._resumer.resume(checkpoint_id, current_w_ref)
|
|
242
|
+
return result.success
|
|
@@ -5,6 +5,7 @@ Owns WorkflowState and infers preconditions from step dependencies.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import uuid
|
|
8
|
+
import warnings
|
|
8
9
|
from dataclasses import dataclass, replace
|
|
9
10
|
from datetime import UTC, datetime
|
|
10
11
|
|
|
@@ -175,11 +176,55 @@ class Workflow:
|
|
|
175
176
|
self._workflow_state.is_satisfied(step.guard_id) for step in self._steps
|
|
176
177
|
)
|
|
177
178
|
|
|
179
|
+
def get_workflow_definition(self) -> dict:
|
|
180
|
+
"""Build workflow definition dict for W_ref computation.
|
|
181
|
+
|
|
182
|
+
Returns a serializable dict representing the workflow structure.
|
|
183
|
+
Used by CheckpointService for W_ref computation.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dict with steps, rmax, and constraints for hashing.
|
|
187
|
+
"""
|
|
188
|
+
return {
|
|
189
|
+
"steps": [
|
|
190
|
+
{
|
|
191
|
+
"guard_id": step.guard_id,
|
|
192
|
+
"requires": list(step.requires),
|
|
193
|
+
"deps": list(step.deps),
|
|
194
|
+
}
|
|
195
|
+
for step in self._steps
|
|
196
|
+
],
|
|
197
|
+
"rmax": self._rmax,
|
|
198
|
+
"constraints": self._constraints,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def get_step(self, guard_id: str) -> WorkflowStep:
|
|
202
|
+
"""Get a workflow step by guard_id.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
guard_id: The identifier of the step to retrieve.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The WorkflowStep with the given guard_id.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
KeyError: If no step with the given guard_id exists.
|
|
212
|
+
"""
|
|
213
|
+
for step in self._steps:
|
|
214
|
+
if step.guard_id == guard_id:
|
|
215
|
+
return step
|
|
216
|
+
raise KeyError(f"Step not found: {guard_id}")
|
|
217
|
+
|
|
178
218
|
|
|
179
219
|
class ResumableWorkflow(Workflow):
|
|
180
220
|
"""
|
|
181
221
|
Workflow with checkpoint and resume support.
|
|
182
222
|
|
|
223
|
+
.. deprecated::
|
|
224
|
+
Use Workflow + CheckpointService + WorkflowResumeService instead.
|
|
225
|
+
This class is maintained for backwards compatibility but will be
|
|
226
|
+
removed in a future version.
|
|
227
|
+
|
|
183
228
|
Extends Workflow with:
|
|
184
229
|
- Automatic checkpoint creation on failure
|
|
185
230
|
- Resume from checkpoint with human amendment
|
|
@@ -202,6 +247,12 @@ class ResumableWorkflow(Workflow):
|
|
|
202
247
|
constraints: Global constraints for the ambient environment
|
|
203
248
|
auto_checkpoint: Create checkpoint on failure (default True)
|
|
204
249
|
"""
|
|
250
|
+
warnings.warn(
|
|
251
|
+
"ResumableWorkflow is deprecated. Use Workflow + CheckpointService + "
|
|
252
|
+
"WorkflowResumeService instead.",
|
|
253
|
+
DeprecationWarning,
|
|
254
|
+
stacklevel=2,
|
|
255
|
+
)
|
|
205
256
|
super().__init__(artifact_dag, rmax, constraints)
|
|
206
257
|
|
|
207
258
|
if checkpoint_dag is None:
|
|
@@ -357,7 +408,7 @@ class ResumableWorkflow(Workflow):
|
|
|
357
408
|
updated_artifact = replace(
|
|
358
409
|
human_artifact,
|
|
359
410
|
status=ArtifactStatus.ACCEPTED,
|
|
360
|
-
guard_result=
|
|
411
|
+
guard_result=result, # Store full GuardResult
|
|
361
412
|
)
|
|
362
413
|
self._dag.store(updated_artifact)
|
|
363
414
|
self._artifacts[step.guard_id] = updated_artifact
|
|
@@ -546,8 +597,7 @@ class ResumableWorkflow(Workflow):
|
|
|
546
597
|
created_at=datetime.now(UTC).isoformat(),
|
|
547
598
|
attempt_number=attempt_number,
|
|
548
599
|
status=ArtifactStatus.PENDING,
|
|
549
|
-
guard_result=None,
|
|
550
|
-
feedback="",
|
|
600
|
+
guard_result=None, # Guard result set after validation
|
|
551
601
|
context=context,
|
|
552
602
|
source=ArtifactSource.HUMAN,
|
|
553
603
|
)
|
|
@@ -18,6 +18,7 @@ from atomicguard.domain.models import (
|
|
|
18
18
|
ContextSnapshot,
|
|
19
19
|
FeedbackEntry,
|
|
20
20
|
GuardResult,
|
|
21
|
+
SubGuardOutcome,
|
|
21
22
|
WorkflowResult,
|
|
22
23
|
WorkflowState,
|
|
23
24
|
WorkflowStatus,
|
|
@@ -37,6 +38,7 @@ __all__ = [
|
|
|
37
38
|
"Context",
|
|
38
39
|
"AmbientEnvironment",
|
|
39
40
|
"GuardResult",
|
|
41
|
+
"SubGuardOutcome",
|
|
40
42
|
"WorkflowState",
|
|
41
43
|
"WorkflowResult",
|
|
42
44
|
"WorkflowStatus",
|