agentscope-runtime 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. agentscope_runtime/adapters/agentscope/stream.py +2 -9
  2. agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
  3. agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
  4. agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
  5. agentscope_runtime/adapters/utils.py +6 -0
  6. agentscope_runtime/cli/commands/deploy.py +383 -0
  7. agentscope_runtime/common/collections/redis_mapping.py +4 -1
  8. agentscope_runtime/common/container_clients/knative_client.py +466 -0
  9. agentscope_runtime/engine/__init__.py +4 -0
  10. agentscope_runtime/engine/app/agent_app.py +48 -5
  11. agentscope_runtime/engine/constant.py +1 -0
  12. agentscope_runtime/engine/deployers/__init__.py +12 -0
  13. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +31 -1
  14. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +458 -41
  15. agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +76 -0
  16. agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +749 -0
  17. agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
  18. agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
  19. agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
  20. agentscope_runtime/engine/deployers/kubernetes_deployer.py +3 -0
  21. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +8 -2
  22. agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +5 -0
  23. agentscope_runtime/engine/deployers/utils/net_utils.py +65 -0
  24. agentscope_runtime/engine/runner.py +17 -3
  25. agentscope_runtime/engine/schemas/exception.py +24 -0
  26. agentscope_runtime/engine/services/agent_state/redis_state_service.py +61 -8
  27. agentscope_runtime/engine/services/agent_state/state_service_factory.py +2 -5
  28. agentscope_runtime/engine/services/memory/redis_memory_service.py +129 -25
  29. agentscope_runtime/engine/services/session_history/redis_session_history_service.py +160 -34
  30. agentscope_runtime/engine/tracing/wrapper.py +18 -4
  31. agentscope_runtime/sandbox/__init__.py +14 -6
  32. agentscope_runtime/sandbox/box/base/__init__.py +2 -2
  33. agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
  34. agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
  35. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
  36. agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
  37. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
  38. agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
  39. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
  40. agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
  41. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
  42. agentscope_runtime/sandbox/box/sandbox.py +98 -65
  43. agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
  44. agentscope_runtime/sandbox/build.py +50 -57
  45. agentscope_runtime/sandbox/client/__init__.py +6 -1
  46. agentscope_runtime/sandbox/client/async_http_client.py +339 -0
  47. agentscope_runtime/sandbox/client/base.py +74 -0
  48. agentscope_runtime/sandbox/client/http_client.py +108 -329
  49. agentscope_runtime/sandbox/enums.py +7 -0
  50. agentscope_runtime/sandbox/manager/sandbox_manager.py +264 -4
  51. agentscope_runtime/sandbox/manager/server/app.py +7 -1
  52. agentscope_runtime/version.py +1 -1
  53. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/METADATA +109 -29
  54. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/RECORD +58 -46
  55. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/WHEEL +0 -0
  56. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/entry_points.txt +0 -0
  57. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/licenses/LICENSE +0 -0
  58. {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@ import platform
5
5
  from typing import Optional, List, Union
6
6
  from urllib.parse import urlencode, urljoin
7
7
 
8
- from agentscope_runtime.sandbox.box.sandbox import Sandbox
8
+ from ..sandbox import Sandbox, SandboxAsync
9
9
 
10
10
  from ...utils import build_image_uri
11
11
  from ...registry import SandboxRegistry
@@ -15,6 +15,107 @@ from ...constant import TIMEOUT
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
 
18
+ class HostPrerequisiteError(Exception):
19
+ """Exception raised when host prerequisites
20
+ for MobileSandbox are not met."""
21
+
22
+
23
+ def _check_host_readiness() -> None:
24
+ logger.info(
25
+ "Performing one-time host environment check for MobileSandbox...",
26
+ )
27
+
28
+ architecture = platform.machine().lower()
29
+ if architecture in ("aarch64", "arm64"):
30
+ logger.warning(
31
+ "\n======================== WARNING ========================\n"
32
+ "ARM64/aarch64 architecture detected (e.g., Apple M-series).\n"
33
+ "Running this mobile sandbox on a non-x86_64 host may lead \n"
34
+ " to unexpected compatibility or performance issues.\n"
35
+ "=========================================================",
36
+ )
37
+
38
+ os_type = platform.system()
39
+ if os_type == "Linux":
40
+ try:
41
+ result = subprocess.run(
42
+ ["lsmod"],
43
+ capture_output=True,
44
+ text=True,
45
+ check=True,
46
+ )
47
+ loaded_modules = result.stdout
48
+ except (FileNotFoundError, subprocess.CalledProcessError):
49
+ loaded_modules = ""
50
+ logger.warning(
51
+ "Could not execute 'lsmod' to verify kernel modules.",
52
+ )
53
+
54
+ if "binder_linux" not in loaded_modules:
55
+ error_message = (
56
+ "\n========== HOST PREREQUISITE FAILED ==========\n"
57
+ "MobileSandbox requires specific kernel modules"
58
+ " that appear to be missing or not loaded.\n\n"
59
+ "To fix this, please run the following commands"
60
+ " on your Linux host:\n\n"
61
+ "## Install required kernel modules\n"
62
+ "sudo apt update"
63
+ " && sudo apt install -y linux-modules-extra-`uname -r`\n"
64
+ "sudo modprobe binder_linux"
65
+ ' devices="binder,hwbinder,vndbinder"\n'
66
+ "## (Optional) Load the ashmem driver for older kernels\n"
67
+ "sudo modprobe ashmem_linux\n"
68
+ "=================================================="
69
+ )
70
+ raise HostPrerequisiteError(error_message)
71
+
72
+ if os_type == "Windows":
73
+ try:
74
+ result = subprocess.run(
75
+ ["wsl", "lsmod"],
76
+ capture_output=True,
77
+ text=True,
78
+ check=True,
79
+ encoding="utf-8",
80
+ )
81
+ loaded_modules = result.stdout
82
+ except (FileNotFoundError, subprocess.CalledProcessError):
83
+ loaded_modules = ""
84
+ logger.warning(
85
+ "Could not execute 'wsl lsmod' to verify kernel modules.",
86
+ )
87
+
88
+ if "binder_linux" not in loaded_modules:
89
+ error_message = (
90
+ "\n========== HOST PREREQUISITE FAILED ==========\n"
91
+ "MobileSandbox on Windows requires Docker Desktop "
92
+ "with the WSL 2 backend.\n"
93
+ "The required kernel modules seem to be missing "
94
+ "in your WSL 2 environment.\n\n"
95
+ "To fix this, please follow these steps:\n\n"
96
+ "1. **Ensure Docker Desktop is using WSL 2**:\n"
97
+ " - Open Docker Desktop -> Settings -> General.\n"
98
+ " - Make sure 'Use the WSL 2 based engine' "
99
+ "is checked.\n\n"
100
+ "2. **Ensure WSL is installed and updated**:\n"
101
+ " - Open PowerShell or Command Prompt "
102
+ "as Administrator.\n"
103
+ " - Run: wsl --install\n"
104
+ " - Run: wsl --update\n"
105
+ " (An update usually installs a recent Linux kernel "
106
+ "with the required modules.)\n\n"
107
+ "3. **Verify manually (Optional)**:\n"
108
+ " - After updating, run 'wsl lsmod | findstr binder' "
109
+ "in your terminal.\n"
110
+ " - If it shows 'binder_linux', "
111
+ "the issue should be resolved.\n"
112
+ "=================================================="
113
+ )
114
+ raise HostPrerequisiteError(error_message)
115
+
116
+ logger.info("Host environment check passed.")
117
+
118
+
18
119
  class MobileMixin:
19
120
  @property
20
121
  def mobile_url(self):
@@ -41,9 +142,41 @@ class MobileMixin:
41
142
  )
