Observability
Sessions
A session is the top-level unit of agent execution in Niitaka. Every time your agent runs — from the first API call to the final output — one session captures the full trace: goals, costs, events, errors, and outcome metrics.
Session lifecycle
A session starts running and ends in one of four terminal statuses.
running
- Session open
- Events streaming in
- Cost accumulating
done
- Context manager exited clean
- Metrics finalised
- ended_at stamped
error
- Explicit failure status
- ended_at stamped
- Not authoritative — use error_count
stopped
- Explicit abort
- ended_at stamped
stale
- No heartbeat for 5 min
- Auto-closed by server
- ended_at stamped
status = 'error' is real and reachable (set explicitly via PATCH /sessions/{id}) — but it's not authoritative for detecting failed runs. error_count isn't a stored column either; it's derived at query time from events where type='error'. Always detect failures via error_count > 0, never by filtering on session status.Key fields
running while active, then one terminal status: done (clean exit) · error (explicit failure) · stopped (explicit abort) · stale (no heartbeat for 5 minutes). Not authoritative for error detection — see the callout above.null means never explicitly closed — the dashboard falls back to an estimated duration from the last event, shown with a "≈" prefix.Duration: real vs. estimated
Duration prefers ended_at - created_at when the session has actually reached a terminal status — authoritative, server-stamped. If ended_at is still null (abandoned or crashed without ever closing, or genuinely still running), the dashboard falls back to the last event's timestamp instead and shows it with a ≈ prefix so an estimate is never confused with a real closed-session duration.
Starting a session
Use the start_session() context manager. The session is opened on entry and closed automatically on exit — even if the agent raises an exception.
import niitaka
with niitaka.start_session(
goal="Summarise the weekly sales report",
agent_id="report-summariser",
) as session:
result = run_agent(report_text)
session.set_metrics({
"goal_completed": result.success,
"score": result.quality, # float 0–1
})Pre-creating a parent session
For a chatbot or multi-turn workflow, use create_session() to establish a stable parent session before individual turns start, instead of wrapping the whole conversation in one long-lived context manager. Fire-and-forget and a no-op when unconfigured.
# For a chatbot/multi-turn workflow, establish a stable parent before
# individual turns start. Fire-and-forget, idempotent, safe to repeat:
niitaka.create_session(
session_id=chat_id,
goal=f"Chat with {user_name}",
agent_id="my-chatbot",
tags={"user_id": user_id},
)
# Each turn is a child session
with niitaka.start_session(
goal=user_message[:200],
agent_id="my-chatbot",
parent_session_id=chat_id,
tags={"user_id": user_id, "chat_id": chat_id},
) as session:
response = run_agent(user_message)Child sessions
Pass parent_session_id to link a sub-session to its parent. The Dashboard renders these as a collapsible tree so you can trace multi-agent orchestration flows end-to-end.
# Top-level session
with niitaka.start_session(
goal="Process customer support ticket",
agent_id="support-router",
) as parent:
intent = classify_intent(ticket)
# Sub-session linked to the parent
with niitaka.start_session(
goal="Draft reply for billing query",
agent_id="reply-drafter",
parent_session_id=parent.session_id,
) as child:
reply = draft_reply(ticket, intent)
child.set_metrics({"goal_completed": reply is not None})Detecting errors programmatically
# status == 'error' is real and reachable, but it's not authoritative for
# error detection -- error_count isn't even a stored column, it's derived
# at query time from the events table. Check error_count, not status:
sessions = niitaka_api.list_sessions(agent_id="report-summariser")
failed = [s for s in sessions if s["error_count"] > 0]Tool calls
Every instrumentation path (OpenAI, Anthropic, Gemini, Groq, LiteLLM, LangChain) normalizes a model's tool/function-call decision to one canonical shape. A pure tool-call turn legitimately has empty content — the dashboard renders tool_calls as explicit → name(args) blocks instead of showing what used to look like an empty response:
{
"role": "assistant",
"content": "", // legitimately empty for a pure tool-call turn
"tool_calls": [
{
"id": "call_abc123",
"name": "get_weather",
"arguments": { "city": "Tokyo" } // always a parsed dict, never a raw JSON string
}
]
}Sessions dashboard
The Sessions page gives you a real-time view of all runs in your organisation.
Total Sessions
Count for the selected date range.
Running
Sessions currently open (live count).
Total Cost
Summed LLM spend across all sessions.
Unique Agents
Distinct agent IDs seen in the range.
Filtering and search
- Date range — presets for last 24 h, 7 d, 30 d, or a custom range.
- Status — filter to running, done, error, stopped, or stale sessions.
- Agent — scope the list to a single agent ID.
- Search — full-text match against the session goal.
Tree view
Sessions with parent_session_id set are rendered as a collapsible subtree under their parent. Expand a row to see all child sessions without leaving the list. Depth is limited to prevent runaway recursion in deeply nested orchestrators.
Session drawer
Click any row to open the session drawer. It shows the event timeline, cost breakdown, signals fired during the run, and the decision traces from the policy engine.
Export
Use the Export button to download the current filtered view as CSV. Useful for offline analysis or sharing a slice of sessions with your team.
Live updates
The session list streams updates in real time via Server-Sent Events (SSE). New sessions appear at the top, and running sessions update their cost and status without a page refresh.
Next steps
- Agent Dashboard — monitor agent health and manage lifecycle across all agents.
- Signals & Alerts — see what the policy engine emitted during each session.
- Experiments — attach sessions to A/B experiments and compare variants.