fix: end a spent tool budget in an answer, not a notice about the loop

Asked for the fish in every Valheim biome, the turn researched for five
rounds and then displayed "Stopped after 5 rounds of tool calls without a
final answer." Three things compounded:

- MAX_TOOL_ITERATIONS of 5 is too few for a question that needs several
  searches and a few pages. It is a safety net against looping, not a
  budget anyone chose.
- The loop discarded everything. `content` was scoped to the iteration, so
  every preamble the model wrote was thrown away and the turn ended with an
  app-generated message about its own control flow.
- The model was never told it was running out, so it kept searching until
  the hard stop instead of answering with what it had.

The budget is now spent on a closing round with the tools taken away and an
instruction to answer from what was gathered, naming anything it could not
confirm. So the worst case is a partial answer that says what is missing.
If the closing round produces nothing, the last preamble is used, and only
then the notice. The cap is 8 because the extra round is not more searching,
it is the answer.

Verified: 349 tests, tsc, eslint, prettier, and vici build.
This commit is contained in:
2026-10-04 16:08:26 +08:00
parent af325b4a62
commit 1ae44adc7b
4 changed files with 179 additions and 14 deletions
+11 -2
View File
@@ -7,8 +7,17 @@ export const DEFAULT_MODEL = "gpt-oss:120b";
export const STORAGE_INDEX_KEY = "lam.sessions.index";
export const STORAGE_SESSION_PREFIX = "lam.session.";
/** How many times the model may call tools before Lam gives up and reports back. */
export const MAX_TOOL_ITERATIONS = 5;
/**
* How many rounds of tool calls one turn may spend before the model is told to
* answer with what it has.
*
* Five was too few for a question that needs several searches and a few pages,
* and the failure mode was the worst one available: the budget ran out, every
* preamble the model had written was discarded, and the turn ended with a notice
* about the limit rather than an answer. The closing round in `agent.ts` now
* turns that into an answer, and this is the room it gets to gather first.
*/
export const MAX_TOOL_ITERATIONS = 8;
/** Oldest sessions past this count are evicted, along with their storage keys. */
export const MAX_SESSIONS = 100;