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.
- atomicguard/__init__.py +8 -3
- atomicguard/application/action_pair.py +7 -1
- atomicguard/application/agent.py +46 -6
- atomicguard/application/workflow.py +494 -11
- atomicguard/domain/__init__.py +4 -1
- atomicguard/domain/exceptions.py +19 -0
- atomicguard/domain/interfaces.py +137 -6
- atomicguard/domain/models.py +120 -6
- atomicguard/guards/__init__.py +16 -5
- atomicguard/guards/composite/__init__.py +11 -0
- atomicguard/guards/dynamic/__init__.py +13 -0
- atomicguard/guards/dynamic/test_runner.py +207 -0
- atomicguard/guards/interactive/__init__.py +11 -0
- atomicguard/guards/static/__init__.py +13 -0
- atomicguard/guards/static/imports.py +177 -0
- atomicguard/infrastructure/__init__.py +4 -1
- atomicguard/infrastructure/llm/__init__.py +3 -1
- atomicguard/infrastructure/llm/huggingface.py +180 -0
- atomicguard/infrastructure/llm/mock.py +32 -6
- atomicguard/infrastructure/llm/ollama.py +40 -17
- atomicguard/infrastructure/persistence/__init__.py +7 -1
- atomicguard/infrastructure/persistence/checkpoint.py +361 -0
- atomicguard/infrastructure/persistence/filesystem.py +69 -5
- atomicguard/infrastructure/persistence/memory.py +25 -3
- atomicguard/infrastructure/registry.py +126 -0
- atomicguard/schemas/__init__.py +142 -0
- {atomicguard-0.1.0.dist-info → atomicguard-1.2.0.dist-info}/METADATA +75 -13
- atomicguard-1.2.0.dist-info/RECORD +37 -0
- {atomicguard-0.1.0.dist-info → atomicguard-1.2.0.dist-info}/WHEEL +1 -1
- atomicguard-1.2.0.dist-info/entry_points.txt +4 -0
- atomicguard/guards/test_runner.py +0 -176
- atomicguard-0.1.0.dist-info/RECORD +0 -27
- /atomicguard/guards/{base.py → composite/base.py} +0 -0
- /atomicguard/guards/{human.py → interactive/human.py} +0 -0
- /atomicguard/guards/{syntax.py → static/syntax.py} +0 -0
- {atomicguard-0.1.0.dist-info → atomicguard-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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)
|
atomicguard/domain/__init__.py
CHANGED
|
@@ -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
|
]
|
atomicguard/domain/exceptions.py
CHANGED
|
@@ -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
|