Files
brain-of-reese/.agents/phases/todo/146_search_tool_scope/00_phase.md
T
ducoterra 152d4f2b88
Build and Push Containers / build-and-push-app (push) Successful in 15s
Build and Push Containers / build-and-push-db (push) Successful in 12s
chore(agent): phase roadmap from TODO.md, 4 phases
Protocol B append (phases 146-149) for the four TODO items: the
search tool line missing the grep's document scope (146), sync-only-
what-changed via a raw-file byte hash checked before extraction and
vision (147), pictorial PDFs showing their rendered pages in the
sources viewer instead of page descriptions (148), and the sync
detail moved to a text box under the button with the button as a
spinner (149). TODO.md cleared (items now live in .agents/phases/
todo/).

LLM-Generated: true
2026-09-30 22:29:15 -04:00

6.1 KiB
Raw Blame History

Phase 146 — the search tool line shows WHERE the LLM is searching: a scoped grep carries its document

Source: TODO.md L3 — "The "search" tool in the UI doesn't show where the LLM is searching. This results in, what looks like, multiple search tool calls with the same value, like the LLM is repeating itself." Story: n/a (TODO-derived phase, Protocol B append). Context: app/rag/agent.py — the grep tool definition (~L395–441: required pattern + optional path — "Rarely needed — only for re-searching one document you already know: that document's combined source/path identity"); app/api/chat.py — the ToolCallPiece → SSE tool frame builder (~L909–947: argument = piece.arguments.get("pattern" if piece.name == "grep" else "path") — a scoped grep's path argument is DROPPED today, so every search line renders the same pattern); app/schemas.py — ChatToolEvent (L182: {name, argument, offset?, limit?} — the additive-omission serializer precedent is DocContent._serialize dropping image_url when None); frontend/assets/app.js — appendToolLine (~L1122: the grep branch renders 🔎 Searching for <code>pattern</code> — no scope, byte-identical for bare and scoped calls), the live tool-frame handler (~L3178–3230: ev.offset/ev.limit additive wire fields + the toolRecord stamp — the exact pattern this phase extends with ev.path), the restore path (~L2064–2086: re-renders saved {name, argument, offset?, limit?} records through the same helper); tests/e2e/mock_llm.py — the scripted-turn idiom (the phase-70/72 grep scripted branches — the house way of driving a model tool call deterministically). Locked (owner delegation 2026-09-30, chat — "pick the best choices"): the fix is the missing SCOPE: the tool frame additively carries the grep's optional path (the re-searched document's combined source/path), and the UI renders it — 🔎 Searching for <pattern> in <source/path> — so a whole-KB search and a re-search of one document are visibly different (the "repeating itself" confusion is gone). Bare greps render byte-identical to pre-phase. The scope also rides the status label ("… is searching for X in Y") and persists additively on the saved turn's tool record (bor.chat.v1 JSONB — old records restore byte-identically, no migration).

Objective

A grep that re-searches a specific document shows THAT document in the chat's tool line and status label, so repeated patterns with different scopes read as distinct searches; every bare-grep rendering (live, restored, persisted) is byte-identical to pre-phase.

Dependencies

  • 145_git_source_links (todo) — pipeline order only; no shared code (this phase touches the tool-frame wire + the chat tool-line renderer; 145 touches read results + the doc viewer meta row).

Design (shared by all tasks — the executor reads this, not the chat)

  • The wire (task 01): ChatToolEvent gains path: str | None = None — ADDITIVE-OMISSION: a model_serializer(mode="wrap") drops the key when None (the DocContent image_url precedent), so every frame that is not a scoped grep serializes byte-identical to pre-phase. The frame builder sets it ONLY for piece.name == "grep" when piece.arguments.get("path") is a string (a non-string path is a model error the backend already refuses — the guard just yields None → omitted). Legacy tool names never appear on the live wire (the server only emits current names), so no legacy handling.
  • The UI (task 02): appendToolLine gains a fifth scope parameter (default null). The grep branch appends, ONLY when scope is a non-empty string, a sibling <span> AFTER the pattern <code>: text node in + a second <code> (the scope is data, never markup; NO new emoji — the frontend emoji guard strips precisely the existing template literals, which stay untouched — the phase-139 read-range-suffix span-append precedent). The live handler reads ev.path (string guard), persists it on the toolRecord ONLY when non-null (additive JSONB key — a bare-grep record keeps the exact pre-phase shape), and extends the grep status label with in ${scope}. The restore path reads the saved t.path through the same helper — a record saved before this phase (no key) renders byte-identically.
  • The E2E (task 02): mock_llm.py gains ONE scripted question branch: a scoped grep (pattern + path) followed by the terminal answer — the house scripted-turn idiom; the bare-grep scripted turns from phases 70/72 stay as the byte-identity pins.

Tasks

  1. 01_tool_frame_scope.md — the additive path field on ChatToolEvent + the frame builder + the serialization matrix.
  2. 02_ui_scope_render_e2e.md — the tool line + status label + persisted record + restore path + the dedicated E2E.

Testing & Quality

  • Unit: the frame-serialization matrix (scoped grep / bare grep / read / ls / null-argument — non-scoped frames byte-identical pins).
  • E2E: one dedicated Playwright file, run in isolation, mock-only.
  • Coverage: >90% on new/modified code (app/).

Completion Criteria

  • a scoped grep streams {"type": "tool", "name": "grep", "argument": <pattern>, "path": <source/path>}; a bare grep's frame is byte-identical to pre-phase (path key ABSENT, not null); read/ls frames unchanged
  • the chat tool line for a scoped grep reads 🔎 Searching for <pattern> in <source/path> (two <code> spans, no new emoji); the bare-grep line is byte-identical (the phase-70/87/117 pins pass unchanged)
  • the status label says "… is searching for in <source/path>" for a scoped grep only
  • a saved scoped-grep turn persists path on the tool record and re-renders the scope on restore; pre-phase saved records restore byte-identically
  • uv run pytest --cov=app --cov-report=term-missing green, app/ coverage >90%; uv run pytest tests/e2e/test_search_tool_scope.py -v --no-cov green in isolation; uv run ruff check . && uv run pyright clean
  • no behavior change in completed phases (the read/ls tool lines, the disclosure count, the saved-turn JSON shape for non-scoped records, the done frame)