feat: wire max_rounds safety boundary into model.act() (#97)#104
Merged
VforVitorio merged 1 commit intodevfrom Apr 7, 2026
Merged
feat: wire max_rounds safety boundary into model.act() (#97)#104VforVitorio merged 1 commit intodevfrom
VforVitorio merged 1 commit intodevfrom
Conversation
- AgentSettings.max_rounds default 50->10 with docstring explaining the trade-off and the 3 ways to override (config, env var, CLI flag) - _run_turn passes max_prediction_rounds= to model.act() in the non-strict branch; strict stays on respond() which has no round concept - Catch LMStudioPredictionError specifically when the message contains "final prediction round" and set self._last_turn_limit_reached. This prevents the run() top-level LMStudioServerError handler (parent class) from misreading the crash as an LM Studio disconnect - Post-act check: ActResult.rounds >= cap also sets the flag (the well-behaved model case where the final round produced a text answer instead of a tool_call) - run() prints an inline amber warning when the flag is set, matching the ctx window warning style - CLI --max-rounds flag is now actually wired: mutates get_settings().agent.max_rounds for the session. Was previously accepted by Typer but silently ignored - 5 regression tests + _make_mock_model fixed to return an ActResult-shaped mock with concrete rounds int (MagicMock >= int raises TypeError) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VforVitorio
added a commit
that referenced
this pull request
Apr 7, 2026
…ing (#97) Builds on PR 1's max_rounds safety core with the UX layer that makes the new boundary visible and teachable. - Per-mode spinner colour: the in-turn spinner tracks the active mode (orange=ask, blue=auto, red=strict) via a new mode_color() accessor in ui/status.py so the current mode is visible at a glance without reading the prompt line. - Auto-mode round counter: in auto mode the spinner label appends " · round N/M" driven by an on_round_start callback wired into model.act(). Only shown in auto mode (ask blocks on user approval between tool calls, strict has no round concept). - First-time auto warning: the first Tab-cycle into auto in a session prints an amber one-liner above the prompt via run_in_terminal, gated by self._auto_warned so it never repeats. - /status surfaces the active max_rounds so users can verify which safety boundary is in effect after config / env / --max-rounds. Tests cover: on_round_start kwarg wiring, _auto_warned initial state and one-shot behaviour, mode-cycle preservation of _always_allowed_tools (regression guard), /status max-rounds row. Stacked on feat/auto-max-rounds — merge PR #104 first.
5 tasks
VforVitorio
added a commit
that referenced
this pull request
Apr 7, 2026
When dev (containing the squash-merged #104) was merged into this branch, git's three-way merge dropped three pieces of PR 2 work because it saw them as conflicts with the squashed version of #104: - self._auto_warned init flag was removed from Agent.__init__, causing mypy `has-type` error in _print_auto_warning - on_round_start=_on_round_start kwarg was dropped from the model.act() call, breaking the round counter test - max_prediction_rounds was re-added in its original post-keepalive position, duplicating the moved-up declaration and causing the mypy `no-redef` error at line 1267 Also restores the 5 PR 2 tests and the 4 PR 2 CHANGELOG entries that the merge silently dropped. Local ruff + mypy + pytest green.
VforVitorio
added a commit
that referenced
this pull request
Apr 7, 2026
…ing (#97) (#105) * feat: wire max_rounds safety boundary into model.act() (#97) - AgentSettings.max_rounds default 50->10 with docstring explaining the trade-off and the 3 ways to override (config, env var, CLI flag) - _run_turn passes max_prediction_rounds= to model.act() in the non-strict branch; strict stays on respond() which has no round concept - Catch LMStudioPredictionError specifically when the message contains "final prediction round" and set self._last_turn_limit_reached. This prevents the run() top-level LMStudioServerError handler (parent class) from misreading the crash as an LM Studio disconnect - Post-act check: ActResult.rounds >= cap also sets the flag (the well-behaved model case where the final round produced a text answer instead of a tool_call) - run() prints an inline amber warning when the flag is set, matching the ctx window warning style - CLI --max-rounds flag is now actually wired: mutates get_settings().agent.max_rounds for the session. Was previously accepted by Typer but silently ignored - 5 regression tests + _make_mock_model fixed to return an ActResult-shaped mock with concrete rounds int (MagicMock >= int raises TypeError) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: auto-mode UX — per-mode spinner, round counter, first-time warning (#97) Builds on PR 1's max_rounds safety core with the UX layer that makes the new boundary visible and teachable. - Per-mode spinner colour: the in-turn spinner tracks the active mode (orange=ask, blue=auto, red=strict) via a new mode_color() accessor in ui/status.py so the current mode is visible at a glance without reading the prompt line. - Auto-mode round counter: in auto mode the spinner label appends " · round N/M" driven by an on_round_start callback wired into model.act(). Only shown in auto mode (ask blocks on user approval between tool calls, strict has no round concept). - First-time auto warning: the first Tab-cycle into auto in a session prints an amber one-liner above the prompt via run_in_terminal, gated by self._auto_warned so it never repeats. - /status surfaces the active max_rounds so users can verify which safety boundary is in effect after config / env / --max-rounds. Tests cover: on_round_start kwarg wiring, _auto_warned initial state and one-shot behaviour, mode-cycle preservation of _always_allowed_tools (regression guard), /status max-rounds row. Stacked on feat/auto-max-rounds — merge PR #104 first. * fix: restore PR 2 content lost in dev→feat/auto-mode-ux merge When dev (containing the squash-merged #104) was merged into this branch, git's three-way merge dropped three pieces of PR 2 work because it saw them as conflicts with the squashed version of #104: - self._auto_warned init flag was removed from Agent.__init__, causing mypy `has-type` error in _print_auto_warning - on_round_start=_on_round_start kwarg was dropped from the model.act() call, breaking the round counter test - max_prediction_rounds was re-added in its original post-keepalive position, duplicating the moved-up declaration and causing the mypy `no-redef` error at line 1267 Also restores the 5 PR 2 tests and the 4 PR 2 CHANGELOG entries that the merge silently dropped. Local ruff + mypy + pytest green. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First of two PRs for #97 (robust auto mode). This one is the safety core — stops runaway tool loops before they burn the context window. No visual changes; follow-up PR 2 (
feat/auto-mode-ux) will add the spinner color per mode, round counter, and first-time auto warning.What changed
AgentSettings.max_roundsdefault 50→10, docstring explains the trade-off. Themax_roundsfield already existed but was never wired tomodel.act()— effectively dead code._run_turnnow passesmax_prediction_rounds=get_settings().agent.max_roundstomodel.act()in the non-strict branch. Strict mode is untouched (usesmodel.respond()which has no round concept).ActResult.rounds == cap→ flag set.tool_callon the final round, SDK raisesLMStudioPredictionError("Model requested tool use on final prediction round.")._run_turncatches this specifically (matching on the"final prediction round"substring constant_MAX_ROUNDS_ERR_MARKER) and sets the flag. Any otherLMStudioPredictionErrorre-raises. Critical:LMStudioPredictionErroris a subclass ofLMStudioServerError, whichrun()catches at the top level as a "LM Studio disconnected" signal. Without the specific catch, hitting the round limit would show the disconnect screen.run()prints an inline warning when the flag is set, matching the ctx window warning style (no panel, just coloured text above theRuleseparator):--max-roundsflag is now actually wired: mutatessettings.agent.max_roundsfor the session. Previously accepted by Typer but silently ignored. Default changed from50toNone(fall back to config).Tests
5 new tests in
tests/test_agent/test_core.py:test_run_turn_passes_max_rounds_to_act— the kwarg flows from config into the SDK call.test_run_turn_max_rounds_none_when_config_zero— a non-positivemax_roundspassesNoneto the SDK (which rejects values< 1).test_run_turn_flags_limit_reached_when_rounds_equal_cap— well-behaved case sets the flag.test_run_turn_flags_limit_reached_on_final_round_error— stubborn case catches the SDK error without re-raising.test_run_turn_reraises_unrelated_prediction_error— pins the catch to the marker string, not toLMStudioPredictionErrorbroadly.Also fixed
_make_mock_modelto return a mock with concreterounds: intandtotal_time_seconds: float— the post-act comparisongetattr(result, "rounds", 0) >= capcrashes on the defaultMagicMock.roundswithTypeError.Test plan
uv run lmcode chat --max-rounds 3, Tab to auto, ask for a task requiring ≥ 4 tool calls (e.g. "read calculator.py, greet.py and data.json then add a comment to each"). Expect the ⚠ warning after the model stops.uv run lmcode chat(default 10) with normal single-step tasks — should behave identically to before.LMCODE_AGENT__MAX_ROUNDS=1 uv run lmcode chat— env var override works.Follow-up — PR 2 (
feat/auto-mode-ux)Depends on this PR being merged first (uses
max_roundsas the denominator for the round counter). Will add:_MODE_COLORS)working… · round 3/10), auto mode only/statusline showing currentmax_rounds_always_allowed_toolssurvivesask → auto → strict → askcycling🤖 Generated with Claude Code