Writing your own memory backend
July 31, 2026 · View on GitHub
A memory backend is an AgentContextManager: an object the persona asks to
remember turns and to assemble the next prompt. You can write one in a few dozen
lines. There are two starting points. Pick by what you are building.
- A retrieval backend (stores items, recalls them by some kind of search):
start from
BaseRetrievalMemoryand implement two small hooks. You inherit history handling, the five inject modes, the context renderer, and asearch()that lets the composite fuse you with other retrievers. - Anything else (a summary, a fact store, a custom rule): start from
AgentContextManagerand implement the three contract methods directly.
The contract
get_history(session_id) -> list[AgentMessage]
update_history(new_messages, session_id) -> None
build_conversation_context(utterance, session_id) -> list[AgentMessage]
build_conversation_context returns the messages sent to the chat model. Two
rules: the first message MAY be a system message, and the last message is
ALWAYS the current user utterance. AgentMessage, MessageRole, and ToolCall
come from ovos_plugin_manager.templates.agents.
Path A: a retrieval backend
Implement _store_document (persist one item) and _query_backend (return ranked
MemoryHits). Everything else is inherited.
from typing import Any, Dict, List
from ovos_memory_plugins.base import BaseRetrievalMemory
from ovos_memory_plugins.common import MemoryHit
class SubstringMemory(BaseRetrievalMemory):
"""Toy retriever: recalls past exchanges by substring count."""
def __init__(self, config=None):
super().__init__(config)
self._docs: Dict[str, str] = {} # doc_id -> text
def _store_document(self, doc_id: str, text: str, session_id: str,
metadata: Dict[str, Any]) -> None:
self._docs[doc_id] = text
def _query_backend(self, query: str, top_k: int) -> List[MemoryHit]:
q = query.lower()
hits = [MemoryHit(content=text, source=doc_id, score=float(text.lower().count(q)))
for doc_id, text in self._docs.items()]
hits = [h for h in hits if h.score > 0]
hits.sort(key=lambda h: h.score, reverse=True)
return hits[:top_k]
That is a complete, usable backend. From the base you get:
update_history: pairs each user-to-assistant turn into a"Q: …\nA: …"document and calls your_store_document(errors are caught, so a storage hiccup never breaks the conversation)search(query, session_id, top_k): calls_query_backend, then applies themin_scorefilter andtop_kcapbuild_conversation_context: runs the search and injects the result per the configuredinject_mode, with the user utterance last- shared config:
retrieval(max_num_results,min_score,query_mode,query_history_turns),context(chunk rendering),inject_mode,system_prompt,max_history. Add your own keys in__init__fromself.config.
Because it exposes search() returning MemoryHits, it can be a member of the
composite and be fused with other retrievers for free.
Path B: any other memory
Subclass AgentContextManager and implement the three methods.
from ovos_plugin_manager.templates.agents import (
AgentContextManager, AgentMessage, MessageRole,
)
class StickyNoteMemory(AgentContextManager):
"""Remembers anything the user says after 'remember', injects it each turn."""
def __init__(self, config=None):
super().__init__(config)
self._notes: dict = {}
def get_history(self, session_id):
return [] # this backend isn't a turn buffer
def update_history(self, new_messages, session_id):
for m in new_messages:
if m.role == MessageRole.USER and "remember" in m.content.lower():
self._notes.setdefault(session_id, []).append(m.content)
def build_conversation_context(self, utterance, session_id):
messages = []
if self.system_prompt:
messages.append(AgentMessage(MessageRole.SYSTEM, self.system_prompt))
notes = self._notes.get(session_id, [])
if notes:
messages.append(AgentMessage(
MessageRole.SYSTEM, "Notes:\n" + "\n".join(f"- {n}" for n in notes)))
messages.append(AgentMessage(MessageRole.USER, utterance)) # always last
return messages
A Path-B backend works standalone and as a context member of the composite
(its leading system block is folded in). To also be fused as a retriever, add a
search(query, session_id=None, top_k=None) -> list[MemoryHit] method.
The MemoryHit type
from ovos_memory_plugins.common import MemoryHit
MemoryHit(content="...", source="doc-42", score=0.87, metadata={"session_id": "s1"})
content is the recalled text, source an optional id, score the
retriever-native relevance (any scale, fusion handles scale differences),
metadata is free-form.
Register it
Expose the class under the opm.agents.memory entry-point group in your
package's pyproject.toml:
[project.entry-points."opm.agents.memory"]
my-cool-memory = "my_package.memory:SubstringMemory"
After installing, select it from a persona:
{
"memory_module": "my-cool-memory",
"my-cool-memory": {"system_prompt": "You are a helpful assistant."}
}
Test it
Backends are plain objects. Instantiate, feed turns, and assert on the result. For a Path-A retriever, inject your own storage so tests need no real services (the bundled backends accept stub backends through their config for exactly this).
def test_recall():
mem = SubstringMemory(config={"retrieval": {"max_num_results": 3}})
mem.update_history([
AgentMessage(MessageRole.USER, "my favourite colour is teal"),
AgentMessage(MessageRole.ASSISTANT, "noted!"),
], "s1")
ctx = mem.build_conversation_context("what colour did I like?", "s1")
assert ctx[-1].role == MessageRole.USER # contract: user last
assert any("teal" in m.content for m in ctx) # it was recalled