aip-agents-binary 0.5.25b9__py3-none-any.whl → 0.6.1__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.

Potentially problematic release.


This version of aip-agents-binary might be problematic. Click here for more details.

Files changed (73) hide show
  1. aip_agents/agent/base_langgraph_agent.py +137 -68
  2. aip_agents/agent/base_langgraph_agent.pyi +3 -2
  3. aip_agents/agent/langgraph_react_agent.py +252 -16
  4. aip_agents/agent/langgraph_react_agent.pyi +40 -1
  5. aip_agents/examples/compare_streaming_client.py +2 -2
  6. aip_agents/examples/compare_streaming_server.py +1 -1
  7. aip_agents/examples/hello_world_ptc.py +51 -0
  8. aip_agents/examples/hello_world_ptc.pyi +5 -0
  9. aip_agents/examples/hello_world_tool_output_client.py +9 -0
  10. aip_agents/examples/todolist_planning_a2a_langchain_client.py +2 -2
  11. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +1 -1
  12. aip_agents/guardrails/engines/base.py +6 -6
  13. aip_agents/mcp/client/connection_manager.py +36 -1
  14. aip_agents/mcp/client/connection_manager.pyi +3 -0
  15. aip_agents/mcp/client/persistent_session.py +318 -68
  16. aip_agents/mcp/client/persistent_session.pyi +9 -0
  17. aip_agents/mcp/client/transports.py +33 -2
  18. aip_agents/mcp/client/transports.pyi +9 -0
  19. aip_agents/ptc/__init__.py +48 -0
  20. aip_agents/ptc/__init__.pyi +10 -0
  21. aip_agents/ptc/doc_gen.py +122 -0
  22. aip_agents/ptc/doc_gen.pyi +40 -0
  23. aip_agents/ptc/exceptions.py +39 -0
  24. aip_agents/ptc/exceptions.pyi +22 -0
  25. aip_agents/ptc/executor.py +143 -0
  26. aip_agents/ptc/executor.pyi +73 -0
  27. aip_agents/ptc/mcp/__init__.py +45 -0
  28. aip_agents/ptc/mcp/__init__.pyi +7 -0
  29. aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
  30. aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
  31. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  32. aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
  33. aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
  34. aip_agents/ptc/naming.py +184 -0
  35. aip_agents/ptc/naming.pyi +76 -0
  36. aip_agents/ptc/payload.py +26 -0
  37. aip_agents/ptc/payload.pyi +15 -0
  38. aip_agents/ptc/prompt_builder.py +571 -0
  39. aip_agents/ptc/prompt_builder.pyi +55 -0
  40. aip_agents/ptc/ptc_helper.py +16 -0
  41. aip_agents/ptc/ptc_helper.pyi +1 -0
  42. aip_agents/ptc/sandbox_bridge.py +58 -0
  43. aip_agents/ptc/sandbox_bridge.pyi +25 -0
  44. aip_agents/ptc/template_utils.py +33 -0
  45. aip_agents/ptc/template_utils.pyi +13 -0
  46. aip_agents/ptc/templates/__init__.py +1 -0
  47. aip_agents/ptc/templates/__init__.pyi +0 -0
  48. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  49. aip_agents/sandbox/__init__.py +43 -0
  50. aip_agents/sandbox/__init__.pyi +5 -0
  51. aip_agents/sandbox/defaults.py +9 -0
  52. aip_agents/sandbox/defaults.pyi +2 -0
  53. aip_agents/sandbox/e2b_runtime.py +267 -0
  54. aip_agents/sandbox/e2b_runtime.pyi +51 -0
  55. aip_agents/sandbox/template_builder.py +131 -0
  56. aip_agents/sandbox/template_builder.pyi +36 -0
  57. aip_agents/sandbox/types.py +24 -0
  58. aip_agents/sandbox/types.pyi +14 -0
  59. aip_agents/sandbox/validation.py +50 -0
  60. aip_agents/sandbox/validation.pyi +20 -0
  61. aip_agents/tools/__init__.py +2 -0
  62. aip_agents/tools/__init__.pyi +2 -1
  63. aip_agents/tools/browser_use/browser_use_tool.py +8 -0
  64. aip_agents/tools/browser_use/streaming.py +2 -0
  65. aip_agents/tools/execute_ptc_code.py +305 -0
  66. aip_agents/tools/execute_ptc_code.pyi +87 -0
  67. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
  68. aip_agents/utils/langgraph/tool_output_management.py +80 -0
  69. aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
  70. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/METADATA +51 -48
  71. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/RECORD +73 -27
  72. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/WHEEL +0 -0
  73. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,25 @@
