[feat-014] LoopController: bound tool dispatches per turn
Wraps the runTools callback in runLLMStream with a small controller that escalates on three triggers (first one wins): - MAX_STEPS_EXCEEDED - total tool dispatches >= 12 (env: OLAVA_MAX_STEPS) - REPEATED_TOOL_CALL - same name+args 3× (env: OLAVA_MAX_REPEATED_CALLS) - WALL_CLOCK_EXCEEDED - > 60s since turn start (env: OLAVA_WALL_CLOCK_MS) On escalation the controller appends a "stop calling tools and synthesise the best answer you can" note to every tool result in the batch. The model still receives the data it just fetched - we only ask it to stop reaching for more. Combined with the existing maxIterations: 10 in streamChatWithTools this is belt-and-suspenders against runaway loops. Also emits a loop.escalated event to the feat-015 audit log so post-hoc "why did this turn behave weirdly?" questions are answerable from SQL. Class is independent of chat code - 7 unit tests in loopController.test.ts cover each trigger + the negative case where args differ + currentStep accounting + escalation note formatting. All 16 backend tests pass (7 new + 9 existing security regressions). Stacked on feat-015 (uses recordEvent for the audit row). Will rebase cleanly onto main after feat-017 + feat-015 land. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| Repository | nwhitehouse/mike |
|---|---|
| Author | Nick Whitehouse <nick.whitehouse@mccarthyfinch.com> |
| Authored | |
| Parents | afeb5d8c |
| Stats | 3 files changed , +250 |
| Part of | LoopController: bound tool dispatches per turn (feat-014) |
Capture this commit into my fork
Download a Markdown prompt that tells Claude how to port this
exact commit into your working tree. Run it via
claude -p < capture-commit-22ab8a76.md
from inside the repo you want the change in.