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
RunResultat the end withoutput,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
| Run | Session | RunContext | |
|---|---|---|---|
| Created when | agent.run() is called | First agent.run(session_id=...) with that id | At the start of every agent.run() |
| Lives until | The call returns | You delete the data | The call returns |
| Identified by | run_id (auto) | session_id (user-chosen) | both |
| Visible to tools | via RunContext | via RunContext.session_id | via get_run_context() |
| Persistent | only via runtime journal | yes (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,
StandardBudgettracks per-user totals. - Audit log: Every entry carries
user_idas 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 sameuser_idtoagent.resume()andagent.list_checkpoints(). - Telemetry:
loom.workflow.stepspans carryuser_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.