agentscope-runtime 1.0.5.post1__py3-none-any.whl → 1.1.0b2__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 (71) hide show
  1. agentscope_runtime/__init__.py +3 -0
  2. agentscope_runtime/adapters/agentscope/message.py +36 -295
  3. agentscope_runtime/adapters/agentscope/stream.py +89 -2
  4. agentscope_runtime/adapters/agno/message.py +11 -2
  5. agentscope_runtime/adapters/agno/stream.py +1 -0
  6. agentscope_runtime/adapters/langgraph/__init__.py +1 -3
  7. agentscope_runtime/adapters/langgraph/message.py +11 -106
  8. agentscope_runtime/adapters/langgraph/stream.py +1 -0
  9. agentscope_runtime/adapters/ms_agent_framework/message.py +11 -1
  10. agentscope_runtime/adapters/ms_agent_framework/stream.py +1 -0
  11. agentscope_runtime/adapters/text/stream.py +1 -0
  12. agentscope_runtime/common/container_clients/agentrun_client.py +0 -3
  13. agentscope_runtime/common/container_clients/boxlite_client.py +26 -15
  14. agentscope_runtime/common/container_clients/fc_client.py +0 -11
  15. agentscope_runtime/common/utils/deprecation.py +14 -17
  16. agentscope_runtime/common/utils/logging.py +44 -0
  17. agentscope_runtime/engine/app/agent_app.py +5 -5
  18. agentscope_runtime/engine/app/celery_mixin.py +43 -4
  19. agentscope_runtime/engine/deployers/adapter/agui/__init__.py +8 -1
  20. agentscope_runtime/engine/deployers/adapter/agui/agui_adapter_utils.py +6 -1
  21. agentscope_runtime/engine/deployers/adapter/agui/agui_protocol_adapter.py +2 -2
  22. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +13 -0
  23. agentscope_runtime/engine/runner.py +31 -6
  24. agentscope_runtime/engine/schemas/agent_schemas.py +28 -0
  25. agentscope_runtime/engine/services/sandbox/sandbox_service.py +41 -9
  26. agentscope_runtime/sandbox/box/base/base_sandbox.py +4 -0
  27. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +4 -0
  28. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +9 -2
  29. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +4 -0
  30. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +5 -1
  31. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +4 -0
  32. agentscope_runtime/sandbox/box/sandbox.py +122 -13
  33. agentscope_runtime/sandbox/client/async_http_client.py +1 -0
  34. agentscope_runtime/sandbox/client/base.py +0 -1
  35. agentscope_runtime/sandbox/client/http_client.py +0 -2
  36. agentscope_runtime/sandbox/manager/heartbeat_mixin.py +486 -0
  37. agentscope_runtime/sandbox/manager/sandbox_manager.py +740 -153
  38. agentscope_runtime/sandbox/manager/server/app.py +18 -11
  39. agentscope_runtime/sandbox/manager/server/config.py +10 -2
  40. agentscope_runtime/sandbox/mcp_server.py +0 -1
  41. agentscope_runtime/sandbox/model/__init__.py +2 -1
  42. agentscope_runtime/sandbox/model/container.py +90 -3
  43. agentscope_runtime/sandbox/model/manager_config.py +45 -1
  44. agentscope_runtime/version.py +1 -1
  45. {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/METADATA +36 -54
  46. {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/RECORD +50 -69
  47. agentscope_runtime/adapters/agentscope/long_term_memory/__init__.py +0 -6
  48. agentscope_runtime/adapters/agentscope/long_term_memory/_long_term_memory_adapter.py +0 -258
  49. agentscope_runtime/adapters/agentscope/memory/__init__.py +0 -6
  50. agentscope_runtime/adapters/agentscope/memory/_memory_adapter.py +0 -152
  51. agentscope_runtime/engine/services/agent_state/__init__.py +0 -25
  52. agentscope_runtime/engine/services/agent_state/redis_state_service.py +0 -166
  53. agentscope_runtime/engine/services/agent_state/state_service.py +0 -179
  54. agentscope_runtime/engine/services/agent_state/state_service_factory.py +0 -52
  55. agentscope_runtime/engine/services/memory/__init__.py +0 -33
  56. agentscope_runtime/engine/services/memory/mem0_memory_service.py +0 -128
  57. agentscope_runtime/engine/services/memory/memory_service.py +0 -292
  58. agentscope_runtime/engine/services/memory/memory_service_factory.py +0 -126
  59. agentscope_runtime/engine/services/memory/redis_memory_service.py +0 -290
  60. agentscope_runtime/engine/services/memory/reme_personal_memory_service.py +0 -109
  61. agentscope_runtime/engine/services/memory/reme_task_memory_service.py +0 -11
  62. agentscope_runtime/engine/services/memory/tablestore_memory_service.py +0 -301
  63. agentscope_runtime/engine/services/session_history/__init__.py +0 -32
  64. agentscope_runtime/engine/services/session_history/redis_session_history_service.py +0 -283
  65. agentscope_runtime/engine/services/session_history/session_history_service.py +0 -267
  66. agentscope_runtime/engine/services/session_history/session_history_service_factory.py +0 -73
  67. agentscope_runtime/engine/services/session_history/tablestore_session_history_service.py +0 -288
  68. {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/WHEEL +0 -0
  69. {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/entry_points.txt +0 -0
  70. {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/licenses/LICENSE +0 -0
  71. {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ from typing import (
14
14
  Union,
15
15
  Dict,
16
16
  AsyncIterator,
17
+ Callable,
17
18
  )
18
19
 
19
20
  from .deployers import (
@@ -49,6 +50,9 @@ class Runner:
49
50
  """
50
51
  self.framework_type = None
51
52
 
53
+ self.in_type_converters: Optional[Dict[str, Callable]] = None
54
+ self.out_type_converters: Optional[Dict[str, Callable]] = None
55
+
52
56
  self._deploy_managers = {}
53
57
  self._health = False
54
58
  self._exit_stack = AsyncExitStack()
@@ -77,7 +81,7 @@ class Runner:
77
81
  else:
78
82
  init_fn()
79
83
  else:
80
- logger.warning("[Runner] init_handler is not callable")
84
+ logger.warning("init_handler is not callable")
81
85
  self._health = True
82
86
  return self
83
87
 
@@ -90,7 +94,7 @@ class Runner:
90
94
  else:
91
95
  shutdown_fn()
92
96
  except Exception as e:
93
- logger.warning(f"[Runner] Exception in shutdown handler: {e}")
97
+ logger.warning(f"Exception in shutdown handler: {e}")
94
98
  try:
95
99
  await self._exit_stack.aclose()
96
100
  except Exception:
@@ -251,7 +255,12 @@ class Runner:
251
255
 
252
256
  stream_adapter = adapt_agentscope_message_stream
253
257
  kwargs.update(
254
- {"msgs": message_to_agentscope_msg(request.input)},
258
+ {
259
+ "msgs": message_to_agentscope_msg(
260
+ request.input,
261
+ type_converters=self.in_type_converters,
262
+ ),
263
+ },
255
264
  )
256
265
  elif self.framework_type == "langgraph":
257
266
  from ..adapters.langgraph.stream import (
@@ -261,7 +270,12 @@ class Runner:
261
270
 
262
271
  stream_adapter = adapt_langgraph_message_stream
263
272
  kwargs.update(
264
- {"msgs": message_to_langgraph_msg(request.input)},
273
+ {
274
+ "msgs": message_to_langgraph_msg(
275
+ request.input,
276
+ type_converters=self.in_type_converters,
277
+ ),
278
+ },
265
279
  )
266
280
  elif self.framework_type == "agno":
267
281
  from ..adapters.agno.stream import (
@@ -271,7 +285,12 @@ class Runner:
271
285
 
272
286
  stream_adapter = adapt_agno_message_stream
273
287
  kwargs.update(
274
- {"msgs": await message_to_agno_message(request.input)},
288
+ {
289
+ "msgs": await message_to_agno_message(
290
+ request.input,
291
+ type_converters=self.in_type_converters,
292
+ ),
293
+ },
275
294
  )
276
295
  elif self.framework_type == "ms_agent_framework":
277
296
  from ..adapters.ms_agent_framework.stream import (
@@ -283,7 +302,12 @@ class Runner:
283
302
 
284
303
  stream_adapter = adapt_ms_agent_framework_message_stream
285
304
  kwargs.update(
286
- {"msgs": message_to_ms_agent_framework_message(request.input)},
305
+ {
306
+ "msgs": message_to_ms_agent_framework_message(
307
+ request.input,
308
+ type_converters=self.in_type_converters,
309
+ ),
310
+ },
287
311
  )
288
312
  # TODO: support other frameworks
289
313
  else:
@@ -303,6 +327,7 @@ class Runner:
303
327
  **query_kwargs,
304
328
  **kwargs,
305
329
  ),
330
+ type_converters=self.out_type_converters,
306
331
  ):
307
332
  if (
308
333
  event.status == RunStatus.Completed
@@ -49,6 +49,7 @@ class ContentType:
49
49
  AUDIO = "audio"
50
50
  FILE = "file"
51
51
  REFUSAL = "refusal"
52
+ VIDEO = "video"
52
53
 
53
54
 
54
55
  class Role:
@@ -231,6 +232,24 @@ class McpApprovalRequest(BaseModel):
231
232
  """The label of the mcp server making the request."""
232
233
 
233
234
 
235
+ class McpApprovalResponse(BaseModel):
236
+ """
237
+ mcp approval response
238
+ """
239
+
240
+ approval_request_id: str
241
+ """The unique ID of the approval request."""
242
+
243
+ approve: bool
244
+ """Whether the request was approved."""
245
+
246
+ id: Optional[str] = None
247
+ """The unique ID of the approval response."""
248
+
249
+ reason: Optional[str] = None
250
+ """Optional reason for the decision."""
251
+
252
+
234
253
  class Error(BaseModel):
235
254
  code: str
236
255
  """The error code of the message."""
@@ -383,6 +402,14 @@ class AudioContent(Content):
383
402
  """
384
403
 
385
404
 
405
+ class VideoContent(Content):
406
+ type: Literal[ContentType.VIDEO] = ContentType.VIDEO
407
+ """The type of the content part."""
408
+
409
+ video_url: Optional[str] = None
410
+ """The video URL details."""
411
+
412
+
386
413
  class FileContent(Content):
387
414
  type: Literal[ContentType.FILE] = ContentType.FILE
388
415
  """The type of the content part."""
@@ -442,6 +469,7 @@ AgentContent = Annotated[
442
469
  AudioContent,
443
470
  FileContent,
444
471
  RefusalContent,
472
+ VideoContent,
445
473
  ],
446
474
  Field(discriminator="type"),
447
475
  ]
@@ -9,11 +9,41 @@ from ....engine.services.base import ServiceWithLifecycleManager
9
9
 
10
10
 
11
11
  class SandboxService(ServiceWithLifecycleManager):
12
- def __init__(self, base_url=None, bearer_token=None):
12
+ def __init__(
13
+ self,
14
+ base_url=None,
15
+ bearer_token=None,
16
+ drain_on_stop: bool = True,
17
+ ):
18
+ """
19
+ Create a SandboxService.
20
+
21
+ Args:
22
+ base_url:
23
+ Sandbox manager API base URL. If None, runs in embedded mode.
24
+ bearer_token:
25
+ Bearer token used to authenticate with the sandbox manager API.
26
+ drain_on_stop:
27
+ Whether to drain (release) all sandboxes associated with this
28
+ service instance when `stop()` is called.
29
+
30
+ - True (default): `stop()` will iterate over all known session
31
+ mappings and release all non-AgentBay sandbox environments.
32
+ This helps prevent resource leaks when the service shuts
33
+ down.
34
+ - False: `stop()` will NOT release sessions/environments. Use
35
+ this when sandboxes are meant to outlive the service process
36
+ (e.g., managed elsewhere).
37
+
38
+ Note: In embedded mode (`base_url is None`), `stop()` will
39
+ still call `manager_api.cleanup()` to tear down embedded
40
+ resources.
41
+ """
13
42
  self.manager_api = None
14
43
  self.base_url = base_url
15
44
  self.bearer_token = bearer_token
16
45
  self._health = False
46
+ self.drain_on_stop = drain_on_stop
17
47
 
18
48
  async def start(self) -> None:
19
49
  if self.manager_api is None:
@@ -29,14 +59,16 @@ class SandboxService(ServiceWithLifecycleManager):
29
59
  self._health = False
30
60
  return
31
61
 
32
- session_keys = self.manager_api.list_session_keys()
33
-
34
- if session_keys:
35
- for session_ctx_id in session_keys:
36
- env_ids = self.manager_api.get_session_mapping(session_ctx_id)
37
- if env_ids:
38
- for env_id in env_ids:
39
- self.manager_api.release(env_id)
62
+ if self.drain_on_stop:
63
+ session_keys = self.manager_api.list_session_keys()
64
+ if session_keys:
65
+ for session_ctx_id in session_keys:
66
+ env_ids = self.manager_api.get_session_mapping(
67
+ session_ctx_id,
68
+ )
69
+ if env_ids:
70
+ for env_id in env_ids:
71
+ self.manager_api.release(env_id)
40
72
 
41
73
  if self.base_url is None:
42
74
  # Embedded mode
@@ -23,6 +23,7 @@ class BaseSandbox(Sandbox):
23
23
  base_url: Optional[str] = None,
24
24
  bearer_token: Optional[str] = None,
25
25
  sandbox_type: SandboxType = SandboxType.BASE,
26
+ workspace_dir: Optional[str] = None,
26
27
  ):
27
28
  super().__init__(
28
29
  sandbox_id,
@@ -30,6 +31,7 @@ class BaseSandbox(Sandbox):
30
31
  base_url,
31
32
  bearer_token,
32
33
  sandbox_type,
34
+ workspace_dir,
33
35
  )
34
36
 
35
37
  def run_ipython_cell(self, code: str):
@@ -66,6 +68,7 @@ class BaseSandboxAsync(SandboxAsync):
66
68
  base_url: Optional[str] = None,
67
69
  bearer_token: Optional[str] = None,
68
70
  sandbox_type: SandboxType = SandboxType.BASE_ASYNC,
71
+ workspace_dir: Optional[str] = None,
69
72
  ):
70
73
  super().__init__(
71
74
  sandbox_id,
@@ -73,6 +76,7 @@ class BaseSandboxAsync(SandboxAsync):
73
76
  base_url,
74
77
  bearer_token,
75
78
  sandbox_type,
79
+ workspace_dir,
76
80
  )
77
81
 
78
82
  async def run_ipython_cell(self, code: str):
@@ -43,6 +43,7 @@ class BrowserSandbox(GUIMixin, BaseSandbox):
43
43
  base_url: Optional[str] = None,
44
44
  bearer_token: Optional[str] = None,
45
45
  sandbox_type: SandboxType = SandboxType.BROWSER,
46
+ workspace_dir: Optional[str] = None,
46
47
  ):
47
48
  super().__init__(
48
49
  sandbox_id,
@@ -50,6 +51,7 @@ class BrowserSandbox(GUIMixin, BaseSandbox):
50
51
  base_url,
51
52
  bearer_token,
52
53
  sandbox_type,
54
+ workspace_dir,
53
55
  )
54
56
 
55
57
  def browser_close(self):
@@ -316,6 +318,7 @@ class BrowserSandboxAsync(GUIMixin, AsyncGUIMixin, BaseSandboxAsync):
316
318
  base_url: Optional[str] = None,
317
319
  bearer_token: Optional[str] = None,
318
320
  sandbox_type: SandboxType = SandboxType.BROWSER_ASYNC,
321
+ workspace_dir: Optional[str] = None,
319
322
  ):
320
323
  super().__init__(
321
324
  sandbox_id,
@@ -323,6 +326,7 @@ class BrowserSandboxAsync(GUIMixin, AsyncGUIMixin, BaseSandboxAsync):
323
326
  base_url,
324
327
  bearer_token,
325
328
  sandbox_type,
329
+ workspace_dir,
326
330
  )
327
331
 
328
332
  async def browser_close(self):
@@ -22,6 +22,13 @@ class DummySandbox(Sandbox):
22
22
  base_url: Optional[str] = None,
23
23
  bearer_token: Optional[str] = None,
24
24
  sandbox_type: SandboxType = SandboxType.DUMMY,
25
+ workspace_dir: Optional[str] = None,
25
26
  ):
26
- self._sandbox_id = sandbox_id
27
- self.sandbox_type = sandbox_type
27
+ super().__init__(
28
+ sandbox_id,
29
+ timeout,
30
+ base_url,
31
+ bearer_token,
32
+ sandbox_type,
33
+ workspace_dir,
34
+ )
@@ -25,6 +25,7 @@ class FilesystemSandbox(GUIMixin, BaseSandbox):
25
25
  base_url: Optional[str] = None,
26
26
  bearer_token: Optional[str] = None,
27
27
  sandbox_type: SandboxType = SandboxType.FILESYSTEM,
28
+ workspace_dir: Optional[str] = None,
28
29
  ):
