Skip to Content
ConceptsRun vs Session vs RunContext

Run vs Session vs RunContext

Three terms, three different scopes. Getting them straight makes the rest of the API obvious.

Run

A Run is one agent.run() (or agent.stream()) call. It has:

  • An auto-generated id (a ULID), exposed to tools as get_run_context().run_id.
  • A start time, end time, and aggregate Usage.
  • A position in the runtime journal (used for replay).
  • A RunResult at the end with output, turns, tokens_in, tokens_out, cost_usd, session_id, interrupted, interruption_reason.

You don’t manage the run id. The framework generates one per call and threads it through RunContext.

Session

A Session is a long-lived conversation that may span many runs. It’s keyed by session_id. A string you choose:

await agent.run("Hi, I'm Alice.", session_id="conv_42", user_id="alice") await agent.run("What did I just say?", session_id="conv_42", user_id="alice") # → "You said you're Alice."

Same session_id = the framework rehydrates the prior turns from memory (Episode store) into the message stack. Different session_id = a fresh conversation.

For resumability (restart the process and pick up where you left off), combine session_id with a journaled Runtime:

agent = Agent("...", model="...", runtime=SqliteRuntime("./journal.db")) # First attempt: result = await agent.run("complex task", session_id="task-2026-05-08") # Process crashed. Later: result = await agent.resume("complex task", session_id="task-2026-05-08")

Without a checkpoint, agent.resume(prompt, session_id=...) runs agent.run(prompt, session_id=session_id) against the session’s last run. The runtime’s journal keys each step on the session and user, the run, the step name and its inputs; it replays cached results and executes only un-completed steps. A run that finished is never replayed into a later run on the same session (see Runs and epochs).

RunContext

A RunContext is the read-only bundle of scope a Run carries:

@dataclass class RunContext: run_id: str session_id: str | None user_id: str | None metadata: dict[str, Any]

It lives in a Python contextvar so anything inside a tool dispatch can read it without explicit plumbing:

from loomflow import get_run_context, tool @tool async def fetch_orders() -> str: ctx = get_run_context() return await db.query("orders", user_id=ctx.user_id)

get_run_context() returns the live RunContext for the active run. Called outside a run (a unit test, a direct tool call), it returns an empty RunContext (user_id and session_id are None) rather than raising.

Three scopes, three lifetimes

RunSessionRunContext
Created whenagent.run() is calledFirst agent.run(session_id=...) with that idAt the start of every agent.run()
Lives untilThe call returnsYou delete the dataThe call returns
Identified byrun_id (auto)session_id (user-chosen)both
Visible to toolsvia RunContextvia RunContext.session_idvia get_run_context()
Persistentonly via runtime journalyes (Memory backend)no

What user_id is, exactly

user_id is a kwarg on agent.run() and a field on RunContext. The framework forwards it to:

  • Memory: Every recall and write partitions by user_id.
  • Budget, StandardBudget tracks per-user totals.
  • Audit log: Every entry carries user_id as a top-level field.
  • Permissions, permissions.check(call, user_id=...).
  • Runtime journal and checkpoints: stored per (session_id, user_id), so another user who reuses a session id never replays this user’s tool results or resumes their transcript. Pass the same user_id to agent.resume() and agent.list_checkpoints().
  • Telemetry: loom.workflow.step spans carry user_id. Metrics don’t, since per-user series are unbounded; the audit log gives per-user attribution.
  • Tools, get_run_context().user_id.

You set it once on the agent.run() call; the framework propagates everywhere. No manual plumbing through tool signatures.

Why a contextvar, not a tool argument? Tools shouldn’t have to re-declare scope on every signature. Threading user_id through ten tools’ arguments is boilerplate that invites namespace-leak bugs. The contextvar scopes every tool dispatch automatically.

Last updated on