symfonic.memory.protocols¶
protocols ¶
Protocol interfaces defining the stable public contract.
Architecture: BaseMemoryStore is the UNIFORM protocol. Every memory layer MUST implement all five methods (retrieve, write, summarize, link, delete). Methods that are not meaningful for a given layer return no-op results: - retrieve -> empty list - write -> no-op - summarize -> empty string - link -> no-op - delete -> no-op
Layers also expose layer-specific domain methods (store_fact, query_events, push, set_trigger, etc.) which are the rich API used by MemoryOrchestrator.commit_pending via layer-type dispatch.
Backend protocols (GraphBackend, VectorBackend, EmbeddingProvider) define the storage abstraction that concrete backends implement.
BaseMemoryStore ¶
Bases: Protocol
Uniform protocol that every memory layer must implement.
This is the adapter interface. Layers that do not support a given operation return no-op results rather than raising NotImplementedError.
delete
async
¶
Delete a memory entry/node from this layer.
No-op if deletion is not meaningful for this layer.
link
async
¶
Create a relationship link between two nodes.
No-op if linking is not meaningful for this layer.
render_for_prompt ¶
render_for_prompt(entry: Any, scrubber: Callable[[dict[str, Any]], dict[str, Any]] | None = None) -> str
Render a single retrieved entry for the MEMORY_CONTEXT block.
v7.7 (adopter procedural-render-bypass follow-up). Each layer
owns its prompt-render contract so the engine's hydrate loop
becomes polymorphic dispatch (one line, no schema-baked key
enumeration). Layers that don't need a layer-specific shape
delegate to :func:symfonic.memory.render.render_legacy --
that helper preserves the pre-v7.7 [layer] content (k=v)
byte-identical output so prompt-cache parity holds for
adopters who haven't opted into any new metadata surface.
scrubber is the agent-bound credential-scrubber callable
(SymfonicAgent._scrub_props). Layers MUST thread it into
any properties rendering so v6.1.9 F2 credential-scrubbing
defense-in-depth holds through every render path. None
when the agent has no credential pattern configured.
Returns the fully-rendered line including the leading
[<layer>] tag. The engine assembles the final
MEMORY_CONTEXT by joining all per-entry lines with \n.
Source code in symfonic/memory/protocols.py
retrieve
async
¶
Retrieve memory entries matching the query.
Returns an empty list if retrieval is not meaningful for this layer.
summarize
async
¶
Produce a summary of the given entries.
Returns an empty string if summarization is not meaningful.
write
async
¶
Write a memory entry to this layer.
No-op if writing is not meaningful for this layer.
EmbeddingProvider ¶
GraphBackend ¶
Bases: Protocol
Protocol for graph node and edge persistence.
All methods enforce tenant isolation via TenantScope. Provides 9 core operations for full graph lifecycle management.
add_edge
async
¶
add_node
async
¶
delete_edge
async
¶
delete_node
async
¶
Delete a node. When cascade=True, also removes all connected edges.
delete_subtree
async
¶
Erase every node at scope and below it. Returns the node count.
The descendant direction, and the only method here that runs it.
Every read above is prefix-visibility-scoped: it answers "what can a
query at P see?", which returns P's ancestors. Nothing enumerates
what lies under a scope, so MemoryLifecyclePort.forget — which must
erase a scope and its descendants — had no call to make.
One backend-native operation, never enumerate-then-delete. Two
calls leave a window in which a concurrent write lands in a scope that
was discovered but not yet swept, and that window is open during
precisely the operation that must not miss a row: a tenant deletion
while other work is still draining. Backends express it as a single
statement (see :mod:symfonic.memory.subtree for the shared
predicate); the in-memory backend does it inside one synchronous span.
Connected edges go with the nodes, as delete_node(cascade=True)
does — an erased memory that left its edges behind would keep a
traversable stub of what was supposed to be gone.
Source code in symfonic/memory/protocols_graph.py
get_neighbors
async
¶
Get neighboring nodes, optionally filtered by relationship type.
get_node
async
¶
query_edges
async
¶
query_edges(scope: TenantScope, filters: dict[str, Any] | None = None, limit: int = 50, offset: int = 0) -> list[Any]
Query edges with optional filters and pagination.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant isolation scope. |
required |
filters
|
dict[str, Any] | None
|
Optional filter dict. Supported key: |
None
|
limit
|
int
|
Maximum number of edges to return. |
50
|
offset
|
int
|
Number of edges to skip (for pagination). |
0
|
Returns:
| Type | Description |
|---|---|
list[Any]
|
List of |
Source code in symfonic/memory/protocols_graph.py
query_nodes
async
¶
Query nodes by filters (layer, label, properties, etc.).
limit=None returns all matching nodes (no truncation).
Source code in symfonic/memory/protocols_graph.py
query_subtree
async
¶
Query nodes at scope and below it. The descendant read.
The mirror of :meth:query_nodes, which is prefix-visibility-scoped
and so returns a query's ancestors. Both directions are needed and
neither substitutes for the other: retrieval asks "what may this scope
see?", while the lifecycle verbs ask "what did this scope and its
children produce?" — flush(scope) must publish a pending write made
in a descendant session, and no ancestor-scoped read can find one.
Same filter vocabulary as :meth:query_nodes; limit=None returns
every match.
Source code in symfonic/memory/protocols_graph.py
traverse
async
¶
update_node
async
¶
Partial update of node properties. Returns the updated node.
upsert_edge
async
¶
Insert edge or increment weight if it already exists.
An edge is considered a duplicate when (tenant_id, source, target, relationship) all match. On conflict the weight is incremented by 1; no duplicate row is created and the edge id is preserved from the first insert.
Source code in symfonic/memory/protocols_graph.py
VectorBackend ¶
Bases: Protocol
Protocol for vector storage and similarity search.
add
async
¶
add(scope: TenantScope, ids: list[str], embeddings: list[list[float]], metadatas: list[dict[str, Any]], documents: list[str]) -> None
Add vectors with metadata to the store.
count
async
¶
delete
async
¶
delete_subtree
async
¶
search
async
¶
Search for the top_k most similar vectors.
Returns list of dicts with keys: id, score, metadata, document.