29
30
  super().__init__(
30
31
  sandbox_id,
@@ -32,6 +33,7 @@ class FilesystemSandbox(GUIMixin, BaseSandbox):
32
33
  base_url,
33
34
  bearer_token,
34
35
  sandbox_type,
36
+ workspace_dir,
35
37
  )
36
38
 
37
39
  def read_file(self, path: str):
@@ -171,6 +173,7 @@ class FilesystemSandboxAsync(GUIMixin, AsyncGUIMixin, BaseSandboxAsync):
171
173
  base_url: Optional[str] = None,
172
174
  bearer_token: Optional[str] = None,
173
175
  sandbox_type: SandboxType = SandboxType.FILESYSTEM_ASYNC,
176
+ workspace_dir: Optional[str] = None,
174
177
  ):
175
178
  super().__init__(
176
179
  sandbox_id,
@@ -178,6 +181,7 @@ class FilesystemSandboxAsync(GUIMixin, AsyncGUIMixin, BaseSandboxAsync):
178
181
  base_url,
179
182
  bearer_token,
180
183
  sandbox_type,
184
+ workspace_dir,
181
185
  )
182
186
 
183
187
  async def read_file(self, path: str):
@@ -77,6 +77,7 @@ class GuiSandbox(GUIMixin, BaseSandbox):
77
77
  base_url: Optional[str] = None,
