Agent Substrate

5 · Memory & History

Two distinct concepts — both live in L2 but serve different purposes:

MemoryHistory
What it storesArbitrary key-value session stateOrdered ChatMessage conversation log
Who reads itAgent business logic (ctx.session_store.get_state())InMemoryHistoryProvider / compaction inside ReActAgent
Kernel ProtocolSessionStore (kernel/storage/memory.py)HistoryProvider (kernel/storage/history.py)
ScopePer-session dictPer-agent × per-session list

Memory backends

diagram
Rendering diagram…

RedisSessionStore

Short-term, volatile session state. Stored as a Redis HASH (session:state:<session_id>). Each field is a JSON-serialised value.

python
from substrate.capabilities.memory import RedisSessionStore

store = RedisSessionStore(redis_url="redis://localhost:6379/0", ttl=3600)
await store.connect()

await store.update_state("sess-123", {"preferred_language": "Python", "step": 3})
state = await store.get_state("sess-123")   # → {"preferred_language": "Python", "step": 3}
await store.set_state("sess-123", {})       # overwrite entire state
await store.clear("sess-123")              # delete key
await store.disconnect()
OperationRedis commandNotes
get_stateHGETALLReturns full dict
set_stateDEL + HSET + EXPIREAtomic via pipeline
update_stateHSET + EXPIREMerges (patch, not replace)
clearDELRemoves key entirely

DurableMemoryStore

Long-term, searchable memory. Stored in an agent_memories table with a generated tsvector column for full-text search — no embeddings required.

python
from substrate.capabilities.memory import DurableMemoryStore

store = DurableMemoryStore(database_url="postgresql+asyncpg://...")
async with store:
    mem_id = await store.save(agent_id, "User prefers Python over JavaScript")
    memories = await store.search(agent_id, "language preference", limit=5)
    await store.delete(agent_id, mem_id)

Schema (auto-created by create_tables()):

sql
CREATE TABLE agent_memories (
    id          TEXT PRIMARY KEY,
    agent_name  TEXT NOT NULL,
    content     TEXT NOT NULL,
    metadata    JSONB NOT NULL DEFAULT '{}',
    namespace   VARCHAR(255) NOT NULL DEFAULT 'default',
    search_vec  TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON agent_memories USING GIN (search_vec);

History backends

diagram
Rendering diagram…

RedisHistoryProvider

Redis LIST (ravi:hist:{agent_type}:{agent_key}:{session_id}). Each element is JSON: {"run_id": "...", "msg": <ChatMessage dict>}.

Key behaviors:

  • max_messages capLTRIM on every write keeps the list bounded (default 200)
  • TTL refreshEXPIRE is reset on every append and refresh_ttl()
  • clear_run — can selectively delete messages from a specific run without clearing the whole session
python
from substrate.capabilities.history import RedisHistoryProvider

provider = RedisHistoryProvider(
    redis_url="redis://localhost:6379/0",
    ttl=3600,
    max_messages=200,
)
await provider.connect()
# Pass to agent via context config

DurableHistoryProvider

SQLAlchemy 2.0 async ORM. Two tables:

  • history_sessions — one row per (agent_type, agent_key, session_id) tuple, tracks created/updated timestamps and message count
  • history_messages — one row per message, JSONB payload, ordered by sequence number

All queries are fully parameterised — no raw SQL string interpolation. Raw session_id values are validated at the public boundary (regex check) before being composed into the internal agent_type:agent_key:session_id key.

python
from substrate.capabilities.history import DurableHistoryProvider

provider = DurableHistoryProvider(
    database_url="postgresql+asyncpg://postgres:postgres@localhost/agentdb",
)
await provider.connect()

Choosing a backend

NeedUse
Fast, ephemeral session stateRedisSessionStore
Durable long-term agent memory with searchDurableMemoryStore
Dev/testing (no infra)InMemoryHistoryProvider
Production chat history, restartableDurableHistoryProvider
High-throughput, tolerate loss on restartRedisHistoryProvider

Wiring

Both providers are injected at lifespan and passed through ContextConfig:

python
# In lifespan
history = RedisHistoryProvider(redis_url=settings.REDIS_URL)
await history.connect()
app.state.history_provider = history

# In agent factory
context = ContextConfig(history_provider=app.state.history_provider)
agent = ReActAgent(llm_client, context=context)