42
143
 
43
144
 
44
- class HostPrerequisiteError(Exception):
45
- """Exception raised when host prerequisites
46
- for MobileSandbox are not met."""
145
+ class AsyncMobileMixin:
146
+ async def get_mobile_url_async(self):
147
+ """
148
+ Asynchronously retrieve the mobile VNC/websockify connection URL.
149
+
150
+ Returns:
151
+ str: Fully qualified URL to access the mobile sandbox UI remotely.
152
+
153
+ Raises:
154
+ RuntimeError: If the sandbox is not healthy.
155
+ """
156
+ # Check health asynchronously
157
+ is_healthy = await self.manager_api.check_health_async(
158
+ identity=self.sandbox_id,
159
+ )
160
+ if not is_healthy:
161
+ raise RuntimeError(f"Sandbox {self.sandbox_id} is not healthy")
162
+
163
+ # Get container info asynchronously
164
+ info = await self.get_info_async()
165
+
166
+ # Local path and remote path (currently the same)
167
+ path = "/websockify/"
168
+ remote_path = "/websockify/"
169
+ params = {"password": info["runtime_token"]}
170
+
171
+ # Local URL if base_url is not set
172
+ if self.base_url is None:
173
+ return urljoin(info["url"], path) + "?" + urlencode(params)
174
+
175
+ # Remote URL
176
+ return (
177
+ f"{self.base_url}/desktop/{self.sandbox_id}{remote_path}"
178
+ f"?{urlencode(params)}"
179
+ )
47
180
 
