mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 08:25:57 +00:00
fix(sandbox): avoid blocking sandbox readiness polling (#2822)
* fix(sandbox): offload async sandbox acquisition Run blocking sandbox provider acquisition through the async provider hook so eager sandbox setup does not stall the event loop. * fix(sandbox): add async readiness polling Introduce an async sandbox readiness poller using httpx and asyncio.sleep while preserving the existing synchronous API. * test(sandbox): cover async readiness polling Lock in non-blocking readiness behavior so the async helper does not regress to requests.get or time.sleep. * fix(sandbox): allow anonymous backend creation * fix(sandbox): use async readiness in provider acquisition * fix(sandbox): use async acquisition for lazy tools * test(sandbox): cover anonymous remote creation * fix(sandbox): clamp async readiness timeout budget * fix(sandbox): offload async lock file handling * fix(sandbox): delegate async middleware fallthrough * docs(sandbox): document async acquisition path * fix(sandbox): offload async sandbox release * docs(sandbox): mention async release hook * fix(sandbox): address async lock review Reduce duplicate sync/async sandbox acquisition state handling and move async thread-lock waits onto a dedicated executor with cancellation-safe cleanup. * chore: retrigger ci Retrigger GitHub Actions after upstream main fixed the stale PR merge lint failure. * test(sandbox): sync backend unit fixtures --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import asyncio
|
||||
import posixpath
|
||||
import re
|
||||
import shlex
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from langchain.tools import tool
|
||||
@@ -1111,6 +1113,68 @@ def ensure_sandbox_initialized(runtime: Runtime | None = None) -> Sandbox:
|
||||
return sandbox
|
||||
|
||||
|
||||
async def ensure_sandbox_initialized_async(runtime: Runtime | None = None) -> Sandbox:
|
||||
"""Async counterpart to ``ensure_sandbox_initialized`` for tool runtimes.
|
||||
|
||||
This keeps lazy sandbox acquisition on the async provider hook, so AIO
|
||||
sandbox startup and readiness polling do not fall back to synchronous
|
||||
``provider.acquire()`` during async tool execution.
|
||||
"""
|
||||
if runtime is None:
|
||||
raise SandboxRuntimeError("Tool runtime not available")
|
||||
|
||||
if runtime.state is None:
|
||||
raise SandboxRuntimeError("Tool runtime state not available")
|
||||
|
||||
sandbox_state = runtime.state.get("sandbox")
|
||||
if sandbox_state is not None:
|
||||
sandbox_id = sandbox_state.get("sandbox_id")
|
||||
if sandbox_id is not None:
|
||||
sandbox = get_sandbox_provider().get(sandbox_id)
|
||||
if sandbox is not None:
|
||||
if runtime.context is not None:
|
||||
runtime.context["sandbox_id"] = sandbox_id
|
||||
return sandbox
|
||||
|
||||
thread_id = runtime.context.get("thread_id") if runtime.context else None
|
||||
if thread_id is None:
|
||||
thread_id = runtime.config.get("configurable", {}).get("thread_id") if runtime.config else None
|
||||
if thread_id is None:
|
||||
raise SandboxRuntimeError("Thread ID not available in runtime context")
|
||||
|
||||
provider = get_sandbox_provider()
|
||||
sandbox_id = await provider.acquire_async(thread_id)
|
||||
|
||||
runtime.state["sandbox"] = {"sandbox_id": sandbox_id}
|
||||
|
||||
sandbox = provider.get(sandbox_id)
|
||||
if sandbox is None:
|
||||
raise SandboxNotFoundError("Sandbox not found after acquisition", sandbox_id=sandbox_id)
|
||||
|
||||
if runtime.context is not None:
|
||||
runtime.context["sandbox_id"] = sandbox_id
|
||||
return sandbox
|
||||
|
||||
|
||||
async def _run_sync_tool_after_async_sandbox_init(
|
||||
func: Callable[..., str] | None,
|
||||
runtime: Runtime,
|
||||
*args: object,
|
||||
) -> str:
|
||||
"""Initialize lazily via async provider, then run sync tool body off-thread."""
|
||||
try:
|
||||
await ensure_sandbox_initialized_async(runtime)
|
||||
except SandboxError as e:
|
||||
return f"Error: {e}"
|
||||
except Exception as e:
|
||||
return f"Error: Unexpected error initializing sandbox: {_sanitize_error(e, runtime)}"
|
||||
|
||||
if func is None:
|
||||
return "Error: Tool implementation not available"
|
||||
|
||||
return await asyncio.to_thread(func, runtime, *args)
|
||||
|
||||
|
||||
def ensure_thread_directories_exist(runtime: Runtime | None) -> None:
|
||||
"""Ensure thread data directories (workspace, uploads, outputs) exist.
|
||||
|
||||
@@ -1273,6 +1337,13 @@ def bash_tool(runtime: Runtime, description: str, command: str) -> str:
|
||||
return f"Error: Unexpected error executing command: {_sanitize_error(e, runtime)}"
|
||||
|
||||
|
||||
async def _bash_tool_async(runtime: Runtime, description: str, command: str) -> str:
|
||||
return await _run_sync_tool_after_async_sandbox_init(bash_tool.func, runtime, description, command)
|
||||
|
||||
|
||||
bash_tool.coroutine = _bash_tool_async
|
||||
|
||||
|
||||
@tool("ls", parse_docstring=True)
|
||||
def ls_tool(runtime: Runtime, description: str, path: str) -> str:
|
||||
"""List the contents of a directory up to 2 levels deep in tree format.
|
||||
@@ -1320,6 +1391,13 @@ def ls_tool(runtime: Runtime, description: str, path: str) -> str:
|
||||
return f"Error: Unexpected error listing directory: {_sanitize_error(e, runtime)}"
|
||||
|
||||
|
||||
async def _ls_tool_async(runtime: Runtime, description: str, path: str) -> str:
|
||||
return await _run_sync_tool_after_async_sandbox_init(ls_tool.func, runtime, description, path)
|
||||
|
||||
|
||||
ls_tool.coroutine = _ls_tool_async
|
||||
|
||||
|
||||
@tool("glob", parse_docstring=True)
|
||||
def glob_tool(
|
||||
runtime: Runtime,
|
||||
@@ -1370,6 +1448,28 @@ def glob_tool(
|
||||
return f"Error: Unexpected error searching paths: {_sanitize_error(e, runtime)}"
|
||||
|
||||
|
||||
async def _glob_tool_async(
|
||||
runtime: Runtime,
|
||||
description: str,
|
||||
pattern: str,
|
||||
path: str,
|
||||
include_dirs: bool = False,
|
||||
max_results: int = _DEFAULT_GLOB_MAX_RESULTS,
|
||||
) -> str:
|
||||
return await _run_sync_tool_after_async_sandbox_init(
|
||||
glob_tool.func,
|
||||
runtime,
|
||||
description,
|
||||
pattern,
|
||||
path,
|
||||
include_dirs,
|
||||
max_results,
|
||||
)
|
||||
|
||||
|
||||
glob_tool.coroutine = _glob_tool_async
|
||||
|
||||
|
||||
@tool("grep", parse_docstring=True)
|
||||
def grep_tool(
|
||||
runtime: Runtime,
|
||||
@@ -1440,6 +1540,32 @@ def grep_tool(
|
||||
return f"Error: Unexpected error searching file contents: {_sanitize_error(e, runtime)}"
|
||||
|
||||
|
||||
async def _grep_tool_async(
|
||||
runtime: Runtime,
|
||||
description: str,
|
||||
pattern: str,
|
||||
path: str,
|
||||
glob: str | None = None,
|
||||
literal: bool = False,
|
||||
case_sensitive: bool = False,
|
||||
max_results: int = _DEFAULT_GREP_MAX_RESULTS,
|
||||
) -> str:
|
||||
return await _run_sync_tool_after_async_sandbox_init(
|
||||
grep_tool.func,
|
||||
runtime,
|
||||
description,
|
||||
pattern,
|
||||
path,
|
||||
glob,
|
||||
literal,
|
||||
case_sensitive,
|
||||
max_results,
|
||||
)
|
||||
|
||||
|
||||
grep_tool.coroutine = _grep_tool_async
|
||||
|
||||
|
||||
@tool("read_file", parse_docstring=True)
|
||||
def read_file_tool(
|
||||
runtime: Runtime,
|
||||
@@ -1495,6 +1621,19 @@ def read_file_tool(
|
||||
return f"Error: Unexpected error reading file: {_sanitize_error(e, runtime)}"
|
||||
|
||||
|
||||
async def _read_file_tool_async(
|
||||
runtime: Runtime,
|
||||
description: str,
|
||||
path: str,
|
||||
start_line: int | None = None,
|
||||
end_line: int | None = None,
|
||||
) -> str:
|
||||
return await _run_sync_tool_after_async_sandbox_init(read_file_tool.func, runtime, description, path, start_line, end_line)
|
||||
|
||||
|
||||
read_file_tool.coroutine = _read_file_tool_async
|
||||
|
||||
|
||||
@tool("write_file", parse_docstring=True)
|
||||
def write_file_tool(
|
||||
runtime: Runtime,
|
||||
@@ -1536,6 +1675,19 @@ def write_file_tool(
|
||||
return f"Error: Unexpected error writing file: {_sanitize_error(e, runtime)}"
|
||||
|
||||
|
||||
async def _write_file_tool_async(
|
||||
runtime: Runtime,
|
||||
description: str,
|
||||
path: str,
|
||||
content: str,
|
||||
append: bool = False,
|
||||
) -> str:
|
||||
return await _run_sync_tool_after_async_sandbox_init(write_file_tool.func, runtime, description, path, content, append)
|
||||
|
||||
|
||||
write_file_tool.coroutine = _write_file_tool_async
|
||||
|
||||
|
||||
@tool("str_replace", parse_docstring=True)
|
||||
def str_replace_tool(
|
||||
runtime: Runtime,
|
||||
@@ -1585,3 +1737,25 @@ def str_replace_tool(
|
||||
return f"Error: Permission denied accessing file: {requested_path}"
|
||||
except Exception as e:
|
||||
return f"Error: Unexpected error replacing string: {_sanitize_error(e, runtime)}"
|
||||
|
||||
|
||||
async def _str_replace_tool_async(
|
||||
runtime: Runtime,
|
||||
description: str,
|
||||
path: str,
|
||||
old_str: str,
|
||||
new_str: str,
|
||||
replace_all: bool = False,
|
||||
) -> str:
|
||||
return await _run_sync_tool_after_async_sandbox_init(
|
||||
str_replace_tool.func,
|
||||
runtime,
|
||||
description,
|
||||
path,
|
||||
old_str,
|
||||
new_str,
|
||||
replace_all,
|
||||
)
|
||||
|
||||
|
||||
str_replace_tool.coroutine = _str_replace_tool_async
|
||||
|
||||
Reference in New Issue
Block a user