78
78
  bearer_token: Optional[str] = None,
79
79
  sandbox_type: SandboxType = SandboxType.GUI,
80
+ workspace_dir: Optional[str] = None,
80
81
  ):
81
82
  super().__init__(
82
83
  sandbox_id,
@@ -84,8 +85,9 @@ class GuiSandbox(GUIMixin, BaseSandbox):
84
85
  base_url,
85
86
  bearer_token,
86
87
  sandbox_type,
88
+ workspace_dir,
87
89
  )
88
- if get_platform() == "linux/arm64":
90
+ if "arm" in get_platform():
89
91
  logger.warning(
90
92
  "\nCompatibility Notice: This GUI Sandbox may have issues on "
91
93
  "arm64 CPU architectures, due to the computer-use-mcp does "
@@ -166,6 +168,7 @@ class GuiSandboxAsync(GUIMixin, AsyncGUIMixin, BaseSandboxAsync):
166
168
  base_url: Optional[str] = None,
167
169
  bearer_token: Optional[str] = None,
168
170
  sandbox_type: SandboxType = SandboxType.GUI_ASYNC,
171
+ workspace_dir: Optional[str] = None,
169
172
  ):
170
173
  super().__init__(
171
174
  sandbox_id,
@@ -173,6 +176,7 @@ class GuiSandboxAsync(GUIMixin, AsyncGUIMixin, BaseSandboxAsync):
173
176
  base_url,
174
177
  bearer_token,
175
178
  sandbox_type,
179
+ workspace_dir,
176
180
  )
