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
+21 -14
View File
@@ -211,11 +211,14 @@ def build_run_config(
When *assistant_id* refers to a custom agent (anything other than
``"lead_agent"`` / ``None``), the name is forwarded as ``agent_name`` in
whichever runtime options container is active: ``context`` for
LangGraph >= 0.6.0 requests, otherwise ``configurable``.
``make_lead_agent`` reads this key to load the matching
``agents/<name>/SOUL.md`` and per-agent config — without it the agent
silently runs as the default lead agent.
both ``configurable`` and ``context`` so it is visible to legacy
configurable readers and to LangGraph ``ToolRuntime.context`` consumers
(e.g. the ``setup_agent`` tool, which since LangGraph >=1.1.9 no longer
falls back from ``context`` to ``configurable``). An explicit
``agent_name`` in either container takes precedence over the value
derived from ``assistant_id``. ``make_lead_agent`` reads this key to
load the matching ``agents/<name>/SOUL.md`` and per-agent config —
without it the agent silently runs as the default lead agent.
This mirrors the channel manager's ``_resolve_run_params`` logic so that
the LangGraph Platform-compatible HTTP API and the IM channel path behave
@@ -253,19 +256,23 @@ def build_run_config(
config["configurable"] = {"thread_id": thread_id}
# Inject custom agent name when the caller specified a non-default assistant.
# Honour an explicit agent_name in the active runtime options container.
# Honour an explicit agent_name in either runtime options container.
if assistant_id and assistant_id != _DEFAULT_ASSISTANT_ID:
normalized = assistant_id.strip().lower().replace("_", "-")
if not normalized or not re.fullmatch(r"[a-z0-9-]+", normalized):
raise ValueError(f"Invalid assistant_id {assistant_id!r}: must contain only letters, digits, and hyphens after normalization.")
if "configurable" in config:
target = config["configurable"]
elif "context" in config:
target = config["context"]
else:
target = config.setdefault("configurable", {})
if target is not None and "agent_name" not in target:
target["agent_name"] = normalized
configurable = config.setdefault("configurable", {})
runtime_context = config.setdefault("context", {})
explicit_agent_name: str | None = None
if isinstance(configurable, dict) and isinstance(configurable.get("agent_name"), str):
explicit_agent_name = configurable["agent_name"]
elif isinstance(runtime_context, dict) and isinstance(runtime_context.get("agent_name"), str):
explicit_agent_name = runtime_context["agent_name"]
effective_agent_name = explicit_agent_name or normalized
if isinstance(configurable, dict):
configurable["agent_name"] = effective_agent_name
if isinstance(runtime_context, dict):
runtime_context["agent_name"] = effective_agent_name
config.setdefault("run_name", resolve_root_run_name(config, normalized))
if metadata:
config.setdefault("metadata", {}).update(metadata)