Skip to content

symfonic.memory.layers.episodic

episodic

EpisodicLayer -- narrative events and timestamped history.

Wraps a VectorBackend for event storage and similarity-based retrieval. Implements BaseMemoryStore with link as a no-op (episodes are vector-stored).

EpisodicLayer

EpisodicLayer(vector_backend: VectorBackend, embedding_provider: EmbeddingProvider, telemetry_sink: EpisodicTelemetrySink | None = None)

Bases: EpisodicEventsMixin

Episodic memory layer for narrative events and history.

BaseMemoryStore contract
  • retrieve: queries events by vector similarity
  • write: stores an event with its embedding
  • summarize: returns empty string (delegates to compaction engine)
  • link: no-op (episodes are vector-stored, not graph-linked)
  • delete: removes an event from the vector store
Source code in symfonic/memory/layers/episodic.py
def __init__(
    self,
    vector_backend: VectorBackend,
    embedding_provider: EmbeddingProvider,
    telemetry_sink: EpisodicTelemetrySink | None = None,
) -> None:
    self._vector = vector_backend
    self._embedder = embedding_provider
    # When None (the default), the write path skips record construction
    # entirely -- zero cost on the hot path, which is the v6.1 contract.
    self._telemetry_sink = telemetry_sink
    # v7.22 T-7.22.8 -- emit-once-per-(tenant, sub_tenant) tracking for
    # MultiScopeIgnoredWarning.  See T-7.22.11 for the non-atomic
    # check-then-add concurrency rationale.
    self._multi_scope_warned: set[tuple[str, str | None]] = set()

delete async

delete(scope: TenantScope, node_id: NodeId) -> None

Delete an episodic event by its ID.

Source code in symfonic/memory/layers/episodic.py
async def delete(self, scope: TenantScope, node_id: NodeId) -> None:
    """Delete an episodic event by its ID."""
    await self._vector.delete(scope, [str(node_id)])
link(scope: TenantScope, source: NodeId, target: NodeId, relationship: str) -> None

No-op. Episodes are vector-stored, not graph-linked.

Source code in symfonic/memory/layers/episodic.py
async def link(
    self, scope: TenantScope, source: NodeId, target: NodeId, relationship: str
) -> None:
    """No-op. Episodes are vector-stored, not graph-linked."""
    return

render_for_prompt

render_for_prompt(entry: Any, scrubber: Callable[[dict[str, Any]], dict[str, Any]] | None = None) -> str

Render an episodic entry for MEMORY_CONTEXT.

v7.7: when metadata['speaker'] is present (per-message row introduced in Phase B), emit [episodic] <speaker>: <content> so the LLM sees a clean speaker-prefixed line. Pre-v7.7 rows (no speaker) or rows tagged speaker == 'legacy' fall back to :func:render_legacy for byte-identical pre-v7.7 output and prompt-cache parity.

Source code in symfonic/memory/layers/episodic.py
def render_for_prompt(
    self,
    entry: Any,
    scrubber: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
) -> str:
    """Render an episodic entry for ``MEMORY_CONTEXT``.

    v7.7: when ``metadata['speaker']`` is present (per-message row
    introduced in Phase B), emit ``[episodic] <speaker>: <content>``
    so the LLM sees a clean speaker-prefixed line.  Pre-v7.7 rows
    (no speaker) or rows tagged ``speaker == 'legacy'`` fall back
    to :func:`render_legacy` for byte-identical pre-v7.7 output
    and prompt-cache parity.
    """
    metadata = getattr(entry, "metadata", {}) or {}
    speaker = metadata.get("speaker")
    if isinstance(speaker, str) and speaker and speaker != "legacy":
        content = str(getattr(entry, "content", ""))
        layer_name = getattr(entry, "layer", "episodic")
        return f"[{layer_name}] {speaker}: {content}"
    return render_legacy(entry, scrubber)

retrieve async

retrieve(scope: TenantScope, query: str, top_k: int = 5) -> list[MemoryEntry]

Retrieve episodic events matching the query.

v7.22 T-7.22.8: emits :class:MultiScopeIgnoredWarning on first encounter of a scope with inherits_from. Episodic events are speaker-attributed timestamped records bound to a single (tenant, sub_tenant) pair; merging inherited scopes would corrupt per-tenant cost accounting and break GDPR scope guarantees (v5.5.0 Gate 3).

Source code in symfonic/memory/layers/episodic.py
async def retrieve(
    self, scope: TenantScope, query: str, top_k: int = 5
) -> list[MemoryEntry]:
    """Retrieve episodic events matching the query.

    v7.22 T-7.22.8: emits :class:`MultiScopeIgnoredWarning` on first
    encounter of a scope with ``inherits_from``.  Episodic events are
    speaker-attributed timestamped records bound to a single
    ``(tenant, sub_tenant)`` pair; merging inherited scopes would
    corrupt per-tenant cost accounting and break GDPR scope
    guarantees (v5.5.0 Gate 3).
    """
    from symfonic.memory._multi_scope_warn import maybe_warn_multi_scope

    maybe_warn_multi_scope(self, self._multi_scope_warned, scope)
    return await self.query_events(scope, query, top_k)

summarize async

summarize(scope: TenantScope, entries: list[MemoryEntry]) -> str

Not directly implemented. Delegates to compaction engine.

Source code in symfonic/memory/layers/episodic.py
async def summarize(self, scope: TenantScope, entries: list[MemoryEntry]) -> str:
    """Not directly implemented. Delegates to compaction engine."""
    return ""

write async

write(scope: TenantScope, entry: MemoryEntry) -> None

Write an episodic event to the vector store.

Source code in symfonic/memory/layers/episodic.py
async def write(self, scope: TenantScope, entry: MemoryEntry) -> None:
    """Write an episodic event to the vector store."""
    await self.store_event(scope, entry)