5 · Memory & History
Two distinct concepts — both live in L2 but serve different purposes:
| Memory | History | |
|---|---|---|
| What it stores | Arbitrary key-value session state | Ordered ChatMessage conversation log |
| Who reads it | Agent business logic (ctx.session_store.get_state()) | InMemoryHistoryProvider / compaction inside ReActAgent |
| Kernel Protocol | SessionStore (kernel/storage/memory.py) | HistoryProvider (kernel/storage/history.py) |
| Scope | Per-session dict | Per-agent × per-session list |
Memory backends
RedisSessionStore
Short-term, volatile session state. Stored as a Redis HASH (session:state:<session_id>). Each field is a JSON-serialised value.
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()| Operation | Redis command | Notes |
|---|---|---|
get_state | HGETALL | Returns full dict |
set_state | DEL + HSET + EXPIRE | Atomic via pipeline |
update_state | HSET + EXPIRE | Merges (patch, not replace) |
clear | DEL | Removes 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.
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()):
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
RedisHistoryProvider
Redis LIST (ravi:hist:{agent_type}:{agent_key}:{session_id}). Each element is JSON: {"run_id": "...", "msg": <ChatMessage dict>}.
Key behaviors:
max_messagescap —LTRIMon every write keeps the list bounded (default 200)- TTL refresh —
EXPIREis reset on everyappendandrefresh_ttl() clear_run— can selectively delete messages from a specific run without clearing the whole session
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 configDurableHistoryProvider
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 counthistory_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.
from substrate.capabilities.history import DurableHistoryProvider
provider = DurableHistoryProvider(
database_url="postgresql+asyncpg://postgres:postgres@localhost/agentdb",
)
await provider.connect()Choosing a backend
| Need | Use |
|---|---|
| Fast, ephemeral session state | RedisSessionStore |
| Durable long-term agent memory with search | DurableMemoryStore |
| Dev/testing (no infra) | InMemoryHistoryProvider |
| Production chat history, restartable | DurableHistoryProvider |
| High-throughput, tolerate loss on restart | RedisHistoryProvider |
Wiring
Both providers are injected at lifespan and passed through ContextConfig:
# 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)