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:
@@ -618,6 +618,15 @@ let lastUserWrap = null;
|
||||
const RETRY_ICON =
|
||||
'<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>';
|
||||
|
||||
/* Phase 160 (task 04, TODO L4): the "Continue researching" button's
|
||||
* forward glyph (aria-hidden decoration — the visible "Continue
|
||||
* researching" label carries the accessible name). Same stroke family
|
||||
* as RETRY_ICON: fill="none", currentColor stroke (the CSS themes it
|
||||
* — ink-soft → ink on hover, the phase-08 palette), 24×24 viewBox,
|
||||
* stroke-width 1.7. No icon font, no CDN (A11). */
|
||||
const CONTINUE_ICON =
|
||||
'<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="m5 6 6 6-6 6"/><path d="m13 6 6 6-6 6"/></svg>';
|
||||
|
||||
/* Phase 159 (task 01, TODO.md L3): the Edit button's pencil glyph
|
||||
* (aria-hidden decoration — the visible "Edit" label carries the
|
||||
* accessible name). Same stroke family as RETRY_ICON: fill="none",
|
||||
@@ -648,6 +657,34 @@ function appendRetryButton(wrap) {
|
||||
meta.appendChild(btn);
|
||||
}
|
||||
|
||||
/* Phase 160 (task 04, TODO L4): the "Continue researching" button —
|
||||
* the one-click continuation of a turn that spent its whole tool
|
||||
* budget (the `done` frame's additive `budget_exhausted` key, task
|
||||
* 03 — ABSENT on every non-cap turn, so the button never appears
|
||||
* elsewhere). The house pattern through getActionsRow (phase 130
|
||||
* task 02 — the role=list row joins the button as a listitem so ARIA
|
||||
* stays valid); one button per bubble. NOT admin-gated, like Retry —
|
||||
* chat is public, so every visitor can keep a long research session
|
||||
* going. On a capped answer the row order is Tune → Retry → Continue
|
||||
* → Save-as-doc (the insertion order phase 130 relies on). Click →
|
||||
* continueTurn() — a plain next turn: the budget is per-turn, so the
|
||||
* new turn has a whole new round budget, and nothing server-side
|
||||
* knows or cares that this is a continuation (A10). */
|
||||
const CONTINUE_QUESTION = "continue";
|
||||
|
||||
function appendContinueButton(wrap) {
|
||||
const meta = getActionsRow(wrap);
|
||||
if (!meta) return;
|
||||
if (meta.querySelector(".continue-btn")) return; // one per bubble
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "continue-btn";
|
||||
if (meta.getAttribute("role") === "list") btn.setAttribute("role", "listitem");
|
||||
btn.innerHTML = CONTINUE_ICON + "<span>Continue researching</span>";
|
||||
btn.addEventListener("click", () => continueTurn());
|
||||
meta.appendChild(btn);
|
||||
}
|
||||
|
||||
/* "Edit" button in the last USER bubble's actions row (phase 159,
|
||||
* task 01, TODO.md L3 — the trim-and-re-ask remedy: a "Question too
|
||||
* long — trim it and re-ask" turn, or any mistyped / half-formed
|
||||
@@ -2688,6 +2725,14 @@ function renderStoredMessage(m) {
|
||||
// precedes the failed record, unchanged; a mid-conversation failed
|
||||
// bubble gets no button — the phase-49 last-bubble-only rule).
|
||||
if (m.failed && m.error) appendFailedNote(wrap, m.error);
|
||||
// Phase 160 (task 04, TODO L4): a cap-reached answer keeps its
|
||||
// Continue affordance on a page refresh — a restored `history` is
|
||||
// exactly what the continuation turn needs (runTurn's plain send
|
||||
// carries conversation.slice(0, -1)). The marker is a client
|
||||
// restore hint only (the schema's budget_exhausted key, never a
|
||||
// server behavior), so pre-phase / non-cap records restore
|
||||
// byte-identically (no key → no button).
|
||||
if (m.budget_exhausted) appendContinueButton(wrap);
|
||||
lastBrainWrap = wrap; // phase 49: the LAST restored brain bubble wins
|
||||
}
|
||||
|
||||
@@ -3304,6 +3349,30 @@ function retryLastTurn(wrap) {
|
||||
return runTurn(text, { reask: true });
|
||||
}
|
||||
|
||||
/* Phase 160 (task 04, TODO L4): the "Continue researching" click —
|
||||
* the retryLastTurn discipline, one turn at a time. No-op while a
|
||||
* turn is in flight (thinking / streaming — the runTurn guard; the
|
||||
* button is never double-fired into two turns), then clear EVERY
|
||||
* rendered .continue-btn FIRST (the markLastRetryable remove-first
|
||||
* pattern: the moment the new turn's answer lands, a stale button on
|
||||
* any earlier bubble is gone by construction; the one-per-bubble
|
||||
* guard + the clear make a double-send impossible), then a PLAIN send
|
||||
* (runTurn's `reask` defaults to false): the "continue" user bubble
|
||||
* appears, and the record is pushed + saved through the ordinary save
|
||||
* points (conversation.push + saveConversation + persistConversation).
|
||||
* The request carries the FULL history (conversation.slice(0, -1)) —
|
||||
* the model resumes with everything it already read and gets a fresh
|
||||
* agent_max_rounds. ASSUMPTION: the continuation question is the
|
||||
* literal word "continue" — what the model is taught to ask the user
|
||||
* for (ROUND_CAP_WRAPUP_INSTRUCTION / the tool-budget-spent trailer,
|
||||
* phase 160 tasks 01–02), so the visible bubble, the typed
|
||||
* equivalent, and the model's own suggestion all agree. */
|
||||
function continueTurn() {
|
||||
if (uiState === UI_STATE.thinking || uiState === UI_STATE.streaming) return;
|
||||
messagesEl.querySelectorAll(".continue-btn").forEach((b) => b.remove());
|
||||
return runTurn(CONTINUE_QUESTION);
|
||||
}
|
||||
|
||||
/* Phase 123 (task 02, TODO L6): the send flow's UPLOAD STEP (locked
|
||||
* A8) — the attached File goes to POST /api/chat-images as multipart
|
||||
* (the session cookie rides the browser; the endpoint is user-gated
|
||||
@@ -3933,6 +4002,12 @@ async function runTurn(text, { reask = false, image = null } = {}) {
|
||||
// row exactly as it looked live.
|
||||
related: ev.related?.length ? ev.related : undefined,
|
||||
suggestions: ev.suggestions,
|
||||
// Phase 160 (task 04, TODO L4): the cap-reached marker
|
||||
// persists with the turn — `undefined` drops the key from
|
||||
// the JSON (the house optional-meta pattern, the
|
||||
// answer_image line above is the precedent), so every
|
||||
// non-cap record stays byte-identical to pre-phase.
|
||||
budget_exhausted: ev.budget_exhausted || undefined,
|
||||
}, leavePartialIndex);
|
||||
lastBrainWrap = wrap; // this bubble is now the last brain answer
|
||||
// Phase 130 (task 02): the row order is the DOM order — the
|
||||
@@ -3943,6 +4018,20 @@ async function runTurn(text, { reask = false, image = null } = {}) {
|
||||
appendTuneButton(wrap); // every completed brain bubble is tunable
|
||||
markLastRetryable(); // phase 49: the Retry button is last-bubble-only
|
||||
markLastEditable(); // phase 159: the done turn's question is editable (the in-flight guard no-ops here — the runTurn idle settle lands it)
|
||||
// Phase 160 (task 04, TODO L4): a cap-reached answer offers
|
||||
// the one-click continuation — first, every earlier bubble's
|
||||
// continue button is cleared (the markLastRetryable
|
||||
// remove-first pattern: the moment a newer turn lands, an old
|
||||
// bubble's stale affordance is gone — its "continue" would
|
||||
// append to a history the conversation already moved past),
|
||||
// then the button is appended ONLY when the done frame is
|
||||
// flagged (ABSENT on every other turn, so the button never
|
||||
// appears elsewhere); the row order stays Tune → Retry →
|
||||
// Continue → Save-as-doc (the insertion order phase 130
|
||||
// relies on — markLastEditable above acts on the user
|
||||
// bubble, so the brain bubble's row order is unchanged).
|
||||
messagesEl.querySelectorAll(".continue-btn").forEach((b) => b.remove());
|
||||
if (ev.budget_exhausted) appendContinueButton(wrap);
|
||||
// Phase 59: the RAW persisted markdown (never the rendered
|
||||
// HTML) — exactly the string rememberBrainTurn stores above,
|
||||
// so a reload (the restore path) offers the identical draft.
|
||||
|
||||
Reference in New Issue
Block a user