Files
cleveragents-core/robot/helper_estimation_actor.py
brent.edwards 6531440431
CI / build (push) Successful in 24s
CI / lint (push) Successful in 3m16s
CI / quality (push) Successful in 3m45s
CI / typecheck (push) Successful in 3m52s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m1s
CI / unit_tests (push) Successful in 7m8s
CI / integration_tests (push) Successful in 7m1s
CI / docker (push) Successful in 1m6s
CI / e2e_tests (push) Successful in 9m49s
CI / coverage (push) Successful in 12m22s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 19m34s
feat(actor): implement estimation actor type (#962)
## Summary

Implement the estimation actor as a functional actor type. The estimation actor provides cost, time, and resource estimates for plan operations, running after Strategize completes (before Execute). Estimation is informational only and optional — plans work without an estimation actor configured.

### Changes

**New files:**
- `src/cleveragents/domain/models/core/estimation.py` — `EstimationResult` frozen Pydantic model with fields for cost (USD), tokens, steps, child plans, time, risk level/factors, summary
- `features/estimation_actor.feature` — 12 Behave test scenarios (model validation, serialization, stub actor, plan integration, optional behavior)
- `features/steps/estimation_actor_steps.py` — Step definitions
- `robot/estimation_actor.robot` — 6 Robot integration tests
- `robot/helper_estimation_actor.py` — Robot test helper

**Modified files:**
- `domain/models/acms/tiers.py` — Added `ESTIMATOR` to `ActorRole` enum
- `domain/models/core/plan.py` — Added `estimation_result: EstimationResult | None` field, estimation data in `as_cli_dict()`
- `application/services/plan_executor.py` — Added `EstimationStubActor` class
- `application/services/plan_lifecycle_service.py` — Added `_run_estimation()` method, invoked in `execute_plan()`
- `cli/commands/plan.py` — Display estimation results in plan status
- `vulture_whitelist.py` — Added 12 new public symbols

### Quality Gates

| Session | Result |
|---|---|
| `nox -s lint` | PASS |
| `nox -s typecheck` | PASS (0 errors) |
| `nox -s unit_tests` | PASS (10,818 scenarios) |
| `nox -s integration_tests` | PASS (1,512 tests) |
| `nox -s coverage_report` | 98% (>= 97%) |

Closes #890

Reviewed-on: #962
Co-authored-by: Brent Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent Edwards <brent.edwards@cleverthis.com>
2026-03-21 01:40:58 +00:00

230 lines
7.1 KiB
Python

"""Helper utilities for estimation actor Robot integration tests.
Covers:
- EstimationResult domain model creation and serialization
- EstimationStubActor placeholder output
- Plan model estimation_result field
- Estimation invocation during Strategize→Execute transition
- ActorRole.ESTIMATOR enum value
Issue #890.
"""
from __future__ import annotations
import sys
from cleveragents.application.services.plan_executor import EstimationStubActor
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.acms.tiers import ActorRole
from cleveragents.domain.models.core.estimation import EstimationResult
from cleveragents.domain.models.core.plan import (
PlanPhase,
ProcessingState,
)
def _create_service() -> PlanLifecycleService:
settings = Settings()
return PlanLifecycleService(settings=settings)
def _estimation_result_model() -> None:
"""Test EstimationResult domain model creation and serialization."""
# All fields
result = EstimationResult(
estimated_cost_usd=0.05,
estimated_tokens=12000,
estimated_steps=5,
estimated_child_plans=2,
estimated_time_seconds=30.0,
risk_level="medium",
risk_factors=("API rate limits", "Large codebase"),
summary="Estimated 5 steps, ~$0.05 cost",
raw_output="raw output",
)
assert result.estimated_cost_usd == 0.05
assert result.estimated_tokens == 12000
assert result.estimated_steps == 5
assert result.estimated_child_plans == 2
assert result.estimated_time_seconds == 30.0
assert result.risk_level == "medium"
assert len(result.risk_factors) == 2
assert result.summary == "Estimated 5 steps, ~$0.05 cost"
# Defaults
default_result = EstimationResult()
assert default_result.estimated_cost_usd is None
assert default_result.estimated_tokens is None
assert default_result.summary == ""
assert default_result.risk_factors == ()
# Serialization round-trip
data = result.model_dump()
restored = EstimationResult(**data)
assert restored == result
# Frozen check
try:
result.summary = "modified" # type: ignore[misc]
raise AssertionError("Should have raised ValidationError")
except Exception:
pass # Expected: frozen model
print("estimation-result-model-ok")
def _estimation_stub_actor() -> None:
"""Test EstimationStubActor placeholder output."""
stub = EstimationStubActor()
# Valid plan ID
result = stub.estimate("01ARZ3NDEKTSV4RRFFQ69G5FAV")
assert isinstance(result, EstimationResult)
assert "stub" in result.summary.lower()
# Empty plan ID should raise
try:
stub.estimate("")
raise AssertionError("Should have raised ValidationError")
except ValidationError:
pass
print("estimation-stub-actor-ok")
def _actor_role_estimator() -> None:
"""Test ActorRole.ESTIMATOR enum value."""
assert hasattr(ActorRole, "ESTIMATOR")
assert ActorRole.ESTIMATOR.value == "estimator"
assert ActorRole("estimator") == ActorRole.ESTIMATOR
print("actor-role-estimator-ok")
def _plan_estimation_field() -> None:
"""Test Plan model estimation_result field."""
service = _create_service()
# Create action with estimation actor
service.create_action(
name="local/est-robot-test",
description="Robot test action",
definition_of_done="Tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
estimation_actor="local/estimator",
)
plan = service.use_action(action_name="local/est-robot-test")
assert plan.estimation_actor == "local/estimator"
assert plan.estimation_result is None
# Set estimation result
plan.estimation_result = EstimationResult(
estimated_cost_usd=1.5,
summary="Test estimation",
)
assert plan.estimation_result is not None
assert plan.estimation_result.estimated_cost_usd == 1.5
# CLI dict
cli_dict = plan.as_cli_dict()
assert "estimation" in cli_dict
assert cli_dict["estimation"]["estimated_cost_usd"] == 1.5
print("plan-estimation-field-ok")
def _estimation_lifecycle_integration() -> None:
"""Test estimation runs during Strategize→Execute transition."""
service = _create_service()
# Action WITH estimation actor
service.create_action(
name="local/est-lifecycle-robot",
description="Lifecycle test",
definition_of_done="Tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
estimation_actor="local/estimator",
)
plan = service.use_action(action_name="local/est-lifecycle-robot")
plan_id = plan.identity.plan_id
# Drive through strategize
service.start_strategize(plan_id)
p = service.get_plan(plan_id)
p.processing_state = ProcessingState.PROCESSING
service._commit_plan(p)
service.complete_strategize(plan_id)
# Check the plan after complete_strategize — auto_progress might
# have advanced it to Execute already (triggering estimation).
p = service.get_plan(plan_id)
if p.phase == PlanPhase.STRATEGIZE:
service.execute_plan(plan_id)
p = service.get_plan(plan_id)
assert p.estimation_result is not None, (
"estimation_result should be set after execute_plan"
)
assert "stub" in p.estimation_result.summary.lower()
print("estimation-lifecycle-integration-ok")
def _no_estimation_lifecycle() -> None:
"""Test estimation is skipped when no estimation_actor is set."""
service = _create_service()
# Action WITHOUT estimation actor
service.create_action(
name="local/no-est-lifecycle-robot",
description="No estimation test",
definition_of_done="Tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
)
plan = service.use_action(action_name="local/no-est-lifecycle-robot")
plan_id = plan.identity.plan_id
# Drive through strategize
service.start_strategize(plan_id)
p = service.get_plan(plan_id)
p.processing_state = ProcessingState.PROCESSING
service._commit_plan(p)
service.complete_strategize(plan_id)
p = service.get_plan(plan_id)
if p.phase == PlanPhase.STRATEGIZE:
service.execute_plan(plan_id)
p = service.get_plan(plan_id)
assert p.estimation_result is None, (
"estimation_result should be None when no estimation_actor is set"
)
print("no-estimation-lifecycle-ok")
_COMMANDS = {
"estimation-result-model": _estimation_result_model,
"estimation-stub-actor": _estimation_stub_actor,
"actor-role-estimator": _actor_role_estimator,
"plan-estimation-field": _plan_estimation_field,
"estimation-lifecycle-integration": _estimation_lifecycle_integration,
"no-estimation-lifecycle": _no_estimation_lifecycle,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>")
sys.exit(1)
_COMMANDS[sys.argv[1]]()