fix(agents): refuse empty SOUL.md updates in update_agent (#4219)

* fix(agents): refuse empty SOUL.md updates in update_agent

setup_agent already rejects empty/whitespace soul (#3553). update_agent
is the sibling write path and previously reported success while wiping
a working SOUL.md. Mirror the same guard before staging.

* fix(agents): guide the retry in the empty-SOUL update rejection

Append "Omit the soul field if you do not want to change it." to the
empty-soul error so the model self-corrects in one step instead of
retrying with another null-like value, matching the "No fields provided"
sibling message's helpfulness. Both regression tests assert the guidance.
This commit is contained in:
Aari
2026-07-16 14:44:22 +08:00
committed by GitHub
parent d57f695769
commit 7df44f586c
2 changed files with 41 additions and 0 deletions
@@ -145,6 +145,13 @@ def update_agent(
if soul is None and description is None and skills is None and tool_groups is None and model is None:
return _err('No fields provided. Pass at least one of: soul, description, skills, tool_groups, model. Omit unchanged fields instead of passing null-like strings such as "null", "none", or "undefined".')
# Reject empty / whitespace-only soul before touching the filesystem.
# setup_agent already refuses this (#3553 / #3549); update_agent must too,
# otherwise a custom agent can report success while wiping a working
# SOUL.md and leaving the next turn with an empty personality.
if soul is not None and not soul.strip():
return _err("soul content is empty; refusing to update agent with an empty SOUL.md. Omit the soul field if you do not want to change it.")
try:
agent_name = validate_agent_name(agent_name_raw)
except ValueError as e:
+34
View File
@@ -262,6 +262,40 @@ def test_update_agent_updates_soul_only(tmp_path, patched_paths):
assert "soul" in result.update["messages"][0].content
def test_update_agent_rejects_empty_soul_and_does_not_overwrite(tmp_path, patched_paths):
"""Mirror setup_agent's empty-SOUL guard (#3553 / #3549).
setup_agent refuses empty/whitespace soul before touching the filesystem.
update_agent previously accepted the same input and reported success while
writing a blank SOUL.md, wiping a working agent personality.
"""
agent_dir = _seed_agent(tmp_path, description="keep me", soul="original soul")
result = update_agent.func(runtime=_runtime(), soul="")
msg = result.update["messages"][0]
assert "soul content is empty" in msg.content
# Message must guide the retry (omit the field) so the model self-corrects
# in one step instead of retrying with another empty-ish value.
assert "Omit the soul field" in msg.content
assert msg.status == "error"
assert (agent_dir / "SOUL.md").read_text() == "original soul"
cfg = yaml.safe_load((agent_dir / "config.yaml").read_text())
assert cfg["description"] == "keep me", "config must be untouched on empty-soul reject"
def test_update_agent_rejects_whitespace_only_soul_and_does_not_overwrite(tmp_path, patched_paths):
agent_dir = _seed_agent(tmp_path, description="keep me", soul="original soul")
result = update_agent.func(runtime=_runtime(), soul=" \n\t ")
msg = result.update["messages"][0]
assert "soul content is empty" in msg.content
assert "Omit the soul field" in msg.content
assert msg.status == "error"
assert (agent_dir / "SOUL.md").read_text() == "original soul"
def test_update_agent_updates_description_only(tmp_path, patched_paths):
agent_dir = _seed_agent(tmp_path, description="old desc", soul="keep this soul")