phase: 146_search_tool_scope
Build and Push Containers / build-and-push-app (push) Successful in 2m24s
Build and Push Containers / build-and-push-db (push) Successful in 13s

Phase 146 final verification pass complete — all green, tree untouched by me.

**Verified (final pass — both tasks already in `complete/`):**
- Scoped grep streams `{"type":"tool","name":"grep","argument":<pattern>,"path":<source/path>}`; bare grep/read/ls frames dump byte-identical (unit matrix in `test_sse_events.py` + builder pins in `test_chat_gate.py` L1364–1448)
- Tool line `🔎 Searching for <pattern> in <source/path>` (two `<code>` spans, `.grep-scope` sibling, no new emoji); bare line byte-identical (unit Node test + E2E)
- Status label "… is searching for X in Y" scoped-only; `path` persisted additively on the saved tool record and re-rendered on `/?chat=<id>` restore; pre-phase records restore byte-identically (E2E tests 2 & 3)

**Results (exact commands):**
- `uv run pytest --cov=app --cov-report=term-missing` → **3419 passed**, `app/` coverage **99%** (>90% ✓)
- `uv run pytest tests/e2e/test_search_tool_scope.py -v --no-cov` → **3 passed** in isolation ✓
- `uv run ruff check . && uv run pyright` → clean (0 errors) ✓

**Completion criteria:** all 6 ✓ (wire shape, UI line, status label, save/restore, gates, no completed-phase behavior change — full suite green includes all prior-phase pins).

**Documentation:** README.md (incl. its phase-146 section) and AGENTS.md reviewed in full against the codebase — verified consistent, no changes needed (`.env.example` ⊆ README config tables; no stale vars; phase-100 72rem rule in AGENTS.md §5 matches shipped CSS; no-CDN/smoke-DB/gpgsign claims all confirmed).

**Notable:** none — no defects found, no live-infrastructure touched, nothing committed per harness protocol.

