# Subplans: Execution and Merge Subplans allow a parent plan to decompose work into coordinated child plans. Each child plan runs in its own sandbox, and results are merged back into the parent plan using a configurable merge strategy. ## Execution Modes The `SubplanConfig.execution_mode` field controls how child plans are scheduled: | Mode | Description | |-----------------------|--------------------------------------------------| | `sequential` | Execute one at a time in order | | `parallel` | Execute concurrently (up to `max_parallel`) | | `dependency_ordered` | Respect DAG dependencies via topological sort | ### Sequential Mode Subplans execute one at a time in the order they appear in `subplan_statuses`. If a subplan fails and `fail_fast` is enabled, remaining subplans are cancelled. ### Parallel Mode Subplans execute concurrently using a thread pool. The `max_parallel` setting (default: 5, range: 1–50) caps the number of concurrent workers. When `fail_fast` is enabled, remaining subplans are cancelled on first failure. Cancelled futures are reported with `CANCELLED` status (not `ERRORED`). Results are returned in completion order, which affects ordering-sensitive merge strategies such as `sequential_apply`. ### Dependency-Ordered Mode Subplans are sorted topologically based on a dependency graph. Independent subplans (those whose dependencies are all satisfied) within the same topological wave are executed concurrently, respecting the `max_parallel` limit. Subsequent waves wait for the previous wave to finish. A circular dependency raises a `ValueError`. The dependency graph maps each `subplan_id` to the list of `subplan_id`s it depends on. ## Merge Strategies After subplans complete, their sandbox outputs are merged using the configured `SubplanMergeStrategy`: | Strategy | Description | |-----------------------|--------------------------------------------------| | `git_three_way` | Three-way merge via `git merge-file` | | `sequential_apply` | Apply changes in completion order | | `fail_on_conflict` | Raise `MergeConflictError` on any conflict | | `last_wins` | Final subplan's output overwrites earlier ones | ### Git Three-Way Merge Uses `git merge-file` to perform content-level merging. Non-overlapping changes from different subplans are combined automatically. Overlapping changes produce conflict markers in the output. ### Sequential Apply Each subplan's output is applied in the order subplans completed, with each becoming the new base for the next. In sequential mode, this matches the input order. In parallel mode, outputs are ordered by actual completion time, so the subplan that finishes last has its changes applied last. Equivalent to replaying edits sequentially. ### Fail on Conflict Same as git three-way merge, but raises `MergeConflictError` if any file has unresolved conflicts. Use this when conflicts must be resolved manually. ### Last Wins The final subplan's version of each file overwrites all earlier versions unconditionally. Simple but may discard concurrent work. ## Merge Outcomes The `SubplanMergeResult` reports: | Field | Description | |-------------------|------------------------------------------------------| | `success` | `True` if all files merged without conflicts | | `merged_files` | Per-file outcomes with content and conflict status | | `conflict_files` | Paths of files with unresolved conflicts | | `total_files` | Total unique files across all subplan outputs | Each `FileMergeOutcome` includes: | Field | Description | |----------------------|---------------------------------------------------| | `path` | Relative file path | | `content` | Merged content (may contain conflict markers) | | `has_conflict` | Whether this file has unresolved conflicts | | `source_subplan_ids` | IDs of subplans that modified this file | ## Timeout Enforcement When `timeout_per_subplan_seconds` is set on `SubplanConfig`, each subplan execution is subject to a wall-clock timeout. If a subplan (including any retries) does not complete within the configured number of seconds, it is marked `ERRORED` with a `SubplanTimeoutError` message. Timeouts are enforced in all execution modes (sequential, parallel, and dependency-ordered). The timeout applies per-subplan, not per-attempt. The entire retry loop for a subplan must complete within the deadline. If the timeout expires during a retry, no further retries are attempted and the subplan is marked as errored immediately. ## Failure Handling The `SubplanFailureHandler` determines retry and stop behavior: - **`fail_fast`**: Stop all subplans on first failure (any mode). - **Sequential mode**: Always stops on failure (even without `fail_fast`). - **Parallel mode**: Other subplans continue unless `fail_fast` is set. - **Retry**: Retriable errors (`TimeoutError`, `ValidationError`, `TemporaryResourceError`, `MergeConflictError`) are retried up to `max_retries` times. - **Non-retriable**: `ConfigurationError`, `AuthenticationError`, `MissingResourceError`, `CircularDependencyError` are never retried. ## Configuration Reference ```yaml subplan_config: execution_mode: parallel # sequential | parallel | dependency_ordered merge_strategy: git_three_way # git_three_way | sequential_apply | fail_on_conflict | last_wins max_parallel: 5 # 1-50, for parallel mode fail_fast: false # stop all on first failure timeout_per_subplan_seconds: ~ # optional per-subplan timeout retry_failed: true # auto-retry failed subplans max_retries: 2 # 0-5, max retry attempts ``` ## Service API ### SubplanExecutionService ```python from cleveragents.application.services import ( SubplanExecutionService, SubplanExecutionOutput, ) service = SubplanExecutionService( config=plan.subplan_config, executor_fn=my_executor, # Callable[[SubplanStatus], SubplanExecutionOutput] ) result = service.execute_all( subplan_statuses=plan.subplan_statuses, base_files={"src/main.py": original_content}, dependency_graph={"sub-2": ["sub-1"]}, # for dependency_ordered mode ) ``` ### SubplanMergeService ```python from cleveragents.application.services import SubplanMergeService merge_service = SubplanMergeService(strategy=SubplanMergeStrategy.GIT_THREE_WAY) result = merge_service.merge( base_files={"src/main.py": "original content"}, subplan_outputs=[ ("sub-1", {"src/main.py": "modified by sub-1"}), ("sub-2", {"src/main.py": "modified by sub-2"}), ], ) ``` ## Design Decisions and Specification Alignment This section documents intentional deviations from the specification (`docs/specification.md`) and the rationale behind them. ### Execution mode via configuration vs. decision tree The specification describes execution mode as emerging from the decision tree (`subplan_spawn` vs `subplan_parallel_spawn` decision types). This implementation uses a configurable `ExecutionMode` enum on `SubplanConfig` instead, which simplifies the service interface and allows the execution mode to be set declaratively without coupling to decision-tree internals. ### Single merge strategy per plan vs. per-resource-type The specification describes merge strategies as resource-type-dependent (e.g., git three-way for git resources, sequential for databases). This implementation uses a single `SubplanMergeStrategy` per `SubplanConfig`, applied uniformly to all files. A per-resource-type merge dispatch can be layered on top by callers that select the strategy based on the resource type before constructing the service. ### Global concurrency and depth limits The specification defines `plan.concurrency` (default 4) and `plan.max-child-depth` (default 5) as global limits. These are not enforced within this service because they are cross-cutting concerns that belong at the orchestration layer above `SubplanExecutionService`. The `max_parallel` setting on `SubplanConfig` controls per-plan concurrency locally.