177
181
  # Architecture compatibility warning
178
182
  if get_platform() == "linux/arm64":
@@ -197,6 +197,7 @@ class MobileSandbox(MobileMixin, Sandbox):
197
197
  base_url: Optional[str] = None,
198
198
  bearer_token: Optional[str] = None,
199
199
  sandbox_type: SandboxType = SandboxType.MOBILE,
200
+ workspace_dir: Optional[str] = None,
200
201
  ):
201
202
  if base_url is None and not self.__class__._host_check_done:
202
203
  _check_host_readiness()
@@ -208,6 +209,7 @@ class MobileSandbox(MobileMixin, Sandbox):
208
209
  base_url,
209
210
  bearer_token,
210
211
  sandbox_type,
212
+ workspace_dir,
211
213
  )
212
214
 
213
215
  def adb_use(
@@ -346,6 +348,7 @@ class MobileSandboxAsync(MobileMixin, AsyncMobileMixin, SandboxAsync):
346
348
  base_url: Optional[str] = None,
347
349
  bearer_token: Optional[str] = None,
348
350
  sandbox_type: SandboxType = SandboxType.MOBILE_ASYNC,
351
+ workspace_dir: Optional[str] = None,
349
352
  ):
350
353
  if base_url is None and not self.__class__._host_check_done:
