Converts the 9 TODO items into an executable phase roadmap (Protocol B, appended after phase 39): - 40 tuning toggle anonymous flash (TODO L3) - 41 sync fail-fast + modal when a model is down (TODO L4) - 42 no reply autoscroll (TODO L5) - 43 thinking scroll back — user scroll + gated autoscroll (TODO L7) - 44 markdown tables (TODO L6) - 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8) - 46 mobile hamburger nav (TODO L9) - 47 quadlet + jinja import formats, A9 revision (TODO L10–L11) Each phase carries a user story, a dedicated Playwright E2E suite plan, and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget revision, A1–A5 scope decisions) confirmed 2026-08-27. Also records the completed phases 30–39 todo/ -> complete/ moves that were pending in the working tree. TODO.md is cleared (items now live in .agent/phases/todo/).
4.6 KiB
Phase 45 — Agent makes as many tool calls as it wants
Source: TODO.md L8 — "Allow the LLM to make as many tool calls as it wants, remove the restrictions, they're causing problems getting correct answers"
Story: .agent/user_stories/agent-unlimited-tools.md
Context: Phase 37 shipped the grounded-turn agent loop (app/rag/agent.py::run_agent) with per-turn budgets — agent_list_calls / agent_read_calls (default 1 each, BOR_AGENT_LIST_CALLS / BOR_AGENT_READ_CALLS), "budgets-as-kill-switch" locked decision. The exhaustion refusals (LIST_EXHAUSTED / READ_EXHAUSTED) are where correct multi-document answers die. Owner direction (2026-08-27): remove both budgets; the loop keeps one guard — a configurable round cap that also doubles as the no-tools kill switch (0).
Objective
list_documents / read_document can be called as many times as the model needs (re-lists included), bounded only by BOR_AGENT_MAX_ROUNDS (default 10; 0 = no tools, byte-identical to the pre-phase-37 path).
Dependencies
44_markdown_tables(todo) — sequential only (no shared files: this phase isapp/+ tests + mock).37_agent_document_tools(complete) — the loop, thetoolSSE event, the UI tool lines, the per-turntool_calls=Nlog field, and the phase-37 locked decision being revised.31_kb_overview_prompt(complete) — theliteone-shot path is untouched by this phase.
Tasks
01_config_round_cap.md— the server core, atomically:agent_max_roundsreplaces the budgets inapp/config.py+app/rag/agent.py, unit + integration rewrites,.env.example(one task so the per-task gate stays green).02_mock_multi_read_flow.md— the E2E mock's deterministic multi-read (list → read #1 → read #2 → answer) flow.03_unlimited_tools_e2e_and_commit.md— story E2E + phase-37 regression + PLAN.md revision note + commit.
Testing & Quality
- Unit:
tests/unit/test_agent.pyrewritten around the round cap (always-calling mock LLM: N tool rounds then a forcedtools=Nonefinal answer;max_rounds=0→ exactly one request withtools=None; rejected-call spam — unknown tool / already-in-context — is bounded by the cap, not by budgets; re-lists execute and count intool_calls);tests/unit/test_config.py(default 10,BOR_AGENT_MAX_ROUNDSoverride,0, the budget env vars are gone). - Integration:
tests/integration/test_chat_api.py— theagent_list_calls=0, agent_read_calls=0fixtures becomeagent_max_rounds=0; the tool SSE event shape and thedone.sourcesextension assertions stay. - Coverage: >90% on
app/—agent.py+config.pyfully covered. - E2E (mandatory, A16):
tests/e2e/test_agent_unlimited_tools.py, run in isolation.
Completion Criteria
BOR_AGENT_LIST_CALLS/BOR_AGENT_READ_CALLSare gone (config,.env.example, agent, tests); no exhaustion refusal remains.- A multi-read turn (list + 2 reads) streams three tool lines, answers non-deflected, and
done.sourceslists the retrieved doc(s) + both reads deduped. agent_max_rounds=0→ singletools=Nonerequest (kill switch); at the cap the loop forces a final no-tools answer (log warning kept).toolSSE event shape andtool_calls=Nper-turn log field unchanged..agent/PLAN.mdcarries the phase-37 revision note (owner permission 2026-08-27,TODO.mdL8) — the only PLAN edit in this phase.uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missing>90%.uv run pytest tests/e2e/test_agent_unlimited_tools.py -v --no-covgreen in isolation (DB up).- Regression E2E suites green in isolation:
test_agent_document_tools.py,test_chat_rag.py,test_smoke.py. uv run ruff check . && uv run pyrightclean.- One
--no-gpg-signcommit; phase dir moved to.agent/phases/complete/.
Locked decisions
- Owner-locked revision (2026-08-27, roadmap R2): the phase-37 "budgets-as-kill-switch" decision is revised — both per-tool budgets removed;
BOR_AGENT_MAX_ROUNDS(default 10) is the only loop guard and the kill switch (0). Recorded as a PLAN.md revision note (the established owner-permission pattern, like the A10/A7/A9/A15 notes) — a recorded revision, not a silent deviation (AGENTS.md rule 3). - A15 extension unchanged — the
toolSSE event shape, thedoneshape, and the per-turn log line (tool_calls=N) are untouched; the revision note amends the phase-37 note's budget wording only. - Rejections kept:
Unknown tool.,MISSING_READ_ARGS,Already in your context.(non-budget rejections; the cap bounds their pathological repetition). - A17 honoured — one atomic commit.