mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(sandbox): unwrap Overwrite-wrapped sandbox state in tools and middlewares (#6015)
* fix(sandbox): unwrap Overwrite-wrapped sandbox state in tools and middlewares * fix(changelog): restore database entry and add #4380 reference link, simplify task_tool unwrap * fix(subagents): preserve Overwrite wrapper when task_tool forwards sandbox state to executor --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
1b691689da
commit
f87a04e581
@ -327,6 +327,13 @@ This release closes that milestone with **181 merged pull requests**.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **sandbox:** Unwrap `Overwrite`-wrapped sandbox state in
|
||||
`ToolOutputBudgetMiddleware` and `ReadBeforeWriteMiddleware`. In delta
|
||||
checkpoint mode, forked or restored threads deliver the `sandbox` channel
|
||||
wrapped in LangGraph's `Overwrite`. Without unwrapping,
|
||||
`isinstance(sandbox_state, dict)` returned `False`, causing large tool output
|
||||
externalization to fail and fall back to inline truncation, and
|
||||
read-before-write lock scoping to miss the active sandbox ID. ([#6051])
|
||||
- **doctor:** `make doctor` now checks the config file the Gateway actually
|
||||
loads. It always inspected `<checkout>/config.yaml` and ignored
|
||||
`DEER_FLOW_CONFIG_PATH` and `DEER_FLOW_PROJECT_ROOT`, so a missing override
|
||||
@ -6226,4 +6233,5 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#5981]: https://github.com/bytedance/deer-flow/pull/5981
|
||||
[#5982]: https://github.com/bytedance/deer-flow/pull/5982
|
||||
[#5987]: https://github.com/bytedance/deer-flow/pull/5987
|
||||
[#6015]: https://github.com/bytedance/deer-flow/pull/6015
|
||||
|
||||
|
||||
@ -332,6 +332,11 @@
|
||||
|
||||
### 修复
|
||||
|
||||
- **沙箱:** 在中间件(`ToolOutputBudgetMiddleware` 与 `ReadBeforeWriteMiddleware`)
|
||||
中解包 `Overwrite` 包装的沙箱状态。在 delta checkpoint 模式下,分叉或回滚
|
||||
的对话交付的 `sandbox` 通道状态会被 LangGraph 的 `Overwrite` 包装。此前直接判断
|
||||
`isinstance(sandbox_state, dict)` 会返回 `False`,导致大工具输出无法外部化到沙箱而退化为内联
|
||||
硬截断,以及写前读锁作用域丢失有效沙箱 ID。([#6015])
|
||||
- **doctor:** `make doctor` 现在检查 Gateway 实际加载的配置文件。此前它
|
||||
固定检查 `<checkout>/config.yaml`,忽略 `DEER_FLOW_CONFIG_PATH` 与
|
||||
`DEER_FLOW_PROJECT_ROOT`:指向不存在路径、会让 Gateway 无法启动的覆盖值仍会
|
||||
@ -5282,3 +5287,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#5981]: https://github.com/bytedance/deer-flow/pull/5981
|
||||
[#5982]: https://github.com/bytedance/deer-flow/pull/5982
|
||||
[#5987]: https://github.com/bytedance/deer-flow/pull/5987
|
||||
[#6015]: https://github.com/bytedance/deer-flow/pull/6015
|
||||
|
||||
@ -279,7 +279,9 @@ class ReadBeforeWriteMiddleware(AgentMiddleware):
|
||||
return thread_id
|
||||
state = request.state
|
||||
if isinstance(state, dict):
|
||||
sandbox_state = state.get("sandbox")
|
||||
from deerflow.sandbox.overwrite import unwrap_sandbox
|
||||
|
||||
sandbox_state, _ = unwrap_sandbox(state.get("sandbox"))
|
||||
if isinstance(sandbox_state, dict):
|
||||
sandbox_id = sandbox_state.get("sandbox_id")
|
||||
if isinstance(sandbox_id, str) and sandbox_id:
|
||||
|
||||
@ -342,7 +342,9 @@ def _resolve_sandbox(request: ToolCallRequest) -> Sandbox | None:
|
||||
state = getattr(runtime, "state", None)
|
||||
if not isinstance(state, dict):
|
||||
return None
|
||||
sandbox_state = state.get("sandbox")
|
||||
from deerflow.sandbox.overwrite import unwrap_sandbox
|
||||
|
||||
sandbox_state, _ = unwrap_sandbox(state.get("sandbox"))
|
||||
if not isinstance(sandbox_state, dict):
|
||||
return None
|
||||
sandbox_id = sandbox_state.get("sandbox_id")
|
||||
|
||||
@ -842,3 +842,18 @@ class TestBlockedPayloadElision:
|
||||
rewritten = self._captured(handler).messages[1]
|
||||
assert rewritten.tool_calls[0]["args"]["content"].startswith("[payload elided: 5000 chars")
|
||||
assert rewritten.tool_calls[1] == ai.tool_calls[1]
|
||||
|
||||
|
||||
def test_lock_scope_resolves_overwrite_wrapped_sandbox():
|
||||
"""Fork-restored state wraps sandbox in Overwrite; lock scope must still resolve it."""
|
||||
from langgraph.types import Overwrite
|
||||
|
||||
from deerflow.agents.middlewares.read_before_write_middleware import ReadBeforeWriteMiddleware
|
||||
|
||||
req = ToolCallRequest(
|
||||
tool_call={"name": "write_file", "args": {}, "id": "tc-1"},
|
||||
tool=None,
|
||||
state={"sandbox": Overwrite({"sandbox_id": "sb-fork-lock"})},
|
||||
runtime=MagicMock(context={}),
|
||||
)
|
||||
assert ReadBeforeWriteMiddleware._lock_scope(req) == "sb-fork-lock"
|
||||
|
||||
@ -683,6 +683,39 @@ def test_task_tool_forwards_is_internal_true_to_executor(monkeypatch):
|
||||
assert captured["executor_kwargs"]["is_internal"] is True
|
||||
|
||||
|
||||
def test_task_tool_preserves_overwrite_sandbox_state_for_executor(monkeypatch):
|
||||
"""Fork-restored checkpoints wrap sandbox state in Overwrite; task_tool must preserve the wrapper so executor retains borrow semantics."""
|
||||
from langgraph.types import Overwrite
|
||||
|
||||
runtime = _make_runtime()
|
||||
runtime.state["sandbox"] = Overwrite({"sandbox_id": "sb-fork-123"})
|
||||
captured = {}
|
||||
|
||||
class DummyExecutor:
|
||||
def __init__(self, **kwargs):
|
||||
captured["executor_kwargs"] = kwargs
|
||||
|
||||
def execute_async(self, prompt, task_id=None):
|
||||
return task_id or "generated-task-id"
|
||||
|
||||
monkeypatch.setattr(task_tool_module, "SubagentStatus", FakeSubagentStatus)
|
||||
monkeypatch.setattr(task_tool_module, "SubagentExecutor", DummyExecutor)
|
||||
monkeypatch.setattr(task_tool_module, "get_subagent_config", lambda _: _make_subagent_config())
|
||||
monkeypatch.setattr(
|
||||
task_tool_module,
|
||||
"get_background_task_result",
|
||||
lambda _: _make_result(FakeSubagentStatus.COMPLETED, result="done"),
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: lambda _event: None)
|
||||
monkeypatch.setattr(task_tool_module.asyncio, "sleep", _no_sleep)
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
|
||||
_run_task_tool(runtime=runtime, description="test", prompt="p", subagent_type="general-purpose", tool_call_id="tc-1")
|
||||
passed_sandbox = captured["executor_kwargs"]["sandbox_state"]
|
||||
assert isinstance(passed_sandbox, Overwrite)
|
||||
assert passed_sandbox.value == {"sandbox_id": "sb-fork-123"}
|
||||
|
||||
|
||||
def test_task_tool_forwards_is_internal_false_to_executor(monkeypatch):
|
||||
"""is_internal=False must also propagate explicitly (not skipped)."""
|
||||
runtime = _make_runtime()
|
||||
|
||||
@ -1516,6 +1516,21 @@ class TestResolveSandbox:
|
||||
req = SimpleNamespace(runtime=SimpleNamespace(state={"sandbox": {"sandbox_id": "sb-1"}}))
|
||||
assert mod._resolve_sandbox(req) is sb
|
||||
|
||||
def test_returns_sandbox_from_provider_when_overwrite_wrapped(self, monkeypatch):
|
||||
from langgraph.types import Overwrite
|
||||
|
||||
from deerflow.agents.middlewares import tool_output_budget_middleware as mod
|
||||
|
||||
sb = _FakeSandbox()
|
||||
monkeypatch.setattr(
|
||||
mod,
|
||||
"get_sandbox_provider",
|
||||
lambda: _FakeProvider(uses_thread_data_mounts=False, sandbox=sb),
|
||||
)
|
||||
# Fork-restored state delivers sandbox wrapped in Overwrite
|
||||
req = SimpleNamespace(runtime=SimpleNamespace(state={"sandbox": Overwrite({"sandbox_id": "sb-fork"})}))
|
||||
assert mod._resolve_sandbox(req) is sb
|
||||
|
||||
def test_returns_none_on_provider_exception(self, monkeypatch):
|
||||
from deerflow.agents.middlewares import tool_output_budget_middleware as mod
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user