Concepts
Sessions and runs
A session is a conversation with an agent. A run is one turn inside it. A session outlives any single run, and that is the whole distinction.
Why they are separate
An agent needs to remember what was said three messages ago, and it also needs to be billed, traced and retried one message at a time. One object cannot be both the thing that accumulates history and the thing that starts and ends. So the session holds the transcript, and each run is addressed by its own invocation id.
A conversation lives where the agent framework wrote it: your agent's own database. Reading one back reads it there, so what you get is exactly what the agent saw rather than a copy some other service kept. You do not have to create a session first — a session id is yours to choose, anything stable such as sess-ada-1, and invoking with one that does not exist creates it. Create one explicitly when it should start with state already in place, or carry a title for a list of conversations.
Whose conversation it is
A conversation is addressed by agent, person and session id. The same id under a different agent is a different conversation, and so is the same id for a different person.
Without a user, every session call acts on the caller's own conversations. For a person signed in to the console that is what they expect. For a backend's service credential it is usually nothing: a backend's conversations belong to the customers it invoked for, not to the machine user. So user names one of your end users by your own id — ada and eu:ada mean the same person — the same value you pass as end_user_id when invoking. Naming one needs a tenant admin, on the session routes and on the invoke alike.
Create and invoke for the same person, or you have made two conversations
A backend that creates a session as itself and then invokes with end_user_id: "ada" has created one conversation owned by the machine user and run a turn in a different one owned by Ada. The state you seeded is in the first; the agent reads the second. Nothing errors. Pass the same user to the create.
The state keys you may write
Session state is a set of keys the agent can read, and the framework scopes it by key prefix. That prefix decides what a caller may write:
| Prefix | Shared by | Writable by a caller |
|---|---|---|
| none | this conversation only | yes |
| app: | every user of the agent | no — one caller would change everybody's conversations |
| user: | every conversation of one user | no — it would reach a conversation other than this one |
| temp: | nothing; never persisted | no — accepting it would promise something that is discarded |
| agentkit: | the platform's own records | no — a stored view must not be forgeable |
A refused key is a 400 naming the key, and the serialized state of one write is capped at 64 KiB, because state is replayed into every turn's context. A file belongs in an artifact, not in state. The same rule is checked where you can see the refusal and again in the runtime, because by the time an invocation reaches a pod it is data from a queue.
Starting state
State given when a session is created is written before the first turn, so the agent reads it on its very first model call. An id already in use is refused with a conflict rather than reused — creating over an existing conversation would hand somebody's history your starting state. A title given at creation is stored with the conversation, kept to one line and cut to 120 characters, and comes back on a listing rather than in its state.
State on one turn
state_delta on an invocation writes keys on that run's own user message, so the agent reads them from its first model call and they stay in the conversation afterwards. Same prefix rule, same cap. One more refusal applies to a workflow: a key that is also one of its declared launch inputs is refused, because either side silently winning would make an input mean something other than what its form showed. That check needs the workflow's manifest, which only the runtime holds, so it arrives as an error event on the stream rather than as a 400. Pass such a value in inputs.
Bounding one run
run_config accepts two fields of the framework's run configuration and refuses the rest by name. max_llm_calls can only tighten a run: the lower of it and the agent's own limit applies, because that limit is the author's bound on what a run may cost. custom_metadata, an object of up to 64 KiB, is merged into every event the run emits — live and stored — so a request id put there is on every frame you read back. Streaming partial events is refused, because it would multiply what is published and billed per run.
An agent deployed before these options existed cannot read them
An invocation carrying state_delta or run_config reaches an older agent image as a message it cannot parse, and it is dead-lettered — your socket sees queued and then nothing. One that carries neither is unchanged. Redeploy the agent once.
What a run goes through
- Queued. The invocation is published. If the agent is scaled to zero this is where the cold start happens.
- Started. A pod picked it up; the event carries the message that started the run.
- Paused, if the run asks a person something — an approval, or an input a workflow's graph needs.
- Finished or error, if it did not pause. One of them ends every run that ran to completion or failed.
A paused run does not finish
A pause is where the run stopped, deliberately — a human can outlast any delivery deadline, so the answer arrives as a new message and the run continues under the same invocation id. A client waiting for finished on a paused run waits out its whole timeout. The last paused event carries an interrupt_id, and that key is how you tell a run that stopped from one still going. The invocation lifecycle covers what happens between these steps.
Reading a conversation back
A conversation's history comes back as the same frames the live stream sends, oldest first, so the code that renders the socket renders history unchanged. A person's own message is a started event, each run ends the way it ended live, and a view an agent showed comes back where it appeared. Reopen a chat from that, not by stitching per-run reads: a run that paused for an afternoon's approval would otherwise come back positioned where it started.
What history cannot show
queued,typingandaction_requested. They are the platform's own live signals rather than events the framework records, so they were never stored.segmentandtarget_version, which are empty on every stored frame.- How a run ended, when the platform stopped it. This is the record of the conversation. A run the platform refused or failed around the agent — a refused payment, a refused option — may read as finished here, or not appear at all. A run's own record,
GET /v1/runs/{invocation_id}, is how it ended and what it cost.
Deleting a conversation removes it and every event in it, with no undo. The runs survive it — they were billed and audited and stay in the run history — so deleting a conversation is not a way to unbill a run. An admin deleting one of their users' conversations is how erasure on a customer's request is done.
What it costs
Each session call is metered after it succeeds, so a refusal costs nothing. Listing, reading state, reading events and reading a workflow's run view are reads; creating and deleting a conversation are writes, and so is every accepted invocation and every answer to a paused workflow, since each appends a turn. Page a long conversation with the newest-N read rather than fetching it whole on every render. The bytes a conversation occupies are billed separately, as the agent's session storage — measured per database, which is never quite empty.
Where to go next
The sessions API has every route, its paging and its refusals, including which ones a backend can use on a customer's behalf. The sessions SDK wraps them, and the runtime SDK handles the subscribe-then-invoke ordering that is easy to get wrong. Agents and workflows covers what pauses a run in the first place, and why a resumed one re-emits events you have already seen.