feat: auto-mode UX — per-mode spinner, round counter, first-time warning (#97)#105
Merged
VforVitorio merged 4 commits intodevfrom Apr 7, 2026
Merged
feat: auto-mode UX — per-mode spinner, round counter, first-time warning (#97)#105VforVitorio merged 4 commits intodevfrom
VforVitorio merged 4 commits 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>
…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.
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.
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
Builds on #104's
max_roundssafety core with the UX layer that makes the new boundary visible and teachable. This is PR 2 of 2 for #97.mode_color(mode)accessor in ui/status.py so agent/core.py doesn't reach into the private_MODE_COLORSdict. Applies to both the initialthinking.spinner and the live-updating_keepalivespinner.· round N/Min auto mode, driven by a newon_round_startcallback wired intomodel.act(). Shown only in auto mode because ask blocks on user approval between tool calls (redraws are rare) and strict has no round concept. Gives real-time visibility into how much of themax_roundsbudget the current turn has consumed.autoin a session prints a one-shot amber hint above the prompt via prompt_toolkit'srun_in_terminal, so it lands cleanly without tearing the ghost-text completion layer. Session-scoped viaself._auto_warned— never re-prints after cycling away and back. Format:auto mode — tools run without asking, up to N rounds per turn. Ctrl+C stops a running turn./statussurfacesmax rounds— new row in the session-state table so users can verify which safety boundary is currently in effect (config / env var /--max-roundsCLI flag all funnel into the same value).Stacked on #104
This PR targets
feat/auto-max-rounds(notdev) so the diff shown here is only the UX layer, not PR 1's safety core. Merge #104 intodevfirst, then re-target this PR todev(GitHub will auto-rebase the base).PR 1 (#104) delivers the actual safety guarantee; this PR turns it into something the user can see and learn from.
Implementation notes
max_prediction_roundsis now computed beforeasyncio.create_task(_keepalive())so the closure sees a bound cell when it first runs. Previously it was defined after the task was created — worked by accident because the firstawaithappened after assignment, but fragile.run_in_terminalrather than a directconsole.printbecause the Tab cycle fires from inside prompt_toolkit's keybinding loop. A direct print would tear the prompt;run_in_terminaltemporarily pauses the prompt, writes above it, and restores._print_auto_warning()is a method onAgentrather than a closure so it's unit-testable without prompt_toolkit terminal plumbing.Tests
test_run_turn_passes_on_round_start_to_act— verifies theon_round_startkwarg is wired as a callable and accepts a 0-based round index.test_agent_auto_warned_initially_false— pins the fresh-agent default.test_print_auto_warning_fires_once_per_session— calls the method twice, assertsconsole.printfires exactly once.test_cycle_mode_preserves_always_allowed_tools— regression guard: a future refactor that adds_always_allowed_tools.clear()to the mode handler will be caught.test_print_status_includes_max_rounds_line— asserts/statusoutput contains the new row.Test plan
uv run ruff check . && uv run ruff format --check . && uv run mypy src/ && uv run pytest— all green (213 passed)uv run lmcode chat, Tab toauto, verify amber warning prints above the prompt once and never againask, spinner becomes orange; Tab toauto, spinner becomes blue; Tab tostrict, spinner becomes red· round 2/10,· round 3/10, …/statusshowsmax rounds 10(or whateverLMCODE_AGENT__MAX_ROUNDS/--max-roundssets)Closes #97 together with #104.