from aimu.sessions import (
InMemorySessionStore,
Session,
SessionLocks,
TinyDBSessionStore,
session_key,
)
MODEL = "ollama:qwen3:8b" # any local/cloud model AIMU supports (only section C needs it)Sessions (multi-user)
ConversationManager (notebook 02) holds one conversation. When a single process serves many users or chats, e.g. an always-on assistant on multiple channels, you need per-conversation state keyed by who is talking. aimu.sessions provides that: a Session (one conversation’s history + optional memory scope + metadata) and a SessionStore that persists sessions by key.
What this notebook covers: - The SessionStore API (InMemorySessionStore) + session_key - Durable sessions with TinyDBSessionStore - Routing two users to two isolated conversations through one agent (the reset + restore + save pattern) - SessionLocks for per-session serialization
It’s a thin, sync layer (same family as
MemoryStore/ConversationManager); the async assistant loop calls it directly. Section C runs a local Ollama model; the rest is model-free.
Gotcha: put the system prompt on the client (
aio.client(system=...)), not the agent. An agent withsystem_messageset resets its client each run (_prepare_run), which would discard the history you just restored.
Setup
A - The store API
session_key(channel, sender) is the canonical key; single-user collapses to "default:default". get returns a detached snapshot (mutating it does nothing until save).
store = InMemorySessionStore()
print("single-user key:", session_key(None, None))
print("telegram key: ", session_key("telegram", "12345"))
key = session_key("telegram", "12345")
s = store.get(key) # fresh empty Session; not persisted yet
print("empty:", s.messages, "| stored keys:", store.list_keys())
s.messages.append({"role": "user", "content": "hi"})
print("not persisted until save:", store.get(key).messages)
store.save(s)
print("after save:", store.get(key).messages, "| keys:", store.list_keys())B - Durable sessions
Swap the store; everything else is unchanged. TinyDBSessionStore writes one row per session and survives restarts.
import pathlib
import tempfile
db = str(pathlib.Path(tempfile.mkdtemp()) / "sessions.json")
store = TinyDBSessionStore(db)
store.save(
Session(
key="web:default",
messages=[{"role": "user", "content": "remember the gate code is 4242"}],
metadata={"name": "Ada"},
)
)
store.close()
# A fresh store over the same file (a "restart") sees the saved session.
reopened = TinyDBSessionStore(db)
s = reopened.get("web:default")
print("restored:", s.messages)
print("metadata:", s.metadata, "| keys:", reopened.list_keys())
reopened.close()C - Routing two users through one agent
Agents never share a live messages list across sessions. Each turn loads that session’s history via restore(), runs, then snapshots back. A single agent instance serves both users; the store is the source of truth.
Gotcha: put the system prompt on the client (
aio.client(system=...)), not the agent. An agent withsystem_messageset resets its client each run (_prepare_run), which would discard the history you just restored.
from aimu import aio
# Put the system prompt on the CLIENT, not the agent. The agent's per-run _prepare_run resets the
# client when agent.system_message is set, which would wipe the history we restore below; with the
# prompt on the client and agent.system_message left None, reset() preserves it and restore survives.
agent = aio.Agent(aio.client(MODEL, system="You are concise. Answer in one short sentence."))
store = InMemorySessionStore()
locks = SessionLocks()
async def handle(channel, sender, text, *, agent=agent, store=store, locks=locks):
key = session_key(channel, sender)
async with locks(key): # serialize this session's turns
session = store.get(key)
agent.restore(session.messages) # load THIS conversation (reset + set)
reply = await agent.run(text)
session.messages = list(agent.model_client.messages) # snapshot back
store.save(session)
return reply# Two users, interleaved, sharing one agent.
print("Ada:", await handle("web", "ada", "My name is Ada. Remember it."))
print("Bob:", await handle("web", "bob", "My name is Bob. Remember it."))
# Each session recalls only its own history:
print("Ada ->", await handle("web", "ada", "What is my name?"))
print("Bob ->", await handle("web", "bob", "What is my name?"))
print("\nsessions stored:", store.list_keys())D - Concurrency and the single-user path
SessionLocks gives a lazy per-key asyncio.Lock: a single session’s turns serialize (shared message state), while different sessions run concurrently, the whole point of multi-user. Single-user needs no ceremony: session_key(None, None) is "default:default", one fixed key for the only conversation (and plain ConversationManager is also fine for that case).
This session store is the substrate for an always-on, multi-channel personal assistant: a channel adapter supplies channel + sender, and handle() above is the per-message routing core. See the Build a personal assistant and Use sessions guides.
Clean up
import shutil
shutil.rmtree(pathlib.Path(db).parent, ignore_errors=True)
del agent, store
print("Cleaned up.")