diff --git a/CHANGELOG.md b/CHANGELOG.md index 4713c0ce3c..e3b8160a7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,13 @@ This section accumulates work toward the **2.3.0** milestone ### Fixed +- **agents:** `PUT /api/agents/{name}` with `"description": null` now resets the + description to empty. It used to store the null, which the agent config + rejects, so the agent then failed every load: the update, GET and repair + updates returned 500, the file backend dropped it from the agent list, the + database backend failed the owner's whole list, and chats with it failed. + Agents already stored with a null description load again. ([#7638]) + - **sandbox:** Cancelled native Runs now stop their owned AIO and POSIX local bash processes, including ordinary `nohup` children, before releasing the execution lease. Concurrent work and background services from successful @@ -15965,6 +15972,7 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#7643]: https://github.com/bytedance/deer-flow/pull/7643 [#7651]: https://github.com/bytedance/deer-flow/pull/7651 [#7659]: https://github.com/bytedance/deer-flow/pull/7659 +[#7638]: https://github.com/bytedance/deer-flow/pull/7638 [#7665]: https://github.com/bytedance/deer-flow/pull/7665 [#7723]: https://github.com/bytedance/deer-flow/pull/7723 [#7727]: https://github.com/bytedance/deer-flow/pull/7727 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index da80474892..8f22cf3393 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -10,7 +10,7 @@ ## [未发布] 本节累积面向 **2.3.0** 里程碑([2.3.0](https://github.com/bytedance/deer-flow/milestone/4))的工作。 -截至 2026-10-28,该里程碑已合并 **1068 个拉取请求**。 + ### 新增 @@ -20,6 +20,11 @@ ### 修复 +- **智能体:** `PUT /api/agents/{name}` 传入 `"description": null` 时现在会把描述重置为 + 空。此前会把 null 原样保存,而智能体配置不接受该值,导致之后每次加载都失败:更新本身、 + GET 和修复性更新都返回 500,文件后端会把它从智能体列表中漏掉,数据库后端会让该用户的 + 整个列表失败,与它的对话也会失败。已以 null 描述保存的智能体现在可以重新加载。([#7638]) + - **智能体:** 当模型提供方在不同轮次复用同一个工具调用 ID(例如每次响应都是 `call_0`)时, 澄清提问不再覆盖之前的问题。此前其消息 ID(同时也是人工输入的 `request_id`)只包含调用 ID, 后一个问题会原地替换前一个问题,后一次调用也因此缺少结果。现在 ID 还会包含发起该调用的 @@ -12719,6 +12724,7 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#7643]: https://github.com/bytedance/deer-flow/pull/7643 [#7651]: https://github.com/bytedance/deer-flow/pull/7651 [#7659]: https://github.com/bytedance/deer-flow/pull/7659 +[#7638]: https://github.com/bytedance/deer-flow/pull/7638 [#7665]: https://github.com/bytedance/deer-flow/pull/7665 [#7723]: https://github.com/bytedance/deer-flow/pull/7723 [#7727]: https://github.com/bytedance/deer-flow/pull/7727 diff --git a/backend/app/gateway/routers/agents.py b/backend/app/gateway/routers/agents.py index e081139d6f..5fd5e84b7f 100644 --- a/backend/app/gateway/routers/agents.py +++ b/backend/app/gateway/routers/agents.py @@ -96,7 +96,7 @@ class AgentUpdateRequest(BaseModel): """Request body for updating a custom agent.""" display_name: AgentDisplayName | None = Field(default=None, description="Updated display name; null clears it") - description: str | None = Field(default=None, description="Updated description") + description: str | None = Field(default=None, description="Updated description; null resets it to empty") model: str | None = Field(default=None, description="Updated model override") tool_groups: list[str] | None = Field(default=None, description="Updated tool group whitelist") mcp_plugins: list[str] | None = Field(default=None, description="MCP installation selection (None=all, []=none)") @@ -601,7 +601,9 @@ async def update_agent(name: str, body: AgentUpdateRequest, request: Request) -> if config_changed: updated = { "name": agent_cfg.name, - "description": body.description if "description" in fields_set else agent_cfg.description, + # Explicit null resets to the AgentConfig default; storing None + # would make every later load of this agent fail validation. + "description": (body.description or "") if "description" in fields_set else agent_cfg.description, } if "display_name" in fields_set: updated["display_name"] = body.display_name or None diff --git a/backend/packages/harness/deerflow/config/AGENTS.md b/backend/packages/harness/deerflow/config/AGENTS.md index 219cb9e5f1..ab72957d44 100644 --- a/backend/packages/harness/deerflow/config/AGENTS.md +++ b/backend/packages/harness/deerflow/config/AGENTS.md @@ -30,7 +30,9 @@ explicitly carries forward the existing owner's display name when re-bootstrappi the Gateway explicitly overrides it when supplied, including null to clear. Both stores use `parse_agent_config` to ignore only an invalid stored `display_name` on read, logging the agent identifier without the invalid value -and without rewriting storage. Other config errors still +and without rewriting storage. It also reads a stored null `description` +(a bare YAML key, or what `PUT /api/agents/{name}` used to write) as `""`; that +route now stores an explicit null as `""`. Other config errors still raise, and API create/update validation remains strict. **Main Configuration** (`config.yaml`): diff --git a/backend/packages/harness/deerflow/persistence/agents/base.py b/backend/packages/harness/deerflow/persistence/agents/base.py index 99ba826b4c..e11d8b0cd2 100644 --- a/backend/packages/harness/deerflow/persistence/agents/base.py +++ b/backend/packages/harness/deerflow/persistence/agents/base.py @@ -49,6 +49,10 @@ def parse_agent_config(data: dict[str, Any], name: str) -> AgentConfig: data["name"] = name known_fields = set(AgentConfig.model_fields.keys()) data = {k: v for k, v in data.items() if k in known_fields} + # A bare ``description:`` in YAML, or a null the agents API used to store, + # means "no description"; read it as the default instead of failing. + if "description" in data and data["description"] is None: + data["description"] = "" try: return AgentConfig(**data) except ValidationError as exc: diff --git a/backend/tests/test_agent_store_sql.py b/backend/tests/test_agent_store_sql.py index 309f73dff0..051e48c5de 100644 --- a/backend/tests/test_agent_store_sql.py +++ b/backend/tests/test_agent_store_sql.py @@ -54,6 +54,16 @@ def test_display_name_is_config_data_not_storage_identity(store): assert store.get_soul("reviewer", user_id="u1") == "Soul" +def test_stored_null_description_reads_as_empty(store): + """One null description must not 500 the owner's whole agent list.""" + store.create("broken", {"name": "broken", "description": None}, "Soul", user_id="u1") + store.create("healthy", {"name": "healthy", "description": "fine"}, "Soul", user_id="u1") + + assert store.get("broken", user_id="u1").description == "" + assert {cfg.name: cfg.description for cfg in store.list(user_id="u1")} == {"broken": "", "healthy": "fine"} + assert {cfg.name: cfg.description for _, cfg in store.list_all()} == {"broken": "", "healthy": "fine"} + + def test_name_is_stored_lowercase_and_excluded_from_document(store): store.create("Mixed", {"name": "Mixed", "description": "d"}, "s", user_id="u1") # Stored lowercase (matches the on-disk layout), and the JSON document does diff --git a/backend/tests/test_custom_agent.py b/backend/tests/test_custom_agent.py index eeba2e83bf..e4593271fc 100644 --- a/backend/tests/test_custom_agent.py +++ b/backend/tests/test_custom_agent.py @@ -918,6 +918,40 @@ class TestAgentsAPI: assert response.status_code == 200 assert response.json()["description"] == "new desc" + def test_update_agent_description_null_resets_to_empty(self, agent_client): + """An explicit null description stores the default, not a null the loader rejects. + + ``AgentUpdateRequest.description`` accepts null but ``AgentConfig.description`` + is a ``str``: the route used to persist ``description: null`` and then fail + to read it back, so the PUT, every later GET and repair PUT returned 500. + """ + agent_client.post("/api/agents", json={"name": "desc-agent", "description": "old desc", "soul": "p"}) + + response = agent_client.put("/api/agents/desc-agent", json={"description": None}) + assert response.status_code == 200, response.text + assert response.json()["description"] == "" + + tmp_path: Path = agent_client._tmp_path # type: ignore[attr-defined] + stored = yaml.safe_load((tmp_path / "users" / "test-user-autouse" / "agents" / "desc-agent" / "config.yaml").read_text(encoding="utf-8")) + assert stored["description"] == "" + assert agent_client.get("/api/agents/desc-agent").json()["description"] == "" + assert [agent["name"] for agent in agent_client.get("/api/agents").json()["agents"]] == ["desc-agent"] + + def test_stored_null_description_loads_as_empty_and_can_be_repaired(self, agent_client): + """A null already in storage (this bug, or a bare ``description:`` in YAML) reads as empty.""" + from deerflow.persistence.agents.file import FileAgentStore + + FileAgentStore().create("reviewer", {"description": None}, "Soul") + + response = agent_client.get("/api/agents/reviewer") + assert response.status_code == 200, response.text + assert response.json()["description"] == "" + assert [agent["name"] for agent in agent_client.get("/api/agents").json()["agents"]] == ["reviewer"] + response = agent_client.put("/api/agents/reviewer", json={"description": "repaired"}) + assert response.status_code == 200, response.text + assert response.json()["description"] == "repaired" + assert response.json()["soul"] == "Soul" + def test_update_agent_preserves_hand_authored_non_managed_fields(self, agent_client): """Hand-authored ``github:`` and ``memory_enabled`` fields must survive PATCH.