Files
FabledScribe/src/scribe/services/reply_preferences.py
T
bvandeusenandClaude Opus 5.5 0720ab6dcf
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m45s
CI & Build / Build & push image (push) Successful in 28s
refactor(retrieval): the completion-report preferences run through the pipeline (milestone 456 step 3, #4905)
reply_preferences.completion_preferences was the last hand-written
rule search + record_retrieval + record_rule_surfaced triple outside the
pipeline. It is now REPORT_PREFERENCE, a RuleArm with kind="preference"
over COMPLETION_QUERY, read back as records rather than as lines.

- RuleArm gains `kind`. _ranked asks the ranker for that kind and drops
  anything else on the way out, before the row is logged.
- RuleResult gains `shown`, the (score, rule) pairs behind the lines, for
  callers that return records.
- RuleMoment.project_id may be None: logged as given, searched as
  `project_id or None`. report_preference rows keep their NULL project.
- Guards: reply_preferences now has 0 direct rule searches. The registry
  constant-source example moves from report_preference to preference_slot,
  the pipeline slot that records through a module constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:38:05 -04:00

142 lines
6.8 KiB
Python

"""The operator's own preferences for a completion report, at the moment it is written.
WHY THIS EXISTS (milestone 409 step 4)
The reporting-back skill ships DEFAULT shapes. An operator will want some of
them different ("my completion reports also say how it was tested", "decisions
as a numbered list"), and those adjustments are `preference` records. The gap
is the query: prompt-time retrieval matches the OPERATOR'S MESSAGE, and a shape
preference is about the REPLY. "Fix the flaky test" never retrieves "completion
reports should say how it was tested", so the preference is on file and never
arrives.
THE DECISION (operator, 2026-09-14, logged on the step): two deliveries, split
by reply kind.
- A COMPLETION REPORT has a moment the server can see — a task closing — so
the server retrieves for it and hands the matches back beside
`report_back`. That is this module.
- EVERY OTHER REPLY KIND (a finding, a decision, a handoff…) has no tool call
in front of it, so the reporting-back skill asks: it tells the agent to
`search(content_type="rule")` for that kind before writing.
Loading reply-shape preferences at session start was the rejected third option:
it is a small copy of the preloading milestone 394 retired, and just as
unmeasurable.
HOW A PREFERENCE SAYS IT IS ABOUT COMPLETION REPORTS
By its trigger, which is what it already has — no tag, no new column. A
preference's `when_to_apply` dominates its embedded document, so one written
for this moment ("writing the report after finishing a task") resembles
COMPLETION_QUERY below, and one about anything else does not. That keeps
delivery entirely in retrieval, as 394 decided, and leaves an operator nothing
new to learn: a preference reaches the completion report the same way every
other record reaches its moment.
THE BAR IS ITS OWN, AND THE FIRST READING EARNED IT (#3860)
This borrowed the prompt arm's key when it shipped, on the argument that a
fixed query against triggers is a different score distribution from an
operator's message against the same documents — true, and the reason the two
could not stay one dial. Five days of traffic settled it.
What the readout said: 69 calls, 69 declines, every one naming the SAME record
at the SAME score (rule 77 at 0.7194 against a 0.72 bar). That constancy is
the signature of this arm — `COMPLETION_QUERY` never varies, so for a given
corpus its best score is a constant, and a constant sitting under the bar is a
dead arm rather than a quiet one. The record it kept declining was about
reading a REQUEST, not about the shape of a report, so the decline was right
and the arm is healthy: this install simply has no completion-report
preference on file.
The bar stayed at 0.72, and the key moved out (REPORTPREF_THRESHOLD_KEY) so
that staying is a decision rather than a side effect of what the prose arm is
set to. A surface whose score cannot vary is the one surface where a borrowed
bar can be wrong forever without a single call looking unusual.
"""
from __future__ import annotations
import logging
from scribe.services import retrieval_pipeline as rp
from scribe.services.embeddings import semantic_search_rules
from scribe.services.retrieval_surfaces import SURFACES, budget_for, floor_for
from scribe.services.retrieval_telemetry import record_retrieval
from scribe.services.rule_usage import record_rule_surfaced
logger = logging.getLogger(__name__)
SOURCE = rp.REPORT_PREFERENCE.source
# Written in the vocabulary of the MOMENT, because that is what a trigger is
# written in and what this query is scored against. Domain-neutral on purpose
# (rule #115): a writing project or a home-infrastructure project closes tasks
# too, and its operator's preferences must match as well as a developer's.
COMPLETION_QUERY = (
"writing the completion report to the operator after finishing a task — "
"how that reply should be laid out and what it should include"
)
# A handful, not a menu. More than a few shape preferences for ONE kind of
# reply would contradict each other before they helped; the limit is here to
# keep one noisy corpus from turning a status change into a wall of text.
# The STARTING budget, not the budget (#4102). Both numbers this arm runs on
# now come from the surface registry, so the model that reads this arm's
# telemetry can move either — which matters more here than anywhere else,
# because a fixed query makes this arm's score a constant and a floor a hair
# above it produces a dead arm no amount of traffic will ever reveal.
LIMIT = SURFACES["report_preference"].budget_default
async def _threshold(user_id: int) -> float:
return await floor_for(user_id, SOURCE)
async def _limit(user_id: int) -> int:
return await budget_for(user_id, SOURCE)
async def completion_preferences(user_id: int, *, project_id: int | None = None) -> list[dict]:
"""The operator's preferences for a completion report, best match first.
KIND-FILTERED, for the reason `_reserve_slot_for_preference` gives: a
binding rule that happened to resemble the query would otherwise ride out
under a key that says "how the operator likes this written", which is a
claim about force the record does not make.
Every call is logged, the empty ones included: a surface that records only
the calls it liked reports a flawless clear-rate however badly its bar is
set. An install with no preferences at all logs nothing, because no search
ran (`searched` stays False) — that is not a decline.
Fails open to an empty list: this decorates a write that has already
happened, and a lookup that errors must not turn it into a failure.
"""
try:
# The stages — the kind filter, the unconditional call row, the
# fresh-only surfacing rows — are the one pipeline's (milestone 456).
# RANKED: this surface chose what it showed, so the name is in
# rule_usage.RANKED_SOURCES and its hits count toward pull-through.
# There is no session ledger here: a completion report is written
# once, so nothing it could repeat has been shown before.
result = await rp.run_rule_arm(
rp.REPORT_PREFERENCE,
rp.RuleMoment(
user_id=user_id, query=COMPLETION_QUERY, project_id=project_id,
),
floor=await _threshold(user_id), budget=await _limit(user_id),
io=rp.RuleIO(
search=semantic_search_rules,
record_retrieval=record_retrieval,
record_rule_surfaced=record_rule_surfaced,
),
)
return [
{"id": rule.id, "title": rule.title, "statement": rule.statement, "kind": "preference"}
for _score, rule in result.shown
]
except Exception: # noqa: BLE001 - a decoration never breaks its payload
logger.warning("completion preference lookup failed", exc_info=True)
return []