48
181
 
49
182
  @SandboxRegistry.register(
@@ -66,7 +199,7 @@ class MobileSandbox(MobileMixin, Sandbox):
66
199
  sandbox_type: SandboxType = SandboxType.MOBILE,
67
200
  ):
68
201
  if base_url is None and not self.__class__._host_check_done:
69
- self._check_host_readiness()
202
+ _check_host_readiness()
70
203
  self.__class__._host_check_done = True
71
204
 
72
205
  super().__init__(
@@ -77,101 +210,6 @@ class MobileSandbox(MobileMixin, Sandbox):
77
210
  sandbox_type,
78
211
  )
79
212
 
80
- def _check_host_readiness(self) -> None:
81
- logger.info(
82
- "Performing one-time host environment check for MobileSandbox...",
83
- )
84
-
85
- architecture = platform.machine().lower()
86
- if architecture in ("aarch64", "arm64"):
87
- logger.warning(
88
- "\n======================== WARNING ========================\n"
89
- "ARM64/aarch64 architecture detected (e.g., Apple M-series).\n"
90
- "Running this mobile sandbox on a non-x86_64 host may lead \n"
91
- " to unexpected compatibility or performance issues.\n"
92
- "=========================================================",
93
- )
94
-
95
- os_type = platform.system()
96
- if os_type == "Linux":
97
- try:
98
- result = subprocess.run(
99
- ["lsmod"],
100
- capture_output=True,
101
- text=True,
102
- check=True,
103
- )
104
- loaded_modules = result.stdout
105
- except (FileNotFoundError, subprocess.CalledProcessError):
106
- loaded_modules = ""
107
- logger.warning(
108
- "Could not execute 'lsmod' to verify kernel modules.",
109
- )
110
-
111
- if "binder_linux" not in loaded_modules:
112
- error_message = (
113
- "\n========== HOST PREREQUISITE FAILED ==========\n"
114
- "MobileSandbox requires specific kernel modules"
115
- " that appear to be missing or not loaded.\n\n"
116
- "To fix this, please run the following commands"
117
- " on your Linux host:\n\n"
118
- "## Install required kernel modules\n"
119
- "sudo apt update"
120
- " && sudo apt install -y linux-modules-extra-`uname -r`\n"
121
- "sudo modprobe binder_linux"
122
- ' devices="binder,hwbinder,vndbinder"\n'
123
- "## (Optional) Load the ashmem driver for older kernels\n"
124
- "sudo modprobe ashmem_linux\n"
125
- "=================================================="
126
- )
127
- raise HostPrerequisiteError(error_message)
128
-
129
- if os_type == "Windows":
130
- try:
131
- result = subprocess.run(
132
- ["wsl", "lsmod"],
133
- capture_output=True,
134
- text=True,
135
- check=True,
136
- encoding="utf-8",
137
- )
138
- loaded_modules = result.stdout
139
- except (FileNotFoundError, subprocess.CalledProcessError):
140
- loaded_modules = ""
141
- logger.warning(
142
- "Could not execute 'wsl lsmod' to verify kernel modules.",
143
- )
144
-
145
- if "binder_linux" not in loaded_modules:
146
- error_message = (
147
- "\n========== HOST PREREQUISITE FAILED ==========\n"
148
- "MobileSandbox on Windows requires Docker Desktop "
149
- "with the WSL 2 backend.\n"
150
- "The required kernel modules seem to be missing "
151
- "in your WSL 2 environment.\n\n"
152
- "To fix this, please follow these steps:\n\n"
153
- "1. **Ensure Docker Desktop is using WSL 2**:\n"
154
- " - Open Docker Desktop -> Settings -> General.\n"
155
- " - Make sure 'Use the WSL 2 based engine' "
156
- "is checked.\n\n"
157
- "2. **Ensure WSL is installed and updated**:\n"
158
- " - Open PowerShell or Command Prompt "
159
- "as Administrator.\n"
160
- " - Run: wsl --install\n"
161
- " - Run: wsl --update\n"
162
- " (An update usually installs a recent Linux kernel "
163
- "with the required modules.)\n\n"
164
- "3. **Verify manually (Optional)**:\n"
165
- " - After updating, run 'wsl lsmod | findstr binder' "
166
- "in your terminal.\n"
167
- " - If it shows 'binder_linux', "
168
- "the issue should be resolved.\n"
169
- "=================================================="
170
- )
171
- raise HostPrerequisiteError(error_message)
172
-
173
- logger.info("Host environment check passed.")
174
-
175
213
  def adb_use(
176
214
  self,
177
215
  action: str,
@@ -288,3 +326,112 @@ class MobileSandbox(MobileMixin, Sandbox):
288
326
  def mobile_get_screenshot(self):
289
327
  """Take a screenshot of the current device screen."""
290
328
  return self.call_tool("adb", {"action": "get_screenshot"})
329
+
330
+
331
+ @SandboxRegistry.register(
332
+ build_image_uri("runtime-sandbox-mobile"),
333
+ sandbox_type=SandboxType.MOBILE_ASYNC,
334
+ security_level="high",
335
+ timeout=TIMEOUT,
336
+ description="Mobile Sandbox (Async)",
337
+ runtime_config={"privileged": True},
338
+ )
339
+ class MobileSandboxAsync(MobileMixin, AsyncMobileMixin, SandboxAsync):
340
+ _host_check_done = False
341
+
342
+ def __init__(
343
+ self,
344
+ sandbox_id: Optional[str] = None,
345
+ timeout: int = 3000,
346
+ base_url: Optional[str] = None,
347
+ bearer_token: Optional[str] = None,
348
+ sandbox_type: SandboxType = SandboxType.MOBILE_ASYNC,
349
+ ):
350
+ if base_url is None and not self.__class__._host_check_done:
351
+ _check_host_readiness()
352
+ self.__class__._host_check_done = True
353
+
354
+ super().__init__(
355
+ sandbox_id,
356
+ timeout,
357
+ base_url,
358
+ bearer_token,
359
+ sandbox_type,
360
+ )
361
+
362
+ async def adb_use(
363
+ self,
364
+ action: str,
365
+ coordinate: Optional[List[int]] = None,
366
+ start: Optional[List[int]] = None,
367
+ end: Optional[List[int]] = None,
368
+ duration: Optional[int] = None,
369
+ code: Optional[Union[int, str]] = None,
370
+ text: Optional[str] = None,
371
+ ):
372
+ """
373
+ Asynchronously execute a general-purpose ADB action.
374
+ """
375
+ payload = {"action": action}
376
+ if coordinate is not None:
377
+ payload["coordinate"] = coordinate
378
+ if start is not None:
379
+ payload["start"] = start
380
+ if end is not None:
381
+ payload["end"] = end
382
+ if duration is not None:
383
+ payload["duration"] = duration
384
+ if code is not None:
385
+ payload["code"] = code
386
+ if text is not None:
387
+ payload["text"] = text
388
+
389
+ return await self.call_tool_async("adb", payload)
390
+
391
+ async def mobile_get_screen_resolution(self):
392
+ """Asynchronously get the screen resolution."""
393
+ return await self.call_tool_async(
394
+ "adb",
395
+ {"action": "get_screen_resolution"},
396
+ )
397
+
398
+ async def mobile_tap(self, coordinate: List[int]):
399
+ """Asynchronously tap specific screen coordinates."""
400
+ return await self.call_tool_async(
401
+ "adb",
402
+ {"action": "tap", "coordinate": coordinate},
403
+ )
404
+
405
+ async def mobile_swipe(
406
+ self,
407
+ start: List[int],
408
+ end: List[int],
409
+ duration: Optional[int] = None,
410
+ ):
411
+ """Asynchronously perform a swipe gesture."""
412
+ payload = {
413
+ "action": "swipe",
414
+ "start": start,
415
+ "end": end,
416
+ }
417
+ if duration is not None:
418
+ payload["duration"] = duration
419
+ return await self.call_tool_async("adb", payload)
420
+
421
+ async def mobile_input_text(self, text: str):
422
+ """Asynchronously input text into the focused UI element."""
423
+ return await self.call_tool_async(
424
+ "adb",
425
+ {"action": "input_text", "text": text},
426
+ )
427
+
428
+ async def mobile_key_event(self, code: Union[int, str]):
429
+ """Asynchronously send a key event to the device."""
430
+ return await self.call_tool_async(
431
+ "adb",
432
+ {"action": "key_event", "code": code},
433
+ )
434
+
435
+ async def mobile_get_screenshot(self):
436
+ """Asynchronously take a screenshot."""
437
+ return await self.call_tool_async("adb", {"action": "get_screenshot"})
@@ -12,71 +12,46 @@ logging.basicConfig(level=logging.INFO)
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
- class Sandbox:
15
+ class SandboxBase:
16
16
  """
17
- Sandbox Interface.
17
+ Common base class for both sync and async Sandbox interfaces.
18
18
  """
19
19
 
20
20
  def __init__(
21
21
  self,
22
22
  sandbox_id: Optional[str] = None,
23
- timeout: int = 3000, # TODO: enable life circle management
23
+ timeout: int = 3000,
24
24
  base_url: Optional[str] = None,
25
- bearer_token: Optional[str] = None, # TODO: support api_key
25
+ bearer_token: Optional[str] = None,
26
26
  sandbox_type: SandboxType = SandboxType.BASE,
27
27
  ) -> None:
28
- """
29
- Initialize the sandbox interface.
30
- """
31
28
  self.base_url = base_url
29
+ self.embed_mode = not bool(base_url)
30
+ self.sandbox_type = sandbox_type
31
+ self.timeout = timeout
32
+ self._sandbox_id = sandbox_id
33
+
32
34
  if base_url:
33
- self.embed_mode = False
34
35
  self.manager_api = SandboxManager(
35
36
  base_url=base_url,
36
37
  bearer_token=bearer_token,
37
- ).__enter__()
38
+ )
38
39
  else:
