deer-flow/backend/packages/harness/deerflow/config/loop_detection_config.py
yetuge 0eed90df3a
refactor(config): converge the boolean guards into one shared helper (#6226)
* refactor(config): converge the boolean guards into one shared helper

Willem-bd's #6171 review follow-up: a tiny shared helper lets the next numeric field get the guard in one line and unifies the messages. Consolidates 14 validator copies across 9 config files into deerflow/config/_boolean_guards.reject_boolean(value, info, kind=...); the bare per-file messages gain their field name prefix, command_timeout keeps its distinct wording.

Signed-off-by: yetuge <2219677952@qq.com>

* refactor(config): converge stream_bridge_config and pin the guard wording

Review follow-ups on #6226: convert the last surviving per-file copy (stream_bridge_config) so deerflow/config is fully converged, make kind required keyword-only so a float field cannot silently inherit integer wording, and pin the unified field-prefixed messages with direct tests.

Signed-off-by: yetuge <2219677952@qq.com>

* test(config): cast the boolean-guard test probe to ValidationInfo

The _info probe was annotated -> object, so a type checker flags every call
site that passes its result to reject_boolean's info: ValidationInfo
parameter (runtime was fine since the helper only reads .field_name).
Return cast(ValidationInfo, _Probe(field_name)) to keep the probe honest
for both.

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
2026-10-04 09:09:15 +08:00

94 lines
3.3 KiB
Python

"""Configuration for loop detection middleware."""
from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator
from deerflow.config._boolean_guards import reject_boolean
class ToolFreqOverride(BaseModel):
"""Per-tool frequency threshold override.
Can be higher or lower than the global defaults. Commonly used to raise
thresholds for high-frequency tools like bash in batch workflows (e.g.
RNA-seq pipelines) without weakening protection on every other tool.
"""
warn: int = Field(ge=1)
hard_limit: int = Field(ge=1)
@field_validator("warn", "hard_limit", mode="before")
@classmethod
def reject_boolean_thresholds(cls, value: object, info: ValidationInfo) -> object:
return reject_boolean(value, info, kind="an integer")
@model_validator(mode="after")
def _validate(self) -> "ToolFreqOverride":
if self.hard_limit < self.warn:
raise ValueError("hard_limit must be >= warn")
return self
class LoopDetectionConfig(BaseModel):
"""Configuration for repetitive tool-call loop detection."""
enabled: bool = Field(
default=True,
description="Whether to enable repetitive tool-call loop detection",
)
warn_threshold: int = Field(
default=3,
ge=1,
description="Number of identical tool-call sets before injecting a warning",
)
hard_limit: int = Field(
default=5,
ge=1,
description="Number of identical tool-call sets before forcing a stop",
)
window_size: int = Field(
default=20,
ge=1,
description="Number of recent tool-call sets to track per thread",
)
max_tracked_threads: int = Field(
default=100,
ge=1,
description="Maximum number of thread/run histories to keep in memory (legacy field name)",
)
tool_freq_warn: int = Field(
default=30,
ge=1,
description="Number of calls to the same tool type before injecting a frequency warning",
)
tool_freq_hard_limit: int = Field(
default=50,
ge=1,
description="Number of calls to the same tool type before forcing a stop",
)
tool_freq_overrides: dict[str, ToolFreqOverride] = Field(
default_factory=dict,
description=("Per-tool overrides for tool_freq_warn / tool_freq_hard_limit, keyed by tool name. Values can be higher or lower than the global defaults. Commonly used to raise thresholds for high-frequency tools like bash."),
)
@field_validator(
"warn_threshold",
"hard_limit",
"window_size",
"max_tracked_threads",
"tool_freq_warn",
"tool_freq_hard_limit",
mode="before",
)
@classmethod
def reject_boolean_thresholds(cls, value: object, info: ValidationInfo) -> object:
return reject_boolean(value, info, kind="an integer")
@model_validator(mode="after")
def validate_thresholds(self) -> "LoopDetectionConfig":
"""Ensure hard stop cannot happen before the warning threshold."""
if self.hard_limit < self.warn_threshold:
raise ValueError("hard_limit must be greater than or equal to warn_threshold")
if self.tool_freq_hard_limit < self.tool_freq_warn:
raise ValueError("tool_freq_hard_limit must be greater than or equal to tool_freq_warn")
return self