phase: 160_tool_budget_telemetry
All green. Final report: **Phase 160 completion pass — verified (final verification; all 5 tasks already in `complete/`)** - **Verified, no code defects found:** budget trailers (`TOOL_BUDGET_REMAINING/LAST/SPENT`), cap-path rejection + `ROUND_CAP_FALLBACK_ANSWER` + `ROUND_CAP_WRAPUP_INSTRUCTION`, `done.budget_exhausted` (ABSENT-when-false), `rounds=/cap_hit=` log fields, `agent_max_rounds=10` default, frontend Continue button + restore + shared-page exclusion, mock markers. - **Tests/lint:** `uv run pytest` 153/153 green (EXIT 0); `uv run pytest --cov=app --cov-report=term-missing` TOTAL **99%** (>90%); `uv run ruff check .` clean; `uv run pyright` 0 errors/0 warnings. - **Criteria:** ① decreasing trailer pinned by unit tests (9→…→spent) ✅ ② cap path ends readable, no tool lines after cap (E2E) ✅ ③ `budget_exhausted` true-only-on-cap, absent otherwise (integration) ✅ ④ Continue button → one fresh `continue` turn, fresh budget, record intact (E2E) ✅ ⑤ flag survives refresh; shared chat never renders it (`shared.js` has no `budget_exhausted` path) ✅ ⑥ `max_rounds=0` byte-identical single `tools=None` request; default still 10 ✅ ⑦ full suite/coverage/lint green ✅ ⑧ `test_tool_budget_telemetry.py` **7/7** in isolation; all 6 regression E2E suites green in isolation ✅ ⑨ commit + phase move = harness (I make no git history/index changes). - **Docs:** README.md — fixed: the "Continue researching" button and cap behavior were undocumented (added a Chat-section paragraph + an Agent-tools-section paragraph, incl. the phase-71 recorded-revision note); env-var table, commands, and architecture all verified consistent otherwise. AGENTS.md — verified consistent, no changes needed. - **Decisions:** no code changes required — phase was fully implemented; only README docs added. No live-infrastructure touched. - **Next pending phase:** `161_reasoning_bubble`.
This commit is contained in:
+39
-1
@@ -312,6 +312,18 @@ class ChatDoneEvent(BaseModel):
|
||||
serialization — ABSENT, never ``null`` — so those frames stay
|
||||
byte-identical to pre-phase (the byte-identical-off contract, task
|
||||
01; the :class:`SourceRef.image_url` omission precedent).
|
||||
|
||||
Phase 160 (task 03, TODO L4): ``budget_exhausted`` — the turn
|
||||
reports that it hit ``BOR_AGENT_MAX_ROUNDS`` (the agent's round
|
||||
cap forced the final no-tools answer; driven by
|
||||
``AgentHolder.cap_reached``). ``True`` ONLY on a cap-reached turn;
|
||||
the UI's "Continue researching" affordance keys on it (task 04).
|
||||
``False`` — every deflected turn (they run no agent), every
|
||||
in-cap grounded turn, and the whole pre-phase — is DROPPED on
|
||||
serialization — ABSENT, never ``false`` — so those frames stay
|
||||
byte-identical to pre-phase (the byte-identical-off contract,
|
||||
PLAN §13; the :attr:`answer_image` / :class:`SourceRef.image_url`
|
||||
omission precedent).
|
||||
"""
|
||||
|
||||
type: str = "done"
|
||||
@@ -323,6 +335,10 @@ class ChatDoneEvent(BaseModel):
|
||||
#: :class:`SourceRef` (no list — LOCKED A3: one image per answer);
|
||||
#: the ref's own phase-83 field caps bind.
|
||||
answer_image: SourceRef | None = None
|
||||
#: Phase 160 (task 03, TODO L4) — see the class docstring. True
|
||||
#: ONLY on the cap-reached turn; ``False`` (every other turn)
|
||||
#: serializes WITHOUT the key — ABSENT, never ``false``.
|
||||
budget_exhausted: bool = False
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def _serialize(self, handler: SerializerFunctionWrapHandler) -> Any:
|
||||
@@ -333,10 +349,15 @@ class ChatDoneEvent(BaseModel):
|
||||
:class:`SourceRef.image_url` / phase-123 ``image`` omission
|
||||
precedents). A shown ref keeps all its fields (its own
|
||||
serializer drops a ``None`` ``image_url`` — a shown doc is an
|
||||
image doc, so in practice it always carries it)."""
|
||||
image doc, so in practice it always carries it). Phase 160
|
||||
(task 03): ``budget_exhausted`` ``False`` (every non-cap turn)
|
||||
serializes WITHOUT the key too — ABSENT, never ``false`` — the
|
||||
same byte-identical-off contract."""
|
||||
data = handler(self)
|
||||
if self.answer_image is None:
|
||||
data.pop("answer_image", None)
|
||||
if not self.budget_exhausted:
|
||||
data.pop("budget_exhausted", None)
|
||||
return data
|
||||
|
||||
|
||||
@@ -1325,6 +1346,23 @@ class ChatMessage(BaseModel):
|
||||
# serializer below), so pre-phase and image-less chats round-trip
|
||||
# byte-identically (the phase-50 contract).
|
||||
answer_image: SourceRef | None = None
|
||||
# Phase 160 (task 04, TODO L4): the BUDGET-EXHAUSTED marker — the
|
||||
# persisted answer that ran out of tool calls (a turn that hit
|
||||
# ``BOR_AGENT_MAX_ROUNDS``, task 03's additive ``done`` key). The
|
||||
# phase-120 ``failed`` / phase-48 ``stopped`` style: a marker on
|
||||
# the brain record itself, no separate error table, no new API.
|
||||
# It is the CLIENT'S RESTORE HINT, never a server behavior: the
|
||||
# done-time auto-save (``rememberBrainTurn``) persists the done
|
||||
# frame's ``budget_exhausted`` into this key, and the restore
|
||||
# path re-renders the "Continue researching" button from it (the
|
||||
# ``answer_image`` precedent — the field must exist so the
|
||||
# ``extra="forbid"`` boundary does NOT 422 those saves; no
|
||||
# migration: ``saved_chats.messages`` is raw JSONB, phase 50, and
|
||||
# ``app/api/chats.py``'s logic is unchanged). ``None`` (every
|
||||
# non-cap turn, every pre-phase record) flows as absent/null
|
||||
# exactly like ``stopped`` — the phase-50 byte-identical
|
||||
# round-trip contract covers the new key automatically.
|
||||
budget_exhausted: bool | None = None
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def _drop_image_when_absent(self, handler: SerializerFunctionWrapHandler) -> Any:
|
||||
|
||||
Reference in New Issue
Block a user