phase: 139_read_range_tool_lines
All verification is complete and green. Phase 139's three tasks (01–03) were already implemented in prior passes; this was the final verification pass, and every gate holds. ## Phase 139 — final verification report **Verified in working tree (no code changes needed):** - `readRangeSuffix` single-source-of-truth helper in `app.js` + byte-identical parity copy in `shared.js` (EN-dash ` (lines N–M)`, `(from line N)`, `(first N lines)`, `Number.isInteger(x) && x >= 1` guard mirroring the server's `_whole_line_number`) - `appendToolLine(wrap, name, argument, offset = null, limit = null)` — `<span class="read-range">` sibling after `<code>`, only when suffix non-empty; `shared.js` `addToolLines` parity - Live `tool` branch: guarded `ev.offset`/`ev.limit`, additive `toolAcc` records (non-range keeps pre-phase shape), status label carries the suffix (A2); restore path passes stored `t.offset`/`t.limit` through, phase-95 truncation note untouched - E2E: `SAME_FILE_TRIGGER` mock flow (same file read twice, offsets 10/20 then 40/15) + 3-test Playwright suite **Test / lint / coverage results:** - `uv run pytest` (unit + integration): **3283 passed, 9 skipped, 0 failed** - `uv run pytest --cov=app --cov-report=term-missing`: **TOTAL 99%** (>90% gate) - `uv run pytest tests/e2e/test_read_range_tool_lines.py -v --no-cov`: **3 passed** in isolation - `uv run ruff check .`: clean; `uv run pyright`: **0 errors, 0 warnings** **Completion criteria:** 1. Range read renders `(lines N–M)` live; two same-file reads show DIFFERENT ranges — ✅ (E2E test 1) 2. Plain read byte-identical; phase-87/117 pins + emoji guard pass unedited — ✅ (E2E test 2 + full suite) 3. Suffix survives save → restore on chat page + shared page — ✅ (E2E test 3) 4. Full suite green, coverage >90%, E2E isolated green, ruff + pyright clean — ✅ 5. Commit + phase move — left to the harness per executor rules (no `git add`/`commit` run) **Notable:** the missing pytest summary line on one run was a transient output-buffering artifact, confirmed by re-run; DB was already up and healthy. **Next pending phase:** `140_pictorial_pdf_pages`
This commit is contained in:
@@ -603,6 +603,49 @@ Implements just enough of the aipi surface:
|
||||
never hit it); verified 2026-09-28: no existing E2E question or
|
||||
fixture file contains the phrase, so every other suite is
|
||||
unaffected.
|
||||
- user message containing ``read the same file twice``
|
||||
(``SAME_FILE_TRIGGER``, phase 139 task 03 — the read
|
||||
range-tool-lines' dedicated story suite
|
||||
``tests/e2e/test_read_range_tool_lines.py``) **and** the system
|
||||
prompt carries the ``<tools>`` section -> the owner's exact
|
||||
"same file over and over" scenario (TODO.md L4), end to end: the
|
||||
model range-reads the SAME document TWICE with different
|
||||
``offset``/``limit``, so the two Reading lines are distinguishable.
|
||||
The question carries its own target after the colon (``read the
|
||||
same file twice: read source/path`` — the question-carried-call
|
||||
convention of ``READ_CAP_TRIGGER`` / ``DRILL_TRIGGER``), and the
|
||||
step is then discriminated statelessly by the tool results in the
|
||||
messages (the house count pattern — streaming only):
|
||||
* request 1 (``tools`` offered, no read results yet): a
|
||||
``tool_calls`` delta — ``read`` on the parsed target with
|
||||
``offset`` = ``SAME_FILE_FIRST_OFFSET`` (10) and ``limit`` =
|
||||
``SAME_FILE_FIRST_LIMIT`` (20) (id ``call_0``);
|
||||
* a ``tool``-role read result is in the messages (the FIRST read
|
||||
landed): the SECOND range read — a ``tool_calls`` delta —
|
||||
``read`` on the SAME target with ``offset`` =
|
||||
``SAME_FILE_SECOND_OFFSET`` (40) and ``limit`` =
|
||||
``SAME_FILE_SECOND_LIMIT`` (15) (id ``call_1``) — the same
|
||||
file again, a different range;
|
||||
* TWO or more ``tool``-role read results are in the messages
|
||||
(both range reads landed): the forced answer, byte-stable, in
|
||||
the phase-131 read-echo shape — ``Read the same file twice —
|
||||
lines 10–29: <q1>; lines 40–54: <q2>`` where each ``<qN>`` is
|
||||
that read result's content with the header lines (the
|
||||
phase-106 D5 ``date:`` line + the phase-131 ``lines: a-b of
|
||||
N`` extent) skipped, first 80 chars (the phase-94 read-echo
|
||||
convention — BOTH ranges reached the model's context). NOTE:
|
||||
for the second read to PROCEED (not be refused
|
||||
``ALREADY_IN_CONTEXT``) the document must already be in the
|
||||
agent's truncated set — the story suite boots the app under
|
||||
test with a ``BOR_READ_MAX_CHARS`` small enough that the
|
||||
FIRST (20-line) range slice truncates, which is exactly the
|
||||
large-document premise of the owner's confusion.
|
||||
Checked BEFORE the plain ``TOOLS_TRIGGER`` flow (disjoint trigger
|
||||
phrases — the phase-71/72/94/118/131 ordering convention; the
|
||||
trigger needs the ``<tools>`` section, so deflected turns never
|
||||
hit it); verified 2026-09-30: no existing E2E question or
|
||||
fixture file contains the phrase, so every other suite is
|
||||
unaffected.
|
||||
|
||||
Failure injection (phase 67, LLM retry, TODO.md L3) — deterministic
|
||||
dead-endpoint behavior for the retry E2E suite (``tests/e2e/
|
||||
@@ -2283,6 +2326,121 @@ def _read_lines_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
|
||||
return ("grep",)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 139 (task 03): the owner's "same file over and over" scenario —
|
||||
# the model range-reads the SAME document TWICE with different
|
||||
# offset/limit, so the two Reading lines are distinguishable. See the
|
||||
# module docstring.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: A user message containing this substring (case-insensitive) —
|
||||
#: combined with the ``<tools>`` section in the system prompt — drives
|
||||
#: the deterministic SAME-FILE-TWICE flow (the read range-tool-lines'
|
||||
#: story suite, ``tests/e2e/test_read_range_tool_lines.py`` — the
|
||||
#: owner's "same file over and over" confusion, TODO.md L4). Checked
|
||||
#: BEFORE the plain ``TOOLS_TRIGGER`` flow (disjoint trigger phrases —
|
||||
#: the phase-71/72/94/118/131 ordering convention); verified
|
||||
#: 2026-09-30: no existing E2E question or fixture file contains the
|
||||
#: phrase, so every other suite is unaffected.
|
||||
SAME_FILE_TRIGGER = "read the same file twice"
|
||||
|
||||
#: The scripted target in the same-file-twice question (case-
|
||||
#: insensitive — the suite's question capitalizes the trigger's first
|
||||
#: letter): the verb (``read``) plus the target — a combined
|
||||
#: ``source/path``, parsed from the RAW user message so the target
|
||||
#: keeps its case. The target is a ``[a-z0-9_./-]`` run (case-
|
||||
#: insensitively), so a `` — `` flavor separator can never bleed into
|
||||
#: it (the read-cap / drill-down convention, ``_READ_CAP_CALL_RE`` /
|
||||
#: ``_DRILL_CALL_RE``).
|
||||
_SAME_FILE_CALL_RE = re.compile(
|
||||
r"read the same file twice:\s*read\s+(?P<arg>[a-z0-9_./-]+)",
|
||||
re.I,
|
||||
)
|
||||
|
||||
#: The FIRST range read's requested extent — the model's REQUEST
|
||||
#: (ASSUMPTION A1: the suffix shows the request, not the actual slice
|
||||
#: extent). ``offset`` 10 + ``limit`` 20 renders `` (lines 10–29)``.
|
||||
SAME_FILE_FIRST_OFFSET = 10
|
||||
SAME_FILE_FIRST_LIMIT = 20
|
||||
|
||||
#: The SECOND range read's requested extent — the SAME file again, a
|
||||
#: different range (the owner's confusion made visible). ``offset`` 40
|
||||
#: + ``limit`` 15 renders `` (lines 40–54)``.
|
||||
SAME_FILE_SECOND_OFFSET = 40
|
||||
SAME_FILE_SECOND_LIMIT = 15
|
||||
|
||||
|
||||
def _same_file_quote(doc_content: str) -> str:
|
||||
"""The phase-94/131 read-echo quote of ONE read result's content.
|
||||
|
||||
The read result's header lines sit between the ``Document …:``
|
||||
header and the document text — the phase-106 D5 ``date:`` line
|
||||
always, the phase-131 ``lines: a-b of N`` extent on a range read
|
||||
(a plain read carries no extent line) — skipped (the drill-flow
|
||||
convention) so the quote is the pure read content, first 80 chars
|
||||
(the RANGE reached the model's context). Mirrors the phase-131
|
||||
``_read_lines_flow`` quote logic so the two flows' echo shapes
|
||||
stay byte-consistent.
|
||||
"""
|
||||
content = doc_content
|
||||
if content.startswith("date: "):
|
||||
content = content.partition("\n")[2]
|
||||
if content.startswith("lines: "):
|
||||
content = content.partition("\n")[2]
|
||||
return content[:80]
|
||||
|
||||
|
||||
def _same_file_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
|
||||
"""Classify a phase-139 same-file-twice request (see the module
|
||||
docstring). The question carries its own target (``read the same
|
||||
file twice: read source/path``); the step is then discriminated
|
||||
statelessly by the READ results in the messages (the house count
|
||||
pattern — the flow only ever issues ``read`` calls, so counting
|
||||
read results is reliable):
|
||||
|
||||
* ``("answer", quote1, quote2)`` — TWO or more ``tool``-role read
|
||||
results are in the messages (both range reads landed): the
|
||||
forced answer, byte-stable, in the phase-131 read-echo shape —
|
||||
``Read the same file twice — lines 10–29: <q1>; lines 40–54:
|
||||
<q2>`` where each ``<qN>`` is that read result's pure content
|
||||
(header lines skipped, first 80 chars — the :func:
|
||||
``_same_file_quote`` convention).
|
||||
* ``("read2", target)`` — EXACTLY ONE read result is in the
|
||||
messages (the first read landed): the SECOND range read on the
|
||||
SAME target — ``offset`` = :data:`SAME_FILE_SECOND_OFFSET`,
|
||||
``limit`` = :data:`SAME_FILE_SECOND_LIMIT` (id ``call_1``).
|
||||
* ``("read1", target)`` — ``tools`` are offered and no read result
|
||||
is in the messages yet: the FIRST range read on the parsed
|
||||
target — ``offset`` = :data:`SAME_FILE_FIRST_OFFSET`, ``limit``
|
||||
= :data:`SAME_FILE_FIRST_LIMIT` (id ``call_0``).
|
||||
* ``None`` — not the flow: the trigger is absent, the ``<tools>``
|
||||
section is missing (deflected turns never carry it), the
|
||||
target is unparseable, or ``tools`` are not offered and no read
|
||||
result is in the messages yet (e.g. ``agent_max_rounds=0``).
|
||||
"""
|
||||
user = _user(body)
|
||||
if SAME_FILE_TRIGGER not in user.lower():
|
||||
return None
|
||||
if "<tools>" not in _system(body):
|
||||
return None
|
||||
match = _SAME_FILE_CALL_RE.search(user)
|
||||
if match is None:
|
||||
return None
|
||||
target = match.group("arg")
|
||||
reads = _read_results(body)
|
||||
if len(reads) >= 2:
|
||||
return (
|
||||
"answer",
|
||||
_same_file_quote(reads[0][1]),
|
||||
_same_file_quote(reads[1][1]),
|
||||
)
|
||||
if len(reads) == 1:
|
||||
return ("read2", target)
|
||||
if not body.get("tools"):
|
||||
return None
|
||||
return ("read1", target)
|
||||
|
||||
|
||||
def long_answer() -> str:
|
||||
"""~900-word deterministic walkthrough (phase 11): numbered steps plus
|
||||
a unique final line that must survive the stream untruncated."""
|
||||
@@ -3525,6 +3683,61 @@ def chat_completions(body: dict[str, Any]) -> Any:
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
# Phase 139 (task 03): the owner's "same file over and over"
|
||||
# scenario (the read range-tool-lines' story suite,
|
||||
# tests/e2e/test_read_range_tool_lines.py — TODO.md L4): the
|
||||
# model range-reads the SAME document TWICE with different
|
||||
# offset/limit, so the two Reading lines are distinguishable.
|
||||
# Checked BEFORE the plain TOOLS_TRIGGER flow (disjoint trigger
|
||||
# phrase — the phase-71/72/94/118/131 ordering convention; the
|
||||
# trigger needs the ``<tools>`` section, so deflected turns
|
||||
# never hit it).
|
||||
same_file = _same_file_flow(body)
|
||||
if same_file is not None:
|
||||
if same_file[0] == "read1":
|
||||
# The FIRST range read on the parsed target — the
|
||||
# model's REQUEST (ASSUMPTION A1: the suffix shows the
|
||||
# request, not the actual slice extent).
|
||||
stream = _tool_call_stream(
|
||||
"read",
|
||||
{
|
||||
"path": same_file[1],
|
||||
"offset": SAME_FILE_FIRST_OFFSET,
|
||||
"limit": SAME_FILE_FIRST_LIMIT,
|
||||
},
|
||||
"call_0",
|
||||
)
|
||||
elif same_file[0] == "read2":
|
||||
# The SECOND range read — the SAME file again, a
|
||||
# different range (the owner's confusion made visible).
|
||||
stream = _tool_call_stream(
|
||||
"read",
|
||||
{
|
||||
"path": same_file[1],
|
||||
"offset": SAME_FILE_SECOND_OFFSET,
|
||||
"limit": SAME_FILE_SECOND_LIMIT,
|
||||
},
|
||||
"call_1",
|
||||
)
|
||||
else: # "answer" — the forced read-echo of BOTH ranges
|
||||
stream = _sse_stream(
|
||||
_apply_max_tokens(
|
||||
f"Read the same file twice — lines "
|
||||
f"{SAME_FILE_FIRST_OFFSET}–"
|
||||
f"{SAME_FILE_FIRST_OFFSET + SAME_FILE_FIRST_LIMIT - 1}: "
|
||||
f"{same_file[1]}; lines "
|
||||
f"{SAME_FILE_SECOND_OFFSET}–"
|
||||
f"{SAME_FILE_SECOND_OFFSET + SAME_FILE_SECOND_LIMIT - 1}: "
|
||||
f"{same_file[2]}",
|
||||
body.get("max_tokens"),
|
||||
),
|
||||
0.0,
|
||||
)
|
||||
return StreamingResponse(
|
||||
stream,
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
# Phase 124 (task 07, image chips and answers, TODO L3): the
|
||||
# scripted SHOW_IMAGE flow — the mock shows the seeded image
|
||||
# document (the ``<documents>`` block with the image-
|
||||
|
||||
Reference in New Issue
Block a user