Skip to main content

Graph Overlay for Agent Memory

SochDB provides a lightweight graph layer on top of its KV storage for modeling agent memory relationships. This is NOT a full graph database - it's optimized for typical agent memory patterns.

Per-SDK surface differs

The graph layer is exposed differently in each SDK:

  • Rust ships a real GraphOverlay type (sochdb::graph) generic over the connection, plus a TemporalGraphOverlay (sochdb::temporal_graph).
  • Node.js exposes graph methods directly on EmbeddedDatabase (re-exported as Database): addNode / addEdge / traverse.
  • Go exposes graph methods on the IPC client and the gRPC client (AddNode / AddEdge / Traverse); the embedded engine is behind the sochdb_embedded build tag.
  • Python exposes graph methods directly on Database (add_node / add_edge / traverse / get_neighbors / find_path) plus temporal edges (add_temporal_edge). There is no importable GraphOverlay class in the Python SDK — the standalone "graph overlay" demos live in the sochdb-python-examples repo, not in the installed package.

SDK versions: Core engine 2.0.3, Python SDK 0.5.9, Node.js SDK 0.5.3, Go SDK 0.4.5.

Overview​

The Graph Overlay enables:

  • Entity relationships: user to conversation to message
  • Causal chains: action1 to action2 to action3
  • Reference graphs: document referenced by citation referenced by quote

Storage Model​

Data TypeKey PatternValue
Nodes_graph/{ns}/nodes/{id}{type, properties}
Edges_graph/{ns}/edges/{from}/{type}/{to}{properties}
Reverse Index_graph/{ns}/index/{type}/{to}/{from}from_id

This enables O(1) node/edge operations and O(degree) traversals.

Quick Start​

In the Python SDK (pip install sochdb, v0.5.9) the graph operations are methods on Database. The first argument is always the graph namespace, and node/edge IDs are plain strings.

from sochdb import Database

db = Database.open("./agent_memory")
ns = "agent_001"

# Create nodes: add_node(namespace, node_id, node_type, properties?)
db.add_node(ns, "user_1", "User", {"name": "Alice"})
db.add_node(ns, "conv_1", "Conversation", {"title": "Planning Session"})
db.add_node(ns, "msg_1", "Message", {"content": "Let's start planning"})

# Create edges: add_edge(namespace, from_id, edge_type, to_id, properties?)
db.add_edge(ns, "user_1", "STARTED", "conv_1")
db.add_edge(ns, "conv_1", "CONTAINS", "msg_1")
db.add_edge(ns, "user_1", "SENT", "msg_1")

# Traverse: returns (nodes, edges) as lists of dicts
nodes, edges = db.traverse(ns, "user_1", max_depth=2, order="bfs")
for node in nodes:
print(f"Node: {node['id']} ({node['node_type']})")

# Shortest path: find_path(from, to, max_depth=10, namespace="default")
result = db.find_path("user_1", "msg_1", max_depth=10, namespace=ns)
if result:
print(result["path"]) # e.g. ["user_1", "conv_1", "msg_1"]
note

Property values passed to add_node / add_edge are serialized to JSON; use string keys and JSON-serializable values.

Node Operations​

These operations are exposed on the Rust GraphOverlay. The other SDKs expose a subset (add_node / add_edge / traverse and, in Python, delete_node / get_neighbors / find_path) as methods on the connection.

OperationDescriptionComplexity
add_node(id, type, props)Create or update nodeO(1)
get_node(id)Retrieve node by IDO(1)
update_node(id, props, type?)Update properties or typeO(1)
delete_node(id, cascade)Delete node (optionally with edges)O(degree)
node_exists(id)Check if node existsO(1)

Edge Operations​

OperationDescriptionComplexity
add_edge(from, type, to, props)Create directed edgeO(1)
get_edge(from, type, to)Get specific edgeO(1)
get_edges(from, type?)Get outgoing edgesO(degree)
get_incoming_edges(to, type?)Get incoming edgesO(degree)
delete_edge(from, type, to)Delete edgeO(1)

Traversal Operations​

Where these live

bfs, dfs, shortest_path, get_neighbors, get_nodes_by_type, and get_subgraph are first-class methods on the Rust GraphOverlay. In Python, use traverse(...) (which returns (nodes, edges)) plus the get_neighbors(...) and find_path(...) helpers on Database. In Node.js and Go, only traverse / Traverse is provided — walk the returned nodes/edges yourself.

BFS / DFS (Rust)​

// Find all reachable nodes within 3 hops
let nodes = graph.bfs(&user, 3, None, None)?;

// Filter by edge types
let nodes = graph.bfs(&user, 3, Some(&["SENT", "CONTAINS"]), None)?;

// Filter by node types
let nodes = graph.bfs(&user, 3, None, Some(&["Message"]))?;

// Depth-first traversal
let nodes = graph.dfs(&user, 5, None, None)?;

Traverse (Python)​

# Breadth-first traversal returns (nodes, edges)
nodes, edges = db.traverse("agent_001", "user_1", max_depth=3, order="bfs")

# Depth-first traversal
nodes, edges = db.traverse("agent_001", "user_1", max_depth=5, order="dfs")