39
- # Launch a local manager
40
- self.embed_mode = True
41
40
  self.manager_api = SandboxManager(
42
41
  default_type=sandbox_type,
43
42
  )
44
43
 
45
- if sandbox_id is None:
46
- logger.debug(
47
- "You are using embed mode, it might take several seconds to "
48
- "init the runtime, please wait.",
49
- )
50
-
51
- sandbox_id = self.manager_api.create_from_pool(
52
- sandbox_type=SandboxType(sandbox_type).value,
53
- )
54
- if sandbox_id is None:
55
- raise RuntimeError(
56
- "No sandbox available. "
57
- "Please check if sandbox images exist, build or pull "
58
- "missing images in sandbox server.",
59
- )
60
- self._sandbox_id = sandbox_id
61
-
62
- self._sandbox_id = sandbox_id
63
- self.sandbox_type = sandbox_type
64
- self.timeout = timeout
65
-
66
- # Clean up function enabled in embed mode
67
- if self.embed_mode:
68
- atexit.register(self._cleanup)
69
- self._register_signal_handlers()
44
+ @property
45
+ def sandbox_id(self) -> Optional[str]:
46
+ return self._sandbox_id
70
47
 
71
- def _register_signal_handlers(self) -> None:
72
- """
73
- Register signal handlers for graceful shutdown and cleanup.
74
- Handles SIGINT (Ctrl+C) and, if available, SIGTERM to ensure that
75
- the sandbox is properly cleaned up when the process receives these
76
- signals. On platforms where SIGTERM is not available (e.g.,
77
- Windows), only SIGINT is handled.
78
- """
48
+ @sandbox_id.setter
49
+ def sandbox_id(self, value: str) -> None:
50
+ if not value:
51
+ raise ValueError("Sandbox ID cannot be empty.")
52
+ self._sandbox_id = value
79
53
 
