diff --git a/docs/specification.md b/docs/specification.md index f0803345d..84266a378 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -46852,6 +46852,131 @@ This section defines the ordered milestone plan for CleverAgents v3.x, mapping a --- +## Decision Recording System (v3.2.0) + +### Overview + +The Decision Recording System captures every strategic decision made during the Strategize and Execute phases of the Plan Lifecycle. Each decision is persisted to the database with full context snapshots, enabling users to inspect the decision tree, understand alternatives considered, and correct decisions retroactively. + +### Module Boundaries + +- **Module**: `cleveragents.decisions` +- **Layer**: Domain (sits between Application and Infrastructure) +- **Responsibilities**: + - Recording decisions with context snapshots during Strategize phase + - Persisting decision trees to the database + - Providing query interfaces for decision retrieval + - Enforcing decision tree integrity constraints +- **Public Interfaces** (what other modules may call): + - `DecisionRecorder` — records a decision during plan execution + - `DecisionRepository` — CRUD operations for decisions (domain repository interface) + - `DecisionTreeQuery` — read-only queries for tree traversal +- **Forbidden Dependencies**: Must not import from `cleveragents.cli` or `cleveragents.tui` + +### Data Models + +#### Decision Entity + +```python +@dataclass +class Decision: + id: UUID + plan_id: UUID + parent_decision_id: Optional[UUID] # None for root decisions + phase: Literal["strategize", "execute"] + decision_type: str # e.g., "tool_selection", "subtask_decomposition", "resource_allocation" + prompt_snapshot: str # Full prompt sent to LLM at decision point + context_snapshot: dict # Serialized context at decision point + chosen_option: str # The option selected + alternatives_considered: list[AlternativeOption] + rationale: str # LLM-provided reasoning + invariants_applied: list[UUID] # Invariant IDs that constrained this decision + created_at: datetime + metadata: dict # Extensible metadata +``` + +#### AlternativeOption + +```python +@dataclass +class AlternativeOption: + option_text: str + rejection_reason: str +``` + +#### DecisionTree + +```python +@dataclass +class DecisionTree: + plan_id: UUID + root_decisions: list[Decision] + + def get_subtree(self, decision_id: UUID) -> list[Decision]: ... + def get_path_to_root(self, decision_id: UUID) -> list[Decision]: ... + def get_children(self, decision_id: UUID) -> list[Decision]: ... +``` + +### Database Schema + +```sql +CREATE TABLE decisions ( + id UUID PRIMARY KEY, + plan_id UUID NOT NULL REFERENCES plans(id) ON DELETE CASCADE, + parent_decision_id UUID REFERENCES decisions(id), + phase VARCHAR(20) NOT NULL CHECK (phase IN ('strategize', 'execute')), + decision_type VARCHAR(100) NOT NULL, + prompt_snapshot TEXT NOT NULL, + context_snapshot JSONB NOT NULL, + chosen_option TEXT NOT NULL, + alternatives_considered JSONB NOT NULL DEFAULT '[]', + rationale TEXT NOT NULL, + invariants_applied UUID[] NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + metadata JSONB NOT NULL DEFAULT '{}' +); + +CREATE INDEX idx_decisions_plan_id ON decisions(plan_id); +CREATE INDEX idx_decisions_parent_id ON decisions(parent_decision_id); +CREATE INDEX idx_decisions_phase ON decisions(phase); +``` + +### CLI Interface Specification + +#### `agents plan tree ` + +- Renders the decision tree for a plan as a rich tree view +- Shows decision type, chosen option, and timestamp for each node +- Supports `--depth N` to limit tree depth +- Supports `--phase strategize|execute` to filter by phase +- Output format: Rich tree (default) or JSON (`--format json`) + +#### `agents plan explain ` + +- Shows full details for a single decision +- Displays: prompt snapshot, chosen option, all alternatives considered with rejection reasons, rationale, invariants applied +- Output format: Rich panel (default) or JSON (`--format json`) + +### Integration Points + +- **Strategize Phase Hook**: `DecisionRecorder.record()` is called by the LangGraph strategize node after each LLM decision +- **Execute Phase Hook**: `DecisionRecorder.record()` is called by the LangGraph execute node for tool selection decisions +- **Invariant System**: `InvariantEnforcer` passes applied invariant IDs to `DecisionRecorder` +- **Plan Correction Engine**: `PlanCorrectionEngine` uses `DecisionTreeQuery` to find the subtree to recompute + +### Error Handling + +- `DecisionNotFoundError(decision_id)` — raised when a decision ID does not exist +- `DecisionTreeCorruptError(plan_id, reason)` — raised when tree integrity is violated +- All errors are domain exceptions, not infrastructure exceptions + +### Cross-Cutting Concerns + +- **Logging**: All decision recordings logged at DEBUG level with decision ID and plan ID +- **Observability**: Decision count per plan tracked as a metric +- **Performance**: Decision recording must complete within 50ms (non-blocking to plan execution) + + ### v3.3.0 — Corrections + Subplans + Checkpoints **Goal**: Plans can spawn child plans (subplans) during execution. Subplans execute in parallel with configurable concurrency limits. Results are merged back using three-way merge strategies. Checkpointing enables rollback to previous plan states.