docs(v3.7.0): update API docs, README, architecture overview, and changelog
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 31s
CI / helm (pull_request) Successful in 31s
CI / quality (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 1m1s
CI / unit_tests (pull_request) Failing after 1m51s
CI / lint (pull_request) Successful in 3m19s
CI / security (pull_request) Successful in 4m8s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Failing after 1m57s
CI / e2e_tests (pull_request) Failing after 15m16s
CI / integration_tests (pull_request) Failing after 21m24s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-regression (pull_request) Successful in 55m9s

- README: add v3.7.0 highlights (first-run UX, estimation lifecycle,
  enriched domain events, correction attempts, devcontainer handler,
  A2A ValueError mapping); add What's New section; add doc links
- CHANGELOG: merge 'Unreleased (pre-3.7.0)' into v3.7.0 as a
  subsection; clear [Unreleased] block
- docs/reference/architecture_overview.md (new): high-level system
  layers, core abstractions, key services, protocols (A2A/MCP/LSP),
  estimation lifecycle, TUI architecture, server mode, observability
- docs/reference/estimation_lifecycle.md (new): EstimationResult
  model reference, configuration, PLAN_ESTIMATION_COMPLETE event,
  plan.cost_estimate_usd, writing a custom estimation actor
- docs/reference/correction_attempts.md (new): CorrectionAttemptRecord
  schema, state machine, CorrectionAttemptRepository API, DDL,
  CorrectionDryRunReport migration guide (removed redundant fields)
- docs/reference/tui.md: add first-run experience section
  (ActorSelectionOverlay, is_first_run, create_default_persona_for_actor),
  session export/import TUI section, persona export/import TUI section;
  update architecture module table with first_run.py and
  actor_selection_overlay.py entries
- mkdocs.yml: add Architecture Overview to top-level nav

ISSUES CLOSED: #1310 #1087 #1242 #1241 #891 #996 #1001
This commit is contained in:
2026-04-02 18:19:07 +00:00
parent 9bbec0e698
commit 737dda643d
12 changed files with 853 additions and 50 deletions
+3 -1
View File
@@ -5,6 +5,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
<!-- No unreleased changes. -->
## [3.7.0] — 2026-04-02
### Added
@@ -165,7 +167,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `agents action create` now accepts `--format`/`-f` flag, matching all
other action subcommands. (#959)
## Unreleased (pre-3.7.0)
### Pre-release changes (merged into v3.7.0)
- Eliminated redundant fields (`excluded_decisions`, `rollback_tier_depth`,
`child_plans_to_rollback`) from `CorrectionDryRunReport` that duplicated data
+56 -5
View File
@@ -9,14 +9,17 @@ embracing modern Python tooling.
- Unified CLI entry points: `cleveragents` and `agents`
- **Interactive TUI** (`agents tui`) — full-screen Textual app with multi-session tabs,
persona switching, slash commands, reference picker, and context-sensitive F1 help
- **First-run experience** — actor selection overlay on first launch; creates a default
persona automatically so you can start chatting immediately
- **Persona system** — YAML-backed personas bind actors, argument presets, and scope
references to named identities; persisted in `~/.config/cleveragents/personas/`
- **Session management** — create, list, export, and import conversation sessions;
full JSON export/import for portability
full JSON export/import for portability; export/import also available inside the TUI
- **Server mode**`agents server connect` configures a remote CleverAgents server;
Kubernetes Helm chart in `k8s/` for production deployment
- **A2A integration** — Agent-to-Agent protocol facade wires CLI and TUI to live
application services (session, plan, registry, event)
application services (session, plan, registry, event); `ValueError` now maps to
`VALIDATION_ERROR` for correct protocol semantics
- **Permissions screen** — TUI overlay for reviewing tool permission requests with
unified, side-by-side, and context diff views; session-scoped allow/reject decisions
- **Actor thought blocks** — expandable reasoning trace widgets rendered inline in the
@@ -25,8 +28,16 @@ embracing modern Python tooling.
graph persistence for ACMS context strategies
- **Database resource handler** — full CRUD and checkpoint/rollback support for SQLite,
PostgreSQL, MySQL, and DuckDB resources
- **Devcontainer resource handler**`delete()`, `list_children()`, `diff()`, and
`create_sandbox()` now fully implemented
- **Estimation lifecycle**`actor.default.estimation` config key wires an estimation
actor into the Strategize-to-Estimate lifecycle hook
actor into the Strategize-to-Estimate lifecycle hook; emits `PLAN_ESTIMATION_COMPLETE`
domain event; failures are informational and never block Execute
- **Enriched domain events**`PLAN_APPLIED` carries changeset statistics
(files changed, lines added/removed, resources modified, apply duration);
`PLAN_CANCELLED` includes progress context; all events carry `user_identity`
- **Correction attempts**`correction_attempts` table tracks every decision correction
with full lifecycle state machine (`pending → executing → complete/failed`)
- Fast Typer/Click-based interface with parity for help/version behavior
- Behavior-driven coverage via Behave and Robot Framework
- Nox automation for linting, typing, testing, docs, builds, and benchmarks
@@ -62,7 +73,8 @@ pip install -e ".[tui]"
agents tui
```
Inside the TUI:
On first launch, the **Actor Selection Overlay** guides you through picking an actor.
A default persona is created automatically. Inside the TUI:
- Type a message and press `Enter` to chat with the active actor
- Press `/` to open the slash command overlay (67 commands across 14 groups)
@@ -91,6 +103,17 @@ agents server connect --url https://my-server.example.com --token <TOKEN>
agents server status
```
### Estimation lifecycle
Configure an estimation actor to get cost/time/risk forecasts before plan execution:
```bash
agents config set actor.default.estimation anthropic/claude-4-sonnet
agents plan use <PLAN_ID>
agents plan execute # estimation runs automatically before Execute phase
agents plan show <PLAN_ID> # shows cost_estimate_usd and risk level
```
## Developing
Pre-commit hooks run automatically on every `git commit` (formatting, linting, type
@@ -121,13 +144,24 @@ nox -s docs
nox -s serve_docs
```
Key reference documents:
- [Architecture Overview](docs/reference/architecture_overview.md)
- [TUI Reference](docs/reference/tui.md)
- [Plan Lifecycle](docs/reference/plan_execute.md)
- [Estimation Lifecycle](docs/reference/estimation_lifecycle.md)
- [Decision Correction](docs/reference/decision_correction.md)
- [Correction Attempts](docs/reference/correction_attempts.md)
- [Resource Handlers](docs/reference/resource_handlers.md)
- [UKO Runtime](docs/reference/uko_runtime.md)
## Tests
Behave feature scenarios live under `features/` and Robot suites under `robot/`. Use the Nox sessions above to execute them in parity with the implementation plan.
## Observability
LangSmith tracing is optional and off by default. Enable it by exporting `CLEVERAGENTS_LANGSMITH_ENABLED=true` along with a project name and API key (`CLEVERAGENTS_LANGSMITH_PROJECT`, `CLEVERAGENTS_LANGSMITH_API_KEY`). The settings module automatically mirrors these values to `LANGCHAIN_TRACING_V2`, `LANGCHAIN_PROJECT`, and `LANGCHAIN_API_KEY`, so LangChain/LangGraph agents emit traces without extra wiring. Additional knobs such as `CLEVERAGENTS_LANGSMITH_ENDPOINT`, `CLEVERAGENTS_LANGSMITH_USER_ID`, and `CLEVERAGENTS_LANGSMITH_TAGS` are documented in `docs/observability.md`.
LangSmith tracing is optional and off by default. Enable it by exporting `CLEVERAGENTS_LANGSMITH_ENABLED=true` along with a project name and API key (`CLEVERAGENTS_LANGSMITH_PROJECT`, `CLEVERAGENTS_LANGSMITH_API_KEY`). The settings module automatically mirrors these values to `LANGCHAIN_TRACING_V2`, `LANGCHAIN_PROJECT`, and `LANGCHAIN_API_KEY`, so LangChain/LangGraph agents emit traces without extra wiring. Additional knobs such as `CLEVERAGENTS_LANGSMITH_ENDPOINT`, `CLEVERAGENTS_LANGSMITH_USER_ID`, and `CLEVERAGENTS_LANGSMITH_TAGS` are documented in `docs/reference/observability.md`.
## LLM provider configuration
@@ -156,3 +190,20 @@ Set `CLEVERAGENTS_DEFAULT_PROVIDER` to pin the global provider (for example `exp
- Built-in actors (`<provider>/<model>`) are immutable, custom actors must be named `local/<id>`, and the default actor cannot be removed. Use `--unsafe` when adding/updating configs marked unsafe; runtime only warns when invoking unsafe actors.
- `CLEVERAGENTS_TESTING_USE_MOCK_AI=true` forces the in-repo mock provider so Behave/Robot suites never hit external APIs.
- The full capability matrix (streaming, tool calls, JSON mode, etc.) is documented in `docs/reference/providers.md`.
## What's New in v3.7.0
See [CHANGELOG.md](CHANGELOG.md) for the full list. Highlights:
- **TUI** — full Textual-based interactive terminal UI with persona system, 67 slash
commands, reference picker, permissions screen, thought blocks, and first-run experience
- **Session management**`agents session` command group with export/import
- **Server mode**`agents server connect` + Kubernetes Helm chart
- **A2A integration** — local facade wired to live application services
- **Estimation lifecycle** — optional pre-Execute cost/time/risk forecasting
- **Enriched domain events**`PLAN_APPLIED` with changeset stats, `user_identity` on all events
- **Correction attempts** — full lifecycle tracking for decision corrections
- **Resource handlers** — DatabaseResourceHandler and DevcontainerHandler protocol completion
- **UKO runtime** — query interface, inference engine, and graph persistence
- **ACMS** — Phase 2 pipeline protocol aliases
- **Bug fixes** — session DI wiring, plan stale-cache, project context commit, action `--format` flag
+279
View File
@@ -0,0 +1,279 @@
# Architecture Overview
CleverAgents is a Python-first automation platform for orchestrating
complex, long-running tasks with AI actors. This document provides a
high-level map of the system's layers, core abstractions, and the
protocols that connect them. For the authoritative design rationale
behind each decision, refer to the relevant
[Architecture Decision Records](../adr/index.md).
> **Specification is the source of truth.** If this document conflicts
> with `docs/specification.md`, the specification takes precedence.
---
## System Layers
CleverAgents follows a strict layered architecture (ADR-001):
```
┌──────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ CLI (Typer/Click) · TUI (Textual) · Server (ASGI/A2A) │
├──────────────────────────────────────────────────────────────────┤
│ Application Layer │
│ Services · Use-case orchestration · Event bus │
├──────────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ Models · Value objects · Domain events · Invariants │
├──────────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ SQLAlchemy ORM · Alembic migrations · Resource handlers │
│ LangChain/LangGraph · MCP adapter · LSP runtime │
└──────────────────────────────────────────────────────────────────┘
```
Dependencies flow **downward only**. The domain layer has no imports
from infrastructure or application layers.
---
## Core Abstractions
### Plan Lifecycle
The central workflow abstraction. A plan moves through four phases:
```
Action ──► Strategize ──► [Estimate] ──► Execute ──► Apply
```
| Phase | Description |
|-------|-------------|
| **Action** | Defines the goal: what the actor should accomplish |
| **Strategize** | Actor produces a decision tree of steps |
| **Estimate** | Optional: estimation actor forecasts cost/time/risk |
| **Execute** | Steps are executed; decisions recorded |
| **Apply** | Validated changes committed to resources |
Phase transitions are governed by **Automation Profiles** (ADR-017),
which set confidence thresholds for autonomous vs. human-approved
transitions.
See [ADR-006](../adr/ADR-006-plan-lifecycle.md) and
[`plan_execute.md`](plan_execute.md).
### Decision Tree
Every plan produces a **decision tree** — a versioned, append-only
record of every choice made during execution. Decisions can be
corrected, rolled back, and replayed (ADR-007, ADR-033035).
Key decision types: `tool_call`, `resource_selection`, `sub_plan`,
`human_approval`, `correction`.
### Actors
Actors are the AI agents that execute plans. Each actor is a named,
namespaced configuration binding a provider/model to a set of
capabilities. Built-in actors follow `<provider>/<model>` naming;
custom actors use `local/<id>`.
See [ADR-010](../adr/ADR-010-actor-and-agent-architecture.md) and
[`actor_runtime.md`](actor_runtime.md).
### Resources
Resources are the managed objects that plans operate on — files,
databases, containers, devcontainers, LSP servers, and more. The
resource system uses a DAG for dependency tracking and a handler
protocol for CRUD, checkpointing, and sandboxing (ADR-008, ADR-036).
See [`resources.md`](resources.md) and [`resource_handlers.md`](resource_handlers.md).
### Skills
Skills are reusable, versioned capability bundles that extend what
actors can do. They are resolved from local files, Git repositories,
or the AgentSkills.io registry (ADR-012, ADR-028, ADR-030).
### Tools
Tools are the atomic operations actors invoke during execution. The
tool system provides reachability analysis, access projection, and
MCP adapter integration (ADR-011, ADR-029, ADR-037).
---
## Key Services (Application Layer)
| Service | Responsibility |
|---------|---------------|
| `PlanLifecycleService` | Phase transitions, estimation hook, event emission |
| `PlanApplyService` | Validation gate, changeset statistics, `PLAN_APPLIED` event |
| `CorrectionService` | Decision correction, dry-run reports, rollback |
| `SessionService` | Session CRUD, message persistence |
| `PermissionService` | Tool permission requests, session-scoped decisions |
| `AuditEventSubscriber` | SEC7 audit log from domain events |
| `UKOQueryInterface` | ACMS context strategy queries against UKO graph |
| `UKOInferenceEngine` | Implicit triple inference (confidence 0.7) |
| `UKOGraphPersistence` | UKO graph serialisation/restore across restarts |
---
## Estimation Lifecycle (v3.7.0)
When `actor.default.estimation` is configured, the Strategize→Execute
transition invokes an estimation actor before execution begins:
```
Strategize complete
EstimationActor.run(plan)
├── stores EstimationResult on plan
├── populates plan.cost_estimate_usd
└── emits PLAN_ESTIMATION_COMPLETE event
▼ (estimation failure is informational — never blocks Execute)
Execute phase begins
```
See [`estimation_lifecycle.md`](estimation_lifecycle.md).
---
## Protocols and Standards
CleverAgents integrates three industry protocols for interoperability:
| Protocol | Purpose | ADR |
|----------|---------|-----|
| **A2A** (Agent-to-Agent) | CLI/TUI ↔ application service communication | ADR-026, ADR-047 |
| **MCP** (Model Context Protocol) | Tool and resource exposure to LLM actors | ADR-029 |
| **LSP** (Language Server Protocol) | Code intelligence for resource types | ADR-027, ADR-040 |
### A2A Facade
The A2A local facade is the single communication channel between the
presentation layer (CLI, TUI) and the application layer. It maps A2A
protocol messages to service calls and translates domain errors to
A2A error codes:
| Domain Error | A2A Code |
|-------------|----------|
| `NotFoundError` | `NOT_FOUND` |
| `ValidationError` | `VALIDATION_ERROR` |
| `InvalidStateError` | `INVALID_STATE` |
| `PlanError` | `PLAN_ERROR` |
| `ValueError` | `VALIDATION_ERROR` |
---
## Dependency Injection
All services are wired through a DI container (ADR-003). The container
is initialised once at startup (`agents init`) and injected into CLI
commands and TUI handlers. This ensures testability and prevents
hidden global state.
See [`di.md`](di.md).
---
## Persistence
| Store | Technology | Purpose |
|-------|-----------|---------|
| Primary DB | SQLite (default) / PostgreSQL | Plans, decisions, sessions, resources, audit log |
| Migrations | Alembic | Schema versioning |
| Persona config | YAML files (`~/.config/cleveragents/personas/`) | TUI persona definitions |
| TUI state | YAML (`~/.config/cleveragents/tui-state.yaml`) | Last-used persona |
| Checkpoints | Per-handler strategy | Rollback points for resource mutations |
See [ADR-019](../adr/ADR-019-storage-and-persistence.md) and
[`database_schema.md`](database_schema.md).
---
## Sandboxing and Safety
Every resource mutation during Execute is wrapped in a sandbox. The
`SandboxManager` coordinates cross-mechanism sandbox commits atomically
(all-or-nothing, LIFO rollback on partial failure). Automation profiles
control which transitions require human approval.
See [ADR-015](../adr/ADR-015-sandbox-and-checkpoint.md),
[ADR-038](../adr/ADR-038-cross-mechanism-sandbox-coordination.md), and
[`sandbox.md`](sandbox.md).
---
## Interactive Terminal UI (v3.7.0)
The TUI (`agents tui`) is a full-screen Textual application that
communicates with the application layer exclusively through the A2A
local facade — the same protocol used by the CLI.
```
TUI (Textual App)
├── InputModeRouter ─── Normal / Command (/) / Shell (!)
├── SlashCommandOverlay ─── 67 commands, 14 groups
├── ReferencePickerOverlay ─── @reference expansion
├── HelpPanelOverlay (F1) ─── context-sensitive help
├── PermissionsScreen ─── tool permission diff review
├── ActorSelectionOverlay ─── first-run actor picker
└── PersonaBar ─── active persona / preset / scope count
A2A Local Facade
Application Services
```
See [`tui.md`](tui.md) and
[ADR-044](../adr/ADR-044-tui-architecture-and-framework.md).
---
## Server Mode (v3.7.0)
`agents server connect` configures a remote CleverAgents server.
The server exposes the same A2A protocol over HTTP/WebSocket. A
Kubernetes Helm chart (`k8s/`) and `Dockerfile.server` are provided
for production deployment.
See [ADR-023](../adr/ADR-023-server-mode.md),
[ADR-048](../adr/ADR-048-server-application-architecture.md), and
[`server_client_stubs.md`](server_client_stubs.md).
---
## Observability
LangSmith tracing is optional and off by default. Domain events flow
through the event bus to the `AuditEventSubscriber`, which writes
structured audit log entries (SEC7 compliance). All `PLAN_APPLIED`
events carry changeset statistics (files changed, lines added/removed,
resources modified, apply duration).
See [ADR-025](../adr/ADR-025-observability-and-logging.md) and
[`observability.md`](observability.md).
---
## Related Documentation
- [Specification](../specification.md) — authoritative source of truth
- [ADR Index](../adr/index.md) — all architectural decisions
- [Plan Lifecycle](plan_execute.md)
- [Decision Correction](decision_correction.md)
- [Resource System](resources.md)
- [Actor Runtime](actor_runtime.md)
- [TUI Reference](tui.md)
- [ACMS / UKO](acms.md)
- [Dependency Injection](di.md)
- [Database Schema](database_schema.md)
+178
View File
@@ -0,0 +1,178 @@
# Correction Attempts
The `correction_attempts` table records every attempt to correct a
decision in a plan's decision tree. Each attempt tracks the original
decision being corrected, the guidance provided, the new decision
produced, and the lifecycle state of the correction process.
Introduced in v3.7.0 (issue #1087). See also
[`decision_correction.md`](decision_correction.md) for the correction
workflow and [`decision_model.md`](decision_model.md) for the decision
domain model.
**Module**: `cleveragents.infrastructure.database.repositories`
**Domain model**: `cleveragents.domain.models.core.correction`
---
## Overview
When `plan correct` is invoked, the `CorrectionService` creates a
`CorrectionAttemptRecord` to track the correction lifecycle:
```
pending ──► executing ──► complete
└──► failed
```
Terminal states (`complete`, `failed`) are irreversible. Once a
correction attempt reaches a terminal state, `update_state()` rejects
further transitions.
---
## CorrectionAttemptRecord
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | `str` | UUID, required | Unique attempt identifier |
| `plan_id` | `str` | FK → plans, RESTRICT | Plan containing the decision |
| `original_decision_id` | `str` | FK → decisions, RESTRICT | Decision being corrected |
| `new_decision_id` | `str \| None` | FK → decisions, nullable | Replacement decision (set on complete) |
| `guidance` | `str` | max 10 000 chars, non-empty | Human or actor guidance for the correction |
| `mode` | `CorrectionMode` | enum | Correction mode (`append`, `replace`, `rollback`) |
| `state` | `CorrectionAttemptState` | enum | Current lifecycle state |
| `created_at` | `datetime` | UTC, millisecond precision | When the attempt was created |
| `completed_at` | `datetime \| None` | UTC, millisecond precision | When the attempt reached a terminal state |
### CorrectionAttemptState
| Value | Description |
|-------|-------------|
| `pending` | Attempt created, not yet executing |
| `executing` | Correction actor is running |
| `complete` | Correction succeeded; `new_decision_id` is set |
| `failed` | Correction failed; `new_decision_id` is `None` |
### Valid Transitions
| From | To |
|------|----|
| `pending` | `executing` |
| `executing` | `complete` |
| `executing` | `failed` |
All other transitions raise `InvalidCorrectionStateTransitionError`.
---
## CorrectionAttemptRepository
**Module**: `cleveragents.infrastructure.database.repositories`
### Methods
#### `create(attempt: CorrectionAttemptRecord) -> CorrectionAttemptRecord`
Persist a new correction attempt. Raises `ValueError` with a
descriptive message if `plan_id` or `original_decision_id` violates a
foreign-key constraint.
```python
attempt = CorrectionAttemptRecord(
id=str(uuid4()),
plan_id="plan-abc",
original_decision_id="decision-xyz",
guidance="The file path should use forward slashes.",
mode=CorrectionMode.REPLACE,
state=CorrectionAttemptState.PENDING,
created_at=datetime.now(UTC),
)
saved = repo.create(attempt)
```
#### `get(attempt_id: str) -> CorrectionAttemptRecord | None`
Retrieve a correction attempt by ID. Returns `None` if not found.
#### `list_by_plan(plan_id: str) -> list[CorrectionAttemptRecord]`
Return all correction attempts for a plan, ordered by `created_at`
ascending.
#### `update_state(attempt_id, new_state, *, new_decision_id=None, completed_at=None) -> CorrectionAttemptRecord`
Transition the attempt to a new state. Rules:
- Validates the transition against `CORRECTION_ATTEMPT_VALID_TRANSITIONS`.
- Raises `InvalidCorrectionStateTransitionError` for invalid transitions.
- Auto-sets `completed_at` to `datetime.now(UTC)` when transitioning to
a terminal state if `completed_at` is not explicitly provided.
- Raises `ValueError` if `completed_at` is provided for a non-terminal
transition.
- `new_decision_id` is stripped of whitespace before storage.
---
## State Transition Constants
```python
from cleveragents.domain.models.core.correction import (
CORRECTION_ATTEMPT_VALID_TRANSITIONS,
CORRECTION_ATTEMPT_TERMINAL_STATES,
)
# CORRECTION_ATTEMPT_VALID_TRANSITIONS: dict[CorrectionAttemptState, set[CorrectionAttemptState]]
# CORRECTION_ATTEMPT_TERMINAL_STATES: frozenset[CorrectionAttemptState]
```
`validate_correction_state_transition(current, new)` raises
`InvalidCorrectionStateTransitionError` if the transition is not in
`CORRECTION_ATTEMPT_VALID_TRANSITIONS`.
---
## Database Schema
```sql
CREATE TABLE correction_attempts (
id TEXT PRIMARY KEY,
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE RESTRICT,
original_decision_id TEXT NOT NULL REFERENCES decisions(id) ON DELETE RESTRICT,
new_decision_id TEXT REFERENCES decisions(id) ON DELETE RESTRICT,
guidance TEXT NOT NULL,
mode TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
completed_at TEXT
);
```
**FK semantics**: Both `original_decision_id` and `new_decision_id` use
`RESTRICT` (not `CASCADE`) to preserve the correction audit trail when
decisions are cleaned up.
---
## CorrectionDryRunReport
`CorrectionService.generate_dry_run_report()` returns a
`CorrectionDryRunReport`. Redundant top-level fields were removed in
v3.7.0 (issue #1087); consumers must now access correction impact data
through the embedded `impact` sub-object:
| Old field (removed) | New canonical path |
|--------------------|--------------------|
| `excluded_decisions` | `report.impact.excluded_decisions` |
| `rollback_tier_depth` | `report.impact.rollback_tier_depth` |
| `child_plans_to_rollback` | `report.impact.affected_child_plans` |
---
## Related Documentation
- [Decision Correction](decision_correction.md)
- [Decision Model](decision_model.md)
- [Decision Service](decision_service.md)
- [Plan Lifecycle](plan_execute.md)
- [Database Schema](database_schema.md)
+192
View File
@@ -0,0 +1,192 @@
# Estimation Lifecycle
The estimation lifecycle is an optional hook that runs between the
**Strategize** and **Execute** phases of a plan. When configured, an
estimation actor analyses the plan's decision tree and produces an
`EstimationResult` containing cost, time, and risk forecasts.
Introduced in v3.7.0 (issue #1310). See also
[`plan_execute.md`](plan_execute.md) for the full plan lifecycle.
**Module**: `cleveragents.application.services.plan_lifecycle_service`
---
## Overview
```
Strategize phase completes
actor.default.estimation configured?
Yes │ No
│──────────────────► Execute phase begins immediately
EstimationActor.run(plan)
├── EstimationResult stored on plan
├── plan.cost_estimate_usd populated
└── PLAN_ESTIMATION_COMPLETE event emitted
▼ (failure is informational — never blocks Execute)
Execute phase begins
```
Estimation failures are **informational only**. A failed or missing
estimation actor never prevents the plan from advancing to Execute.
---
## Configuration
Set the estimation actor via the `actor.default.estimation` config key:
```bash
agents config set actor.default.estimation anthropic/claude-4-sonnet
```
Or in `~/.config/cleveragents/config.yaml`:
```yaml
actor:
default:
estimation: anthropic/claude-4-sonnet
```
When this key is absent or empty, the estimation hook is skipped.
---
## EstimationResult
**Module**: `cleveragents.domain.models.core.estimation`
All fields are optional. The estimation actor populates whichever
metrics it can compute.
| Field | Type | Description |
|-------|------|-------------|
| `estimated_cost_usd` | `float \| None` | Estimated LLM cost in USD (≥ 0) |
| `estimated_tokens` | `int \| None` | Estimated total token usage (≥ 0) |
| `estimated_steps` | `int \| None` | Expected number of execution steps (≥ 0) |
| `estimated_child_plans` | `int \| None` | Expected number of child plans |
| `estimated_time_seconds` | `float \| None` | Estimated execution time in seconds (≥ 0) |
| `risk_level` | `"low" \| "medium" \| "high" \| "critical" \| None` | Risk assessment |
| `risk_factors` | `tuple[str, ...]` | Identified risk factors (max 100 items) |
| `summary` | `str` | Human-readable estimation summary (max 10 000 chars) |
| `raw_output` | `str` | Raw actor output before parsing (max 100 000 chars) |
`EstimationResult` is a **frozen** Pydantic model. Instances are
immutable after construction.
### Example
```python
from cleveragents.domain.models.core.estimation import EstimationResult
result = EstimationResult(
estimated_cost_usd=0.42,
estimated_tokens=15_000,
estimated_steps=8,
risk_level="low",
risk_factors=("no external API calls", "read-only resources"),
summary="Low-risk plan with ~8 steps and minimal token usage.",
)
# Only non-empty fields for display
print(result.as_display_dict())
# {
# "estimated_cost_usd": 0.42,
# "estimated_tokens": 15000,
# "estimated_steps": 8,
# "risk_level": "low",
# "risk_factors": ["no external API calls", "read-only resources"],
# "summary": "Low-risk plan with ~8 steps and minimal token usage.",
# }
```
---
## PLAN_ESTIMATION_COMPLETE Event
When estimation succeeds, `PlanLifecycleService` emits a
`PLAN_ESTIMATION_COMPLETE` domain event with the following details:
| Field | Value |
|-------|-------|
| `event_type` | `EventType.PLAN_ESTIMATION_COMPLETE` |
| `plan_id` | ID of the plan being estimated |
| `actor` | Name of the estimation actor |
| `estimated_cost_usd` | From `EstimationResult.estimated_cost_usd` |
| `risk_level` | From `EstimationResult.risk_level` |
| `summary` | From `EstimationResult.summary` |
The event is published to the event bus and picked up by the
`AuditEventSubscriber` for SEC7 audit logging.
---
## plan.cost_estimate_usd
After a successful estimation, `plan.cost_estimate_usd` is set to
`EstimationResult.estimated_cost_usd`. This value is included in:
- `agents plan show` output (when non-null)
- `agents plan list` table (when non-null)
- The `PLAN_APPLIED` event's audit details
---
## CLI Visibility
```bash
# Show plan details including cost estimate
agents plan show <PLAN_ID>
# Example output (with estimation configured)
# Plan: my-feature-plan
# Phase: Execute
# Cost estimate: $0.42
# Risk: low
# ...
```
---
## Writing an Estimation Actor
An estimation actor is a standard CleverAgents actor. It receives the
plan's decision tree as context and should return a structured
`EstimationResult`-compatible response.
The actor is invoked with the plan's action description and decision
tree summary. It should return JSON that the lifecycle service parses
into an `EstimationResult`.
```yaml
# Example custom estimation actor
name: my-estimator
namespace: local
provider: anthropic
model: claude-4-sonnet
system_prompt: |
You are a cost and risk estimation expert for AI automation plans.
Given a plan's action and decision tree, estimate:
- Total LLM cost in USD
- Total token usage
- Number of execution steps
- Risk level (low/medium/high/critical)
- Key risk factors
Return a JSON object matching the EstimationResult schema.
```
---
## Related Documentation
- [Plan Lifecycle](plan_execute.md)
- [Plan Apply](plan_apply.md)
- [Actor Runtime](actor_runtime.md)
- [Domain Events](event_bus.md)
- [Audit Logging](audit_logging.md)
+96
View File
@@ -352,6 +352,8 @@ Key modules:
| `tui/permissions/models.py` | `ToolPermissionRequest`, `PermissionDecision`, `DiffDisplayMode` |
| `tui/permissions/screen.py` | `PermissionsScreen` — split-pane diff view for tool permission requests |
| `tui/permissions/service.py` | `PermissionRequestService` — request queue and session-scoped decisions |
| `tui/first_run.py` | First-run detection and default persona creation |
| `tui/widgets/actor_selection_overlay.py` | Actor selection overlay for first-run experience |
## Permissions Screen
@@ -381,6 +383,100 @@ and can be expanded or collapsed with `Space`.
See [`tui_thought_block.md`](tui_thought_block.md) for the full API reference.
## First-Run Experience
On the first launch (when no personas are configured), the TUI displays
the **Actor Selection Overlay** — a guided setup screen that lets the
user pick an actor to get started.
```
┌─────────────────────────────────────────────────────┐
│ Welcome to CleverAgents │
│ │
│ Select an actor to get started: │
│ │
│ > anthropic/claude-4-sonnet │
│ anthropic/claude-4-opus │
│ openai/gpt-4o │
│ openai/o3 │
│ google/gemini-2 │
│ │
│ Search: ___________
│ [Enter] Select [Esc] Cancel │
└─────────────────────────────────────────────────────┘
```
After the user selects an actor, a `"default"` persona is automatically
created and persisted to the persona registry. Subsequent launches
restore the last-used persona and skip the first-run overlay.
**First-run detection**: `is_first_run(registry)` returns `True` when
`PersonaRegistry.list_personas()` returns an empty list.
**Module**: `cleveragents.tui.first_run`
| Function | Description |
|----------|-------------|
| `is_first_run(registry)` | Returns `True` when no personas are configured |
| `create_default_persona_for_actor(registry, actor)` | Creates and persists a `"default"` persona for the given actor |
**Widget**: `cleveragents.tui.widgets.actor_selection_overlay`
| Function | Description |
|----------|-------------|
| `render_actor_selection(actors, selected_index, search_query)` | Renders the actor selection overlay content |
Default actors offered in the overlay:
- `anthropic/claude-4-sonnet`
- `anthropic/claude-4-opus`
- `openai/gpt-4o`
- `openai/o3`
- `google/gemini-2`
---
## Session Export / Import (TUI)
Sessions can be exported and imported directly from the TUI using slash
commands or the Sessions Screen.
```bash
# Export the current session to JSON
/session:export
# Import a session from a JSON file
/session:import
```
Export produces a JSON file containing the full session transcript and
metadata. Import restores a session from a previously exported file.
Both operations are also available via the CLI:
```bash
agents session export --session-id <ID> --output session.json
agents session import --input session.json
```
---
## Persona Export / Import (TUI)
Personas can be exported to YAML and imported from YAML files:
```bash
# Export a persona
/persona:export
# Import a persona
/persona:import
```
Export paths must be relative to the current working directory.
Imported personas are validated against the persona schema before
being saved to the registry.
## Related Documentation
- [ADR-044: TUI Architecture and Framework](../adr/ADR-044-tui-architecture-and-framework.md)
+11 -21
View File
@@ -50,6 +50,7 @@ def _make_async_service(context: Context) -> AuditService:
engine = create_engine(db_url, connect_args={"check_same_thread": False})
Base.metadata.create_all(engine, tables=[])
from cleveragents.infrastructure.database.models import AuditLogModel
Base.metadata.create_all(engine, tables=[AuditLogModel.__table__])
engine.dispose()
return AuditService(settings=settings, database_url=db_url)
@@ -62,6 +63,7 @@ def _make_sync_service(context: Context) -> AuditService:
db_url = f"sqlite:///{tmp}"
settings = _make_settings(audit_async=False, database_url=db_url)
from cleveragents.infrastructure.database.models import AuditLogModel
engine = create_engine(db_url)
Base.metadata.create_all(engine, tables=[AuditLogModel.__table__])
engine.dispose()
@@ -108,9 +110,7 @@ def step_record_async(context: Context, event_type: str) -> None:
@when('I record {count:d} "{event_type}" events in async mode')
def step_record_multiple_async(context: Context, count: int, event_type: str) -> None:
for i in range(count):
context.async_service.record(
event_type=event_type, plan_id=f"plan-async-{i}"
)
context.async_service.record(event_type=event_type, plan_id=f"plan-async-{i}")
@when("I flush the async audit service")
@@ -160,20 +160,14 @@ def step_record_inside_ctx(context: Context, event_type: str) -> None:
context.async_closed = True
@when(
'I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode'
)
def step_record_ordered_async(
context: Context, p1: str, p2: str, p3: str
) -> None:
@when('I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode')
def step_record_ordered_async(context: Context, p1: str, p2: str, p3: str) -> None:
for pid in (p1, p2, p3):
context.async_service.record(event_type="plan_applied", plan_id=pid)
context.ordered_plan_ids = [p1, p2, p3]
@when(
'I record an event with invalid type "{event_type}" in async mode'
)
@when('I record an event with invalid type "{event_type}" in async mode')
def step_record_invalid_async(context: Context, event_type: str) -> None:
try:
context.async_service.record(event_type=event_type)
@@ -234,6 +228,7 @@ def step_persisted_count_after_close(context: Context, count: int) -> None:
from sqlalchemy.orm import sessionmaker as _sm
from cleveragents.infrastructure.database.models import AuditLogModel as _M
engine = _ce(f"sqlite:///{db_path}")
session = _sm(bind=engine)()
actual = session.query(_M).count()
@@ -254,6 +249,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None:
from sqlalchemy.orm import sessionmaker as _sm
from cleveragents.infrastructure.database.models import AuditLogModel as _M
engine = _ce(f"sqlite:///{db_path}")
session = _sm(bind=engine)()
actual = session.query(_M).count()
@@ -261,9 +257,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None:
engine.dispose()
else:
actual = context.async_service.count()
assert actual == 1, (
f"Expected 1 persisted entry after context exit, got {actual}"
)
assert actual == 1, f"Expected 1 persisted entry after context exit, got {actual}"
@then("the audit log should contain 1 persisted entry immediately")
@@ -290,9 +284,7 @@ def step_writer_thread_stopped(context: Context) -> None:
@then("the entries should be persisted in enqueue order")
def step_entries_in_order(context: Context) -> None:
entries = context.async_service.list_entries(
event_type="plan_applied", limit=100
)
entries = context.async_service.list_entries(event_type="plan_applied", limit=100)
# list_entries returns newest-first; reverse to get enqueue order.
persisted_ids = [e.plan_id for e in reversed(entries)]
assert persisted_ids == context.ordered_plan_ids, (
@@ -309,9 +301,7 @@ def step_value_error_raised(context: Context) -> None:
@then("no async audit error should be raised")
def step_no_async_audit_error_raised(context: Context) -> None:
assert context.async_error is None, (
f"Expected no error, got {context.async_error}"
)
assert context.async_error is None, f"Expected no error, got {context.async_error}"
# ── Settings steps ────────────────────────────────────────────────
+27 -18
View File
@@ -120,9 +120,7 @@ def step_each_def_has_protocol(context: Context) -> None:
@then("each definition should have a non-empty description")
def step_each_def_has_description(context: Context) -> None:
for ep in context.ep_definitions:
assert ep.description, (
f"Extension point {ep.name} has empty description"
)
assert ep.description, f"Extension point {ep.name} has empty description"
# ---------------------------------------------------------------------------
@@ -134,16 +132,12 @@ def step_each_def_has_description(context: Context) -> None:
def step_lookup_by_name(context: Context, name: str) -> None:
eps = context.ep_manager.list_extension_points()
matches = [ep for ep in eps if ep.name == name]
context.ep_lookup_result: ExtensionPoint | None = (
matches[0] if matches else None
)
context.ep_lookup_result: ExtensionPoint | None = matches[0] if matches else None
@then('the looked-up extension point name should be "{name}"')
def step_lookup_name_is(context: Context, name: str) -> None:
assert context.ep_lookup_result is not None, (
f"Extension point '{name}' not found"
)
assert context.ep_lookup_result is not None, f"Extension point '{name}' not found"
assert context.ep_lookup_result.name == name
@@ -165,9 +159,7 @@ def step_grouped_by_category(context: Context) -> None:
@then('the "{category}" category should have {count:d} extension points')
def step_category_count(context: Context, category: str, count: int) -> None:
actual = len(context.ep_categories.get(category, []))
assert actual == count, (
f"Category '{category}': expected {count}, got {actual}"
)
assert actual == count, f"Category '{category}': expected {count}, got {actual}"
# ---------------------------------------------------------------------------
@@ -233,9 +225,7 @@ def step_pipeline_without_pm(context: Context) -> None:
@then("the pipeline should report {count:d} context extension points")
def step_pipeline_context_eps(context: Context, count: int) -> None:
actual = len(context.ep_pipeline.context_extension_points)
assert actual == count, (
f"Expected {count} context extension points, got {actual}"
)
assert actual == count, f"Expected {count} context extension points, got {actual}"
# ---------------------------------------------------------------------------
@@ -253,9 +243,7 @@ def step_all_protocols_runtime_checkable(context: Context) -> None:
for name, proto in context.ep_protocol_map.items():
# runtime_checkable protocols have _is_runtime_protocol = True
is_rc: Any = getattr(proto, "_is_runtime_protocol", False)
assert is_rc, (
f"Protocol {name} ({proto.__name__}) is not runtime-checkable"
)
assert is_rc, f"Protocol {name} ({proto.__name__}) is not runtime-checkable"
# ---------------------------------------------------------------------------
@@ -499,6 +487,7 @@ class _StubSafetyGuardrail:
# --- Context Strategy ---
@given("a concrete ContextStrategyExtension implementation")
def step_concrete_ctx_strategy(context: Context) -> None:
context.ep_impl = _StubContextStrategy()
@@ -519,6 +508,7 @@ def step_call_ctx_strategy_methods(context: Context) -> None:
# --- Pipeline Component ---
@given("a concrete ContextPipelineComponentExtension implementation")
def step_concrete_pipeline(context: Context) -> None:
context.ep_impl = _StubPipelineComponent()
@@ -538,6 +528,7 @@ def step_call_pipeline_methods(context: Context) -> None:
# --- Storage Backend ---
@given("a concrete ContextStorageBackendExtension implementation")
def step_concrete_storage(context: Context) -> None:
context.ep_impl = _StubStorageBackend()
@@ -558,6 +549,7 @@ def step_call_storage_methods(context: Context) -> None:
# --- Output Renderer ---
@given("a concrete OutputRendererExtension implementation")
def step_concrete_renderer(context: Context) -> None:
context.ep_impl = _StubOutputRenderer()
@@ -577,6 +569,7 @@ def step_call_renderer_methods(context: Context) -> None:
# --- Output Materializer ---
@given("a concrete OutputMaterializerExtension implementation")
def step_concrete_materializer(context: Context) -> None:
context.ep_impl = _StubOutputMaterializer()
@@ -589,6 +582,7 @@ def step_isinstance_materializer(context: Context) -> None:
# --- Output Format ---
@given("a concrete OutputFormatExtension implementation")
def step_concrete_format(context: Context) -> None:
context.ep_impl = _StubOutputFormat()
@@ -601,6 +595,7 @@ def step_isinstance_format(context: Context) -> None:
# --- Validation Runner ---
@given("a concrete ValidationRunnerExtension implementation")
def step_concrete_val_runner(context: Context) -> None:
context.ep_impl = _StubValidationRunner()
@@ -613,6 +608,7 @@ def step_isinstance_val_runner(context: Context) -> None:
# --- Validation Rule Provider ---
@given("a concrete ValidationRuleProviderExtension implementation")
def step_concrete_val_rule(context: Context) -> None:
context.ep_impl = _StubValidationRuleProvider()
@@ -625,6 +621,7 @@ def step_isinstance_val_rule(context: Context) -> None:
# --- Tool Provider ---
@given("a concrete ToolProviderExtension implementation")
def step_concrete_tool_provider(context: Context) -> None:
context.ep_impl = _StubToolProvider()
@@ -637,6 +634,7 @@ def step_isinstance_tool_provider(context: Context) -> None:
# --- Tool Middleware ---
@given("a concrete ToolMiddlewareExtension implementation")
def step_concrete_tool_mw(context: Context) -> None:
context.ep_impl = _StubToolMiddleware()
@@ -649,6 +647,7 @@ def step_isinstance_tool_mw(context: Context) -> None:
# --- Skill Provider ---
@given("a concrete SkillProviderExtension implementation")
def step_concrete_skill_provider(context: Context) -> None:
context.ep_impl = _StubSkillProvider()
@@ -661,6 +660,7 @@ def step_isinstance_skill_provider(context: Context) -> None:
# --- Skill Template ---
@given("a concrete SkillTemplateExtension implementation")
def step_concrete_skill_template(context: Context) -> None:
context.ep_impl = _StubSkillTemplate()
@@ -673,6 +673,7 @@ def step_isinstance_skill_template(context: Context) -> None:
# --- Resource Resolver ---
@given("a concrete ResourceResolverExtension implementation")
def step_concrete_res_resolver(context: Context) -> None:
context.ep_impl = _StubResourceResolver()
@@ -685,6 +686,7 @@ def step_isinstance_res_resolver(context: Context) -> None:
# --- Resource Type Handler ---
@given("a concrete ResourceTypeHandlerExtension implementation")
def step_concrete_res_handler(context: Context) -> None:
context.ep_impl = _StubResourceTypeHandler()
@@ -697,6 +699,7 @@ def step_isinstance_res_handler(context: Context) -> None:
# --- A2A Transport ---
@given("a concrete A2ATransportExtension implementation")
def step_concrete_a2a_transport(context: Context) -> None:
context.ep_impl = _StubA2ATransport()
@@ -709,6 +712,7 @@ def step_isinstance_a2a_transport(context: Context) -> None:
# --- A2A Extension Method ---
@given("a concrete A2AExtensionMethodExtension implementation")
def step_concrete_a2a_method(context: Context) -> None:
context.ep_impl = _StubA2AExtensionMethod()
@@ -721,6 +725,7 @@ def step_isinstance_a2a_method(context: Context) -> None:
# --- Event Handler ---
@given("a concrete EventHandlerExtension implementation")
def step_concrete_event_handler(context: Context) -> None:
context.ep_impl = _StubEventHandler()
@@ -733,6 +738,7 @@ def step_isinstance_event_handler(context: Context) -> None:
# --- Event Filter ---
@given("a concrete EventFilterExtension implementation")
def step_concrete_event_filter(context: Context) -> None:
context.ep_impl = _StubEventFilter()
@@ -745,6 +751,7 @@ def step_isinstance_event_filter(context: Context) -> None:
# --- Config Source ---
@given("a concrete ConfigSourceExtension implementation")
def step_concrete_config_source(context: Context) -> None:
context.ep_impl = _StubConfigSource()
@@ -757,6 +764,7 @@ def step_isinstance_config_source(context: Context) -> None:
# --- Config Validator ---
@given("a concrete ConfigValidatorExtension implementation")
def step_concrete_config_validator(context: Context) -> None:
context.ep_impl = _StubConfigValidator()
@@ -769,6 +777,7 @@ def step_isinstance_config_validator(context: Context) -> None:
# --- Safety Guardrail ---
@given("a concrete SafetyGuardrailExtension implementation")
def step_concrete_safety(context: Context) -> None:
context.ep_impl = _StubSafetyGuardrail()
+1
View File
@@ -10,6 +10,7 @@ site_dir: build/site
nav:
- Specification: specification.md
- Architecture Overview: reference/architecture_overview.md
- Development:
- CI/CD Pipeline: development/ci-cd.md
- Quality Automation: development/quality-automation.md
@@ -70,9 +70,7 @@ class _ExtensionPointDef:
__slots__ = ("description", "name", "protocol_type")
def __init__(
self, name: str, protocol_type: type[Any], description: str
) -> None:
def __init__(self, name: str, protocol_type: type[Any], description: str) -> None:
self.name = name
self.protocol_type = protocol_type
self.description = description
@@ -45,7 +45,9 @@ class ContextPipelineComponentExtension(Protocol):
@property
def component_name(self) -> str: ...
def process(self, fragments: Sequence[Any], context: Mapping[str, Any]) -> Sequence[Any]: ... # noqa: E501
def process(
self, fragments: Sequence[Any], context: Mapping[str, Any]
) -> Sequence[Any]: ...
@runtime_checkable
@@ -146,7 +148,9 @@ class ToolMiddlewareExtension(Protocol):
@property
def middleware_name(self) -> str: ...
def before_invoke(self, tool_name: str, arguments: Mapping[str, Any]) -> Mapping[str, Any]: ... # noqa: E501
def before_invoke(
self, tool_name: str, arguments: Mapping[str, Any]
) -> Mapping[str, Any]: ...
def after_invoke(self, tool_name: str, result: Any) -> Any: ...
+3
View File
@@ -1209,3 +1209,6 @@ create_workspace_snapshot # noqa: B018, F821
selective_rollback # noqa: B018, F821
archive_artifacts # noqa: B018, F821
revert_decisions # noqa: B018, F821
# OutputMaterializerExtension and A2ATransportExtension protocol parameters (#extension-plugins)
destination # noqa: B018, F821