ddc6fb7372
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 49s
CI / quality (pull_request) Successful in 45s
CI / security (pull_request) Successful in 56s
CI / helm (pull_request) Successful in 26s
CI / build (pull_request) Successful in 32s
CI / unit_tests (pull_request) Failing after 7m5s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Failing after 11m14s
CI / integration_tests (pull_request) Failing after 23m7s
CI / coverage (pull_request) Successful in 11m9s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m59s
- docs/reference/session_cli.md: add Rich output panels documentation for agents session export (Session Export, Contents, Integrity panels with example output); update success message to 'Export completed'; add stdout export note and example (#3424) - docs/reference/plan_execute.md: add Subplan Spawning section documenting SubplanService and SubplanExecutionService injection into PlanExecutor, execution flow (_spawn_subplans, _execute_subplans, _apply_subplan_results_to_plan), failure handling, and updated architecture diagram (#3561)
412 lines
16 KiB
Markdown
412 lines
16 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)
|
|
├── SubplanService (optional — spawn child plans from decisions)
|
|
├── SubplanExecutionService (optional — execute spawned child plans)
|
|
└── ErrorRecoveryService (optional — error recording + retry logic)
|
|
```
|
|
|
|
## Subplan Spawning (v3.8.0+)
|
|
|
|
When a plan's Strategize phase records `subplan_spawn` or
|
|
`subplan_parallel_spawn` decisions, the Execute phase automatically
|
|
spawns and executes child plans via the injected `SubplanService` and
|
|
`SubplanExecutionService`.
|
|
|
|
### Wiring
|
|
|
|
Both services are injected as optional constructor parameters:
|
|
|
|
```python
|
|
from cleveragents.application.services.plan_executor import PlanExecutor
|
|
from cleveragents.application.services.subplan_service import SubplanService
|
|
from cleveragents.application.services.subplan_execution_service import (
|
|
SubplanExecutionService,
|
|
)
|
|
|
|
executor = PlanExecutor(
|
|
lifecycle_service=lifecycle,
|
|
tool_runner=runner,
|
|
execution_context=ctx,
|
|
subplan_service=subplan_svc, # optional
|
|
subplan_execution_service=exec_svc, # optional
|
|
)
|
|
```
|
|
|
|
When either service is `None`, all subplan spawning logic is skipped
|
|
(backward-compatible no-op).
|
|
|
|
### Execution Flow
|
|
|
|
After the actor completes the Execute phase:
|
|
|
|
1. **`_spawn_subplans()`** — queries `SubplanService.get_spawn_decisions()`
|
|
for the plan, then calls `SubplanService.spawn()` for each decision,
|
|
creating child plan records.
|
|
2. **`_execute_subplans()`** — calls `SubplanExecutionService.execute_all()`,
|
|
routing sequential `subplan_spawn` decisions through ordered execution
|
|
and `subplan_parallel_spawn` groups through concurrent execution.
|
|
3. **`_apply_subplan_results_to_plan()`** — propagates child plan failure
|
|
information back to the parent plan. When one or more child plans fail,
|
|
the parent plan's `error_details` field is annotated with a
|
|
`failed_subplan_ids` list.
|
|
|
|
### Properties
|
|
|
|
| Property | Type | Description |
|
|
|----------|------|-------------|
|
|
| `subplan_service` | `SubplanService \| None` | Injected subplan service |
|
|
| `subplan_execution_service` | `SubplanExecutionService \| None` | Injected execution service |
|
|
|
|
### Failure Handling
|
|
|
|
Child plan failures are **annotated, not raised**. The parent plan
|
|
continues to the Apply phase even if child plans fail; the
|
|
`failed_subplan_ids` list in `error_details` allows downstream
|
|
consumers to detect and handle failures.
|
|
|
|
See [`subplan_service.md`](subplan_service.md) and
|
|
[`subplans.md`](subplans.md) for full subplan API reference.
|
|
|
|
## 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)
|
|
```
|
|
|
|
## CLI Executor Wiring (`_get_plan_executor`)
|
|
|
|
The `plan execute` CLI command constructs a `PlanExecutor` via the
|
|
internal `_get_plan_executor()` helper in
|
|
`cleveragents.cli.commands.plan`. This helper resolves the
|
|
`ProviderRegistry` from the DI container and builds
|
|
`LLMStrategizeActor` / `LLMExecuteActor` for real LLM calls.
|
|
|
|
```python
|
|
def _get_plan_executor(lifecycle_service: PlanLifecycleService | None = None):
|
|
"""Build a PlanExecutor wired with real LLM actors.
|
|
|
|
Args:
|
|
lifecycle_service: Optional pre-existing PlanLifecycleService.
|
|
When provided the executor shares the same service (and its
|
|
in-memory plan cache) as the caller.
|
|
"""
|
|
```
|
|
|
|
### Shared Lifecycle Service Instance
|
|
|
|
Because `PlanLifecycleService` is registered as a **Factory** provider
|
|
in the DI container, each `container.plan_lifecycle_service()` call
|
|
returns a **new instance** with its own in-memory `_plans` cache. The
|
|
`plan execute` handler creates one service for CLI-level state reads
|
|
and passes it to `_get_plan_executor()` so the executor shares the
|
|
same instance:
|
|
|
|
```python
|
|
# In the plan execute CLI handler:
|
|
service = _get_lifecycle_service()
|
|
executor = _get_plan_executor(lifecycle_service=service) # shared instance
|
|
```
|
|
|
|
This is critical because `executor.run_strategize()` mutates plan
|
|
state through the lifecycle service (e.g. advancing from
|
|
`strategize/complete` to `execute/queued` via `auto_progress`). If
|
|
the executor used a separate service instance, the CLI handler's
|
|
subsequent `service.get_plan()` call would return stale cached state,
|
|
causing spurious "not in an executable state" errors.
|
|
|
|
> **Note:** When `lifecycle_service` is omitted (e.g. from test code
|
|
> or standalone scripts), `_get_plan_executor` falls back to creating
|
|
> a fresh instance from the container.
|
|
|
|
### 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 `modify_config` threshold (named per spec § Automatable
|
|
Tasks; controls transient-failure retry gating).
|
|
|
|
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)
|