agentscope-runtime 0.1.0__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 (131) hide show
  1. agentscope_runtime/__init__.py +4 -0
  2. agentscope_runtime/engine/__init__.py +9 -0
  3. agentscope_runtime/engine/agents/__init__.py +2 -0
  4. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +6 -0
  5. agentscope_runtime/engine/agents/agentscope_agent/agent.py +342 -0
  6. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +156 -0
  7. agentscope_runtime/engine/agents/agno_agent.py +220 -0
  8. agentscope_runtime/engine/agents/base_agent.py +29 -0
  9. agentscope_runtime/engine/agents/langgraph_agent.py +59 -0
  10. agentscope_runtime/engine/agents/llm_agent.py +51 -0
  11. agentscope_runtime/engine/deployers/__init__.py +3 -0
  12. agentscope_runtime/engine/deployers/adapter/__init__.py +0 -0
  13. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +2 -0
  14. agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +425 -0
  15. agentscope_runtime/engine/deployers/adapter/a2a/a2a_agent_adapter.py +69 -0
  16. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +60 -0
  17. agentscope_runtime/engine/deployers/adapter/protocol_adapter.py +24 -0
  18. agentscope_runtime/engine/deployers/base.py +17 -0
  19. agentscope_runtime/engine/deployers/local_deployer.py +586 -0
  20. agentscope_runtime/engine/helpers/helper.py +127 -0
  21. agentscope_runtime/engine/llms/__init__.py +3 -0
  22. agentscope_runtime/engine/llms/base_llm.py +60 -0
  23. agentscope_runtime/engine/llms/qwen_llm.py +47 -0
  24. agentscope_runtime/engine/misc/__init__.py +0 -0
  25. agentscope_runtime/engine/runner.py +186 -0
  26. agentscope_runtime/engine/schemas/__init__.py +0 -0
  27. agentscope_runtime/engine/schemas/agent_schemas.py +551 -0
  28. agentscope_runtime/engine/schemas/context.py +54 -0
  29. agentscope_runtime/engine/services/__init__.py +9 -0
  30. agentscope_runtime/engine/services/base.py +77 -0
  31. agentscope_runtime/engine/services/context_manager.py +129 -0
  32. agentscope_runtime/engine/services/environment_manager.py +50 -0
  33. agentscope_runtime/engine/services/manager.py +174 -0
  34. agentscope_runtime/engine/services/memory_service.py +270 -0
  35. agentscope_runtime/engine/services/sandbox_service.py +198 -0
  36. agentscope_runtime/engine/services/session_history_service.py +256 -0
  37. agentscope_runtime/engine/tracing/__init__.py +40 -0
  38. agentscope_runtime/engine/tracing/base.py +309 -0
  39. agentscope_runtime/engine/tracing/local_logging_handler.py +356 -0
  40. agentscope_runtime/engine/tracing/tracing_metric.py +69 -0
  41. agentscope_runtime/engine/tracing/wrapper.py +321 -0
  42. agentscope_runtime/sandbox/__init__.py +14 -0
  43. agentscope_runtime/sandbox/box/__init__.py +0 -0
  44. agentscope_runtime/sandbox/box/base/__init__.py +0 -0
  45. agentscope_runtime/sandbox/box/base/base_sandbox.py +37 -0
  46. agentscope_runtime/sandbox/box/base/box/__init__.py +0 -0
  47. agentscope_runtime/sandbox/box/browser/__init__.py +0 -0
  48. agentscope_runtime/sandbox/box/browser/box/__init__.py +0 -0
  49. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +176 -0
  50. agentscope_runtime/sandbox/box/dummy/__init__.py +0 -0
  51. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +26 -0
  52. agentscope_runtime/sandbox/box/filesystem/__init__.py +0 -0
  53. agentscope_runtime/sandbox/box/filesystem/box/__init__.py +0 -0
  54. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +87 -0
  55. agentscope_runtime/sandbox/box/sandbox.py +115 -0
  56. agentscope_runtime/sandbox/box/shared/__init__.py +0 -0
  57. agentscope_runtime/sandbox/box/shared/app.py +44 -0
  58. agentscope_runtime/sandbox/box/shared/dependencies/__init__.py +5 -0
  59. agentscope_runtime/sandbox/box/shared/dependencies/deps.py +22 -0
  60. agentscope_runtime/sandbox/box/shared/routers/__init__.py +12 -0
  61. agentscope_runtime/sandbox/box/shared/routers/generic.py +173 -0
  62. agentscope_runtime/sandbox/box/shared/routers/mcp.py +207 -0
  63. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +153 -0
  64. agentscope_runtime/sandbox/box/shared/routers/runtime_watcher.py +187 -0
  65. agentscope_runtime/sandbox/box/shared/routers/workspace.py +325 -0
  66. agentscope_runtime/sandbox/box/training_box/__init__.py +0 -0
  67. agentscope_runtime/sandbox/box/training_box/base.py +120 -0
  68. agentscope_runtime/sandbox/box/training_box/env_service.py +752 -0
  69. agentscope_runtime/sandbox/box/training_box/environments/__init__.py +0 -0
  70. agentscope_runtime/sandbox/box/training_box/environments/appworld/appworld_env.py +987 -0
  71. agentscope_runtime/sandbox/box/training_box/registry.py +54 -0
  72. agentscope_runtime/sandbox/box/training_box/src/trajectory.py +278 -0
  73. agentscope_runtime/sandbox/box/training_box/training_box.py +219 -0
  74. agentscope_runtime/sandbox/build.py +213 -0
  75. agentscope_runtime/sandbox/client/__init__.py +5 -0
  76. agentscope_runtime/sandbox/client/http_client.py +527 -0
  77. agentscope_runtime/sandbox/client/training_client.py +265 -0
  78. agentscope_runtime/sandbox/constant.py +5 -0
  79. agentscope_runtime/sandbox/custom/__init__.py +16 -0
  80. agentscope_runtime/sandbox/custom/custom_sandbox.py +40 -0
  81. agentscope_runtime/sandbox/custom/example.py +37 -0
  82. agentscope_runtime/sandbox/enums.py +68 -0
  83. agentscope_runtime/sandbox/manager/__init__.py +4 -0
  84. agentscope_runtime/sandbox/manager/collections/__init__.py +22 -0
  85. agentscope_runtime/sandbox/manager/collections/base_mapping.py +20 -0
  86. agentscope_runtime/sandbox/manager/collections/base_queue.py +25 -0
  87. agentscope_runtime/sandbox/manager/collections/base_set.py +25 -0
  88. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +22 -0
  89. agentscope_runtime/sandbox/manager/collections/in_memory_queue.py +28 -0
  90. agentscope_runtime/sandbox/manager/collections/in_memory_set.py +27 -0
  91. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +26 -0
  92. agentscope_runtime/sandbox/manager/collections/redis_queue.py +27 -0
  93. agentscope_runtime/sandbox/manager/collections/redis_set.py +23 -0
  94. agentscope_runtime/sandbox/manager/container_clients/__init__.py +8 -0
  95. agentscope_runtime/sandbox/manager/container_clients/base_client.py +39 -0
  96. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +170 -0
  97. agentscope_runtime/sandbox/manager/sandbox_manager.py +694 -0
  98. agentscope_runtime/sandbox/manager/server/__init__.py +0 -0
  99. agentscope_runtime/sandbox/manager/server/app.py +194 -0
  100. agentscope_runtime/sandbox/manager/server/config.py +68 -0
  101. agentscope_runtime/sandbox/manager/server/models.py +17 -0
  102. agentscope_runtime/sandbox/manager/storage/__init__.py +10 -0
  103. agentscope_runtime/sandbox/manager/storage/data_storage.py +16 -0
  104. agentscope_runtime/sandbox/manager/storage/local_storage.py +44 -0
  105. agentscope_runtime/sandbox/manager/storage/oss_storage.py +89 -0
  106. agentscope_runtime/sandbox/manager/utils.py +78 -0
  107. agentscope_runtime/sandbox/mcp_server.py +192 -0
  108. agentscope_runtime/sandbox/model/__init__.py +12 -0
  109. agentscope_runtime/sandbox/model/api.py +16 -0
  110. agentscope_runtime/sandbox/model/container.py +72 -0
  111. agentscope_runtime/sandbox/model/manager_config.py +158 -0
  112. agentscope_runtime/sandbox/registry.py +129 -0
  113. agentscope_runtime/sandbox/tools/__init__.py +12 -0
  114. agentscope_runtime/sandbox/tools/base/__init__.py +8 -0
  115. agentscope_runtime/sandbox/tools/base/tool.py +52 -0
  116. agentscope_runtime/sandbox/tools/browser/__init__.py +57 -0
  117. agentscope_runtime/sandbox/tools/browser/tool.py +597 -0
  118. agentscope_runtime/sandbox/tools/filesystem/__init__.py +32 -0
  119. agentscope_runtime/sandbox/tools/filesystem/tool.py +319 -0
  120. agentscope_runtime/sandbox/tools/function_tool.py +321 -0
  121. agentscope_runtime/sandbox/tools/mcp_tool.py +191 -0
  122. agentscope_runtime/sandbox/tools/sandbox_tool.py +104 -0
  123. agentscope_runtime/sandbox/tools/tool.py +123 -0
  124. agentscope_runtime/sandbox/tools/utils.py +68 -0
  125. agentscope_runtime/version.py +2 -0
  126. agentscope_runtime-0.1.0.dist-info/METADATA +327 -0
  127. agentscope_runtime-0.1.0.dist-info/RECORD +131 -0
  128. agentscope_runtime-0.1.0.dist-info/WHEEL +5 -0
  129. agentscope_runtime-0.1.0.dist-info/entry_points.txt +4 -0
  130. agentscope_runtime-0.1.0.dist-info/licenses/LICENSE +202 -0
  131. agentscope_runtime-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,694 @@
