phase: 182_verbatim_show_text
**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**
- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
This commit is contained in:
+105
-7
@@ -66,6 +66,20 @@ backstop). The ``DEFLECT_MODE`` marker and everything else in the
|
||||
prompt stay put — the E2E mock LLM keys on the marker's *presence*,
|
||||
not the wording, so that contract is unchanged.
|
||||
|
||||
Display-tool clauses (phase 124 ``show_image``; phase 182 ``show_text``):
|
||||
the **HIGH** prompt's ``<tools>`` section can carry two extra clauses
|
||||
before its closing tag, each appended only when its own setting is on —
|
||||
:data:`SHOW_IMAGE_CLAUSE` when ``BOR_ANSWER_IMAGES`` (default OFF) and
|
||||
:data:`SHOW_TEXT_CLAUSE` when ``BOR_ANSWER_TEXT`` (default ON). The text
|
||||
clause teaches the two things the phase-182 incident needed said: the
|
||||
user copies from what is DISPLAYED (so never retype a file), and the
|
||||
inline `` ```bor <source/path> [range] ``` `` locator fence whose EMPTY
|
||||
body the app replaces with the server-resolved bytes. Both clauses
|
||||
compose through one mechanism (section minus its final tag + the present
|
||||
clauses + the tag), so a build with neither clause is byte-identical to
|
||||
pre-phase and a ``show_image``-only build reproduces the phase-124 bytes
|
||||
exactly; the LOW prompt carries neither.
|
||||
|
||||
Disclosed-answer contract (phase 112, task 03; owner decision
|
||||
2026-09-14, roadmap confirmation — option (iii) of the three the
|
||||
2026-09-15 interactive deflection test raised): that test found the
|
||||
@@ -388,6 +402,61 @@ SHOW_IMAGE_CLAUSE: str = (
|
||||
"in text either way."
|
||||
)
|
||||
|
||||
#: The ``show_text`` contract clause (phase 182, task 04, LOCKED A1–A3/A6) —
|
||||
#: appended INSIDE the ``<tools>`` section of the HIGH prompt, after the
|
||||
#: existing three-tool copy (and after :data:`SHOW_IMAGE_CLAUSE` when that
|
||||
#: rides too), ONLY when :func:`build_high_prompt` is called with
|
||||
#: ``show_text=True`` (the ``BOR_ANSWER_TEXT`` master switch, via
|
||||
#: ``app.api.chat``'s grounded plan — the only call site that passes it).
|
||||
#: It exists because the incident this phase fixes is a PROMPT gap, not a
|
||||
#: tool gap: nothing ever told the model that the user copies from what is
|
||||
#: DISPLAYED, so it retyped files out of context and every retype differed
|
||||
#: from the stored bytes by a character. What each sentence pins, in order:
|
||||
#: (1) the display contract — the tool shows the document EXACTLY as stored
|
||||
#: and the copy source is the display, not the prose; (2) when to call it —
|
||||
#: the user wants to SEE, COPY or RUN a file (the owner's quadlet / config /
|
||||
#: script shapes) rather than read content out of the answer; (3) the do-
|
||||
#: NOT-retype rule with its reason (anything the model types may differ by
|
||||
#: one character and the user cannot tell — the phase's reason for
|
||||
#: existing); (4) the placement syntax — the inline `` ```bor <source/path>
|
||||
#: [range] ``` `` locator fence with an EMPTY body (D1/D7: a fence is a
|
||||
#: resolution request, the app supplies the text, and D3 keeps the model's
|
||||
#: body out of the DOM); (5) the honesty-about-numbers rule — line numbers
|
||||
#: come from ``grep``/``read``, and when unsure the range is OMITTED
|
||||
#: because the displayed extent is the server's (A2); (6) the failure rule
|
||||
#: — refuse out loud, never reconstruct from memory (the persona's radical
|
||||
#: honesty, D3's posture on the model side); (7) the image pointer
|
||||
#: (LOCKED A6: images belong to ``show_image``, never silently converted).
|
||||
#: ``TOOLS_SECTION`` itself stays BYTE-IDENTICAL (the sha pins in
|
||||
#: :mod:`tests.unit.test_prompt_lock` hold); a ``show_text=False`` build
|
||||
#: (the default — every pre-phase call site) is byte-identical to the
|
||||
#: pre-task output, and a ``show_image=True, show_text=False`` build
|
||||
#: reproduces the phase-124 bytes EXACTLY. The ``<tools>`` marker the E2E
|
||||
#: mock keys on is untouched either way — both clauses ride on its
|
||||
#: presence, not its wording. The LOW/deflection prompt never carries the
|
||||
#: clause: a deflected turn has no tools, and phase 182's D9 forbids
|
||||
#: resolving a locator on a turn that just disclaimed the knowledge base.
|
||||
SHOW_TEXT_CLAUSE: str = (
|
||||
"`show_text` displays part of a document to the user EXACTLY as stored "
|
||||
"— the user copies from what is displayed, not from your prose. Call "
|
||||
"it whenever the user wants to SEE, COPY or RUN a file — a quadlet, a "
|
||||
"config, a script, a unit file — instead of writing the content into "
|
||||
"your answer yourself. After calling it, do NOT repeat the displayed "
|
||||
"lines in your answer — describe them, explain them, point at them. "
|
||||
"Anything you type yourself may differ from the file by a character, "
|
||||
"and the user cannot tell. To put a displayed block exactly where you "
|
||||
"want it in your answer, write a locator line on its own: three "
|
||||
"backticks followed by `bor`, a space, the combined `source/path`, and "
|
||||
"optionally the line range (` 12-40`). Leave its body EMPTY — the app "
|
||||
"replaces it with the exact stored text. Never type the file's content "
|
||||
"inside a `bor` block. Never guess line numbers — use the numbers "
|
||||
"`grep` and `read` reported. When you are not sure which lines, OMIT "
|
||||
"the range and display the whole document: the numbers you display are "
|
||||
"the server's, never yours. If `show_text` refuses, say what you could "
|
||||
"not display — do not reconstruct it from memory. For an image use "
|
||||
"`show_image` instead."
|
||||
)
|
||||
|
||||
|
||||
def history_to_messages(
|
||||
history: Sequence[HistoryTurn],
|
||||
@@ -762,6 +831,7 @@ def build_high_prompt(
|
||||
kb_overview: str | None = None,
|
||||
folder_lines: Sequence[str] = (),
|
||||
show_image: bool = False,
|
||||
show_text: bool = False,
|
||||
) -> str:
|
||||
"""Grounded turn: locked persona (+ steering, + KB overview) + the
|
||||
top-ranked documents' SUMMARY blocks + the ``<tools>`` instructions
|
||||
@@ -771,7 +841,8 @@ def build_high_prompt(
|
||||
seeds SUMMARIES, never full texts — full text enters the context
|
||||
only through the capped ``read`` tool; phase 119, LOCKED A4: the
|
||||
suggested-folder context lines, below; phase 124, task 03, LOCKED
|
||||
A5: the optional :data:`SHOW_IMAGE_CLAUSE`, below).
|
||||
A5: the optional :data:`SHOW_IMAGE_CLAUSE`, below; phase 182, task 04,
|
||||
LOCKED A1–A3: the optional :data:`SHOW_TEXT_CLAUSE`, below).
|
||||
|
||||
Section order: ``<relevance>`` → ``<knowledge_base>`` → ``<tuning>``
|
||||
→ ``<documents>`` → ``<tools>``; empty steering/overview omit their
|
||||
@@ -796,6 +867,22 @@ def build_high_prompt(
|
||||
never carries the clause — a deflected turn has no tools and never
|
||||
shows an image.
|
||||
|
||||
*show_text* (phase 182, task 04, LOCKED A1–A3): when true (the caller
|
||||
passes the ``BOR_ANSWER_TEXT`` master switch — the same single grounded
|
||||
call site), the :data:`SHOW_TEXT_CLAUSE` rides INSIDE the same
|
||||
``<tools>`` section, AFTER :data:`SHOW_IMAGE_CLAUSE` when that is
|
||||
present too and always before the closing tag. The two flags compose
|
||||
independently through one mechanism (the built section is
|
||||
``TOOLS_SECTION`` minus its final ``</tools>`` + the present clauses,
|
||||
blank-line separated, + the closing tag): ``show_image`` only
|
||||
reproduces the phase-124 bytes exactly, ``show_text`` only is the
|
||||
three-tool copy plus the text clause, both is the image clause then
|
||||
the text clause, neither is byte-identical to pre-phase. The default
|
||||
``False`` keeps every pre-existing call byte-identical. The deflection
|
||||
prompt NEVER carries it — a deflected turn has no tools, and phase
|
||||
182's D9 forbids resolving a ``bor`` locator on a turn whose answer
|
||||
just disclaimed the knowledge base.
|
||||
|
||||
When at least one block is present, the section leads with the
|
||||
start-here :data:`SUGGEST_INTRO` line (before the first block — the
|
||||
phase-15 ``_STEERING_INTRO`` / phase-31 ``_KB_INTRO`` precedent): the
|
||||
@@ -861,13 +948,24 @@ def build_high_prompt(
|
||||
for part in (build_kb_section(kb_overview or ""), build_steering_section(notes or [])):
|
||||
if part:
|
||||
prompt += "\n" + part
|
||||
# Phase 124 (task 03, LOCKED A5) / phase 182 (task 04, LOCKED A1): the
|
||||
# display-tool clauses ride inside <tools> — TOOLS_SECTION's text minus
|
||||
# its final </tools>, the clauses offered for this build (each on its
|
||||
# own line, blank-line separated, after the existing three-tool copy),
|
||||
# then the closing tag. TOOLS_SECTION itself is unedited, the
|
||||
# no-clause path keeps the exact pre-task concatenation, and a
|
||||
# show_image-only build reproduces the phase-124 bytes exactly.
|
||||
clauses: list[str] = []
|
||||
if show_image:
|
||||
# Phase 124 (task 03, LOCKED A5): the clause rides inside
|
||||
# <tools> — TOOLS_SECTION's text minus its final </tools>, the
|
||||
# clause (on its own line, after the existing three-tool copy),
|
||||
# then the closing tag. TOOLS_SECTION itself is unedited and the
|
||||
# show_image=False path keeps the exact pre-task concatenation.
|
||||
tools_section = TOOLS_SECTION[: -len("</tools>")] + SHOW_IMAGE_CLAUSE + "\n</tools>"
|
||||
clauses.append(SHOW_IMAGE_CLAUSE)
|
||||
if show_text:
|
||||
clauses.append(SHOW_TEXT_CLAUSE)
|
||||
if clauses:
|
||||
tools_section = (
|
||||
TOOLS_SECTION[: -len("</tools>")]
|
||||
+ "\n\n".join(clauses)
|
||||
+ "\n</tools>"
|
||||
)
|
||||
else:
|
||||
tools_section = TOOLS_SECTION
|
||||
return prompt + "\n<documents>\n" + body + "\n</documents>\n" + tools_section
|
||||
|
||||
Reference in New Issue
Block a user