Files
cleveragents-core/docs/reference/plan_model.md
T

356 lines
16 KiB
Markdown

# Plan Model Reference
> Source: `src/cleveragents/domain/models/core/plan.py`
## Overview
The Plan model is the fundamental unit of orchestration in CleverAgents v3. Plans follow a four-phase lifecycle (Action, Strategize, Execute, Apply) and are instantiated from Action templates via `agents plan use`.
## NamespacedName
All plans, actions, and projects use a `NamespacedName` for scoped identification:
```
[server:][namespace/]<name>
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `server` | `str \| None` | `None` | Optional server qualifier (e.g., `prod`, `dev`) |
| `namespace` | `str` | `"local"` | Namespace scope (e.g., `local`, username, orgname) |
| `name` | `str` | *(required)* | The item name (1-255 chars) |
### Parsing Rules
- `"my-action"` -> `namespace="local"`, `name="my-action"`
- `"local/my-action"` -> `namespace="local"`, `name="my-action"`
- `"myorg/my-action"` -> `namespace="myorg"`, `name="my-action"`
- `"prod:myorg/my-action"` -> `server="prod"`, `namespace="myorg"`, `name="my-action"`
### Validation
- **Namespace**: lowercase alphanumeric with hyphens only. Empty defaults to `"local"`.
- **Name**: alphanumeric with hyphens or underscores only. Stored lowercase.
## PlanIdentity
Every plan has a unique identity with optional hierarchy links:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `plan_id` | `str` | *(required)* | Unique ULID (26-char Crockford base32) |
| `parent_plan_id` | `str \| None` | `None` | Parent plan ULID (set for subplans) |
| `root_plan_id` | `str \| None` | `None` | Root plan ULID (top-most in tree) |
| `attempt` | `int` | `1` | Attempt counter (increments on re-run, >= 1) |
## Lifecycle Phases
Plans progress through four phases:
| Phase | Description | Valid Next |
|-------|-------------|-----------|
| **ACTION** | Template stage — defines the work to be done. No significant processing occurs. When used (via `agents plan use`), spawns a new plan entity in Strategize. The original action remains available by default; single-use actions are deleted after first use. | STRATEGIZE |
| **STRATEGIZE** | LLM generates a strategy / decision tree | EXECUTE |
| **EXECUTE** | Tool-based execution in sandboxed environment. May create additional decisions constrained by Strategize-phase decisions. | APPLY, or revert to STRATEGIZE |
| **APPLY** | Final phase. Merges changes to the target project(s). Terminal states: `applied` (success), `constrained` (cannot complete within strategy constraints — may revert to STRATEGIZE), `errored`, `cancelled`. | *(terminal — see Apply Terminal States)* |
Action is formally a phase in the lifecycle but is unique in that it functions primarily as a reusable template from which plans are instantiated. Actions are also managed as a separate domain model (`Action`) since they can exist independently of any plan.
### Phase Transitions
- `get_next_phase()` returns the next phase in order, or `None` if the plan is in the Apply phase (which is terminal).
- `can_transition_to_next_phase` is `True` only when `processing_state == COMPLETE` and phase is not APPLY.
- The normal progression is forward. However, Execute may revert to Strategize when execution discovers that Strategize-phase constraints are too restrictive to proceed. Apply may also revert to Strategize when it enters the `constrained` state (controlled by the `auto_reversion_from_apply` automation profile flag). Attempting to skip phases (e.g., STRATEGIZE -> APPLY) raises an error.
## Processing States
Within each phase, a plan has a processing state:
| State | Meaning | Applicable Phases |
|-------|---------|-------------------|
| `QUEUED` | Waiting for compute / worker (default) | Strategize, Execute, Apply |
| `PROCESSING` | Currently running | Strategize, Execute, Apply |
| `ERRORED` | Failed; includes error metadata | Strategize, Execute, Apply |
| `COMPLETE` | Finished successfully | Strategize, Execute |
| `APPLIED` | Terminal — changes successfully committed | Apply only |
| `CONSTRAINED` | Terminal — cannot complete within strategy constraints; may trigger reversion to Strategize | Apply only |
| `CANCELLED` | User/system cancelled (terminal for any phase) | All phases |
- `processing_state` always has a value (defaults to `QUEUED`).
- The Apply phase uses `APPLIED` and `CONSTRAINED` as terminal states instead of `COMPLETE`.
### Phase/State Consistency Rules
The `validate_phase_state_consistency` model validator enforces:
1. **Apply terminal states** — the Apply phase uses `APPLIED` (success) and `CONSTRAINED` (cannot proceed within constraints) as terminal states. `COMPLETE` is not a valid terminal state for the Apply phase.
2. **CANCELLED is terminal** — a plan with `processing_state=CANCELLED` in any phase is considered terminal.
3. **CONSTRAINED triggers reversion** — when the Apply phase reaches the `CONSTRAINED` state, reversion to Strategize may be triggered automatically (controlled by the `auto_reversion_from_apply` automation profile flag) or require manual user approval.
## Computed Properties
| Property | Type | Description |
|----------|------|-------------|
| `state` | `ProcessingState` | Alias for `processing_state` |
| `is_terminal` | `bool` | `True` if state is APPLIED, CONSTRAINED, or CANCELLED |
| `is_errored` | `bool` | `True` if `processing_state == ERRORED` |
| `can_transition_to_next_phase` | `bool` | `True` if state is COMPLETE and phase is not APPLY |
| `is_subplan` | `bool` | `True` if `parent_plan_id` is set |
| `is_root_plan` | `bool` | `True` if no `root_plan_id` or root == self |
| `depth` | `int` | `0` for root, `-1` (placeholder) for non-root |
| `has_subplans` | `bool` | `True` if `subplan_statuses` is non-empty |
## Action Linkage
Every plan carries an `action_name` field (required, string) that references the namespaced name of the source Action template. This replaces the old `action_id` pattern.
```
action_name: "local/code-coverage"
```
## Project Links
Plans reference projects through `project_links: list[ProjectLink]` instead of a flat `project_ids` list. Each link carries:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `project_name` | `str` | *(required)* | Namespaced project name |
| `alias` | `str \| None` | `None` | Optional alias for disambiguation |
| `read_only` | `bool` | `False` | Whether the project is read-only in this plan |
### Alias Validation
Aliases must match the pattern `^[a-z][a-z0-9_-]{0,63}$`:
- Lowercase, starts with a letter
- Alphanumeric, hyphens, underscores only
- Maximum 64 characters
Aliases must be unique within a plan. The `validate_project_link_alias_uniqueness` model validator raises `ValueError` on duplicates.
## Automation Profile
Plans can carry a resolved automation profile with provenance tracking:
```python
automation_profile: AutomationProfileRef | None
```
The `AutomationProfileRef` contains:
| Field | Type | Description |
|-------|------|-------------|
| `profile_name` | `str` | The namespaced profile name (e.g., `"trusted"`) |
| `provenance` | `AutomationProfileProvenance` | Where the profile was resolved from |
`AutomationProfileProvenance` values: `plan`, `action`, `project`, `global`.
Resolution follows plan > action > project > global precedence. Once set at `plan use` time, the profile is locked to the plan.
### Automation Level
| Level | Description |
|-------|-------------|
| `MANUAL` | User must explicitly trigger each phase transition (default) |
| `REVIEW_BEFORE_APPLY` | Auto strategize + execute, pause before apply |
| `FULL_AUTOMATION` | All phases run automatically without human input |
## Invariants
Plans carry invariant constraints with source provenance:
```python
invariants: list[PlanInvariant]
```
Each `PlanInvariant` has:
| Field | Type | Description |
|-------|------|-------------|
| `text` | `str` | The constraint text (natural language, min 1 char) |
| `source` | `InvariantSource` | Where it originated |
`InvariantSource` values: `plan`, `action`, `project`, `global`.
Precedence is plan > action > project > global.
## Arguments
Plans store resolved argument values and their display order:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `arguments` | `dict[str, Any]` | `{}` | Resolved values (name -> value) |
| `arguments_order` | `list[str]` | `[]` | Stable ordering for CLI rendering |
## Actor Fields
Plans reference actors for each phase:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `strategy_actor` | `str \| None` | `None` | Actor for Strategize phase |
| `execution_actor` | `str \| None` | `None` | Actor for Execute phase |
| `review_actor` | `str \| None` | `None` | Optional actor for code review/QA |
| `apply_actor` | `str \| None` | `None` | Optional actor for release/merge |
| `estimation_actor` | `str \| None` | `None` | Optional actor for cost/risk estimation |
| `invariant_actor` | `str \| None` | `None` | Optional actor for invariant reconciliation |
## PlanTimestamps
Lifecycle timestamps are tracked in a dedicated `PlanTimestamps` model:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `created_at` | `datetime` | `now()` | When the plan was created |
| `updated_at` | `datetime` | `now()` | Last modification time |
| `strategize_started_at` | `datetime \| None` | `None` | When strategize phase began |
| `strategize_completed_at` | `datetime \| None` | `None` | When strategize phase completed |
| `execute_started_at` | `datetime \| None` | `None` | When execute phase began |
| `execute_completed_at` | `datetime \| None` | `None` | When execute phase completed |
| `apply_started_at` | `datetime \| None` | `None` | When apply phase began |
| `applied_at` | `datetime \| None` | `None` | When the plan reached terminal APPLIED state |
## Description and Definition of Done
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `description` | `str` | *(required)* | Rendered prompt/description defining what the plan does (min 1 char) |
| `definition_of_done` | `str \| None` | `None` | Rendered, testable completion criteria |
Both are rendered from the action template at plan creation time and stored as plain strings.
## Error Tracking
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `error_message` | `str \| None` | `None` | Error message if errored |
| `error_details` | `dict[str, str] \| None` | `None` | Additional error context (key-value pairs) |
## Execution Placeholders
These fields are populated during the plan lifecycle:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `changeset_id` | `str \| None` | `None` | ChangeSet ID from Execute phase |
| `sandbox_refs` | `list[str]` | `[]` | Active sandbox reference IDs |
| `validation_summary` | `dict[str, Any] \| None` | `None` | Validation results before Apply |
| `decision_root_id` | `str \| None` | `None` | Root decision node ULID |
## Metadata
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `created_by` | `str \| None` | `None` | User/session that created the plan |
| `tags` | `list[str]` | `[]` | Tags for organization and filtering |
| `reusable` | `bool` | `True` | Whether the source action remains available after use |
| `read_only` | `bool` | `False` | Whether plan only performs read operations |
## CLI Output
`Plan.as_cli_dict()` returns a stable dictionary for CLI rendering in table, JSON, and YAML formats. Fields are ordered consistently:
1. `plan_id`, `name`, `action`, `phase`, `state`, `description`
2. `definition_of_done` (if set)
3. `automation_profile`, `automation_profile_source` (if set)
4. `automation_level`
5. `projects` (if any, with alias/read_only)
6. `invariants` (if any, with source tags)
7. `arguments` (if any, in stable order)
8. `strategy_actor`, `execution_actor` (if set)
9. `error` (if present)
10. `created_at`, `updated_at`
11. `parent_plan_id` (if subplan)
12. `subplan_count` (if has subplans)
## Subplan Hierarchy
Plans support a parent/child hierarchy for decomposition:
- `identity.parent_plan_id` — Links to parent plan
- `identity.root_plan_id` — Links to root of the tree
- `subplan_config` — Configuration for child execution
- `subplan_statuses` — Status tracking for each child
### ExecutionMode
Controls how subplans are scheduled:
| Mode | Description |
|------|-------------|
| `SEQUENTIAL` | One after another, ordered by sequence (default) |
| `PARALLEL` | All at once, up to `max_parallel` |
### SubplanMergeStrategy
How to merge results from parallel subplans:
| Strategy | Description |
|----------|-------------|
| `GIT_THREE_WAY` | Use git merge-file for code (default) |
| `SEQUENTIAL_APPLY` | Apply in completion order |
| `FAIL_ON_CONFLICT` | Error if any conflicts |
| `LAST_WINS` | Later changes overwrite earlier |
### SubplanConfig
Configuration for subplan execution, set on parent plans:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `execution_mode` | `ExecutionMode` | `SEQUENTIAL` | How to execute subplans |
| `merge_strategy` | `SubplanMergeStrategy` | `GIT_THREE_WAY` | How to merge subplan results |
| `max_parallel` | `int` | `5` | Max concurrent subplans in PARALLEL mode (1-50) |
| `fail_fast` | `bool` | `False` | Stop all subplans on first failure |
| `timeout_per_subplan_seconds` | `int \| None` | `None` | Timeout per subplan in seconds |
| `retry_failed` | `bool` | `True` | Automatically retry failed subplans |
| `max_retries` | `int` | `2` | Max retry attempts per subplan (0-5) |
### SubplanStatus
Stored on the parent plan to track each child:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `subplan_id` | `str` | *(required)* | The subplan's ULID |
| `action_name` | `str` | *(required)* | Namespaced action name used to create the subplan |
| `target_resources` | `list[str]` | `[]` | Resource IDs this subplan operates on |
| `status` | `ProcessingState` | `QUEUED` | Current processing state of the subplan |
| `started_at` | `datetime \| None` | `None` | When execution started |
| `completed_at` | `datetime \| None` | `None` | When execution completed |
| `error` | `str \| None` | `None` | Error message if subplan failed |
| `changeset_summary` | `str \| None` | `None` | Brief summary of changes produced |
| `files_changed` | `int` | `0` | Number of files modified (>= 0) |
| `attempt_number` | `int` | `1` | Current attempt number (>= 1) |
| `previous_attempts` | `list[SubplanAttempt]` | `[]` | History of prior execution attempts |
### SubplanAttempt
Record of a subplan execution attempt for post-mortem analysis:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `attempt_number` | `int` | *(required)* | 1-based attempt counter |
| `started_at` | `datetime` | *(required)* | When this attempt started |
| `completed_at` | `datetime \| None` | `None` | When this attempt ended |
| `error` | `str \| None` | `None` | Error message if attempt failed |
| `was_retried` | `bool` | `False` | Whether a subsequent attempt was made |
### Computed Properties
| Property | Description |
|----------|-------------|
| `is_subplan` | `True` if `parent_plan_id` is set |
| `is_root_plan` | `True` if no root or root == self |
| `depth` | `0` for root, `-1` (placeholder) for non-root |
| `has_subplans` | `True` if `subplan_statuses` is non-empty |
### Failure Handling
`SubplanFailureHandler` makes stateless decisions about:
- **should_stop_others**: Halt remaining subplans? (Yes if `fail_fast` or sequential mode)
- **should_retry**: Retry the failed subplan? (Yes for retriable errors within retry limits)
Retriable errors: `ValidationError`, `TimeoutError`, `TemporaryResourceError`, `MergeConflictError`
Non-retriable errors: `ConfigurationError`, `AuthenticationError`, `MissingResourceError`, `CircularDependencyError`
Unknown errors are not retried (conservative default).