deepset-ai/haystack · error

`min_keep_steps` must be at least 0, got {min_keep_steps}.

Error message

`min_keep_steps` must be at least 0, got {min_keep_steps}.

What it means

SlidingWindowCompactor validates min_keep_steps in __init__ (haystack/hooks/compaction/sliding_window.py:239) and rejects negative values with ValueError. min_keep_steps controls how many recent conversation steps are always retained; a negative count is meaningless.

Source

Thrown at haystack/hooks/compaction/sliding_window.py:239

        tools=[web_search],
        hooks={"before_llm": [hook]},
    )
    ```
    """

    def __init__(self, *, min_keep_steps: int = 1, omission_note: str | None = _DEFAULT_OMISSION_NOTE) -> None:
        """
        Initialize the compactor.

        :param min_keep_steps: The fewest complete recent Agent steps to keep even when they exceed the target. A step
            is an assistant message and all immediately following tool results. `0` allows all completed steps to be
            removed when none fit.
        :param omission_note: The user message left in place of what was removed, or None to remove the messages
            silently. Include `{num_removed}` to have the number of removed messages substituted in.
        :raises ValueError: If `min_keep_steps` is negative.
        """
        if min_keep_steps < 0:
            raise ValueError(f"`min_keep_steps` must be at least 0, got {min_keep_steps}.")
        self.min_keep_steps = min_keep_steps
        self.omission_note = omission_note

    def compact(
        self, messages: list[ChatMessage], target_tokens: int, token_counter: TokenCounter
    ) -> list[ChatMessage] | None:
        """
        Drop older history while preserving the task anchor and a complete recent conversation window.

        :param messages: The conversation to compact, oldest to newest.
        :param target_tokens: The size the kept conversation should come in under.
        :param token_counter: The `TokenCounter` to measure messages with.
        :returns: The conversation that survived, with an omission note if configured standing where the removed
            messages used to sit; or None when there is nothing to remove but an earlier note.
        """
        if token_counter.count(messages=messages) <= target_tokens:
            return None
        kept, note_index, removable = _task_and_step_split(

View on GitHub (pinned to e318778c9b)

Solutions

  1. Pass min_keep_steps >= 0, e.g. min_keep_steps=2.
  2. Clamp before construction: min_keep_steps = max(0, configured_value).
  3. Fix the config source if it supplies negative values.

Example fix

// before
compactor = SlidingWindowCompactor(min_keep_steps=-1)
// after
compactor = SlidingWindowCompactor(min_keep_steps=max(0, configured_keep))
Defensive patterns

Strategy: validation

Validate before calling

def validate_min_keep_steps(v):
    if not isinstance(v, int) or v < 0:
        raise ValueError(f"min_keep_steps must be >= 0, got {v!r}")
validate_min_keep_steps(cfg["min_keep_steps"])

Type guard

def is_non_negative_int(v) -> bool:
    return isinstance(v, int) and not isinstance(v, bool) and v >= 0

Try / catch

try:
    compactor = SlidingWindowCompactor(min_keep_steps=n)
except ValueError as e:
    logger.error("bad min_keep_steps: %s", e)
    compactor = SlidingWindowCompactor(min_keep_steps=0)

Prevention

When it happens

Trigger: Constructing SlidingWindowCompactor with min_keep_steps < 0, e.g. min_keep_steps=-1, typically from a subtraction or a bad config value.

Common situations: Computing min_keep_steps as (desired - something) that goes negative; loading a negative value from YAML/JSON config; off-by-one in defaults.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/f8eb4de3ffe52025. Report an issue: GitHub.