Skip to content

fix: max_steps=0 falsy fallback and planning remaining_steps override#2526

Open
ErenAta16 wants to merge 1 commit into
huggingface:mainfrom
ErenAta16:fix/max-steps-zero-and-planning-override
Open

fix: max_steps=0 falsy fallback and planning remaining_steps override#2526
ErenAta16 wants to merge 1 commit into
huggingface:mainfrom
ErenAta16:fix/max-steps-zero-and-planning-override

Conversation

@ErenAta16

Copy link
Copy Markdown

Summary

Fixes #2458 and #2510, two related bugs in how max_steps propagates through a run.

#2458: max_steps=0 silently ignored

run() does max_steps = max_steps or self.max_steps. Since max_steps: int | None = None and 0 is falsy, run(task, max_steps=0) silently falls back to the agent's constructor default instead of running zero steps.

Fixing this exposed a pre-existing edge case: when the loop body never executes (zero steps), action_step was never assigned, but the max-steps-reached branch unconditionally did yield action_step, raising UnboundLocalError. Initialized action_step = None before the loop and guarded the yield.

#2510: planning remaining_steps placeholder never interpolated, and uses the wrong budget

Two stacked issues on the same value:

  1. The three built-in planning templates (code_agent.yaml, structured_code_agent.yaml, toolcalling_agent.yaml) use {remaining_steps} (single brace). populate_template renders with Jinja2 (Template(..., undefined=StrictUndefined)), which requires {{ }} for interpolation, single braces are literal text. So the model receives the literal string {remaining_steps} instead of a number.
  2. Even fixed, the value was computed from self.max_steps (the constructor default) inside _generate_planning_step, not the effective per-run budget. When run(task, max_steps=N) overrides the default, the execution loop correctly uses N, but planning was still reading the constructor value. Threaded max_steps through _run_stream_generate_planning_step so both agree.

Testing

  • test_run_max_steps_zero_is_respected: run(..., max_steps=0) takes zero action steps and returns cleanly (no UnboundLocalError).
  • test_planning_step_remaining_steps_uses_per_run_max_steps_override: planning text contains the per-run override's remaining-steps number, not the constructor default's, and never contains the literal {remaining_steps} placeholder.
  • Updated test_planning_step's call site for the new _generate_planning_step(..., max_steps=...) signature.

Ran the full tests/test_agents.py suite before and after against unfixed code to isolate what's actually caused by this change: the same ~15 failures are present identically on unfixed main (missing numpy/transformers extras, from_folder version-compat fixtures, one ImportError, one flaky timing assertion, all unrelated to max_steps), confirming this diff doesn't introduce any new failures.

…huggingface#2458, huggingface#2510)

Two related bugs in max_steps handling:

1. run(max_steps=0) silently fell back to the agent constructor default
   via `max_steps = max_steps or self.max_steps` (0 is falsy). max_steps
   is typed `int | None = None`, so 0 is a legitimate, distinct value from
   not-provided, changed to an explicit is-not-None check. Fixing this
   exposed an existing edge case: with zero steps taken, the loop body
   that assigns action_step never runs, and the max-steps-reached branch
   unconditionally yielded it, raising UnboundLocalError. Initialized
   action_step to None before the loop and guarded the yield.

2. Planning updates render the literal text "{remaining_steps}" instead
   of a number, and even once fixed, the value comes from
   self.max_steps (the constructor default) rather than the effective
   per-run budget when run(max_steps=N) overrides it. populate_template
   uses Jinja2 with StrictUndefined, which requires {{ }} for
   interpolation; the templates used single-brace {remaining_steps},
   which Jinja2 never substitutes, so it reached the model as literal
   text. Fixed the three built-in planning templates (code_agent,
   structured_code_agent, toolcalling_agent) to use {{ remaining_steps }},
   and threaded the effective max_steps through
   _run_stream -> _generate_planning_step so remaining_steps is computed
   from the actual per-run budget.

Testing: added test_run_max_steps_zero_is_respected and
test_planning_step_remaining_steps_uses_per_run_max_steps_override,
updated the existing test_planning_step call site for the new
_generate_planning_step signature. Ran the full tests/test_agents.py
suite before and after against the unfixed code to confirm: all
remaining failures (missing numpy/transformers, from_folder version
compat, an ImportError, one flaky timing assertion) are pre-existing
and unrelated, present identically on unfixed main.

Fixes huggingface#2458
Fixes huggingface#2510

Signed-off-by: ErenAta16 <erena6466@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: max_steps=0 silently ignored due to falsy-value or operator

1 participant