54
+ def _register_signal_handlers(self):
80
55
  def _handler(signum, frame): # pylint: disable=unused-argument
81
56
  logger.debug(
82
57
  f"Received signal {signum}, stopping Sandbox"
@@ -85,7 +60,6 @@ class Sandbox:
85
60
  self._cleanup()
86
61
  raise SystemExit(0)
87
62
 
88
- # Windows does not support SIGTERM
89
63
  if hasattr(signal, "SIGTERM"):
90
64
  signals = [signal.SIGINT, signal.SIGTERM]
91
65
  else:
@@ -107,39 +81,36 @@ class Sandbox:
107
81
  specific sandbox instance.
108
82
  """
109
83
  try:
110
- # Remote not need to close the embed_manager
111
84
  if self.embed_mode:
112
- # Clean all
113
85
  self.manager_api.__exit__(None, None, None)
114
86
  else:
115
- # Clean the specific sandbox
116
87
  self.manager_api.release(self.sandbox_id)
117
88
  except Exception as e:
118
89
  import traceback
119
90
 
120
91
  logger.error(
121
- f"Cleanup {self.sandbox_id} error: {e}\n"
122
- f"{traceback.format_exc()}",
92
+ f"Cleanup {self.sandbox_id} error: {e}"
93
+ f"\n{traceback.format_exc()}",
123
94
  )
124
95
 
96
+
97
+ class Sandbox(SandboxBase):
125
98
  def __enter__(self):
99
+ # Create sandbox if sandbox_id not provided
100
+ if self.sandbox_id is None:
101
+ self.sandbox_id = self.manager_api.create_from_pool(
102
+ sandbox_type=SandboxType(self.sandbox_type).value,
103
+ )
104
+ if self.sandbox_id is None:
105
+ raise RuntimeError("No sandbox available.")
106
+ if self.embed_mode:
107
+ atexit.register(self._cleanup)
108
+ self._register_signal_handlers()
126
109
  return self
127
110
 
128
111
  def __exit__(self, exc_type, exc_value, traceback):
129
112
  self._cleanup()
130
113
 
131
- @property
132
- def sandbox_id(self) -> str:
133
- """Get the sandbox ID."""
134
- return self._sandbox_id
135
-
136
- @sandbox_id.setter
137
- def sandbox_id(self, value: str) -> None:
138
- """Set the sandbox ID."""
139
- if not value:
140
- raise ValueError("Sandbox ID cannot be empty.")
141
- self._sandbox_id = value
142
-
143
114
  def get_info(self) -> dict:
144
115
  return self.manager_api.get_info(self.sandbox_id)
145
116
 
@@ -156,15 +127,77 @@ class Sandbox:
156
127
  ) -> Any:
157
128
  if arguments is None:
158
129
  arguments = {}
159
-
160
130
  return self.manager_api.call_tool(self.sandbox_id, name, arguments)
161
131
 
162
- def add_mcp_servers(
132
+ def add_mcp_servers(self, server_configs: dict, overwrite=False):
133
+ return self.manager_api.add_mcp_servers(
134
+ self.sandbox_id,
135
+ server_configs,
136
+ overwrite,
137
+ )
138
+
139
+
140
+ class SandboxAsync(SandboxBase):
141
+ async def __aenter__(self):
142
+ if self.sandbox_id is None:
143
+ self.sandbox_id = await self.manager_api.create_from_pool_async(
144
+ sandbox_type=SandboxType(self.sandbox_type).value,
145
+ )
146
+ if self.sandbox_id is None:
147
+ raise RuntimeError("No sandbox available.")
148
+ if self.embed_mode:
149
+ atexit.register(self._cleanup)
150
+ self._register_signal_handlers()
151
+ return self
152
+
153
+ async def __aexit__(self, exc_type, exc_value, traceback):
154
+ await self._cleanup_async()
155
+
156
+ async def _cleanup_async(self):
157
+ try:
158
+ if self.embed_mode:
159
+ await self.manager_api.__aexit__(None, None, None)
160
+ else:
161
+ await self.manager_api.release_async(self.sandbox_id)
162
+ except Exception as e:
163
+ import traceback
164
+
165
+ logger.error(
166
+ f"Async Cleanup {self.sandbox_id} error: {e}"
167
+ f"\n{traceback.format_exc()}",
168
+ )
169
+
170
+ async def get_info_async(self) -> dict:
171
+ return await self.manager_api.get_info_async(self.sandbox_id)
172
+
173
+ async def list_tools_async(
174
+ self,
175
+ tool_type: Optional[str] = None,
176
+ ) -> dict:
177
+ return await self.manager_api.list_tools_async(
178
+ self.sandbox_id,
179
+ tool_type=tool_type,
180
+ )
181
+
182
+ async def call_tool_async(
183
+ self,
184
+ name: str,
185
+ arguments: Optional[dict[str, Any]] = None,
186
+ ) -> Any:
187
+ if arguments is None:
188
+ arguments = {}
189
+ return await self.manager_api.call_tool_async(
190
+ self.sandbox_id,
191
+ name,
192
+ arguments,
193
+ )
194
+
195
+ async def add_mcp_servers_async(
163
196
  self,
164
197
  server_configs: dict,
165
198
  overwrite=False,
166
199
  ):
167
- return self.manager_api.add_mcp_servers(
200
+ return await self.manager_api.add_mcp_servers_async(
168
201
  self.sandbox_id,
169
202
  server_configs,
170
203
  overwrite,