Skip to content

Level 2: Persistent Memory

Give your agent a brain that survives restarts. This level connects symfonic to PostgreSQL with pgvector for the full 5-layer Hierarchical Memory System (HMS).

Prerequisites

  • Completed Level 1
  • Docker (for Postgres)

Install

pip install symfonic-core[postgres]

Start Postgres with pgvector

docker run -d \
  --name symfonic-pg \
  -e POSTGRES_USER=symfonic \
  -e POSTGRES_PASSWORD=symfonic_dev \
  -e POSTGRES_DB=symfonic \
  -p 5432:5432 \
  pgvector/pgvector:pg16

Set the connection string:

export POSTGRES_DSN="postgresql://symfonic:symfonic_dev@localhost:5432/symfonic"

Agent with Persistent Memory

Create memory_agent.py:

import asyncio
import os
from symfonic.agent.config import FrameworkConfig
from symfonic.agent.engine import SymfonicAgent
from symfonic.agent.types import FrameworkTenantScope
from symfonic.core.providers import AnthropicProvider
from symfonic.memory.backends.pool import PostgresPoolManager
from symfonic.memory.backends.postgres_graph import PostgresGraphBackend
from symfonic.memory.backends.postgres_vector import PostgresVectorBackend


class SimpleEmbeddingProvider:
    """Minimal embedding provider for development.

    Replace with sentence-transformers or OpenAI embeddings in production.
    """
    @property
    def dimensions(self) -> int:
        return 384

    async def embed(self, texts: list[str]) -> list[list[float]]:
        results = []
        for text in texts:
            h = hash(text) & 0xFFFFFFFF
            vec = [((h >> (i % 32)) & 1) * 0.1 for i in range(384)]
            results.append(vec)
        return results


async def main():
    dsn = os.environ["POSTGRES_DSN"]

    # 1. Create connection pool and backends
    pool = PostgresPoolManager(dsn=dsn, min_size=2, max_size=10)
    await pool.open()

    graph_backend = PostgresGraphBackend(pool)
    vector_backend = PostgresVectorBackend(pool)
    await graph_backend.ensure_schema()
    await vector_backend.ensure_schema()

    # 2. Build agent with full HMS
    agent = SymfonicAgent(
        model_provider=AnthropicProvider(),
        embedding_provider=SimpleEmbeddingProvider(),
        graph_backend=graph_backend,
        vector_backend=vector_backend,
        config=FrameworkConfig(
            auto_hydrate=True,         # inject memory context before LLM
            auto_consolidate=True,     # extract + commit memories after LLM
            spreading_activation=True, # fetch neighbor nodes on semantic hits
            enable_hms_prompt=True,    # inject HMS system prompt sections
        ),
    )

    scope = FrameworkTenantScope(tenant_id="acme-corp")

    # 3. First conversation -- agent learns
    r1 = await agent.run("Our brand voice is friendly and expert.", scope=scope)
    print(f"Turn 1: {r1.final_response}")

    # 4. Second conversation -- agent remembers
    r2 = await agent.run("What do you know about our brand?", scope=scope)
    print(f"Turn 2: {r2.final_response}")
    print(f"Memory entries used: {r2.memory_entries_used}")

    await pool.close()

asyncio.run(main())

Run it:

ANTHROPIC_API_KEY=sk-ant-... python memory_agent.py

Stop the script, then run it again. The agent still remembers "friendly and expert" because facts are persisted in Postgres.

How HMS Works

The key constructor parameters that enable memory:

Parameter Effect
enable_hms_prompt=True Injects system prompt sections instructing the LLM to emit <GRAPH_OPERATIONS> blocks
auto_hydrate=True Before each LLM call, reads relevant memory from all enabled layers into the system prompt
auto_consolidate=True After each LLM call, parses extraction blocks and commits new facts to the graph
spreading_activation=True When semantic retrieval hits, automatically fetches 1st-degree neighbor nodes

The 5 Memory Layers

HMS organizes memory into 5 cognitive layers, each backed by a different storage strategy:

