Skip to content

Sessions

Sessions let you persist an agent's conversation history and state across process restarts, serverless invocations, or separate user requests.

Each session manager is bound to a session_id (the conversation bucket). Each Agent has an agent_id (the agent within that session). For a single assistant per user, use the user's id as session_id and a fixed name like "assistant" as agent_id.

File-based sessions

Store sessions on the local filesystem:

python
from elsai import Agent
from elsai.agent import AgentConfig
from elsai.session import FileSessionManager

session_manager = FileSessionManager(
    session_id="user-alice",
    storage_dir="./sessions",
)

agent = Agent(
    config=AgentConfig(
        agent_id="assistant",
        session_manager=session_manager,
    ),
)

agent("My name is Alice.")
# session saved to ./sessions/session_user-alice/agents/agent_assistant/

# Later (even in a new process):
session_manager = FileSessionManager(
    session_id="user-alice",
    storage_dir="./sessions",
)
agent2 = Agent(
    config=AgentConfig(
        agent_id="assistant",
        session_manager=session_manager,
    ),
)
result = agent2("What is my name?")
print(result)  # → "Your name is Alice."

S3-based sessions

Store sessions in Amazon S3 for serverless or distributed deployments:

python
from elsai import Agent
from elsai.agent import AgentConfig
from elsai.session import S3SessionManager

session_manager = S3SessionManager(
    session_id="user-bob",
    bucket="my-agent-sessions",
    prefix="sessions/",
    region_name="us-west-2",
)

agent = Agent(
    config=AgentConfig(
        agent_id="assistant",
        session_manager=session_manager,
    ),
)

Repository-based sessions

For database-backed sessions, implement SessionRepository and pass it to RepositorySessionManager:

python
from elsai import Agent
from elsai.agent import AgentConfig
from elsai.session import RepositorySessionManager, SessionRepository
from elsai.session import Session, SessionAgent, SessionMessage

class PostgresSessionRepository(SessionRepository):
    def create_session(self, session: Session, **kwargs) -> Session:
        ...

    def read_session(self, session_id: str, **kwargs) -> Session | None:
        ...

    def create_agent(self, session_id: str, session_agent: SessionAgent, **kwargs) -> None:
        ...

    def read_agent(self, session_id: str, agent_id: str, **kwargs) -> SessionAgent | None:
        ...

    def update_agent(self, session_id: str, session_agent: SessionAgent, **kwargs) -> None:
        ...

    def create_message(
        self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs
    ) -> None:
        ...

    def read_message(
        self, session_id: str, agent_id: str, message_id: int, **kwargs
    ) -> SessionMessage | None:
        ...

    def update_message(
        self, session_id: str, agent_id: str, session_message: SessionMessage, **kwargs
    ) -> None:
        ...

    def list_messages(
        self, session_id: str, agent_id: str, limit: int | None = None, offset: int = 0, **kwargs
    ) -> list[SessionMessage]:
        ...

manager = RepositorySessionManager(
    session_id="user-123",
    session_repository=PostgresSessionRepository(),
)
agent = Agent(config=AgentConfig(agent_id="assistant", session_manager=manager))

File storage structure

When using FileSessionManager, sessions are stored in this directory layout:

<storage_dir>/
└── session_<session_id>/
    ├── session.json                  ← session metadata & timestamps
    ├── agents/
    │   └── agent_<agent_id>/
    │       ├── agent.json            ← agent state & configuration
    │       └── messages/
    │           ├── message_<id>.json ← individual conversation messages
    │           └── message_<id>.json
    └── multi_agents/
        └── multi_agent_<id>/
            └── multi_agent.json      ← orchestrator state & node history

Each message_<id>.json holds a single conversation turn:

json
{
  "role": "user",
  "content": [{ "text": "What is 2 + 2?" }],
  "created_at": "2025-06-01T12:00:00Z"
}

S3 storage structure

S3SessionManager mirrors the same hierarchy in S3:

<prefix>/
└── session_<session_id>/
    ├── session.json
    ├── agents/
    │   └── agent_<agent_id>/
    │       ├── agent.json
    │       └── messages/
    │           └── message_<id>.json
    └── multi_agents/
        └── multi_agent_<id>/
            └── multi_agent.json

Required IAM permissions:

json
{
  "Effect": "Allow",
  "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"],
  "Resource": ["arn:aws:s3:::my-bucket/*", "arn:aws:s3:::my-bucket"]
}

SessionManager interface

The SessionManager is a plugin — it registers hooks to persist agent state after each message and on initialization.

