fix(agents): harden update_agent null-like args (#3237)

* fix(agents): harden update_agent null-like args

* docs: mention undefined null-like update args

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Eilen Shin
2026-06-04 07:10:59 +08:00
committed by GitHub
parent 3fddc24c5f
commit 28b1da2172
4 changed files with 101 additions and 9 deletions
+73
View File
@@ -15,6 +15,7 @@ from unittest.mock import MagicMock, patch
import pytest
import yaml
from langchain.tools import ToolRuntime
from deerflow.config.agents_config import AgentConfig
from deerflow.tools.builtins.update_agent_tool import update_agent
@@ -31,6 +32,18 @@ def _runtime(agent_name: str | None = "test-agent", tool_call_id: str = "call_1"
return _DummyRuntime(context={"agent_name": agent_name} if agent_name is not None else {}, tool_call_id=tool_call_id)
def _tool_runtime(agent_name: str | None = "test-agent", tool_call_id: str = "call_1") -> ToolRuntime:
return ToolRuntime(
state={"sandbox": {"sandbox_id": "local"}, "thread_data": {}},
context={"agent_name": agent_name} if agent_name is not None else {},
config={"configurable": {"thread_id": "thread-1"}},
stream_writer=lambda _: None,
tools=[],
tool_call_id=tool_call_id,
store=None,
)
def _make_paths_mock(tmp_path: Path) -> MagicMock:
paths = MagicMock()
paths.base_dir = tmp_path
@@ -115,6 +128,7 @@ def test_update_agent_requires_at_least_one_field(tmp_path, patched_paths):
msg = result.update["messages"][0]
assert "No fields provided" in msg.content
assert msg.status == "error"
def test_update_agent_rejects_unknown_model(tmp_path, patched_paths, stub_app_config):
@@ -141,6 +155,65 @@ def test_update_agent_accepts_known_model(tmp_path, patched_paths, stub_app_conf
assert "model" in result.update["messages"][0].content
def test_update_agent_treats_nullish_optional_text_as_omitted(tmp_path, patched_paths):
"""Models sometimes pass literal "null" strings while trying to omit fields.
Treat those as omitted for optional text fields so they do not get persisted
as a model name or SOUL.md content and feed repeated update_agent retries.
"""
agent_dir = _seed_agent(tmp_path, description="old desc", soul="old soul")
result = update_agent.invoke(
{
"runtime": _tool_runtime(),
"soul": "null",
"description": "none",
"model": "undefined",
}
)
msg = result.update["messages"][0]
assert "No fields provided" in msg.content
assert msg.status == "error"
cfg = yaml.safe_load((agent_dir / "config.yaml").read_text())
assert cfg["description"] == "old desc"
assert "model" not in cfg
assert (agent_dir / "SOUL.md").read_text() == "old soul"
def test_update_agent_rejects_string_list_fields(tmp_path, patched_paths):
"""skills/tool_groups must be real arrays; string placeholders are invalid."""
agent_dir = _seed_agent(tmp_path, skills=["existing"])
assert update_agent.args_schema is not None
with pytest.raises(ValueError, match="skills"):
update_agent.args_schema.model_validate({"skills": "alpha,beta"})
cfg = yaml.safe_load((agent_dir / "config.yaml").read_text())
assert cfg["skills"] == ["existing"]
def test_update_agent_treats_nullish_string_list_fields_as_omitted(tmp_path, patched_paths):
agent_dir = _seed_agent(tmp_path, skills=["existing"])
result = update_agent.invoke(
{
"runtime": _tool_runtime(),
"skills": "null",
"tool_groups": "none",
}
)
msg = result.update["messages"][0]
assert "No fields provided" in msg.content
assert msg.status == "error"
cfg = yaml.safe_load((agent_dir / "config.yaml").read_text())
assert cfg["skills"] == ["existing"]
assert "tool_groups" not in cfg
# --- Partial update tests ---