From 63e399f2bdf6ad1269724c10cfc98bb40c7550d3 Mon Sep 17 00:00:00 2001 From: yc2bgr8 <119500159+yc2bgr8@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:30:10 +0800 Subject: [PATCH] fix(config): reject boolean loop detection thresholds (#6017) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(config): reject boolean loop detection thresholds * refactor(config): 复用循环阈值布尔校验 * Revise CHANGELOG for configuration and upload updates Updated CHANGELOG to reflect changes in configuration loading and document ownership. * Update the document of ChangeLog 修复了配置加载阶段的整数阈值字段报错行为,并更新了文档转换的归属记录处理。 --------- Co-authored-by: Willem Jiang --- CHANGELOG.md | 8 ++++ CHANGELOG_zh.md | 6 +++ .../deerflow/config/loop_detection_config.py | 26 ++++++++++++- backend/tests/test_loop_detection_config.py | 39 +++++++++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f82a521f9c..f1c286869f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -460,6 +460,14 @@ This release closes that milestone with **301 merged pull requests**. ### Fixed +- **agents:** Loop-detection integer thresholds now reject YAML booleans instead + of coercing `true` to `1`. A configuration such as `warn_threshold: true` + and `hard_limit: true` previously made the first tool-call set meet the hard + limit and forced the agent to stop; booleans on the tracking-window, + per-tool-frequency, and per-tool override fields similarly collapsed their + limits to one. All integer threshold fields now fail configuration loading + with a field-specific error while valid integers and numeric strings retain + their existing behavior.([#6017]) - **uploads:** Converted Markdown ownership is now recorded when a document is converted. `list_uploaded_files` hides only verified conversion outputs, and document outlines use only the recorded companion; a user-uploaded Markdown diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index acc22cabb2..a28aa6470c 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -419,6 +419,12 @@ ### 修复 +- **智能体:** 循环检测的整数阈值现在会拒绝 YAML 布尔值,而不是把 + `true` 静默转换为 `1`。此前若配置 `warn_threshold: true` 和 + `hard_limit: true`,第一组工具调用就会达到硬上限并强制终止智能体; + 跟踪窗口、工具频率和按工具覆盖项中的布尔值也会把对应限制缩小为 + 1。现在所有整数阈值字段都会在配置加载阶段按字段名报错,同时保持 + 有效整数和数字字符串的既有行为。([#6017]) - **上传:** 文档转换时现在会记录原文件与 Markdown 的归属关系。 `list_uploaded_files` 只隐藏归属已验证的转换文件,文档大纲也只读取 记录中指定的 Markdown;用户自行上传的同名文件会正常显示,不会被 diff --git a/backend/packages/harness/deerflow/config/loop_detection_config.py b/backend/packages/harness/deerflow/config/loop_detection_config.py index 684e9649ba..6ecb4a21fe 100644 --- a/backend/packages/harness/deerflow/config/loop_detection_config.py +++ b/backend/packages/harness/deerflow/config/loop_detection_config.py @@ -1,6 +1,12 @@ """Configuration for loop detection middleware.""" -from pydantic import BaseModel, Field, model_validator +from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator + + +def _reject_boolean_threshold(value: object, info: ValidationInfo) -> object: + if isinstance(value, bool): + raise ValueError(f"{info.field_name} must be an integer, not a boolean") + return value class ToolFreqOverride(BaseModel): @@ -14,6 +20,11 @@ class ToolFreqOverride(BaseModel): 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_threshold(value, info) + @model_validator(mode="after") def _validate(self) -> "ToolFreqOverride": if self.hard_limit < self.warn: @@ -63,6 +74,19 @@ class LoopDetectionConfig(BaseModel): 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_threshold(value, info) + @model_validator(mode="after") def validate_thresholds(self) -> "LoopDetectionConfig": """Ensure hard stop cannot happen before the warning threshold.""" diff --git a/backend/tests/test_loop_detection_config.py b/backend/tests/test_loop_detection_config.py index 93bc6ac3e0..2e21167188 100644 --- a/backend/tests/test_loop_detection_config.py +++ b/backend/tests/test_loop_detection_config.py @@ -36,6 +36,28 @@ class TestLoopDetectionConfig: assert config.tool_freq_warn == 60 assert config.tool_freq_hard_limit == 80 + def test_accepts_numeric_string_thresholds(self): + config = LoopDetectionConfig.model_validate( + { + "warn_threshold": "10", + "hard_limit": "20", + "window_size": "50", + "max_tracked_threads": "200", + "tool_freq_warn": "60", + "tool_freq_hard_limit": "80", + "tool_freq_overrides": {"bash": {"warn": "150", "hard_limit": "300"}}, + }, + ) + + assert config.warn_threshold == 10 + assert config.hard_limit == 20 + assert config.window_size == 50 + assert config.max_tracked_threads == 200 + assert config.tool_freq_warn == 60 + assert config.tool_freq_hard_limit == 80 + assert config.tool_freq_overrides["bash"].warn == 150 + assert config.tool_freq_overrides["bash"].hard_limit == 300 + def test_rejects_zero_thresholds(self): with pytest.raises(ValueError): LoopDetectionConfig(warn_threshold=0) @@ -49,6 +71,23 @@ class TestLoopDetectionConfig: with pytest.raises(ValueError): LoopDetectionConfig(tool_freq_hard_limit=0) + @pytest.mark.parametrize( + ("payload", "field"), + [ + ({"warn_threshold": True}, "warn_threshold"), + ({"warn_threshold": 1, "hard_limit": True}, "hard_limit"), + ({"window_size": True}, "window_size"), + ({"max_tracked_threads": True}, "max_tracked_threads"), + ({"tool_freq_warn": True}, "tool_freq_warn"), + ({"tool_freq_warn": 1, "tool_freq_hard_limit": True}, "tool_freq_hard_limit"), + ({"tool_freq_overrides": {"bash": {"warn": True, "hard_limit": 2}}}, "warn"), + ({"tool_freq_overrides": {"bash": {"warn": 1, "hard_limit": True}}}, "hard_limit"), + ], + ) + def test_rejects_boolean_integer_thresholds(self, payload, field): + with pytest.raises(ValueError, match=field): + LoopDetectionConfig(**payload) + def test_rejects_hard_limit_below_warn_threshold(self): with pytest.raises(ValueError, match="hard_limit"): LoopDetectionConfig(warn_threshold=5, hard_limit=4)