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:
@@ -232,6 +232,12 @@ def _expect(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"stopped": m.get("stopped"),
|
||||
"failed": m.get("failed"),
|
||||
"error": m.get("error"),
|
||||
# Phase 160 (task 04): the budget-exhausted marker —
|
||||
# explicit null where the record does not carry it (the
|
||||
# bor.chat.v1 shape is explicit-null for every optional
|
||||
# key; a record WITH the key round-trips it byte-
|
||||
# identically — the pins below).
|
||||
"budget_exhausted": m.get("budget_exhausted"),
|
||||
}
|
||||
for m in records
|
||||
]
|
||||
@@ -464,8 +470,10 @@ def test_create_round_trips_full_brain_record(admin_client: TestClient) -> None:
|
||||
assert r.status_code == 201
|
||||
# The bor.chat.v1-shaped payload round-trips losslessly: every
|
||||
# optional key (sources/related/deflected/suggestions/thinking/
|
||||
# tools/stopped) survives identical.
|
||||
assert r.json()["messages"][1] == FULL_BRAIN
|
||||
# tools/stopped/failed/error/budget_exhausted — the phase-160
|
||||
# additive marker, explicit null where absent) survives identical
|
||||
# (the _expect full stored shape).
|
||||
assert r.json()["messages"][1] == _expect([_user(FIRST_QUESTION), FULL_BRAIN])[1]
|
||||
|
||||
|
||||
def test_create_rejects_empty_messages(admin_client: TestClient) -> None:
|
||||
@@ -1036,6 +1044,128 @@ def test_shared_serve_includes_the_brain_answer_image_ref(
|
||||
assert "answer_image" not in messages[0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 160 (task 04, TODO L4): the budget-exhausted marker — the
|
||||
# persisted answer that ran out of tool calls. The done-time
|
||||
# auto-save (``rememberBrainTurn``) persists the done frame's
|
||||
# ``budget_exhausted`` into the brain record's ``budget_exhausted``
|
||||
# key (the ``answer_image`` precedent — the field must exist so the
|
||||
# ``extra="forbid"`` boundary does NOT 422 those saves; the
|
||||
# phase-113 lesson, applied up front), and the restore path re-
|
||||
# renders the "Continue researching" button from it (task 04 — the
|
||||
# marker is a client restore hint only, never a server behavior; no
|
||||
# migration: ``saved_chats.messages`` is raw JSONB, phase 50, and
|
||||
# ``app/api/chats.py``'s logic is unchanged). These round-trips are
|
||||
# that regression pin.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_create_round_trips_the_budget_exhausted_marker(
|
||||
admin_client: TestClient,
|
||||
) -> None:
|
||||
"""The phase-113 regression pin (the A2 quiet-failure path): a
|
||||
brain record carrying ``budget_exhausted: true`` →
|
||||
``POST /api/chats`` is 201 (NOT a silent 422) and the admin GET
|
||||
round-trips the record BYTE-FOR-BYTE through Postgres (the
|
||||
``_expect`` full bor.chat.v1 shape — the restore path re-renders
|
||||
the Continue button from the marker). The user record carries the
|
||||
key as an explicit null (it never did, and the stored shape is
|
||||
explicit-null for every optional key)."""
|
||||
brain = {**FULL_BRAIN, "budget_exhausted": True}
|
||||
r = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), brain]},
|
||||
)
|
||||
assert r.status_code == 201, f"expected 201, got {r.status_code}: {r.text}"
|
||||
body = r.json()
|
||||
assert body["messages"][1]["budget_exhausted"] is True
|
||||
assert body["messages"][0]["budget_exhausted"] is None
|
||||
|
||||
got = admin_client.get(f"/api/chats/{body['id']}")
|
||||
assert got.status_code == 200
|
||||
assert got.json()["messages"] == _expect(
|
||||
[_user(FIRST_QUESTION), {**FULL_BRAIN, "budget_exhausted": True}]
|
||||
)
|
||||
|
||||
|
||||
def test_create_without_budget_exhausted_marker_round_trips_unchanged(
|
||||
admin_client: TestClient,
|
||||
) -> None:
|
||||
"""A record WITHOUT the marker (every non-cap turn, every pre-
|
||||
phase record) round-trips with the key as an explicit null — the
|
||||
stored shape never gains a value (the byte-identical contract,
|
||||
the phase-50 precedent: the key is absent from the client's
|
||||
``bor.chat.v1`` record via the ``undefined``-drops-the-key
|
||||
pattern, and explicit-null here keeps the restore path null-safe).
|
||||
The ``FULL_BRAIN`` shape (no marker) is the pin: its stored and
|
||||
served form is byte-identical to pre-phase-160 apart from the
|
||||
one new null key."""
|
||||
r = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), FULL_BRAIN]},
|
||||
)
|
||||
assert r.status_code == 201, r.text
|
||||
body = r.json()
|
||||
assert body["messages"][1]["budget_exhausted"] is None
|
||||
got = admin_client.get(f"/api/chats/{body['id']}")
|
||||
assert got.status_code == 200
|
||||
assert got.json()["messages"] == _expect([_user(FIRST_QUESTION), FULL_BRAIN])
|
||||
|
||||
|
||||
def test_shared_serve_includes_the_budget_exhausted_marker(
|
||||
admin_client: TestClient,
|
||||
) -> None:
|
||||
"""The shared view is faithful (the data is in the public
|
||||
snapshot), but the control is NOT: the shared page
|
||||
(``/shared/<token>``, rendered server-side by ``shared.js`` —
|
||||
never by ``app.js``, owner-locked zero interactive controls)
|
||||
IGNORES the key by construction — no Continue control renders for
|
||||
a flagged record (work item 8; pinned in
|
||||
``test_frontend_tool_budget.py::test_shared_page_never_shows_
|
||||
the_continue_control``). The user record stays with the explicit
|
||||
null."""
|
||||
brain = {**FULL_BRAIN, "budget_exhausted": True}
|
||||
r = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), brain]},
|
||||
)
|
||||
assert r.status_code == 201, r.text
|
||||
share_url = _share(admin_client, r.json()["id"])["share_url"]
|
||||
|
||||
anon = TestClient(fastapi_app) # fresh jar: truly anonymous
|
||||
got = anon.get(f"/api{share_url}")
|
||||
assert got.status_code == 200
|
||||
messages = got.json()["messages"]
|
||||
assert messages[1]["budget_exhausted"] is True # faithful snapshot
|
||||
assert messages[0]["budget_exhausted"] is None
|
||||
|
||||
|
||||
def test_put_round_trips_the_budget_exhausted_marker(
|
||||
admin_client: TestClient,
|
||||
) -> None:
|
||||
"""The re-Save upsert (the auto-save path the client's restore
|
||||
relies on) round-trips the marker byte-identically too: a PUT
|
||||
with the flagged record replaces the stored JSONB and the 200
|
||||
body + the GET both carry it (a refreshed chat page restores the
|
||||
Continue affordance from the persisted record)."""
|
||||
created = admin_client.post(
|
||||
"/api/chats",
|
||||
json={"messages": [_user(FIRST_QUESTION), FULL_BRAIN]},
|
||||
).json()
|
||||
brain = {**FULL_BRAIN, "budget_exhausted": True}
|
||||
r = admin_client.put(
|
||||
f"/api/chats/{created['id']}",
|
||||
json={"messages": [_user(FIRST_QUESTION), brain]},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["messages"][1]["budget_exhausted"] is True
|
||||
got = admin_client.get(f"/api/chats/{created['id']}")
|
||||
assert got.status_code == 200
|
||||
assert got.json()["messages"] == _expect(
|
||||
[_user(FIRST_QUESTION), {**FULL_BRAIN, "budget_exhausted": True}]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 159 (task 02): edit-and-resend — the SAVE shape. The client
|
||||
# edits the last question: the record is replaced in place, EVERY
|
||||
|
||||
Reference in New Issue
Block a user