def recall_handler(capability: Any) -> Callable[[Any], Any]:
"""Recall for this turn's scope, and offer it to the prompt."""
async def handle(context: Any) -> StageResult[Any]:
turn_request = getattr(context, "request", None)
if turn_request is None: # pragma: no cover - defensive
return no_change("no turn request on the stage context")
# The turn's scope wins over the configured one. A bundle is folded
# once, at construction, so its scope is a *default* -- and using it
# for a turn that named a tenant queried the wrong scope, returned
# nothing, and produced a prompt with no recall and no error. The
# configured scope stays the fallback for callers that state none.
turn_scope = getattr(turn_request, "scope", None) or capability.scope
hydration = await capability.hydrator.hydrate(
MemoryQuery(
scope=turn_scope,
cue=capability.cue_from(turn_request),
limit=capability.limit,
recall_budget=capability.recall_budget,
session_id=str(getattr(turn_request, "session_id", "") or ""),
)
)
if hydration.contribution is None or not hydration.block:
# ``block``, not ``result.memories``: the block is both
# segments composed, so a turn whose recall found nothing but
# whose conversation window has three lines still contributes.
# Testing the recall alone dropped the window silently, which is
# the failure this task exists to prevent.
#
# An unreachable store and an empty one are different facts, and
# the reason says which. Contributing an empty entry would cost
# the compiler a delimiter and a budget slot for no content.
# ``contribution is None`` is not redundant with an empty
# block, and mypy asking about it found a real gap: the
# coordinator declares nothing in JIT mode *whatever* the block
# says, so a hydration can carry text and no declaration. The
# old code would have called ``replace(None, ...)`` there.
return no_change(
"a memory store was unreachable this turn"
if hydration.degraded
else recall_reason(hydration)
or "nothing was declared for this turn: no recall, an "
"empty conversation window, or hydration is in JIT mode",
# Counted even here, and especially here: ``found=0`` is a
# retrieval that ran and recalled nothing, which is what a
# reader needs to tell from a retrieval that never ran.
counts=recall_counts(hydration),
)
# The block arrives already composed -- window first, then recall,
# under one ceiling with one drop ledger -- because
# ``HydrationCoordinator`` owns that composition and is the only
# thing that does. This capability used to render the recall itself
# from ``result.render()``, which could only ever see half the turn's
# memory: the conversation window is not in a retrieval result and
# never was.
#
# Still rendered before the snapshot rather than handed over live:
# review of PR #94 found that a retrieval result carries
# MemoryRecord -> MemoryLayer, and an enum member is a process-wide
# singleton, so one stage could set an attribute on it and every
# later turn in the process would see it.
source = ComposedMemorySource(
block=hydration.block,
revision=hydration.revision,
# The turn's scope, not the configured one -- the same rule the
# query above follows. Tagging the block with the bundle's
# default while querying the turn's tenant made the source's
# provenance a lie, and nothing checked it until the compiler
# started receiving a scope: ``ComposedMemorySource`` then
# refused its own block and the recall vanished from the prompt.
scope_path=turn_scope.path,
)
# ``replace``, not a fresh construction. Naming fields by hand meant
# hardcoding ``order=0`` -- so an adopter's ``order=37`` published 0.
# ``ordering_key`` is ``(layer, order, contribution_id)``, so 0 ties
# with every other 0 and ``memory.recall`` wins the alphabetical
# tie-break, silently promoting the recall to the head of its layer.
# Under budget pressure ``BudgetRow`` drops in reverse, so the
# deployment then loses the operator's blocks *and* the recall.
composed = replace(hydration.contribution, source=source)
return applied(
ResolvedInput(
capability=HMS_CAPABILITY,
value=(contribution_spec(composed),),
# Provenance, not a digest of the prompt: what lets a reader
# of the compiled prompt get from a rendered line back to the
# hydration that produced it.
provenance=hydration.revision,
),
counts=recall_counts(hydration),
# An APPLIED result carries no reason, so a route that could
# not be reached is said here. Silence would make a turn that
# recalled less look exactly like one with less to recall.
diagnostics=(
(reason,) if (reason := recall_reason(hydration)) else ()
),
)
return handle