Skip to content

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 shutdown

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.

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.

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()

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.