symfonic.memory.graph.store¶
store ¶
GraphMemoryStore -- node and edge persistence with tenant isolation.
Delegates all storage operations to a GraphBackend protocol implementation. Provides query convenience methods and access-count tracking for scoring.
GraphMemoryStore ¶
GraphMemoryStore(backend: GraphBackend, embedding_provider: EmbeddingProvider | None = None, embedding_cache: EmbeddingCache | None = None)
Bases: GraphQueryMixin
Graph-aware memory store wrapping a GraphBackend.
All operations enforce tenant isolation via TenantScope. Access counts are automatically incremented on reads to support frequency-based retrieval scoring.
v7.3 Item 13.1: optional auto-embed at the store level.
The graph-backed memory LAYERS (semantic / procedural /
prospective) wrap their own writes through
:func:~symfonic.memory.embeddings.auto_embed.maybe_embed so
layer-API callers benefit even when constructing layers without
the orchestrator. Consolidation phases that bypass the layers
(Item 12 EntityLinker calls GraphMemoryStore.add_node
directly) need the same auto-embed pathway -- that's what the
constructor kwargs here are for.
The double wrap (layer -> store) is idempotent: maybe_embed
short-circuits when node.embedding is already populated, so
a node that went through SemanticLayer.store_fact only
triggers one provider call regardless of how many layers of
wrapping it traverses.
Both kwargs default to None so legacy callers that build
GraphMemoryStore directly are byte-identical to v7.2.
Source code in src/symfonic/memory/graph/store.py
backend
property
¶
What this store persists through. Named, so callers stop reaching.
add_edge
async
¶
Add an edge to the graph. Returns the persisted edge.
add_node
async
¶
Add a node to the graph. Returns the persisted node.
v7.3 Item 13.1: when an embedding_provider is wired into
this store, nodes the caller left with embedding=None
auto-embed before the backend write. Caller-provided embeddings
always win (see :func:maybe_embed).
Source code in src/symfonic/memory/graph/store.py
bump_spreading
async
¶
bump_spreading(scope: TenantScope, node_id: NodeId, *, include_retracted: bool = False) -> MemoryNode | None
Fetch a node and increment its spreading_access_count.
v6.2 T02: BFS-induced reads funnel through this helper instead
of :meth:get_node, so the Phase 1 recurrence signal can
distinguish direct fetches from one-shot spreading-activation
visits. Both counters carry independent monotonic semantics:
- access_count grows on get_node
- spreading_access_count grows on bump_spreading
The method is intentionally named bump_spreading (rather than
get_node_via_spreading or a private _bump_spreading) so
alternative traversal strategies in future sprints can call it
directly without reaching into private API.
Returns None if the node does not exist or belongs to another tenant; otherwise returns the updated node.
Soft-retracted nodes return None by default (and are not bumped) so
a retracted false memory cannot re-enter context via spreading
activation -- notably as a live node's neighbor. Pass
include_retracted=True only for audit/maintenance reads.
Source code in src/symfonic/memory/graph/store.py
delete_edge
async
¶
delete_node
async
¶
Delete a node and cascade-delete all connected edges.
get_neighbors
async
¶
get_neighbors(scope: TenantScope, node_id: NodeId, relationship: str | None = None, *, include_retracted: bool = False) -> list[MemoryNode]
Get neighboring nodes, optionally filtered by relationship type.
Soft-retracted neighbours are excluded by default so a retracted node
cannot re-enter context as a graph neighbour of a live node. Audit /
maintenance callers pass include_retracted=True.
Source code in src/symfonic/memory/graph/store.py
get_node
async
¶
get_node(scope: TenantScope, node_id: NodeId, *, include_retracted: bool = False) -> MemoryNode | None
Get a node by ID, incrementing its access count.
Returns None if the node does not exist or belongs to another tenant.
Soft-retracted nodes (see symfonic.memory.retraction for the
namespaced marker contract) return None by default so a
corrected/false-positive memory never re-enters context through a
direct fetch; the access count is NOT bumped for them. Audit /
erasure / prune callers pass include_retracted=True to reach them.
Source code in src/symfonic/memory/graph/store.py
list_edges
async
¶
list_edges(scope: TenantScope, *, limit: int = 50, offset: int = 0, relationship: str | None = None) -> list[MemoryEdge]
List edges for the tenant with optional filtering and pagination.
Delegates directly to GraphBackend.query_edges — no node iteration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant isolation scope. |
required |
limit
|
int
|
Maximum number of edges to return (default 50). |
50
|
offset
|
int
|
Number of edges to skip for pagination (default 0). |
0
|
relationship
|
str | None
|
If provided, only edges of this type are returned. |
None
|
Source code in src/symfonic/memory/graph/store.py
neighbor_probe
async
¶
neighbor_probe(scope: TenantScope, node_id: NodeId, minimum: int = 1, relationship: str | None = None, target: str | None = None) -> bool
Answer a threshold/existence question without loading a neighborhood.
Source code in src/symfonic/memory/graph/store.py
related_candidates
async
¶
Bounded exact-owner semantic candidates for offline comparison.
Source code in src/symfonic/memory/graph/store.py
update_node
async
¶
Partial update of node properties.
upsert_edge
async
¶
Insert edge or increment weight if it already exists.
Uniqueness key: (tenant_id, source, target, relationship). Each repeated call on the same pair increments weight by 1.
Source code in src/symfonic/memory/graph/store.py
upsert_edge_props
async
¶
Update edge properties in-place.
Attempts backend.update_edge when available; otherwise falls back to delete+re-add. Returns the updated edge, or None on failure.