atomicguard 0.1.0__py3-none-any.whl

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,116 @@
1
+ """
2
+ AtomicGuard: Dual-State Framework for LLM Output Management.
3
+
4
+ A framework for managing stochastic LLM outputs through deterministic
5
+ control flow, implementing the formal model from Thompson (2025).
6
+
7
+ Example:
8
+ from atomicguard import Workflow, ActionPair
9
+ from atomicguard.guards import SyntaxGuard
10
+ from atomicguard.infrastructure import OllamaGenerator
11
+
12
+ generator = OllamaGenerator(model="qwen2.5-coder:7b")
13
+ guard = SyntaxGuard()
14
+ action_pair = ActionPair(generator=generator, guard=guard)
15
+
16
+ workflow = Workflow(rmax=3)
17
+ workflow.add_step('g_code', action_pair=action_pair)
18
+ result = workflow.execute("Write a function that adds two numbers")
19
+ """
20
+
21
+ # Domain models (most commonly used)
22
+ # Application layer (orchestration)
23
+ from atomicguard.application.action_pair import ActionPair
24
+ from atomicguard.application.agent import DualStateAgent
25
+ from atomicguard.application.workflow import Workflow, WorkflowStep
26
+
27
+ # Domain exceptions
28
+ from atomicguard.domain.exceptions import RmaxExhausted
29
+
30
+ # Domain interfaces (for type hints and custom implementations)
31
+ from atomicguard.domain.interfaces import (
32
+ ArtifactDAGInterface,
33
+ GeneratorInterface,
34
+ GuardInterface,
35
+ )
36
+ from atomicguard.domain.models import (
37
+ AmbientEnvironment,
38
+ Artifact,
39
+ ArtifactStatus,
40
+ Context,
41
+ ContextSnapshot,
42
+ FeedbackEntry,
43
+ GuardResult,
44
+ WorkflowResult,
45
+ WorkflowState,
46
+ )
47
+
48
+ # Prompts and tasks (structures only - content defined by calling applications)
49
+ from atomicguard.domain.prompts import (
50
+ PromptTemplate,
51
+ StepDefinition,
52
+ TaskDefinition,
53
+ )
54
+
55
+ # Guards (commonly composed)
56
+ from atomicguard.guards import (
57
+ CompositeGuard,
58
+ DynamicTestGuard,
59
+ HumanReviewGuard,
60
+ SyntaxGuard,
61
+ TestGuard,
62
+ )
63
+ from atomicguard.infrastructure.llm import (
64
+ MockGenerator,
65
+ OllamaGenerator,
66
+ )
67
+
68
+ # Infrastructure (explicit import encouraged for dependency injection)
69
+ from atomicguard.infrastructure.persistence import (
70
+ FilesystemArtifactDAG,
71
+ InMemoryArtifactDAG,
72
+ )
73
+
74
+ __version__ = "0.1.0"
75
+
76
+ __all__ = [
77
+ # Version
78
+ "__version__",
79
+ # Domain models
80
+ "Artifact",
81
+ "ArtifactStatus",
82
+ "ContextSnapshot",
83
+ "FeedbackEntry",
84
+ "Context",
85
+ "AmbientEnvironment",
86
+ "GuardResult",
87
+ "WorkflowState",
88
+ "WorkflowResult",
89
+ # Prompts and tasks (structures only)
90
+ "PromptTemplate",
91
+ "StepDefinition",
92
+ "TaskDefinition",
93
+ # Domain interfaces
94
+ "GeneratorInterface",
95
+ "GuardInterface",
96
+ "ArtifactDAGInterface",
97
+ # Domain exceptions
98
+ "RmaxExhausted",
99
+ # Application layer
100
+ "ActionPair",
101
+ "DualStateAgent",
102
+ "Workflow",
103
+ "WorkflowStep",
104
+ # Infrastructure - Persistence
105
+ "InMemoryArtifactDAG",
106
+ "FilesystemArtifactDAG",
107
+ # Infrastructure - LLM
108
+ "OllamaGenerator",
109
+ "MockGenerator",
110
+ # Guards
111
+ "CompositeGuard",
112
+ "SyntaxGuard",
113
+ "TestGuard",
114
+ "DynamicTestGuard",
115
+ "HumanReviewGuard",
116
+ ]
@@ -0,0 +1,16 @@
1
+ """
2
+ Application layer for the Dual-State Framework.
3
+
4
+ Contains use cases and orchestration logic that coordinates domain objects.
5
+ """
6
+
7
+ from atomicguard.application.action_pair import ActionPair
8
+ from atomicguard.application.agent import DualStateAgent
9
+ from atomicguard.application.workflow import Workflow, WorkflowStep
10
+
11
+ __all__ = [
12
+ "ActionPair",
13
+ "DualStateAgent",
14
+ "Workflow",
15
+ "WorkflowStep",
16
+ ]
@@ -0,0 +1,65 @@
1
+ """
2
+ ActionPair: Atomic generation-verification transaction.
3
+
4
+ Paper Definition 6: A = ⟨a_gen, G⟩
5
+ Precondition ρ is handled at the Workflow level.
6
+ """
7
+
8
+ from atomicguard.domain.interfaces import GeneratorInterface, GuardInterface
9
+ from atomicguard.domain.models import Artifact, Context, GuardResult
10
+ from atomicguard.domain.prompts import PromptTemplate
11
+
12
+
13
+ class ActionPair:
14
+ """
15
+ Atomic generation-verification transaction.
16
+
17
+ Couples a generator with a guard to form an indivisible unit.
18
+ The precondition ρ is handled at the Workflow level.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ generator: GeneratorInterface,
24
+ guard: GuardInterface,
25
+ prompt_template: PromptTemplate | None = None,
26
+ ):
27
+ """
28
+ Args:
29
+ generator: The artifact generator
30
+ guard: The validator for generated artifacts
31
+ prompt_template: Optional structured prompt template
32
+ """
33
+ self._generator = generator
34
+ self._guard = guard
35
+ self._prompt_template = prompt_template
36
+
37
+ @property
38
+ def generator(self) -> GeneratorInterface:
39
+ """Access the generator (read-only)."""
40
+ return self._generator
41
+
42
+ @property
43
+ def guard(self) -> GuardInterface:
44
+ """Access the guard (read-only)."""
45
+ return self._guard
46
+
47
+ def execute(
48
+ self,
49
+ context: Context,
50
+ dependencies: dict[str, Artifact] | None = None,
51
+ ) -> tuple[Artifact, GuardResult]:
52
+ """
53
+ Execute the atomic generate-then-validate transaction.
54
+
55
+ Args:
56
+ context: Generation context
57
+ dependencies: Artifacts from prior workflow steps
58
+
59
+ Returns:
60
+ Tuple of (generated artifact, guard result)
61
+ """
62
+ dependencies = dependencies or {}
63
+ artifact = self._generator.generate(context, self._prompt_template)
64
+ result = self._guard.validate(artifact, **dependencies)
65
+ return artifact, result
@@ -0,0 +1,129 @@
1
+ """
2
+ DualStateAgent: Stateless executor for a single ActionPair.
3
+
4
+ Manages only EnvironmentState (the retry loop).
5
+ WorkflowState is managed by Workflow.
6
+ """
7
+
8
+ from atomicguard.application.action_pair import ActionPair
9
+ from atomicguard.domain.exceptions import RmaxExhausted
10
+ from atomicguard.domain.interfaces import ArtifactDAGInterface
11
+ from atomicguard.domain.models import (
12
+ AmbientEnvironment,
13
+ Artifact,
14
+ Context,
15
+ )
16
+
17
+
18
+ class DualStateAgent:
19
+ """
20
+ Stateless executor for a single ActionPair.
21
+
22
+ Manages only EnvironmentState (retry loop).
23
+ WorkflowState is managed by Workflow.
24
+
25
+ Dependencies from prior workflow steps are passed to both:
26
+ - Generator (via Context.dependencies) - so it can see prior artifacts
27
+ - Guard (via validate(**deps)) - for validation against prior artifacts
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ action_pair: ActionPair,
33
+ artifact_dag: ArtifactDAGInterface,
34
+ rmax: int = 3,
35
+ constraints: str = "",
36
+ ):
37
+ """
38
+ Args:
39
+ action_pair: The generator-guard pair to execute
40
+ artifact_dag: Repository for storing artifacts
41
+ rmax: Maximum retry attempts (default: 3)
42
+ constraints: Global constraints for the ambient environment
43
+ """
44
+ self._action_pair = action_pair
45
+ self._artifact_dag = artifact_dag
46
+ self._rmax = rmax
47
+ self._constraints = constraints
48
+
49
+ def execute(
50
+ self,
51
+ specification: str,
52
+ dependencies: dict[str, Artifact] | None = None,
53
+ ) -> Artifact:
54
+ """
55
+ Execute the action pair with retry logic.
56
+
57
+ Args:
58
+ specification: The task specification
59
+ dependencies: Artifacts from prior workflow steps (key -> Artifact)
60
+ Passed to both generator (via Context) and guard (via validate)
61
+
62
+ Returns:
63
+ The accepted artifact
64
+
65
+ Raises:
66
+ RmaxExhausted: If all retries fail
67
+ """
68
+ dependencies = dependencies or {}
69
+ context = self._compose_context(specification, dependencies)
70
+ feedback_history: list[tuple[Artifact, str]] = []
71
+ retry_count = 0
72
+
73
+ while retry_count <= self._rmax:
74
+ artifact, result = self._action_pair.execute(context, dependencies)
75
+
76
+ self._artifact_dag.store(
77
+ artifact, metadata="" if result.passed else result.feedback
78
+ )
79
+
80
+ if result.passed:
81
+ return artifact
82
+ else:
83
+ feedback_history.append((artifact, result.feedback))
84
+ retry_count += 1
85
+ context = self._refine_context(
86
+ specification, artifact, feedback_history, dependencies
87
+ )
88
+
89
+ raise RmaxExhausted(
90
+ f"Failed after {self._rmax} retries", provenance=feedback_history
91
+ )
92
+
93
+ def _compose_context(
94
+ self,
95
+ specification: str,
96
+ dependencies: dict[str, Artifact] | None = None,
97
+ ) -> Context:
98
+ """Compose initial context with dependencies."""
99
+ dependencies = dependencies or {}
100
+ ambient = AmbientEnvironment(
101
+ repository=self._artifact_dag, constraints=self._constraints
102
+ )
103
+ return Context(
104
+ ambient=ambient,
105
+ specification=specification,
106
+ current_artifact=None,
107
+ feedback_history=(),
108
+ dependencies=tuple(dependencies.items()), # Pass to generator
109
+ )
110
+
111
+ def _refine_context(
112
+ self,
113
+ specification: str,
114
+ artifact: Artifact,
115
+ feedback_history: list[tuple[Artifact, str]],
116
+ dependencies: dict[str, Artifact] | None = None,
117
+ ) -> Context:
118
+ """Refine context with feedback from failed attempt."""
119
+ dependencies = dependencies or {}
120
+ ambient = AmbientEnvironment(
121
+ repository=self._artifact_dag, constraints=self._constraints
122
+ )
123
+ return Context(
124
+ ambient=ambient,
125
+ specification=specification,
126
+ current_artifact=artifact.content,
127
+ feedback_history=tuple((a.content, f) for a, f in feedback_history),
128
+ dependencies=tuple(dependencies.items()), # Preserve dependencies on retry
129
+ )
@@ -0,0 +1,149 @@
1
+ """
2
+ Workflow: Orchestrates ActionPair execution across multiple steps.
3
+
4
+ Owns WorkflowState and infers preconditions from step dependencies.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from atomicguard.application.action_pair import ActionPair
10
+ from atomicguard.application.agent import DualStateAgent
11
+ from atomicguard.domain.exceptions import RmaxExhausted
12
+ from atomicguard.domain.interfaces import ArtifactDAGInterface
13
+ from atomicguard.domain.models import Artifact, WorkflowResult, WorkflowState
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class WorkflowStep:
18
+ """Internal step representation."""
19
+
20
+ guard_id: str
21
+ action_pair: ActionPair
22
+ requires: tuple[str, ...]
23
+ deps: tuple[str, ...] # Artifacts to pass to guard
24
+
25
+
26
+ class Workflow:
27
+ """
28
+ Orchestrates ActionPair execution.
29
+
30
+ Owns WorkflowState and infers preconditions from requires.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ artifact_dag: ArtifactDAGInterface | None = None,
36
+ rmax: int = 3,
37
+ constraints: str = "",
38
+ ):
39
+ """
40
+ Args:
41
+ artifact_dag: Repository for storing artifacts (creates InMemory if None)
42
+ rmax: Maximum retries per step
43
+ constraints: Global constraints for the ambient environment
44
+ """
45
+ # Lazy import to avoid circular dependency
46
+ if artifact_dag is None:
47
+ from atomicguard.infrastructure.persistence import InMemoryArtifactDAG
48
+
49
+ artifact_dag = InMemoryArtifactDAG()
50
+
51
+ self._dag = artifact_dag
52
+ self._rmax = rmax
53
+ self._constraints = constraints
54
+ self._steps: list[WorkflowStep] = []
55
+ self._workflow_state = WorkflowState()
56
+ self._artifacts: dict[str, Artifact] = {}
57
+
58
+ def add_step(
59
+ self,
60
+ guard_id: str,
61
+ action_pair: ActionPair,
62
+ requires: tuple[str, ...] = (),
63
+ deps: tuple[str, ...] | None = None,
64
+ ) -> "Workflow":
65
+ """
66
+ Register a step. Precondition inferred from requires.
67
+
68
+ Args:
69
+ guard_id: Unique identifier for this step (e.g., 'g_test', 'g_impl')
70
+ action_pair: The generator-guard pair for this step
71
+ requires: Guard IDs that must be satisfied before this step
72
+ deps: Artifact dependencies to pass to guard (defaults to requires)
73
+
74
+ Returns:
75
+ Self for fluent chaining
76
+ """
77
+ if deps is None:
78
+ deps = requires
79
+ self._steps.append(WorkflowStep(guard_id, action_pair, requires, deps))
80
+ return self # Fluent
81
+
82
+ def execute(self, specification: str) -> WorkflowResult:
83
+ """
84
+ Execute the workflow until completion or failure.
85
+
86
+ Args:
87
+ specification: The task specification
88
+
89
+ Returns:
90
+ WorkflowResult with success status and artifacts
91
+ """
92
+ while not self._is_goal_state():
93
+ step = self._find_applicable()
94
+
95
+ if step is None:
96
+ return WorkflowResult(
97
+ success=False,
98
+ artifacts=self._artifacts,
99
+ failed_step="No applicable step",
100
+ )
101
+
102
+ # Extract dependencies
103
+ dependencies = {
104
+ gid.replace("g_", ""): self._artifacts[gid]
105
+ for gid in step.deps
106
+ if gid in self._artifacts
107
+ }
108
+
109
+ # Execute via stateless agent
110
+ agent = DualStateAgent(
111
+ action_pair=step.action_pair,
112
+ artifact_dag=self._dag,
113
+ rmax=self._rmax,
114
+ constraints=self._constraints,
115
+ )
116
+
117
+ try:
118
+ artifact = agent.execute(specification, dependencies)
119
+ self._artifacts[step.guard_id] = artifact
120
+ self._workflow_state.satisfy(step.guard_id, artifact.artifact_id)
121
+
122
+ except RmaxExhausted as e:
123
+ return WorkflowResult(
124
+ success=False,
125
+ artifacts=self._artifacts,
126
+ failed_step=step.guard_id,
127
+ provenance=tuple(e.provenance),
128
+ )
129
+
130
+ return WorkflowResult(success=True, artifacts=self._artifacts)
131
+
132
+ def _precondition_met(self, step: WorkflowStep) -> bool:
133
+ """Precondition: all required guards satisfied."""
134
+ return all(self._workflow_state.is_satisfied(req) for req in step.requires)
135
+
136
+ def _find_applicable(self) -> WorkflowStep | None:
137
+ """Find first step not done and with precondition met."""
138
+ for step in self._steps:
139
+ if not self._workflow_state.is_satisfied(
140
+ step.guard_id
141
+ ) and self._precondition_met(step):
142
+ return step
143
+ return None
144
+
145
+ def _is_goal_state(self) -> bool:
146
+ """Check if all steps are satisfied."""
147
+ return all(
148
+ self._workflow_state.is_satisfied(step.guard_id) for step in self._steps
149
+ )
@@ -0,0 +1,51 @@
1
+ """
2
+ Domain layer for the Dual-State Framework.
3
+
4
+ Contains core business logic with no external dependencies.
5
+ """
6
+
7
+ from atomicguard.domain.exceptions import RmaxExhausted
8
+ from atomicguard.domain.interfaces import (
9
+ ArtifactDAGInterface,
10
+ GeneratorInterface,
11
+ GuardInterface,
12
+ )
13
+ from atomicguard.domain.models import (
14
+ AmbientEnvironment,
15
+ Artifact,
16
+ ArtifactStatus,
17
+ Context,
18
+ ContextSnapshot,
19
+ FeedbackEntry,
20
+ GuardResult,
21
+ WorkflowResult,
22
+ WorkflowState,
23
+ )
24
+ from atomicguard.domain.prompts import (
25
+ PromptTemplate,
26
+ StepDefinition,
27
+ TaskDefinition,
28
+ )
29
+
30
+ __all__ = [
31
+ # Models
32
+ "Artifact",
33
+ "ArtifactStatus",
34
+ "ContextSnapshot",
35
+ "FeedbackEntry",
36
+ "Context",
37
+ "AmbientEnvironment",
38
+ "GuardResult",
39
+ "WorkflowState",
40
+ "WorkflowResult",
41
+ # Prompts and Tasks (structures only, no content)
42
+ "PromptTemplate",
43
+ "StepDefinition",
44
+ "TaskDefinition",
45
+ # Interfaces
46
+ "GeneratorInterface",
47
+ "GuardInterface",
48
+ "ArtifactDAGInterface",
49
+ # Exceptions
50
+ "RmaxExhausted",
51
+ ]
@@ -0,0 +1,28 @@
1
+ """
2
+ Domain exceptions for the Dual-State Framework.
3
+
4
+ These represent business rule violations in the domain layer.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from atomicguard.domain.models import Artifact
11
+
12
+
13
+ class RmaxExhausted(Exception):
14
+ """
15
+ Raised when maximum retry attempts are exhausted.
16
+
17
+ This is a domain exception representing the business rule that
18
+ generation must succeed within r_max attempts.
19
+ """
20
+
21
+ def __init__(self, message: str, provenance: list[tuple["Artifact", str]]):
22
+ """
23
+ Args:
24
+ message: Human-readable error message
25
+ provenance: List of (artifact, feedback) tuples for each failed attempt
26
+ """
27
+ super().__init__(message)
28
+ self.provenance = provenance
@@ -0,0 +1,119 @@
1
+ """
2
+ Domain interfaces (Ports) for the Dual-State Framework.
3
+
4
+ These abstract base classes define the contracts that implementations must satisfy.
5
+ They have no external dependencies and represent the core domain boundaries.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ if TYPE_CHECKING:
12
+ from atomicguard.domain.models import (
13
+ Artifact,
14
+ Context,
15
+ GuardResult,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from atomicguard.domain.models import Artifact, Context, GuardResult
20
+ from atomicguard.domain.prompts import PromptTemplate
21
+
22
+
23
+ class GeneratorInterface(ABC):
24
+ """
25
+ Port for artifact generation.
26
+
27
+ Implementations connect to LLMs or other generation sources.
28
+ """
29
+
30
+ @abstractmethod
31
+ def generate(
32
+ self, context: "Context", template: Optional["PromptTemplate"] = None
33
+ ) -> "Artifact":
34
+ """
35
+ Generate an artifact based on context.
36
+
37
+ Args:
38
+ context: The generation context including specification and feedback
39
+ template: Optional prompt template for structured generation
40
+
41
+ Returns:
42
+ A new Artifact containing the generated content
43
+ """
44
+ pass
45
+
46
+
47
+ class GuardInterface(ABC):
48
+ """
49
+ Port for artifact validation.
50
+
51
+ Guards are deterministic validators that return ⊤ (pass) or ⊥ (fail with feedback).
52
+ """
53
+
54
+ @abstractmethod
55
+ def validate(
56
+ self, artifact: "Artifact", **dependencies: "Artifact"
57
+ ) -> "GuardResult":
58
+ """
59
+ Validate an artifact.
60
+
61
+ Args:
62
+ artifact: The artifact to validate
63
+ **dependencies: Artifacts from prior workflow steps (key -> Artifact)
64
+
65
+ Returns:
66
+ GuardResult with passed=True/False and optional feedback
67
+ """
68
+ pass
69
+
70
+
71
+ class ArtifactDAGInterface(ABC):
72
+ """
73
+ Port for artifact persistence.
74
+
75
+ Implementations provide append-only storage for the Versioned Repository (Definition 4).
76
+ """
77
+
78
+ @abstractmethod
79
+ def store(self, artifact: "Artifact", metadata: str = "") -> str:
80
+ """
81
+ Store an artifact in the DAG.
82
+
83
+ Args:
84
+ artifact: The artifact to store
85
+ metadata: Optional metadata string
86
+
87
+ Returns:
88
+ The artifact_id
89
+ """
90
+ pass
91
+
92
+ @abstractmethod
93
+ def get_artifact(self, artifact_id: str) -> "Artifact":
94
+ """
95
+ Retrieve an artifact by ID.
96
+
97
+ Args:
98
+ artifact_id: The unique identifier
99
+
100
+ Returns:
101
+ The artifact
102
+
103
+ Raises:
104
+ KeyError: If artifact not found
105
+ """
106
+ pass
107
+
108
+ @abstractmethod
109
+ def get_provenance(self, artifact_id: str) -> list["Artifact"]:
110
+ """
111
+ Trace the retry chain via previous_attempt_id.
112
+
113
+ Args:
114
+ artifact_id: Starting artifact
115
+
116
+ Returns:
117
+ List of artifacts from oldest to newest in the chain
118
+ """
119
+ pass