mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(config): reject booleans for the subagent and ACP invocation backstops (#6242)
YAML booleans coerce into the ge=1 integer backstops that bound every subagent and ACP invocation: timeout_seconds: true became 1 (the executor's future.result(timeout=...) then killed every subagent run after one second) and max_turns: true became 1 (the LangGraph recursion budget collapsed to a single super-step). The ACP agent timeout mirrors subagents.timeout_seconds and shared the hole. false was already rejected by ge=1, so only true slipped through - silently. Mirror the app-config boolean guard: reject bools before coercion in SubagentOverrideConfig, CustomSubagentConfig and SubagentsAppConfig (timeout_seconds/max_turns/max_total_per_run) and ACPAgentConfig (timeout_seconds), keeping None, integers, and numeric strings as-is. Signed-off-by: yetuge <2219677952@qq.com>
This commit is contained in:
parent
1f7600e355
commit
e59f6e5e92
@ -3,7 +3,9 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, ValidationInfo, field_validator
|
||||
|
||||
from deerflow.config._boolean_guards import reject_boolean
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -36,6 +38,11 @@ class ACPAgentConfig(BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("timeout_seconds", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_timeout(cls, value: object, info: ValidationInfo) -> object:
|
||||
return reject_boolean(value, info, kind="an integer")
|
||||
|
||||
|
||||
_acp_agents: dict[str, ACPAgentConfig] = {}
|
||||
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
|
||||
import logging
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, ValidationInfo, field_validator
|
||||
|
||||
from deerflow.config._boolean_guards import reject_boolean
|
||||
from deerflow.config.prompt_overlay import PromptOverlay
|
||||
from deerflow.config.token_budget_config import TokenBudgetConfig
|
||||
|
||||
@ -114,6 +115,11 @@ class SubagentOverrideConfig(BaseModel):
|
||||
description="Per-run token budget override for this subagent (None = use the global subagents.token_budget default). Symmetric with timeout_seconds/max_turns.",
|
||||
)
|
||||
|
||||
@field_validator("timeout_seconds", "max_turns", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_override_backstops(cls, value: object, info: ValidationInfo) -> object:
|
||||
return reject_boolean(value, info, kind="an integer")
|
||||
|
||||
|
||||
class CustomSubagentConfig(BaseModel):
|
||||
"""User-defined subagent type declared in config.yaml."""
|
||||
@ -151,6 +157,11 @@ class CustomSubagentConfig(BaseModel):
|
||||
description="Maximum execution time in seconds",
|
||||
)
|
||||
|
||||
@field_validator("timeout_seconds", "max_turns", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_custom_backstops(cls, value: object, info: ValidationInfo) -> object:
|
||||
return reject_boolean(value, info, kind="an integer")
|
||||
|
||||
|
||||
class SubagentsAppConfig(BaseModel):
|
||||
"""Configuration for the subagent system."""
|
||||
@ -171,6 +182,12 @@ class SubagentsAppConfig(BaseModel):
|
||||
le=MAX_TOTAL_SUBAGENTS_PER_RUN,
|
||||
description="Default total number of subagent delegations allowed in one lead-agent run. This is a deterministic backstop against repeated legal-sized task batches. Valid range: 1-50.",
|
||||
)
|
||||
|
||||
@field_validator("timeout_seconds", "max_turns", "max_total_per_run", mode="before")
|
||||
@classmethod
|
||||
def _reject_boolean_global_backstops(cls, value: object, info: ValidationInfo) -> object:
|
||||
return reject_boolean(value, info, kind="an integer")
|
||||
|
||||
token_budget: TokenBudgetConfig = Field(
|
||||
default_factory=default_subagent_token_budget,
|
||||
description="Default per-run token budget for subagents — a cost-ceiling backstop that engages by default (#3875 Phase 2). Set enabled: false to disable, or override per agent via agents.<name>.token_budget.",
|
||||
|
||||
73
backend/tests/test_subagents_acp_boolean_backstops.py
Normal file
73
backend/tests/test_subagents_acp_boolean_backstops.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""YAML booleans must not coerce into the subagent and ACP invocation backstops.
|
||||
|
||||
`config.yaml` legitimately mixes booleans (`enabled: true`) with integer knobs on
|
||||
neighbouring lines, so a typo like `subagents.timeout_seconds: true` loads cleanly —
|
||||
and Pydantic coerces it to `1`. The subagent executor bounds every run with
|
||||
`future.result(timeout=self.config.timeout_seconds)`, so 1800 seconds becomes a
|
||||
1-second kill, and `resolve_recursion_limit(self.config.max_turns)` turns a
|
||||
`max_turns: true` typo into a LangGraph budget of a single super-step. The ACP
|
||||
agent timeout mirrors `subagents.timeout_seconds` and has the same hole. Mirrors
|
||||
the app-config, loop-detection, and tool-output boolean guards already in the repo.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from deerflow.config.acp_config import ACPAgentConfig
|
||||
from deerflow.config.subagents_config import CustomSubagentConfig, SubagentOverrideConfig, SubagentsAppConfig
|
||||
|
||||
BACKSTOP_FIELDS = ("timeout_seconds", "max_turns")
|
||||
GLOBAL_INT_FIELDS = ("timeout_seconds", "max_turns", "max_total_per_run")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
@pytest.mark.parametrize("field", BACKSTOP_FIELDS)
|
||||
def test_override_backstops_reject_booleans(field: str, value: bool) -> None:
|
||||
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
||||
SubagentOverrideConfig(**{field: value})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
@pytest.mark.parametrize("field", BACKSTOP_FIELDS)
|
||||
def test_custom_backstops_reject_booleans(field: str, value: bool) -> None:
|
||||
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
||||
CustomSubagentConfig(description="d", system_prompt="s", **{field: value})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
@pytest.mark.parametrize("field", GLOBAL_INT_FIELDS)
|
||||
def test_global_backstops_reject_booleans(field: str, value: bool) -> None:
|
||||
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
||||
SubagentsAppConfig(**{field: value})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", BACKSTOP_FIELDS)
|
||||
def test_override_backstops_keep_none_and_numeric_inputs(field: str) -> None:
|
||||
config = SubagentOverrideConfig(**{field: None})
|
||||
assert getattr(config, field) is None
|
||||
config = SubagentOverrideConfig(**{field: "30"})
|
||||
assert getattr(config, field) == 30
|
||||
|
||||
|
||||
def test_custom_backstops_keep_numeric_inputs() -> None:
|
||||
config = CustomSubagentConfig(description="d", system_prompt="s", max_turns="25", timeout_seconds=600)
|
||||
assert config.max_turns == 25
|
||||
assert config.timeout_seconds == 600
|
||||
|
||||
|
||||
def test_global_backstops_keep_numeric_inputs() -> None:
|
||||
config = SubagentsAppConfig(timeout_seconds="900", max_turns="40", max_total_per_run=8)
|
||||
assert config.timeout_seconds == 900
|
||||
assert config.max_turns == 40
|
||||
assert config.max_total_per_run == 8
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
def test_acp_timeout_rejects_booleans(value: bool) -> None:
|
||||
with pytest.raises(ValidationError, match="must be an integer, not a boolean"):
|
||||
ACPAgentConfig(command="cmd", description="d", timeout_seconds=value)
|
||||
|
||||
|
||||
def test_acp_timeout_keeps_numeric_input() -> None:
|
||||
config = ACPAgentConfig(command="cmd", description="d", timeout_seconds="600")
|
||||
assert config.timeout_seconds == 600
|
||||
Loading…
x
Reference in New Issue
Block a user