atomicguard 0.1.0__py3-none-any.whl → 1.2.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.
Files changed (37) hide show
  1. atomicguard/__init__.py +8 -3
  2. atomicguard/application/action_pair.py +7 -1
  3. atomicguard/application/agent.py +46 -6
  4. atomicguard/application/workflow.py +494 -11
  5. atomicguard/domain/__init__.py +4 -1
  6. atomicguard/domain/exceptions.py +19 -0
  7. atomicguard/domain/interfaces.py +137 -6
  8. atomicguard/domain/models.py +120 -6
  9. atomicguard/guards/__init__.py +16 -5
  10. atomicguard/guards/composite/__init__.py +11 -0
  11. atomicguard/guards/dynamic/__init__.py +13 -0
  12. atomicguard/guards/dynamic/test_runner.py +207 -0
  13. atomicguard/guards/interactive/__init__.py +11 -0
  14. atomicguard/guards/static/__init__.py +13 -0
  15. atomicguard/guards/static/imports.py +177 -0
  16. atomicguard/infrastructure/__init__.py +4 -1
  17. atomicguard/infrastructure/llm/__init__.py +3 -1
  18. atomicguard/infrastructure/llm/huggingface.py +180 -0
  19. atomicguard/infrastructure/llm/mock.py +32 -6
  20. atomicguard/infrastructure/llm/ollama.py +40 -17
  21. atomicguard/infrastructure/persistence/__init__.py +7 -1
  22. atomicguard/infrastructure/persistence/checkpoint.py +361 -0
  23. atomicguard/infrastructure/persistence/filesystem.py +69 -5
  24. atomicguard/infrastructure/persistence/memory.py +25 -3
  25. atomicguard/infrastructure/registry.py +126 -0
  26. atomicguard/schemas/__init__.py +142 -0
  27. {atomicguard-0.1.0.dist-info → atomicguard-1.2.0.dist-info}/METADATA +75 -13
  28. atomicguard-1.2.0.dist-info/RECORD +37 -0
  29. {atomicguard-0.1.0.dist-info → atomicguard-1.2.0.dist-info}/WHEEL +1 -1
  30. atomicguard-1.2.0.dist-info/entry_points.txt +4 -0
  31. atomicguard/guards/test_runner.py +0 -176
  32. atomicguard-0.1.0.dist-info/RECORD +0 -27
  33. /atomicguard/guards/{base.py → composite/base.py} +0 -0
  34. /atomicguard/guards/{human.py → interactive/human.py} +0 -0
  35. /atomicguard/guards/{syntax.py → static/syntax.py} +0 -0
  36. {atomicguard-0.1.0.dist-info → atomicguard-1.2.0.dist-info}/licenses/LICENSE +0 -0
  37. {atomicguard-0.1.0.dist-info → atomicguard-1.2.0.dist-info}/top_level.txt +0 -0
@@ -4,13 +4,28 @@ Workflow: Orchestrates ActionPair execution across multiple steps.
4
4
  Owns WorkflowState and infers preconditions from step dependencies.
5
5
  """
6
6
 
7
- from dataclasses import dataclass
7
+ import uuid
8
+ from dataclasses import dataclass, replace
9
+ from datetime import UTC, datetime
8
10
 
9
11
  from atomicguard.application.action_pair import ActionPair
10
12
  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
13
+ from atomicguard.domain.exceptions import EscalationRequired, RmaxExhausted
14
+ from atomicguard.domain.interfaces import ArtifactDAGInterface, CheckpointDAGInterface
15
+ from atomicguard.domain.models import (
16
+ AmendmentType,
17
+ Artifact,
18
+ ArtifactSource,
19
+ ArtifactStatus,
20
+ ContextSnapshot,
21
+ FailureType,
22
+ FeedbackEntry,
23
+ HumanAmendment,
24
+ WorkflowCheckpoint,
25
+ WorkflowResult,
26
+ WorkflowState,
27
+ WorkflowStatus,
28
+ )
14
29
 
15
30
 
16
31
  @dataclass(frozen=True)
@@ -89,21 +104,22 @@ class Workflow:
89
104
  Returns:
90
105
  WorkflowResult with success status and artifacts
91
106
  """
107
+ # Generate a unique workflow_id for this execution
108
+ workflow_id = str(uuid.uuid4())
109
+
92
110
  while not self._is_goal_state():
93
111
  step = self._find_applicable()
94
112
 
95
113
  if step is None:
