fix(agents): honor tool_max_rounds precedence and document it #147
No reviewers
Labels
No labels
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Bounty
$100
Bounty
$1000
Bounty
$10000
Bounty
$20
Bounty
$2000
Bounty
$250
Bounty
$50
Bounty
$500
Bounty
$5000
Bounty
$750
MoSCoW
Could have
MoSCoW
Must have
MoSCoW
Should have
Needs Feedback
Points
1
Points
13
Points
2
Points
21
Points
3
Points
34
Points
5
Points
55
Points
8
Points
88
Priority
Backlog
Priority
CI Blocker
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
State
Completed
State
Duplicate
State
In Progress
State
In Review
State
Paused
State
Unverified
State
Verified
State
Wont Do
Type
Automation
Type
Bug
Type
Discussion
Type
Documentation
Type
Epic
Type
Feature
Type
Legendary
Type
Refactor
Type
Support
Type
Task
Type
Testing
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Blocks
#144 tool_max_rounds config silently ignored when falsy, and undocumented in the LLM agent specification
cleveragents/cleveractors-core
Reference
cleveragents/cleveractors-core!147
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bugfix/m1-tool-max-rounds-precedence"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
LLMAgent._execute_tool_loop()resolved its round limit viaconfig.get("tool_max_rounds") or os.environ.get("TOOL_MAX_ROUNDS", "20"), so an explicitly configured falsytool_max_rounds: 0was discarded instead of being clamped to 1, contradicting the precedence CHANGELOG.md already documented (issue #59 entry).tool_max_roundsis now validated and stored atLLMAgent.__init__()time, consistent with sibling config fields, distinguishing "absent from config" from "present but falsy". Precedence: explicit config value (including 0) >TOOL_MAX_ROUNDSenv var (only consulted when config is absent) > default of 20.tool_max_roundsin Actor Configuration Standard §4.4 (Version 1.7.0).docs/guides/reasoning-aware-llm-agents.mdupdated with concrete precedence examples.@tdd_expected_failfrom the@tdd_issue_144regression scenario now that the fix is in place; added two new scenarios covering explicit-config-over-env-var precedence and env-var-fallback-when-absent; adjusted two pre-existing scenarios whose bad-value source moved from the loop to agent construction.Test plan
nox -s unit_tests— 154 features / 3100 scenarios / 14350 steps passednox -s integration_tests— 365 Robot tests passednox -s e2e_tests— passednox -s coverage_report— 96.9% (threshold 96.5%)nox -s security_scan,nox -s dead_code,nox -s complexity— all passednox -s docs,nox -s build— passednox -s benchmark/benchmark_regression— not evaluated (informational-only ASV sessions, per maintainer instruction)Closes #144
LLMAgent._execute_tool_loop() resolved its round limit via config.get("tool_max_rounds") or os.environ.get("TOOL_MAX_ROUNDS", "20"), so an agent explicitly configured with tool_max_rounds: 0 had that value discarded by Python's `or` truthiness check and silently ran with the TOOL_MAX_ROUNDS/default limit instead, contradicting the config/env/clamp precedence CHANGELOG.md already documented publicly (issue #59 entry). tool_max_rounds is now validated and stored at LLMAgent.__init__() time, consistent with sibling config fields (token_budget_percent, pruning_threshold, max_retries), distinguishing "absent from config" from "present but falsy". Precedence: an explicit per-agent value (including 0) always wins, clamped to a minimum of 1; TOOL_MAX_ROUNDS is consulted only when tool_max_rounds is absent from config; the default of 20 applies only when both are absent. A non-integer TOOL_MAX_ROUNDS env var still raises ConfigurationError from within _execute_tool_loop(), the one remaining path that parses the value at call time rather than construction time. The governing ADR-2031 is revised in place with a new D-8 formally introducing tool_max_rounds as an Actor Configuration Standard §4.4 LLM-agent configuration field (Version 1.7.0), since its precedence contract was already a public commitment via CHANGELOG.md. The reasoning-aware-llm-agents guide is updated with concrete examples of the corrected precedence. Two pre-existing scenarios exercising the old parse-inside-the-loop behavior are adjusted for the new construction-time validation: the config-supplied bad-value case now fails at LLMAgent construction (features/llm_agent_tool_calling.feature), and the "_ToolLoopError raised before any ainvoke" case is re-targeted at a malformed TOOL_MAX_ROUNDS env var, the only remaining way to trigger that path (features/llm_agent_tool_loop.feature). Two new scenarios cover explicit config precedence over the env var and env-var fallback when config is absent. ISSUES CLOSED: #144PR Review: !147 (Ticket #144)
Verdict: Approve
The implementation correctly fixes the
tool_max_rounds: 0precedence bug, moves validation toLLMAgent.__init__()consistently with sibling fields, and updates the specification (ADR-2031 D-8, docs/index.md v1.7.0), user guide, and BDD scenarios. Code logic and test structure are sound. Only minor issues remain.Critical Issues
None.
Major Issues
None.
Minor Issues
Coverage is reported at 96.9%, below the documented 97% merge gate.
noxfile.pycurrently enforces 96.5%, so the PR passes CI, butCONTRIBUTING.mdand issue #144's acceptance criteria both require ≥97%. This is a pre-existing threshold/documentation inconsistency; consider adding targeted tests or aligning the threshold docs before it becomes a blocking gate.Inconsistent float coercion between config and env var paths.
src/cleveractors/agents/llm.py:403usesint(_raw_max_rounds)for config values (silently truncates floats like2.7to2), while the env-var path at line 1081 usesint(str(_raw_max_rounds))(rejects non-integer strings/floats). Since the field is specified as integer, consider making the two paths consistent.Missing test coverage for env-var clamping edge cases. No scenario verifies that
TOOL_MAX_ROUNDS=0or a negative env var is clamped to 1, or that an invalid env var is still rejected. These are covered by code inspection but not by dedicated BDD scenarios.Nits
_raw_max_roundsis reused to hold the clamped integer value (src/cleveractors/agents/llm.py:400-408), which shadows the original raw input. Using a separate variable (e.g.,_clamped_max_rounds) would improve readability.Error message for env-var parse failure still refers to
tool_max_rounds(src/cleveractors/agents/llm.py:1084) rather thanTOOL_MAX_ROUNDS, which may confuse users debugging an environment-only issue.Summary
Solid, focused bugfix that closes #144. The precedence contract is now correctly implemented and normatively documented. The remaining items are minor and should not block merge.
Minor issues 1, is not relevant, we have a noxfile threshold at 96.5% while having a 97% in the specification, but the 97% is the rounded value, which is also obtainable from a 96.5% real coverage.
4b9d0c5ad5bf09e4facc@hurui200320 thanks for the review. Response to each item below (verified against
docs/index.md§4.4 / ADR-2031 D-8 before acting on any of them). Pushed as an amended commit (bf09e4f, was4b9d0c5) since the branch hadn't been merged yet.Minor Issues
1. Coverage 96.9% vs the documented 97% gate — not changed.
Already addressed in my earlier comment on this PR:
noxfile.py's actualCOVERAGE_THRESHOLDconstant is96.5, and 96.9% real coverage rounds to the "97%" figure quoted inCONTRIBUTING.md/issue #144's acceptance criteria. This is a pre-existing display/rounding inconsistency between the docs and the constant, not something introduced by this PR, so I've left it as-is rather than adding tests just to move a number that already satisfies the real gate.2. Inconsistent float coercion between config and env-var paths — fixed.
LLMAgent.__init__now validatestool_max_roundswithisinstance(_raw_max_rounds, int)instead ofint(_raw_max_rounds), matching the pattern already used by the sibling fieldpruning_threshold. A float such as2.7is now rejected withConfigurationErroron the config path exactly as it already was on theTOOL_MAX_ROUNDSenv-var path (whereint(str(x))rejects a float string), instead of being silently truncated to2.docs/index.md§4.4 documents the field's type asinteger, so this brings the implementation in line with the existing spec rather than requiring any spec change.3. Missing test coverage for env-var clamping edge cases — fixed, and extended to the config path too.
Added to
features/llm_agent_tool_loop.feature:TOOL_MAX_ROUNDS=0clamped to 1 roundTOOL_MAX_ROUNDS=-5clamped to 1 roundFor symmetry (both the config field and the env var implement the same clamp-to-1 contract per §4.4/D-8), I also added:
tool_max_rounds: -3config value clamped to 1 round (llm_agent_tool_loop.feature)tool_max_rounds: 2.7config value rejected withConfigurationError(llm_agent_tool_calling.feature), covering the isinstance fix from item 2 aboveNits
1.
_raw_max_roundsreused to hold the clamped value — fixed.The clamped result is now stored in its own
_clamped_max_roundsvariable;_raw_max_roundsno longer changes meaning partway through the block.2. Env-var parse-failure error message naming
tool_max_rounds— fixed.The
ConfigurationErrorraised from theTOOL_MAX_ROUNDS-parsing branch in_execute_tool_loop()now says"TOOL_MAX_ROUNDS must be an integer, got ...", naming the actual source of the bad value.Verification
nox -s unit_tests(154 features / 3104 scenarios / 0 failures),lint,typecheck,security_scan, anddead_codeall green. Ran a scopednox -s coverage_report -- features/llm_agent_tool_loop.feature features/llm_agent_tool_calling.featureand confirmed every changed line (the__init__validation block and theTOOL_MAX_ROUNDSresolution block in_execute_tool_loop) is exercised — none appear in slipcover's missing-lines list.No ADR or
docs/index.mdchanges were needed for this round — the field's documented type (integer) didn't change, only the implementation's fidelity to it.bf09e4facc374405682e