Skip to content

symfonic.memory -- Hierarchical Memory System

symfonic.memory is an async-first, protocol-driven memory library that can be used standalone or wired into a SymfonicAgent through symfonic.agent. All five layers persist to Postgres by default.

The Pentad Model

The library organizes memory into five cognitive layers, collectively called the Hierarchical Memory System (HMS) or Pentad. Each layer answers a different question about stored information.

Layer Enum value Cognitive role Key question Backend
Semantic MemoryLayer.SEMANTIC Permanent facts and graph entities What is true? Postgres Graph (+ optional MongoDB mirror)
Episodic MemoryLayer.EPISODIC Narrative events with timestamps When/where did this happen? Postgres Vector
Working MemoryLayer.WORKING Active conversation context What is happening now? Postgres Graph (24h TTL)
Procedural MemoryLayer.PROCEDURAL Skills, code snippets, workflows How is this done? Postgres Graph + SkillRegistry
Prospective MemoryLayer.PROSPECTIVE Commitments, reminders, tasks What should happen next? Postgres Graph

All five layers share the BaseMemoryStore protocol with a uniform write / retrieve / delete interface. The differences between layers are in how they index, score, and decay entries -- not in how callers interact with them.

Semantic Layer

The semantic layer stores permanent facts, entities, and relationships as graph nodes. Each node has a label, importance score, properties dict, and tenant- scoped identity. Relationships between nodes are stored as directed edges with typed relationship labels. This layer is the primary target for upsert_node extraction operations.

When MongoDB is configured as a secondary backend, a dual-write observer mirrors semantic nodes to MongoDB for graph visualization (vis.js). Postgres remains the source of truth.

Episodic Layer

Stores timestamped interaction history. Each entry records a turn summary (User: ... | Agent: ...) with low importance (3.0) and metadata like source=auto_turn. Uses vector similarity for retrieval, allowing the agent to recall past conversations by semantic relevance rather than just recency.

Working Layer

Session-scoped FIFO buffer backed by a Postgres graph store. Entries are short-lived reasoning context -- including thinking nodes from Anthropic extended thinking (written with ttl_hours=24 and source=thinking). The working layer is excluded from query-time hydration (it is session context, not knowledge) but is written to during consolidation.

Procedural Layer

Stores learned skills, workflows, and tool routing rules. Two specialized helpers sit on top:

  • ToolCatalog: Named skills with steps and active/inactive flags.
  • CapabilityRouter: Given a query, returns the subset of registered tools relevant to that query. Used by SymfonicAgent for lazy tool routing.

Prospective Layer

Stores future-oriented information: commitments, reminders, scheduled tasks, and pending intentions. Populated by set_trigger extraction operations when the user mentions something they need to do later.

Hydration

Hydration is the process of injecting memory context into the LLM prompt before execution. The _hydrate method in SymfonicAgent.engine:

  1. Iterates all enabled non-working layers.
  2. Calls layer.retrieve(scope, query, top_k=N) on each.
  3. Collects all returned MemoryEntry objects.
  4. Runs spreading activation (if enabled) to expand hits with first-degree graph neighbors.
  5. Formats entries into a rich context string: [layer_name] content (key=val, key=val).
  6. Appends linked context from spreading activation under an --- Associated Context --- header.
  7. Returns a dict with context, entries_count, graph_edges, and activation_log.

Hydration failures on individual layers are logged and suppressed -- a failing layer does not block the query.

Spreading Activation

After initial retrieval, spreading activation expands the result set by fetching first-degree neighbors from the knowledge graph. This surfaces related facts that were not directly matched by the query.

How It Works

The SpreadingActivation class in symfonic.memory.retrieval.spreading:

  1. Takes the list of MemoryNode hit nodes from semantic retrieval.
  2. For each hit, calls graph_store.get_neighbors(scope, node_id).
  3. Filters neighbors by min_importance threshold.
  4. Deduplicates against already-seen IDs.
  5. Sorts by importance descending.
  6. Caps at 10 neighbors total to avoid token explosion.