96
114
  return WorkflowResult(
97
- success=False,
115
+ status=WorkflowStatus.FAILED,
98
116
  artifacts=self._artifacts,
99
117
  failed_step="No applicable step",
100
118
  )
101
119
 
102
- # Extract dependencies
120
+ # Extract dependencies (keys match action_pair_ids from workflow.json)
103
121
  dependencies = {
104
- gid.replace("g_", ""): self._artifacts[gid]
105
- for gid in step.deps
106
- if gid in self._artifacts
122
+ gid: self._artifacts[gid] for gid in step.deps if gid in self._artifacts
107
123
  }
108
124
 
109
125
  # Execute via stateless agent
@@ -112,6 +128,8 @@ class Workflow:
112
128
  artifact_dag=self._dag,
113
129
  rmax=self._rmax,
114
130
  constraints=self._constraints,
131
+ action_pair_id=step.guard_id,
132
+ workflow_id=workflow_id,
115
133
  )
116
134
 
117
135
  try:
@@ -119,15 +137,24 @@ class Workflow:
119
137
  self._artifacts[step.guard_id] = artifact
120
138
  self._workflow_state.satisfy(step.guard_id, artifact.artifact_id)
121
139
 
140
+ except EscalationRequired as e:
141
+ return WorkflowResult(
142
+ status=WorkflowStatus.ESCALATION,
143
+ artifacts=self._artifacts,
144
+ failed_step=step.guard_id,
145
+ escalation_artifact=e.artifact,
146
+ escalation_feedback=e.feedback,
147
+ )
148
+
122
149
  except RmaxExhausted as e:
123
150
  return WorkflowResult(
124
- success=False,
151
+ status=WorkflowStatus.FAILED,
125
152
  artifacts=self._artifacts,
126
153
  failed_step=step.guard_id,
127
154
  provenance=tuple(e.provenance),
128
155
  )
129
156
 
130
- return WorkflowResult(success=True, artifacts=self._artifacts)
157
+ return WorkflowResult(status=WorkflowStatus.SUCCESS, artifacts=self._artifacts)
131
158
 
132
159
  def _precondition_met(self, step: WorkflowStep) -> bool:
133
160
  """Precondition: all required guards satisfied."""
@@ -147,3 +174,459 @@ class Workflow:
147
174
  return all(
148
175
  self._workflow_state.is_satisfied(step.guard_id) for step in self._steps
149
176
  )