351
354
  _check_host_readiness()
@@ -357,6 +360,7 @@ class MobileSandboxAsync(MobileMixin, AsyncMobileMixin, SandboxAsync):
357
360
  base_url,
358
361
  bearer_token,
359
362
  sandbox_type,
363
+ workspace_dir,
360
364
  )
361
365
 
362
366
  async def adb_use(
@@ -4,18 +4,51 @@ import logging
4
4
  import signal
5
5
  from typing import Any, Optional
6
6
 
7
+ import shortuuid
8
+
7
9
  from ..enums import SandboxType
8
10
  from ..manager.sandbox_manager import SandboxManager
9
11
  from ..manager.server.app import get_config
10
12
 
11
13
 
12
- logging.basicConfig(level=logging.INFO)
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
16
17
  class SandboxBase:
17
18
  """
18
19
  Common base class for both sync and async Sandbox interfaces.
20
+
21
+ This class holds shared configuration and lifecycle behaviors used by
22
+ `Sandbox` (sync) and `SandboxAsync` (async). It can operate in:
23
+
24
+ - Embedded mode: `base_url` is not provided; a local `SandboxManager`
25
+ is used.
26
+ - Remote mode: `base_url` is provided; operations are delegated to a remote
27
+ `SandboxManager` over HTTP.
28
+
29
+ Args:
30
+ sandbox_id: Existing sandbox/container identifier to attach to. If not
31
+ provided, a new sandbox will be created when entering the context
32
+ manager.
33
+ timeout: HTTP request timeout in seconds for client-side calls to the
34
+ sandbox runtime/manager (e.g., `list_tools`, `call_tool`, and other
35
+ network requests). This parameter does not control sandbox idle,
36
+ recycle, or heartbeat timeouts, which are configured separately by
37
+ the sandbox runtime (for example via the `HEARTBEAT_TIMEOUT`
38
+ environment variable).
39
+ base_url: Remote SandboxManager service URL. If provided, the sandbox
40
+ runs in remote mode; otherwise, embedded mode is used.
41
+ bearer_token: Optional bearer token for authenticating to the remote
42
+ manager.
43
+ sandbox_type: Sandbox runtime type/image selection.
44
+
45
+ Attributes:
46
+ base_url: Remote manager URL, if any.
47
+ embed_mode: Whether the sandbox is running with an embedded local
48
+ manager.
49
+ sandbox_type: Selected sandbox type.
50
+ timeout: HTTP request timeout in seconds.
51
+ _sandbox_id: The bound sandbox id (may be None until created).
19
52
  """
20
53
 
21
54
  def __init__(
@@ -25,12 +58,22 @@ class SandboxBase:
25
58
  base_url: Optional[str] = None,
26
59
  bearer_token: Optional[str] = None,
27
60
  sandbox_type: SandboxType = SandboxType.BASE,
61
+ workspace_dir: Optional[str] = None,
28
62
  ) -> None:
29
63
  self.base_url = base_url
30
64
  self.embed_mode = not bool(base_url)
31
65
  self.sandbox_type = sandbox_type
32
66
  self.timeout = timeout
33
67
  self._sandbox_id = sandbox_id
68
+ self._warned_sandbox_not_started = False
69
+
70
+ self.workspace_dir = workspace_dir
71
+
72
+ if self.base_url and self.workspace_dir:
73
+ raise RuntimeError(
74
+ "workspace_dir is only supported in embedded(local) mode; "
75
+ "remote mode mounts server paths and is not allowed.",
76
+ )
34
77
 
35
78
  if base_url:
36
79
  # Remote Manager
@@ -40,13 +83,24 @@ class SandboxBase:
40
83
  )
