Document lifecycle details, security warnings, and CLI usage for the new module guides so implementors have a single reference. ISSUES CLOSED: #4848
15 KiB
Correction Attempts Module
Package: cleveragents.domain.models.core.correction
Introduced: M4 (v3.3.0); CorrectionAttemptRecord and correction_attempts table added in M7 (v3.6.0, issue #920)
The correction module provides the domain models and lifecycle management for plan decision tree corrections. When a plan's execution produces an undesirable result, a correction allows the user to revert to a prior decision point or append new guidance without restarting the entire plan.
For the design rationale see ADR-007 (Decision Tree & Correction), ADR-035 (Rollback & Replay).
Purpose
During plan execution, the actor makes a series of decisions (tool calls, resource writes, sub-plan spawns). These decisions form a tree. If the user or an automated check determines that a decision was wrong, a correction provides two recovery paths:
| Mode | Description |
|---|---|
| Revert | Invalidate the subtree rooted at the target decision; restore checkpoints; re-execute from that point with new guidance |
| Append | Add a new child plan at the target decision without invalidating existing decisions |
Core Domain Models
CorrectionRequest
from cleveragents.domain.models.core.correction import CorrectionRequest, CorrectionMode
request = CorrectionRequest(
plan_id="01ARZ3...",
target_decision_id="01BCD4...",
mode=CorrectionMode.REVERT,
guidance="The file path was wrong — use src/api/ not src/app/",
)
| Field | Type | Description |
|---|---|---|
correction_id |
str (ULID) |
Unique identifier (auto-generated) |
plan_id |
str |
Plan that owns the targeted decision tree |
target_decision_id |
str |
Decision node to apply correction at |
mode |
CorrectionMode |
revert or append |
guidance |
str (max 10,000 chars) |
Human-supplied guidance |
dry_run |
bool |
When True, only compute impact without executing |
status |
CorrectionStatus |
Current lifecycle status |
created_at |
datetime (UTC) |
When the request was created |
!!! warning "Security: validate untrusted guidance input"
The guidance field is injected directly into the LLM actor during
re-execution. When accepting guidance from untrusted integrations, validate
and sanitise the input to reduce prompt-injection risk. The 10,000-character
limit is enforced at the model layer but does not prevent malicious
instructions.
CorrectionMode
from cleveragents.domain.models.core.correction import CorrectionMode
CorrectionMode.REVERT # "revert" — invalidate subtree and re-execute
CorrectionMode.APPEND # "append" — add new child plan at target
CorrectionStatus
Lifecycle states for a correction request:
PENDING → ANALYZING → EXECUTING → APPLIED
└──► FAILED
└──► CANCELLED
└──► REJECTED
| Status | Meaning |
|---|---|
PENDING |
Request created, not yet processed |
ANALYZING |
Impact analysis in progress |
EXECUTING |
Correction is being applied |
APPLIED |
Correction successfully applied |
FAILED |
Correction failed |
CANCELLED |
Correction was cancelled |
REJECTED |
Correction rejected (e.g. applied child plans block revert) |
CorrectionImpact
Result of the impact analysis — computed before executing a correction.
from cleveragents.domain.models.core.correction import CorrectionImpact
impact = CorrectionImpact(
affected_decisions=["01BCD4...", "01BCD5..."],
excluded_decisions=["01BCD6..."],
affected_files=["src/api/handler.py"],
affected_child_plans=["01EFG7..."],
risk_level="medium",
rollback_tier="full",
rollback_tier_depth=2,
)
| Field | Type | Description |
|---|---|---|
affected_decisions |
list[str] |
Decision IDs in the affected subtree (BFS order) |
excluded_decisions |
list[str] |
Decision IDs NOT in the affected subtree (preserved) |
affected_files |
list[str] |
Files touched by affected decisions |
affected_child_plans |
list[str] |
Child plan IDs spawned by affected decisions |
estimated_cost |
float | None |
Estimated recompute cost |
risk_level |
str |
low, medium, or high |
rollback_tier |
str |
full, phase, or append_only |
rollback_tier_depth |
int |
Depth of the targeted decision from the tree root |
artifacts_to_archive |
list[str] |
Artifact paths to archive on revert |
CorrectionResult
Outcome of executing a correction. For revert corrections, includes fields for checkpoint restoration and re-execution signalling.
from cleveragents.domain.models.core.correction import CorrectionResult, CorrectionStatus
result = CorrectionResult(
correction_id="01ARZ3...",
status=CorrectionStatus.APPLIED,
reverted_decisions=["01BCD4...", "01BCD5..."],
checkpoint_restored=True,
actor_state_ref="lc:checkpoint:01XYZ...",
user_intervention_decision_id="01HIJ8...",
phase_transition_target="strategize",
)
Revert re-execution fields (spec § Correction Flow, Revert Mode):
| Field | Type | Description |
|---|---|---|
checkpoint_restored |
bool |
Whether a sandbox checkpoint was restored |
actor_state_ref |
str |
LangGraph actor checkpoint reference for reasoning rollback |
user_intervention_decision_id |
str |
ULID of the user_intervention decision created to inject guidance |
phase_transition_target |
str |
Target plan phase after revert (e.g. "strategize") |
CorrectionDryRunReport
Detailed report showing what a correction would do, without modifying state.
from cleveragents.domain.models.core.correction import CorrectionDryRunReport
report = service.generate_dry_run_report(request)
# Access impact data via the nested impact object:
print(report.impact.affected_decisions)
print(report.impact.rollback_tier_depth)
print(report.impact.affected_child_plans)
Note:
excluded_decisions,rollback_tier_depth, andchild_plans_to_rollbackwere previously duplicated at the top level. They were removed in M7 (issue #1087) to eliminate divergence risk. Access them viareport.impact.*instead.
Correction Attempt Records
The correction_attempts table (added in M7, issue #920) provides a
persistent audit trail of every correction execution attempt.
CorrectionAttemptRecord
from cleveragents.domain.models.core.correction import (
CorrectionAttemptRecord,
CorrectionAttemptState,
CorrectionMode,
)
record = CorrectionAttemptRecord(
plan_id="01ARZ3...",
original_decision_id="01BCD4...",
mode=CorrectionMode.REVERT,
guidance="Use src/api/ not src/app/",
)
# record.state == CorrectionAttemptState.PENDING
# record.correction_attempt_id — auto-generated ULID
| Field | Type | Description |
|---|---|---|
correction_attempt_id |
str (ULID) |
Unique identifier (auto-generated) |
plan_id |
str |
Plan that owns the targeted decision tree |
original_decision_id |
str |
Decision node being corrected |
new_decision_id |
str | None |
New decision created by the correction (if any) |
mode |
CorrectionMode |
revert or append |
guidance |
str (max 10,000 chars) |
Human-supplied guidance (required, non-empty) |
archived_artifacts_path |
str | None |
Path to archived artifacts (revert mode) |
state |
CorrectionAttemptState |
Current lifecycle state |
created_at |
datetime (UTC) |
When the record was created |
completed_at |
datetime | None |
When the attempt completed |
Note:
archived_artifacts_pathis always system-generated within the configured artifacts directory. The path is validated to stay inside that directory before storage, and no direct user input is accepted for this field.
Validation rules:
plan_id,original_decision_id, andguidancemust be non-empty (whitespace stripped)new_decision_idmust be non-empty when setcreated_atandcompleted_atare normalised to UTC (naive datetimes are coerced)
CorrectionAttemptState
Spec-defined lifecycle for correction attempt records:
PENDING → EXECUTING → COMPLETE
└──► FAILED
| State | Meaning |
|---|---|
PENDING |
Attempt created, not yet started |
EXECUTING |
Correction is actively running |
COMPLETE |
Correction completed successfully (terminal) |
FAILED |
Correction failed (terminal) |
from cleveragents.domain.models.core.correction import (
CorrectionAttemptState,
CORRECTION_ATTEMPT_VALID_TRANSITIONS,
CORRECTION_ATTEMPT_TERMINAL_STATES,
validate_correction_state_transition,
)
# Check valid transitions
CORRECTION_ATTEMPT_VALID_TRANSITIONS[CorrectionAttemptState.PENDING]
# → frozenset({CorrectionAttemptState.EXECUTING})
# Check terminal states
CorrectionAttemptState.COMPLETE in CORRECTION_ATTEMPT_TERMINAL_STATES
# → True
# Validate a transition (raises ValueError on invalid)
validate_correction_state_transition(
CorrectionAttemptState.PENDING,
CorrectionAttemptState.EXECUTING,
)
Cross-Plan Cascade
When a correction targets a decision that spawned child plans, the cascade mechanism determines what to do with each child plan.
ChildPlanState
Observed state of a child plan for cascade classification:
| State | Meaning |
|---|---|
NOT_STARTED |
Child plan exists but has not begun executing |
IN_PROGRESS |
Child plan is currently executing |
COMPLETED_UNAPPLIED |
Child plan finished but changes not yet applied |
APPLIED |
Child plan's changes have been applied (blocks revert) |
CascadeAction
from cleveragents.domain.models.core.correction import CascadeAction, ChildPlanState
action = CascadeAction(
child_plan_id="01EFG7...",
child_plan_state=ChildPlanState.IN_PROGRESS,
action="cancel_and_rollback",
sandbox_rolled_back=True,
)
action value |
Meaning |
|---|---|
cancel |
Cancel the child plan (no sandbox rollback needed) |
cancel_and_rollback |
Cancel and roll back the child plan's sandbox |
reject |
Cannot cascade — child plan is already applied |
CorrectionRejection
Returned when a correction cannot proceed because applied child plans block it:
from cleveragents.domain.models.core.correction import CorrectionRejection
rejection = CorrectionRejection(
correction_id="01ARZ3...",
reason="Child plan 01EFG7... has already been applied and cannot be reverted",
affected_applied_child_plan_ids=["01EFG7..."],
)
CascadeResult
Outcome of the full cascade operation:
from cleveragents.domain.models.core.correction import CascadeResult
result = CascadeResult(
correction_id="01ARZ3...",
cascade_actions=[...],
rejected=False,
all_cancelled_plan_ids=["01EFG7..."],
all_rolled_back_plan_ids=["01EFG7..."],
)
Using the Correction Service
The CorrectionService (in cleveragents.application.services) orchestrates
the full correction workflow. Obtain it via the DI container:
service = container.correction_service()
# Dry run — compute impact without executing
report = service.generate_dry_run_report(request)
print(f"Risk: {report.impact.risk_level}")
print(f"Affected decisions: {report.impact.affected_decisions}")
# Execute a revert correction
result = service.execute_revert(request)
if result.status == CorrectionStatus.APPLIED:
print(f"Reverted {len(result.reverted_decisions)} decisions")
print(f"Phase transition target: {result.phase_transition_target}")
# Execute an append correction
result = service.execute_append(request)
if result.spawned_child_plan_id:
print(f"Spawned child plan: {result.spawned_child_plan_id}")
CLI Usage
# Revert to a prior decision point
agents plan correct --plan <PLAN_ID> --decision <DECISION_ID> \
--mode revert \
--guidance "Use src/api/ not src/app/"
# Dry run first to see impact
agents plan correct --plan <PLAN_ID> --decision <DECISION_ID> \
--mode revert --dry-run
# Append new guidance without reverting
agents plan correct --plan <PLAN_ID> --decision <DECISION_ID> \
--mode append \
--guidance "Also add error handling for the 404 case"
Database Schema
The correction_attempts table stores CorrectionAttemptRecord instances:
CREATE TABLE correction_attempts (
correction_attempt_id TEXT PRIMARY KEY,
plan_id TEXT NOT NULL REFERENCES v3_plans(plan_id) ON DELETE CASCADE,
original_decision_id TEXT NOT NULL REFERENCES decisions(decision_id) ON DELETE RESTRICT,
new_decision_id TEXT REFERENCES decisions(decision_id) ON DELETE RESTRICT,
mode TEXT NOT NULL,
guidance TEXT NOT NULL,
archived_artifacts_path TEXT,
state TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
completed_at TEXT
);
FK semantics:
plan_id→CASCADE(correction attempts are deleted with the plan)original_decision_id→RESTRICT(preserves correction audit trail when decisions are cleaned up)new_decision_id→RESTRICT(consistent withoriginal_decision_id)
Testing
The correction module is covered by 53 BDD scenarios in
features/correction_attempts.feature and 5 Robot Framework integration
tests in robot/correction_attempts.robot.
from cleveragents.domain.models.core.correction import (
CorrectionAttemptRecord,
CorrectionAttemptState,
CorrectionMode,
validate_correction_state_transition,
)
# Create a record
record = CorrectionAttemptRecord(
plan_id="plan-001",
original_decision_id="dec-001",
mode=CorrectionMode.REVERT,
guidance="Fix the path",
)
assert record.state == CorrectionAttemptState.PENDING
# Validate transition
validate_correction_state_transition(
CorrectionAttemptState.PENDING,
CorrectionAttemptState.EXECUTING,
) # OK
validate_correction_state_transition(
CorrectionAttemptState.COMPLETE,
CorrectionAttemptState.EXECUTING,
) # raises ValueError — terminal state
Related Documentation
- ADR-007 Decision Tree & Correction
- ADR-035 Decision Tree Rollback & Replay
- Architecture — Plan Lifecycle
- Sandbox Module — checkpoint/rollback used during revert