177
+
178
+
179
+ class ResumableWorkflow(Workflow):
180
+ """
181
+ Workflow with checkpoint and resume support.
182
+
183
+ Extends Workflow with:
184
+ - Automatic checkpoint creation on failure
185
+ - Resume from checkpoint with human amendment
186
+ - Provenance tracking through amendments
187
+ """
188
+
189
+ def __init__(
190
+ self,
191
+ artifact_dag: ArtifactDAGInterface | None = None,
192
+ checkpoint_dag: CheckpointDAGInterface | None = None,
193
+ rmax: int = 3,
194
+ constraints: str = "",
195
+ auto_checkpoint: bool = True,
196
+ ):
197
+ """
198
+ Args:
199
+ artifact_dag: Repository for storing artifacts
200
+ checkpoint_dag: Repository for storing checkpoints (creates InMemory if None)
201
+ rmax: Maximum retries per step
202
+ constraints: Global constraints for the ambient environment
203
+ auto_checkpoint: Create checkpoint on failure (default True)
204
+ """
205
+ super().__init__(artifact_dag, rmax, constraints)
206
+
207
+ if checkpoint_dag is None:
208
+ from atomicguard.infrastructure.persistence import InMemoryCheckpointDAG
209
+
210
+ checkpoint_dag = InMemoryCheckpointDAG()
211
+
212
+ self._checkpoint_dag = checkpoint_dag
213
+ self._auto_checkpoint = auto_checkpoint
214
+ self._current_specification: str = ""
215
+ self._current_workflow_id: str = ""
216
+
217
+ def execute(self, specification: str) -> WorkflowResult:
218
+ """
219
+ Execute workflow with automatic checkpointing on failure.
220
+
221
+ Args:
222
+ specification: The task specification
223
+
224
+ Returns:
225
+ WorkflowResult (may include checkpoint if failed)
226
+ """
227
+ self._current_specification = specification
228
+ self._current_workflow_id = str(uuid.uuid4())
229
+
230
+ while not self._is_goal_state():
231
+ step = self._find_applicable()
232
+
233
+ if step is None:
234
+ return WorkflowResult(
235
+ status=WorkflowStatus.FAILED,
236
+ artifacts=self._artifacts,
237
+ failed_step="No applicable step",
238
+ )
239
+
240
+ # Extract dependencies (keys match action_pair_ids from workflow.json)
241
+ dependencies = {
242
+ gid: self._artifacts[gid] for gid in step.deps if gid in self._artifacts
243
+ }
244
+
245
+ # Execute via stateless agent
246
+ agent = DualStateAgent(
247
+ action_pair=step.action_pair,
248
+ artifact_dag=self._dag,
249
+ rmax=self._rmax,
250
+ constraints=self._constraints,
251
+ action_pair_id=step.guard_id,
252
+ workflow_id=self._current_workflow_id,
253
+ )
254
+
255
+ try:
256
+ artifact = agent.execute(specification, dependencies)
257
+ self._artifacts[step.guard_id] = artifact
258
+ self._workflow_state.satisfy(step.guard_id, artifact.artifact_id)
259
+
260
+ except EscalationRequired as e:
261
+ checkpoint = (
262
+ self._create_checkpoint(
263
+ failed_step=step.guard_id,
264
+ failure_type=FailureType.ESCALATION,
265
+ feedback=e.feedback,
266
+ failed_artifact=e.artifact,
267
+ provenance=[],
268
+ )
269
+ if self._auto_checkpoint
270
+ else None
271
+ )
272
+
273
+ return WorkflowResult(
274
+ status=WorkflowStatus.CHECKPOINT
275
+ if checkpoint
276
+ else WorkflowStatus.ESCALATION,
277
+ artifacts=self._artifacts,
278
+ failed_step=step.guard_id,
279
+ escalation_artifact=e.artifact,
280
+ escalation_feedback=e.feedback,
281
+ checkpoint=checkpoint,
282
+ )
283
+
284
+ except RmaxExhausted as e:
285
+ checkpoint = (
286
+ self._create_checkpoint(
287
+ failed_step=step.guard_id,
288
+ failure_type=FailureType.RMAX_EXHAUSTED,
289
+ feedback=e.provenance[-1][1] if e.provenance else str(e),
290
+ failed_artifact=e.provenance[-1][0] if e.provenance else None,
291
+ provenance=e.provenance,
292
+ )
293
+ if self._auto_checkpoint
294
+ else None
295
+ )
296
+
297
+ return WorkflowResult(
298
+ status=WorkflowStatus.CHECKPOINT
299
+ if checkpoint
300
+ else WorkflowStatus.FAILED,
301
+ artifacts=self._artifacts,
302
+ failed_step=step.guard_id,
303
+ provenance=tuple(e.provenance),
304
+ checkpoint=checkpoint,
305
+ )
306
+
307
+ return WorkflowResult(status=WorkflowStatus.SUCCESS, artifacts=self._artifacts)
308
+
309
+ def resume(
310
+ self,
311
+ checkpoint_id: str,
312
+ amendment: HumanAmendment,
313
+ ) -> WorkflowResult:
314
+ """
315
+ Resume workflow from checkpoint with human amendment.
316
+
317
+ Args:
318
+ checkpoint_id: ID of checkpoint to resume from
319
+ amendment: Human-provided amendment
320
+
321
+ Returns:
322
+ WorkflowResult (may include new checkpoint if fails again)
323
+ """
324
+ # 1. Load and validate checkpoint
325
+ checkpoint = self._checkpoint_dag.get_checkpoint(checkpoint_id)
326
+
327
+ # 2. Store amendment
328
+ self._checkpoint_dag.store_amendment(amendment)
329
+
330
+ # 3. Restore state from checkpoint
331
+ self._restore_state_from_checkpoint(checkpoint)
332
+ self._current_specification = checkpoint.specification
333
+ self._current_workflow_id = checkpoint.workflow_id
334
+
335
+ # 4. Find the failed step
336
+ step = self._find_step_by_id(checkpoint.failed_step)
337
+ if step is None:
338
+ raise ValueError(f"Step not found: {checkpoint.failed_step}")
339
+
340
+ # 5. Handle amendment based on type
341
+ if amendment.amendment_type == AmendmentType.ARTIFACT:
342
+ # Human provided artifact - validate it directly
343
+ human_artifact = self._create_amendment_artifact(
344
+ amendment, checkpoint, step
345
+ )
346
+ self._dag.store(human_artifact)
347
+
348
+ # Run guard on human artifact
349
+ dependencies = {
350
+ gid: self._artifacts[gid] for gid in step.deps if gid in self._artifacts
351
+ }
352
+
353
+ result = step.action_pair.guard.validate(human_artifact, **dependencies)
354
+
355
+ if result.passed:
356
+ # Update artifact status and proceed
357
+ updated_artifact = replace(
358
+ human_artifact,
359
+ status=ArtifactStatus.ACCEPTED,
360
+ guard_result=True,
361
+ )
362
+ self._dag.store(updated_artifact)
363
+ self._artifacts[step.guard_id] = updated_artifact
364
+ self._workflow_state.satisfy(
365
+ step.guard_id, updated_artifact.artifact_id
366
+ )
367
+ else:
368
+ # Guard failed human artifact - create new checkpoint
369
+ new_checkpoint = (
370
+ self._create_checkpoint(
371
+ failed_step=step.guard_id,
372
+ failure_type=FailureType.ESCALATION,
373
+ feedback=result.feedback,
374
+ failed_artifact=human_artifact,
375
+ provenance=[],
376
+ )
377
+ if self._auto_checkpoint
378
+ else None
379
+ )
380
+
381
+ return WorkflowResult(
382
+ status=WorkflowStatus.CHECKPOINT
383
+ if new_checkpoint
384
+ else WorkflowStatus.FAILED,
385
+ artifacts=self._artifacts,
386
+ failed_step=step.guard_id,
387
+ escalation_feedback=result.feedback,
388
+ checkpoint=new_checkpoint,
389
+ )
390
+
391
+ elif amendment.amendment_type == AmendmentType.FEEDBACK:
392
+ # Human provided feedback - inject into context and retry with agent
393
+ # Use additional_rmax from amendment
394
+ effective_rmax = self._rmax + amendment.additional_rmax
395
+
396
+ dependencies = {
397
+ gid: self._artifacts[gid] for gid in step.deps if gid in self._artifacts
398
+ }
399
+
400
+ agent = DualStateAgent(
401
+ action_pair=step.action_pair,
402
+ artifact_dag=self._dag,
403
+ rmax=effective_rmax,
404
+ constraints=self._constraints + "\n\n" + amendment.content,
405
+ action_pair_id=step.guard_id,
406
+ workflow_id=self._current_workflow_id,
407
+ )
408
+
409
+ try:
410
+ artifact = agent.execute(
411
+ self._current_specification
412
+ + "\n\n[Human Feedback]: "
413
+ + amendment.content,
414
+ dependencies,
415
+ )
416
+ self._artifacts[step.guard_id] = artifact
417
+ self._workflow_state.satisfy(step.guard_id, artifact.artifact_id)
418
+
419
+ except (EscalationRequired, RmaxExhausted) as e:
420
+ # Create new checkpoint for the new failure
421
+ if isinstance(e, EscalationRequired):
422
+ new_checkpoint = (
423
+ self._create_checkpoint(
424
+ failed_step=step.guard_id,
425
+ failure_type=FailureType.ESCALATION,
426
+ feedback=e.feedback,
427
+ failed_artifact=e.artifact,
428
+ provenance=[],
429
+ )
430
+ if self._auto_checkpoint
431
+ else None
432
+ )
433
+ else:
434
+ new_checkpoint = (
435
+ self._create_checkpoint(
436
+ failed_step=step.guard_id,
437
+ failure_type=FailureType.RMAX_EXHAUSTED,
438
+ feedback=e.provenance[-1][1] if e.provenance else str(e),
439
+ failed_artifact=e.provenance[-1][0]
440
+ if e.provenance
441
+ else None,
442
+ provenance=e.provenance,
443
+ )
444
+ if self._auto_checkpoint
445
+ else None
446
+ )
447
+
448
+ return WorkflowResult(
449
+ status=WorkflowStatus.CHECKPOINT
450
+ if new_checkpoint
451
+ else WorkflowStatus.FAILED,
452
+ artifacts=self._artifacts,
453
+ failed_step=step.guard_id,
454
+ checkpoint=new_checkpoint,
455
+ )
456
+
457
+ # 6. Continue executing remaining steps
458
+ return self._continue_execution()
459
+
460
+ def _create_checkpoint(
461
+ self,
462
+ failed_step: str,
463
+ failure_type: FailureType,
464
+ feedback: str,
465
+ failed_artifact: Artifact | None,
466
+ provenance: list[tuple[Artifact, str]],
467
+ ) -> WorkflowCheckpoint:
468
+ """Create and persist a checkpoint."""
469
+ checkpoint = WorkflowCheckpoint(
470
+ checkpoint_id=str(uuid.uuid4()),
471
+ workflow_id=self._current_workflow_id,
472
+ created_at=datetime.now(UTC).isoformat(),
473
+ specification=self._current_specification,
474
+ constraints=self._constraints,
475
+ rmax=self._rmax,
476
+ completed_steps=tuple(
477
+ step.guard_id
478
+ for step in self._steps
479
+ if self._workflow_state.is_satisfied(step.guard_id)
480
+ ),
481
+ artifact_ids=tuple(
482
+ (gid, art.artifact_id) for gid, art in self._artifacts.items()
483
+ ),
484
+ failure_type=failure_type,
485
+ failed_step=failed_step,
486
+ failed_artifact_id=failed_artifact.artifact_id if failed_artifact else None,
487
+ failure_feedback=feedback,
488
+ provenance_ids=tuple(a.artifact_id for a, _ in provenance),
489
+ )
490
+ self._checkpoint_dag.store_checkpoint(checkpoint)
491
+ return checkpoint
492
+
493
+ def _restore_state_from_checkpoint(self, checkpoint: WorkflowCheckpoint) -> None:
494
+ """Restore workflow state from checkpoint."""
495
+ # Restore satisfied guards
496
+ for guard_id in checkpoint.completed_steps:
497
+ self._workflow_state.guards[guard_id] = True
498
+
499
+ # Restore artifact references
500
+ for guard_id, artifact_id in checkpoint.artifact_ids:
501
+ artifact = self._dag.get_artifact(artifact_id)
502
+ self._artifacts[guard_id] = artifact
503
+ self._workflow_state.artifact_ids[guard_id] = artifact_id
504
+
505
+ def _find_step_by_id(self, guard_id: str) -> WorkflowStep | None:
506
+ """Find a step by its guard_id."""
507
+ for step in self._steps:
508
+ if step.guard_id == guard_id:
509
+ return step
510
+ return None
511
+
512
+ def _create_amendment_artifact(
513
+ self,
514
+ amendment: HumanAmendment,
515
+ checkpoint: WorkflowCheckpoint,
516
+ step: WorkflowStep,
517
+ ) -> Artifact:
518
+ """Create an artifact from human amendment."""
519
+ # Determine attempt number
520
+ latest = self._dag.get_latest_for_action_pair(
521
+ step.guard_id, checkpoint.workflow_id
522
+ )
523
+ attempt_number = (latest.attempt_number + 1) if latest else 1
524
+
525
+ # Build context snapshot
526
+ context = ContextSnapshot(
527
+ workflow_id=checkpoint.workflow_id,
528
+ specification=checkpoint.specification,
529
+ constraints=checkpoint.constraints,
530
+ feedback_history=tuple(
531
+ FeedbackEntry(artifact_id=aid, feedback=checkpoint.failure_feedback)
532
+ for aid in checkpoint.provenance_ids
533
+ ),
534
+ dependency_artifacts=tuple(
535
+ (gid, art.artifact_id) for gid, art in self._artifacts.items()
536
+ ),
537
+ )
538
+
539
+ return Artifact(
540
+ artifact_id=str(uuid.uuid4()),
541
+ workflow_id=checkpoint.workflow_id,
542
+ content=amendment.content,
543
+ previous_attempt_id=checkpoint.failed_artifact_id,
544
+ parent_action_pair_id=None,
545
+ action_pair_id=step.guard_id,
546
+ created_at=datetime.now(UTC).isoformat(),
547
+ attempt_number=attempt_number,
548
+ status=ArtifactStatus.PENDING,
549
+ guard_result=None,
550
+ feedback="",
551
+ context=context,
552
+ source=ArtifactSource.HUMAN,
553
+ )
554
+
555
+ def _continue_execution(self) -> WorkflowResult:
556
+ """Continue executing remaining steps after resume."""
557
+ while not self._is_goal_state():
558
+ step = self._find_applicable()
559
+
560
+ if step is None:
561
+ return WorkflowResult(
562
+ status=WorkflowStatus.FAILED,
563
+ artifacts=self._artifacts,
564
+ failed_step="No applicable step",
565
+ )
566
+
567
+ dependencies = {
568
+ gid: self._artifacts[gid] for gid in step.deps if gid in self._artifacts
569
+ }
570
+
571
+ agent = DualStateAgent(
572
+ action_pair=step.action_pair,
573
+ artifact_dag=self._dag,
574
+ rmax=self._rmax,
575
+ constraints=self._constraints,
576
+ action_pair_id=step.guard_id,
577
+ workflow_id=self._current_workflow_id,
578
+ )
579
+
580
+ try:
581
+ artifact = agent.execute(self._current_specification, dependencies)
582
+ self._artifacts[step.guard_id] = artifact
583
+ self._workflow_state.satisfy(step.guard_id, artifact.artifact_id)
584
+
585
+ except EscalationRequired as e:
586
+ checkpoint = (
587
+ self._create_checkpoint(
588
+ failed_step=step.guard_id,
589
+ failure_type=FailureType.ESCALATION,
590
+ feedback=e.feedback,
591
+ failed_artifact=e.artifact,
592
+ provenance=[],
593
+ )
594
+ if self._auto_checkpoint
595
+ else None
596
+ )
597
+
598
+ return WorkflowResult(
599
+ status=WorkflowStatus.CHECKPOINT
600
+ if checkpoint
601
+ else WorkflowStatus.ESCALATION,
602
+ artifacts=self._artifacts,
603
+ failed_step=step.guard_id,
604
+ escalation_artifact=e.artifact,
605
+ escalation_feedback=e.feedback,
606
+ checkpoint=checkpoint,
607
+ )
608
+
609
+ except RmaxExhausted as e:
610
+ checkpoint = (
611
+ self._create_checkpoint(
612
+ failed_step=step.guard_id,
613
+ failure_type=FailureType.RMAX_EXHAUSTED,
614
+ feedback=e.provenance[-1][1] if e.provenance else str(e),
615
+ failed_artifact=e.provenance[-1][0] if e.provenance else None,
616
+ provenance=e.provenance,
617
+ )
618
+ if self._auto_checkpoint
619
+ else None
620
+ )
621
+
622
+ return WorkflowResult(
623
+ status=WorkflowStatus.CHECKPOINT
624
+ if checkpoint
625
+ else WorkflowStatus.FAILED,
626
+ artifacts=self._artifacts,
627
+ failed_step=step.guard_id,
628
+ provenance=tuple(e.provenance),
629
+ checkpoint=checkpoint,
630
+ )
631
+
632
+ return WorkflowResult(status=WorkflowStatus.SUCCESS, artifacts=self._artifacts)
@@ -4,7 +4,7 @@ Domain layer for the Dual-State Framework.
4
4
  Contains core business logic with no external dependencies.
