feat(agent): strip raw tool-scaffolding from streamed answers — deterministic filter with one bounded recovery

This commit is contained in:
2026-09-03 13:39:15 -04:00
parent 801639efcc
commit 575d6c88d0
38 changed files with 2793 additions and 50 deletions
+128
View File
@@ -127,6 +127,35 @@ 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 ``emit raw tool markup``
(``SCAFFOLD_TRIGGER``, phase 71, tool-scaffolding guardrails — the
2026-09-03 incident where a deflected round streamed the model's
raw ``<|tool_call_start|>…<|tool_call_end|>`` markup into the UI)
**or** ``always emit raw tool markup``
(``SCAFFOLD_ALWAYS_TRIGGER``, checked FIRST — it contains the
former phrase) -> the deterministic SCAFFOLDING flow, independent
of the ``<tools>`` marker (both grounded and deflected turns hit
it):
* ``SCAFFOLD_ALWAYS_TRIGGER``: EVERY request (the one bounded
recovery included) streams ONLY ``delta.content`` chunks
carrying the incident span ``SCAFFOLD_SPAN`` —
``<|tool_call_start|>[read(path='search_docs/reese-notes.md')]
<|tool_call_end|>`` — split across the mock's 12-char chunks
(the filter's boundary path), ``finish_reason: "stop"``, no
structured ``tool_calls``, no reasoning — the terminal
malformed-reply path.
* ``SCAFFOLD_TRIGGER``: request 1 (no ``CORRECTION_INSTRUCTION``
in the system prompt) streams the same scaffolding-only span;
request 2 (the system prompt carries the harness constant — a
stable substring of ``app.rag.agent.CORRECTION_INSTRUCTION``,
IMPORTED into this module so the mock can never drift from
it: the one bounded recovery, ``tools=None`` with the
correction folded into the single system prompt) streams the
clean ``SCAFFOLD_RECOVERY_ANSWER`` — the recovery path.
Checked BEFORE the ``SEARCH_TRIGGER`` / ``TOOLS_TRIGGER`` flows
(the trigger needs no ``<tools>`` section); no existing E2E
question or fixture file contains the phrase, so every other
suite is unaffected.
- user message containing ``show me a table`` (phase 44, markdown
tables, TODO.md L6) -> the fixed table answer (``TABLE_ANSWER``):
a 3-column service table, an ``<img onerror>`` XSS probe line, and
@@ -188,6 +217,8 @@ from typing import Any
from fastapi import FastAPI
from fastapi.responses import JSONResponse, StreamingResponse
from app.rag.agent import CORRECTION_INSTRUCTION # phase 71: the harness constant
app = FastAPI()
DIM = 768
@@ -364,6 +395,51 @@ ALWAYS_FAIL_TRIGGER = "always fail"
#: bag-of-words vector — the endpoint's pre-stream embedding retry loop.
EMBED_FAIL_TRIGGER = "embed fail once"
# ---------------------------------------------------------------------------
# Phase 71 (tool-scaffolding guardrails, 2026-09-03 incident):
# deterministic raw-markup flows — see the module docstring
# ---------------------------------------------------------------------------
#: A user message containing this substring (case-insensitive) drives
#: the scaffolding flow: request 1 streams ONLY the incident's raw tool
#: markup as ``delta.content``; the follow-up request carrying the
#: harness correction in the system prompt (the one bounded recovery)
#: streams the clean answer. Independent of the ``<tools>`` marker —
#: both grounded and deflected turns hit it. Existing E2E questions do
#: not contain the phrase, so every other suite is unaffected.
SCAFFOLD_TRIGGER = "emit raw tool markup"
#: A user message containing this substring (checked BEFORE
#: ``SCAFFOLD_TRIGGER`` — it contains that phrase) streams the
#: scaffolding-only span on EVERY request, recovery included — the
#: terminal malformed-reply path (the dedicated error frame, no done).
SCAFFOLD_ALWAYS_TRIGGER = "always emit raw tool markup"
#: The incident span (2026-09-03): the model's chat-template tool
#: syntax, emitted as plain ``delta.content`` although no tools were
#: offered. Streamed through the mock's 12-char chunking, so it always
#: spans ≥2 wire chunks (the filter's boundary path).
SCAFFOLD_SPAN = (
"<|tool_call_start|>[read(path='search_docs/reese-notes.md')]"
"<|tool_call_end|>"
)
#: The clean answer the one bounded recovery produces (byte-stable —
#: the dedicated E2E suite asserts the recovered bubble and the wire's
#: delta text against it).
SCAFFOLD_RECOVERY_ANSWER = "Here is the plain-text answer the recovery produced."
#: The stable substring of the harness-owned correction constant the
#: recovery request carries in its system prompt. Keyed on a substring
#: (not the whole constant) so a re-wrap of the constant cannot silently
#: re-route the mock; the module-level assert below fails loudly if the
#: substring ever leaves the constant (the mock must never drift from
#: ``app.rag.agent.CORRECTION_INSTRUCTION``).
_CORRECTION_MARKER = "no tool syntax"
assert _CORRECTION_MARKER in CORRECTION_INSTRUCTION, (
"mock drift: the correction marker left CORRECTION_INSTRUCTION"
)
#: One DEAD app-level chat attempt costs exactly this many HTTP POSTs
#: while the endpoint stays down: the openai SDK's default policy
#: (max_retries=2 — the app's ``LLMClient`` keeps it) re-POSTs a 500'd
@@ -535,6 +611,35 @@ def _search_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
return ("search",)
def _scaffold_flow(body: dict[str, Any]) -> str | None:
"""Classify a phase-71 scaffolding request (see the module docstring).
* ``"scaffold"`` — stream ONLY the incident span
(``SCAFFOLD_SPAN``) as ``delta.content`` chunks: ``finish_reason:
"stop"``, no structured ``tool_calls``, no reasoning. EVERY
request for ``SCAFFOLD_ALWAYS_TRIGGER`` (the recovery included),
and the FIRST request of ``SCAFFOLD_TRIGGER`` (no correction in
the system prompt yet).
* ``"recovery"`` — ``SCAFFOLD_TRIGGER`` whose system prompt carries
the harness correction (the one bounded recovery: ``tools=None``,
the constant folded into the single system prompt by
``app.api.chat`` / ``app.rag.agent``): stream the clean
``SCAFFOLD_RECOVERY_ANSWER``.
* ``None`` — not the scaffolding flow. The discrimination is
stateless, like the other marker flows: the trigger phrase in
the user message plus the correction's presence in the system
prompt.
"""
user = _user(body).lower()
if SCAFFOLD_ALWAYS_TRIGGER in user: # checked FIRST — it contains SCAFFOLD_TRIGGER
return "scaffold"
if SCAFFOLD_TRIGGER in user:
if _CORRECTION_MARKER in _system(body):
return "recovery"
return "scaffold"
return None
def _tool_flow(body: dict[str, Any]) -> tuple[str, ...] | None:
"""Classify a marker request into one step of the tool flow.
@@ -1061,6 +1166,29 @@ def chat_completions(body: dict[str, Any]) -> Any:
if _chat_dead(RETRY_TRIGGER, RETRY_DEAD_ATTEMPTS):
return _llm_500(RETRY_TRIGGER)
_fail_posts[RETRY_TRIGGER] = 0 # the answer streamed — restart
# Phase 71 (tool-scaffolding guardrails): the deterministic raw-
# markup flow — checked BEFORE the search/tool marker flows (the
# trigger is independent of the ``<tools>`` marker, so both
# grounded and deflected turns hit it; SCAFFOLD_ALWAYS_TRIGGER
# is checked first inside the classifier — the more specific
# phrase wins, same convention as THINK_PARAS_TRIGGER).
scaffold_flow = _scaffold_flow(body)
if scaffold_flow is not None:
# Request 1 (or EVERY request for the ALWAYS trigger): the
# incident span as plain delta.content, 12-char chunks
# (the span always spans ≥2 chunks — the filter's boundary
# path), finish_reason "stop", no tool_calls, no reasoning.
# Request 2 of the recovery trigger: the clean answer.
answer = (
SCAFFOLD_RECOVERY_ANSWER
if scaffold_flow == "recovery"
else SCAFFOLD_SPAN
)
return StreamingResponse(
_sse_stream(answer, 0.0),
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).