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
_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
135 lines
5.5 KiB
Markdown
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...")
|
|
```
|