Skip to content

symfonic.capabilities.memory.hydration

hydration

Turning a ranked recall into one block of text and one declaration.

Hydration is the last thing memory does before the prompt compiler runs, and the only thing it hands over: a block, a revision, and a :class:~.contribution.MemoryContribution declaring where that block belongs. Everything upstream — layers, scoring, gates, activation — has already happened; this module composes and bounds the result.

Order is a decision, not a formatting detail. The conversation window leads, then the ranked recall. The window is what the user just said; a recall that displaced it would answer a question about the conversation with a fact about the user.

Lines drop whole, from the tail. The ceiling cuts the weakest recall rather than half of one. A memory truncated mid-sentence still reads as a complete statement, and the statement it reads as is not the one the store held — the same rule :func:~.queries.select applies per memory, applied again to the assembled block.

JIT hydrates nothing. In :attr:HydrationMode.JIT the block is empty, no contribution is declared, and the store is never called: the model asks for a recall through a tool once it decides it needs one, which is :meth:HydrationCoordinator.hydrate_on_demand. That is a real difference in what happens, not a rendering mode — the round trip a JIT deployment is paying to avoid is the one this skips.

Compression arrives bound. A deterministic text compressor lives in the framework's utilities, outside this layer, so the policy takes a callable rather than importing one. A capability that reached across for it would be two layers wearing one name, and a capability that reimplemented its abbreviation table would drift from it silently.

CoordinatedHydration dataclass

CoordinatedHydration(query: MemoryQuery, retrieval: CoordinatedRetrieval, working: WorkingContext = WorkingContext(), block: str = '', revision: str = '', contribution: MemoryContribution | None = None, request: MemoryRequest = (lambda: MemoryRequest(contribution_id=''))(), dropped: tuple[tuple[str, str], ...] = ())

One completed hydration: what was found, what renders, what is declared.

degraded property

degraded: bool

Whether any half of this turn's memory answered from a broken store.

events

events() -> tuple[ActivationEventSpec, ...]

The activation events this hydration licenses a consumer to emit.

Source code in src/symfonic/capabilities/memory/hydration.py
def events(self) -> tuple[ActivationEventSpec, ...]:
    """The activation events this hydration licenses a consumer to emit."""
    return activation_events(self.retrieval.activation)

HydrationCoordinator

HydrationCoordinator(*, retrieval: RetrievalCoordinator, working: WorkingWindow | None = None, policy: HydrationPolicy | None = None, contribution_id: str = 'memory.recall', order: int = 0)

Composes the recall block and declares it to the prompt compiler.

Source code in src/symfonic/capabilities/memory/hydration.py
def __init__(
    self,
    *,
    retrieval: RetrievalCoordinator,
    working: WorkingWindow | None = None,
    policy: HydrationPolicy | None = None,
    contribution_id: str = "memory.recall",
    order: int = 0,
) -> None:
    self._retrieval = retrieval
    self._working = working
    self._policy = policy or HydrationPolicy()
    self._order = order
    # Declared once against an empty block, at wiring time, purely so a
    # forged contribution id fails the deployment before its first turn
    # rather than on its first compile. Every other rule the declaration
    # enforces is fixed by _declare below, so this really only tests the id.
    MemoryContribution(
        contribution_id=contribution_id,
        source=ComposedMemorySource(block="", revision="", scope_path=""),
        order=order,
    )
    self._contribution_id = contribution_id

hydrate async

hydrate(query: MemoryQuery) -> CoordinatedHydration

The prompt/input pass. Retrieves nothing in JIT mode.

Source code in src/symfonic/capabilities/memory/hydration.py
async def hydrate(self, query: MemoryQuery) -> CoordinatedHydration:
    """The prompt/input pass. Retrieves nothing in JIT mode."""
    if self._policy.mode is HydrationMode.JIT:
        return CoordinatedHydration(
            query=query,
            retrieval=CoordinatedRetrieval(result=RetrievalResult()),
            request=self._request(query),
        )
    return await self._compose(query, declare=True)

hydrate_on_demand async

hydrate_on_demand(query: MemoryQuery) -> CoordinatedHydration

The tool-facing pass: retrieve now, declare nothing.

The compiler has already run by the time a model calls a tool, so a contribution here would be a declaration nobody reads. The block is the tool's return value.

Source code in src/symfonic/capabilities/memory/hydration.py
async def hydrate_on_demand(self, query: MemoryQuery) -> CoordinatedHydration:
    """The tool-facing pass: retrieve now, declare nothing.

    The compiler has already run by the time a model calls a tool, so a
    contribution here would be a declaration nobody reads. The block is the
    tool's return value.
    """
    return await self._compose(query, declare=False)

HydrationMode

Bases: StrEnum

When the recall is fetched.

EAGER class-attribute instance-attribute

EAGER = 'eager'

Retrieve before the compile and declare a block. The default.

JIT class-attribute instance-attribute

JIT = 'jit'

Retrieve nothing up front; the model asks for it through a tool.

HydrationPolicy dataclass

HydrationPolicy(mode: HydrationMode = HydrationMode.EAGER, max_block_chars: int = DEFAULT_BLOCK_CHARS, compressor: Callable[[str], str] | None = None)

How the block is assembled and bounded.