Files
HAL9000 44652c216a docs: add sandbox, correction, and invariant module guides
Document lifecycle details, security warnings, and CLI usage for the new module guides so implementors have a single reference.

ISSUES CLOSED: #4848
2026-06-16 18:18:18 -04:00

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, and child_plans_to_rollback were previously duplicated at the top level. They were removed in M7 (issue #1087) to eliminate divergence risk. Access them via report.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_path is 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, and guidance must be non-empty (whitespace stripped)
  • new_decision_id must be non-empty when set
  • created_at and completed_at are 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_idCASCADE (correction attempts are deleted with the plan)
  • original_decision_idRESTRICT (preserves correction audit trail when decisions are cleaned up)
  • new_decision_idRESTRICT (consistent with original_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