Skip to content
nerva docs v0.2.1

ExecContext

ExecContext is primitive #0 — the shared nervous system of a request. It flows through every method in every primitive, carrying identity, permissions, memory scope, observability, and lifecycle state.

Why It Exists

Without ExecContext, you end up with one of two outcomes:

  1. Argument explosion — every function takes 6+ separate parameters for user ID, trace ID, permissions, memory scope, timeout, and streaming
  2. God object later — you build a request context object in v2 after the codebase becomes unmanageable

ExecContext makes this explicit from day one.

What It Carries

Identity

ctx.request_id # unique per request
ctx.trace_id # groups related requests (multi-turn conversation)
ctx.user_id # who is asking
ctx.session_id # conversation session

Permissions

ctx.permissions # what this user/agent can access
ctx.permissions.can_use_tool("search_flights") # check tool access
ctx.permissions.can_use_agent("flight_agent") # check agent access
ctx.permissions.has_role("admin") # check role

Memory Scope

ctx.memory_scope # "user" | "session" | "agent" | "global"

Memory operations are automatically scoped — user A’s context never leaks to user B.

Observability

ctx.spans # OpenTelemetry-compatible trace spans
ctx.events # structured log events
ctx.token_usage # accumulated LLM token counts across all calls

Lifecycle

ctx.created_at # when the request started
ctx.timeout_at # when it should be killed
ctx.cancelled # cooperative cancellation event (asyncio.Event)
ctx.is_timed_out() # check if timeout exceeded
ctx.is_cancelled() # check if cancellation signalled

Streaming

ctx.stream # if set, primitives push tokens here as they are produced

Creating a Context

from nerva import ExecContext
# Minimal
ctx = ExecContext.create(user_id="user_123")
# Full control
ctx = ExecContext.create(
user_id="user_123",
session_id="session_abc",
permissions=my_permissions,
memory_scope="session",
timeout_seconds=30,
)

Who Uses It

Every primitive receives ExecContext and uses the parts it needs:

PrimitiveWhat it reads from ExecContext
Routerpermissions — permission-aware handler selection
Runtimetimeout_at, cancelled, spans — lifecycle and tracing
Toolspermissions — which tools this user/agent can call
Memorymemory_scope, session_id, user_id — scope isolation
Responderstream — streaming vs batch mode
Registrypermissions — filtered discovery
Policytoken_usage, user_id — budget and rate limit evaluation

Framework Bridge

When Nerva runs inside FastAPI, NestJS, or Express, you bridge your framework’s auth into ExecContext:

# FastAPI — map JWT user to Nerva context
@app.post("/chat")
async def chat(req: ChatRequest, user: User = Depends(get_current_user)):
ctx = ExecContext.create(
user_id=user.id,
session_id=req.session_id,
permissions=permissions_from_user(user),
)
return await orchestrator.handle(req.message, ctx)

The contrib helpers (nerva.contrib.fastapi, nerva/contrib/express, nerva/contrib/nestjs) automate this bridge.