feat(500): reply shapes are delivered - the core every turn through the ledger, each slice at its moment, reply mounts before the reply (#5495)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m31s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Failing after 1m31s
CI & Build / Build & push image (push) Skipped
- Every turn (/api/plugin/retrieve, UserPromptSubmit): the core reply shape leads the payload - in full the first time, as its one-line reminder after that - followed by whatever is mounted on reply.report, under the shared rule ledger. Fresh keys come back as shape_keys. - The ledger is <sid>.shapes.ids in scribe-priorart (scribe_shapes_file / _seen / _append), so the compaction sweep that clears every .ids ledger is what brings the full core back after one. - At a moment (/api/plugin/moment): the slice for that reply - completion on work.finish, asks on reply.ask, plan on work.plan - ahead of the mounted rules. reachable_tools now lists tools reaching a shaped moment even on an install with nothing mounted. - Scribe's own tools (attach_moment_rules): reply_shape in the response, in full, since that door has no ledger. enter_project carries the core for clients with no prompt hook. - Telemetry: one AppLog row per delivery (plugin / reply_shape), each shape with full or pointer and the door (turn, hook, mcp). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ from scribe.config import Config
|
||||
from scribe.services import lesson_rules as lesson_rules_svc
|
||||
from scribe.services import moment_delivery as moment_delivery_svc
|
||||
from scribe.services import plugin_context as plugin_ctx_svc
|
||||
from scribe.services import reply_shapes as reply_shapes_svc
|
||||
from scribe.services import repo_bindings as repo_bindings_svc
|
||||
from scribe.services import report_check as report_check_svc
|
||||
from scribe.services import rule_moment_judgments as judgments_svc
|
||||
@@ -126,6 +127,17 @@ async def autoinject_retrieve():
|
||||
different things about what the reader holds —
|
||||
so they get different lines (#4100). Shares the
|
||||
ledger directory and the same clear-on-compact.
|
||||
shapes_seen (opt) — comma-separated reply-shape keys this session was
|
||||
already shown in full (milestone 500). The core
|
||||
goes out in full when `core` is absent, as its
|
||||
one-line reminder when present; the hook's
|
||||
ledger is swept on compaction, which is how the
|
||||
full core comes back after one.
|
||||
|
||||
THE TURN'S REPLY SHAPE COMES FIRST (milestone 500 step 3). Every turn ends
|
||||
in a reply and this is the last point before it is written, so the core
|
||||
shape and whatever is mounted on `reply.report` lead the payload. Fresh
|
||||
shape keys come back as `shape_keys` for the ledger.
|
||||
|
||||
TWO ARMS, TWO SETS OF GATES. Rules ride the same hook and the same query
|
||||
but nothing else: the notes menu can be disabled, thresholded and top-k'd
|
||||
@@ -159,9 +171,19 @@ async def autoinject_retrieve():
|
||||
g.user.id, result.get("lesson_ids") or [], rules.get("shown_rule_ids") or [],
|
||||
arm="p", situation=q, project_id=project_id or None,
|
||||
)
|
||||
blocks = [b for b in (rules["context"], result["context"], proposal) if b]
|
||||
# The reply mounts go through the same rule ledger; a rule the prompt arm
|
||||
# just named is not quoted a second time by the turn's delivery.
|
||||
turn = await moment_delivery_svc.deliver_for_turn(
|
||||
g.user.id, seen=reply_shapes_svc.parse_seen(request.args.get("shapes_seen")),
|
||||
project_id=project_id,
|
||||
exclude=frozenset(exclude_rule_ids) | frozenset(rules["rule_ids"]),
|
||||
held=frozenset(held_rule_ids),
|
||||
)
|
||||
blocks = [b for b in (turn["context"], rules["context"], result["context"], proposal) if b]
|
||||
result["context"] = "\n\n".join(blocks)
|
||||
result["rule_ids"] = rules["rule_ids"]
|
||||
result["rule_ids"] = list(dict.fromkeys([*rules["rule_ids"], *turn["rule_ids"]]))
|
||||
result["shape_keys"] = [k for k, form in turn["shape_forms"].items()
|
||||
if form == reply_shapes_svc.FULL]
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@@ -275,12 +297,16 @@ async def moment():
|
||||
`rule_ids` (the FRESH ones, for the hook to append to the ledger) and
|
||||
`moments` (the names reached). A lookup, not a ranked search: no
|
||||
retrieval_logs row; each fresh rule is recorded surfaced with its moment.
|
||||
|
||||
A moment that carries a default reply shape (milestone 500) brings it too,
|
||||
ahead of the rules: in full unless `shapes_seen` names it, then as its
|
||||
reminder. `shape_keys` are the ones sent in full, for the hook's ledger.
|
||||
"""
|
||||
event = await request.get_json(silent=True) or {}
|
||||
tool = str(event.get("tool_name") or "").strip()
|
||||
tool_input = event.get("tool_input")
|
||||
if not tool:
|
||||
return jsonify({"context": "", "rule_ids": [], "moments": []})
|
||||
return jsonify({"context": "", "rule_ids": [], "moments": [], "shape_keys": []})
|
||||
project_id, _repo, _unbound = await _project_scope()
|
||||
reached, result = await moment_delivery_svc.deliver_for_act(
|
||||
g.user.id, tool, tool_input if isinstance(tool_input, dict) else {},
|
||||
@@ -288,10 +314,16 @@ async def moment():
|
||||
exclude=frozenset(_int_list(request.args.get("exclude_rule_ids"))),
|
||||
held=frozenset(_int_list(request.args.get("held_rule_ids"))),
|
||||
)
|
||||
shapes, forms = moment_delivery_svc.shapes_for_act(
|
||||
reached, reply_shapes_svc.parse_seen(request.args.get("shapes_seen")),
|
||||
)
|
||||
await reply_shapes_svc.record_delivery(g.user.id, forms, via="hook")
|
||||
context = "\n\n".join(b for b in ("\n\n".join(shapes), "\n".join(result.lines)) if b)
|
||||
return jsonify({
|
||||
"context": "\n".join(result.lines),
|
||||
"context": context,
|
||||
"rule_ids": result.rule_ids,
|
||||
"moments": [hit["moment"] for hit in reached],
|
||||
"shape_keys": [k for k, form in forms.items() if form == reply_shapes_svc.FULL],
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user