atomicguard 1.1.0__py3-none-any.whl → 2.0.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.
- atomicguard/__init__.py +4 -3
- atomicguard/application/__init__.py +5 -0
- atomicguard/application/action_pair.py +2 -2
- atomicguard/application/agent.py +2 -3
- atomicguard/application/checkpoint_service.py +130 -0
- atomicguard/application/resume_service.py +242 -0
- atomicguard/application/workflow.py +53 -3
- atomicguard/domain/__init__.py +2 -0
- atomicguard/domain/extraction.py +257 -0
- atomicguard/domain/interfaces.py +17 -2
- atomicguard/domain/models.py +91 -13
- atomicguard/domain/multiagent.py +279 -0
- atomicguard/domain/prompts.py +12 -1
- atomicguard/domain/workflow.py +642 -0
- atomicguard/infrastructure/llm/__init__.py +2 -0
- atomicguard/infrastructure/llm/huggingface.py +162 -0
- atomicguard/infrastructure/llm/mock.py +5 -4
- atomicguard/infrastructure/llm/ollama.py +5 -23
- atomicguard/infrastructure/persistence/filesystem.py +133 -46
- atomicguard/infrastructure/persistence/memory.py +10 -0
- {atomicguard-1.1.0.dist-info → atomicguard-2.0.0.dist-info}/METADATA +68 -6
- atomicguard-2.0.0.dist-info/RECORD +42 -0
- {atomicguard-1.1.0.dist-info → atomicguard-2.0.0.dist-info}/WHEEL +1 -1
- {atomicguard-1.1.0.dist-info → atomicguard-2.0.0.dist-info}/entry_points.txt +1 -0
- atomicguard-1.1.0.dist-info/RECORD +0 -36
- {atomicguard-1.1.0.dist-info → atomicguard-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {atomicguard-1.1.0.dist-info → atomicguard-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workflow Reference and Resume Support (Extension 01: Versioned Environment).
|
|
3
|
+
|
|
4
|
+
Implements:
|
|
5
|
+
- W_ref content-addressed workflow hashing (Definition 11)
|
|
6
|
+
- WorkflowResumer for checkpoint resume with integrity verification (Definition 15)
|
|
7
|
+
- HumanAmendmentProcessor for human-in-the-loop support (Definition 16)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from atomicguard.domain.models import (
|
|
18
|
+
AmbientEnvironment,
|
|
19
|
+
Artifact,
|
|
20
|
+
ArtifactSource,
|
|
21
|
+
ArtifactStatus,
|
|
22
|
+
Context,
|
|
23
|
+
ContextSnapshot,
|
|
24
|
+
FeedbackEntry,
|
|
25
|
+
GuardResult,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from atomicguard.domain.interfaces import (
|
|
30
|
+
ArtifactDAGInterface,
|
|
31
|
+
CheckpointDAGInterface,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# WORKFLOW REFERENCE (Definition 11)
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WorkflowIntegrityError(Exception):
|
|
41
|
+
"""Raised when W_ref verification fails on resume."""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class WorkflowRegistry:
|
|
47
|
+
"""
|
|
48
|
+
Singleton registry storing workflow definitions by W_ref.
|
|
49
|
+
|
|
50
|
+
Enables resolve_workflow_ref to retrieve stored workflows.
|
|
51
|
+
Thread-safe via module-level singleton pattern.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_instance: WorkflowRegistry | None = None
|
|
55
|
+
_workflows: dict[str, dict[str, Any]]
|
|
56
|
+
|
|
57
|
+
def __new__(cls) -> WorkflowRegistry:
|
|
58
|
+
if cls._instance is None:
|
|
59
|
+
cls._instance = super().__new__(cls)
|
|
60
|
+
cls._instance._workflows = {}
|
|
61
|
+
return cls._instance
|
|
62
|
+
|
|
63
|
+
def store(self, workflow: dict[str, Any]) -> str:
|
|
64
|
+
"""Store workflow and return its W_ref.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
workflow: Workflow definition dict.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Content-addressed hash (W_ref) of the workflow.
|
|
71
|
+
"""
|
|
72
|
+
w_ref = compute_workflow_ref(workflow, store=False) # Avoid recursion
|
|
73
|
+
self._workflows[w_ref] = workflow
|
|
74
|
+
return w_ref
|
|
75
|
+
|
|
76
|
+
def resolve(self, w_ref: str) -> dict[str, Any]:
|
|
77
|
+
"""Retrieve workflow by W_ref.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
w_ref: Content-addressed hash to look up.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Workflow definition dict.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
KeyError: If w_ref not found in registry.
|
|
87
|
+
"""
|
|
88
|
+
if w_ref not in self._workflows:
|
|
89
|
+
raise KeyError(f"Workflow reference not found: {w_ref}")
|
|
90
|
+
return self._workflows[w_ref]
|
|
91
|
+
|
|
92
|
+
def clear(self) -> None:
|
|
93
|
+
"""Clear all stored workflows (for testing)."""
|
|
94
|
+
self._workflows.clear()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def compute_workflow_ref(workflow: dict[str, Any], store: bool = True) -> str:
|
|
98
|
+
"""Compute content-addressed hash of workflow structure (Definition 11).
|
|
99
|
+
|
|
100
|
+
Produces deterministic hash by:
|
|
101
|
+
1. Canonical JSON serialization (sorted keys, no whitespace)
|
|
102
|
+
2. SHA-256 hash of the canonical form
|
|
103
|
+
|
|
104
|
+
By default, also stores the workflow in the registry so that
|
|
105
|
+
resolve_workflow_ref can retrieve it (integrity axiom support).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
workflow: Workflow definition dict.
|
|
109
|
+
store: If True (default), store workflow in registry for later resolution.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Hex-encoded SHA-256 hash string.
|
|
113
|
+
"""
|
|
114
|
+
# Canonical JSON: sorted keys, no extra whitespace, ensure_ascii for determinism
|
|
115
|
+
canonical = json.dumps(workflow, sort_keys=True, separators=(",", ":"))
|
|
116
|
+
w_ref = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
117
|
+
|
|
118
|
+
# Store workflow for resolve_workflow_ref support
|
|
119
|
+
if store:
|
|
120
|
+
registry = WorkflowRegistry()
|
|
121
|
+
registry._workflows[w_ref] = workflow
|
|
122
|
+
|
|
123
|
+
return w_ref
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def resolve_workflow_ref(w_ref: str) -> dict[str, Any]:
|
|
127
|
+
"""Retrieve workflow definition by W_ref.
|
|
128
|
+
|
|
129
|
+
Uses the singleton WorkflowRegistry to look up stored workflows.
|
|
130
|
+
For the integrity axiom (hash(resolve(W_ref)) == W_ref) to hold,
|
|
131
|
+
the workflow must have been stored via WorkflowRegistry.store().
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
w_ref: Content-addressed hash to look up.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Workflow definition dict.
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
KeyError: If w_ref not found in registry.
|
|
141
|
+
"""
|
|
142
|
+
registry = WorkflowRegistry()
|
|
143
|
+
return registry.resolve(w_ref)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# CONFIGURATION REFERENCE (Definition 33 - Extension 07)
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def compute_config_ref(
|
|
152
|
+
action_pair_id: str,
|
|
153
|
+
workflow_config: dict[str, Any],
|
|
154
|
+
prompt_config: dict[str, Any],
|
|
155
|
+
upstream_artifacts: dict[str, Artifact] | None = None,
|
|
156
|
+
) -> str:
|
|
157
|
+
"""Compute configuration reference Ψ_ref for an action pair (Definition 33).
|
|
158
|
+
|
|
159
|
+
The Ψ_ref is a content-addressable fingerprint that changes when:
|
|
160
|
+
- Prompt configuration changes
|
|
161
|
+
- Model/guard configuration changes
|
|
162
|
+
- Upstream action pair Ψ_ref changes
|
|
163
|
+
- Upstream artifact content changes
|
|
164
|
+
|
|
165
|
+
This enables incremental execution - skip unchanged action pairs.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
action_pair_id: ID of the action pair to compute ref for.
|
|
169
|
+
workflow_config: Full workflow configuration dict.
|
|
170
|
+
prompt_config: Full prompt configuration dict.
|
|
171
|
+
upstream_artifacts: Map of dependency action_pair_id → Artifact.
|
|
172
|
+
If None, computes ref for root action pair with no dependencies.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Hex-encoded SHA-256 hash string.
|
|
176
|
+
"""
|
|
177
|
+
ap_config = workflow_config.get("action_pairs", {}).get(action_pair_id, {})
|
|
178
|
+
prompt = prompt_config.get(action_pair_id, {})
|
|
179
|
+
|
|
180
|
+
# Collect upstream refs and artifact content hashes
|
|
181
|
+
upstream_refs: dict[str, str | None] = {}
|
|
182
|
+
artifact_hashes: dict[str, str] = {}
|
|
183
|
+
|
|
184
|
+
if upstream_artifacts:
|
|
185
|
+
for dep_id in ap_config.get("requires", []):
|
|
186
|
+
dep_artifact = upstream_artifacts.get(dep_id)
|
|
187
|
+
if dep_artifact:
|
|
188
|
+
upstream_refs[dep_id] = dep_artifact.config_ref
|
|
189
|
+
artifact_hashes[dep_id] = hashlib.sha256(
|
|
190
|
+
dep_artifact.content.encode("utf-8")
|
|
191
|
+
).hexdigest()
|
|
192
|
+
|
|
193
|
+
# Build canonical input for hashing
|
|
194
|
+
hash_input = {
|
|
195
|
+
"prompt": prompt,
|
|
196
|
+
"model": ap_config.get("model", workflow_config.get("model")),
|
|
197
|
+
"guard": ap_config.get("guard"),
|
|
198
|
+
"guard_config": ap_config.get("guard_config", {}),
|
|
199
|
+
"upstream_refs": upstream_refs,
|
|
200
|
+
"artifact_hashes": artifact_hashes,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Canonical JSON: sorted keys, no extra whitespace
|
|
204
|
+
canonical = json.dumps(hash_input, sort_keys=True, separators=(",", ":"))
|
|
205
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# =============================================================================
|
|
209
|
+
# WORKFLOW RESUME SUPPORT (Definition 15)
|
|
210
|
+
# =============================================================================
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class ResumeResult:
|
|
215
|
+
"""Result of resuming a workflow from checkpoint."""
|
|
216
|
+
|
|
217
|
+
success: bool
|
|
218
|
+
workflow_state: RestoredWorkflowState | None = None
|
|
219
|
+
error: str | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class RestoredWorkflowState:
|
|
224
|
+
"""Reconstructed workflow state for resume."""
|
|
225
|
+
|
|
226
|
+
completed_steps: tuple[str, ...]
|
|
227
|
+
artifact_ids: dict[str, str]
|
|
228
|
+
next_step: str | None = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class WorkflowResumer:
|
|
232
|
+
"""Handles checkpoint resume with W_ref integrity verification (Definition 15).
|
|
233
|
+
|
|
234
|
+
Ensures that:
|
|
235
|
+
1. Workflow hasn't changed since checkpoint (W_ref verification)
|
|
236
|
+
2. Context is reconstructed from repository, not checkpoint
|
|
237
|
+
3. Feedback history is preserved from provenance chain
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
checkpoint_dag: CheckpointDAGInterface,
|
|
243
|
+
artifact_dag: ArtifactDAGInterface | None = None,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Initialize resumer with checkpoint and artifact DAGs.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
checkpoint_dag: DAG storing checkpoints and amendments.
|
|
249
|
+
artifact_dag: DAG storing artifacts (optional, for context reconstruction).
|
|
250
|
+
"""
|
|
251
|
+
self._checkpoint_dag = checkpoint_dag
|
|
252
|
+
self._artifact_dag = artifact_dag
|
|
253
|
+
|
|
254
|
+
def verify_workflow_integrity(self, checkpoint_id: str) -> None:
|
|
255
|
+
"""Verify W_ref integrity for checkpoint.
|
|
256
|
+
|
|
257
|
+
Does nothing if W_ref is not set on checkpoint (backwards compatibility).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
checkpoint_id: ID of checkpoint to verify.
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
WorkflowIntegrityError: If checkpoint W_ref doesn't match stored workflow.
|
|
264
|
+
"""
|
|
265
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
266
|
+
if checkpoint is None:
|
|
267
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
268
|
+
|
|
269
|
+
# Check if checkpoint has workflow_ref (may not if created before Extension 01)
|
|
270
|
+
if hasattr(checkpoint, "workflow_ref") and checkpoint.workflow_ref:
|
|
271
|
+
try:
|
|
272
|
+
resolved = resolve_workflow_ref(checkpoint.workflow_ref)
|
|
273
|
+
re_hashed = compute_workflow_ref(resolved)
|
|
274
|
+
if re_hashed != checkpoint.workflow_ref:
|
|
275
|
+
raise WorkflowIntegrityError(
|
|
276
|
+
f"Workflow integrity check failed: hash mismatch for {checkpoint_id}"
|
|
277
|
+
)
|
|
278
|
+
except KeyError:
|
|
279
|
+
# Workflow not in registry - cannot verify, but don't fail
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
def resume(self, checkpoint_id: str, current_workflow_ref: str) -> ResumeResult:
|
|
283
|
+
"""Resume workflow from checkpoint with W_ref verification.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
checkpoint_id: ID of checkpoint to resume from.
|
|
287
|
+
current_workflow_ref: W_ref of current workflow definition.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
ResumeResult with success status and workflow state.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
WorkflowIntegrityError: If current_workflow_ref doesn't match checkpoint.
|
|
294
|
+
"""
|
|
295
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
296
|
+
if checkpoint is None:
|
|
297
|
+
return ResumeResult(
|
|
298
|
+
success=False, error=f"Checkpoint not found: {checkpoint_id}"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Verify W_ref matches
|
|
302
|
+
if (
|
|
303
|
+
hasattr(checkpoint, "workflow_ref")
|
|
304
|
+
and checkpoint.workflow_ref
|
|
305
|
+
and checkpoint.workflow_ref != current_workflow_ref
|
|
306
|
+
):
|
|
307
|
+
raise WorkflowIntegrityError(
|
|
308
|
+
f"Workflow changed since checkpoint. "
|
|
309
|
+
f"Expected: {checkpoint.workflow_ref}, Got: {current_workflow_ref}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Reconstruct state
|
|
313
|
+
state = self.restore_state(checkpoint_id)
|
|
314
|
+
return ResumeResult(success=True, workflow_state=state)
|
|
315
|
+
|
|
316
|
+
def restore_state(self, checkpoint_id: str) -> RestoredWorkflowState:
|
|
317
|
+
"""Restore workflow state from checkpoint.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
checkpoint_id: ID of checkpoint to restore from.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
RestoredWorkflowState with completed steps and artifact IDs.
|
|
324
|
+
"""
|
|
325
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
326
|
+
if checkpoint is None:
|
|
327
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
328
|
+
|
|
329
|
+
artifact_ids = dict(checkpoint.artifact_ids)
|
|
330
|
+
|
|
331
|
+
return RestoredWorkflowState(
|
|
332
|
+
completed_steps=checkpoint.completed_steps,
|
|
333
|
+
artifact_ids=artifact_ids,
|
|
334
|
+
next_step=checkpoint.failed_step,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def reconstruct_context(self, checkpoint_id: str) -> Context:
|
|
338
|
+
"""Reconstruct context from repository artifacts, not checkpoint.
|
|
339
|
+
|
|
340
|
+
This ensures context derivability (Definition 13) - context is
|
|
341
|
+
derived from stored items, not from external state.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
checkpoint_id: ID of checkpoint to reconstruct context for.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Reconstructed Context.
|
|
348
|
+
"""
|
|
349
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
350
|
+
if checkpoint is None:
|
|
351
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
352
|
+
|
|
353
|
+
# Build feedback history from provenance chain
|
|
354
|
+
feedback_history = self.reconstruct_feedback_history(checkpoint_id)
|
|
355
|
+
|
|
356
|
+
# Build dependency artifacts from completed steps
|
|
357
|
+
dependency_artifacts = tuple(checkpoint.artifact_ids)
|
|
358
|
+
|
|
359
|
+
# Create ambient environment (repository may be None during resume)
|
|
360
|
+
ambient = AmbientEnvironment(
|
|
361
|
+
repository=self._artifact_dag, # type: ignore[arg-type]
|
|
362
|
+
constraints=checkpoint.constraints,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return Context(
|
|
366
|
+
ambient=ambient,
|
|
367
|
+
specification=checkpoint.specification,
|
|
368
|
+
current_artifact=checkpoint.failed_artifact_id,
|
|
369
|
+
feedback_history=feedback_history,
|
|
370
|
+
dependency_artifacts=dependency_artifacts,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def get_next_attempt_number(self, checkpoint_id: str) -> int:
|
|
374
|
+
"""Get the next attempt number for the failed step.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
checkpoint_id: ID of checkpoint.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Next attempt number (previous max + 1).
|
|
381
|
+
"""
|
|
382
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
383
|
+
if checkpoint is None:
|
|
384
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
385
|
+
|
|
386
|
+
# Count provenance IDs (failed attempts) and add 1
|
|
387
|
+
return len(checkpoint.provenance_ids) + 1
|
|
388
|
+
|
|
389
|
+
def reconstruct_feedback_history(
|
|
390
|
+
self, checkpoint_id: str
|
|
391
|
+
) -> tuple[tuple[str, str], ...]:
|
|
392
|
+
"""Reconstruct H_feedback from provenance chain.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
checkpoint_id: ID of checkpoint.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Tuple of (artifact_id, feedback) pairs.
|
|
399
|
+
"""
|
|
400
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
401
|
+
if checkpoint is None:
|
|
402
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
403
|
+
|
|
404
|
+
# Build feedback history from provenance and failure feedback
|
|
405
|
+
history = []
|
|
406
|
+
|
|
407
|
+
# Add feedback from provenance chain
|
|
408
|
+
for artifact_id in checkpoint.provenance_ids:
|
|
409
|
+
if self._artifact_dag is not None:
|
|
410
|
+
try:
|
|
411
|
+
artifact = self._artifact_dag.get_artifact(artifact_id)
|
|
412
|
+
if (
|
|
413
|
+
artifact
|
|
414
|
+
and artifact.guard_result
|
|
415
|
+
and artifact.guard_result.feedback
|
|
416
|
+
):
|
|
417
|
+
history.append((artifact_id, artifact.guard_result.feedback))
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
# Add the failure feedback
|
|
422
|
+
if checkpoint.failed_artifact_id and checkpoint.failure_feedback:
|
|
423
|
+
history.append((checkpoint.failed_artifact_id, checkpoint.failure_feedback))
|
|
424
|
+
|
|
425
|
+
return tuple(history)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# =============================================================================
|
|
429
|
+
# HUMAN AMENDMENT PROCESSOR (Definition 16)
|
|
430
|
+
# =============================================================================
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@dataclass
|
|
434
|
+
class ProcessResult:
|
|
435
|
+
"""Result of processing a human amendment."""
|
|
436
|
+
|
|
437
|
+
success: bool
|
|
438
|
+
artifact: Artifact | None = None
|
|
439
|
+
guard_result: GuardResult | None = None
|
|
440
|
+
should_retry: bool = False
|
|
441
|
+
error: str | None = None
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class HumanAmendmentProcessor:
|
|
445
|
+
"""Processes human amendments for the human-in-the-loop pattern (Definition 16).
|
|
446
|
+
|
|
447
|
+
Human amendments can:
|
|
448
|
+
1. Provide new artifact content directly
|
|
449
|
+
2. Provide additional feedback for LLM retry
|
|
450
|
+
3. Skip optional steps
|
|
451
|
+
|
|
452
|
+
All human artifacts flow through the same guard validation as generated artifacts.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
def __init__(
|
|
456
|
+
self,
|
|
457
|
+
checkpoint_dag: CheckpointDAGInterface,
|
|
458
|
+
artifact_dag: ArtifactDAGInterface | None = None,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Initialize processor with checkpoint and artifact DAGs.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
checkpoint_dag: DAG storing checkpoints and amendments.
|
|
464
|
+
artifact_dag: DAG storing artifacts (for artifact creation).
|
|
465
|
+
"""
|
|
466
|
+
self._checkpoint_dag = checkpoint_dag
|
|
467
|
+
self._artifact_dag = artifact_dag
|
|
468
|
+
|
|
469
|
+
def process_amendment(self, amendment_id: str) -> ProcessResult:
|
|
470
|
+
"""Process a human amendment.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
amendment_id: ID of amendment to process.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
ProcessResult with success status.
|
|
477
|
+
"""
|
|
478
|
+
amendment = self._checkpoint_dag.get_amendment(amendment_id)
|
|
479
|
+
if amendment is None:
|
|
480
|
+
return ProcessResult(
|
|
481
|
+
success=False, error=f"Amendment not found: {amendment_id}"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Create artifact from amendment
|
|
485
|
+
artifact = self.create_artifact_from_amendment(amendment_id)
|
|
486
|
+
|
|
487
|
+
# Default guard result (actual guard validation happens in workflow)
|
|
488
|
+
guard_result = GuardResult(passed=True, feedback="Human amendment accepted")
|
|
489
|
+
|
|
490
|
+
return ProcessResult(
|
|
491
|
+
success=True,
|
|
492
|
+
artifact=artifact,
|
|
493
|
+
guard_result=guard_result,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def create_artifact_from_amendment(self, amendment_id: str) -> Artifact:
|
|
497
|
+
"""Create artifact from human amendment.
|
|
498
|
+
|
|
499
|
+
The artifact is marked with ArtifactSource.HUMAN to distinguish
|
|
500
|
+
it from generated artifacts.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
amendment_id: ID of amendment.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
New Artifact with human content.
|
|
507
|
+
"""
|
|
508
|
+
amendment = self._checkpoint_dag.get_amendment(amendment_id)
|
|
509
|
+
if amendment is None:
|
|
510
|
+
raise ValueError(f"Amendment not found: {amendment_id}")
|
|
511
|
+
|
|
512
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(amendment.checkpoint_id)
|
|
513
|
+
if checkpoint is None:
|
|
514
|
+
raise ValueError(f"Checkpoint not found: {amendment.checkpoint_id}")
|
|
515
|
+
|
|
516
|
+
# Build context snapshot
|
|
517
|
+
context_snapshot = ContextSnapshot(
|
|
518
|
+
workflow_id=checkpoint.workflow_id,
|
|
519
|
+
specification=checkpoint.specification,
|
|
520
|
+
constraints=checkpoint.constraints,
|
|
521
|
+
feedback_history=tuple(
|
|
522
|
+
FeedbackEntry(artifact_id=aid, feedback=fb)
|
|
523
|
+
for aid, fb in [
|
|
524
|
+
(checkpoint.failed_artifact_id, checkpoint.failure_feedback)
|
|
525
|
+
]
|
|
526
|
+
if aid and fb
|
|
527
|
+
),
|
|
528
|
+
dependency_artifacts=checkpoint.artifact_ids,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Create artifact with HUMAN source
|
|
532
|
+
artifact = Artifact(
|
|
533
|
+
artifact_id=f"human-{amendment_id}",
|
|
534
|
+
workflow_id=checkpoint.workflow_id,
|
|
535
|
+
content=amendment.content,
|
|
536
|
+
previous_attempt_id=amendment.parent_artifact_id,
|
|
537
|
+
parent_action_pair_id=None,
|
|
538
|
+
action_pair_id=checkpoint.failed_step,
|
|
539
|
+
created_at=amendment.created_at,
|
|
540
|
+
attempt_number=len(checkpoint.provenance_ids) + 1,
|
|
541
|
+
status=ArtifactStatus.PENDING,
|
|
542
|
+
guard_result=None, # Guard result set after validation
|
|
543
|
+
context=context_snapshot,
|
|
544
|
+
source=ArtifactSource.HUMAN,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Store artifact if DAG is available
|
|
548
|
+
if self._artifact_dag is not None:
|
|
549
|
+
self._artifact_dag.store(artifact)
|
|
550
|
+
|
|
551
|
+
return artifact
|
|
552
|
+
|
|
553
|
+
def process_amendment_with_guard(
|
|
554
|
+
self, amendment_id: str, guard_passes: bool
|
|
555
|
+
) -> ProcessResult:
|
|
556
|
+
"""Process amendment with explicit guard result.
|
|
557
|
+
|
|
558
|
+
Used for testing and scenarios where guard validation is external.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
amendment_id: ID of amendment.
|
|
562
|
+
guard_passes: Whether guard should pass.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
ProcessResult with should_retry flag if guard fails.
|
|
566
|
+
"""
|
|
567
|
+
amendment = self._checkpoint_dag.get_amendment(amendment_id)
|
|
568
|
+
if amendment is None:
|
|
569
|
+
return ProcessResult(
|
|
570
|
+
success=False, error=f"Amendment not found: {amendment_id}"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
artifact = self.create_artifact_from_amendment(amendment_id)
|
|
574
|
+
|
|
575
|
+
if guard_passes:
|
|
576
|
+
guard_result = GuardResult(passed=True, feedback="")
|
|
577
|
+
return ProcessResult(
|
|
578
|
+
success=True,
|
|
579
|
+
artifact=artifact,
|
|
580
|
+
guard_result=guard_result,
|
|
581
|
+
should_retry=False,
|
|
582
|
+
)
|
|
583
|
+
else:
|
|
584
|
+
guard_result = GuardResult(
|
|
585
|
+
passed=False, feedback="Guard rejected human artifact"
|
|
586
|
+
)
|
|
587
|
+
return ProcessResult(
|
|
588
|
+
success=True,
|
|
589
|
+
artifact=artifact,
|
|
590
|
+
guard_result=guard_result,
|
|
591
|
+
should_retry=True,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def get_checkpoint_context(self, checkpoint_id: str) -> Context:
|
|
595
|
+
"""Get context from checkpoint.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
checkpoint_id: ID of checkpoint.
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Context from checkpoint.
|
|
602
|
+
"""
|
|
603
|
+
checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
|
|
604
|
+
if checkpoint is None:
|
|
605
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
606
|
+
|
|
607
|
+
ambient = AmbientEnvironment(
|
|
608
|
+
repository=self._artifact_dag, # type: ignore[arg-type]
|
|
609
|
+
constraints=checkpoint.constraints,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
return Context(
|
|
613
|
+
ambient=ambient,
|
|
614
|
+
specification=checkpoint.specification,
|
|
615
|
+
current_artifact=checkpoint.failed_artifact_id,
|
|
616
|
+
feedback_history=(),
|
|
617
|
+
dependency_artifacts=checkpoint.artifact_ids,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
def apply_amendment_to_context(self, amendment_id: str) -> Context:
|
|
621
|
+
"""Apply amendment to checkpoint context (⊕ operator).
|
|
622
|
+
|
|
623
|
+
The amendment's content/context is appended to the checkpoint's
|
|
624
|
+
specification using the monotonic amendment operator.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
amendment_id: ID of amendment.
|
|
628
|
+
|
|
629
|
+
Returns:
|
|
630
|
+
Amended Context with additional information.
|
|
631
|
+
"""
|
|
632
|
+
amendment = self._checkpoint_dag.get_amendment(amendment_id)
|
|
633
|
+
if amendment is None:
|
|
634
|
+
raise ValueError(f"Amendment not found: {amendment_id}")
|
|
635
|
+
|
|
636
|
+
# Get base context from checkpoint
|
|
637
|
+
base_context = self.get_checkpoint_context(amendment.checkpoint_id)
|
|
638
|
+
|
|
639
|
+
# Apply amendment using ⊕ operator
|
|
640
|
+
if amendment.context:
|
|
641
|
+
return base_context.amend(delta_spec=amendment.context)
|
|
642
|
+
return base_context
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
LLM adapters for artifact generation.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from atomicguard.infrastructure.llm.huggingface import HuggingFaceGenerator
|
|
5
6
|
from atomicguard.infrastructure.llm.mock import MockGenerator
|
|
6
7
|
from atomicguard.infrastructure.llm.ollama import OllamaGenerator
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
10
|
+
"HuggingFaceGenerator",
|
|
9
11
|
"MockGenerator",
|
|
10
12
|
"OllamaGenerator",
|
|
11
13
|
]
|