Session Store Protocol

Swap session backends with a unified protocol interface

The SessionStoreProtocol defines the methods any session backend must implement — swap JSON files, Redis, or your own store without changing agent code.

python
from praisonaiagents import Agent

agent = Agent(
    name="Assistant",
    memory={"session_id": "chat-123"},
)
agent.start("Hello!")

The user plugs in a custom session backend; agents call the same protocol methods regardless of storage.

Diagram

🤖 Agent

SessionStoreProtocol

💬 Bot

📁 JSON Files

⚡ Redis

🍃 MongoDB

🔧 Your Store

How It Works

Diagram
Session Store ProtocolAgentUserSession Store ProtocolAgentUserRequestProcess requestResult Agent–>>User: Response

Quick Start

  1. Use the built-in JSON store (zero config)
    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="Assistant",
        memory={"session_id": "chat-123"}
    )
    agent.start("Hello!")

    Sessions are automatically persisted to ~/.praisonai/sessions/chat-123.json.

  2. Swap to a custom store
    python
    from praisonaiagents.session import SessionStoreProtocol
    
    class RedisSessionStore:
        """Your custom Redis-backed session store."""
    
        def add_message(self, session_id, role, content, metadata=None):
            # Save to Redis
            ...
            return True
    
        def get_chat_history(self, session_id, max_messages=None):
            # Load from Redis
            return [{"role": "user", "content": "Hi"}]
    
        def clear_session(self, session_id):
            return True
    
        def delete_session(self, session_id):
            return True
    
        def session_exists(self, session_id):
            return False
    
    # Verify at runtime
    store = RedisSessionStore()
    assert isinstance(store, SessionStoreProtocol)  # ✅ True

Protocol Methods

The protocol requires exactly five methods:

MethodReturnsPurpose
add_message(session_id, role, content, metadata)boolStore a single message
get_chat_history(session_id, max_messages)list[dict]Retrieve messages in LLM format
clear_session(session_id)boolRemove all messages (keep metadata)
delete_session(session_id)boolDelete session completely
session_exists(session_id)boolCheck if a session exists
set_chat_history(session_id, messages)boolRecommended. Atomically replace chat history for idempotent saves
update_session_metadata(session_id, **fields)boolMerge metadata fields without touching messages
get_working_history(session_id, max_messages=None)list[dict]Optional. Compacted resume path — summary + tail when a checkpoint exists, else raw history
upsert_partial_assistant_message(session_id, content, *, finalize=False)boolOptional. Create or overwrite the single in-progress assistant turn so an interrupted stream resumes with its partial answer
discard_partial_assistant_message(session_id)boolOptional. Drop a trailing in-progress assistant turn, if any

set_chat_history (Recommended)

set_chat_history(session_id, messages) (recommended). Atomically replaces the chat history for a session. If your store omits this method, Session.save_state() falls back to add_message() per message and may produce duplicates when called repeatedly. The built-in DefaultSessionStore implements this with reload-under-lock atomic-write semantics.

python
class MyStore:
    def set_chat_history(self, session_id: str, messages: list[dict]) -> bool:
        """Replace the chat history for a session atomically.

        Args:
            session_id: Session identifier
            messages: list of {"role": str, "content": str}

        Returns:
            True if successful
        """
        with self._lock:
            self._data[session_id] = list(messages)
            self._flush()
        return True

update_session_metadata() API

The update_session_metadata() method enables safe metadata updates across processes without touching messages:

Parameters

ParameterTypeDescription
session_idstrUnique session identifier
**fieldsAnyMetadata fields to merge (e.g., model, total_tokens, cost)

Reserved Metadata Fields

The SDK uses these reserved fields internally:

FieldTypeDescriptionAuto-populated
modelstrLLM model name (e.g., “gpt-4o-mini”)✅
total_tokensintCumulative input + output tokens✅
costfloatEstimated USD cost✅
agent_idstrGateway or registry agent ID✅
sourcestrOrigin: “chat”, “gateway”, “cli”, “api”✅

Example Usage

python
from praisonaiagents.session import get_default_session_store

store = get_default_session_store()

# Merge metadata into an existing session without touching messages
store.update_session_metadata(
    "user-123-chat",
    model="gpt-4o-mini",
    total_tokens=125,
    cost=0.0032,
    source="chat",
    agent_id="assistant-001",
)

# None values are skipped
store.update_session_metadata(
    "user-123-chat",
    cost=None,  # Ignored, doesn't overwrite existing cost
    custom_field="my value"
)

Concurrency Safety

In DefaultSessionStore, update_session_metadata() reloads under a cross-process file lock so concurrent metadata updates and message appends are both preserved.

Built-in Implementations

Using with Bots

Bots use the same SessionStoreProtocol for persistent per-user sessions:

python
from praisonaiagents.session import get_default_session_store

# Bot sessions persist automatically
from praisonai.bots import Bot

bot = Bot(
    "telegram",
    agent=my_agent,
    session_store=get_default_session_store(),
)

Each user gets a deterministic session key like bot_telegram_12345, stored in the same ~/.praisonai/sessions/ directory as agent sessions.

Diagram

/chat

/chat

👤 User A

💬 Telegram Bot

👤 User B

SessionStoreProtocol

📁 bot_telegram_userA.json

📁 bot_telegram_userB.json

