phase: 192_verbatim_inline_midturn
Build and Push Containers / build-and-push-app (push) Successful in 1m56s
Build and Push Containers / build-and-push-db (push) Successful in 14s

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:
2026-10-19 12:19:51 -04:00
parent 4f20c256d7
commit fb17d15fbe
49 changed files with 17313 additions and 102 deletions
+9 -9
View File
@@ -47,14 +47,14 @@ from app.config import Settings
from app.db import SessionLocal
from app.models import Document, FolderSummary, GitSource
from app.rag import agent
from app.rag.agent import AGENT_TOOLS, SHOW_TEXT_TOOL, AgentHolder, run_agent
from app.rag.llm import (
LLMClient,
RetryPiece,
StreamPiece,
ToolCallPiece,
ToolResultPiece,
from app.rag.agent import (
AGENT_TOOLS,
SHOW_TEXT_TOOL,
AgentHolder,
AgentPiece,
run_agent,
)
from app.rag.llm import LLMClient, StreamPiece, ToolCallPiece
if TYPE_CHECKING:
from app.rag.scaffolding import ScaffoldingFilter
@@ -286,8 +286,8 @@ async def _consume(
llm: LLMClient,
db_factory: Callable[[], Session],
holder: AgentHolder,
) -> list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]:
out: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece] = []
) -> list[AgentPiece]:
out: list[AgentPiece] = []
async for piece in run_agent(
llm,
db_factory, # SEC-14-04: session factory (short-lived sessions)
+4 -4
View File
@@ -28,8 +28,8 @@ from sqlalchemy.orm import Session
from app.config import Settings
from app.db import SessionLocal
from app.models import Document, GitSource
from app.rag.agent import AgentHolder, _budget_note, run_agent
from app.rag.llm import LLMClient, RetryPiece, StreamPiece, ToolCallPiece, ToolResultPiece
from app.rag.agent import AgentHolder, AgentPiece, _budget_note, run_agent
from app.rag.llm import LLMClient, StreamPiece, ToolCallPiece
if TYPE_CHECKING:
from app.rag.scaffolding import ScaffoldingFilter
@@ -128,9 +128,9 @@ def _run_call(
async def _consume(
llm: LLMClient, db: Session, holder: AgentHolder
) -> list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]:
) -> list[AgentPiece]:
"""SEC-14-04: uses a short-lived session per tool call."""
out: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece] = []
out: list[AgentPiece] = []
async for piece in run_agent(
llm,
lambda: SessionLocal(), # SEC-14-04: session factory (short-lived sessions)
+254 -1
View File
@@ -41,7 +41,7 @@ from app.rag.importer import import_sources
from app.rag.llm import EmbeddingError, LLMError, StreamPiece, ToolCallPiece, ToolResultPiece
from app.rag.prompts import SHOW_TEXT_CLAUSE, build_high_prompt
from app.rag.retriever import TRUNCATION_MARKER
from app.schemas import ChatDoneEvent, SourceRef
from app.schemas import ChatDoneEvent, SourceRef, VerbatimBlockRef
from tests.conftest import ADMIN_PASSWORD
#: The fixture documents' fixed creation date (phase 106, D5): the
@@ -3276,6 +3276,9 @@ def test_grounded_turn_carries_show_text_clause_and_tool_per_the_flag(
#: A fixture doc with a stable, countable body (16 lines as imported).
_K8S = "docs/homelab/kubernetes.md"
#: The second countable fixture doc (phase 192: the two-call turn needs two
#: different canonical locators — the ``shown_texts`` dedupe is per locator).
_BACKUPS = "docs/homelab/backups.md"
def _verbatim_settings(**overrides: Any) -> Settings:
@@ -3299,6 +3302,20 @@ def _doc_lines(db, combined: str) -> list[str]:
return content.splitlines()
def _doc_title(db, combined: str) -> str:
"""The indexed row's ``title`` — the same authority the resolver puts on
the block, never a string retyped here (the phase-182 ``_doc_lines``
convention)."""
source, _, path = combined.partition("/")
title = db.scalar(
select(Document.title).where(
Document.source == source, Document.path == path
)
)
assert title, combined
return str(title)
def test_show_text_turn_done_frame_carries_the_server_block(
client, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
@@ -3608,3 +3625,239 @@ def test_resolution_failure_degrades_to_no_blocks(
assert any("verbatim block resolution failed" in m for m in lines)
turn = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
assert turn and turn[-1].endswith("verbatim_blocks=0")
# ---------------------------------------------------------------------------
# Phase 192 (task 01, A1) — the additive mid-turn ``verbatim_block`` frame.
#
# A successful ``show_text`` used to reach the client ONLY on the ``done``
# frame's ``answer_text`` list (phase 182 task 05), so every panel popped in
# after the model had finished talking about the bytes. The agent loop now
# also yields the block the moment the call succeeds, and the endpoint turns
# it into one additive SSE frame — after the round's ``tool`` /
# ``tool_result`` frames, before the next model round. The ``done`` frame,
# the saved record and the ``verbatim_blocks=N`` log field keep the phase-182
# contract byte for byte; a ``BOR_ANSWER_TEXT=0`` turn streams NOTHING new.
# ---------------------------------------------------------------------------
def test_verbatim_block_frame_lands_midturn_before_done(
client, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
"""A scripted grounded turn whose model calls
``show_text(docs/homelab/kubernetes.md, offset=3, limit=3)`` → the stream
carries exactly ONE ``type="verbatim_block"`` frame with the EXACT
:class:`VerbatimBlockRef` field set (plus ``type``), positioned after the
round's ``tool`` frame and before the answer's ``delta`` frames and the
``done`` frame. Its bytes are the DB's, its extent is the server's, and
the ``done`` frame still carries the SAME block in ``answer_text`` (the
restore/shared-page contract is untouched) — counted ONCE by the per-turn
``verbatim_blocks=N`` line, which never counts mid-turn frames."""
scripted = FakeRagLLM(
tool_script=[
[
ToolCallPiece(
id="call_1",
name="show_text",
arguments={"path": _K8S, "offset": 3, "limit": 3},
)
]
]
)
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: scripted
try:
with caplog.at_level("INFO", logger="app.chat"):
_, _, frames = _stream_chat(client, QUESTION)
finally:
fastapi_app.dependency_overrides.clear()
types = [f["type"] for f in frames]
assert types.count("verbatim_block") == 1
i_tool, i_block, i_done = types.index("tool"), types.index("verbatim_block"), len(frames) - 1
assert types[i_done] == "done"
# The frame sits inside its own round: after the ``tool`` frame, before
# the next round's answer deltas and long before ``done``.
assert i_tool < i_block < types.index("delta") < i_done
lines = _doc_lines(db, _K8S)
frame = frames[i_block]
assert frame == {
"type": "verbatim_block",
"source": "docs",
"path": "homelab/kubernetes.md",
"title": "Kubernetes Homelab Cluster",
"start_line": 3,
"end_line": 5,
"total_lines": len(lines),
"text": "\n".join(lines[2:5]),
"truncated": False,
"locator": f"{_K8S}#3-5",
}
# The key set is the done-frame item's set plus ``type`` — the client's
# ``isVerbatimBlock`` guard accepts the event as-is.
assert set(frame) == set(VerbatimBlockRef.model_fields) | {"type"}
# The done frame is UNCHANGED from phase 182: the same block, in the same
# shape, at the same place (the mid-turn frame is additive, never a
# replacement — the client's idempotence keeps it from painting twice).
done = frames[i_done]
assert done["answer_text"] == [{k: v for k, v in frame.items() if k != "type"}]
turn = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
assert turn and turn[-1].endswith("verbatim_blocks=1")
def test_kill_switch_stream_is_byte_identical(
client, db, seeded_kb: FakeRagLLM, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The wire pin for the kill switch: with ``BOR_ANSWER_TEXT=0`` the tool
is never offered, so a scripted ``show_text`` attempt lands on the
phase-182 refusal path and the captured frame sequence is EXACTLY the
pre-phase baseline — one ``tool`` frame, the answer's ``delta`` frames,
the pre-phase ``done`` frame — with NO ``verbatim_block`` frame anywhere
and no ``answer_text`` key on any frame."""
answer = "The flag is off, so nothing is displayed here."
monkeypatch.setattr(
chat_api, "get_settings", lambda: _verbatim_settings(answer_text=False)
)
scripted = FakeRagLLM(
answer=answer,
tool_script=[
[
ToolCallPiece(
id="call_1",
name="show_text",
arguments={"path": _K8S, "offset": 3, "limit": 3},
)
]
],
)
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: scripted
try:
_, _, frames = _stream_chat(client, QUESTION)
finally:
fastapi_app.dependency_overrides.clear()
# The documented pre-phase baseline, spelled out: the refused call's
# ``tool`` frame (the exact ChatToolEvent bytes — no range keys, they are
# ``read``-only), then one ``delta`` frame per 12-char slice of the
# canned answer, then ``done``. Nothing else, in that order.
expected = [{"type": "tool", "name": "show_text", "argument": _K8S}] + [
{"type": "delta", "text": answer[i : i + 12]} for i in range(0, len(answer), 12)
]
assert frames[:-1] == expected
assert frames[-1]["type"] == "done"
assert set(frames[-1]) == {"type", "deflected", "sources", "related", "suggestions"}
assert "verbatim_block" not in json.dumps(frames)
assert "answer_text" not in json.dumps(frames)
# The refusal the model saw is the phase-182 one (the tool does not exist
# while the flag is off), and no block was recorded anywhere.
tool_message = scripted.seen_messages[1][3]
assert tool_message["role"] == "tool"
assert tool_message["content"].startswith("Unknown tool.")
def test_done_frame_and_log_line_unchanged_by_midturn_frames(
client, db, seeded_kb: FakeRagLLM, caplog: pytest.LogCaptureFixture
) -> None:
"""Phase 192 (task 03, A4) — the frame the mid-turn pass adds is a COPY of
a done-frame item, never a participant in the done-frame contract.
Two agent rounds, two successful ``show_text`` calls, no fence in the
answer: the stream carries TWO ``verbatim_block`` frames (each in its own
round, before ``done``) and the ``done`` frame still carries exactly the
phase-182 resolution — both blocks in CALL order with the server's
extents, byte-identical to what the resolver reads out of the row. That
list is the saved record's ``answer_text`` and what the reload / shared
page hydrates from, so the dedupe the client performs lands on data the
server never changed. The per-turn line therefore reads
``verbatim_blocks=2`` — the DONE-frame count — and never 4: the mid-turn
frames are counted NOWHERE."""
scripted = FakeRagLLM(
tool_script=[
[
ToolCallPiece(
id="call_1",
name="show_text",
arguments={"path": _K8S, "offset": 3, "limit": 3},
)
],
[
ToolCallPiece(
id="call_2",
name="show_text",
arguments={"path": _BACKUPS, "offset": 1, "limit": 2},
)
],
]
)
fastapi_app.dependency_overrides[chat_api.get_llm] = lambda: scripted
try:
with caplog.at_level("INFO", logger="app.chat"):
_, _, frames = _stream_chat(client, QUESTION)
finally:
fastapi_app.dependency_overrides.clear()
types = [f["type"] for f in frames]
done = frames[-1]
assert types[-1] == "done"
mid = [f for f in frames if f["type"] == "verbatim_block"]
assert len(mid) == 2 and types.count("verbatim_block") == 2
# Each frame rides its own round: after that round's ``tool`` frame and
# long before ``done`` (the phase-192 emission order, twice).
tool_positions = [i for i, t in enumerate(types) if t == "tool"]
assert len(tool_positions) == 2
for round_index, frame in enumerate(mid):
assert tool_positions[round_index] < frames.index(frame) < len(frames) - 1, (
"the frame lands inside its OWN round: after its tool line, before done"
)
# The done frame is the phase-182 resolution, spelled out from the row —
# the same two blocks, in call order, with the server's extent/locator.
k8s_lines = _doc_lines(db, _K8S)
backup_lines = _doc_lines(db, _BACKUPS)
assert done["answer_text"] == [
{
"source": "docs",
"path": "homelab/kubernetes.md",
"title": _doc_title(db, _K8S),
"start_line": 3,
"end_line": 5,
"total_lines": len(k8s_lines),
"text": "\n".join(k8s_lines[2:5]),
"truncated": False,
"locator": f"{_K8S}#3-5",
},
{
"source": "docs",
"path": "homelab/backups.md",
"title": _doc_title(db, _BACKUPS),
"start_line": 1,
"end_line": 2,
"total_lines": len(backup_lines),
"text": "\n".join(backup_lines[0:2]),
"truncated": False,
"locator": f"{_BACKUPS}#1-2",
},
]
# Each mid-turn frame is EXACTLY its done-frame twin minus ``type``: the
# client hydrates both from one shape, and the mid-turn copy resolved at
# the call moment cannot disagree with the done-time resolution.
assert done["answer_text"] == [
{k: v for k, v in frame.items() if k != "type"} for frame in mid
]
# The done frame's key set is the phase-182 set plus the additive
# ``answer_text`` — no new key, none renamed, none dropped.
assert set(done) == {
"type",
"deflected",
"sources",
"related",
"suggestions",
"answer_text",
}
# The ONE log line counts the blocks SENT on ``done`` (2), never the sum
# of the mid-turn frames and the done items (4) — AGENTS.md rule 10.
lines = [r.getMessage() for r in caplog.records if "question=" in r.getMessage()]
assert len(lines) == 1, lines
assert lines[-1].endswith("verbatim_blocks=2"), lines[-1]
assert "verbatim_blocks=4" not in json.dumps(lines)
@@ -71,16 +71,11 @@ from app.rag.agent import (
REREAD_PARTIAL,
SHOW_TEXT_TOOL,
AgentHolder,
AgentPiece,
run_agent,
)
from app.rag.importer import import_sources
from app.rag.llm import (
LLMClient,
RetryPiece,
StreamPiece,
ToolCallPiece,
ToolResultPiece,
)
from app.rag.llm import LLMClient, StreamPiece, ToolCallPiece, ToolResultPiece
from app.rag.pdf import extract_pdf_text
from app.rag.retriever import TRUNCATION_MARKER
from tests.fakes import FakeEmbedder
@@ -278,11 +273,11 @@ async def _consume(
llm: HandbookAgentLLM,
holder: AgentHolder,
settings: Settings,
) -> list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]:
) -> list[AgentPiece]:
"""Consume one scripted ``run_agent`` turn (the owner's question,
no seed docs). SEC-14-04: a short-lived session per tool call —
the same pattern as production and the phase-70 integration suite."""
out: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece] = []
out: list[AgentPiece] = []
async for piece in run_agent(
cast("LLMClient", llm),
lambda: SessionLocal(),
@@ -296,7 +291,7 @@ async def _consume(
return out
def _answer(pieces: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]) -> str:
def _answer(pieces: list[AgentPiece]) -> str:
"""The turn's final answer text (the content pieces)."""
return "".join(
piece.text for piece in pieces if isinstance(piece, StreamPiece) and piece.kind == "content"
@@ -79,15 +79,10 @@ from app.rag import agent
from app.rag.agent import (
READ_TRUNCATION_NOTICE,
AgentHolder,
AgentPiece,
run_agent,
)
from app.rag.llm import (
LLMClient,
RetryPiece,
StreamPiece,
ToolCallPiece,
ToolResultPiece,
)
from app.rag.llm import LLMClient, StreamPiece, ToolCallPiece, ToolResultPiece
from app.rag.retriever import TRUNCATION_MARKER
if TYPE_CHECKING:
@@ -291,11 +286,11 @@ async def _consume(
llm: TwoRangeAgentLLM,
holder: AgentHolder,
settings: Settings,
) -> list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]:
) -> list[AgentPiece]:
"""Consume one scripted ``run_agent`` turn. SEC-14-04: a
short-lived session per tool call — the same pattern as
production and the phase-70/131 integration suites."""
out: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece] = []
out: list[AgentPiece] = []
async for piece in run_agent(
cast("LLMClient", llm),
lambda: SessionLocal(),
@@ -309,7 +304,7 @@ async def _consume(
return out
def _answer(pieces: list[StreamPiece | ToolCallPiece | RetryPiece | ToolResultPiece]) -> str:
def _answer(pieces: list[AgentPiece]) -> str:
"""The turn's final answer text (the content pieces)."""
return "".join(
piece.text for piece in pieces if isinstance(piece, StreamPiece) and piece.kind == "content"