Layer Purpose Backend Example
Semantic Long-term knowledge graph (facts, entities, relationships) Graph "Acme's brand voice is friendly"
Episodic Past interaction records (vector-indexed similarity search) Vector "On March 15, user asked about pricing"
Working Short-term context for the current conversation Graph Current session state
Procedural Learned workflows and step-by-step procedures Graph "To generate a report: 1. Pull data 2. Chart 3. PDF"
Prospective Future goals and intentions Graph "User wants to launch in Q3"

Enable or disable layers selectively:

config = FrameworkConfig(
    enabled_layers={"semantic", "episodic", "working"},  # skip procedural + prospective
)

Querying Memory Directly: label_prefix

The graph store supports prefix matching on node labels. This is essential when labels follow the "PREFIX: detail" convention used by HMS:

from symfonic.memory.graph.store import GraphMemoryStore
from symfonic.memory.types import MemoryLayer

# Access the graph store from the agent's orchestrator
graph_store = agent.orchestrator.graph_store

# Exact match -- only returns nodes labeled exactly "SOUL"
exact = await graph_store.query_nodes(scope, label="SOUL")

# Prefix match -- returns "SOUL: Joy", "SOUL: name: Amiel", etc.
prefixed = await graph_store.query_nodes(scope, label_prefix="SOUL")

# Combine with layer filter
semantic_soul = await graph_store.query_nodes(
    scope,
    layer=MemoryLayer.SEMANTIC,
    label_prefix="SOUL",
)

All three backends (Postgres, InMemory, MongoDB) implement prefix matching safely with injection-proof queries.

Verify Memory with the FastAPI Bridge

Wrap your agent in HTTP endpoints to inspect memory state:

from fastapi import FastAPI
from symfonic.agent.fastapi.router import create_agent_router

app = FastAPI(title="Memory Agent")
router = create_agent_router(agent, prefix="/api/v1")
app.include_router(router)
pip install symfonic-core[agent-api]
uvicorn my_app:app --reload

Then query the memory status:

# Chat with the agent
curl -X POST http://localhost:8000/api/v1/chat \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: acme-corp" \
  -d '{"query": "What is our brand voice?", "tenant_id": "acme-corp"}'

# Check memory status
curl http://localhost:8000/api/v1/memories/status \
  -H "X-Tenant-ID: acme-corp"

The /memories/status endpoint returns counts per layer, total nodes, and the last consolidation timestamp.

Multi-Tenant Isolation

Every memory operation requires a FrameworkTenantScope. Tenants never see each other's data:

acme = FrameworkTenantScope.root("tenant", "acme-corp")
globex = FrameworkTenantScope.root("tenant", "globex-inc")

# These operate on completely separate memory graphs
await agent.run("Our brand is premium.", scope=acme)
await agent.run("Our brand is budget-friendly.", scope=globex)

# Acme's agent knows "premium", not "budget-friendly"
r = await agent.run("What's our brand positioning?", scope=acme)

Construction: prefer the factories — root(kind, id), .child(kind, id), from_path([...]), from_state_dict(...). Bare FrameworkTenantScope(tenant_id="acme-corp") still works but emits a DeprecationWarning (v8.0) and can only express a 1-/2-level scope.

Multi-level scoping (v8.0)

TenantScope is a root-first path of arbitrary depth. Model real hierarchies — e.g. org → brand → conversation — directly:

scope = (
    FrameworkTenantScope.root("org", "acme")
    .child("brand", "myhalos")
    .child("conversation", "c-123")
)

A query at the conversation tip sees memories at its own path and every ancestor prefix (its conversation, its brand, its org) — and NEVER a sibling conversation or sibling brand. This prefix isolation is an always-on security boundary. With FrameworkConfig(scope_blend_mode="blend"), ancestor memories are blended into the result set, down-weighted by hierarchy distance (tip ×1.0, parent ×0.5, grandparent ×0.25). See Hierarchical TenantScope for the full model.

Next Steps

Your agent has persistent memory. Level 3 shows how to stream responses in real-time and observe the full execution lifecycle.

Next: Streaming & Callbacks

See also: - Feature Catalog § Memory (5-Pentad) — the five SDK methods + five POST /api/v1/memories/{layer} endpoints (v6.1.4). - Feature Catalog § Retrieval and Hydration — spreading activation, N-hop BFS, credential scrubbing.