phase: 192_verbatim_inline_midturn
Phase 192 — final verification pass complete. - **Verified (all 4 tasks already complete):** additive `verbatim_block` SSE frame (`VerbatimBlockPiece` → `ChatVerbatimBlockEvent` → chat dispatch, after the round's `tool`/`tool_result`, before the next round); `hydrateVerbatimNow` matched-only mid-turn pass + app.js branch; `dropPlacedTwin` per-locator done/restore dedupe; the dedicated E2E suite. - **Root cause of the rejected attempt:** my own concurrent background `pytest` run deadlocked the harness validation run on the shared Postgres (`psycopg.errors.DeadlockDetected`). No code defect — re-ran every gate **exclusively**. - `uv run pytest -q --cov=app --cov-report=term-missing` → **exit 0**, `TOTAL 7181 16 99%` (>90%) - `uv run pytest tests/e2e/test_verbatim_inline_midturn.py --no-cov` → **6 passed** (isolated) - `uv run pytest tests/e2e/test_verbatim_show_text.py tests/e2e/test_chat_history.py --no-cov` → **18 passed** (`test_chat_history.py` byte-unedited; the phase-182 suite's "no eighth type" pin was tightened, not loosened, to seven + `verbatim_block` with frame-order asserts) - `uv run ruff check .` → All checks passed (exit 0) · `uv run pyright` → **0 errors** - **Completion criteria:** single frame per success + zero per refusal ✅ · `BOR_ANSWER_TEXT=0` wire byte-identical (integration spelled baseline + E2E App B) ✅ · fence hydrates in place / unfenced lands at the call moment, before `done` ✅ · append+fence → 1 panel, two ranges → 2 panels ✅ · restore 1 panel/locator, `done.answer_text` + record key + `verbatim_blocks=N` unchanged ✅ · suite green, coverage 99%, lint/types clean ✅ · no-show_text turn byte-identical ✅ · commit → left to the harness (no git index/history commands run). - **Docs:** README reviewed line-by-line (2399 lines) + AGENTS.md in full; mechanically verified all 86 `BOR_*` settings, every `.env.example` var, table defaults, internal anchors (0 broken), ports 8123/8901, tree depth 1–8, pagination 25/1–100, 17 palette colors, `0029`/`0030` migrations, and every cited test file. Fixed 1 inconsistency: the verbatim "Parity" bullet claimed the live turn/reload/shared page share "the same three globals" — now names the settle-path trio and states that `hydrateVerbatimNow` is the ONE chat-only addition (a shared page has no stream). - **Notable:** `run_agent`'s four repeated piece unions were collapsed into the ONE exported `AgentPiece` alias, so a future piece type is spelled in one place; `test_frontend_edit_message.py`'s `runTurn` byte-pin was regenerated, not weakened. - **Next pending phase:** `193_sync_remaining_count`.
This commit is contained in:
+88
-3
@@ -233,7 +233,17 @@ task 04):
|
||||
:data:`SHOW_TEXT_NOT_A_TEXT_DOC` refusal, the exact mirror of
|
||||
:data:`NOT_AN_IMAGE_DOC` — never a silent conversion), and the inline
|
||||
``` ```bor ``` locator-fence form of the same contract (phase 182 D2)
|
||||
is resolved at the API layer (task 05), NOT here.
|
||||
is resolved at the API layer (task 05), NOT here. **Phase 192 (task
|
||||
01, A1): the block reaches the user at the CALL moment, not at the
|
||||
end of the answer** — the round loop snapshots
|
||||
``len(holder.shown_texts)`` before the execution and yields one
|
||||
:class:`VerbatimBlockPiece` for whatever the call appended, which the
|
||||
API layer turns into the additive ``verbatim_block`` SSE frame (the
|
||||
panel lands while the model is still talking). Success-only, like the
|
||||
holder entry: a refusal appends nothing and yields nothing. The
|
||||
``done`` frame's ``answer_text`` list and the per-turn
|
||||
``verbatim_blocks=N`` count (the done-frame blocks) are UNCHANGED —
|
||||
that log field never counts the mid-turn frames.
|
||||
4. Every call the model emits is appended back to the message history as
|
||||
the assistant tool-call message + the tool result (refusals included),
|
||||
consumes one round, and the model is called again. Every tool result
|
||||
@@ -344,6 +354,55 @@ from app.rag.verbatim import VerbatimBlock, block_for
|
||||
|
||||
logger = logging.getLogger("app.agent")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VerbatimBlockPiece:
|
||||
"""One verbatim block a ``show_text`` call JUST displayed (phase 192).
|
||||
|
||||
The mid-turn sibling of :class:`app.rag.llm.ToolResultPiece` (phase
|
||||
95 — the other piece the agent loop yields that does NOT come from
|
||||
the model stream: it is derived from the executed call). Phase 182
|
||||
handed the block to the client only on the ``done`` frame's
|
||||
``answer_text`` list, so every panel popped in AFTER the model had
|
||||
finished talking about the bytes; phase 192 (task 01, A1) sends the
|
||||
very same block the moment it exists, as the additive
|
||||
``verbatim_block`` SSE frame — AFTER the round's ``tool`` /
|
||||
``tool_result`` frames and BEFORE the next model round.
|
||||
|
||||
SUCCESS-ONLY by construction: the single gate is the
|
||||
``holder.shown_texts.append`` in the ``show_text`` branch — every
|
||||
refusal (missing args, the ``answer_text=False`` posture, no
|
||||
document, an image row, the block cap, the already-shown dedupe)
|
||||
returns before it, so a refused call yields NO piece. :attr:`block`
|
||||
is the exact :class:`app.rag.verbatim.VerbatimBlock` the tool put on
|
||||
the holder (the server's bytes and the server's extent — LOCKED
|
||||
A1/A3), never a re-derivation. Frozen like its siblings: an
|
||||
immutable wire value. The ``done`` frame's ``answer_text``
|
||||
list and its ``verbatim_blocks=N`` count are UNCHANGED by this
|
||||
piece (task 05's resolution still runs over the finished answer);
|
||||
the mid-turn frames are not counted there.
|
||||
"""
|
||||
|
||||
#: The block the call displayed — the same object that rode
|
||||
#: ``holder.shown_texts`` (the phase-182 task-03 lineage: the
|
||||
#: server resolved it, the tool appended it on success only).
|
||||
block: VerbatimBlock
|
||||
|
||||
|
||||
#: The ONE union of every piece :func:`run_agent` yields — the model
|
||||
#: stream's pieces (:class:`app.rag.llm.StreamPiece` /
|
||||
#: :class:`~app.rag.llm.ToolCallPiece` / :class:`~app.rag.llm.RetryPiece`)
|
||||
#: plus the pieces DERIVED from an executed call: the phase-95
|
||||
#: :class:`~app.rag.llm.ToolResultPiece` (a truncated ``read``) and the
|
||||
#: phase-192 :class:`VerbatimBlockPiece` (a successful ``show_text``, the
|
||||
#: mid-turn ``verbatim_block`` frame). Named so the wire consumers — the
|
||||
#: ``app.api.chat`` pump and the test harnesses — spell the SAME union and
|
||||
#: a future piece type is added in exactly one place.
|
||||
type AgentPiece = (
|
||||
StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece | VerbatimBlockPiece
|
||||
)
|
||||
|
||||
|
||||
#: The three agent tools (phase 70: the harness-aligned surface —
|
||||
#: ``ls`` / ``read`` / ``grep``, the pi.dev tool shapes the model was
|
||||
#: trained on, replacing the phase-37 list/read and phase-68 search
|
||||
@@ -2413,7 +2472,7 @@ async def run_agent(
|
||||
settings: Settings,
|
||||
holder: AgentHolder,
|
||||
history: Sequence[dict[str, Any]] = (),
|
||||
) -> AsyncIterator[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]:
|
||||
) -> AsyncIterator[AgentPiece]:
|
||||
"""Run the grounded-turn tool loop, yielding every stream piece.
|
||||
|
||||
Every piece (``thinking`` / ``content`` / tool calls /
|
||||
@@ -2423,7 +2482,18 @@ async def run_agent(
|
||||
longer than ``settings.read_max_chars``) additionally yields one
|
||||
:class:`ToolResultPiece` per truncated call, AFTER the round's
|
||||
``tool`` frame and BEFORE the next model round (the API layer turns it
|
||||
into an SSE ``tool_result`` frame, task 02). After the loop finishes,
|
||||
into an SSE ``tool_result`` frame, task 02). A successful
|
||||
``show_text`` (phase 182) additionally yields exactly ONE
|
||||
:class:`VerbatimBlockPiece` per block it appended to
|
||||
``holder.shown_texts``, with the SAME emission-order contract —
|
||||
AFTER the round's ``tool`` / ``tool_result`` frames and BEFORE the
|
||||
next model round — so the API layer can put the verbatim panel on
|
||||
screen the moment the call succeeds instead of at ``done`` (phase
|
||||
192, task 01, A1). Emission rides the ``shown_texts`` append, which
|
||||
is the branch's SINGLE success gate: every refusal (missing ``path``,
|
||||
the ``answer_text=False`` posture, no document, an ``is_image`` row,
|
||||
the ``show_text_max_blocks`` cap, the already-shown dedupe) returns
|
||||
before it and emits nothing. After the loop finishes,
|
||||
*holder* carries the read documents, the executed tool-call count
|
||||
(re-lists included), the ``read_truncations`` signal list, and the
|
||||
``read_coverage`` spans (phase 141, task 01 — the lines each
|
||||
@@ -2640,6 +2710,12 @@ async def run_agent(
|
||||
# round, so at most one new entry — the loop still iterates the
|
||||
# tail, so a future multi-call round stays correct).
|
||||
trunc_before = len(holder.read_truncations)
|
||||
# Phase 192 (task 01, A1): the same snapshot-then-yield shape for
|
||||
# the verbatim blocks — only what THIS call appended becomes a
|
||||
# ``VerbatimBlockPiece`` (a round adds at most one, the
|
||||
# already-shown dedupe being a refusal, but the tail loop keeps a
|
||||
# future multi-add correct).
|
||||
shown_before = len(holder.shown_texts)
|
||||
with db_factory() as tool_db:
|
||||
result = _execute_tool(tool_db, call, seed_docs, holder, settings)
|
||||
rounds += 1 # every call the model emits consumes a round
|
||||
@@ -2694,6 +2770,15 @@ async def run_agent(
|
||||
chars_shown=shown,
|
||||
chars_total=total,
|
||||
)
|
||||
# Phase 192 (task 01, A1): surface this round's verbatim block(s)
|
||||
# to the API layer — the block the ``show_text`` call JUST
|
||||
# displayed, sent at the moment it exists instead of at ``done``.
|
||||
# Same emission order as the truncation piece above (AFTER the
|
||||
# round's ``tool`` / ``tool_result`` frames, BEFORE the next model
|
||||
# round); task 02 turns it into the additive ``verbatim_block``
|
||||
# SSE frame. A refused call appended nothing, so nothing is sent.
|
||||
for block in holder.shown_texts[shown_before:]:
|
||||
yield VerbatimBlockPiece(block=block)
|
||||
if rounds >= max_rounds:
|
||||
logger.warning(
|
||||
"agent round cap reached (rounds=%d) — forcing a final "
|
||||
|
||||
Reference in New Issue
Block a user