phase: 146_search_tool_scope
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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user