Dependency Injection¶
AgenticAPI provides FastAPI-compatible Depends() injection for handler parameters, letting you wire databases, auth, config, and other shared resources without mutating global state.
Usage¶
from agenticapi import AgenticApp, Depends, Intent
async def get_db():
async with engine.connect() as conn:
yield conn # cleanup runs after the handler returns
async def get_current_user(db = Depends(get_db)) -> dict:
return await db.fetch_one("SELECT * FROM users WHERE id = 1")
app = AgenticApp()
@app.agent_endpoint(name="orders")
async def orders(
intent: Intent,
context,
db = Depends(get_db),
user = Depends(get_current_user),
):
return {"user": user["name"], "orders": [...]}
Supports:
- Sync and async providers
- yield providers with teardown after the handler returns
- Nested dependencies (a provider can itself declare Depends() params)
- Caching within a request — the same dependency is resolved once per request, so multiple handlers in a call graph share the same instance
Implementation¶
The dependency system lives in src/agenticapi/dependencies/ and has three parts:
depends.py — the Depends() marker¶
from agenticapi.dependencies import Depends, Dependency
dep = Depends(get_db) # returns a Dependency(provider=get_db) instance
Dependency is a frozen dataclass wrapping the provider callable. Depends() is just a thin factory — it exists so the public API mirrors FastAPI's Depends() one-to-one.
scanner.py — handler signature scanner¶
scan_handler(handler) inspects a handler's signature and returns an InjectionPlan describing, for each parameter:
InjectionKind— which framework-managed value to inject (INTENT,CONTEXT,AGENT_TASKS,UPLOADED_FILES,HTMX_HEADERS, orDEPENDENCYfor user-declaredDepends()params)- The Pydantic model for typed intent payloads (when
intent: MyPayloadModelis declared) - The
Dependencyinstance for user-provided providers
Scanning happens once at registration time, so hot-path requests don't pay the inspection cost.
solver.py — dependency graph resolver¶
ResolvedHandlerCall is the result of resolving an InjectionPlan against a concrete request. The solver:
- Topologically sorts nested dependencies
- Calls each provider with its own resolved params
- Handles sync, async, and
yield-based providers uniformly - Caches resolutions within a single request so diamond-shaped graphs don't double-invoke providers
- Runs cleanup (post-
yieldcode) in reverse order after the handler returns, even on errors
DependencyResolutionError is raised if the graph can't be resolved (missing provider, cycle, type mismatch).
Injection Kinds¶
Beyond user-declared Depends() params, the scanner automatically injects framework values based on type annotations:
| Annotation | Injected value |
|---|---|
intent: Intent |
The parsed Intent object |
intent: Intent[MyPayloadModel] |
Typed intent — LLM output validated against the Pydantic model, available via intent.payload |
context: AgentContext |
The AgentContext for this request |
tasks: AgentTasks |
Background-task accumulator |
files: UploadedFiles |
Dict of uploaded files (multipart) |
htmx: HtmxHeaders |
Parsed HTMX request headers |
param = Depends(provider) |
User-provided dependency |
The order of parameters in the handler signature does not matter — the scanner routes each by annotation.
Caching and Isolation¶
Dependencies are request-scoped by default. Within a single request, two Depends(get_db) calls return the same instance. Across requests, each gets a fresh resolution.
There is no global or application-scoped cache — state that outlives a request should be held by a regular module-level object (the harness, the tool registry, etc.) rather than a dependency.
Errors¶
| Situation | Exception |
|---|---|
| Provider raises | DependencyResolutionError wraps the original |
Depends() used on unsupported param |
DependencyResolutionError at registration |
| Cycle in dependency graph | DependencyResolutionError at scan time |
All DependencyResolutionErrors inherit from AgentRuntimeError and map to HTTP 500.