_get_plan_executor() created a second PlanLifecycleService Factory instance with its own in-memory _plans cache. After the executor's run_strategize() advanced the plan to execute/queued (via auto_progress), the CLI handler's separate service instance returned stale strategize/queued state from its cache, causing spurious "Plan is not in an executable state" errors. Fix: _get_plan_executor() now accepts an optional lifecycle_service parameter; the plan execute handler passes its own service instance so both share the same cache. Also addressed review feedback: - Improved type safety: lifecycle_service parameter typed as PlanLifecycleService | None instead of Any | None. - Added BDD regression test verifying the lifecycle service is shared between the CLI handler and the executor. - Updated reference documentation to reflect the type annotation change. ISSUES CLOSED: #1026
5.5 KiB
Dependency Injection Reference
The CleverAgents DI container is built on dependency-injector's
DeclarativeContainer. All service registrations live in
src/cleveragents/application/container.py.
Container Overview
| Provider | Type | Description |
|---|---|---|
settings |
Singleton | Application settings |
provider_registry |
Singleton | AI provider registry |
database_url |
Callable | Database URL resolution |
unit_of_work |
Factory | Unit of Work (new per request) |
ai_provider |
Singleton | AI provider instance |
project_service |
Factory | Project management service |
vector_store_service |
Factory | Vector store operations |
context_service |
Factory | Context management service |
actor_service |
Factory | Actor configuration service |
actor_registry |
Factory | Actor registry with provider binding |
plan_service |
Factory | Legacy plan management (deprecated) |
decision_service |
Factory | Decision tree recording service |
plan_lifecycle_service |
Factory | V3 four-phase plan lifecycle |
resource_registry_service |
Factory | Resource registry operations |
namespaced_project_repo |
Factory | Namespaced project repository |
project_resource_link_repo |
Factory | Project-resource link repository |
stream_router |
Singleton | Reactive stream routing |
langgraph_bridge |
Singleton | LangGraph integration bridge |
route_bridge |
Singleton | Reactive route bridge |
Decision Service Registration
The DecisionService is registered as a Factory provider, meaning
each resolution returns a new instance. It depends on:
settings— Application settings (Singleton)unit_of_work— Unit of Work (Factory)
decision_service = providers.Factory(
DecisionService,
settings=settings,
unit_of_work=unit_of_work,
)
DecisionService wraps DecisionRepository (accessible via
UnitOfWorkContext.decisions) and provides:
| Method | Description |
|---|---|
record_decision() |
Persist a new decision |
get_decision() |
Retrieve a decision by ID |
list_decisions() |
Get all decisions for a plan |
get_tree() |
BFS traversal of the decision tree |
get_path_to_root() |
Walk from a decision up to the root |
mark_superseded() |
Mark a decision as superseded |
list_by_type() |
List decisions by type for a plan |
Plan Lifecycle Service Registration
The PlanLifecycleService is registered as a Factory provider
with the DecisionService injected:
plan_lifecycle_service = providers.Factory(
PlanLifecycleService,
settings=settings,
unit_of_work=unit_of_work,
decision_service=decision_service,
)
When a DecisionService is wired, the lifecycle service automatically
records decisions during phase transitions:
- start_strategize → records a
strategy_choicedecision - start_execute → records an
implementation_choicedecision
If no DecisionService is provided (e.g. in legacy tests), decision
recording is silently skipped.
Cache-Sharing Caveat
Because plan_lifecycle_service is a Factory, each
container.plan_lifecycle_service() call returns a new instance
with its own in-memory _plans cache. Code that creates a
PlanLifecycleService and separately constructs a PlanExecutor
must pass the same service instance to both; otherwise the
executor's phase mutations (e.g. complete_strategize →
auto_progress → execute/queued) will not be visible to the
caller's service instance, producing stale state reads.
# Correct: shared instance
service = container.plan_lifecycle_service()
executor = PlanExecutor(lifecycle_service=service, ...)
# Wrong: two independent instances with separate caches
service = container.plan_lifecycle_service() # Instance A
executor = PlanExecutor(
lifecycle_service=container.plan_lifecycle_service(), # Instance B
...
)
The CLI helper _get_plan_executor(lifecycle_service=...) accepts an
optional pre-existing service for exactly this purpose. See
CLI Executor Wiring.
Usage Example
from cleveragents.application.container import get_container
container = get_container()
# Resolve services
decision_svc = container.decision_service()
lifecycle_svc = container.plan_lifecycle_service()
# Record a decision manually
from cleveragents.domain.models.core.decision import DecisionType
decision = decision_svc.record_decision(
plan_id="01HV...",
decision_type=DecisionType.STRATEGY_CHOICE,
question="Which approach?",
chosen_option="Approach A",
)
# Retrieve decisions
decisions = decision_svc.list_decisions("01HV...")