d70a5d84ae
ISSUES CLOSED: #186
302 lines
11 KiB
Markdown
302 lines
11 KiB
Markdown
# Plan Execute: Strategize & Execute Integration
|
|
|
|
## Overview
|
|
|
|
The plan executor connects the `PlanLifecycleService` to stub actors that
|
|
drive plans through the **Strategize** and **Execute** phases. In M1, these
|
|
actors are local-only stubs (no LLM calls); future milestones will integrate
|
|
real AI providers.
|
|
|
|
When a `PlanExecutionContext` is provided, the execute phase delegates to
|
|
`RuntimeExecuteActor` for full tool-calling runtime integration with
|
|
changeset capture through `ChangeSetStore`.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
PlanExecutor
|
|
├── StrategizeStubActor (read-only, produces decision tree)
|
|
├── ExecuteStubActor (legacy stub: sandbox + ChangeSetCapture)
|
|
├── RuntimeExecuteActor (runtime: ToolRunner + ChangeSetStore)
|
|
├── PlanExecutionContext (plan metadata + resource bindings)
|
|
├── PlanLifecycleService (phase transitions, persistence)
|
|
└── ErrorRecoveryService (optional — error recording + retry logic)
|
|
```
|
|
|
|
## Execution Modes
|
|
|
|
| Mode | Actor | Trigger | Output |
|
|
|---------|---------------------|-----------------------------------|------------------------|
|
|
| Stub | ExecuteStubActor | No `execution_context` | `ExecuteResult` |
|
|
| Runtime | RuntimeExecuteActor | `execution_context` is provided | `RuntimeExecuteResult` |
|
|
|
|
## PlanExecutionContext
|
|
|
|
The `PlanExecutionContext` bridges plan metadata into the tool runtime:
|
|
|
|
```python
|
|
from cleveragents.application.services.plan_execution_context import (
|
|
PlanExecutionContext,
|
|
)
|
|
from cleveragents.domain.models.core.change import InMemoryChangeSetStore
|
|
|
|
ctx = PlanExecutionContext(
|
|
plan_id="01HGZ...",
|
|
decision_root_id="01HGZ...",
|
|
sandbox_root="/tmp/sandbox",
|
|
automation_profile="trusted",
|
|
project_resources={"repo": {"path": "/code"}},
|
|
changeset_store=InMemoryChangeSetStore(),
|
|
)
|
|
|
|
# Start a changeset for tracking mutations
|
|
changeset_id = ctx.start_changeset()
|
|
|
|
# Record changes during execution
|
|
ctx.record_change(entry)
|
|
|
|
# Retrieve changeset
|
|
cs = ctx.get_changeset(changeset_id)
|
|
|
|
# Summarize context state
|
|
summary = ctx.summarize()
|
|
```
|
|
|
|
### Fields
|
|
|
|
| Field | Type | Required | Description |
|
|
|---------------------|---------------------------|----------|------------------------------------|
|
|
| `plan_id` | `str` | Yes | ULID of the plan |
|
|
| `decision_root_id` | `str \| None` | No | Root decision from strategize |
|
|
| `sandbox_root` | `str \| None` | No | Sandbox filesystem path |
|
|
| `automation_profile` | `str \| None` | No | Automation profile name |
|
|
| `project_resources` | `dict[str, Any]` | No | Project resource metadata |
|
|
| `resource_bindings` | `dict[str, BoundResource]`| No | Resolved resource bindings |
|
|
| `changeset_store` | `ChangeSetStore` | No | Defaults to InMemoryChangeSetStore |
|
|
|
|
## RuntimeExecuteActor
|
|
|
|
Wraps `ToolRunner` to execute strategy decisions with changeset capture:
|
|
|
|
```python
|
|
from cleveragents.application.services.plan_execution_context import (
|
|
RuntimeExecuteActor,
|
|
RuntimeExecuteResult,
|
|
)
|
|
|
|
actor = RuntimeExecuteActor(
|
|
tool_runner=runner,
|
|
execution_context=ctx,
|
|
)
|
|
result: RuntimeExecuteResult = actor.execute(decisions)
|
|
```
|
|
|
|
### RuntimeExecuteResult Fields
|
|
|
|
| Field | Type | Description |
|
|
|--------------------------|---------------|----------------------------------|
|
|
| `changeset_id` | `str` | ULID of the produced changeset |
|
|
| `tool_call_count` | `int` | Number of tool calls made |
|
|
| `sandbox_refs` | `list[str]` | Sandbox reference paths |
|
|
| `decision_ids_processed` | `list[str]` | Processed decision node IDs |
|
|
| `execution_duration_ms` | `float` | Wall-clock execution time (ms) |
|
|
|
|
## PlanExecutor Runtime Mode
|
|
|
|
The `PlanExecutor` auto-selects runtime vs stub mode:
|
|
|
|
```python
|
|
from cleveragents.application.services.plan_executor import PlanExecutor
|
|
|
|
# Stub mode (no execution_context)
|
|
executor = PlanExecutor(lifecycle_service=lifecycle, tool_runner=runner)
|
|
assert not executor.has_runtime
|
|
|
|
# Runtime mode (with execution_context)
|
|
executor = PlanExecutor(
|
|
lifecycle_service=lifecycle,
|
|
tool_runner=runner,
|
|
execution_context=ctx,
|
|
)
|
|
assert executor.has_runtime
|
|
assert executor.changeset_store is not None
|
|
|
|
# Execute auto-dispatches to RuntimeExecuteActor
|
|
result = executor.run_execute(plan_id)
|
|
```
|
|
|
|
### Properties
|
|
|
|
| Property | Type | Description |
|
|
|---------------------|-----------------------------|--------------------------------------|
|
|
| `has_runtime` | `bool` | True if execution_context is set |
|
|
| `changeset_store` | `ChangeSetStore \| None` | Store from execution context |
|
|
| `execution_context` | `PlanExecutionContext \| None` | The execution context |
|
|
|
|
## ChangeSetStore Wiring
|
|
|
|
The `ChangeSetStore` protocol defines the interface for changeset persistence:
|
|
|
|
```python
|
|
class ChangeSetStore(Protocol):
|
|
def start(self, plan_id: str) -> str: ...
|
|
def record(self, changeset_id: str, entry: ChangeEntry) -> None: ...
|
|
def get(self, changeset_id: str) -> SpecChangeSet | None: ...
|
|
def get_for_plan(self, plan_id: str) -> list[SpecChangeSet]: ...
|
|
def summarize(self, changeset_id: str) -> dict[str, Any]: ...
|
|
```
|
|
|
|
`InMemoryChangeSetStore` is the default for M1. Database-backed
|
|
implementations will be added in D1 milestone.
|
|
|
|
## Phase Lifecycle
|
|
|
|
| Phase | Actor | Mode | Output |
|
|
|------------|------------------------|-----------|----------------------------------|
|
|
| Strategize | StrategizeStubActor | Read-only | Decision tree, invariant records |
|
|
| Execute | RuntimeExecuteActor | Runtime | ChangeSet via ChangeSetStore |
|
|
| Execute | ExecuteStubActor | Stub | ChangeSet via ChangeSetCapture |
|
|
|
|
## Strategize Phase
|
|
|
|
The strategize phase is **read-only**: it produces a decision tree from the
|
|
action's `definition_of_done` without modifying any resources.
|
|
|
|
### Decision Tree
|
|
|
|
The stub actor parses `definition_of_done` into discrete steps, each
|
|
represented as a `StrategyDecision` node with a ULID identifier:
|
|
|
|
```python
|
|
from cleveragents.application.services.plan_executor import (
|
|
PlanExecutor,
|
|
StrategizeResult,
|
|
)
|
|
|
|
executor = PlanExecutor(
|
|
lifecycle_service=lifecycle,
|
|
tool_runner=runner,
|
|
execution_context=ctx, # optional — enables runtime mode
|
|
error_recovery_service=er_service, # optional — enables retry logic
|
|
)
|
|
result: StrategizeResult = executor.run_strategize(plan_id)
|
|
|
|
# result.decision_root_id -> ULID of root node
|
|
# result.decisions -> list[StrategyDecision]
|
|
# result.invariant_records -> list[dict] (stub enforcement records)
|
|
```
|
|
|
|
### Invariant Propagation
|
|
|
|
Project and action invariants are propagated into the strategize context.
|
|
In M1, enforcement is stubbed (all invariants are accepted). Full
|
|
reconciliation via the Invariant Reconciliation Actor lands in D2.
|
|
|
|
## Execute Phase
|
|
|
|
The execute phase uses sandbox resources with tool calls routed through
|
|
`ToolRunner` and captured by `ChangeSetCapture`.
|
|
|
|
### ChangeSet Capture
|
|
|
|
All tool mutations during execute are recorded in a `ChangeSet`:
|
|
|
|
```python
|
|
result: ExecuteResult = executor.run_execute(plan_id)
|
|
|
|
# result.changeset_id -> ULID of the changeset
|
|
# result.changeset -> ChangeSet with entries
|
|
# result.tool_calls_count -> int
|
|
# result.sandbox_refs -> list[str]
|
|
```
|
|
|
|
### Metadata Persistence
|
|
|
|
After execute completes, the following metadata is persisted on the Plan:
|
|
|
|
- `changeset_id`: The ChangeSet identifier
|
|
- `sandbox_refs`: List of sandbox reference paths
|
|
- `error_details`: Tool call count and sandbox ref count
|
|
|
|
## Phase Guards
|
|
|
|
- **Execute requires Strategize COMPLETE**: The executor validates that the
|
|
plan has completed strategize (has a `decision_root_id`) before allowing
|
|
execute to proceed.
|
|
- **Phase validation**: Both `run_strategize()` and `run_execute()` verify
|
|
the plan is in the correct phase before proceeding.
|
|
|
|
## Error Handling
|
|
|
|
Failures in either phase are captured with full error context:
|
|
|
|
- `error_message`: The exception message string
|
|
- `error_details`: Dict with `exception_type`, `traceback`, `mode`, and
|
|
structured error-recovery metadata (category, retry count, recovery
|
|
hints) when an `ErrorRecoveryService` is attached
|
|
- The plan transitions to `ERRORED` processing state
|
|
|
|
### Automatic Retry (D1b)
|
|
|
|
When an `ErrorRecoveryService` is provided to the executor, the
|
|
**Execute** phase (stub mode) includes an automatic retry loop:
|
|
|
|
1. On failure the error is classified (transient, validation, etc.)
|
|
and recorded via `ErrorRecoveryService.record_error()`.
|
|
2. If the error is retriable and retry attempts remain, the execute
|
|
actor is re-invoked automatically.
|
|
3. If retries are exhausted or the error is non-retriable, the plan
|
|
transitions to `ERRORED` with full recovery hints stored in
|
|
`error_details`.
|
|
|
|
Retry limits default to 3 and are controlled by the automation
|
|
profile's `auto_retry_transient` threshold.
|
|
|
|
Use `agents plan errors <plan_id>` to inspect error decisions,
|
|
retry history, and recovery suggestions for a plan.
|
|
|
|
See [Error Recovery Reference](error_recovery.md) for full details.
|
|
|
|
### Manual Recovery
|
|
|
|
Plans in `ERRORED` state without automatic retry can be recovered by:
|
|
|
|
1. Reviewing errors via `agents plan errors <plan_id>`
|
|
2. Resetting the plan's processing state back to `QUEUED`
|
|
3. Re-running the failed phase via the executor
|
|
|
|
|
|
## Streaming Hooks
|
|
|
|
Both phases accept an optional `stream_callback`:
|
|
|
|
### Event Types
|
|
|
|
| Event | Phase | Actor | Description |
|
|
|----------------------------|----------|---------|---------------------------------|
|
|
| `strategize_started` | Strat. | Stub | Phase processing began |
|
|
| `strategize_decisions` | Strat. | Stub | Decisions produced |
|
|
| `strategize_complete` | Strat. | Stub | Phase completed |
|
|
| `execute_started` | Execute | Stub | Stub execute began |
|
|
| `execute_step` | Execute | Stub | Stub decision step |
|
|
| `execute_complete` | Execute | Stub | Stub execute completed |
|
|
| `runtime_execute_started` | Execute | Runtime | Runtime execute began |
|
|
| `runtime_execute_step` | Execute | Runtime | Runtime decision step |
|
|
| `runtime_execute_complete` | Execute | Runtime | Runtime execute completed |
|
|
|
|
## Module Reference
|
|
|
|
- **`cleveragents.application.services.plan_execution_context`**
|
|
- `PlanExecutionContext`: Execution context bridging plan to runtime
|
|
- `RuntimeExecuteActor`: Tool-calling execute actor
|
|
- `RuntimeExecuteResult`: Runtime execution output model
|
|
- **`cleveragents.application.services.plan_executor`**
|
|
- `PlanExecutor`: Orchestrator with runtime/stub mode selection
|
|
- `StrategizeStubActor`: Local-only strategize actor
|
|
- `ExecuteStubActor`: Local-only execute actor
|
|
- `StrategyDecision`, `StrategizeResult`, `ExecuteResult`: Data models
|
|
- `StreamCallback`: Type alias for streaming callbacks
|
|
- **`cleveragents.application.services.error_recovery_service`**: Error
|
|
recovery integration (optional)
|
|
- See [Error Recovery Reference](error_recovery.md)
|