44652c216a
Document lifecycle details, security warnings, and CLI usage for the new module guides so implementors have a single reference. ISSUES CLOSED: #4848
456 lines
15 KiB
Markdown
456 lines
15 KiB
Markdown
# 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](../adr/ADR-007-decision-tree-and-correction.md) (Decision Tree & Correction),
|
|
[ADR-035](../adr/ADR-035-decision-tree-rollback-and-replay.md) (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`
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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) |
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```sql
|
|
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 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`.
|
|
|
|
```python
|
|
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/ADR-007-decision-tree-and-correction.md)
|
|
- [ADR-035 Decision Tree Rollback & Replay](../adr/ADR-035-decision-tree-rollback-and-replay.md)
|
|
- [Architecture — Plan Lifecycle](../architecture.md#plan-lifecycle)
|
|
- [Sandbox Module](sandbox.md) — checkpoint/rollback used during revert
|