Files
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

135 lines
5.5 KiB
Markdown

# 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)
```python
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:
```python
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_strategize`
`auto_progress``execute/queued`) will not be visible to the
caller's service instance, producing stale state reads.
```python
# 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](plan_execute.md#cli-executor-wiring-_get_plan_executor).
## Usage Example
```python
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...")
```