Sessions
A session is a conversation with an agent: several prompts, each answered by an agent invocation of its own. Each
turn is an ordinary trace, so the trace list, the trace page and every search work on it unchanged. What ties the
turns together is the session/id attribute every one of them carries, and the session itself leaves one more
trace when its block ends: a summary that carries what belongs to the session rather than to any turn, its
reward first of all.
import metrana.tracing as tracing
tracing.init(workspace_name="my-team", project_name="agents", run_name=None) # project-anchored
def serve(session_id: str, prompts: list[str]) -> None: with tracing.session(session_id, agent="support-bot") as session: for prompt in prompts: with session.new_turn(prompt) as turn: # a SUB_AGENT trace named "session_turn" with tracing.llm_call("policy", prompt=prompt) as llm: answer = model(prompt) llm.completion(answer) turn.final_output(answer) session.set_reward(score(prompts)) # on the summary, written at the block's exit
tracing.close() # at shutdownWhat gets written
Section titled “What gets written”Inside the block, session.new_turn(prompt) is agent_trace("session_turn", task_input=prompt) with the
session’s attributes stamped on it, the way a tracing.attributes(...) block stamps them: the turn’s root, and
every span under it, carries session/id and, when given, session/agent. Turns are sampled like any root.
The turn’s root is stamped wherever it is opened. The spans under it take the names from the block, which, like
tracing.step(...), is in effect on the thread or task it was entered on: a turn served on a worker thread runs
inside the block’s context (contextvars.copy_context().run(serve_turn, prompt)) for its spans to carry the session
too.
When the block ends, the session writes one more trace under the same attributes: a childless root of kind
SESSION_SUMMARY named session_summary, whose attributes are
| Attribute | Value |
|---|---|
session/id, session/agent |
The session’s, like on the turns |
session/reward |
The last set_reward(...) value; absent when none was set |
session/turn_count |
Turns this block ran |
anything from set_attributes(...) |
The session’s own attributes, set at any point inside the block |
Its status is OK, or ERROR when an exception escaped the block. The summary is written whatever the tracer’s
sample_rate: it carries the reward, and a dropped summary would lose it where a dropped turn loses only itself.
The reward may be set at any point in the block, however many turns follow, and the last value wins. After the
block it cannot: a summary’s attributes travel with its close, like any span’s, so set_reward and set_attributes
raise MetranaTracingError once the block has ended.
Sessions across processes
Section titled “Sessions across processes”A session need not fit one block or one process. A chat backend that serves each turn in a different request, or a scorer that runs after the fact, opens another block with the same id:
with tracing.session(session_id, agent="support-bot") as session: # later, elsewhere with session.new_turn(next_prompt) as turn: ...
with tracing.session(session_id, agent="support-bot") as session: # a re-scoring: no turns at all session.set_reward(human_rating)Each block writes its own summary, so a session can have several; the newest is its current state, and a re-scoring
is simply a newer summary. The turns of the session are every session_turn trace with that id, in start order,
whichever block wrote them. Every block repeats the agent: a summary carries only what its own block gave it, so
one written without the agent is missing from the per-agent queries below, which would then report the older,
stale reward as the session’s state.
In training
Section titled “In training”On a run-anchored tracer every root carries a policy step, the summary included. Pass it to the session, which makes
the block a tracing.step(step) block too, or open the session inside one:
metrana.init(organization="my-org", workspace_name="my-team", project_name="agents", run_name="grpo-7b-v2")
with tracing.session(f"episode-{i}", agent="policy", step=step) as session: for prompt in dialogue: with session.new_turn(prompt) as turn: ... session.set_reward(episode_return)
tracing.close()metrana.close()Reading sessions back
Section titled “Reading sessions back”The turns of one session, and the summaries of an agent, are searches on the trace list; the ids themselves come
from the attribute-values listing, which returns each distinct value once. The names are spelled out below; a
reading-side client takes them as constants from metrana.query.tracing_well_known_attrs (SESSION_ID_ATTRIBUTE,
SESSION_TURN_OP_NAME, …) without importing the logging side:
from metrana.query import AttrFilter, SpanFilter, SpanOrderBy, TraceSpanKind
# The turns of one session, oldest first.client.fetch_spans( filter=(SpanFilter.op_name == "session_turn") & (AttrFilter("session/id") == session_id), order_by=SpanOrderBy.start_time.asc(),)
# The current state of every session of an agent: its newest summary carries the reward.client.fetch_spans( filter=(SpanFilter.kind == TraceSpanKind.SESSION_SUMMARY) & (AttrFilter("session/agent") == "support-bot"), attributes=["session/id", "session/reward", "session/turn_count"],)
# The sessions whose reward crossed a threshold: each id once.client.list_tracing_attribute_values( attribute_name="session/id", filter=(SpanFilter.kind == TraceSpanKind.SESSION_SUMMARY) & (AttrFilter("session/reward") >= 0.8),)See Searching traces.