agentscope-runtime 1.0.3__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 (49) 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 +371 -0
  7. agentscope_runtime/common/container_clients/knative_client.py +466 -0
  8. agentscope_runtime/engine/__init__.py +4 -0
  9. agentscope_runtime/engine/constant.py +1 -0
  10. agentscope_runtime/engine/deployers/__init__.py +12 -0
  11. agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +26 -51
  12. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +19 -10
  13. agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +4 -201
  14. agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +134 -25
  15. agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
  16. agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
  17. agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
  18. agentscope_runtime/engine/runner.py +12 -0
  19. agentscope_runtime/engine/services/agent_state/redis_state_service.py +2 -2
  20. agentscope_runtime/engine/services/memory/redis_memory_service.py +2 -2
  21. agentscope_runtime/engine/services/session_history/redis_session_history_service.py +2 -2
  22. agentscope_runtime/engine/tracing/wrapper.py +18 -4
  23. agentscope_runtime/sandbox/__init__.py +14 -6
  24. agentscope_runtime/sandbox/box/base/__init__.py +2 -2
  25. agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
  26. agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
  27. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
  28. agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
  29. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
  30. agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
  31. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
  32. agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
  33. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
  34. agentscope_runtime/sandbox/box/sandbox.py +98 -65
  35. agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
  36. agentscope_runtime/sandbox/client/__init__.py +6 -1
  37. agentscope_runtime/sandbox/client/async_http_client.py +339 -0
  38. agentscope_runtime/sandbox/client/base.py +74 -0
  39. agentscope_runtime/sandbox/client/http_client.py +108 -329
  40. agentscope_runtime/sandbox/enums.py +7 -0
  41. agentscope_runtime/sandbox/manager/sandbox_manager.py +264 -4
  42. agentscope_runtime/sandbox/manager/server/app.py +7 -1
  43. agentscope_runtime/version.py +1 -1
  44. {agentscope_runtime-1.0.3.dist-info → agentscope_runtime-1.0.4.dist-info}/METADATA +102 -28
  45. {agentscope_runtime-1.0.3.dist-info → agentscope_runtime-1.0.4.dist-info}/RECORD +49 -40
  46. {agentscope_runtime-1.0.3.dist-info → agentscope_runtime-1.0.4.dist-info}/WHEEL +0 -0
  47. {agentscope_runtime-1.0.3.dist-info → agentscope_runtime-1.0.4.dist-info}/entry_points.txt +0 -0
  48. {agentscope_runtime-1.0.3.dist-info → agentscope_runtime-1.0.4.dist-info}/licenses/LICENSE +0 -0
  49. {agentscope_runtime-1.0.3.dist-info → agentscope_runtime-1.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,290 @@
1
+ # -*- coding: utf-8 -*-
2
+ import logging
3
+ import os
4
+ from typing import Optional, Dict, List, Union, Any
5
+
6
+ from pydantic import BaseModel, Field
7
+ from .utils.docker_image_utils import (
8
+ ImageFactory,
9
+ RegistryConfig,
10
+ )
11
+ from .adapter.protocol_adapter import ProtocolAdapter
12
+ from .base import DeployManager
13
+ from ...common.container_clients.knative_client import (
14
+ KnativeClient,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class K8sConfig(BaseModel):
21
+ # Kubernetes settings
22
+ k8s_namespace: Optional[str] = Field(
23
+ "agentscope-runtime",
24
+ description="Kubernetes namespace to deploy KService. ",
25
+ )
26
+ kubeconfig_path: Optional[str] = Field(
27
+ None,
28
+ description="Path to kubeconfig file. If not set, will try "
29
+ "in-cluster config or default kubeconfig.",
30
+ )
31
+
32
+
33
+ class BuildConfig(BaseModel):
34
+ """Build configuration"""
35
+
36
+ build_context_dir: str = "/tmp/k8s_build"
37
+ dockerfile_template: str = None
38
+ build_timeout: int = 600 # 10 minutes
39
+ push_timeout: int = 300 # 5 minutes
40
+ cleanup_after_build: bool = True
41
+
42
+
43
+ class KnativeDeployManager(DeployManager):
44
+ """
45
+ Deploy an AgentScope runner as a Knative Service.
46
+ Requires a Kubernetes cluster with Knative Serving installed.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ kube_config: K8sConfig = None,
52
+ registry_config: RegistryConfig = RegistryConfig(),
53
+ build_context_dir: str = "/tmp/k8s_build",
54
+ ):
55
+ """
56
+ Initialize the Knative deployer.
57
+ """
58
+ super().__init__()
59
+ self.kubeconfig = kube_config
60
+ self.registry_config = registry_config
61
+ self.image_factory = ImageFactory()
62
+ self.build_context_dir = build_context_dir
63
+ self._deployed_resources = {}
64
+ self._built_images = {}
65
+
66
+ self.knative_client = KnativeClient(
67
+ config=self.kubeconfig,
68
+ image_registry=self.registry_config.get_full_url(),
69
+ )
70
+
71
+ async def deploy(
72
+ self,
73
+ app=None,
74
+ runner=None,
75
+ stream: bool = True,
76
+ protocol_adapters: Optional[list[ProtocolAdapter]] = None,
77
+ requirements: Optional[Union[str, List[str]]] = None,
78
+ extra_packages: Optional[List[str]] = None,
79
+ base_image: str = "python:3.9-slim",
80
+ environment: Dict = None,
81
+ runtime_config: Dict = None,
82
+ annotations: Dict = None,
83
+ labels: Dict = None,
84
+ port: int = 8080,
85
+ mount_dir: str = None,
86
+ image_name: str = "agent_llm",
87
+ image_tag: str = "latest",
88
+ push_to_registry: bool = False,
89
+ **kwargs,
90
+ ) -> Dict[str, Any]:
91
+ """
92
+ Deploy the runner as a Knative Service.
93
+
94
+ Args:
95
+ app: Agent app to be deployed
96
+ runner: Complete Runner object with agent, environment_manager,
97
+ context_manager
98
+ stream: Enable streaming responses
99
+ protocol_adapters: protocol adapters
100
+ requirements: PyPI dependencies (following _agent_engines.py
101
+ pattern)
102
+ extra_packages: User code directory/file path
103
+ base_image: Docker base image
104
+ port: Container port
105
+ environment: Environment variables dict
106
+ mount_dir: Mount directory
107
+ runtime_config: K8s runtime configuration
108
+ annotations: knative service annotations
109
+ labels: knative service labels
110
+ # Backward compatibility
111
+ image_name: Image name
112
+ image_tag: Image tag
113
+ push_to_registry: Push to registry
114
+ **kwargs: Additional arguments
115
+
116
+ Returns:
117
+ Dict containing deploy_id, url, resource_name
118
+
119
+ Raises:
120
+ RuntimeError: If kservice fails
121
+
122
+ """
123
+ created_resources = []
124
+ deploy_id = self.deploy_id
125
+ try:
126
+ logger.info(f"Starting Knative Service {deploy_id}")
127
+
128
+ # Step 1: Build image with proper error handling
129
+ logger.info("Building runner image...")
130
+ try:
131
+ built_image_name = self.image_factory.build_image(
132
+ app=app,
133
+ runner=runner,
134
+ base_image=base_image,
135
+ build_context_dir=self.build_context_dir,
136
+ registry_config=self.registry_config,
137
+ image_name=image_name,
138
+ image_tag=image_tag,
139
+ push_to_registry=push_to_registry,
140
+ port=port,
141
+ protocol_adapters=protocol_adapters,
142
+ **kwargs,
143
+ )
144
+ if not built_image_name:
145
+ raise RuntimeError(
146
+ "Image build failed - no image name returned",
147
+ )
148
+
149
+ created_resources.append(f"image:{built_image_name}")
150
+ self._built_images[deploy_id] = built_image_name
151
+ logger.info(f"Image built successfully: {built_image_name}")
152
+ except Exception as e:
153
+ logger.error(f"Image build failed: {e}")
154
+ raise RuntimeError(f"Failed to build image: {e}") from e
155
+
156
+ if mount_dir:
157
+ if not os.path.isabs(mount_dir):
158
+ mount_dir = os.path.abspath(mount_dir)
159
+
160
+ volume_bindings = {
161
+ mount_dir: {
162
+ "bind": mount_dir,
163
+ "mode": "rw",
164
+ },
165
+ }
166
+ else:
167
+ volume_bindings = {}
168
+
169
+ resource_name = self.get_resource_name(deploy_id)
170
+
171
+ logger.info(f"Building Knative Service for {deploy_id}")
172
+
173
+ # Create Knative Service
174
+ name, url = self.knative_client.create_kservice(
175
+ name=resource_name,
176
+ image=built_image_name,
177
+ ports=[port],
178
+ volumes=volume_bindings,
179
+ environment=environment,
180
+ runtime_config=runtime_config or {},
181
+ annotations=annotations or {},
182
+ labels=labels or {},
183
+ )
184
+ if not url:
185
+ import traceback
186
+
187
+ raise RuntimeError(
188
+ f"Failed to create resource: "
189
+ f"{resource_name}, {traceback.format_exc()}",
190
+ )
191
+
192
+ logger.info(f"Knative Service url {url} successful")
193
+ self._deployed_resources[deploy_id] = {
194
+ "resource_name": name,
195
+ "config": {
196
+ "runner": runner.__class__.__name__,
197
+ "extra_packages": extra_packages,
198
+ "requirements": requirements, # New format
199
+ "base_image": base_image,
200
+ "port": port,
201
+ "environment": environment,
202
+ "runtime_config": runtime_config,
203
+ "stream": stream,
204
+ "protocol_adapters": protocol_adapters,
205
+ **kwargs,
206
+ },
207
+ }
208
+ return {
209
+ "deploy_id": deploy_id,
210
+ "resource_name": resource_name,
211
+ "url": url,
212
+ }
213
+
214
+ except Exception as e:
215
+ import traceback
216
+
217
+ logger.error(f"Knative Service {deploy_id} failed: {e}")
218
+ # Enhanced rollback with better error handling
219
+ raise RuntimeError(
220
+ f"Knative Service failed: {e}, {traceback.format_exc()}",
221
+ ) from e
222
+
223
+ @staticmethod
224
+ def get_resource_name(deploy_id: str) -> str:
225
+ return f"agent-{deploy_id[:8]}"
226
+
227
+ async def stop(
228
+ self,
229
+ deploy_id: str,
230
+ **kwargs,
231
+ ) -> Dict[str, Any]:
232
+ """Stop Knative Service.
233
+
234
+ Args:
235
+ deploy_id: Deployment identifier
236
+ **kwargs: Additional parameters
237
+
238
+ Returns:
239
+ Dict with success status, message, and details
240
+ """
241
+
242
+ resource_name = self.get_resource_name(deploy_id)
243
+ try:
244
+ # Try to remove the KService
245
+ success = self.knative_client.delete_kservice(resource_name)
246
+
247
+ if success:
248
+ return {
249
+ "success": True,
250
+ "message": f"Knative deployment {resource_name} "
251
+ f"removed",
252
+ "details": {
253
+ "deploy_id": deploy_id,
254
+ "resource_name": resource_name,
255
+ },
256
+ }
257
+ else:
258
+ return {
259
+ "success": False,
260
+ "message": f"Knative deployment {resource_name} not "
261
+ f"found (may already be deleted), Please check the "
262
+ f"detail in cluster",
263
+ "details": {
264
+ "deploy_id": deploy_id,
265
+ "resource_name": resource_name,
266
+ },
267
+ }
268
+ except Exception as e:
269
+ logger.error(
270
+ f"Failed to remove Knative service {resource_name}: {e}",
271
+ )
272
+ return {
273
+ "success": False,
274
+ "message": f"Failed to remove Knative service: {e}",
275
+ "details": {
276
+ "deploy_id": deploy_id,
277
+ "resource_name": resource_name,
278
+ "error": str(e),
279
+ },
280
+ }
281
+
282
+ def get_status(self) -> str:
283
+ """Get KService status"""
284
+ if self.deploy_id not in self._deployed_resources:
285
+ return "not_found"
286
+
287
+ resources = self._deployed_resources[self.deploy_id]
288
+ kservice_name = resources["resource_name"]
289
+
290
+ return self.knative_client.get_kservice_status(kservice_name)
@@ -272,6 +272,18 @@ class Runner:
272
272
  kwargs.update(
273
273
  {"msgs": await message_to_agno_message(request.input)},
274
274
  )
275
+ elif self.framework_type == "ms_agent_framework":
276
+ from ..adapters.ms_agent_framework.stream import (
277
+ adapt_ms_agent_framework_message_stream,
278
+ )
279
+ from ..adapters.ms_agent_framework.message import (
280
+ message_to_ms_agent_framework_message,
281
+ )
282
+
283
+ stream_adapter = adapt_ms_agent_framework_message_stream
284
+ kwargs.update(
285
+ {"msgs": message_to_ms_agent_framework_message(request.input)},
286
+ )
275
287
  # TODO: support other frameworks
276
288
  else:
277
289
 
@@ -23,7 +23,7 @@ class RedisStateService(StateService):
23
23
  redis_client: Optional[aioredis.Redis] = None,
24
24
  socket_timeout: Optional[float] = 5.0,
25
25
  socket_connect_timeout: Optional[float] = 5.0,
26
- max_connections: Optional[int] = 50,
26
+ max_connections: Optional[int] = None,
27
27
  retry_on_timeout: bool = True,
28
28
  ttl_seconds: Optional[int] = 3600, # 1 hour in seconds
29
29
  health_check_interval: Optional[float] = 30.0,
@@ -39,7 +39,7 @@ class RedisStateService(StateService):
39
39
  socket_connect_timeout: Socket connect timeout in seconds
40
40
  (default: 5.0)
41
41
  max_connections: Maximum number of connections in the pool
42
- (default: 50)
42
+ (default: None)
43
43
  retry_on_timeout: Whether to retry on timeout (default: True)
44
44
  ttl_seconds: Time-to-live in seconds for state data. If None,
45
45
  data never expires (default: 3600, i.e., 1 hour)
@@ -19,7 +19,7 @@ class RedisMemoryService(MemoryService):
19
19
  redis_client: Optional[aioredis.Redis] = None,
20
20
  socket_timeout: Optional[float] = 5.0,
21
21
  socket_connect_timeout: Optional[float] = 5.0,
22
- max_connections: Optional[int] = 50,
22
+ max_connections: Optional[int] = None,
23
23
  retry_on_timeout: bool = True,
24
24
  ttl_seconds: Optional[int] = 3600, # 1 hour in seconds
25
25
  max_messages_per_session: Optional[int] = None,
@@ -36,7 +36,7 @@ class RedisMemoryService(MemoryService):
36
36
  socket_connect_timeout: Socket connect timeout in seconds
37
37
  (default: 5.0)
38
38
  max_connections: Maximum number of connections in the pool
39
- (default: 50)
39
+ (default: None)
40
40
  retry_on_timeout: Whether to retry on timeout (default: True)
41
41
  ttl_seconds: Time-to-live in seconds for memory data.
42
42
  If None, data never expires (default: 3600, i.e., 1 hour)
@@ -17,7 +17,7 @@ class RedisSessionHistoryService(SessionHistoryService):
17
17
  redis_client: Optional[aioredis.Redis] = None,
18
18
  socket_timeout: Optional[float] = 5.0,
19
19
  socket_connect_timeout: Optional[float] = 5.0,
20
- max_connections: Optional[int] = 50,
20
+ max_connections: Optional[int] = None,
21
21
  retry_on_timeout: bool = True,
22
22
  ttl_seconds: Optional[int] = 3600, # 1 hour in seconds
23
23
  max_messages_per_session: Optional[int] = None,
@@ -34,7 +34,7 @@ class RedisSessionHistoryService(SessionHistoryService):
34
34
  socket_connect_timeout: Socket connect timeout in seconds
35
35
  (default: 5.0)
36
36
  max_connections: Maximum number of connections in the pool
37
- (default: 50)
37
+ (default: None)
38
38
  retry_on_timeout: Whether to retry on timeout (default: True)
39
39
  ttl_seconds: Time-to-live in seconds for session data.
40
40
  If None, data never expires (default: 3600, i.e., 1 hour)
@@ -26,7 +26,11 @@ from typing import (
26
26
  from pydantic import BaseModel
27
27
  from opentelemetry.propagate import extract
28
28
  from opentelemetry.context import attach
29
- from opentelemetry.trace import StatusCode, NoOpTracerProvider
29
+ from opentelemetry.trace import (
30
+ ProxyTracerProvider,
31
+ StatusCode,
32
+ NoOpTracerProvider,
33
+ )
30
34
  from opentelemetry import trace as ot_trace
31
35
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
32
36
  OTLPSpanExporter as OTLPSpanGrpcExporter,
@@ -917,12 +921,22 @@ def _get_ot_tracer() -> ot_trace.Tracer:
917
921
  ot_trace.Tracer: The OpenTelemetry tracer instance.
918
922
  """
919
923
 
920
- def _get_ot_tracer_inner() -> ot_trace.Tracer:
924
+ def _has_existing_trace_provider() -> bool:
925
+ from opentelemetry.trace import _TRACER_PROVIDER
926
+
921
927
  existing_provider = ot_trace.get_tracer_provider()
928
+ if isinstance(existing_provider, NoOpTracerProvider):
929
+ return False
930
+ elif isinstance(existing_provider, ProxyTracerProvider):
931
+ # ProxyTracerProvider will use the _TRACER_PROVIDER as real tracer
932
+ # provider to get the tracer
933
+ return bool(_TRACER_PROVIDER)
922
934
 
923
- if not isinstance(existing_provider, NoOpTracerProvider):
924
- return ot_trace.get_tracer("agentscope_runtime")
935
+ return True
925
936
 
937
+ def _get_ot_tracer_inner() -> ot_trace.Tracer:
938
+ if _has_existing_trace_provider():
939
+ return ot_trace.get_tracer("agentscope_runtime")
926
940
  resource = Resource(
927
941
  attributes={
928
942
  SERVICE_NAME: _get_service_name(),
@@ -3,22 +3,30 @@
3
3
  # This ensures SandboxRegistry.register() runs at import time.
4
4
  # Without this, lazy loading delays module import and types may not be
5
5
  # registered.
6
- from .box.base.base_sandbox import BaseSandbox
7
- from .box.browser.browser_sandbox import BrowserSandbox
8
- from .box.filesystem.filesystem_sandbox import FilesystemSandbox
9
- from .box.gui.gui_sandbox import GuiSandbox
6
+ from .box.base.base_sandbox import BaseSandbox, BaseSandboxAsync
7
+ from .box.browser.browser_sandbox import BrowserSandbox, BrowserSandboxAsync
8
+ from .box.filesystem.filesystem_sandbox import (
9
+ FilesystemSandbox,
10
+ FilesystemSandboxAsync,
11
+ )
12
+ from .box.gui.gui_sandbox import GuiSandbox, GuiSandboxAsync
13
+ from .box.mobile.mobile_sandbox import MobileSandbox, MobileSandboxAsync
10
14
  from .box.training_box.training_box import TrainingSandbox
11
15
  from .box.cloud.cloud_sandbox import CloudSandbox
12
- from .box.mobile.mobile_sandbox import MobileSandbox
13
16
  from .box.agentbay.agentbay_sandbox import AgentbaySandbox
14
17
 
15
18
  __all__ = [
16
19
  "BaseSandbox",
20
+ "BaseSandboxAsync",
17
21
  "BrowserSandbox",
22
+ "BrowserSandboxAsync",
18
23
  "FilesystemSandbox",
24
+ "FilesystemSandboxAsync",
19
25
  "GuiSandbox",
26
+ "GuiSandboxAsync",
27
+ "MobileSandbox",
28
+ "MobileSandboxAsync",
20
29
  "TrainingSandbox",
21
30
  "CloudSandbox",
22
- "MobileSandbox",
23
31
  "AgentbaySandbox",
24
32
  ]
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
- from .base_sandbox import BaseSandbox
2
+ from .base_sandbox import BaseSandbox, BaseSandboxAsync
3
3
 
4
- __all__ = ["BaseSandbox"]
4
+ __all__ = ["BaseSandbox", "BaseSandboxAsync"]
@@ -4,7 +4,7 @@ from typing import Optional
4
4
  from ...utils import build_image_uri
5
5
  from ...registry import SandboxRegistry
6
6
  from ...enums import SandboxType
7
- from ...box.sandbox import Sandbox
7
+ from ...box.sandbox import Sandbox, SandboxAsync
8
8
  from ...constant import TIMEOUT
9
9
 
10
10
 
@@ -49,3 +49,53 @@ class BaseSandbox(Sandbox):
49
49
  command (str): Shell command to execute.
50
50
  """
51
51
  return self.call_tool("run_shell_command", {"command": command})
52
+
53
+
54
+ @SandboxRegistry.register(
55
+ build_image_uri("runtime-sandbox-base"),
56
+ sandbox_type=SandboxType.BASE_ASYNC,
57
+ security_level="medium",
58
+ timeout=TIMEOUT,
59
+ description="Base Sandbox (Async)",
60
+ )
61
+ class BaseSandboxAsync(SandboxAsync):
62
+ def __init__(
63
+ self,
64
+ sandbox_id: Optional[str] = None,
65
+ timeout: int = 3000,
66
+ base_url: Optional[str] = None,
67
+ bearer_token: Optional[str] = None,
68
+ sandbox_type: SandboxType = SandboxType.BASE_ASYNC,
69
+ ):
70
+ super().__init__(
71
+ sandbox_id,
72
+ timeout,
73
+ base_url,
74
+ bearer_token,
75
+ sandbox_type,
76
+ )
77
+
78
+ async def run_ipython_cell(self, code: str):
79
+ """
80
+ Run an IPython cell asynchronously.
81
+
82
+ Args:
83
+ code (str): IPython code to execute.
84
+ Returns:
85
+ Any: Response from sandbox execution
86
+ """
87
+ return await self.call_tool_async("run_ipython_cell", {"code": code})
88
+
89
+ async def run_shell_command(self, command: str):
90
+ """
91
+ Run a shell command asynchronously.
92
+
93
+ Args:
94
+ command (str): Shell command to execute.
95
+ Returns:
96
+ Any: Response from sandbox execution
97
+ """
98
+ return await self.call_tool_async(
99
+ "run_shell_command",
100
+ {"command": command},
101
+ )
@@ -1,4 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
- from .browser_sandbox import BrowserSandbox
2
+ from .browser_sandbox import BrowserSandbox, BrowserSandboxAsync
3
3
 
4
- __all__ = ["BrowserSandbox"]
4
+ __all__ = ["BrowserSandbox", "BrowserSandboxAsync"]