symfonic.capabilities.memory.candidates¶
candidates ¶
A MemoryRetrievalPort seen as the CandidateSource the coordinator wants.
The two seams were built for different jobs and neither is wrong. A port
returns a :class:RetrievalResult — ranked, capped, already selected. A
candidate source returns :class:Candidate rows and lets
:class:RetrievalCoordinator do the ranking, the gates and the scope blending.
Path A of #14 puts HydrationCoordinator in charge of composing the block,
so the port has to arrive as a source, and this is that translation. It is
deliberately a thin, honest one:
- the port has already ranked, so its order is preserved and this adds none;
- the port's
scorebecomes thecuesignal, because that is the signal the score answers — a similarity to the query — and putting it anywhere else would have the coordinator weigh it as something it is not; - every other signal stays
None.Nonemeans "this store cannot answer that question", which is exactly true of a port that never reported it, and is the distinction :class:RelevanceSignalsexists to keep. Filling them with0.0would turn silence into an observation of absence.
PortCandidateSource ¶
Adapts one :class:MemoryRetrievalPort to the coordinator's source seam.
Source code in src/symfonic/capabilities/memory/candidates.py
drain_dropped ¶
What the last search refused, and clearing it as it is read.
The port reports provenance refusals in RetrievalResult.dropped;
CandidateSource.search returns candidates and has nowhere to put
them, so before this they died here. A scope whose every row was
refused then looked exactly like a scope that remembered nothing — same
empty block, same reason — and "this deployment's data cannot be
attributed" is not "this user has no history".
Drained rather than accumulated because one source serves every turn of an agent's life: a ledger that grew would attribute one turn's refusals to the next, forever.
Source code in src/symfonic/capabilities/memory/candidates.py
drain_sources ¶
What each route contributed to the last search, and what was lost.
Drained for the same reason the refusals are: one source serves every turn of an agent's life, and a tally that accumulated would attribute one turn's recall to the next.
Source code in src/symfonic/capabilities/memory/candidates.py
search
async
¶
Ask the port and hand over what it answered, unfiltered.
This used to drop rows whose layer the deployment had disabled, framed
as a courtesy that saved the coordinator work. It was not a courtesy: a
row filtered here leaves no entry in dropped, so a deployment that
disabled a layer saw its memories vanish with no reason recorded --
indistinguishable from a scope that never held them.
The coordinator gates by layer and writes down what it refused, which is the behaviour its own docstring promises ("a source that answers outside the set is a bug this seam should surface, not one it should hide"). Surfacing it means letting it through to the component that keeps the ledger.
MemoryUnavailable is not caught. The coordinator and the
hydration policy above it decide what a degraded store means for a
turn; swallowing it here would hand them an empty result that reads as
"nothing was remembered", which is the one thing an unreachable store
must never look like.
Source code in src/symfonic/capabilities/memory/candidates.py
overscan ¶
How many candidates to request so the gates have a choice.