Skip to Content
Core ConceptsSessions and Memory

Sessions and Memory

A session is the history one conversation’s model calls are built from. Naming one — run and stream take an optional session_id — is what decides whether a turn remembers the ones before it:

from agentdeck import Agent greeter = Agent(name="Greeter", instructions="You are a friendly scheduling assistant. Keep replies to one short sentence.")
import asyncio from agentdeck import Deck async def main() -> None: async with Deck.from_project() as deck: await deck.run("Greeter", "my name is Sagi", session_id="wa-1") # A peek at the session's own history, to show it grew — not how a consumer # normally reads a conversation back; see "Reading a session" below. first_turn = await deck.session_for("wa-1").get_items() print("items after turn 1:", len(first_turn)) await deck.run("Greeter", "what is my name?", session_id="wa-1") second_turn = await deck.session_for("wa-1").get_items() print("items after turn 2:", len(second_turn)) first_item = second_turn[0] print("turn 1 is still there:", first_item["role"], first_item["content"]) asyncio.run(main())
items after turn 1: 2 items after turn 2: 4 turn 1 is still there: user my name is Sagi

By the second call, the session already holds the first turn’s user message and reply — that is what “my name is Sagi” being present at second_turn[0] proves. A call with no session_id gets none of this: each one is a one-shot with no session and no memory of the last one.

Where a session lives

deck.session_for(session_id) is a thin lookup, not a store of its own: AGENTDECK_SESSION set to a redis:// URL means every session is a RedisSession sharing one Redis client, reachable from any worker and surviving a restart; unset, each session_id gets one in-process SQLite session, held in the Deck instance that created it — gone the moment that process exits, invisible to any other worker. AGENTDECK_SESSION_REDIS_KEY_PREFIX and AGENTDECK_SESSION_REDIS_TTL are the Redis-only knobs. FR-6’s promise — a conversation surviving a process restart — needs Redis configured; the fallback is a dev convenience, not a second durable option. Full table: Settings → SessionSettings.

The same session_id names one conversation everywhere it is used — over HTTP (POST /agents/{name}/chat), from the Python API, or from an agent that hands off to a peer — so a caller does not need to know which door a conversation started at to keep talking to it.

Not the event log

This session is what a run’s engine reads to answer the next turn. It is not the platform’s record of the run, which is a different store entirely: see Runs and the Event Log for what the log keeps instead and why the two are not derived from one another. In short: the log is what every consumer — replay, audit, a dashboard — is allowed to read; this session is private to the engine that owns it, and it is the only thing that actually feeds the model. get_items() above is a peek for demonstration, not a documented read API — a real consumer reads the log.

One turn at a time

A session holds one turn in flight. Starting a second one against the same session_id before the first finishes does not queue or interleave it — it is refused outright:

from agentdeck import SessionBusyError try: await deck.run("Greeter", "another message, same session", session_id="wa-1") except SessionBusyError as busy: ... # "session 'wa-1' already has run '<run_id>' in flight, so run '<run_id>' cannot start on it"

The refusal is the log deciding, not a lock the caller has to manage: opening a run is a conditional write that fails if the session already has one open, so exactly one caller ever wins, whether the two calls came from the same process or two different ones. Over HTTP the same refusal arrives as 409. A run that dies without closing cleanly (a killed worker, not a graceful exit) still frees the session eventually — once it has gone quiet for AGENTDECK_RUNTIME_STALE_RUN_AFTER_SECONDS, described in Run Control.

Last updated on