Activation Log

The result is returned in AgentResponse.activation_log with three fields:

Field Type Description
activated_nodes list[{name, score}] All activated nodes (seed hits + neighbors), deduplicated, sorted by score descending
inference_paths list[[source, target]] Pairs of node labels showing how neighbors were reached
pending_connections list[{source, relationship, target, status}] Inferred (non-explicit) relationships tagged PENDING CONSOLIDATION

Pending connections are relationships inferred via generic associations (associated, linked_semantic, linked_episodic) that have not been confirmed. They can be passed to the Deep Sleep consolidation endpoint to be written as real edges.

Deep Sleep Consolidation

The SleepConsolidator (in symfonic.core.learning.consolidation) is an offline engine designed to run as a nightly Celery beat task or via the POST /memory/consolidate API endpoint.

The 7 Phases

Phase Name What it does
1 Strengthen Boosts importance for recently recurring nodes (nodes updated within the lookback window)
1.5 Co-occurrence edges Creates CO_OCCURRED edges between nodes updated within the same 1-hour window that have no direct edge
2 Tag risk Marks nodes whose content contains negative-sentiment markers (risk, danger, failure, etc.)
2.5 Pending edge consolidation Writes inferred pending_connections (from spreading activation) as real graph edges
3 Prune orphans Deletes stale low-confidence orphan nodes (<=1 edge, older than 7 days, importance < 0.4)
4 Generate meta-nodes Clusters nodes by label prefix and creates summary meta-nodes for groups of 3+
5 SOUL corrections Applies user corrections to the domain SOUL schema from recent node data

ConsolidationReport

Each run returns a ConsolidationReport dataclass:

Field Type Description
tenant_id str Tenant that was consolidated
started_at datetime Run start time
finished_at datetime Run end time
nodes_strengthened int Nodes whose importance was boosted
nodes_pruned int Stale orphan nodes deleted
risk_nodes_tagged int Nodes flagged with risk markers
meta_nodes_created int Summary meta-nodes generated
soul_updates int SOUL schema corrections applied
edges_created int New edges (co-occurrence + pending)
errors list[str] Non-fatal errors encountered during the run

Configuration

consolidator = SleepConsolidator(
    graph_store=graph_store,
    lookback_hours=24,       # How far back to look for recent nodes
    llm_summariser=None,     # Optional LLM for meta-node summaries
)

report = await consolidator.run(
    scope,
    soul_schema=domain_template.soul_schema,
    pending_connections=activation_log["pending_connections"],
)

Query Capabilities

label_prefix (v5.4.0)

GraphMemoryStore.query_nodes() accepts a label_prefix parameter for prefix matching on node labels. This is important when labels follow the "PREFIX: detail" convention (e.g. "SOUL: Joy", "PRODUCT: SKU-123").

# Returns all nodes whose label starts with "SOUL"
nodes = await graph_store.query_nodes(scope, label_prefix="SOUL")

All three backends support this parameter: - Postgres: LIKE $N || '%' (positional parameter, injection-safe) - InMemory: str.startswith() - MongoDB: anchored regex ^PREFIX with re.escape()

label (exact match) and label_prefix can be used together with AND logic.

Tenant Isolation

Every operation in the library requires a TenantScope. No data crosses tenant boundaries.

from symfonic.core.scope import TenantScope, ScopeLevel

# v8.0 canonical (hierarchical) construction via the blessed factories:
scope = (
    TenantScope.root("org", "acme")
    .child("brand", "myhalos")
    .child("conversation", "c-123")
)
scope.tenant_id   # "acme"  (computed: path root id — backward-compatible)
scope.scope_depth # 3

# Legacy bare construction still works (deprecation warning):
scoped = TenantScope(tenant_id="acme", namespace="engineering")

Hierarchical N-level scope (v8.0)

