fix(agents): sync agent_name across context/configurable and reject empty soul (#3549) (#3553)

* fix(agents): sync agent_name across context/configurable and reject empty soul (#3549)

Two independent issues caused custom agent creation to silently fail:

1. build_run_config only wrote agent_name into one container (configurable
   or context), so setup_agent — which reads ToolRuntime.context exclusively
   since LangGraph >=1.1.9 — saw agent_name=None and wrote SOUL.md to the
   global base_dir instead of users/{user_id}/agents/{name}/. Mirror the
   dual-write pattern already used by merge_run_context_overrides and
   naming.py so both containers always carry the same value.

2. setup_agent persisted whatever soul string it received, including empty
   or whitespace-only content, and still reported success. The frontend
   then surfaced an unusable agent and the global default SOUL.md could be
   silently overwritten with empty content. Reject empty soul before any
   filesystem operation so the model can retry.

Tests:
- test_gateway_services.py: dual-write regressions for both configurable
  and context entry paths, explicit-agent-name precedence on both sides,
  and a shape-parity test against merge_run_context_overrides.
- test_setup_agent_tool.py: empty/whitespace soul rejection, plus
  no-overwrite guarantees for existing global and per-agent SOUL.md.

* Update services.py
This commit is contained in:
Huixin615
2026-06-14 10:40:16 +08:00
committed by GitHub
parent 47e9570d86
commit f43aa78107
4 changed files with 158 additions and 19 deletions
@@ -28,6 +28,25 @@ def setup_agent(
skills: Optional list of skill names this agent should use. None means use all enabled skills, empty list means no skills.
"""
# Reject empty / whitespace-only soul before touching the filesystem.
# Without this guard the tool would happily persist an empty SOUL.md and
# still report success, which caused the frontend to enter the "agent
# created" state for an unusable agent (issue #3549). Failing loud lets
# the model retry instead of silently producing a broken artifact and,
# together with the upstream agent_name fix, prevents the global default
# SOUL.md from being overwritten with empty content.
if not soul or not soul.strip():
return Command(
update={
"messages": [
ToolMessage(
content="Error: soul content is empty; refusing to create agent with an empty SOUL.md",
tool_call_id=runtime.tool_call_id,
)
]
}
)
agent_name: str | None = runtime.context.get("agent_name") if runtime.context else None
agent_dir = None
is_new_dir = False