Shortest Path​

# find_path(from, to, max_depth=10, namespace="default")
result = db.find_path("user_1", "msg_10", max_depth=10, namespace="agent_001")
# result is {"path": [...], "edges": [...]} or None if unreachable

Query Operations​

Get Neighbors​

# get_neighbors(node_id, direction="outgoing", edge_type=None, namespace="default")
out = db.get_neighbors("user_1", direction="outgoing", namespace="agent_001")
incoming = db.get_neighbors("msg_1", direction="incoming", namespace="agent_001")
both = db.get_neighbors("conv_1", direction="both", namespace="agent_001")
filtered = db.get_neighbors("user_1", edge_type="STARTED", namespace="agent_001")
# Each returns a dict like {"neighbors": [...]}

Get Nodes by Type (Rust)​

// Get all User nodes (scans, use sparingly)
let users = graph.get_nodes_by_type("User", 100)?;

Get Subgraph (Rust)​

// Extract a subgraph around a node
let subgraph = graph.get_subgraph(&user, 2)?;
println!("Nodes: {}", subgraph.nodes.len());
println!("Edges: {}", subgraph.edges.len());

Temporal Edges​

For time-travel queries ("what did the system know at time T?"), edges can carry a validity interval.

  • Rust: TemporalGraphOverlay (sochdb::temporal_graph) with add_edge_at, invalidate_edge_at, get_edges_at, get_edges_in_window, neighbors_at, and subgraph_at.
  • Python: db.add_temporal_edge(...), db.query_temporal_graph(...), and db.end_temporal_edge(...) on Database.
import time

now = int(time.time() * 1000)
one_hour = 60 * 60 * 1000

# valid_from / valid_until are Unix epoch milliseconds (0 = no expiry)
db.add_temporal_edge(
namespace="smart_home",
from_id="door_front",
edge_type="STATE",
to_id="open",
valid_from=now - one_hour,
valid_until=now,
properties={"sensor": "motion_1"},
)

# modes: "CURRENT" | "POINT_IN_TIME" | "RANGE"
edges = db.query_temporal_graph(
namespace="smart_home",
node_id="door_front",
mode="POINT_IN_TIME",
timestamp=now - 30 * 60 * 1000,
)

Agent Memory Patterns​

The examples below use the Python Database graph API. The first argument is the namespace ("agent_001" here).

Conversation History​

ns = "agent_001"
# Model a conversation thread
db.add_node(ns, "conv_1", "Conversation", {"title": "Support Chat"})
db.add_node(ns, "msg_1", "Message", {"role": "user", "content": "Help!"})
db.add_node(ns, "msg_2", "Message", {"role": "assistant", "content": "I can help"})

db.add_edge(ns, "conv_1", "CONTAINS", "msg_1")
db.add_edge(ns, "conv_1", "CONTAINS", "msg_2")
db.add_edge(ns, "msg_1", "FOLLOWED_BY", "msg_2")

# Walk the conversation
nodes, edges = db.traverse(ns, "conv_1", max_depth=2)

Tool Call Chains​

ns = "agent_001"
# Model tool execution sequences
db.add_node(ns, "action_1", "ToolCall", {"tool": "search", "query": "docs"})
db.add_node(ns, "action_2", "ToolCall", {"tool": "read_file", "path": "README.md"})
db.add_node(ns, "action_3", "ToolCall", {"tool": "summarize", "input": "..."})

db.add_edge(ns, "action_1", "CAUSED", "action_2")
db.add_edge(ns, "action_2", "CAUSED", "action_3")

# Walk the causal chain
nodes, edges = db.traverse(ns, "action_1", max_depth=10, order="dfs")

Knowledge References​

ns = "agent_001"
# Model document references
db.add_node(ns, "doc_1", "Document", {"title": "API Guide"})
db.add_node(ns, "chunk_1", "Chunk", {"text": "Authentication uses..."})
db.add_node(ns, "chunk_2", "Chunk", {"text": "Rate limits are..."})

db.add_edge(ns, "doc_1", "CONTAINS", "chunk_1")
db.add_edge(ns, "doc_1", "CONTAINS", "chunk_2")
db.add_edge(ns, "chunk_2", "REFERENCES", "chunk_1")

# Find chunks reachable from the document
nodes, edges = db.traverse(ns, "doc_1", max_depth=1)

Performance Characteristics​

OperationTime ComplexityNotes
Add/Get NodeO(1)Direct KV lookup
Add/Get EdgeO(1)Direct KV lookup
Outgoing EdgesO(degree)Prefix scan
Incoming EdgesO(degree)Reverse index lookup
BFS/DFSO(V + E)For reachable subgraph
Shortest PathO(V + E)BFS-based

Best Practices​

  1. Use meaningful edge types: SENT, CONTAINS, REFERENCES are clearer than generic RELATES_TO
  2. Namespace by agent: Use separate namespaces for each agent's memory
  3. Limit traversal depth: Set reasonable max_depth to avoid runaway queries
  4. Use cascade delete carefully: In Rust, delete_node(id, true) removes all connected edges
  5. Filter early: In Rust traversals, use edge_types and node_types to reduce work

See Also​