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:
- Argument explosion — every function takes 6+ separate parameters for user ID, trace ID, permissions, memory scope, timeout, and streaming
- 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 requestctx.trace_id # groups related requests (multi-turn conversation)ctx.user_id # who is askingctx.session_id # conversation sessionctx.requestId // unique per requestctx.traceId // groups related requests (multi-turn conversation)ctx.userId // who is askingctx.sessionId // conversation sessionctx.RequestID // unique per requestctx.TraceID // groups related requests (multi-turn conversation)ctx.UserID // who is askingctx.SessionID // conversation sessionPermissions
ctx.permissions # what this user/agent can accessctx.permissions.can_use_tool("search_flights") # check tool accessctx.permissions.can_use_agent("flight_agent") # check agent accessctx.permissions.has_role("admin") # check rolectx.permissions // what this user/agent can accessctx.permissions.canUseTool("search_flights") // check tool accessctx.permissions.canUseAgent("flight_agent") // check agent accessctx.permissions.hasRole("admin") // check rolectx.Permissions // what this user/agent can accessctx.Permissions.CanUseTool("search_flights") // check tool accessctx.Permissions.CanUseAgent("flight_agent") // check agent accessctx.Permissions.HasRole("admin") // check roleMemory Scope
ctx.memory_scope # "user" | "session" | "agent" | "global"ctx.memoryScope // "user" | "session" | "agent" | "global"ctx.MemoryScope // ScopeUser | ScopeSession | ScopeAgent | ScopeGlobalMemory operations are automatically scoped — user A’s context never leaks to user B.
Observability
ctx.spans # OpenTelemetry-compatible trace spansctx.events # structured log eventsctx.token_usage # accumulated LLM token counts across all callsctx.spans // OpenTelemetry-compatible trace spansctx.events // structured log eventsctx.tokenUsage // accumulated LLM token counts across all callsctx.Spans() // OpenTelemetry-compatible trace spans (returns a copy)ctx.Events() // structured log events (returns a copy)ctx.TokenUsage // accumulated LLM token counts across all callsLifecycle
ctx.created_at # when the request startedctx.timeout_at # when it should be killedctx.cancelled # cooperative cancellation event (asyncio.Event)ctx.is_timed_out() # check if timeout exceededctx.is_cancelled() # check if cancellation signalledctx.createdAt // when the request startedctx.timeoutAt // when it should be killedctx.cancelSignal // AbortSignal for cooperative cancellationctx.isTimedOut() // check if timeout exceededctx.isCancelled() // check if cancellation signalledctx.CreatedAt // when the request startedctx.TimeoutAt // when it should be killed (*time.Time, nil if no timeout)ctx.Context() // underlying context.Context for cancellationctx.IsTimedOut() // check if timeout exceededctx.IsCancelled() // check if cancellation signalledStreaming
ctx.stream # if set, primitives push tokens here as they are producedctx.stream // if set, primitives push tokens here as they are produced// Go uses the standard context.Context pattern for streaming.// Pass a StreamSink via ctx.Metadata or a custom wrapper.Creating a Context
from nerva import ExecContext
# Minimalctx = ExecContext.create(user_id="user_123")
# Full controlctx = ExecContext.create( user_id="user_123", session_id="session_abc", permissions=my_permissions, memory_scope="session", timeout_seconds=30,)import { ExecContext } from "nerva";
// Minimalconst ctx = ExecContext.create({ userId: "user_123" });
// Full controlconst ctx = ExecContext.create({ userId: "user_123", sessionId: "session_abc", permissions: myPermissions, memoryScope: "session", timeoutSeconds: 30,});import nctx "github.com/otomus/nerva/go/context"
// Minimalctx := nctx.NewContext(nctx.WithUserID("user_123"))
// Full controlctx := nctx.NewContext( nctx.WithUserID("user_123"), nctx.WithSessionID("session_abc"), nctx.WithPermissions(myPermissions), nctx.WithMemoryScope(nctx.ScopeSession), nctx.WithTimeout(30),)Who Uses It
Every primitive receives ExecContext and uses the parts it needs:
| Primitive | What it reads from ExecContext |
|---|---|
| Router | permissions — permission-aware handler selection |
| Runtime | timeout_at, cancelled, spans — lifecycle and tracing |
| Tools | permissions — which tools this user/agent can call |
| Memory | memory_scope, session_id, user_id — scope isolation |
| Responder | stream — streaming vs batch mode |
| Registry | permissions — filtered discovery |
| Policy | token_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)// Express — map JWT user to Nerva contextapp.post("/chat", async (req, res) => { const user = await getCurrentUser(req); const ctx = ExecContext.create({ userId: user.id, sessionId: req.body.sessionId, permissions: permissionsFromUser(user), }); const result = await orchestrator.handle(req.body.message, ctx); res.json(result);});// net/http — map JWT user to Nerva contextfunc chatHandler(w http.ResponseWriter, r *http.Request) { user := getCurrentUser(r) ctx := nctx.NewContext( nctx.WithUserID(user.ID), nctx.WithSessionID(req.SessionID), nctx.WithPermissions(permissionsFromUser(user)), ) result, err := orchestrator.Handle(ctx, req.Message) json.NewEncoder(w).Encode(result)}The contrib helpers (nerva.contrib.fastapi, nerva/contrib/express, nerva/contrib/nestjs) automate this bridge.