41
84
  else:
42
85
  # Embedded Manager
86
+ config = get_config()
87
+ # Allow in embedded mode
88
+ config.allow_mount_dir = True
43
89
  self.manager_api = SandboxManager(
44
- config=get_config(),
90
+ config=config,
45
91
  default_type=sandbox_type,
46
92
  )
47
93
 
48
94
  @property
49
95
  def sandbox_id(self) -> Optional[str]:
96
+ if self._sandbox_id is None and not self._warned_sandbox_not_started:
97
+ self._warned_sandbox_not_started = True
98
+ logger.error(
99
+ "Sandbox is not started yet (sandbox_id is None). "
100
+ "Use `with Sandbox(...) as sandbox:` / "
101
+ "`async with SandboxAsync(...) as sandbox:` "
102
+ "or call `start() / start_async()` first.",
103
+ )
50
104
  return self._sandbox_id
51
105
 
52
106
  @sandbox_id.setter
@@ -101,12 +155,33 @@ class SandboxBase:
101
155
  class Sandbox(SandboxBase):
102
156
  def __enter__(self):
103
157
  # Create sandbox if sandbox_id not provided
104
- if self.sandbox_id is None:
105
- self.sandbox_id = self.manager_api.create_from_pool(
106
- sandbox_type=SandboxType(self.sandbox_type).value,
107
- )
108
- if self.sandbox_id is None:
109
- raise RuntimeError("No sandbox available.")
158
+ if self._sandbox_id is None:
159
+ short_uuid = shortuuid.ShortUUID().uuid()
160
+ session_ctx_id = str(short_uuid)
161
+ if self.workspace_dir:
162
+ # bypass pool when workspace_dir is set
163
+ _id = self.manager_api.create(
164
+ sandbox_type=SandboxType(self.sandbox_type).value,
165
+ mount_dir=self.workspace_dir,
166
+ # TODO: support bind self-define id
167
+ meta={"session_ctx_id": session_ctx_id},
168
+ )
169
+ else:
170
+ _id = self.manager_api.create_from_pool(
171
+ sandbox_type=SandboxType(self.sandbox_type).value,
172
+ # TODO: support bind self-define id
173
+ meta={"session_ctx_id": session_ctx_id},
174
+ )
175
+
176
+ self._sandbox_id = _id
177
+
178
+ if self._sandbox_id is None:
179
+ raise RuntimeError(
180
+ "No sandbox available. This may happen if: "
181
+ "(1) the sandbox pool is exhausted, "
182
+ "(2) max sandbox instances limit has been reached, or "
183
+ "(3) sandbox container startup failed. ",
184
+ )
110
185
  if self.embed_mode:
111
186
  atexit.register(self._cleanup)
112
187
  self._register_signal_handlers()
@@ -115,6 +190,14 @@ class Sandbox(SandboxBase):
115
190
  def __exit__(self, exc_type, exc_value, traceback):
116
191
  self._cleanup()
117
192
 
193
+ def start(self) -> "Sandbox":
194
+ """Explicitly start sandbox without context manager."""
195
+ return self.__enter__()
196
+
197
+ def close(self) -> None:
198
+ """Explicitly cleanup sandbox without context manager."""
199
+ self.__exit__(None, None, None)
200
+
118
201
  def get_info(self) -> dict:
119
202
  return self.manager_api.get_info(self.sandbox_id)
120
203
 
@@ -143,11 +226,26 @@ class Sandbox(SandboxBase):
143
226
 
144
227
  class SandboxAsync(SandboxBase):
145
228
  async def __aenter__(self):
146
- if self.sandbox_id is None:
147
- self.sandbox_id = await self.manager_api.create_from_pool_async(
148
- sandbox_type=SandboxType(self.sandbox_type).value,
149
- )
150
- if self.sandbox_id is None:
229
+ if self._sandbox_id is None:
230
+ short_uuid = shortuuid.ShortUUID().uuid()
231
+ session_ctx_id = str(short_uuid)
232
+ if self.workspace_dir:
233
+ _id = await self.manager_api.create_async(
234
+ sandbox_type=SandboxType(self.sandbox_type).value,
235
+ mount_dir=self.workspace_dir,
236
+ # TODO: support bind self-define id
237
+ meta={"session_ctx_id": session_ctx_id},
238
+ )
239
+ else:
240
+ _id = await self.manager_api.create_from_pool_async(
241
+ sandbox_type=SandboxType(self.sandbox_type).value,
242
+ # TODO: support bind self-define id
243
+ meta={"session_ctx_id": session_ctx_id},
244
+ )
245
+
246
+ self._sandbox_id = _id
247
+
248
+ if self._sandbox_id is None:
151
249
  raise RuntimeError("No sandbox available.")