1
+ # -*- coding: utf-8 -*-
2
+ # pylint: disable=redefined-outer-name, protected-access, too-many-branches
3
+ import logging
4
+ import os
5
+ import secrets
6
+ import inspect
7
+ import socket
8
+ import traceback
9
+
10
+ from functools import wraps
11
+ from typing import Optional, Dict
12
+ from uuid import uuid4
13
+
14
+ import requests
15
+
16
+ from ..model import (
17
+ ContainerModel,
18
+ SandboxManagerEnvConfig,
19
+ DEFAULT_LOCAL_MANAGER_CONFIG,
20
+ )
21
+ from ..enums import SandboxType
22
+ from ..registry import SandboxRegistry
23
+ from ..client import SandboxHttpClient, TrainingSandboxClient
24
+
25
+ from ..manager.utils import is_port_available, sweep_port
26
+ from ..manager.collections import (
27
+ RedisMapping,
28
+ RedisSetCollection,
29
+ RedisQueue,
30
+ InMemoryMapping,
31
+ InMemoryQueue,
32
+ InMemorySetCollection,
33
+ )
34
+ from ..manager.storage import (
35
+ LocalStorage,
36
+ OSSStorage,
37
+ )
38
+ from ..manager.container_clients import DockerClient
39
+ from ..constant import BROWSER_SESSION_ID
40
+
41
+ logging.basicConfig(level=logging.INFO)
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ def remote_wrapper(
46
+ method: str = "POST",
47
+ success_key: str = "data",
48
+ ):
49
+ """
50
+ Decorator to handle both remote and local method execution.
51
+ """
52
+
53
+ def decorator(func):
54
+ @wraps(func)
55
+ def wrapper(self, *args, **kwargs):
56
+ if not self.http_session:
57
+ # Execute the original function locally
58
+ return func(self, *args, **kwargs)
59
+
60
+ endpoint = "/" + func.__name__
61
+
62
+ # Prepare data for remote call
63
+ sig = inspect.signature(func)
64
+ param_names = list(sig.parameters.keys())[1:] # Skip 'self'
65
+ data = dict(zip(param_names, args))
66
+ data.update(kwargs)
67
+
68
+ # Make the remote HTTP request
69
+ response = self._make_request(method, endpoint, data)
70
+
71
+ # Process response
72
+ if success_key:
73
+ return response.get(success_key)
74
+ return response
75
+
76
+ wrapper._is_remote_wrapper = True
77
+ wrapper._http_method = method
78
+ wrapper._path = "/" + func.__name__
79
+
80
+ return wrapper
81
+
82
+ return decorator
83
+
84
+
85
+ class SandboxManager:
86
+ def __init__(
87
+ self,
88
+ config: SandboxManagerEnvConfig = DEFAULT_LOCAL_MANAGER_CONFIG,
89
+ base_url=None,
90
+ bearer_token=None,
91
+ default_type: SandboxType | str = SandboxType.BASE,
92
+ ):
93
+ if base_url:
94
+ # Initialize HTTP session for remote mode with bearer token
95
+ # authentication
96
+ self.http_session = requests.Session()
97
+ self.http_session.timeout = 30
98
+ self.base_url = base_url.rstrip("/")
99
+ if bearer_token:
100
+ self.http_session.headers.update(
101
+ {"Authorization": f"Bearer {bearer_token}"},
102
+ )
103
+ else:
104
+ self.http_session = None
105
+ self.base_url = None
106
+
107
+ self.default_type = SandboxType(default_type)
108
+ self.workdir = "/workspace"
109
+
110
+ self.config = config
111
+ self.pool_size = self.config.pool_size
112
+ self.prefix = self.config.container_prefix_key
113
+ self.default_mount_dir = (
114
+ self.config.default_mount_dir or "sessions_mount_dir"
115
+ )
116
+ self.storage_folder = (
117
+ self.config.storage_folder or self.default_mount_dir
118
+ )
119
+
120
+ if self.config.redis_enabled:
121
+ import redis
122
+
123
+ redis_client = redis.Redis(
124
+ host=self.config.redis_server,
125
+ port=self.config.redis_port,
126
+ db=self.config.redis_db,
127
+ username=self.config.redis_user,
128
+ password=self.config.redis_password,
129
+ decode_responses=True,
130
+ )
131
+ try:
132
+ redis_client.ping()
133
+ except ConnectionError as e:
134
+ raise RuntimeError(
135
+ "Unable to connect to the Redis server.",
136
+ ) from e
137
+
138
+ self.container_mapping = RedisMapping(redis_client)
139
+ self.port_set = RedisSetCollection(
140
+ redis_client,
141
+ set_name=self.config.redis_port_key,
142
+ )
143
+ self.pool_queue = RedisQueue(
144
+ redis_client,
145
+ self.config.redis_container_pool_key,
146
+ )
147
+ else:
148
+ self.container_mapping = InMemoryMapping()
149
+ self.port_set = InMemorySetCollection()
150
+ self.pool_queue = InMemoryQueue()
151
+
152
+ self.container_deployment = self.config.container_deployment
153
+
154
+ if base_url is None:
155
+ if self.container_deployment == "docker":
156
+ self.client = DockerClient()
157
+ else:
158
+ # TODO: support k8s deployment
159
+ raise NotImplementedError("Not implemented")
160
+ else:
161
+ self.client = None
162
+
163
+ self.port_range = range(*self.config.port_range)
164
+
165
+ self.file_system = self.config.file_system
166
+ if self.file_system == "oss":
167
+ self.storage = OSSStorage(
168
+ self.config.oss_access_key_id,
169
+ self.config.oss_access_key_secret,
170
+ self.config.oss_endpoint,
171
+ self.config.oss_bucket_name,
172
+ )
173
+ else:
174
+ self.storage = LocalStorage()
175
+
176
+ if self.pool_size > 0:
177
+ self._init_container_pool()
178
+
179
+ logger.debug(str(config))
180
+
181
+ def __enter__(self):
182
+ logger.debug(
183
+ "Entering SandboxManager context (sync). "
184
+ "Cleanup will be performed automatically on exit.",
185
+ )
186
+ return self
187
+
188
+ def __exit__(self, exc_type, exc_value, traceback):
189
+ logger.debug(
190
+ "Exiting SandboxManager context (sync). Cleaning up resources.",
191
+ )
192
+ self.cleanup()
193
+
194
+ def _generate_container_key(self, session_id):
195
+ return f"{self.prefix}{session_id}"
196
+
197
+ def _make_request(self, method: str, endpoint: str, data: dict):
198
+ """
199
+ Make an HTTP request to the specified endpoint.
200
+ """
201
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
202
+ if method.upper() == "GET":
203
+ response = self.http_session.get(url, params=data)
204
+ else:
205
+ response = self.http_session.request(method, url, json=data)
206
+
207
+ try:
208
+ response.raise_for_status()
209
+ except requests.exceptions.HTTPError as e:
210
+ logger.error(f"Error making request: {e}")
211
+ return {"data": f"Error: {e}"}
212
+
213
+ return response.json()
214
+
215
+ def _init_container_pool(self):
216
+ """
217
+ Init runtime pool
218
+ """
219
+ while self.pool_queue.size() < self.pool_size:
220
+ try:
221
+ container_name = self.create()
222
+ container_model = self.container_mapping.get(container_name)
223
+ if container_model:
224
+ # Check the pool size again to avoid race condition
225
+ if self.pool_queue.size() < self.pool_size:
226
+ self.pool_queue.enqueue(container_model)
227
+ else:
228
+ # The pool size has reached the limit
229
+ self.release(container_name)
230
+ break
231
+ else:
232
+ logger.error("Failed to create container for pool")
233
+ break
234
+ except Exception as e:
235
+ logger.error(f"Error initializing runtime pool: {e}")
236
+ break
237
+
238
+ def _find_free_ports(self, n):
239
+ free_ports = []
240
+
241
+ for port in self.port_range:
242
+ if len(free_ports) >= n:
243
+ break # We have found enough ports
244
+
245
+ if not self.port_set.add(port):
246
+ continue
247
+
248
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
249
+ try:
250
+ s.bind(("", port))
251
+ free_ports.append(port) # Port is available
252
+
253
+ except OSError:
254
+ # Bind failed, port is in use
255
+ self.port_set.remove(port)
256
+ # Try the next one
257
+ continue
258
+
259
+ if len(free_ports) < n:
260
+ raise RuntimeError(
261
+ "Not enough free ports available in the specified range.",
262
+ )
263
+
264
+ return free_ports
265
+
266
+ @remote_wrapper()
267
+ def cleanup(self):
268
+ logger.debug(
269
+ "Cleaning up resources.",
270
+ )
271
+
272
+ # Clean up pool first
273
+ try:
274
+ while self.pool_queue.size() > 0:
275
+ container_json = self.pool_queue.dequeue()
276
+ if container_json:
277
+ container_model = ContainerModel(**container_json)
278
+ logger.debug(
279
+ f"Destroy container {container_model.container_id}",
280
+ )
281
+ self.release(container_model.session_id)
282
+ except Exception as e:
283
+ logger.error(f"Error cleaning up runtime pool: {e}")
284
+
285
+ # Clean up rest container
286
+ for key in self.container_mapping.scan(self.prefix):
287
+ try:
288
+ container_json = self.container_mapping.get(key)
289
+ if container_json:
290
+ container_model = ContainerModel(**container_json)
291
+ logger.debug(
292
+ f"Destroy container {container_model.container_id}",
293
+ )
294
+ self.release(container_model.session_id)
295
+ except Exception as e:
296
+ logger.error(
297
+ f"Error cleaning up container {key}: {e}",
298
+ )
299
+
300
+ @remote_wrapper()
301
+ def create_from_pool(self, sandbox_type=None):
302
+ """Try to get a container from runtime pool"""
303
+ sandbox_type = SandboxType(sandbox_type)
304
+ if sandbox_type != self.default_type:
305
+ return self.create(sandbox_type=sandbox_type.value)
306
+
307
+ cnt = 0
308
+ try:
309
+ while True:
310
+ if cnt > self.pool_size:
311
+ raise RuntimeError(
312
+ "No container available in pool after check the pool.",
313
+ )
314
+ cnt += 1
315
+
316
+ # Add a new one to container
317
+ container_name = self.create()
318
+ new_container_model = self.container_mapping.get(
319
+ container_name,
320
+ )
321
+
322
+ if new_container_model:
323
+ self.pool_queue.enqueue(
324
+ new_container_model,
325
+ )
326
+
327
+ container_json = self.pool_queue.dequeue()
328
+
329
+ if not container_json:
330
+ raise RuntimeError(
331
+ "No container available in pool.",
332
+ )
333
+
334
+ container_model = ContainerModel(**container_json)
335
+ logger.debug(
336
+ f"Retrieved container from pool:"
337
+ f" {container_model.session_id}",
338
+ )
339
+
340
+ if (
341
+ container_model.version
342
+ != SandboxRegistry.get_image_by_type(
343
+ self.default_type,
344
+ )
345
+ ):
346
+ logger.warning(
347
+ f"Container {container_model.session_id} outdated, "
348
+ f"trying next one in pool",
349
+ )
350
+ self.release(container_model.session_id)
351
+ continue
352
+
353
+ if self.client.inspect(container_model.container_id) is None:
354
+ logger.warning(
355
+ f"Container {container_model.container_id} not found "
356
+ f"or unexpected error happens.",
357
+ )
358
+ continue
359
+
360
+ if (
361
+ self.client.get_status(container_model.container_id)
362
+ == "running"
363
+ ):
364
+ return container_model.container_name
365
+ else:
366
+ logger.error(
367
+ f"Container {container_model.container_id} is not "
368
+ f"running. Trying next one in pool.",
369
+ )
370
+ # Destroy the stopped container
371
+ self.release(container_model.session_id)
372
+
373
+ except Exception as e:
374
+ logger.error(
375
+ f"Error getting container from pool, create a "
376
+ f"new one. {e}: {traceback.format_exc()}",
377
+ )
378
+ return self.create()
379
+
380
+ @remote_wrapper()
381
+ def create(
382
+ self,
383
+ sandbox_type=None,
384
+ mount_dir=None,
385
+ storage_path=None,
386
+ environment: Optional[Dict] = None,
387
+ ):
388
+ if sandbox_type is not None:
389
+ target_sandbox_type = SandboxType(sandbox_type)
390
+ else:
391
+ target_sandbox_type = self.default_type
392
+
393
+ image = SandboxRegistry.get_image_by_type(target_sandbox_type)
394
+ if not image:
395
+ logger.warning(
396
+ f"No image found for sandbox {target_sandbox_type}, "
397
+ f"using default",
398
+ )
399
+ image = SandboxRegistry.get_image_by_type(
400
+ self.default_type,
401
+ )
402
+
403
+ # TODO: enable for timeout for the sandbox (auto cleanup)
404
+ config = SandboxRegistry.get_config_by_type(target_sandbox_type)
405
+ environment = {
406
+ **(config.environment if config.environment else {}),
407
+ **(environment if environment else {}),
408
+ }
409
+
410
+ for key, value in environment.items():
411
+ if value is None:
412
+ logger.error(
413
+ f"Env variable {key} is None.",
414
+ )
415
+ return None
416
+
417
+ session_id = str(uuid4())
418
+
419
+ if mount_dir is None:
420
+ mount_dir = os.path.join(self.default_mount_dir, session_id)
421
+ os.makedirs(mount_dir, exist_ok=True)
422
+
423
+ if not os.path.isabs(mount_dir):
424
+ mount_dir = os.path.abspath(mount_dir)
425
+
426
+ if storage_path is None:
427
+ storage_path = self.storage.path_join(
428
+ self.storage_folder,
429
+ session_id,
430
+ )
431
+
432
+ self.storage.download_folder(storage_path, mount_dir)
433
+
434
+ try:
435
+ # Check for an existing container with the same name
436
+ container_name = self._generate_container_key(session_id)
437
+ if self.client.inspect(container_name):
438
+ raise ValueError(
439
+ f"Container with name {container_name} already exists.",
440
+ )
441
+
442
+ free_ports = self._find_free_ports(1)
443
+
444
+ ports = {
445
+ "80/tcp": free_ports[0], # nginx
446
+ }
447
+
448
+ # Generate a random secret token
449
+ runtime_token = secrets.token_hex(16)
450
+
451
+ # Prepare volume bindings if a mount directory is provided
452
+ volume_bindings = {
453
+ mount_dir: {
454
+ "bind": self.workdir,
455
+ "mode": "rw",
456
+ },
457
+ }
458
+
459
+ if not self.client.create(
460
+ image,
461
+ name=container_name,
462
+ ports=ports,
463
+ volumes=volume_bindings,
464
+ environment={
465
+ "SECRET_TOKEN": runtime_token,
466
+ **environment,
467
+ },
468
+ runtime_config=config.runtime_config,
469
+ ):
470
+ return None
471
+
472
+ # Check the container status
473
+ status = self.client.get_status(container_name)
474
+ if self.client.get_status(container_name) != "running":
475
+ logger.warning(
476
+ f"Container {container_name} is not running. Current "
477
+ f"status: {status}",
478
+ )
479
+ return None
480
+
481
+ # Build the ContainerModel
482
+ container_attrs = self.client.inspect(container_name)
483
+
484
+ container_model = ContainerModel(
485
+ session_id=session_id,
486
+ container_id=container_attrs["Id"], # Docker id pattern
487
+ container_name=container_name,
488
+ base_url=f"http://localhost:{ports['80/tcp']}/fastapi",
489
+ browser_url=f"http://localhost:{ports['80/tcp']}/steel-api"
490
+ f"/{runtime_token}",
491
+ front_browser_ws=f"ws://localhost:"
492
+ f"{ports['80/tcp']}/steel-api/"
493
+ f"{runtime_token}/v1/sessions/cast",
494
+ client_browser_ws=f"ws://localhost:"
495
+ f"{ports['80/tcp']}/steel-api/{runtime_token}/&sessionId"
496
+ f"={BROWSER_SESSION_ID}",
497
+ artifacts_sio=f"http://localhost" f":{ports['80/tcp']}/v1",
498
+ ports=free_ports,
499
+ mount_dir=str(mount_dir),
500
+ storage_path=storage_path,
501
+ runtime_token=runtime_token,
502
+ version=image,
503
+ )
504
+ # Register in mapping
505
+ self.container_mapping.set(
506
+ container_model.container_name,
507
+ container_model.model_dump(),
508
+ )
509
+
510
+ logger.debug(
511
+ f"Created container {container_name}"
512
+ f":{container_model.model_dump()}",
513
+ )
514
+ return container_name
515
+ except Exception as e:
516
+ logger.error(
517
+ f"Failed to create container: {e}: {traceback.format_exc()}",
518
+ )
519
+ return None
520
+
521
+ @remote_wrapper()
522
+ def release(self, identity):
523
+ try:
524
+ container_json = self.get_info(identity)
525
+
526
+ if not container_json:
527
+ logger.warning(
528
+ f"No container found for {identity}.",
529
+ )
530
+ return True
531
+
532
+ container_info = ContainerModel(**container_json)
533
+
534
+ # remove key in mapping before we remove container
535
+ self.container_mapping.delete(container_json.get("container_name"))
536
+
537
+ self.client.stop(container_info.container_id, timeout=1)
538
+ self.client.remove(container_info.container_id, force=True)
539
+
540
+ # Release ports after the container is removed
541
+ for port in container_info.ports:
542
+ self.port_set.remove(port)
543
+
544
+ logger.debug(f"Container for {identity} destroyed.")
545
+
546
+ # Upload to storage
547
+ self.storage.upload_folder(
548
+ container_info.mount_dir,
549
+ container_info.storage_path,
550
+ )
551
+
552
+ return True
553
+ except Exception as e:
554
+ logger.error(
555
+ f"Failed to destroy container: {e}: "
556
+ f"{traceback.format_exc()}",
557
+ )
558
+ return False
559
+
560
+ @remote_wrapper()
561
+ def start(self, identity):
562
+ try:
563
+ container_json = self.get_info(identity)
564
+
565
+ if not container_json:
566
+ logger.warning(
567
+ f"No container found for {identity}.",
568
+ )
569
+ return False
570
+
571
+ container_info = ContainerModel(**container_json)
572
+
573
+ # Check whether the ports are occupied by other processes
574
+ for port in container_info.ports:
575
+ if is_port_available(port):
576
+ continue
577
+
578
+ # If the port is occupied, sweep it
579
+ sweep_port(port)
580
+
581
+ self.client.start(container_info.container_id)
582
+ status = self.client.get_status(container_info.container_id)
583
+ if status != "running":
584
+ logger.error(
585
+ f"Failed to start container {identity}. "
586
+ f"Current status: {status}",
587
+ )
588
+ return False
589
+
590
+ logger.debug(f"Container {identity} started.")
591
+ return True
592
+
593
+ except Exception as e:
594
+ logger.error(
595
+ f"Failed to start container: {e}:"
596
+ f" {traceback.format_exc()}",
597
+ )
598
+ return False
599
+
600
+ @remote_wrapper()
601
+ def stop(self, identity):
602
+ try:
603
+ container_json = self.get_info(identity)
604
+
605
+ if not container_json:
606
+ logger.warning(f"No container found for {identity}.")
607
+ return True
608
+
609
+ container_info = ContainerModel(**container_json)
610
+
611
+ self.client.stop(container_info.container_id, timeout=1)
612
+
613
+ status = self.client.get_status(container_info.container_id)
614
+ if status != "exited":
615
+ logger.error(
616
+ f"Failed to stop container {identity}. "
617
+ f"Current status: {status}",
618
+ )
619
+ return False
620
+
621
+ logger.debug(f"Container {identity} stopped.")
622
+ return True
623
+
624
+ except Exception as e:
625
+ logger.error(
626
+ f"Failed to stop container: {e}: {traceback.format_exc()}",
627
+ )
628
+ return False
629
+
630
+ @remote_wrapper()
631
+ def get_status(self, identity):
632
+ """Get container status by container_name or container_id."""
633
+ return self.client.get_status(identity)
634
+
635
+ @remote_wrapper()
636
+ def get_info(self, identity):
637
+ """Get container information by container_name or container_id."""
638
+ container_model = self.container_mapping.get(identity)
639
+ if container_model is None:
640
+ container_model = self.container_mapping.get(
641
+ self._generate_container_key(identity),
642
+ )
643
+ if container_model is None:
644
+ return None
645
+ if hasattr(container_model, "model_dump_json"):
646
+ container_model = container_model.model_dump_json()
647
+
648
+ return container_model
649
+
650
+ def _establish_connection(self, identity):
651
+ container_model = ContainerModel(**self.get_info(identity))
652
+ # TODO: make this more robust
653
+ enable_browser = "browser" in container_model.version
654
+
655
+ # TODO: remake docker name
656
+ if "appworld" in container_model.version:
657
+ return TrainingSandboxClient(
658
+ base_url=f"http://localhost:{container_model.ports[0]}",
659
+ ).__enter__()
660
+
661
+ return SandboxHttpClient(
662
+ container_model,
663
+ enable_browser=enable_browser,
664
+ ).__enter__()
665
+
666
+ @remote_wrapper()
667
+ def list_tools(self, identity, tool_type=None, **kwargs):
668
+ """List tool"""
669
+ client = self._establish_connection(identity)
670
+ return client.list_tools(tool_type=tool_type, **kwargs)
671
+
672
+ @remote_wrapper()
673
+ def call_tool(self, identity, tool_name=None, arguments=None):
674
+ """Call tool"""
675
+ client = self._establish_connection(identity)
676
+ return client.call_tool(tool_name, arguments)
677
+
678
+ @remote_wrapper()
679
+ def add_mcp_servers(self, identity, server_configs, overwrite=False):
680
+ """
681
+ Add MCP servers to runtime.
682
+ """
683
+ client = self._establish_connection(identity)
684
+ return client.add_mcp_servers(
685
+ server_configs=server_configs,
686
+ overwrite=overwrite,
687
+ )
688
+
689
+
690
+ if __name__ == "__main__":
691
+ with SandboxManager() as manager:
692
+ name = manager.create("12345")
693
+ if name:
694
+ print(f"Created container: {name}")
File without changes