phase: 160_tool_budget_telemetry
Build and Push Containers / build-and-push-app (push) Successful in 2m27s
Build and Push Containers / build-and-push-db (push) Failing after 2m8s

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:
2026-10-06 01:42:44 -04:00
parent cb8767512a
commit 8172956c15
55 changed files with 4124 additions and 344 deletions
@@ -565,6 +565,88 @@ def test_done_event_answer_image_ref_drops_its_own_null_image_url() -> None:
assert "image_url" not in dump["answer_image"]
# --- the ``budget_exhausted`` done key (phase 160, task 03) ---
def test_done_event_without_budget_exhausted_drops_the_key() -> None:
"""``budget_exhausted=False`` (the default — every deflected /
in-cap / pre-phase turn) → ``model_dump()`` has NO
``budget_exhausted`` key — ABSENT, never ``false`` — and a full
done frame (sources + related + suggestions, no flag) serializes
byte-identically to the pre-phase expectation (the
byte-identical-off contract, PLAN §13)."""
event = ChatDoneEvent(
deflected=False,
sources=[SourceRef(source="docs", path="a.md", title="A")],
related=[SourceRef(source="docs", path="b.md", title="B")],
suggestions=["Maybe try X?"],
)
dump = event.model_dump()
assert "budget_exhausted" not in dump
# The pre-phase-160 key set, exactly (answer_image absent too).
assert set(dump) == {"type", "deflected", "sources", "related", "suggestions"}
assert _json_dumps(dump, sort_keys=True) == _json_dumps(
{
"type": "done",
"deflected": False,
"sources": [{"source": "docs", "path": "a.md", "title": "A"}],
"related": [{"source": "docs", "path": "b.md", "title": "B"}],
"suggestions": ["Maybe try X?"],
},
sort_keys=True,
)
raw = _json_dumps(dump)
assert "budget_exhausted" not in raw
def test_done_event_with_budget_exhausted_carries_the_key() -> None:
"""``budget_exhausted=True`` (a cap-reached turn) → the key is
present and ``true``, the rest of the frame untouched; an
explicit ``False`` behaves exactly like the default (absent)."""
dump = ChatDoneEvent(deflected=False, sources=[], budget_exhausted=True).model_dump()
assert dump["budget_exhausted"] is True
assert dump["type"] == "done"
assert dump["deflected"] is False
assert dump["related"] == []
assert dump["suggestions"] == []
explicit_false = ChatDoneEvent(
deflected=False, sources=[], budget_exhausted=False
).model_dump()
assert "budget_exhausted" not in explicit_false
def test_done_event_budget_exhausted_and_answer_image_are_independent() -> None:
"""Both optional keys shown on the same turn: the two omission
rules are independent — a cap-reached turn with a shown answer
image carries BOTH keys; the answer-image ref's own serializer
still applies inside the frame."""
dump = ChatDoneEvent(
deflected=False,
sources=[],
answer_image=SourceRef(**_SHOWN_REF),
budget_exhausted=True,
).model_dump()
assert dump["budget_exhausted"] is True
assert dump["answer_image"] == _SHOWN_REF
def test_done_event_without_budget_exhausted_parses_a_pre_phase_frame() -> None:
"""Back-compat: a pre-phase-160 done frame (no
``budget_exhausted`` key) parses with the default ``False`` and
re-dumps byte-identically (PLAN §4 — the field is additive in
both directions)."""
old_payload = {
"type": "done",
"deflected": True,
"sources": [],
"related": [],
"suggestions": [],
}
event = ChatDoneEvent(**old_payload)
assert event.budget_exhausted is False
assert event.model_dump() == old_payload
# --- the ``ChatMessage`` boundary (the saved-chat record key) ---