152
250
  if self.embed_mode:
153
251
  atexit.register(self._cleanup)
@@ -157,6 +255,14 @@ class SandboxAsync(SandboxBase):
157
255
  async def __aexit__(self, exc_type, exc_value, traceback):
158
256
  await self._cleanup_async()
159
257
 
258
+ async def start_async(self) -> "SandboxAsync":
259
+ """Explicitly start sandbox without async context manager."""
260
+ return await self.__aenter__()
261
+
262
+ async def close_async(self) -> None:
263
+ """Explicitly cleanup sandbox without async context manager."""
264
+ await self.__aexit__(None, None, None)
265
+
160
266
  async def _cleanup_async(self):
161
267
  try:
162
268
  if self.embed_mode:
@@ -171,6 +277,9 @@ class SandboxAsync(SandboxBase):
171
277
  f"\n{traceback.format_exc()}",
172
278
  )
173
279
 
280
+ def get_info(self) -> dict:
281
+ return self.manager_api.get_info(self.sandbox_id)
282
+
174
283
  async def get_info_async(self) -> dict:
175
284
  return await self.manager_api.get_info_async(self.sandbox_id)
176
285
 
@@ -10,6 +10,7 @@ from pydantic import Field
10
10
  from .base import SandboxHttpBase
11
11
  from ..model import ContainerModel
12
12
 
13
+ logging.getLogger("httpx").setLevel(logging.WARNING)
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
@@ -4,7 +4,6 @@ from urllib.parse import urljoin
4
4
 
5
5
  DEFAULT_TIMEOUT = 60
6
6
 
7
- logging.basicConfig(level=logging.INFO)
8
7
  logger = logging.getLogger(__name__)
9
8
 
10
9
 
@@ -13,8 +13,6 @@ from ..model import ContainerModel
13
13
 
14
14
  DEFAULT_TIMEOUT = 60
15
15
 
16
- logging.getLogger("httpx").setLevel(logging.CRITICAL)
17
- logging.basicConfig(level=logging.INFO)
18
16
  logger = logging.getLogger(__name__)
19
17
 
20
18