Files
cleveragents-core/docs/reference/di.md
T
CoreRasurae ff2d824f17
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / lint (pull_request) Successful in 19s
CI / quality (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 43s
CI / security (pull_request) Successful in 50s
CI / unit_tests (pull_request) Successful in 3m11s
CI / integration_tests (pull_request) Successful in 3m37s
CI / e2e_tests (pull_request) Successful in 3m52s
CI / docker (pull_request) Successful in 56s
CI / coverage (pull_request) Successful in 5m57s
CI / build (push) Successful in 14s
CI / lint (push) Successful in 20s
CI / quality (push) Successful in 25s
CI / typecheck (push) Successful in 49s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 52s
CI / unit_tests (push) Successful in 3m18s
CI / integration_tests (push) Successful in 3m31s
CI / docker (push) Successful in 55s
CI / e2e_tests (push) Successful in 4m42s
CI / coverage (push) Successful in 6m0s
CI / benchmark-publish (push) Successful in 20m10s
CI / benchmark-regression (pull_request) Successful in 37m16s
fix(cli): share PlanLifecycleService instance between CLI handler and PlanExecutor
_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
2026-03-17 12:22:32 +00:00

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_choice decision
  • start_execute → records an implementation_choice decision

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_strategizeauto_progressexecute/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...")