Python API¶
nervapack.memory is a plain Python library — no server, no MCP, no agent framework required. Import MemoryStore and recall and you have a persistent memory layer in three lines.
Core imports¶
from nervapack.memory import (
MemoryStore, # The database — store, retrieve, manage nodes
recall, # Scored, budget-capped recall returning Markdown
recall_timeline, # Chronological trace of a topic
)
MemoryStore¶
MemoryStore is the low-level interface. It manages the SQLite database, schema, nodes, edges, sessions, and namespaces.
Initialisation¶
from nervapack.memory import MemoryStore
# Auto-resolves to .nervapack/memory.db (walks up from cwd, then ~/.nervapack/)
store = MemoryStore()
# Explicit path
store = MemoryStore(db_path="/path/to/memory.db")
# Named namespace (isolates all reads and writes)
store = MemoryStore(namespace="project_b")
# Override via environment variable (useful in production / testing)
# export NERVAPACK_MEMORY_DB=/shared/memory.db
store = MemoryStore()
add_node — store a memory¶
node_id = store.add_node(
kind="decision", # fact | decision | action | outcome | entity | procedure | preference
content="Chose JWT for auth — stateless horizontal scaling",
confidence=0.9, # 0.0–1.0, default 1.0
session_id=sid, # link to a session node (optional)
valid_from=None, # ISO-8601 or None (defaults to now)
data={ # arbitrary JSON metadata
"rationale": "No session store needed",
"alternatives_rejected": ["session cookies", "opaque tokens"],
},
)
# Returns e.g. "d_0019f2b3c4d5e6f7a8b9c0d1e2f3a4b5"
Node kind prefixes:
| Kind | Prefix | Use for |
|---|---|---|
session |
s_ |
A unit of work (task, conversation, request) |
fact |
f_ |
Observed truth |
decision |
d_ |
A choice made, with optional rationale |
action |
a_ |
Something that was done |
outcome |
o_ |
Result of an action or decision |
entity |
e_ |
Named service, component, user, or concept |
procedure |
p_ |
Repeatable steps |
preference |
pr_ |
A standing preference or convention |
get_node¶
node = store.get_node("d_0019f2...")
# Returns a sqlite3.Row (dict-like) or None
print(node["content"])
print(node["confidence"])
print(node["recorded_at"])
add_edge — link nodes¶
store.add_edge(
src="d_0019f2...", # source node id
dst="e_0019f3...", # destination node id
kind="ABOUT", # ABOUT | OCCURRED_IN | SUPERSEDES | CONTRADICTS | CAUSED | DERIVED_FROM | TOUCHES
)
tombstone / purge¶
# Soft-delete (recoverable — sets tombstoned=1, excluded from recall)
count = store.tombstone(["f_0019f2...", "f_0019f3..."])
# Hard-delete (irreversible — removes row, FTS entry, edges)
count = store.purge(["f_0019f2..."])
Sessions¶
from nervapack.memory.store import _now_iso
# Open a session
sid = store.add_node("session", "JWT auth refactor",
data={"started_at": _now_iso(), "agent_id": "my_bot"})
# List all sessions
sessions = store.list_sessions(limit=20)
for s in sessions:
print(s["id"], s["content"], s["node_count"])
# Close a session (set valid_until)
store.close_session(sid)
# Delete a session and all its nodes
result = store.delete_session(sid, purge=False) # purge=True for hard delete
Namespaces¶
# All nodes written to store are tagged with store.namespace
store = MemoryStore(namespace="chatbot_user_42")
store.add_node("fact", "User prefers concise answers")
# Switch namespace (all subsequent reads/writes use the new namespace)
store.namespace = "chatbot_user_99"
# List all namespaces in the database
s = store.stats()
print(s["namespaces"]) # ["default", "chatbot_user_42", "chatbot_user_99"]
stats¶
s = store.stats()
print(s["kind_counts"]) # {"fact": 14, "decision": 5, ...}
print(s["db_size_bytes"]) # 49152
print(s["namespaces"]) # ["default"]
print(s["top_entities"]) # [{"id": "e_...", "content": "auth_service", "degree": 6}]
FTS search¶
results = store.fts_search("JWT authentication", limit=10, kinds=["decision", "fact"])
for r in results:
print(r["id"], r["content"])
recall — scored, budget-capped retrieval¶
recall runs the full pipeline: FTS5 search → graph expansion → temporal mask → scoring → budget packing.
from nervapack.memory import recall
text = recall(
store,
query="authentication design",
budget_tokens=300, # hard limit on output length, default 500
kinds=None, # filter to specific kinds, e.g. ["fact", "decision"]
as_of=None, # ISO-8601 for point-in-time recall
hops=1, # graph expansion depth (0–2)
min_confidence=0.0, # exclude nodes below this threshold
)
print(text)
Return value: always a Markdown string, always ≤ budget_tokens. Safe to inject directly into any prompt.
Scoring formula:
score = relevance × recency × frequency × connectivity
relevance = normalised BM25 rank (0–1)
recency = exp(−ln2 × age_days / 30) ← 30-day half-life
frequency = 1 + 0.1 × ln(1 + access_count)
connectivity= 1 + 0.1 × ln(1 + node_degree)
After packing, access_count and last_accessed are incremented on every returned node.
recall_timeline¶
Chronological trace of all nodes matching a topic, including superseded versions:
from nervapack.memory import recall_timeline
text = recall_timeline(
store,
topic="auth_service",
since=None, # ISO-8601 lower bound on recorded_at
budget_tokens=1000,
)
print(text)
Output:
## Memory timeline: 'auth_service'
- [d_0019f1...] 2026-01-01 · conf 1.00 [superseded by d_0019f2...] — Chose session cookies for auth_service
- [d_0019f2...] 2026-07-03 · conf 0.90 — Chose JWT over session cookies for auth_service
Entity resolution helper¶
Entity names are normalised so AuthService, auth_service, and auth-service resolve to the same node. Use resolve_entities when building your own store pipeline:
from nervapack.memory import resolve_entities
linked_ids, created_ids = resolve_entities(store, ["AuthService", "payment_service"])
for entity_id in linked_ids:
store.add_edge(node_id, entity_id, "ABOUT")
Complete example — chatbot memory¶
from nervapack.memory import MemoryStore, recall
class ChatMemory:
def __init__(self, user_id: str):
self.store = MemoryStore(namespace=f"user_{user_id}")
def remember(self, content: str, kind: str = "fact") -> str:
return self.store.add_node(kind, content)
def context(self, topic: str, tokens: int = 300) -> str:
return recall(self.store, topic, budget_tokens=tokens)
mem = ChatMemory("alice")
mem.remember("User prefers concise answers under 3 sentences", kind="preference")
mem.remember("User is building a FastAPI app with PostgreSQL", kind="fact")
mem.remember("User wants to avoid ORMs — prefers raw asyncpg", kind="preference")
# At the start of the next conversation turn:
context = mem.context("what the user is building")
print(context)
# → "## Memory recall … User is building a FastAPI app …"
See the full chatbot guide for a multi-turn conversation example: Chatbot memory.
See Also¶
- Chatbot use case — full multi-turn chatbot with message history and preference recall
- Concepts & data model — bi-temporal schema, FTS5, edge kinds
- MCP Tools — use the same store from Claude Code / Cursor without writing Python
- CLI — inspect, export, and manage the store from the command line