symfonic.memory¶
memory ¶
symfonic.memory: Production-grade, async-first, multi-layer memory library.
Provides protocol-driven graph-aware memory with tenant isolation,
multi-signal retrieval scoring, and capability routing. Memory
consolidation is handled by SleepConsolidator (see
:mod:symfonic.core.learning.consolidation).
AssembledContext ¶
Bases: BaseModel
Fully assembled context ready for LLM consumption.
.. deprecated:: AssembledContext is deprecated. Hydration is handled by SymfonicAgent._hydrate() which returns HydratedMemory.
Source code in src/symfonic/memory/models/context.py
is_within_budget ¶
Check whether this context respects all budget constraints.
Returns True if all component counts are within the budget limits.
max_memory_entries was removed in v5.6.0 (v6.0 API freeze prep);
memory-entry count is no longer budget-enforced -- the live
SymfonicAgent._hydrate path uses max_tokens instead.
Source code in src/symfonic/memory/models/context.py
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 (Jarvio 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 src/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.
CandidateNode ¶
Bases: BaseModel
Intermediate model normalizing vector and graph results before scoring.
The RetrievalEngine converts all candidate results (whether from vector search or graph traversal) into CandidateNode instances. Hits without a graph node get a synthetic MemoryNode with access_count=0 and graph_proximity=0.0.
CapabilityRouter ¶
CapabilityRouter(
procedural_layer: ProceduralLayer,
catalog: ToolCatalog,
llm: Any,
max_tools: int = 5,
*,
observability_hook: ObservabilityHook | None = None,
)
Routes user queries to the appropriate tools via LLM selection.
Combines procedural memory (learned skills) with a static tool catalog to select the minimal set of tools needed for a given query.
Source code in src/symfonic/memory/layers/procedural/router.py
route
async
¶
route(
scope: TenantScope,
query: str,
conversation_history: list[Any] | None = None,
*,
callback_manager: CallbackManager | None = None,
run_id: str = "",
model_name: str = "procedural_router",
) -> RoutingResult
Route a query to appropriate tools.
Steps: 1. Query procedural layer for relevant skills. 2. Search catalog for matching tools. 3. Use LLM with structured output to select top tools. 4. Return only minimal schemas for selected tools.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant scope for isolation. |
required |
query
|
str
|
The user's query or request. |
required |
conversation_history
|
list[Any] | None
|
Optional recent conversation for context. |
None
|
callback_manager
|
CallbackManager | None
|
v7.4.3 (Jarvio Ask 5) -- when provided, fires
|
None
|
run_id
|
str
|
Engine run identifier for callback correlation. |
''
|
model_name
|
str
|
Optional model identifier stamped on the
LLMEndEvent. Defaults to |
'procedural_router'
|
Returns:
| Type | Description |
|---|---|
RoutingResult
|
RoutingResult with intent, selected tools, and reasoning. |
Source code in src/symfonic/memory/layers/procedural/router.py
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 | |
ContextBudget ¶
Bases: BaseModel
Defines the limits for context assembly.
.. deprecated:: ContextBudget is deprecated. Budget constraints are now handled internally by SymfonicAgent._hydrate() via FrameworkConfig.
Source code in src/symfonic/memory/models/context.py
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.
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 src/symfonic/memory/protocols.py
query_nodes
async
¶
Query nodes by filters (layer, label, properties, etc.).
limit=None returns all matching nodes (no truncation).
Source code in src/symfonic/memory/protocols.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 src/symfonic/memory/protocols.py
GraphMemoryStore ¶
GraphMemoryStore(
backend: GraphBackend,
embedding_provider: EmbeddingProvider | None = None,
embedding_cache: EmbeddingCache | None = None,
)
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
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.
distinct_labels
async
¶
Get distinct labels with counts and last_updated.
Aggregates all nodes in the tenant scope by their label field.
Soft-retracted nodes are excluded by default so a label whose only
node has been retracted is NOT reported as present -- otherwise a
required-memory discovery check (retrieval.discovery) would mark a
block satisfied when no live node backs it. Pass
include_retracted=True for audit aggregation.
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
List of dicts with keys |
Source code in src/symfonic/memory/graph/store.py
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
query_nodes
async
¶
query_nodes(
scope: TenantScope,
layer: MemoryLayer | None = None,
label: str | None = None,
label_prefix: str | None = None,
filters: dict[str, object] | None = None,
limit: int | None = None,
include_retracted: bool = False,
) -> list[MemoryNode]
Query nodes by layer, label, label_prefix, and/or arbitrary filters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant isolation scope. |
required |
layer
|
MemoryLayer | None
|
Exact match on the memory layer. |
None
|
label
|
str | None
|
Exact match on the node label. |
None
|
label_prefix
|
str | None
|
Prefix match on the node label (e.g. |
None
|
filters
|
dict[str, object] | None
|
Arbitrary key/value filters passed through to the backend. |
None
|
limit
|
int | None
|
Maximum rows to return. |
None
|
include_retracted
|
bool
|
When |
False
|
A positive limit combined with include_retracted=False no
longer under-returns when a retracted row happens to fall inside the
backend's page: the wrapper over-fetches (doubling the requested
window) and refills with live nodes until limit is satisfied or
the backend is exhausted, then trims to exactly limit. This keeps
limit semantics honest for every backend (InMemory/Postgres/
Mongo) without any backend-side change.
Source code in src/symfonic/memory/graph/store.py
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | |
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
¶
upsert_edge_props(
scope: TenantScope,
edge_id: EdgeId,
props: dict[str, Any],
) -> MemoryEdge | None
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.
Source code in src/symfonic/memory/graph/store.py
InMemoryGraphBackend ¶
In-memory implementation of the GraphBackend protocol.
Stores nodes and edges in dicts keyed by (tenant_id, node_id/edge_id). Suitable for testing and local development only.
Source code in src/symfonic/memory/backends/in_memory.py
add_edge
async
¶
Add an edge to the in-memory store.
add_node
async
¶
Add a node to the in-memory store.
v8.0: stamps the materialised scope_path into the node's
property bag (zero-migration) so the prefix-isolation filter (§5.d)
can be enforced on every read.
Source code in src/symfonic/memory/backends/in_memory.py
delete_edge
async
¶
Delete a single edge by its ID.
delete_node
async
¶
Delete a node, optionally cascading to connected edges.
Source code in src/symfonic/memory/backends/in_memory.py
get_neighbors
async
¶
get_neighbors(
scope: TenantScope,
node_id: NodeId,
relationship: str | None = None,
) -> list[MemoryNode]
Get nodes connected to the given node by edges.
Source code in src/symfonic/memory/backends/in_memory.py
get_node
async
¶
Get a node by ID within the scope's visible prefix chain.
Source code in src/symfonic/memory/backends/in_memory.py
query_edges
async
¶
query_edges(
scope: TenantScope,
filters: dict[str, Any] | None = None,
limit: int = 50,
offset: int = 0,
) -> list[MemoryEdge]
Query edges with optional relationship filter and pagination.
Source code in src/symfonic/memory/backends/in_memory.py
query_nodes
async
¶
query_nodes(
scope: TenantScope,
filters: dict[str, Any],
limit: int | None = 50,
) -> list[MemoryNode]
Filter nodes by properties within the tenant scope.
limit=None returns every matching node (no truncation);
limit=0 returns no rows (v8.7.1 — unified across backends).
Source code in src/symfonic/memory/backends/in_memory.py
traverse
async
¶
BFS traversal from start node up to max_depth.
Source code in src/symfonic/memory/backends/in_memory.py
update_node
async
¶
Partial update of node properties.
Source code in src/symfonic/memory/backends/in_memory.py
upsert_edge
async
¶
Insert edge or increment weight by 1 if (tenant, source, target, rel) matches.
Source code in src/symfonic/memory/backends/in_memory.py
InMemoryVectorBackend ¶
In-memory implementation of the VectorBackend protocol.
Stores embeddings in a list and uses brute-force cosine similarity for search. Suitable for testing only.
Source code in src/symfonic/memory/backends/in_memory.py
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.
v8.0: stamps the materialised scope_path into the metadata bag so
the prefix-isolation filter (§5.d) can run on every search.
Source code in src/symfonic/memory/backends/in_memory.py
count
async
¶
delete
async
¶
Delete vectors by their IDs.
Source code in src/symfonic/memory/backends/in_memory.py
search
async
¶
Brute-force cosine similarity search.
v8.0: ALWAYS-ON prefix-isolation (§5.d) — only entries whose stored
scope_path is a prefix of the query path are scored. Dual-reads
pre-v8.0 metadata (no scope_path) as a 1-level root path.
Source code in src/symfonic/memory/backends/in_memory.py
MemoryEdge ¶
Bases: BaseModel
A directed edge in the tenant-scoped memory graph.
Edges represent typed relationships between MemoryNodes, such as 'KNOWS', 'USED_IN', 'TRIGGERED_BY', etc. Weight can be used to express relationship strength.
MemoryEntry ¶
Bases: BaseModel
A memory entry returned from retrieval or submitted for writing.
Represents a single piece of information from any memory layer, optionally linked to a graph node and carrying a retrieval score.
MemoryLayer ¶
Bases: str, Enum
The five layers of the Pentad memory model.
Each layer serves a distinct cognitive function and is backed by an independent store that implements BaseMemoryStore.
EPISODIC
class-attribute
instance-attribute
¶
Narrative events, scenarios, and timestamps (When/Where).
PROCEDURAL
class-attribute
instance-attribute
¶
Skills, code snippets, and workflows (How).
PROSPECTIVE
class-attribute
instance-attribute
¶
Commitments, reminders, and pending tasks (Future).
SEMANTIC
class-attribute
instance-attribute
¶
Permanent facts and graph entities (What).
WORKING
class-attribute
instance-attribute
¶
Active conversation context, session-scoped (Now).
MemoryNode ¶
Bases: BaseModel
A node in the tenant-scoped memory graph.
Nodes represent facts, events, skills, triggers, or working context depending on their layer assignment. Each node carries an optional embedding vector for similarity search and tracks access frequency for retrieval scoring.
durability
property
¶
Return the node's durability marker (v7.26.2 -- Shape C1).
Reads from properties['durability']. Pre-v7.26.2 nodes (and
adopters who never set the key) get "durable" -- the safe
default that preserves byte-identical promotion behaviour. An
unknown persisted value falls back to "durable" so the
consolidation gate never silently drops a row.
This is a computed property, NOT a model Field: it does not change the serialised shape (Contract A).
is_promotable ¶
Return True when the node is eligible for consolidation promotion.
Only "durable" nodes promote. "transient" and "expired"
are gated out of Phases 10/11/12.
Source code in src/symfonic/memory/models/node.py
MemoryOperation ¶
Bases: BaseModel
A pending memory mutation to be applied by the orchestrator.
Each operation targets a specific layer and action. The node and edge fields are populated depending on the action type.
MemoryOrchestrator ¶
MemoryOrchestrator(
config: OrchestratorConfig,
retrieval_engine: RetrievalEngine,
router: CapabilityRouter,
layers: dict[MemoryLayer, BaseMemoryStore],
embedding_provider: EmbeddingProvider | None = None,
embedding_cache: EmbeddingCache | None = None,
graph_store: GraphMemoryStore | None = None,
)
Main entry point for the graph memory library.
Coordinates retrieval, routing, and layer management.
Construct via from_config for automatic wiring of all internal
components from backends and configuration.
Memory consolidation is handled by
:class:symfonic.core.learning.consolidation.SleepConsolidator.
Source code in src/symfonic/memory/orchestrator/orchestrator.py
commit_pending
async
¶
Apply pending operations to the appropriate layers.
Filters operations below the write_policy.min_importance_threshold before committing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant scope for isolation. |
required |
operations
|
list[MemoryOperation]
|
List of operations to apply. |
required |
Source code in src/symfonic/memory/orchestrator/orchestrator.py
extract_memories
async
¶
extract_memories(
scope: TenantScope,
interaction: dict[str, Any],
llm: Any,
*,
callback_manager: Any = None,
run_id: str = "",
) -> list[MemoryOperation]
Extract structured memory operations from a conversation turn.
Uses the LLM to analyze the interaction and produce MemoryOperations for each memory layer as appropriate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant scope for isolation. |
required |
interaction
|
dict[str, Any]
|
Dict with 'user_message' and 'assistant_response'. |
required |
llm
|
Any
|
LLM instance for extraction. |
required |
callback_manager
|
Any
|
v7.4.3 (Jarvio Ask 5) -- forwarded to the
underlying |
None
|
run_id
|
str
|
Engine run identifier for callback correlation. |
''
|
Returns:
| Type | Description |
|---|---|
list[MemoryOperation]
|
List of MemoryOperations to be applied via commit_pending. |
Source code in src/symfonic/memory/orchestrator/orchestrator.py
from_config
classmethod
¶
from_config(
config: OrchestratorConfig,
graph_backend: GraphBackend,
vector_backend: VectorBackend,
embedding_provider: EmbeddingProvider,
llm: Any,
catalog: ToolCatalog | None = None,
) -> MemoryOrchestrator
Wire all internal components from configuration and backends.
This is the primary DI entry point for library consumers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
OrchestratorConfig
|
Orchestrator configuration. |
required |
graph_backend
|
GraphBackend
|
Graph storage backend. |
required |
vector_backend
|
VectorBackend
|
Vector storage backend. |
required |
embedding_provider
|
EmbeddingProvider
|
Text embedding provider. |
required |
llm
|
Any
|
LLM instance (BaseChatModel from langchain-core). |
required |
catalog
|
ToolCatalog | None
|
Optional tool catalog. Creates empty one if not provided. |
None
|
Returns:
| Type | Description |
|---|---|
MemoryOrchestrator
|
Fully wired MemoryOrchestrator instance. |
Source code in src/symfonic/memory/orchestrator/orchestrator.py
hydrate_context
async
¶
hydrate_context(
scope: TenantScope,
query: str,
history: list[Any] | None = None,
) -> AssembledContext
Retrieve relevant memories and assemble context within budget.
.. deprecated::
hydrate_context is deprecated and will be removed in a future
release. Hydration is now performed exclusively by
SymfonicAgent._hydrate(), which iterates all enabled layers and
runs spreading activation. This method is never called by the active
pipeline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant scope for isolation. |
required |
query
|
str
|
The user's current query. |
required |
history
|
list[Any] | None
|
Optional conversation history messages. |
None
|
Returns:
| Type | Description |
|---|---|
AssembledContext
|
AssembledContext with system prompt, memories, tools, and history. |
Source code in src/symfonic/memory/orchestrator/orchestrator.py
upgrade_durability
async
¶
Flip the durability marker on a stored graph node (v7.26.2).
The explicit promotion / retirement signal for the consolidation
gate. Typical use: write a transient row ("syncing..."), then once
the state stabilises write a durable row ("complete: 42 messages")
and call upgrade_durability(transient_id, "expired") so the
transient row is pruned on the next Phase 8 cleanup.
Implementation (Contract E, verified per-backend at T0): all three
graph backends' update_node either REPLACE the whole properties
dict (InMemory, Postgres) or route a top-level properties key to
a full $set (Mongo, after the v7.26.2 translation fix). A flat
durability key cannot be used because it collides with the
read-only MemoryNode.durability computed property. So this is a
read-modify-write: fetch the node, mutate the properties dict, write
the FULL dict back. This preserves every OTHER property on the node.
Raises:
| Type | Description |
|---|---|
ValueError
|
on an unknown durability literal (fail-loud). |
KeyError / backend error
|
when the node does not exist. |
Source code in src/symfonic/memory/orchestrator/orchestrator.py
OrchestratorConfig ¶
Bases: BaseModel
Immutable configuration for the MemoryOrchestrator and its components.
All tunable parameters are centralized here. Construct via
with_defaults() for quick setup or from_env() to read
from environment variables prefixed with SYMFONIC_GRAPH_MEMORY_.
from_env
classmethod
¶
Create a config by reading SYMFONIC_GRAPH_MEMORY_ environment variables.
v8.0: the three legacy env vars
(SYMFONIC_GRAPH_MEMORY_COMPACTION_THRESHOLD,
SYMFONIC_GRAPH_MEMORY_LLM_MODEL,
SYMFONIC_GRAPH_MEMORY_LLM_TEMPERATURE) were silent no-ops
with a DeprecationWarning for many minor releases. They
now raise ValueError at config-load time so silent-setters
get a loud signal instead of an invisible warning.
Supported variables
SYMFONIC_GRAPH_MEMORY_DEFAULT_TOP_K
Source code in src/symfonic/memory/orchestrator/config.py
ProceduralLayer ¶
ProceduralLayer(
graph_store: GraphMemoryStore,
dedup_threshold: float = 0.65,
embedding_provider: EmbeddingProvider | None = None,
embedding_cache: EmbeddingCache | None = None,
)
Procedural memory layer for skills and workflows.
BaseMemoryStore contract
- retrieve: queries skills by content matching
- write: stores a skill as a graph node
- summarize: returns empty string (skills are standalone)
- link: no-op (skills are standalone graph nodes)
- delete: removes a skill node from the graph
v7.3 Item 13.1: optional embedding_provider for auto-embed.
Default None preserves byte-identical pre-Item-13.1 behaviour.
When supplied, every store_skill write runs through
:func:maybe_embed before the de-dup janitor sees it -- so
procedural skills auto-embed when the caller leaves
node.embedding=None. Caller-provided embeddings always win.
The embedding_cache is shared across all three graph-backed
layers by the orchestrator.
Per-kind embedding-text rule: procedural skills always fall into
the default branch of
:func:symfonic.memory.embeddings.auto_embed.resolve_embedding_text
and embed properties['content'] (the skill's rich description)
rather than the label (which is the skill_id).
Source code in src/symfonic/memory/layers/procedural/layer.py
approve_skill
async
¶
Promote a draft skill to approved+active.
Source code in src/symfonic/memory/layers/procedural/layer.py
delete
async
¶
link
async
¶
query_skills
async
¶
query_skills(
scope: TenantScope,
query: str,
top_k: int = 5,
*,
include_drafts: bool = False,
) -> list[MemoryEntry]
Query procedural skills by word-level matching.
Uses word intersection (like SemanticLayer.query_facts) instead of exact substring matching so that "deploy the app" can find a skill whose label/content contains "deploy" or "app".
By default only active (approved) skills are returned. Pass
include_drafts=True to return skills of all statuses.
Ordering contract (v7.19.2): the returned list is sorted by
node_id so the "first matching skill" downstream
consumers rely on (precondition_gate's first-call wins,
engine _maybe_resolve_forced_tool_choice's candidate
procedure selection) is deterministic across the in-memory /
mongodb / postgres backends. The in-memory backend's
dict.values() order happens to be insertion-stable on
CPython 3.7+, but mongodb / postgres do not guarantee
registration order without an explicit sort. Sorting at
query time pins the contract at the layer boundary so any
future backend additions inherit it automatically.
v7.22 (T-7.22.12-15) multi-scope: when scope.inherits_from
is non-empty, the query fans out to [scope, *scope.inherits_from]
and merges results by shadow override on
metadata['identifier'] first, then metadata['label'].
Empty inherits_from (the common case) delegates to the
single-scope fast path unchanged. See
docs/concepts/procedural-shadow-inheritance.md for merge semantics
and cache-invariant trade-offs.
Source code in src/symfonic/memory/layers/procedural/layer.py
reject_skill
async
¶
Mark a draft skill as rejected+inactive.
Source code in src/symfonic/memory/layers/procedural/layer.py
render_for_prompt ¶
render_for_prompt(
entry: Any,
scrubber: Callable[[dict[str, Any]], dict[str, Any]]
| None = None,
) -> str
Render a procedural entry for MEMORY_CONTEXT.
v7.7: closes the v7.6.4 procedural-render-bypass structurally.
When the entry carries v7.5+ skill-aware metadata (source
starts with "authored:" OR non-empty precondition of
either str or list[str] shape), delegate to the
v7.5.1 _render_skill_for_prompt helper so PRE-FLIGHT
markers reach the prompt. Pre-v7.5 procedural entries fall
through to :func:render_legacy for byte-identical pre-v7.7
output and prompt-cache parity.
Supersedes the inline _is_skill_aware_entry predicate +
dispatch block that lived in engine.py after the v7.6.4
hotfix; the engine's render loop is now polymorphic dispatch
with zero procedural-specific knowledge.
Source code in src/symfonic/memory/layers/procedural/layer.py
retrieve
async
¶
Retrieve skills matching the query.
seed_authored_skill
async
¶
seed_authored_skill(
scope: TenantScope,
skill: MemoryEntry,
*,
source_tag: str,
precondition: str
| list[str | dict[str, Any]]
| None = None,
) -> MemoryNode
Seed a hand-authored procedural skill (Jarvio Ask 7 Option B).
Convenience wrapper around :meth:store_skill for the
domain-plugin tier of procedural memory. The result lands at
status="approved" (immediately active in
:meth:query_skills), carries the required source tag so
:class:~symfonic.memory.janitor.SemanticMerge leaves it
alone, and optionally records one or more pre-flight
preconditions.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant scope for isolation. |
required |
skill
|
MemoryEntry
|
The skill |
required |
source_tag
|
str
|
A discriminator the janitor uses to recognise
the authored tier. MUST start with |
required |
precondition
|
str | list[str | dict[str, Any]] | None
|
Optional pre-flight rule. Three shapes accepted (v7.6.2 -- Jarvio multi-precondition ask):
|
None
|
Returns:
| Type | Description |
|---|---|
MemoryNode
|
The persisted :class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
When |
Source code in src/symfonic/memory/layers/procedural/layer.py
store_skill
async
¶
Store a skill as a MemoryNode with steps and context properties.
Before inserting, the incoming skill is checked against existing procedural nodes via SemanticMerge. If a sufficiently similar node already exists the two are merged in-place and no new node is created.
The skill's metadata should include 'steps' (list[str]) and 'context' (str) keys for proper procedural storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
str
|
"draft" (inactive, pending review), "approved" (active), "rejected" (archived, will not be queried). |
'draft'
|
Source code in src/symfonic/memory/layers/procedural/layer.py
summarize
async
¶
write
async
¶
ScoringWeights ¶
Bases: BaseModel
Weights for the four-signal retrieval scoring formula.
The four weights must sum to 1.0 (within floating-point tolerance). Default allocation: semantic 40%, recency 20%, frequency 20%, graph 20%.
TenantScope ¶
Bases: BaseModel
Immutable hierarchical N-level multi-tenant request scope.
Canonical representation is the root-first path (path[0] broadest,
path[-1] most-specific). The legacy tenant_id / sub_tenant_id
fields are RETAINED for backward compatibility and, when no explicit path
override is supplied, ARE the 1-/2-level path. When an explicit N-level
path is supplied via the factories, tenant_id / sub_tenant_id read
back off the path.
Isolation (design §5.d): a query at path P sees a memory at path Q iff Q is
a prefix of P. Enforced via :meth:ancestor_prefix_paths exact-IN at the
backend, NEVER a string prefix scan.
path
property
¶
Root-first ordered path (canonical hierarchical representation).
Derived from the flat fields when no explicit override is set
- tenant_id only → 1-level
(tenant:tenant_id,) -
- sub_tenant_id → 2-level
(tenant:tenant_id, sub_tenant:sub)
- sub_tenant_id → 2-level
scope_depth
property
¶
Number of levels in the path (1-based; root-only path is depth 1).
scope_path
property
¶
Materialised US-delimited, root-first path string for the backend.
org\x1facme\x1fbrand\x1fmyhalos\x1fconversation\x1fc-123.
This is the stored column/field value (design §5.b).
ancestor_prefix_paths ¶
The exact-IN list for the isolation filter (design §5.b/§5.d).
Returns the materialised scope_path string of EVERY prefix of this
scope's path (root → … → self), inclusive. A query at path P uses
this list as WHERE scope_path = ANY(list) so it matches a stored
memory at path Q iff Q is a prefix of P — exact set membership, never
a LIKE 'prefix%' scan (which would leak acme-corp to acme).
Source code in src/symfonic/core/scope.py
child ¶
Return a new scope with one more (deeper) level appended.
org_scope.child("brand", "myhalos").child("conversation", "c-123")
builds the full hierarchy. Namespace and inherits_from are preserved.
The result preserves the receiver's concrete type (type(self)) so
chaining .child() off a subclass such as
:class:~symfonic.agent.types.FrameworkTenantScope keeps the subclass
(and its converter methods) instead of silently downcasting to the
base TenantScope (P1 — subclass-drop on chaining).
Source code in src/symfonic/core/scope.py
from_path
classmethod
¶
from_path(
path: list[ScopeLevel] | tuple[ScopeLevel, ...],
*,
namespace: str | None = None,
) -> TenantScope
Construct an N-level scope from an explicit root-first path.
Source code in src/symfonic/core/scope.py
from_request
classmethod
¶
from_request(
tenant_id: str,
*,
sub_tenant_id: str | None = None,
namespace: str | None = None,
path: list[ScopeLevel]
| tuple[ScopeLevel, ...]
| None = None,
) -> TenantScope
Construct a scope from request-edge fields (blessed adopter entry).
Supply path for N-level hierarchies, or the flat fields for the
legacy 1-/2-level shape.
Source code in src/symfonic/core/scope.py
from_state_dict
classmethod
¶
Reconstruct a :class:TenantScope from a state-dict mapping.
Accepts UPPER_SNAKE and lower_snake casings (UPPER wins). Reads an
explicit path / PATH key (list of {kind, id} dicts) when
present — the N-level form; otherwise derives from the legacy flat
fields. inherits_from is read recursively (v7.22 compat).
Source code in src/symfonic/core/scope.py
narrow ¶
Create a new scope narrowed to a sub-tenant (legacy 2-level shim).
Retained byte-identically from v7.22: sets both sub_tenant_id and
namespace to the supplied value and preserves inherits_from.
For N-level hierarchies prefer :meth:child.
Source code in src/symfonic/core/scope.py
root
classmethod
¶
Construct a 1-level root scope (kind:id,).
to_state_dict ¶
Convert to state-dict fields for BaseAgentState injection.
Backward-compatible: a legacy 1-/2-level scope (no explicit path
override) emits EXACTLY the pre-v8.0 keys
(tenant_id + sub_tenant_id [+ namespace] [+
inherits_from]) — byte-identical. An explicit N-level path
additionally emits a path key (list of {kind, id} dicts) so
the hierarchy round-trips.
Source code in src/symfonic/core/scope.py
ToolCatalog ¶
Registry of available tools and capabilities.
Tools are registered dynamically and can be searched by keyword. The catalog provides tool metadata to the CapabilityRouter for LLM-driven tool selection.
Source code in src/symfonic/memory/layers/procedural/catalog.py
get ¶
list_all ¶
register ¶
Register a tool with its description and schema.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tool_id
|
str
|
Unique identifier for the tool. |
required |
description
|
str
|
Human-readable description of what the tool does. |
required |
schema
|
dict[str, Any]
|
JSON schema or dict describing the tool's parameters. |
required |
Source code in src/symfonic/memory/layers/procedural/catalog.py
search ¶
Search tools by keyword matching against ID and description.
Simple case-insensitive substring matching for v1.
Source code in src/symfonic/memory/layers/procedural/catalog.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
¶
search
async
¶
Search for the top_k most similar vectors.
Returns list of dicts with keys: id, score, metadata, document.
Source code in src/symfonic/memory/protocols.py
WritePolicy ¶
Bases: BaseModel
Controls which operations are applied and how many can be pending.
Operations with importance below min_importance_threshold are silently dropped during commit_pending.