Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/unit_test_sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:

- name: Run tests with coverage (default)
run: |
coverage run -m pytest tests/sandbox/test_sandbox.py tests/sandbox/test_sandbox_service.py tests/sandbox/test_heartbeat.py
coverage run -m pytest tests/sandbox/test_sandbox.py tests/sandbox/test_sandbox_service.py tests/sandbox/test_heartbeat.py tests/sandbox/test_heartbeat_timeout_restore.py

- name: Generate coverage report
run: |
Expand Down
3 changes: 2 additions & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
[![GitHub Forks](https://img.shields.io/github/forks/agentscope-ai/agentscope-runtime?style=flat&logo=github&color=purple&label=Forks)](https://github.com/agentscope-ai/agentscope-runtime/network)
[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg?logo=githubactions&label=Build)](https://github.com/agentscope-ai/agentscope-runtime/actions)
[![Cookbook](https://img.shields.io/badge/📚_Cookbook-English|中文-teal.svg)](https://runtime.agentscope.io)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-agentscope--runtime-navy.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/agentscope-ai/agentscope-runtime)[![A2A](https://img.shields.io/badge/A2A-Agent_to_Agent-blue.svg?label=A2A)](https://a2a-protocol.org/)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-Ask_Devin-navy.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/agentscope-ai/agentscope-runtime)
[![A2A](https://img.shields.io/badge/A2A-Agent_to_Agent-blue.svg?label=A2A)](https://a2a-protocol.org/)
[![MCP](https://img.shields.io/badge/MCP-Model_Context_Protocol-purple.svg?logo=plug&label=MCP)](https://modelcontextprotocol.io/)
[![Discord](https://img.shields.io/badge/Discord-Join_Us-blueviolet.svg?logo=discord)](https://discord.gg/eYMpfnkG8h)
[![DingTalk](https://img.shields.io/badge/DingTalk-Join_Us-orange.svg)](https://qr.dingtalk.com/action/joingroup?code=v1,k1,OmDlBXpjW+I2vWjKDsjvI9dhcXjGZi3bQiojOq3dlDw=&_dt_no_comment=1&origin=11)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ def create(
# Convert environment dict to list of tuples
env_list = []
if environment:
env_list = list(environment.items())
env_list = [
(str(k), "" if v is None else str(v))
for k, v in environment.items()
]
Comment thread
rayrayraykk marked this conversation as resolved.

# Convert volumes to BoxLite format
volume_list = []
Expand Down
50 changes: 41 additions & 9 deletions src/agentscope_runtime/engine/services/sandbox/sandbox_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,41 @@


class SandboxService(ServiceWithLifecycleManager):
def __init__(self, base_url=None, bearer_token=None):
def __init__(
self,
base_url=None,
bearer_token=None,
drain_on_stop: bool = True,
):
"""
Create a SandboxService.

Args:
base_url:
Sandbox manager API base URL. If None, runs in embedded mode.
bearer_token:
Bearer token used to authenticate with the sandbox manager API.
drain_on_stop:
Whether to drain (release) all sandboxes associated with this
service instance when `stop()` is called.

- True (default): `stop()` will iterate over all known session
mappings and release all non-AgentBay sandbox environments.
This helps prevent resource leaks when the service shuts
down.
- False: `stop()` will NOT release sessions/environments. Use
this when sandboxes are meant to outlive the service process
(e.g., managed elsewhere).

Note: In embedded mode (`base_url is None`), `stop()` will
still call `manager_api.cleanup()` to tear down embedded
resources.
"""
Comment thread
rayrayraykk marked this conversation as resolved.
self.manager_api = None
self.base_url = base_url
self.bearer_token = bearer_token
self._health = False
self.drain_on_stop = drain_on_stop

async def start(self) -> None:
if self.manager_api is None:
Expand All @@ -29,14 +59,16 @@ async def stop(self) -> None:
self._health = False
return

session_keys = self.manager_api.list_session_keys()

if session_keys:
for session_ctx_id in session_keys:
env_ids = self.manager_api.get_session_mapping(session_ctx_id)
if env_ids:
for env_id in env_ids:
self.manager_api.release(env_id)
if self.drain_on_stop:
session_keys = self.manager_api.list_session_keys()
if session_keys:
for session_ctx_id in session_keys:
env_ids = self.manager_api.get_session_mapping(
session_ctx_id,
)
if env_ids:
for env_id in env_ids:
self.manager_api.release(env_id)

if self.base_url is None:
# Embedded mode
Expand Down
30 changes: 23 additions & 7 deletions src/agentscope_runtime/sandbox/box/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import signal
from typing import Any, Optional

import shortuuid

from ..enums import SandboxType
from ..manager.sandbox_manager import SandboxManager
from ..manager.server.app import get_config
Expand Down Expand Up @@ -154,17 +156,25 @@ class Sandbox(SandboxBase):
def __enter__(self):
# Create sandbox if sandbox_id not provided
if self._sandbox_id is None:
short_uuid = shortuuid.ShortUUID().uuid()
session_ctx_id = str(short_uuid)
if self.workspace_dir:
# bypass pool when workspace_dir is set
self._sandbox_id = self.manager_api.create(
_id = self.manager_api.create(
sandbox_type=SandboxType(self.sandbox_type).value,
mount_dir=self.workspace_dir,
# TODO: support bind self-define id
meta={"session_ctx_id": session_ctx_id},
)
else:
self._sandbox_id = self.manager_api.create_from_pool(
_id = self.manager_api.create_from_pool(
sandbox_type=SandboxType(self.sandbox_type).value,
# TODO: support bind self-define id
meta={"session_ctx_id": session_ctx_id},
)

self._sandbox_id = _id

if self._sandbox_id is None:
raise RuntimeError(
"No sandbox available. This may happen if: "
Expand Down Expand Up @@ -217,18 +227,24 @@ def add_mcp_servers(self, server_configs: dict, overwrite=False):
class SandboxAsync(SandboxBase):
async def __aenter__(self):
if self._sandbox_id is None:
short_uuid = shortuuid.ShortUUID().uuid()
session_ctx_id = str(short_uuid)
if self.workspace_dir:
self._sandbox_id = await self.manager_api.create_async(
_id = await self.manager_api.create_async(
sandbox_type=SandboxType(self.sandbox_type).value,
mount_dir=self.workspace_dir,
# TODO: support bind self-define id
meta={"session_ctx_id": session_ctx_id},
)
else:
self._sandbox_id = (
await self.manager_api.create_from_pool_async(
sandbox_type=SandboxType(self.sandbox_type).value,
)
_id = await self.manager_api.create_from_pool_async(
sandbox_type=SandboxType(self.sandbox_type).value,
# TODO: support bind self-define id
meta={"session_ctx_id": session_ctx_id},
)

self._sandbox_id = _id

if self._sandbox_id is None:
raise RuntimeError("No sandbox available.")
if self.embed_mode:
Expand Down
Loading
Loading