Skip to main content

Sessions

Sessions group related agent runs under a single identifier. A common use case is tracking multi-turn conversations where each user message triggers a separate agent run, but you want to analyze the entire conversation as a unit.

What Gets Tracked

When runs share a session_id, Waxell Observe automatically aggregates:

MetricDescription
run_countNumber of runs in the session
first_runTimestamp of the earliest run
last_activityTimestamp of the most recent run
total_durationCombined execution time across all runs (seconds)
total_costSum of LLM costs across all runs (USD)
total_tokensSum of tokens used across all runs
agentsList of distinct agent names that participated

Setting a Session ID

Call waxell.init() before importing your LLM SDK and pass session_id at call time. Every LLM call inside the decorated function is auto-captured and attributed to the session:

import waxell_observe as waxell

waxell.init() # BEFORE importing the LLM SDK

import openai
client = openai.OpenAI()

@waxell.observe(agent_name="chat-agent")
async def handle_message(user_message: str) -> str:
response = client.chat.completions.create( # auto-captured
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
)
return response.choices[0].message.content

# Pass session_id at call time -- groups every call with this id together
await handle_message("Hello!", session_id="session-abc123", user_id="user_456")

You can also bake session_id into the decorator if it's static for that agent: @waxell.observe(agent_name="chat-agent", session_id="session-abc123"). For real apps, call-time is more common since the session is per-conversation.

Dynamic Session IDs

Derive the session ID from your application's conversation or request context. Use generate_session_id() to mint one, or use your own identifier:

import waxell_observe as waxell
from waxell_observe import generate_session_id

# Generate a new session ID (format: sess_ + 16 hex chars)
session_id = generate_session_id()
# e.g. "sess_a1b2c3d4e5f60718"

# Or use your own identifier
session_id = f"conv-{conversation.id}"

# All runs with this session_id are grouped together
await handle_message("Hi", session_id=session_id, user_id=f"user-{user.id}")
await handle_message("Follow up", session_id=session_id, user_id=f"user-{user.id}")
info

The session_id is propagated to both the HTTP data path (stored on the AgentExecutionRun record) and the OTel tracing path (as a waxell.session_id span attribute). This means sessions are queryable from both the Waxell UI and Grafana TraceQL.

REST API

List Sessions

GET /api/v1/observability/sessions/

Authentication: Session (UI)

Query Parameters:

ParameterTypeDefaultDescription
searchstringFilter by session_id (substring match)
agentstringFilter by agent name
startISO8601Only sessions with runs after this time
endISO8601Only sessions with runs before this time
sortstring-last_activitySort field. Options: last_activity, -last_activity, first_run, -first_run, run_count, -run_count
limitint25Page size (max 100)
offsetint0Pagination offset

Example:

curl -s "https://acme.waxell.dev/api/v1/observability/sessions/?limit=10&sort=-run_count" \
-H "Cookie: sessionid=..."

Response:

{
"results": [
{
"session_id": "sess_a1b2c3d4e5f6g7h8",
"run_count": 5,
"first_run": "2026-02-07T10:00:00Z",
"last_activity": "2026-02-07T10:05:32Z",
"total_duration": 12.45,
"total_cost": 0.0234,
"total_tokens": 4520,
"agents": ["chat-agent", "retrieval-agent"]
}
],
"count": 42,
"next": "?offset=10&limit=10",
"previous": null
}

Get Session Detail

GET /api/v1/observability/sessions/{session_id}/

Authentication: Session (UI)

Returns the session's aggregate metrics and a chronological list of all runs.

Example:

curl -s "https://acme.waxell.dev/api/v1/observability/sessions/sess_a1b2c3d4e5f6g7h8/" \
-H "Cookie: sessionid=..."

Response:

{
"session_id": "sess_a1b2c3d4e5f6g7h8",
"aggregates": {
"run_count": 5,
"total_duration": 12.45,
"total_cost": 0.0234,
"total_tokens": 4520,
"agents": ["chat-agent", "retrieval-agent"]
},
"runs": [
{
"id": 101,
"agent_name": "chat-agent",
"workflow_name": "default",
"started_at": "2026-02-07T10:00:00Z",
"completed_at": "2026-02-07T10:00:02Z",
"duration": 2.1,
"status": "success",
"cost": 0.0045,
"tokens": 890,
"trace_id": "abcdef1234567890abcdef1234567890"
}
]
}

UI Walkthrough

Sessions List

The sessions list view shows all tracked sessions with sortable columns:

  • Session ID -- click to open session detail
  • Runs -- number of agent runs in the session
  • First Run / Last Activity -- time range of the session
  • Duration -- total execution time
  • Cost -- aggregated LLM spend
  • Tokens -- total token usage
  • Agents -- which agents participated

Use the search bar to filter by session ID, or the agent dropdown to see sessions for a specific agent.

Session Detail

The session detail page shows:

  1. Summary cards at the top with run count, total duration, total cost, and total tokens
  2. Vertical timeline of runs in chronological order, showing each run's agent, duration, cost, and status
  3. Click any run to navigate to its full trace detail

Multi-Agent Sessions

Sessions are especially useful for multi-agent workflows where several agents collaborate on a single request. Decorate each agent with @observe and pass the same session_id at call time:

@waxell.observe(agent_name="router")
async def classify(query: str) -> str:
...

@waxell.observe(agent_name="retrieval")
async def search(query: str) -> list:
...

@waxell.observe(agent_name="synthesizer")
async def synthesize(query: str, docs: list) -> str:
...

session_id = generate_session_id()
intent = await classify(query, session_id=session_id)
docs = await search(query, session_id=session_id)
answer = await synthesize(query, docs, session_id=session_id)

All three runs appear under the same session, giving you a complete picture of the multi-agent pipeline.

Advanced: Multiple Runs Per Function with WaxellContext

If a single function needs to spawn multiple distinct runs (e.g., a batch loop processing items, where each item should be its own run with the same session), use WaxellContext directly:

from waxell_observe import WaxellContext, generate_session_id

session_id = generate_session_id()

for item in batch:
async with WaxellContext(
agent_name="batch-processor",
session_id=session_id,
user_id="user_456",
) as ctx:
result = await process(item) # auto-captured LLM calls go here
ctx.set_result({"output": result})

For the common case of one function = one run, the decorator is simpler and recommended.

Next Steps

  • User Tracking -- Attribute sessions and costs to individual users
  • Scoring -- Attach quality scores to runs within a session
  • LLM Call Tracking -- Understand token and cost breakdown per run