Building a Custom Store

Full example: In-memory store for testing
python
from typing import Any, Dict, List, Optional

class InMemorySessionStore:
    """Fast in-memory store for unit tests."""

    def __init__(self):
        self._data: Dict[str, List[Dict]] = {}

    def add_message(
        self, session_id: str, role: str, content: str,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> bool:
        self._data.setdefault(session_id, []).append(
            {"role": role, "content": content}
        )
        return True

    def get_chat_history(
        self, session_id: str, max_messages: Optional[int] = None,
    ) -> List[Dict[str, str]]:
        msgs = self._data.get(session_id, [])
        return msgs[-max_messages:] if max_messages else list(msgs)

    def clear_session(self, session_id: str) -> bool:
        self._data[session_id] = []
        return True

    def delete_session(self, session_id: str) -> bool:
        self._data.pop(session_id, None)
        return True

    def session_exists(self, session_id: str) -> bool:
        return session_id in self._data
Verifying protocol conformance
python
from praisonaiagents.session import SessionStoreProtocol

store = InMemorySessionStore()

# Runtime check — no registration needed
assert isinstance(store, SessionStoreProtocol)

# Use it anywhere a SessionStoreProtocol is expected
store.add_message("test", "user", "Hello")
history = store.get_chat_history("test")
assert history == [{"role": "user", "content": "Hello"}]

Checkpoint Query Protocol

For session stores that support checkpoints and rollback functionality:

python
from praisonaiagents.session.protocols import CheckpointQueryProtocol

class CheckpointEnabledStore:
    """Session store with checkpoint support."""

    def list_checkpoints(self, session_id: str) -> List[Dict[str, Any]]:
        """Return checkpoint metadata for a session."""
        return [
            {
                "checkpoint_id": "checkpoint_001",
                "created_at": "2026-01-01T12:00:00Z",
                "message_count": 10,
                "description": "Before complex task"
            }
        ]

    def get_checkpoint(self, session_id: str, checkpoint_id: str) -> Optional[Dict[str, Any]]:
        """Return full checkpoint data."""
        return {
            "checkpoint_id": checkpoint_id,
            "session_id": session_id,
            "messages": [...],  # Full message history at checkpoint
            "metadata": {...}
        }

# Verify checkpoint protocol
store = CheckpointEnabledStore()
assert isinstance(store, CheckpointQueryProtocol)

Checkpoint Usage

python
from praisonaiagents.session import get_hierarchical_session_store

store = get_hierarchical_session_store()

snapshot_id = store.create_snapshot("session-123", label="Before complex task")
store.revert_to_snapshot("session-123", snapshot_id)

Portable Store Protocol

PortableSessionStoreProtocol adds store-agnostic backup / migrate / restore alongside SessionStoreProtocol, SearchableSessionStoreProtocol, and SessionMirrorProtocol. DefaultSessionStore and SqliteSessionStore implement it.

MethodSignatureReturns
export_session(session_id, *, include_lineage=True)dict — {"version": 1, "sessions": [...]}
export_all()dict — {"version": 1, "sessions": [...]}
import_sessions(payload, *, max_sessions=10_000, reset_live_fields=True, overwrite=False)ImportReport
python
from praisonaiagents.session import DefaultSessionStore, PortableSessionStoreProtocol, ImportReport

store = DefaultSessionStore()
assert isinstance(store, PortableSessionStoreProtocol)  # runtime_checkable

payload = store.export_all()
report: ImportReport = store.import_sessions(payload)

ImportReport is JSON-serialisable via as_dict():

json
{
  "imported": 3,
  "skipped": [{"session_id": "s1", "reason": "already exists (use overwrite)"}],
  "skipped_count": 1,
  "version": 1
}

PortableSessionStoreProtocol is @runtime_checkable, so isinstance(store, PortableSessionStoreProtocol) verifies conformance. See Gateway Session Portability for the operator-facing story.


Incremental Session Store Protocol

IncrementalSessionStoreProtocol is an optional protocol for incremental streamed-turn persistence.

Without it, an interrupted stream loses the whole in-progress turn — praisonai run --continue / --session <id> resumed from the last completed turn. With it, the agent flushes the growing partial assistant turn as tokens arrive, so resume shows whatever was produced before the interrupt. DefaultSessionStore, SqliteSessionStore, and EncryptedSessionStore implement it.

MethodSignatureReturns
upsert_partial_assistant_message(session_id, content, *, finalize=False)bool
discard_partial_assistant_message(session_id)bool
python
from praisonaiagents.session import (
    DefaultSessionStore,
    IncrementalSessionStoreProtocol,
)

store = DefaultSessionStore()
assert isinstance(store, IncrementalSessionStoreProtocol)  # runtime_checkable

store.upsert_partial_assistant_message("s1", "Hello")
store.upsert_partial_assistant_message("s1", "Hello world")
store.upsert_partial_assistant_message("s1", "Hello world", finalize=True)

Best Practices

Implement set_chat_history for idempotent saves

Without it, repeated Session.save_state() calls may duplicate messages via the add_message fallback.

Reload shared storage on every read

Redis, Postgres, and multi-worker file stores must not serve stale in-process caches.

Use runtime isinstance checks

SessionStoreProtocol is @runtime_checkable — verify custom stores with isinstance(store, SessionStoreProtocol).


Related