**Next pending phase:** `147_sync_changed_only`.
This commit is contained in:
2026-10-01 12:48:04 -04:00
parent 32a3ec4f5e
commit ab3f5f685b
27 changed files with 2151 additions and 29 deletions
+106
View File
@@ -170,6 +170,30 @@ Implements just enough of the aipi surface:
specific phrase — same convention as ``think in paragraphs``); no
existing E2E question or fixture file contains the trigger, so
every other suite is unaffected.
- user message containing ``search within one document``
(``SCOPED_SEARCH_TRIGGER``, phase 146 task 02 — the search tool's
SCOPE: a re-search of ONE document, so the UI can show where the
search runs and repeated patterns read as distinct searches,
TODO.md L3) **and** the system prompt carries the ``<tools>``
section -> the deterministic SCOPED-SEARCH flow, discriminated
statelessly from the messages (streaming only; the bare-grep
``SEARCH_TRIGGER`` flow above is the byte-identity pin and stays
untouched):
* request 1 (``tools`` offered, no search result yet): stream
ONLY ``tool_calls`` deltas — ``grep`` with
``{"pattern": SEARCH_PATTERN, "path": SCOPED_SEARCH_PATH}``
(id ``call_0``) — the SCOPED form (pattern + the re-searched
document's combined ``source/path``);
* request 2 (a ``tool``-role search result in the messages —
the agent's REAL scoped grep matched the fixture's sentinel
line): the content answer, deterministic: ``Found <first
matched line's content up to 80 chars>`` (the same echo shape
as the bare flow).
Checked BEFORE the bare ``SEARCH_TRIGGER`` flow (disjoint trigger
phrases — the phase-71/72 ordering convention; the trigger needs
the ``<tools>`` section, so deflected turns never hit it); no
existing E2E question or fixture file contains the phrase, so
every other suite is unaffected.
- user message containing ``emit raw tool markup``
(``SCAFFOLD_TRIGGER``, phase 71, tool-scaffolding guardrails — the
2026-09-03 incident where a deflected round streamed the model's
@@ -1042,6 +1066,25 @@ SEARCH_TRIGGER = "search your documents"
#: convention of ``END_OF_NOTES_TRIGGER``).
SEARCH_PATTERN = "reese-sentinel-42"
#: Phase 146 (task 02 — the search tool's scope, TODO.md L3): a user
#: message containing this substring (case-insensitive) — combined with
#: the ``<tools>`` section in the system prompt — drives the
#: deterministic SCOPED-SEARCH flow (``grep`` with ``pattern`` +
#: ``path`` → the "Found …" answer), documented in the module
#: docstring. Disjoint from ``SEARCH_TRIGGER`` (the bare-grep flow —
#: the byte-identity pin) and from every other marker; verified 2026-10:
#: no existing E2E question or fixture file contains the phrase, so
#: every other suite is unaffected.
SCOPED_SEARCH_TRIGGER = "search within one document"
#: The scoped flow's target — the SAME fixture document the bare-grep
#: flow already greps (``tests/fixtures/search_docs/`` → source
#: ``search_docs``, doc ``reese-notes.md``): the agent's REAL scoped
#: grep finds its sentinel line, so the follow-up round carries a tool
#: result. The exact string the UI renders is the model's ``path``
#: argument — the fixture's real combined identity.
SCOPED_SEARCH_PATH = "search_docs/reese-notes.md"
#: Phase 44 (markdown-tables story, TODO.md L6): a user message
#: containing this substring (case-insensitive) gets the fixed table
#: answer (``TABLE_ANSWER`` below) — a 3-column table, an XSS probe
@@ -1708,6 +1751,43 @@ def _search_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
return ("search",)
def _scoped_search_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
"""Classify a SCOPED_SEARCH_TRIGGER request into a step.
Phase 146 (task 02): the SCOPED search flow — the owner's "looks
like the LLM is repeating itself" scenario (TODO.md L3): a re-search
of ONE document (``grep(pattern, path)``) so the UI can show WHERE
the search runs. The same scripted-turn idiom as ``_search_flow``
(the phase-70/72 grep branches): stateless classification from the
messages (streaming only).
* ``("search",)`` — ``tools`` are offered and no search result is
in the messages yet: the model greps the ONE fixture document
(``SCOPED_SEARCH_PATH``) for ``SEARCH_PATTERN`` (id ``call_0``) —
the scoped form (``pattern`` + ``path``).
* ``("found", first_line)`` — a ``tool``-role search result is in
the messages (the agent's REAL scoped grep matched the fixture's
sentinel line): the model answers, quoting the first matched
line (``Found <first matched line's content up to 80 chars>``).
Reached regardless of the ``tools`` parameter (phase 45 keeps the
tools offered until the round cap).
* ``None`` — not the scoped flow: the trigger is absent, the
``<tools>`` section is missing (deflected turns never carry it),
or ``tools`` are not offered and no search result is in the
messages yet (e.g. ``agent_max_rounds=0``).
"""
if SCOPED_SEARCH_TRIGGER not in _user(body).lower():
return None
if "<tools>" not in _system(body):
return None
first_line = _search_result_line(body)
if first_line is not None:
return ("found", first_line)
if not body.get("tools"):
return None
return ("search",)
def _scaffold_flow(body: dict[str, Any]) -> str | None:
"""Classify a phase-71 scaffolding request (see the module docstring).
@@ -3895,6 +3975,32 @@ def chat_completions(body: dict[str, Any]) -> Any:
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
# Phase 146 (task 02): the deterministic SCOPED search marker
# flow — checked BEFORE the phase-68 search flow (disjoint
# trigger phrases — the phase-71/72 ordering convention; the
# trigger needs the ``<tools>`` section, so deflected turns
# never hit it). The scoped form (pattern + path) is the wire
# subject: the app's tool frame carries the scope as its
# additive ``path`` field, and the UI shows WHERE the search
# runs (the "repeating itself" confusion, TODO.md L3).
scoped_search = _scoped_search_flow(body)
if scoped_search is not None:
if scoped_search[0] == "search":
stream = _tool_call_stream(
"grep",
{"pattern": SEARCH_PATTERN, "path": SCOPED_SEARCH_PATH},
"call_0",
)
else: # "found" — quote the first matched line (80 chars)
answer = _apply_max_tokens(
f"Found {scoped_search[1][:80]}", body.get("max_tokens")
)
stream = _sse_stream(answer, 0.0)
return StreamingResponse(
stream,
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
# Phase 68 (search tool): the deterministic search marker flow —
# checked BEFORE the phase-37 tool flow (the more specific
# trigger phrase wins, same convention as THINK_PARAS_TRIGGER).