Skip to content

symfonic.capabilities.memory.sources

sources

What a store contributes to a retrieval, and the in-process store that does.

The split this module draws is the one that lets the coordinator be the same component behind a vector database, a graph, and a dict: a store says what it found and what it observed about each hit; it does not say what any of that is worth, how many survive, or in what order they render. Those are deployment decisions, and a backend that made them would give every backend a vote on the prompt — with two of them disagreeing.

So :class:CandidateSource answers with a record plus :class:~.relevance.RelevanceSignals, and stops. It is deliberately not the :class:~.ports.MemoryRetrievalPort: the port is what the bridge holds and returns a finished, ranked, capped result. The coordinator is the thing in between, and it is the component that implements the port over this seam.

:class:LexicalCandidateSource is the in-process reference (CON-S-4, same reasoning as :class:~.in_memory.InMemoryHms): the framework must retrieve with no database and no embedding model installed, so the default source scores cue overlap with arithmetic. It is not a test double — a single-process deployment selects it on purpose — but a deployment that outgrows lexical matching swaps it without the coordinator noticing.

Candidate dataclass

Candidate(record: MemoryRecord, signals: RelevanceSignals = RelevanceSignals(), pre_ranked: bool = False)

One memory a store found, with whatever it observed about it.

The signals are the store's honest report, including its silences: a layer with no embedding leaves cue at None, and the coordinator treats that as "cannot answer" rather than "answered zero".

CandidateSource

Bases: Protocol

A store, as the retrieval coordinator needs it.

search async

search(query: MemoryQuery, layers: frozenset[MemoryLayer]) -> Sequence[Candidate]

Return what this store can find for query within layers.

layers is the effective set — the deployment's enabled layers narrowed by the query's — passed so a store can skip work rather than return rows the coordinator will drop. The coordinator re-checks what comes back regardless: a source that answers outside the set is a bug this seam should surface, not one it should hide.

Ranking, limits, and ceilings are not this method's business. Return everything plausible; the coordinator decides what reaches a prompt.

Raises :class:~.errors.MemoryUnavailable when the store is unreachable. Returning an empty sequence instead would be indistinguishable from a scope that genuinely remembers nothing.

Source code in src/symfonic/capabilities/memory/sources.py
async def search(
    self, query: MemoryQuery, layers: frozenset[MemoryLayer]
) -> Sequence[Candidate]:
    """Return what this store can find for ``query`` within ``layers``.

    ``layers`` is the *effective* set — the deployment's enabled layers
    narrowed by the query's — passed so a store can skip work rather than
    return rows the coordinator will drop. The coordinator re-checks what
    comes back regardless: a source that answers outside the set is a bug
    this seam should surface, not one it should hide.

    Ranking, limits, and ceilings are not this method's business. Return
    everything plausible; the coordinator decides what reaches a prompt.

    Raises :class:`~.errors.MemoryUnavailable` when the store is unreachable.
    Returning an empty sequence instead would be indistinguishable from a
    scope that genuinely remembers nothing.
    """
    ...

LexicalCandidateSource dataclass

LexicalCandidateSource(records: tuple[MemoryRecord, ...] = ())

A complete candidate source over records held in this process.

Scores cue overlap with token arithmetic rather than an embedding, because this is the zero-dependency default: a source that needed a model would make "retrieval works out of the box" false. Salience is left to the coordinator, which reads it off the record — a source should not restate what the record already carries.