ConceptDescription
session_idSet on the session manager — identifies the conversation bucket
agent_idSet on the agent — identifies the agent within the session
storage_dir / bucketBackend location on FileSessionManager / S3SessionManager

What gets persisted

By default sessions save:

  • Conversation messages (agent.messages)
  • Agent state (agent.state)
  • Conversation manager state

Snapshots (in-memory)

Take a point-in-time snapshot without a session manager:

python
# Save state
snapshot = agent.take_snapshot(preset="session")

# Serialise if needed
import json
snapshot_dict = snapshot.model_dump()

# Restore later
from elsai.agent import Snapshot

agent2 = Agent(config=AgentConfig(agent_id="user-alice"))
agent2.load_snapshot(Snapshot(**snapshot_dict))

Snapshot fields

FieldDescription
messagesFull conversation history
stateAgent state key-value pairs
conversation_manager_stateConversation manager internals
interrupt_stateInterrupt/resume state
system_promptSystem prompt content blocks
python
# Selective snapshot
snapshot = agent.take_snapshot(include=["messages", "state"])
snapshot = agent.take_snapshot(exclude=["interrupt_state"])

Custom data in snapshots

python
snapshot = agent.take_snapshot(
    preset="session",
    app_data={"user_tier": "premium", "feature_flags": ["beta_tools"]},
)
print(snapshot.app_data["user_tier"])  # "premium"

Multi-user example

python
from elsai import Agent
from elsai.agent import AgentConfig
from elsai.session import FileSessionManager

def get_agent_for_user(user_id: str) -> Agent:
    session_manager = FileSessionManager(
        session_id=user_id,
        storage_dir="./sessions",
    )
    return Agent(
        system_prompt="You are a personal assistant for this user.",
        config=AgentConfig(
            agent_id="assistant",
            session_manager=session_manager,
        ),
    )

# Each user gets their own isolated conversation
alice_agent = get_agent_for_user("alice")
bob_agent = get_agent_for_user("bob")

alice_agent("My favourite colour is blue.")
bob_agent("I love Python programming.")

# Later:
alice_agent2 = get_agent_for_user("alice")
print(alice_agent2("What is my favourite colour?"))  # blue

A2A sessions

When you expose an agent via A2AServer, each incoming A2A request carries a context_id. Elsai treats that ID as the conversation bucket:

ConceptA2A mapping
session_idEquals the A2A context_id
agent_idYour agent's configured agent_id (for example "assistant")
Session storageOne isolated session per context_id
Tool statePer-context directories under $ELSAI_A2A_SESSION_DIR/<context_id>/

By default, A2AServer creates a FileSessionManager per context:

$ELSAI_A2A_SESSION_DIR/<context_id>/
├── session.json
├── agents/agent_<agent_id>/messages/…
└── repl/, shell/, browser/, …   ← tool state when those tools are used

Set ELSAI_A2A_SESSION_DIR to a persistent volume in production (EBS, EFS, etc.). Conversation history and on-disk tool state survive process restarts and LRU eviction of in-memory agents.

File-backed A2A sessions

Override the default storage root with session_manager_factory:

python
from elsai.multiagent.a2a import A2AServer
from elsai.session import FileSessionManager

server = A2AServer(
    agent=template_agent,
    session_manager_factory=lambda ctx_id: FileSessionManager(
        session_id=ctx_id,
        storage_dir="/data/a2a-sessions",
    ),
)

Each context_id gets its own subdirectory under /data/a2a-sessions/session_<context_id>/ following the file storage structure above.

S3-backed A2A sessions (production)

For distributed or serverless deployments on AWS, persist each context's conversation history in S3:

python
import os
from elsai.multiagent.a2a import A2AServer
from elsai.session import S3SessionManager

def s3_session_for_context(context_id: str) -> S3SessionManager:
    return S3SessionManager(
        session_id=context_id,
        bucket=os.environ["A2A_SESSIONS_BUCKET"],
        prefix=os.getenv("A2A_SESSIONS_PREFIX", "a2a/contexts/"),
        region_name=os.getenv("AWS_REGION", "us-east-1"),
    )

server = A2AServer(
    agent=template_agent,
    session_manager_factory=s3_session_for_context,
    max_active_contexts=200,
    strict_tool_policy=True,
)

S3 keys follow the same hierarchy as S3 storage structure — one session_<context_id>/ prefix per A2A conversation. Tool state files (REPL, shell, browser profiles, etc.) remain on the server filesystem under $ELSAI_A2A_SESSION_DIR unless your tools use external stores.

See Agent-to-Agent (A2A) for agent factory mode, LRU pooling, stateful tool policy, the full production S3 example, and environment variables.

Copyright © 2026 elsai foundry.