Context & Dependency Injection¶
Real-world tools need more than LLM-provided arguments — they need database connections, API clients, user sessions, and configuration. RunContext lets you pass runtime dependencies to your tools cleanly and type-safely.
Basic Usage¶
from dataclasses import dataclass
from fastaiagent import Agent, LLMClient, RunContext, tool
@dataclass
class Deps:
db: DatabaseClient
api_key: str
@tool(name="get_customer")
def get_customer(ctx: RunContext[Deps], customer_id: str) -> str:
"""Fetch customer details."""
return ctx.state.db.get("customers", customer_id)
agent = Agent(
name="support",
system_prompt="You help customers with their accounts.",
llm=LLMClient(provider="openai", model="gpt-4o"),
tools=[get_customer],
)
# Create context with your runtime dependencies
ctx = RunContext(state=Deps(db=DatabaseClient(), api_key="sk-..."))
result = agent.run("Look up customer C-100", context=ctx)
print(result.output)
How it works:
- Annotate any tool parameter with
RunContext[YourType] - The SDK detects this at tool creation time and excludes it from the LLM schema
- At runtime, pass
RunContext(state=your_object)toagent.run() - The SDK injects it into every tool that declares it
- Tools without
RunContextwork exactly as before
Multiple Tools, Same Context¶
All tools in an agent share the same RunContext instance. This is useful for passing shared dependencies like a database connection or user session.
@dataclass
class AppState:
db: DatabaseClient
user_id: str
permissions: list[str]
@tool(name="get_orders")
def get_orders(ctx: RunContext[AppState], status: str) -> str:
"""Get orders filtered by status."""
return ctx.state.db.query(
"orders",
user_id=ctx.state.user_id,
status=status,
)
@tool(name="cancel_order")
def cancel_order(ctx: RunContext[AppState], order_id: str) -> str:
"""Cancel an order."""
if "order:cancel" not in ctx.state.permissions:
return "Error: insufficient permissions"
ctx.state.db.update("orders", order_id, status="cancelled")
return f"Order {order_id} cancelled"
# Both tools receive the same context
agent = Agent(
name="order-manager",
system_prompt="You manage customer orders.",
llm=LLMClient(provider="openai", model="gpt-4o"),
tools=[get_orders, cancel_order],
)
ctx = RunContext(state=AppState(
db=DatabaseClient(),
user_id="u-789",
permissions=["order:read", "order:cancel"],
))
result = agent.run("Cancel order ORD-456", context=ctx)
Mixing Context and Non-Context Tools¶
Tools with and without RunContext work together seamlessly. The SDK only injects context into tools that declare it.
@tool(name="search_kb")
def search_kb(ctx: RunContext[Deps], query: str) -> str:
"""Search the knowledge base (needs DB connection)."""
return ctx.state.db.search(query)
@tool(name="calculate_total")
def calculate_total(prices: list[float], tax_rate: float) -> float:
"""Calculate total with tax (pure computation, no context needed)."""
subtotal = sum(prices)
return round(subtotal * (1 + tax_rate), 2)
agent = Agent(
name="assistant",
tools=[search_kb, calculate_total],
llm=LLMClient(provider="openai", model="gpt-4o"),
)
# search_kb gets ctx, calculate_total doesn't — both work
ctx = RunContext(state=Deps(db=DatabaseClient(), api_key="sk-..."))
result = agent.run("Search for pricing info and calculate total for $10, $20 with 8% tax", context=ctx)
Async Tools with Context¶
Async tools receive context the same way.
@tool(name="fetch_weather")
async def fetch_weather(ctx: RunContext[Deps], city: str) -> str:
"""Fetch current weather for a city."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.weather.com/v1/{city}",
headers={"Authorization": f"Bearer {ctx.state.api_key}"},
)
return resp.text
# Works with both arun() and astream()
ctx = RunContext(state=Deps(db=db, api_key="weather-key-123"))
result = await agent.arun("What's the weather in Amsterdam?", context=ctx)
Streaming with Context¶
Context flows through streaming execution identically.
ctx = RunContext(state=Deps(db=DatabaseClient(), api_key="sk-..."))
async for event in agent.astream("Look up customer C-100", context=ctx):
if isinstance(event, TextDelta):
print(event.text, end="", flush=True)
Testing Tools with Context¶
RunContext makes tools easy to unit test — construct the context with mocks and call the tool directly.
import pytest
from unittest.mock import MagicMock
def test_get_customer():
mock_db = MagicMock()
mock_db.get.return_value = {"id": "C-100", "name": "Alice"}
ctx = RunContext(state=Deps(db=mock_db, api_key="test"))
result = get_customer.execute({"customer_id": "C-100"}, context=ctx)
assert result.success
assert "Alice" in str(result.output)
mock_db.get.assert_called_once_with("customers", "C-100")
Per-Request Isolation¶
Each call to agent.run() gets its own RunContext. This is safe for concurrent requests — no shared mutable state between calls.
# FastAPI example: each request gets isolated context
from fastapi import FastAPI, Depends
app = FastAPI()
@app.post("/chat")
async def chat(message: str, user_id: str = Depends(get_current_user)):
ctx = RunContext(state=AppState(
db=get_db_session(),
user_id=user_id,
permissions=get_permissions(user_id),
))
result = await agent.arun(message, context=ctx)
return {"response": result.output}
What Context is NOT¶
- Not serialized —
RunContextnever appears inagent.to_dict()orfa.push(). It is runtime-only. - Not sent to the LLM — Context parameters are excluded from the tool schema. The LLM never sees them.
- Not available on the platform — When you push an agent to FastAIAgent Platform, the platform uses its own dependency injection (ContextBuilder + FastAPI DI). The tool schema is the shared contract between SDK and platform.
- Not a replacement for ChainState —
ChainStatecarries workflow state between chain nodes.RunContextcarries runtime dependencies into tool functions. They serve different purposes.
API Reference¶
RunContext¶
Typed dependency injection container for agent execution.
| Parameter | Type | Required | Description |
|---|---|---|---|
state |
T |
Yes | The runtime state object (dataclass, dict, or any type) |
| Property | Type | Description |
|---|---|---|
state |
T |
The wrapped state object (read-only) |
Using RunContext with from __future__ import annotations¶
If your file uses from __future__ import annotations (common in modern Python for deferred type evaluation), there is an important requirement: RunContext and any type referenced in the generic parameter must be imported at module level, not lazily inside a function or method.
With from __future__ import annotations, all annotations become strings that are resolved lazily via get_type_hints(fn) against the function's __globals__ (the defining module's top-level namespace). If RunContext is imported inside a function scope, resolution fails silently and the SDK falls back to treating the context parameter as a plain str type in the tool's JSON schema. The LLM then sees ctx: string and hallucinates a string value instead of the SDK injecting the real context.
# WRONG — RunContext imported lazily, not visible in __globals__
from __future__ import annotations
def make_tools():
from fastaiagent.agent.context import RunContext # Too late!
@tool(name="whoami")
def whoami(ctx: RunContext[AppState]) -> str:
return ctx.state.user_id # ctx is actually a string here!
# CORRECT — RunContext and AppState imported at module level
from __future__ import annotations
from fastaiagent.agent.context import RunContext
from myapp import AppState
@tool(name="whoami")
def whoami(ctx: RunContext[AppState]) -> str:
return ctx.state.user_id # ctx is a real RunContext[AppState]
This also applies to the dataclass/type used as the generic parameter (AppState in the example). Both must be in the module's top-level namespace for get_type_hints() to resolve RunContext[AppState] correctly.
If you're not using from __future__ import annotations, this is not a concern — annotations are evaluated eagerly at class/function definition time and RunContext just needs to be importable at that point.
Next Steps¶
- FunctionTool — Wrap Python functions as tools
- Using Tools with Agents — How to attach tools to agents
- Tools Overview — All tool types at a glance