1
+ from aip_agents.mcp.client.base_mcp_client import BaseMCPClient as BaseMCPClient
2
+ from aip_agents.ptc.mcp.sandbox_bridge import build_mcp_payload as build_mcp_payload
3
+ from aip_agents.ptc.payload import SandboxPayload as SandboxPayload
4
+
5
+ async def build_sandbox_payload(mcp_client: BaseMCPClient | None = None, default_tool_timeout: float = 60.0) -> SandboxPayload:
6
+ """Build sandbox payload from MCP client configuration (MCP-only).
7
+
8
+ Args:
9
+ mcp_client: The MCP client with configured servers.
10
+ default_tool_timeout: Default timeout for tool calls in seconds.
11
+
12
+ Returns:
13
+ SandboxPayload containing files and env vars for the sandbox.
14
+ """
15
+ def wrap_ptc_code(code: str) -> str:
16
+ """Wrap user PTC code with necessary imports and setup (MCP-only).
17
+
18
+ This prepends sys.path setup to ensure the tools package is importable.
19
+
20
+ Args:
21
+ code: User-provided Python code.
22
+
23
+ Returns:
24
+ Wrapped code ready for sandbox execution.
25
+ """
@@ -0,0 +1,33 @@
1
+ """Template rendering helpers for PTC payloads.
2
+
3
+ Authors:
4
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from importlib import resources
11
+ from string import Template
12
+
13
+
14
+ def render_template(
15
+ package: str,
16
+ template_name: str,
17
+ values: Mapping[str, str] | None = None,
18
+ ) -> str:
19
+ """Render a template from package resources with optional substitutions.
20
+
21
+ Args:
22
+ package: Package path containing the template.
23
+ template_name: Template filename.
24
+ values: Optional mapping of template variables.
25
+
26
+ Returns:
27
+ Rendered template content.
28
+ """
29
+ template_file = resources.files(package).joinpath(template_name)
30
+ template_text = template_file.read_text(encoding="utf-8")
31
+ if not values:
32
+ return template_text
33
+ return Template(template_text).substitute(values)
@@ -0,0 +1,13 @@
1
+ from collections.abc import Mapping
2
+
3
+ def render_template(package: str, template_name: str, values: Mapping[str, str] | None = None) -> str:
4
+ """Render a template from package resources with optional substitutions.
5
+
6
+ Args:
7
+ package: Package path containing the template.
8
+ template_name: Template filename.
9
+ values: Optional mapping of template variables.
10
+
11
+ Returns:
12
+ Rendered template content.
13
+ """
@@ -0,0 +1 @@
1
+ """Shared templates for PTC sandbox code generation."""
File without changes
@@ -0,0 +1,134 @@
1
+ """PTC Discovery Helper Module.
2
+
3
+ This module provides discovery functions for exploring available PTC tools
4
+ inside the PTC sandbox. Use these functions to find packages, list tools,
5
+ and get detailed documentation.
6
+
7
+ Usage:
8
+ from tools.ptc_helper import list_packages, list_tools, describe_tool
9
+
10
+ # List all available packages
11
+ packages = list_packages()
12
+
13
+ # List tools in a package
14
+ tools = list_tools("package_name")
15
+
16
+ # Get tool documentation
17
+ doc = describe_tool("package_name", "tool_name")
18
+ """
19
+
20
+ import difflib
21
+ import json
22
+ import os
23
+ from typing import Any
24
+
25
+ # Load the index at module import time
26
+ _INDEX_PATH = os.path.join(os.path.dirname(__file__), "ptc_index.json")
27
+ _DOCS_DIR = os.path.join(os.path.dirname(__file__), "docs")
28
+
29
+ _index: dict[str, Any] = {}
30
+ if os.path.exists(_INDEX_PATH):
31
+ with open(_INDEX_PATH, "r") as f:
32
+ _index = json.load(f)
33
+
34
+
35
+ def _suggest_closest(name: str, valid_names: list[str], kind: str = "name") -> str:
36
+ """Generate a suggestion message for closest match.
37
+
38
+ Args:
39
+ name: The name that was not found.
40
+ valid_names: List of valid names to match against.
41
+ kind: Type of name (package or tool) for the error message.
42
+
43
+ Returns:
44
+ Suggestion string or empty string if no close match.
45
+ """
46
+ matches = difflib.get_close_matches(name, valid_names, n=1, cutoff=0.6)
47
+ if matches:
48
+ return f" Did you mean '{matches[0]}'?"
49
+ return ""
50
+
51
+
52
+ def list_packages() -> list[str]:
53
+ """List all available package names.
54
+
55
+ Returns:
56
+ Sorted list of sanitized package names.
57
+ """
58
+ packages = _index.get("packages", {})
59
+ return sorted(packages.keys())
60
+
61
+
62
+ def list_tools(package: str) -> list[dict[str, str]]:
63
+ """List tools available in a package.
64
+
65
+ Args:
66
+ package: Sanitized package name (e.g., 'deepwiki').
67
+
68
+ Returns:
69
+ List of tool info dicts with 'name' keys.
70
+
71
+ Raises:
72
+ ValueError: If package is not found.
73
+ """
74
+ packages = _index.get("packages", {})
75
+ if package not in packages:
76
+ valid = list(packages.keys())
77
+ suggestion = _suggest_closest(package, valid, "package")
78
+ raise ValueError(f"Unknown package '{package}'.{suggestion}")
79
+
80
+ pkg_data = packages[package]
81
+ tools = pkg_data.get("tools", [])
82
+ return [{"name": t["name"]} for t in tools]
83
+
84
+
85
+ def describe_tool(package: str, tool: str) -> dict[str, Any]:
86
+ """Get detailed documentation for a tool.
87
+
88
+ Args:
89
+ package: Sanitized package name.
90
+ tool: Sanitized tool name.
91
+
92
+ Returns:
93
+ Dict with 'name', 'signature', 'doc', and 'doc_path' keys.
94
+
95
+ Raises:
96
+ ValueError: If package or tool is not found.
97
+ """
98
+ packages = _index.get("packages", {})
99
+ if package not in packages:
100
+ valid = list(packages.keys())
101
+ suggestion = _suggest_closest(package, valid, "package")
102
+ raise ValueError(f"Unknown package '{package}'.{suggestion}")
103
+
104
+ pkg_data = packages[package]
105
+ tools = pkg_data.get("tools", [])
106
+ tool_names = [t["name"] for t in tools]
107
+
108
+ if tool not in tool_names:
109
+ suggestion = _suggest_closest(tool, tool_names, "tool")
110
+ raise ValueError(f"Unknown tool '{tool}' in package '{package}'.{suggestion}")
111
+
112
+ tool_info = next(t for t in tools if t["name"] == tool)
113
+ doc_path = tool_info.get("doc_path", "")
114
+
115
+ doc_content = ""
116
+ if doc_path:
117
+ full_path = os.path.join(os.path.dirname(__file__), doc_path.replace("tools/", ""))
118
+ if os.path.exists(full_path):
119
+ with open(full_path, "r") as f:
120
+ doc_content = f.read()
121
+ else:
122
+ doc_content = f"# {tool}\n\n**Signature:** `{tool_info.get('signature', f'{tool}(**kwargs: Any)')}`"
123
+ else:
124
+ doc_content = f"# {tool}\n\n**Signature:** `{tool_info.get('signature', f'{tool}(**kwargs: Any)')}`"
125
+
126
+ return {
127
+ "name": tool,
128
+ "signature": tool_info.get("signature", f"{tool}(**kwargs)"),
129
+ "doc": doc_content,
130
+ "doc_path": doc_path,
131
+ }
132
+
133
+
134
+ __all__ = ["list_packages", "list_tools", "describe_tool"]
@@ -0,0 +1,43 @@
1
+ # flake8: noqa: F401
2
+ """Sandbox module for isolated code execution.
3
+
4
+ This module provides abstractions for running code in sandboxed environments.
5
+ All components support lazy loading to work with optional dependencies (e2b).
6
+
7
+ Authors:
8
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
9
+ """
10
+
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime
15
+ from aip_agents.sandbox.template_builder import ensure_ptc_template
16
+ from aip_agents.sandbox.types import SandboxExecutionResult
17
+
18
+ _IMPORT_MAP = {
19
+ "E2BSandboxRuntime": "aip_agents.sandbox.e2b_runtime",
20
+ "ensure_ptc_template": "aip_agents.sandbox.template_builder",
21
+ "SandboxExecutionResult": "aip_agents.sandbox.types",
22
+ }
23
+
24
+ _cache: dict[str, Any] = {}
25
+
26
+
27
+ def __getattr__(name: str) -> Any:
28
+ """Lazy import components on first access."""
29
+ if name in _cache:
30
+ return _cache[name]
31
+
32
+ if name in _IMPORT_MAP:
33
+ try:
34
+ module = __import__(_IMPORT_MAP[name], fromlist=[name])
35
+ _cache[name] = getattr(module, name)
36
+ return _cache[name]
37
+ except ImportError as e:
38
+ raise ImportError(f"Failed to import {name}. Install with: pip install aip-agents[local]") from e
39
+
40
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
41
+
42
+
43
+ __all__ = list(_IMPORT_MAP.keys())
@@ -0,0 +1,5 @@
1
+ from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime as E2BSandboxRuntime
2
+ from aip_agents.sandbox.template_builder import ensure_ptc_template as ensure_ptc_template
3
+ from aip_agents.sandbox.types import SandboxExecutionResult as SandboxExecutionResult
4
+
5
+ __all__ = ['E2BSandboxRuntime', 'ensure_ptc_template', 'SandboxExecutionResult']
@@ -0,0 +1,9 @@
1
+ """Defaults for PTC sandbox templates and packages."""
2
+
3
+ DEFAULT_PTC_TEMPLATE = "aip-agents-ptc-v1"
4
+ DEFAULT_PTC_PACKAGES: tuple[str, ...] = (
5
+ "aip-agents-binary[local]",
6
+ "mcp",
7
+ "httpx",
8
+ "gllm-plugin-binary==0.0.7",
9
+ )
@@ -0,0 +1,2 @@
1
+ DEFAULT_PTC_TEMPLATE: str
2
+ DEFAULT_PTC_PACKAGES: tuple[str, ...]
@@ -0,0 +1,267 @@
1
+ """E2B Sandbox Runtime for PTC.
2
+
3
+ This module provides direct E2B SDK integration for sandbox code execution.
4
+
5
+ Authors:
6
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
7
+ """
8
+
9
+ from e2b_code_interpreter import AsyncSandbox, OutputMessage
10
+
11
+ from aip_agents.sandbox.defaults import DEFAULT_PTC_PACKAGES, DEFAULT_PTC_TEMPLATE
12
+ from aip_agents.sandbox.types import SandboxExecutionResult
13
+ from aip_agents.sandbox.validation import validate_package_names
14
+ from aip_agents.utils.logger import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ SANDBOX_NOT_INITIALIZED_ERROR = "Sandbox not initialized"
19
+
20
+
21
+ class E2BSandboxRuntime:
22
+ """E2B Sandbox runtime for executing code in isolated environments.
23
+
24
+ This runtime manages per-run sandbox lifecycle:
25
+ - Create sandbox on first execute
26
+ - Reuse sandbox for subsequent executes
27
+ - Destroy sandbox on cleanup
28
+
29
+ Example:
30
+ runtime = E2BSandboxRuntime()
31
+ result = await runtime.execute(
32
+ code="print('Hello')",
33
+ timeout=60.0,
34
+ files={"tools/mcp.py": "# MCP client code"},
35
+ )
36
+ await runtime.cleanup()
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ template: str | None = None,
42
+ ptc_packages: list[str] | None = None,
43
+ ) -> None:
44
+ """Initialize E2B sandbox runtime.
45
+
46
+ Args:
47
+ template: Optional E2B template ID for custom sandbox environments.
48
+ ptc_packages: Packages to install in sandbox. If None or empty, skip install.
49
+ """
50
+ self._template = template
51
+ self._ptc_packages = ptc_packages
52
+ self._sandbox: AsyncSandbox | None = None
53
+ self._sandbox_created_with_template = False
54
+
55
+ async def execute(
56
+ self,
57
+ code: str,
58
+ *,
59
+ timeout: float = 300.0,
60
+ files: dict[str, str] | None = None,
61
+ env: dict[str, str] | None = None,
62
+ template: str | None = None,
63
+ ) -> SandboxExecutionResult:
64
+ """Execute code inside the sandbox.
65
+
66
+ Args:
67
+ code: Python code to execute.
68
+ timeout: Execution timeout in seconds.
69
+ files: Files to upload to the sandbox (path -> content).
70
+ env: Environment variables to set.
71
+ template: Optional template override for this execution.
72
+
73
+ Returns:
74
+ SandboxExecutionResult with stdout, stderr, and exit_code.
75
+ """
76
+ # Create sandbox if not exists
77
+ if self._sandbox is None:
78
+ await self._create_sandbox(template or self._template)
79
+
80
+ # Upload files if provided
81
+ if files:
82
+ await self._upload_files(files)
83
+
84
+ # Execute code
85
+ return await self._run_code(code, timeout, env)
86
+
87
+ async def cleanup(self) -> None:
88
+ """Destroy the sandbox and release resources."""
89
+ if self._sandbox is not None:
90
+ try:
91
+ logger.info("Destroying E2B sandbox")
92
+ await self._sandbox.kill()
93
+ except Exception as e:
94
+ logger.warning(f"Error destroying sandbox: {e}")
95
+ finally:
96
+ self._sandbox = None
97
+
98
+ self._reset_async_transport()
99
+
100
+ @property
101
+ def is_active(self) -> bool:
102
+ """Check if a sandbox is currently active."""
103
+ return self._sandbox is not None
104
+
105
+ def _reset_async_transport(self) -> None:
106
+ try:
107
+ from e2b.api.client_async import AsyncTransportWithLogger
108
+
109
+ AsyncTransportWithLogger.singleton = None
110
+ except Exception:
111
+ return
112
+
113
+ def _should_skip_default_ptc_install(self, template: str | None) -> bool:
114
+ if not self._sandbox_created_with_template:
115
+ return False
116
+
117
+ if template != DEFAULT_PTC_TEMPLATE:
118
+ return False
119
+
120
+ return self._ptc_packages == list(DEFAULT_PTC_PACKAGES)
121
+
122
+ async def _create_sandbox(self, template: str | None = None) -> None:
123
+ """Create a new E2B sandbox.
124
+
125
+ Implements canonical runtime rules:
126
+ - If template provided, try creating sandbox with template
127
+ - On any error, fall back to default sandbox (without template)
128
+ - Install ptc_packages regardless of template usage (even after fallback)
129
+
130
+ Note: Package installation occurs after sandbox creation, so fallback
131
+ to default sandbox does not skip package installation.
132
+
133
+ Args:
134
+ template: Optional template ID.
135
+ """
136
+ logger.info(f"Creating E2B sandbox (template={template})")
137
+
138
+ async def create_default_sandbox() -> None:
139
+ self._sandbox = await AsyncSandbox.create()
140
+ logger.info(f"E2B sandbox created (default): {self._sandbox.sandbox_id}")
141
+
142
+ if template:
143
+ try:
144
+ self._sandbox = await AsyncSandbox.create(template=template)
145
+ self._sandbox_created_with_template = True
146
+ logger.info(f"E2B sandbox created: {self._sandbox.sandbox_id}")
147
+ except Exception as e:
148
+ logger.warning(f"Template creation failed ({template}): {e}")
149
+ logger.info("Falling back to default sandbox")
150
+ self._sandbox_created_with_template = False
151
+ await create_default_sandbox()
152
+ else:
153
+ self._sandbox_created_with_template = False
154
+ await create_default_sandbox()
155
+
156
+ # Install ptc_packages if non-empty
157
+ if self._ptc_packages:
158
+ if self._should_skip_default_ptc_install(template):
159
+ logger.info("Skipping PTC package install (default template already includes defaults)")
160
+ else:
161
+ await self._install_ptc_packages()
162
+
163
+ async def _install_ptc_packages(self) -> None:
164
+ """Install PTC packages in the sandbox."""
165
+ if self._sandbox is None:
166
+ raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
167
+
168
+ # Validate all packages before constructing command
169
+ validate_package_names(self._ptc_packages)
170
+
171
+ # Note: packages_str is safe because ptc_packages is a controlled list from
172
+ # configuration, not user input. E2B SDK's commands.run() only accepts str,
173
+ # not list, so string joining is required.
174
+ packages_str = " ".join(self._ptc_packages)
175
+ logger.info(f"Installing PTC packages in sandbox: {packages_str}")
176
+
177
+ try:
178
+ result = await self._sandbox.commands.run(
179
+ f"pip install -q {packages_str}",
180
+ timeout=120,
181
+ )
182
+ except Exception as e:
183
+ logger.error(f"Error installing PTC packages: {e}")
184
+ raise
185
+
186
+ if result.exit_code != 0:
187
+ logger.error(f"Failed to install PTC packages: {result.stderr}")
188
+ raise RuntimeError(f"Failed to install PTC packages: {result.stderr}")
189
+
190
+ logger.info("PTC packages installed successfully")
191
+
192
+ async def _upload_files(self, files: dict[str, str]) -> None:
193
+ """Upload files to the sandbox.
194
+
195
+ Args:
196
+ files: Mapping of path -> content.
197
+ """
198
+ if self._sandbox is None:
199
+ raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
200
+
201
+ for path, content in files.items():
202
+ logger.debug(f"Uploading file to sandbox: {path}")
203
+ await self._sandbox.files.write(path, content)
204
+
205
+ async def _run_code(
206
+ self,
207
+ code: str,
208
+ timeout: float,
209
+ env: dict[str, str] | None = None,
210
+ ) -> SandboxExecutionResult:
211
+ """Run code in the sandbox.
212
+
213
+ Args:
214
+ code: Python code to execute.
215
+ timeout: Execution timeout in seconds.
216
+ env: Environment variables.
217
+
218
+ Returns:
219
+ SandboxExecutionResult with execution output.
220
+ """
221
+ if self._sandbox is None:
222
+ raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
223
+
224
+ stdout_lines: list[str] = []
225
+ stderr_lines: list[str] = []
226
+
227
+ def on_stdout(msg: OutputMessage) -> None:
228
+ if hasattr(msg, "line"):
229
+ stdout_lines.append(msg.line)
230
+
231
+ def on_stderr(msg: OutputMessage) -> None:
232
+ if hasattr(msg, "line"):
233
+ stderr_lines.append(msg.line)
234
+
235
+ try:
236
+ execution = await self._sandbox.run_code(
237
+ code=code,
238
+ language="python",
239
+ on_stdout=on_stdout,
240
+ on_stderr=on_stderr,
241
+ envs=env,
242
+ timeout=timeout,
243
+ )
244
+
245
+ # Determine exit code
246
+ exit_code = 0
247
+ if execution.error:
248
+ exit_code = 1
249
+ # Add error to stderr
250
+ error_msg = f"{execution.error.name}: {execution.error.value}"
251
+ if execution.error.traceback:
252
+ error_msg = f"{execution.error.traceback}\n{error_msg}"
253
+ stderr_lines.append(error_msg)
254
+
255
+ return SandboxExecutionResult(
256
+ stdout="\n".join(stdout_lines),
257
+ stderr="\n".join(stderr_lines),
258
+ exit_code=exit_code,
259
+ )
260
+
261
+ except Exception as e:
262
+ logger.error(f"Sandbox execution failed: {e}")
263
+ return SandboxExecutionResult(
264
+ stdout="",
265
+ stderr=str(e),
266
+ exit_code=1,
267
+ )
@@ -0,0 +1,51 @@
1
+ from _typeshed import Incomplete
2
+ from aip_agents.sandbox.defaults import DEFAULT_PTC_PACKAGES as DEFAULT_PTC_PACKAGES, DEFAULT_PTC_TEMPLATE as DEFAULT_PTC_TEMPLATE
3
+ from aip_agents.sandbox.types import SandboxExecutionResult as SandboxExecutionResult
4
+ from aip_agents.sandbox.validation import validate_package_names as validate_package_names
5
+ from aip_agents.utils.logger import get_logger as get_logger
6
+
7
+ logger: Incomplete
8
+ SANDBOX_NOT_INITIALIZED_ERROR: str
9
+
10
+ class E2BSandboxRuntime:
11
+ '''E2B Sandbox runtime for executing code in isolated environments.
12
+
13
+ This runtime manages per-run sandbox lifecycle:
14
+ - Create sandbox on first execute
15
+ - Reuse sandbox for subsequent executes
16
+ - Destroy sandbox on cleanup
17
+
18
+ Example:
19
+ runtime = E2BSandboxRuntime()
20
+ result = await runtime.execute(
21
+ code="print(\'Hello\')",
22
+ timeout=60.0,
23
+ files={"tools/mcp.py": "# MCP client code"},
24
+ )
25
+ await runtime.cleanup()
26
+ '''
27
+ def __init__(self, template: str | None = None, ptc_packages: list[str] | None = None) -> None:
28
+ """Initialize E2B sandbox runtime.
29
+
30
+ Args:
31
+ template: Optional E2B template ID for custom sandbox environments.
32
+ ptc_packages: Packages to install in sandbox. If None or empty, skip install.
33
+ """
34
+ async def execute(self, code: str, *, timeout: float = 300.0, files: dict[str, str] | None = None, env: dict[str, str] | None = None, template: str | None = None) -> SandboxExecutionResult:
35
+ """Execute code inside the sandbox.
36
+
37
+ Args:
38
+ code: Python code to execute.
39
+ timeout: Execution timeout in seconds.
40
+ files: Files to upload to the sandbox (path -> content).
41
+ env: Environment variables to set.
42
+ template: Optional template override for this execution.
43
+
44
+ Returns:
45
+ SandboxExecutionResult with stdout, stderr, and exit_code.
46
+ """
47
+ async def cleanup(self) -> None:
48
+ """Destroy the sandbox and release resources."""
49
+ @property
50
+ def is_active(self) -> bool:
51
+ """Check if a sandbox is currently active."""