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:
@@ -750,6 +750,22 @@ Implements just enough of the aipi surface:
|
||||
for, so the client must land on its honest unresolved state (no badge,
|
||||
no body, none of the model's bytes). Checked BEFORE the
|
||||
``DEFLECT_MODE`` branch, same convention.
|
||||
- user message containing ``walk through the stored text as you go``
|
||||
(``MIDTURN_TRIGGER``, phase 192 task 04 — the mid-turn panel suite
|
||||
``tests/e2e/test_verbatim_inline_midturn.py``) -> the MID-TURN turn the
|
||||
phase-182 flow cannot produce: a content delta (the intro, plus the
|
||||
closed fence in the fence-first arm) THEN the ``show_text`` call, and
|
||||
the final answer only after :data:`MIDTURN_GAP_S` of silence — the
|
||||
window in which the ``verbatim_block`` frame's panel is the only thing
|
||||
on screen. Arm markers, the two ranges of one file, the sentinel token
|
||||
and the byte-exact answer constants are documented at the definitions
|
||||
(``MIDTURN_*``). NOT clause-gated on purpose: the kill-switch arm boots
|
||||
a ``BOR_ANSWER_TEXT=0`` app with the SAME question, so the attempt
|
||||
reaches the server-side ``Unknown tool.`` refusal and the wire can be
|
||||
pinned byte-identical to pre-phase. Checked BEFORE the phase-182
|
||||
``SHOW_TEXT_TRIGGER`` flow (disjoint phrases — the ordering
|
||||
convention); no existing E2E question or fixture file carries the
|
||||
phrase.
|
||||
- user message containing ``which diagram is in your notes``
|
||||
(``DIAGRAM_TRIGGER``, phase 124 task 07 — the BUG-FIX-ONLY
|
||||
trigger, same suite) -> the deterministic ZERO-READ answer: the
|
||||
@@ -1619,6 +1635,157 @@ INLINE_ANSWER = (
|
||||
"And the inline markdown ends here — a plain closing line."
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 192 (task 04): the MID-TURN show_text flow (the phase's own E2E suite)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: A user message containing this substring (case-insensitive) drives the
|
||||
#: deterministic phase-192 MID-TURN turn for
|
||||
#: ``tests/e2e/test_verbatim_inline_midturn.py`` — the shape the phase-182
|
||||
#: flow CANNOT produce, because that one streams the tool call with NO
|
||||
#: content in front of it and answers with NO gap behind it:
|
||||
#:
|
||||
#: round 1 ``delta``(the intro prose — and, in the fence-first arm, the
|
||||
#: closed ```bor fence itself) → a ``show_text`` tool_calls delta
|
||||
#: round 2 (the server has by now sent ``tool`` + the task-01
|
||||
#: ``verbatim_block`` frame) → a deliberate
|
||||
#: ``MIDTURN_GAP_S`` silence → the answer's ``delta`` frames,
|
||||
#: the LAST of them carrying ``MIDTURN_SENTINEL`` → ``done``
|
||||
#:
|
||||
#: The gap is the test's observation window: the panel the frame paints is
|
||||
#: on screen while the answer has NOT streamed a single token, so
|
||||
#: "the panel landed mid-turn" is pinned by the DOM itself, not by a poll.
|
||||
#: Arms, discriminated by markers in the question (all disjoint from every
|
||||
#: other trigger, ordering convention intact):
|
||||
#
|
||||
#: * default — no fence anywhere: the mid-turn block is APPENDED at the
|
||||
#: answer position (``appendUnlocatedVerbatim``).
|
||||
#: * ``MIDTURN_FENCE_BEFORE_MARKER`` — the fence rides round 1, so the
|
||||
#: placeholder EXISTS when the frame lands: the mid-turn
|
||||
#: ``hydrateVerbatimNow`` pass resolves it IN PLACE.
|
||||
#: * ``MIDTURN_FENCE_AFTER_MARKER`` — the fence rides the final answer
|
||||
#: (the realistic shape: the model quotes after the call), so the
|
||||
#: mid-turn panel is appended and the ``done`` pass re-homes it onto
|
||||
#: the fence and drops the twin (task 03's ``dropPlacedTwin``).
|
||||
#: * ``MIDTURN_TWO_RANGES_MARKER`` — TWO ``show_text`` calls (the two
|
||||
#: ranges the question names), no fences: two panels, in call order.
|
||||
#: Stateless per the house rule (the tool-result count is the state).
|
||||
#:
|
||||
#: Deliberately NOT gated on ``_SHOW_TEXT_CLAUSE_MARKER``: the kill-switch
|
||||
#: arm boots a ``BOR_ANSWER_TEXT=0`` app and asks the SAME question, where
|
||||
#: the attempt must reach the server-side ``UNKNOWN_TOOL`` refusal (the
|
||||
#: tool is not offered) so the wire can be pinned byte-identical to the
|
||||
#: pre-phase baseline. The trigger phrase appears in no other suite's
|
||||
#: question (verified by the suite's own grep) and no other trigger is a
|
||||
#: substring of it.
|
||||
MIDTURN_TRIGGER = "walk through the stored text as you go"
|
||||
#: The module KB's source + path (the locator the panel captions).
|
||||
MIDTURN_SOURCE = "midturnlab"
|
||||
MIDTURN_PATH = "runbooks/valve-panel.md"
|
||||
#: The ``show_text`` ``path`` argument: the COMBINED ``source/path`` identity
|
||||
#: (``app/rag/agent.py`` resolves it through ``_resolve_path`` — a bare
|
||||
#: ``path`` names a document that does not exist, the phase-72 refusal). The
|
||||
#: call identity stays DERIVED, never a label (phase 191 LOCKED D2).
|
||||
MIDTURN_COMBINED = f"{MIDTURN_SOURCE}/{MIDTURN_PATH}"
|
||||
#: The range the single-call arms request.
|
||||
MIDTURN_START, MIDTURN_END = 12, 20
|
||||
#: The second call's range in the two-range arm.
|
||||
MIDTURN_SECOND_START, MIDTURN_SECOND_END = 1, 10
|
||||
#: Arm markers (question text only — never rendered by the app).
|
||||
MIDTURN_FENCE_BEFORE_MARKER = "fence it before the call"
|
||||
MIDTURN_FENCE_AFTER_MARKER = "fence it in the answer"
|
||||
MIDTURN_TWO_RANGES_MARKER = "then the next block"
|
||||
#: The round-1 content delta — on screen BEFORE the panel, so the suite can
|
||||
#: assert the panel lands BELOW the text the model had already written.
|
||||
MIDTURN_INTRO = "Let me pull the stored block up here — MIDTURN_INTRO_4412"
|
||||
#: The final answer's witness token: it exists ONLY in the deltas that
|
||||
#: stream after the frame, so "no sentinel in the DOM" == "the turn is live".
|
||||
MIDTURN_SENTINEL = "MIDTURN_SENTINEL_7731"
|
||||
#: Bytes the model re-types into a fence body — they must NEVER paint.
|
||||
MIDTURN_FENCE_BODY_MARKER = "MIDTURN-FENCE-BODY-NEVER-ON-SCREEN"
|
||||
#: The silence between the ``verbatim_block`` frame and the answer's first
|
||||
#: delta (seconds). 1.5 s is comfortably longer than any Playwright query
|
||||
#: round trip and comfortably shorter than every turn guard.
|
||||
MIDTURN_GAP_S = 1.5
|
||||
#: The unfenced / fence-first answer (byte-exact: the suite compares the
|
||||
#: concatenated ``delta`` payloads against it on BOTH apps).
|
||||
MIDTURN_PLAIN_ANSWER = (
|
||||
"The panel above is the file itself, not my retyping of it.\n\n"
|
||||
f"{MIDTURN_SENTINEL} — the answer kept streaming after the panel landed.\n\n"
|
||||
"(Deterministic mock answer for E2E.)"
|
||||
)
|
||||
#: The fence-after-call answer (the dedupe arm).
|
||||
MIDTURN_FENCED_ANSWER = (
|
||||
"The block I pulled is quoted below.\n\n"
|
||||
f"```bor {MIDTURN_COMBINED}#{MIDTURN_START}-{MIDTURN_END}\n"
|
||||
f"{MIDTURN_FENCE_BODY_MARKER} — the model re-typed these bytes\n"
|
||||
"```\n\n"
|
||||
f"{MIDTURN_SENTINEL} — the answer kept streaming after the panel landed.\n\n"
|
||||
"(Deterministic mock answer for E2E.)"
|
||||
)
|
||||
|
||||
|
||||
def _midturn_fence() -> str:
|
||||
"""One CLOSED ```bor fence for the arm's primary range (the model's
|
||||
own bytes inside — the placeholder drops them), padded by blank lines:
|
||||
the client concatenates a mid-round ``delta`` and the next round's answer
|
||||
with NO separator, so a fence that ends without its own trailing newline
|
||||
has its closing ``` glued to the following word and stops being a fence
|
||||
(a REAL model writes the blank line — the mock has to be as well-formed
|
||||
as the thing it stands in for)."""
|
||||
return (
|
||||
f"```bor {MIDTURN_COMBINED}#{MIDTURN_START}-{MIDTURN_END}\n"
|
||||
f"{MIDTURN_FENCE_BODY_MARKER} — the model re-typed these bytes\n"
|
||||
"```\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _midturn_ranges(text: str) -> list[tuple[int, int]]:
|
||||
"""Every ``N-M`` line range the question names, in order (the two-range
|
||||
arm carries two). Empty → the primary range."""
|
||||
found = [
|
||||
(int(start), int(end))
|
||||
for start, end in re.findall(r"\b(\d+)\s*-\s*(\d+)\b", text)
|
||||
]
|
||||
return found or [(MIDTURN_START, MIDTURN_END)]
|
||||
|
||||
|
||||
def _midturn_flow(body: dict) -> tuple[str, ...] | None:
|
||||
"""The deterministic phase-192 mid-turn step (see MIDTURN_TRIGGER).
|
||||
|
||||
``("call", path, start, end, mid_round_delta)`` — the round's content
|
||||
delta (possibly empty) followed by the scripted ``show_text`` call (the
|
||||
line numbers are STRINGS, the ``_show_text_request`` convention);
|
||||
``("answer", text)`` — the final answer, streamed after
|
||||
``MIDTURN_GAP_S`` of silence.
|
||||
"""
|
||||
text = _user(body)
|
||||
lower = text.lower()
|
||||
if MIDTURN_TRIGGER not in lower:
|
||||
return None
|
||||
ranges = _midturn_ranges(text)
|
||||
results = _tool_results(body)
|
||||
if not results:
|
||||
delta = MIDTURN_INTRO
|
||||
if MIDTURN_FENCE_BEFORE_MARKER in lower:
|
||||
delta = f"{MIDTURN_INTRO}\n\n{_midturn_fence()}"
|
||||
start, end = ranges[0]
|
||||
return ("call", MIDTURN_COMBINED, str(start), str(end), delta)
|
||||
if (
|
||||
MIDTURN_TWO_RANGES_MARKER in lower
|
||||
and len(results) == 1
|
||||
and len(ranges) > 1
|
||||
):
|
||||
start, end = ranges[1]
|
||||
return ("call", MIDTURN_COMBINED, str(start), str(end), "")
|
||||
return (
|
||||
"answer",
|
||||
MIDTURN_FENCED_ANSWER
|
||||
if MIDTURN_FENCE_AFTER_MARKER in lower
|
||||
else MIDTURN_PLAIN_ANSWER,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 67 (LLM retry, TODO.md L3): deterministic failure injection
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -5710,6 +5877,34 @@ def chat_completions(body: dict[str, Any]) -> Any:
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
# Phase 192 (task 04): the MID-TURN show_text turn — the round-1
|
||||
# content delta (+ optionally the fence itself) then the call, the
|
||||
# final answer only after MIDTURN_GAP_S of silence, so the panel
|
||||
# the verbatim_block frame paints is on screen BEFORE any answer
|
||||
# token. Checked BEFORE the phase-182 show_text flow (disjoint
|
||||
# trigger phrases — the ordering convention); that flow's own arms
|
||||
# stay byte-identical.
|
||||
midturn = _midturn_flow(body)
|
||||
if midturn is not None:
|
||||
if midturn[0] == "call":
|
||||
stream = _midround_tool_stream(
|
||||
"",
|
||||
midturn[4],
|
||||
"show_text",
|
||||
_show_text_call_arguments(midturn[1], midturn[2], midturn[3]),
|
||||
"call_midturn_show",
|
||||
)
|
||||
else: # "answer" — the final answer, after the observation gap
|
||||
stream = _sse_stream(
|
||||
_apply_max_tokens(midturn[1], body.get("max_tokens")),
|
||||
0.0,
|
||||
pre_content_delay=MIDTURN_GAP_S,
|
||||
)
|
||||
return StreamingResponse(
|
||||
stream,
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
turn_progress = _turn_progress_flow(body)
|
||||
if turn_progress is not None:
|
||||
stream = _turn_progress_stream(turn_progress)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,8 +28,9 @@ Postgres so the fixtures' exact text is the assertion baseline):
|
||||
badge (locked A5). Wire pins: exactly FIVE tools offered (the three base
|
||||
+ ``show_image`` + ``show_text``), exactly one executed call, and — from
|
||||
an in-page fetch tap over the real ``/api/chat`` stream — the turn's SSE
|
||||
``type`` set stays inside the pre-phase seven with ``answer_text``
|
||||
riding INSIDE ``done`` (no eighth event type).
|
||||
``type`` set stays inside the pre-phase seven PLUS the one additive type
|
||||
phase 192 introduced (``verbatim_block``, the mid-turn copy of a shown
|
||||
block), with ``answer_text`` still riding INSIDE ``done``.
|
||||
- *append path* — the same turn with ``describe it without a block`` (no
|
||||
fence in the answer): the shown block renders as a panel APPENDED to the
|
||||
bubble body, below the answer text and ABOVE the source clusters (the
|
||||
@@ -246,7 +247,7 @@ DOC_A_LINES = DOC_A_CONTENT.split("\n")
|
||||
DOC_B_LINES = DOC_B_CONTENT.split("\n")
|
||||
|
||||
#: The SSE ``type`` values the pre-phase stream already carried
|
||||
#: (``app/schemas.py``'s chat events): the phase added NO eighth type.
|
||||
#: (``app/schemas.py``'s chat events): phase 182 added NO eighth type.
|
||||
PRE_PHASE_EVENT_TYPES = {
|
||||
"delta",
|
||||
"thinking",
|
||||
@@ -257,6 +258,15 @@ PRE_PHASE_EVENT_TYPES = {
|
||||
"retry",
|
||||
}
|
||||
|
||||
#: The ONE type phase 192 (task 01, A1) adds: the mid-turn
|
||||
#: ``verbatim_block`` frame, sent by the turn whose ``show_text`` call just
|
||||
#: succeeded (one frame per shown block, before the next model round). App A
|
||||
#: calls ``show_text``, so its vocabulary is the seven PLUS this type and
|
||||
#: NOTHING else; a turn with no successful show never carries it, and the
|
||||
#: kill-switch stream's byte-identity is pinned on the wire in
|
||||
#: ``tests/integration/test_chat_api.py::test_kill_switch_stream_is_byte_identical``.
|
||||
PHASE_192_EVENT_TYPES = {"verbatim_block"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module apps: verbatim ON (App A) and the kill switch OFF (App B).
|
||||
@@ -847,13 +857,23 @@ def test_show_text_turn_paints_the_files_bytes(
|
||||
"Text shown to the user" in r for r in _tool_results(captured[1])
|
||||
), "the show_text confirming result never reached the model"
|
||||
|
||||
# --- the frame vocabulary (no eighth SSE type) ----------------------
|
||||
# --- the frame vocabulary (the seven + phase 192's ONE type) --------
|
||||
frames = _sse_frames(page)
|
||||
types: set[str] = {str(f.get("type")) for f in frames}
|
||||
assert types <= PRE_PHASE_EVENT_TYPES, (
|
||||
order = [str(f.get("type")) for f in frames]
|
||||
types: set[str] = set(order)
|
||||
allowed = PRE_PHASE_EVENT_TYPES | PHASE_192_EVENT_TYPES
|
||||
assert types <= allowed, (
|
||||
f"the stream invented an event type: {sorted(types)}"
|
||||
)
|
||||
assert len(types) <= 7
|
||||
assert len(types) <= 8
|
||||
# Phase 192 (task 01): the shown block also rides its own mid-turn frame,
|
||||
# ahead of the answer's deltas (the panel is on screen while the model is
|
||||
# still talking) — ADDITIVE, the done frame below still carries it.
|
||||
block_frames = [f for f in frames if f.get("type") == "verbatim_block"]
|
||||
assert [b["locator"] for b in block_frames] == [seeded["a_locator"]]
|
||||
i_block = order.index("verbatim_block")
|
||||
assert "delta" not in order[:i_block], order
|
||||
assert order.index("tool") < i_block, order
|
||||
assert "answer_text" not in types, "answer_text leaked out as its own event"
|
||||
done = [f for f in frames if f.get("type") == "done"]
|
||||
assert len(done) == 1
|
||||
|
||||
Reference in New Issue
Block a user