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")
if getattr(turn_request, "checkpoint_continuation", False):
return no_change("authenticated checkpoint continuation preserves memory state")
triage = getattr(capability, "triage", None)
prompt = str(getattr(turn_request, "prompt", "") or "")
if triage is not None and triage.classify(prompt) == "action":
# A pure-action turn carries nothing to recall against: the cue is
# a command, not a question, and a store round trip can only return
# memories the turn has no use for. The verdict comes from the
# composed triage instance, not the snapshot: STG-7 withholds a
# sibling stage's entry from a resolution stage, so the instance
# travels on the capability instead. The prompt read here is the
# same one the triage stage classifies, and classification is
# deterministic, so the recorded verdict and this skip agree.
# Absent triage this never fires, which is the old behaviour
# exactly. The skip is said here and counted as found=0: a
# retrieval that never ran must not look like one that ran and
# found nothing.
return no_change(
"triage classified this turn as pure action: no recall queried",
counts={"found": 0, "discarded": 0, "admitted": 0, "window": 0},
)
# 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 = stated_turn_scope(capability.scope, getattr(turn_request, "scope", None))
hydration = await capability.hydrator.hydrate(
MemoryQuery(
scope=turn_scope,
cue=capability.cue_from(turn_request),
limit=capability.limit,
recall_budget=capability.recall_budget,
read_admission=capability.read_admission,
session_id=str(getattr(turn_request, "session_id", "") or ""),
)
)
if capability.capture is not None:
try:
capability.capture.finalize(hydration)
except Exception:
# A local diagnostic cannot fail or change the recall turn.
capability.capture.invalidate()
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,
recalled_tags=recalled_tags(hydration.retrieval.result),
_selected_lines=hydration.selected_lines,
)
# ``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