5
5
  """
6
6
 
7
- from atomicguard.domain.exceptions import RmaxExhausted
7
+ from atomicguard.domain.exceptions import EscalationRequired, RmaxExhausted
8
8
  from atomicguard.domain.interfaces import (
9
9
  ArtifactDAGInterface,
10
10
  GeneratorInterface,
@@ -20,6 +20,7 @@ from atomicguard.domain.models import (
20
20
  GuardResult,
21
21
  WorkflowResult,
22
22
  WorkflowState,
23
+ WorkflowStatus,
23
24
  )
24
25
  from atomicguard.domain.prompts import (
25
26
  PromptTemplate,
@@ -38,6 +39,7 @@ __all__ = [
38
39
  "GuardResult",
39
40
  "WorkflowState",
40
41
  "WorkflowResult",
42
+ "WorkflowStatus",
41
43
  # Prompts and Tasks (structures only, no content)
42
44
  "PromptTemplate",
43
45
  "StepDefinition",
@@ -48,4 +50,5 @@ __all__ = [
48
50
  "ArtifactDAGInterface",
49
51
  # Exceptions
50
52
  "RmaxExhausted",
53
+ "EscalationRequired",
51
54
  ]
@@ -26,3 +26,22 @@ class RmaxExhausted(Exception):
26
26
  """
27
27
  super().__init__(message)
28
28
  self.provenance = provenance
29
+
30
+
31
+ class EscalationRequired(Exception):
32
+ """
33
+ Raised when guard returns ⊥_fatal - human intervention needed.
34
+
35
+ This indicates a non-recoverable failure that should not be retried.
36
+ The workflow should surface this to the caller for human review.
37
+ """
38
+
39
+ def __init__(self, artifact: "Artifact", feedback: str):
40
+ """
41
+ Args:
42
+ artifact: The artifact that triggered escalation
43
+ feedback: Human-readable feedback explaining the fatal condition
44
+ """
45
+ super().__init__(feedback)
46
+ self.artifact = artifact
47
+ self.feedback = feedback