phase: 131_read_line_ranges
All 44 progress lines are pure dots, final line at 100%, exit 0. Everything is verified. Final report: **Phase 131 — final verification pass (all 4 tasks already complete in working tree)** - **Verified implemented:** `read(offset, limit)` schema + teaching descriptions; rewritten truncation notice; full 6-row dedupe matrix + range validation + `lines: a-b of N` header + slice cap in `_execute_tool`; wire-level `offset`/`limit` on `ChatToolEvent` (absent keys dropped → non-range frames byte-identical); mock-LLM `read the lines` flow; handbook repro (wording + line numbers derived from the real PDF at test time) - **Tests:** `uv run pytest --no-cov` → 3097 passed, exit 0; `uv run pytest --cov=app` → **99% on app/** (>90%); `uv run pytest tests/e2e/test_read_line_ranges.py -v --no-cov` → 2/2 in isolation (47.6 s); `tests/integration/test_read_line_ranges_handbook.py` → 2/2 (3.5 s); 10-file targeted E2E batch pinning pre-phase surfaces (grep/ls/plain-read/ALREADY_IN_CONTEXT/truncation) → 31/31; `ruff check .` clean; `pyright` 0 errors - **Completion criteria:** all 5 green — (1) exact range lines + `lines:` header, plain reads byte-identical (existing pins pass unchanged) ✅ (2) truncated-then-range succeeds end-to-end on the real handbook; other re-reads get teaching refusals ✅ (3) find-traps answered from real PDF text, no hardcoded wording ✅ (4) coverage + isolated E2E ✅ (5) no behavior change in completed phases ✅ - **Docs:** README + AGENTS reviewed in full. Fixed 3 inconsistencies: README missing `BOR_HISTORY_MAX_TURNS`/`BOR_HISTORY_MAX_CHARS` rows (pre-existing phase-74 gap — env-tunable, unit-pinned, undocumented); `.env.example` missing the same two vars (added); `.env.example` stale `BOR_READ_MAX_CHARS` comment (still said "grep-pointer notice (phase 95)" — updated to the phase-131 workflow notice). AGENTS.md verified consistent — no changes needed. All 67 `.env.example` vars documented, all defaults match `app/config.py`, pages/auth tables match live routes - **Note:** PLAN.md §7.1 still states the retired 46rem chat column (superseded by phase 100, which AGENTS.md rule 5 records correctly) — PLAN.md is off-limits for this pass, flagged only - **Next pending phase:** `132_image_upload_feedback`
This commit is contained in:
+42
-1
@@ -303,6 +303,20 @@ def get_llm() -> LLMClient:
|
||||
return _llm
|
||||
|
||||
|
||||
def _whole_line_number(value: Any) -> bool:
|
||||
"""A ``read`` range argument's whole-line-number guard (phase 131,
|
||||
task 04).
|
||||
|
||||
The same validation the agent's ``_read_range_args`` applies
|
||||
server-side (``app/rag/agent.py``): an ``int`` (bools excluded —
|
||||
the house guard) and ``>= 1``. A value that fails is a REFUSED
|
||||
call (the ``BAD_RANGE_ARGS`` teaching), and its ``tool`` frame
|
||||
carries no range — the refusal surfaces as the round's result, not
|
||||
as the call's arguments.
|
||||
"""
|
||||
return isinstance(value, int) and not isinstance(value, bool) and value >= 1
|
||||
|
||||
|
||||
def sse_event(payload: dict[str, Any]) -> str:
|
||||
"""Serialize one SSE frame: ``data: <json>\\n\\n`` (PLAN §4)."""
|
||||
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||
@@ -905,8 +919,35 @@ async def chat(
|
||||
"pattern" if piece.name == "grep" else "path"
|
||||
)
|
||||
argument = argument if isinstance(argument, str) else None
|
||||
# Phase 131 (task 04): a range ``read``'s line
|
||||
# range rides the SAME frame — the additive
|
||||
# ``offset`` / ``limit`` fields (the wire's
|
||||
# assertion surface for the owner's
|
||||
# grep → read(offset/limit) dance). Only a
|
||||
# well-formed range (``_whole_line_number``,
|
||||
# the agent's own guard) lands on the frame;
|
||||
# absent → omitted on serialization, so every
|
||||
# non-range frame stays byte-identical to
|
||||
# pre-phase (the ChatToolEvent serializer).
|
||||
offset = (
|
||||
piece.arguments.get("offset")
|
||||
if piece.name == "read"
|
||||
else None
|
||||
)
|
||||
offset = offset if _whole_line_number(offset) else None
|
||||
limit = (
|
||||
piece.arguments.get("limit")
|
||||
if piece.name == "read"
|
||||
else None
|
||||
)
|
||||
limit = limit if _whole_line_number(limit) else None
|
||||
yield sse_event(
|
||||
ChatToolEvent(name=piece.name, argument=argument).model_dump()
|
||||
ChatToolEvent(
|
||||
name=piece.name,
|
||||
argument=argument,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
).model_dump()
|
||||
)
|
||||
continue
|
||||
if isinstance(piece, RetryPiece):
|
||||
|
||||
Reference in New Issue
Block a user