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.
- agentscope_runtime/__init__.py +3 -0
- agentscope_runtime/adapters/agentscope/message.py +36 -295
- agentscope_runtime/adapters/agentscope/stream.py +89 -2
- agentscope_runtime/adapters/agno/message.py +11 -2
- agentscope_runtime/adapters/agno/stream.py +1 -0
- agentscope_runtime/adapters/langgraph/__init__.py +1 -3
- agentscope_runtime/adapters/langgraph/message.py +11 -106
- agentscope_runtime/adapters/langgraph/stream.py +1 -0
- agentscope_runtime/adapters/ms_agent_framework/message.py +11 -1
- agentscope_runtime/adapters/ms_agent_framework/stream.py +1 -0
- agentscope_runtime/adapters/text/stream.py +1 -0
- agentscope_runtime/common/container_clients/agentrun_client.py +0 -3
- agentscope_runtime/common/container_clients/boxlite_client.py +26 -15
- agentscope_runtime/common/container_clients/fc_client.py +0 -11
- agentscope_runtime/common/utils/deprecation.py +14 -17
- agentscope_runtime/common/utils/logging.py +44 -0
- agentscope_runtime/engine/app/agent_app.py +5 -5
- agentscope_runtime/engine/app/celery_mixin.py +43 -4
- agentscope_runtime/engine/deployers/adapter/agui/__init__.py +8 -1
- agentscope_runtime/engine/deployers/adapter/agui/agui_adapter_utils.py +6 -1
- agentscope_runtime/engine/deployers/adapter/agui/agui_protocol_adapter.py +2 -2
- agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +13 -0
- agentscope_runtime/engine/runner.py +31 -6
- agentscope_runtime/engine/schemas/agent_schemas.py +28 -0
- agentscope_runtime/engine/services/sandbox/sandbox_service.py +41 -9
- agentscope_runtime/sandbox/box/base/base_sandbox.py +4 -0
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +4 -0
- agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +9 -2
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +4 -0
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +5 -1
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +4 -0
- agentscope_runtime/sandbox/box/sandbox.py +122 -13
- agentscope_runtime/sandbox/client/async_http_client.py +1 -0
- agentscope_runtime/sandbox/client/base.py +0 -1
- agentscope_runtime/sandbox/client/http_client.py +0 -2
- agentscope_runtime/sandbox/manager/heartbeat_mixin.py +486 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +740 -153
- agentscope_runtime/sandbox/manager/server/app.py +18 -11
- agentscope_runtime/sandbox/manager/server/config.py +10 -2
- agentscope_runtime/sandbox/mcp_server.py +0 -1
- agentscope_runtime/sandbox/model/__init__.py +2 -1
- agentscope_runtime/sandbox/model/container.py +90 -3
- agentscope_runtime/sandbox/model/manager_config.py +45 -1
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/METADATA +36 -54
- {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/RECORD +50 -69
- agentscope_runtime/adapters/agentscope/long_term_memory/__init__.py +0 -6
- agentscope_runtime/adapters/agentscope/long_term_memory/_long_term_memory_adapter.py +0 -258
- agentscope_runtime/adapters/agentscope/memory/__init__.py +0 -6
- agentscope_runtime/adapters/agentscope/memory/_memory_adapter.py +0 -152
- agentscope_runtime/engine/services/agent_state/__init__.py +0 -25
- agentscope_runtime/engine/services/agent_state/redis_state_service.py +0 -166
- agentscope_runtime/engine/services/agent_state/state_service.py +0 -179
- agentscope_runtime/engine/services/agent_state/state_service_factory.py +0 -52
- agentscope_runtime/engine/services/memory/__init__.py +0 -33
- agentscope_runtime/engine/services/memory/mem0_memory_service.py +0 -128
- agentscope_runtime/engine/services/memory/memory_service.py +0 -292
- agentscope_runtime/engine/services/memory/memory_service_factory.py +0 -126
- agentscope_runtime/engine/services/memory/redis_memory_service.py +0 -290
- agentscope_runtime/engine/services/memory/reme_personal_memory_service.py +0 -109
- agentscope_runtime/engine/services/memory/reme_task_memory_service.py +0 -11
- agentscope_runtime/engine/services/memory/tablestore_memory_service.py +0 -301
- agentscope_runtime/engine/services/session_history/__init__.py +0 -32
- agentscope_runtime/engine/services/session_history/redis_session_history_service.py +0 -283
- agentscope_runtime/engine/services/session_history/session_history_service.py +0 -267
- agentscope_runtime/engine/services/session_history/session_history_service_factory.py +0 -73
- agentscope_runtime/engine/services/session_history/tablestore_session_history_service.py +0 -288
- {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/WHEEL +0 -0
- {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.5.post1.dist-info → agentscope_runtime-1.1.0b2.dist-info}/licenses/LICENSE +0 -0
- {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("
|
|
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"
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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__(
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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()
|
|
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=
|
|
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.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|