feat(500): the reply shapes become server product content - a compact core and per-kind slices, each tied to its moment (#5493)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m29s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m29s
CI & Build / Build & push image (push) Skipped
services/reply_shapes.py is the single source of the default reply shapes: the core (every reply, ~1,800 chars against a 2,200 budget) and three slices - completion on work.finish, asks on reply.ask, plan on work.plan. Read through list_reply_shapes (MCP, read-only) and GET /api/retrieval/reply-shapes, one service behind both doors. Nothing delivers them yet; that is step 3. The skill still carries its copy until step 4 shrinks it to the long-form reference. The software-only vocabulary guard moves into tests/helpers.py (DEV_ONLY, dev_only_hits) rather than becoming a fourth copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"""The default shapes of a reply, and the moment each one rides (milestone 500).
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
A reply is where the person the work is for finds out what happened, and they
|
||||
were not there while it was done. Milestone 409 wrote the shapes that make a
|
||||
reply readable to them — conclusion first, the shortest reply that carries the
|
||||
answer, where the work stands — and put them in one bundled skill. A skill
|
||||
arrives only when the agent decides to load it, so in a long session the shape
|
||||
faded out of context exactly when replies were getting longer.
|
||||
|
||||
So the shapes are product content held here, on the server, and delivered by
|
||||
the moments they belong to rather than by the agent remembering to ask:
|
||||
|
||||
- the CORE applies to every reply, so it rides every turn — in full once per
|
||||
session and after a compaction, and as a one-line pointer otherwise;
|
||||
- each SLICE belongs to one kind of reply, and arrives at the moment that
|
||||
comes just before that kind is written: closing a task comes before a
|
||||
completion report, a structured question before an ask, opening a plan
|
||||
before a plan is put up for review.
|
||||
|
||||
The moment already knows which kind of reply is coming, so there is no "pick
|
||||
the type, then load its shape" step for an agent to skip.
|
||||
|
||||
ONE COPY
|
||||
|
||||
This module is the single source. The `reporting-back` skill keeps the
|
||||
long-form reference (a worked example, the reasoning) and points here; the
|
||||
hooks carry timing and transport and say nothing of their own about shape. An
|
||||
operator's adjustments are `preference` records mounted on the same moments,
|
||||
and arrive beside the default — where the two differ, the preference is what
|
||||
the operator asked for.
|
||||
|
||||
HOW TO WRITE ONE
|
||||
|
||||
Domain-neutral (rule 115): the work may be software, configuration,
|
||||
infrastructure or anything else a person drives through an agent. "Evidence"
|
||||
is what the reader could open to check; "verified" is whatever checking means
|
||||
for that work. The test beside this module refuses software-only vocabulary.
|
||||
|
||||
Short, because it is paid for in every session's context. The core's budget is
|
||||
asserted; a sentence added to it should replace one.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from scribe.services import moments
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReplyShape:
|
||||
"""One piece of the default reply shape, and when it arrives."""
|
||||
|
||||
key: str
|
||||
"""Stable identifier: what a delivery records and the Settings view keys on."""
|
||||
|
||||
title: str
|
||||
"""What the piece is, as the Settings view names it."""
|
||||
|
||||
moment: str
|
||||
"""The catalog moment this piece rides — where an operator's own
|
||||
preferences for it are mounted too."""
|
||||
|
||||
delivered: str
|
||||
"""When it arrives, said plainly for the Settings view."""
|
||||
|
||||
text: str
|
||||
"""The shape itself, as the agent reads it."""
|
||||
|
||||
|
||||
CORE_KEY = "core"
|
||||
|
||||
# ~550 tokens at four characters a token. The core is paid for in every
|
||||
# session, so this is the ceiling the test holds it to — not a target. It
|
||||
# shipped at ~1,800, leaving room for the one-line judgement (milestone 500
|
||||
# step 2) and little else.
|
||||
CORE_BUDGET_CHARS = 2200
|
||||
# A slice is paid for only at its moment, and once per session; it can afford
|
||||
# more than the core and still has to stay a slice, not the old skill again.
|
||||
SLICE_BUDGET_CHARS = 1400
|
||||
|
||||
|
||||
_CORE = """\
|
||||
Shape the reply around where the work stands, not the order you did things in. \
|
||||
The reader was not there while you worked.
|
||||
|
||||
- **Conclusion first** — the result, the verdict or the question, then what supports it.
|
||||
- **The shortest reply that carries the answer.** Length is work handed back to the \
|
||||
reader. It is earned by a comparison they asked for, options that need laying side by \
|
||||
side, or numbers that are the point.
|
||||
- **One topic per section; bold the few things that matter.**
|
||||
- **Plain words** — the reader's vocabulary, not names you coined while working.
|
||||
- **Place the work** — the task or plan it belongs to, by id and title, read from the \
|
||||
record rather than recalled. Work with no record behind it says so.
|
||||
- **Answer the standing questions, even with "nothing"**: does anything need them, and \
|
||||
what happens next. A section that explains earns its place only when it changes what \
|
||||
they do or decide; the rest belongs in the record's log.
|
||||
- **A decision they have made is the input to the work.** Act on it; reopen it only \
|
||||
for new evidence, said once.
|
||||
- **Holding an action until they say yes?** Put it near the top, under "Approval requested".
|
||||
- **End with the one thing they need to decide or do, in bold** — or say there is none.
|
||||
|
||||
By kind:
|
||||
- **Answer** — the answer, then what they could open to check it. An evaluation is \
|
||||
verdict · what exists · the gaps · a recommendation.
|
||||
- **Proposal** — two or three approaches, the trade-off of each, one recommendation. \
|
||||
A review is findings ranked by how much they matter.
|
||||
- **Progress** — a line or two. **Blocked** — what stopped, what you tried, what you need.
|
||||
- **Where are we** — the plan and its progress · done · open · needs you · next.
|
||||
|
||||
Before sending, read it as someone who was not there, reading quickly; then once more \
|
||||
for what can go."""
|
||||
|
||||
|
||||
_COMPLETION = """\
|
||||
A completion report, from the `placement` the closing call returned:
|
||||
|
||||
- **Where this sits** — the plan and the step, or the task alone when it has no plan.
|
||||
- **What now works** — outcomes the reader would notice ("you can now…", "X no longer…"), \
|
||||
not the steps behind them; those belong in the task's log.
|
||||
- **How / why** — only the decisions worth knowing, and how it was verified. Anything \
|
||||
that could not be verified is said here rather than left to read as passed.
|
||||
- **Needs you** — an action, an approval, a decision, or "nothing". It takes two tests: \
|
||||
is it theirs to decide, and is work waiting on it? A question you could settle by \
|
||||
reading or measuring something is work not yet done — settle it, say which way you went, \
|
||||
and leave them free to overrule.
|
||||
- **Next** — from `placement.next`, or say the plan is finished. An offer to fix \
|
||||
something you found goes here."""
|
||||
|
||||
|
||||
_ASKS = """\
|
||||
Asking them to decide or to act — pick the shape:
|
||||
|
||||
- **Decision** — the question first · two to four options, each with what it changes · \
|
||||
your recommendation first among them.
|
||||
- **Clarification** — "My reading is X · the gap is Y · unless you say otherwise I'll do Z."
|
||||
- **Handoff** (only they can do it) — the action · why it needs them · what it unblocks · \
|
||||
what you will do after.
|
||||
- **Approval** (ready, and holding for a yes) — under "Approval requested": exactly what \
|
||||
happens on yes, one numbered item per change so they can approve part · why it needs \
|
||||
them · how it is undone.
|
||||
- **Conflict** (what you are about to do clashes with a rule, a plan or an earlier \
|
||||
decision) — what it says · what you were about to do · where they clash · A or B?
|
||||
|
||||
Before asking, check whether you can find the answer yourself: something you can read \
|
||||
or measure is a fact to check, not a question to send. When an option rests on existing \
|
||||
behaviour, say whose call that was — theirs (cite it) or a past session's nobody confirmed."""
|
||||
|
||||
|
||||
_PLAN = """\
|
||||
A plan put up for review, before starting:
|
||||
|
||||
- **Goal** — what done looks like, and why, in a sentence or two.
|
||||
- **Steps** — in order, each small enough to check on its own.
|
||||
- **How you will know it works** — something observable, not "it is written".
|
||||
- **Open questions** — only the ones that are theirs to answer."""
|
||||
|
||||
|
||||
SHAPES: dict[str, ReplyShape] = {s.key: s for s in (
|
||||
ReplyShape(CORE_KEY, "Every reply", "reply.report",
|
||||
"every turn — in full once per session and after a compaction, "
|
||||
"otherwise as a one-line pointer", _CORE),
|
||||
ReplyShape("completion", "Completion report", "work.finish",
|
||||
"when a piece of work is closed", _COMPLETION),
|
||||
ReplyShape("asks", "Asking them to decide or act", "reply.ask",
|
||||
"when a structured question is put to them", _ASKS),
|
||||
ReplyShape("plan", "A plan for review", "work.plan",
|
||||
"when a plan is opened", _PLAN),
|
||||
)}
|
||||
|
||||
|
||||
def core() -> ReplyShape:
|
||||
return SHAPES[CORE_KEY]
|
||||
|
||||
|
||||
def for_moments(names: list[str]) -> list[ReplyShape]:
|
||||
"""The slices that ride these moments, in catalog order. The core is not a
|
||||
slice: it rides every turn, whatever moment the turn reaches."""
|
||||
wanted = set(names)
|
||||
return [s for s in SHAPES.values() if s.key != CORE_KEY and s.moment in wanted]
|
||||
|
||||
|
||||
def catalog() -> dict:
|
||||
"""The shapes as plain data, for the MCP tool and the REST door alike."""
|
||||
return {
|
||||
"shapes": [
|
||||
{"key": s.key, "title": s.title, "moment": s.moment,
|
||||
"means": moments.MOMENTS[s.moment].means,
|
||||
"delivered": s.delivered, "text": s.text}
|
||||
for s in SHAPES.values()
|
||||
],
|
||||
"total": len(SHAPES),
|
||||
}
|
||||
Reference in New Issue
Block a user