mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
fix: surface configured sandbox mounts to agents (#1638)
* fix: surface configured sandbox mounts to agents * fix: address PR review feedback --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
3ff15423d6
commit
aae59a8ba8
@ -257,6 +257,8 @@ sandbox:
|
||||
read_only: false
|
||||
```
|
||||
|
||||
When you configure `sandbox.mounts`, DeerFlow exposes those `container_path` values in the agent prompt so the agent can discover and operate on mounted directories directly instead of assuming everything must live under `/mnt/user-data`.
|
||||
|
||||
### Skills
|
||||
|
||||
Configure the skills directory for specialized workflows:
|
||||
|
||||
@ -477,6 +477,28 @@ def _build_acp_section() -> str:
|
||||
)
|
||||
|
||||
|
||||
def _build_custom_mounts_section() -> str:
|
||||
"""Build a prompt section for explicitly configured sandbox mounts."""
|
||||
try:
|
||||
from deerflow.config import get_app_config
|
||||
|
||||
mounts = get_app_config().sandbox.mounts or []
|
||||
except Exception:
|
||||
logger.exception("Failed to load configured sandbox mounts for the lead-agent prompt")
|
||||
return ""
|
||||
|
||||
if not mounts:
|
||||
return ""
|
||||
|
||||
lines = []
|
||||
for mount in mounts:
|
||||
access = "read-only" if mount.read_only else "read-write"
|
||||
lines.append(f"- Custom mount: `{mount.container_path}` - Host directory mapped into the sandbox ({access})")
|
||||
|
||||
mounts_list = "\n".join(lines)
|
||||
return f"\n**Custom Mounted Directories:**\n{mounts_list}\n- If the user needs files outside `/mnt/user-data`, use these absolute container paths directly when they match the requested directory"
|
||||
|
||||
|
||||
def apply_prompt_template(subagent_enabled: bool = False, max_concurrent_subagents: int = 3, *, agent_name: str | None = None, available_skills: set[str] | None = None) -> str:
|
||||
# Get memory context
|
||||
memory_context = _get_memory_context(agent_name)
|
||||
@ -511,6 +533,8 @@ def apply_prompt_template(subagent_enabled: bool = False, max_concurrent_subagen
|
||||
|
||||
# Build ACP agent section only if ACP agents are configured
|
||||
acp_section = _build_acp_section()
|
||||
custom_mounts_section = _build_custom_mounts_section()
|
||||
acp_and_mounts_section = "\n".join(section for section in (acp_section, custom_mounts_section) if section)
|
||||
|
||||
# Format the prompt with dynamic skills and memory
|
||||
prompt = SYSTEM_PROMPT_TEMPLATE.format(
|
||||
@ -522,7 +546,7 @@ def apply_prompt_template(subagent_enabled: bool = False, max_concurrent_subagen
|
||||
subagent_section=subagent_section,
|
||||
subagent_reminder=subagent_reminder,
|
||||
subagent_thinking=subagent_thinking,
|
||||
acp_section=acp_section,
|
||||
acp_section=acp_and_mounts_section,
|
||||
)
|
||||
|
||||
return prompt + f"\n<current_date>{datetime.now().strftime('%Y-%m-%d, %A')}</current_date>"
|
||||
|
||||
@ -81,7 +81,9 @@ class RunManager:
|
||||
async def list_by_thread(self, thread_id: str) -> list[RunRecord]:
|
||||
"""Return all runs for a given thread, newest first."""
|
||||
async with self._lock:
|
||||
return list(reversed([r for r in self._runs.values() if r.thread_id == thread_id]))
|
||||
# Dict insertion order matches creation order, so reversing it gives
|
||||
# us deterministic newest-first results even when timestamps tie.
|
||||
return [r for r in reversed(self._runs.values()) if r.thread_id == thread_id]
|
||||
|
||||
async def set_status(self, run_id: str, status: RunStatus, *, error: str | None = None) -> None:
|
||||
"""Transition a run to a new status."""
|
||||
|
||||
@ -37,6 +37,7 @@ You have access to the sandbox environment:
|
||||
- User uploads: `/mnt/user-data/uploads`
|
||||
- User workspace: `/mnt/user-data/workspace`
|
||||
- Output files: `/mnt/user-data/outputs`
|
||||
- Deployment-configured custom mounts may also be available at other absolute container paths; use them directly when the task references those mounted directories
|
||||
</working_directory>
|
||||
""",
|
||||
tools=["bash", "ls", "read_file", "write_file", "str_replace"], # Sandbox tools only
|
||||
|
||||
@ -38,6 +38,7 @@ You have access to the same sandbox environment as the parent agent:
|
||||
- User uploads: `/mnt/user-data/uploads`
|
||||
- User workspace: `/mnt/user-data/workspace`
|
||||
- Output files: `/mnt/user-data/outputs`
|
||||
- Deployment-configured custom mounts may also be available at other absolute container paths; use them directly when the task references those mounted directories
|
||||
</working_directory>
|
||||
""",
|
||||
tools=None, # Inherit all tools from parent
|
||||
|
||||
46
backend/tests/test_lead_agent_prompt.py
Normal file
46
backend/tests/test_lead_agent_prompt.py
Normal file
@ -0,0 +1,46 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from deerflow.agents.lead_agent import prompt as prompt_module
|
||||
|
||||
|
||||
def test_build_custom_mounts_section_returns_empty_when_no_mounts(monkeypatch):
|
||||
config = SimpleNamespace(sandbox=SimpleNamespace(mounts=[]))
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
|
||||
assert prompt_module._build_custom_mounts_section() == ""
|
||||
|
||||
|
||||
def test_build_custom_mounts_section_lists_configured_mounts(monkeypatch):
|
||||
mounts = [
|
||||
SimpleNamespace(container_path="/home/user/shared", read_only=False),
|
||||
SimpleNamespace(container_path="/mnt/reference", read_only=True),
|
||||
]
|
||||
config = SimpleNamespace(sandbox=SimpleNamespace(mounts=mounts))
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
|
||||
section = prompt_module._build_custom_mounts_section()
|
||||
|
||||
assert "**Custom Mounted Directories:**" in section
|
||||
assert "`/home/user/shared`" in section
|
||||
assert "read-write" in section
|
||||
assert "`/mnt/reference`" in section
|
||||
assert "read-only" in section
|
||||
|
||||
|
||||
def test_apply_prompt_template_includes_custom_mounts(monkeypatch):
|
||||
mounts = [SimpleNamespace(container_path="/home/user/shared", read_only=False)]
|
||||
config = SimpleNamespace(
|
||||
sandbox=SimpleNamespace(mounts=mounts),
|
||||
skills=SimpleNamespace(container_path="/mnt/skills"),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
monkeypatch.setattr(prompt_module, "load_skills", lambda enabled_only=True: [])
|
||||
monkeypatch.setattr(prompt_module, "get_deferred_tools_prompt_section", lambda: "")
|
||||
monkeypatch.setattr(prompt_module, "_build_acp_section", lambda: "")
|
||||
monkeypatch.setattr(prompt_module, "_get_memory_context", lambda agent_name=None: "")
|
||||
monkeypatch.setattr(prompt_module, "get_agent_soul", lambda agent_name=None: "")
|
||||
|
||||
prompt = prompt_module.apply_prompt_template()
|
||||
|
||||
assert "`/home/user/shared`" in prompt
|
||||
assert "Custom Mounted Directories" in prompt
|
||||
@ -86,6 +86,18 @@ async def test_list_by_thread(manager: RunManager):
|
||||
assert runs[1].run_id == r1.run_id
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_list_by_thread_is_stable_when_timestamps_tie(manager: RunManager, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Newest-first ordering should not depend on timestamp precision."""
|
||||
monkeypatch.setattr("deerflow.runtime.runs.manager._now_iso", lambda: "2026-01-01T00:00:00+00:00")
|
||||
|
||||
r1 = await manager.create("thread-1")
|
||||
r2 = await manager.create("thread-1")
|
||||
|
||||
runs = await manager.list_by_thread("thread-1")
|
||||
assert [run.run_id for run in runs] == [r2.run_id, r1.run_id]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_has_inflight(manager: RunManager):
|
||||
"""has_inflight should be True when a run is pending or running."""
|
||||
|
||||
@ -397,6 +397,9 @@ sandbox:
|
||||
# # - host_path: /path/on/host
|
||||
# # container_path: /home/user/shared
|
||||
# # read_only: false
|
||||
# #
|
||||
# # # DeerFlow will surface configured container_path values to the agent,
|
||||
# # # so it can directly read/write mounted directories such as /home/user/shared
|
||||
#
|
||||
# # Optional: Environment variables to inject into the sandbox container
|
||||
# # Values starting with $ will be resolved from host environment variables
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user