fix(agents): propagate agent_name into ToolRuntime.context for setup_agent (#2679)
* fix(agents): propagate agent_name into ToolRuntime.context for setup_agent (#2677) When creating a custom agent via the web UI, SOUL.md was always written to the global base_dir/SOUL.md instead of agents/<name>/SOUL.md. Root cause: the bootstrap flow sends agent_name via body.context, but two layers were broken: 1. services.py only forwarded body.context keys into config["configurable"]; config["context"] was never populated. 2. worker.py constructed the parent Runtime with a hard-coded {thread_id, run_id} context, ignoring config["context"] entirely. After the langgraph >= 1.1.9 bump (#98a5b34f), ToolRuntime.context no longer falls back to configurable, so setup_agent's runtime.context.get("agent_name") returned None and the tool's silent agent_name=None -> base_dir fallback kicked in, overwriting the global SOUL.md. Fix: - services.py: extract merge_run_context_overrides() and write the whitelisted context keys into both configurable (legacy readers) and context (langgraph 1.1+ ToolRuntime consumers). - worker.py: extract _build_runtime_context() and merge config["context"] into the Runtime's context (without letting callers override thread_id/run_id). The base_dir fallback in setup_agent_tool.py is left in place because the IM /bootstrap channel command depends on it. That code path can be tightened in a follow-up. Adds regression tests covering both helpers. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, call
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.runtime.runs.worker import _agent_factory_supports_app_config, _rollback_to_pre_run_checkpoint
|
||||
from deerflow.runtime.runs.worker import _agent_factory_supports_app_config, _build_runtime_context, _rollback_to_pre_run_checkpoint
|
||||
|
||||
|
||||
class FakeCheckpointer:
|
||||
@@ -221,6 +221,43 @@ def test_agent_factory_supports_app_config_detects_supported_signature():
|
||||
assert _agent_factory_supports_app_config(factory) is True
|
||||
|
||||
|
||||
def test_build_runtime_context_defaults_to_thread_and_run_id():
|
||||
ctx = _build_runtime_context("thread-1", "run-1", None)
|
||||
assert ctx == {"thread_id": "thread-1", "run_id": "run-1"}
|
||||
|
||||
|
||||
def test_build_runtime_context_merges_caller_context():
|
||||
"""Regression for issue #2677: keys from ``config['context']`` (e.g. ``agent_name``)
|
||||
must be merged into the Runtime's context so that ``ToolRuntime.context`` — which
|
||||
is what ``setup_agent`` reads — can see them."""
|
||||
caller_context = {"agent_name": "my-agent", "is_bootstrap": True, "model_name": "gpt-4"}
|
||||
|
||||
ctx = _build_runtime_context("thread-1", "run-1", caller_context)
|
||||
|
||||
assert ctx["thread_id"] == "thread-1"
|
||||
assert ctx["run_id"] == "run-1"
|
||||
assert ctx["agent_name"] == "my-agent"
|
||||
assert ctx["is_bootstrap"] is True
|
||||
assert ctx["model_name"] == "gpt-4"
|
||||
|
||||
|
||||
def test_build_runtime_context_caller_cannot_override_thread_id_or_run_id():
|
||||
"""A malicious or buggy caller must not be able to overwrite the worker-assigned
|
||||
``thread_id`` / ``run_id`` by stuffing them into ``config['context']``."""
|
||||
caller_context = {"thread_id": "spoofed", "run_id": "spoofed", "agent_name": "ok"}
|
||||
|
||||
ctx = _build_runtime_context("real-thread", "real-run", caller_context)
|
||||
|
||||
assert ctx["thread_id"] == "real-thread"
|
||||
assert ctx["run_id"] == "real-run"
|
||||
assert ctx["agent_name"] == "ok"
|
||||
|
||||
|
||||
def test_build_runtime_context_ignores_non_dict_caller_context():
|
||||
ctx = _build_runtime_context("thread-1", "run-1", "not-a-dict")
|
||||
assert ctx == {"thread_id": "thread-1", "run_id": "run-1"}
|
||||
|
||||
|
||||
def test_agent_factory_supports_app_config_returns_false_when_signature_lookup_fails(monkeypatch):
|
||||
class BrokenCallable:
|
||||
def __call__(self, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user