As of v8.0, TenantScope is an arbitrary-depth, root-first path of ScopeLevel(kind, id) values (e.g. org → brand → conversation). The flat tenant_id / sub_tenant_id fields are retained and computed off the path, so existing call sites keep working. Three behaviours flow from the path:

  • Prefix isolation (ALWAYS-ON security boundary). A query at path P sees a memory at path Q iff Q is a prefix of P — enforced by exact-IN over the query's ancestor prefixes (never a LIKE 'prefix%' scan). A conversation sees its own conversation + brand + org, never a sibling conversation or sibling brand. This closes the cross-session-bleed class.
  • Materialised scope_path. Each level is serialised into a US-delimited (\x1f), root-first string stored on every memory row, so acme and acme-corp produce distinct, non-overlapping strings.
  • Scope-distance blend (opt-in). With FrameworkConfig.scope_blend_mode = "blend", ancestor-level memories are blended into a descendant query, down-weighted by hierarchy distance via a multiplicative gate on the composite score (tip ×1.0, parent ×0.5, grandparent ×0.25). ScoringWeights still sums to 1.0; the four-signal formula below is untouched.

Per-layer: Semantic + Episodic honour the blend; Procedural keeps shadow-override re-expressed on the path; Working + Prospective stay tip-only. See hierarchical-tenant-scope.md for the full model, the migration of pre-v8.0 rows, and promotion/provenance.

Backends

Two backend protocols define where data is physically stored:

Protocol Purpose Implementations
GraphBackend Entity/relation graph (nodes + edges) PostgresGraphBackend, MongoGraphBackend, InMemoryGraphBackend
VectorBackend Dense vector similarity search PostgresVectorBackend, InMemoryVectorBackend, ChromaVectorBackend

The in-memory implementations hold data in Python dicts and lists with no external dependencies -- suitable for development, testing, and embedded deployments. Production deployments use PostgresGraphBackend and PostgresVectorBackend for durability.

The MongoDB backend (MongoGraphBackend) is used as a secondary for graph visualization. It is not a primary store.

Retrieval Engine and Scoring

Retrieval from episodic and semantic layers uses a four-signal scoring formula:

score = w_semantic * cosine_sim
      + w_recency  * exp_decay(last_access)
      + w_frequency * log_norm(access_count)
      + w_graph    * inv_path_distance

Default weights: semantic 40%, recency 20%, frequency 20%, graph 20%.

from symfonic.memory import ScoringWeights, OrchestratorConfig

weights = ScoringWeights(
    semantic_similarity=0.5,
    recency=0.2,
    frequency=0.1,
    graph_proximity=0.2,
)
config = OrchestratorConfig(scoring_weights=weights)

MemoryOrchestrator

MemoryOrchestrator is the top-level facade. It holds a reference to each enabled layer and exposes get_layer() and all_layers() accessors.

from symfonic.memory import MemoryOrchestrator, OrchestratorConfig, MemoryLayer

config = OrchestratorConfig(
    default_top_k=5,
    compaction_threshold=50,
    enabled_layers=frozenset({MemoryLayer.WORKING, MemoryLayer.EPISODIC}),
)
orchestrator = MemoryOrchestrator(config=config)
working = orchestrator.get_layer(MemoryLayer.WORKING)

The recommended way to construct a fully wired orchestrator is via HMSFactory (exported from symfonic.agent).

Public API Reference

Full docstring-based API documentation is available at docs/api/symfonic/memory/.

Key exported symbols:

Symbol Description
MemoryOrchestrator Top-level HMS facade
OrchestratorConfig Central configuration (frozen Pydantic)
TenantScope Tenant isolation scope
MemoryLayer Enum: WORKING, EPISODIC, SEMANTIC, PROCEDURAL, PROSPECTIVE
ScoringWeights Retrieval scoring formula weights
MemoryEntry Single memory record
MemoryNode / MemoryEdge Graph primitives
GraphMemoryStore Graph node/edge store (supports label_prefix queries since v5.4.0)
CapabilityRouter Lazy tool routing via procedural layer
ToolCatalog Skill registry for procedural layer
BaseMemoryStore Layer protocol
GraphBackend Graph persistence protocol
VectorBackend Vector search protocol
EmbeddingProvider Text-to-vector protocol