docs(spec): clarify fail_fast cancel semantics and A2A facade idempotency #7410

Closed
HAL9000 wants to merge 1 commits from spec/arch-failfast-cancel-semantics into master
+38
View File
@@ -18508,6 +18508,14 @@ The `local/plan-tools` skill references this tool (and others) by name:
Parallel execution is bounded by `SubplanConfig.max_parallel` (default: `5`, range: 150). This cap prevents runaway resource consumption when a large number of child plans are spawned simultaneously. The runtime uses a `ThreadPoolExecutor` with `min(max_parallel, len(subplans))` workers. The `SubplanConfig` model also controls `merge_strategy` (default: `git_three_way`), `fail_fast` (default: `false`), `timeout_per_subplan_seconds` (default: `null`), `retry_failed` (default: `true`), and `max_retries` (default: `2`).
**`fail_fast` semantics**: When `fail_fast: true` and any parallel subplan transitions to `errored`, the runtime MUST:
1. Stop dispatching new subplans from the queue (no new futures submitted to the thread pool)
2. Transition all **queued** (not yet started) subplans to `cancelled` state — NOT `complete`
3. Allow already-running subplans to finish naturally (they are not forcibly interrupted)
4. After all running subplans complete, proceed with failure handling for the parent plan
The `cancelled` state is semantically distinct from `complete`: `cancelled` means the subplan was never started due to a sibling failure, while `complete` means the subplan ran to completion. Downstream tooling (plan inspectors, orchestrators, CI systems) relies on this distinction to determine whether work was skipped vs. completed. Transitioning unstarted subplans to `complete` when `fail_fast` triggers is a **spec violation**.
=== "Dependency-Ordered"
When child plans have explicit dependencies on each other, the `DEPENDENCY_ORDERED` execution mode respects those dependencies while maximizing concurrency. The dependency graph is provided as a `dependency_graph: dict[str, list[str]]` mapping each `subplan_id` to the list of `subplan_ids` it depends on.
@@ -43470,6 +43478,36 @@ Where `{entity}` is one of: `actor`, `skill`, `tool`, `validation`, `resource`,
| `_cleveragents/health/check` | Health check handler |
| `_cleveragents/diagnostics/run` | Diagnostic runner |
##### A2A Facade Idempotency Contract
All `_cleveragents/plan/*` extension methods handled by `A2aLocalFacade` MUST be **idempotent** — calling the same method twice with the same parameters must produce the same result without raising an error. This is required because:
1. Network retries and orchestrators may call the same method multiple times
2. The A2A protocol does not guarantee exactly-once delivery
3. CLI commands may be retried by users or automation scripts
**Idempotency implementation pattern**: Before invoking the underlying service method, check the current plan phase. If the plan has already reached or passed the target phase, return the current state without re-invoking:
```python
def _handle_plan_execute(self, params: dict) -> dict:
plan = svc.get_plan(plan_id)
# Idempotency guard: if already at or past execute phase, acknowledge
if plan.phase.value in ("execute", "apply"):
return {"plan_id": plan.identity.plan_id, "status": plan.phase.value}
plan = svc.execute_plan(plan_id)
return {"plan_id": plan.identity.plan_id, "status": plan.phase.value}
def _handle_plan_apply(self, params: dict) -> dict:
plan = svc.get_plan(plan_id)
# Idempotency guard: if already applied, acknowledge without re-applying
if plan.phase.value == "apply":
return {"plan_id": plan.identity.plan_id, "status": plan.phase.value}
plan = svc.apply_plan(plan_id)
return {"plan_id": plan.identity.plan_id, "status": plan.phase.value}
```
Raising `InvalidPhaseTransitionError` to the A2A caller when the plan is already in the target phase is a **spec violation** — it breaks idempotency and causes orchestrators to treat a successful (already-applied) plan as a failure.
##### Streaming Architecture
A2A streaming uses **Server-Sent Events (SSE)** via `message/stream`. The server pushes `TaskStatusUpdateEvent` and `TaskArtifactUpdateEvent` to the client in real-time during long-running operations: