aip-agents-binary 0.0.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.
- aip_agents/__init__.py +66 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1125 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/a2ui/__init__.py +26 -0
- aip_agents/a2ui/catalog_provider.py +109 -0
- aip_agents/a2ui/examples/0.8/booking_form.json +1 -0
- aip_agents/a2ui/examples/0.8/confirmation.json +1 -0
- aip_agents/a2ui/examples/0.8/single_column_list.json +1 -0
- aip_agents/a2ui/examples/0.9/booking_form.json +1 -0
- aip_agents/a2ui/examples/0.9/confirmation.json +1 -0
- aip_agents/a2ui/examples/0.9/single_column_list.json +1 -0
- aip_agents/a2ui/middleware.py +754 -0
- aip_agents/a2ui/models.py +153 -0
- aip_agents/a2ui/public_url.py +138 -0
- aip_agents/a2ui/stream_filter.py +67 -0
- aip_agents/agent/__init__.py +72 -0
- aip_agents/agent/base_agent.py +1437 -0
- aip_agents/agent/base_langgraph_agent.py +4048 -0
- aip_agents/agent/google_adk_agent.py +932 -0
- aip_agents/agent/google_adk_constants.py +11 -0
- aip_agents/agent/graph_runtime.py +185 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_react_agent.py +3741 -0
- aip_agents/agent/model_invocation_adapter.py +346 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/agent/tool_execution_engine.py +587 -0
- aip_agents/audio_interface/__init__.py +30 -0
- aip_agents/audio_interface/audio_agent_adapter.py +254 -0
- aip_agents/audio_interface/config.py +146 -0
- aip_agents/audio_interface/errors.py +15 -0
- aip_agents/audio_interface/livekit_audio_session.py +504 -0
- aip_agents/audio_interface/livekit_realtime_audio_session.py +142 -0
- aip_agents/audio_interface/session_factory.py +40 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/common/__init__.py +5 -0
- aip_agents/common/callable_introspection.py +17 -0
- aip_agents/constants.py +23 -0
- aip_agents/context/__init__.py +5 -0
- aip_agents/context/budget.py +237 -0
- aip_agents/context/compaction.py +339 -0
- aip_agents/context/metadata.py +169 -0
- aip_agents/context/offload.py +449 -0
- aip_agents/context/preparation.py +196 -0
- aip_agents/context/prompt.py +61 -0
- aip_agents/context/runtime.py +149 -0
- aip_agents/context/session_recall.py +69 -0
- aip_agents/context/state.py +48 -0
- aip_agents/context/utils.py +44 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/audio_livekit_session.py +116 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +141 -0
- aip_agents/examples/gl_connectors_smoke.py +148 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +78 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +104 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +78 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +77 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +80 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +91 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +83 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +131 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +250 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_filesystem.py +162 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_gl_connector_twitter.py +44 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_skills.py +62 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_multi_agent_tool_output_client.py +57 -0
- aip_agents/examples/hello_world_multi_agent_tool_output_server.py +131 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_ptc.py +49 -0
- aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +100 -0
- aip_agents/examples/hello_world_tool_output_client.py +55 -0
- aip_agents/examples/hello_world_tool_output_server.py +113 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +125 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +246 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +87 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/multiply_tool.py +43 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/guardrails/__init__.py +83 -0
- aip_agents/guardrails/engines/__init__.py +69 -0
- aip_agents/guardrails/engines/base.py +90 -0
- aip_agents/guardrails/engines/nemo.py +101 -0
- aip_agents/guardrails/engines/phrase_matcher.py +113 -0
- aip_agents/guardrails/exceptions.py +39 -0
- aip_agents/guardrails/manager.py +163 -0
- aip_agents/guardrails/middleware.py +199 -0
- aip_agents/guardrails/schemas.py +63 -0
- aip_agents/guardrails/utils.py +45 -0
- aip_agents/integration/__init__.py +43 -0
- aip_agents/integration/agent.py +58 -0
- aip_agents/integration/guardrails.py +52 -0
- aip_agents/integration/hitl.py +71 -0
- aip_agents/integration/ptc.py +95 -0
- aip_agents/integration/skills.py +37 -0
- aip_agents/integration/storage.py +81 -0
- aip_agents/integration/version.py +103 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +60 -0
- aip_agents/mcp/client/base_mcp_client.py +377 -0
- aip_agents/mcp/client/connection_manager.py +231 -0
- aip_agents/mcp/client/errors.py +119 -0
- aip_agents/mcp/client/google_adk/__init__.py +45 -0
- aip_agents/mcp/client/google_adk/client.py +383 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +704 -0
- aip_agents/mcp/client/session_pool.py +358 -0
- aip_agents/mcp/client/transports.py +288 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +815 -0
- aip_agents/memory/adapters/mem0.py +286 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +65 -0
- aip_agents/middleware/backends/__init__.py +72 -0
- aip_agents/middleware/backends/in_memory.py +578 -0
- aip_agents/middleware/backends/local_disk.py +1203 -0
- aip_agents/middleware/backends/protocol.py +353 -0
- aip_agents/middleware/backends/sandbox.py +1431 -0
- aip_agents/middleware/backends/sandbox_files.py +113 -0
- aip_agents/middleware/backends/utils.py +293 -0
- aip_agents/middleware/base.py +239 -0
- aip_agents/middleware/filesystem.py +690 -0
- aip_agents/middleware/manager.py +291 -0
- aip_agents/middleware/memory.py +532 -0
- aip_agents/middleware/pii_middleware.py +192 -0
- aip_agents/middleware/schema.py +19 -0
- aip_agents/middleware/skills.py +872 -0
- aip_agents/middleware/task_delegation.py +1656 -0
- aip_agents/middleware/todolist.py +276 -0
- aip_agents/middleware/tools/__init__.py +23 -0
- aip_agents/middleware/tools/edit_file.py +70 -0
- aip_agents/middleware/tools/execute.py +428 -0
- aip_agents/middleware/tools/grep_file.py +79 -0
- aip_agents/middleware/tools/ls.py +71 -0
- aip_agents/middleware/tools/read_file.py +239 -0
- aip_agents/middleware/tools/skill_load.py +193 -0
- aip_agents/middleware/tools/skill_resource.py +338 -0
- aip_agents/middleware/tools/write_file.py +114 -0
- aip_agents/middleware/vision.py +322 -0
- aip_agents/ptc/__init__.py +87 -0
- aip_agents/ptc/custom_tools.py +511 -0
- aip_agents/ptc/custom_tools_payload.py +499 -0
- aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
- aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +24 -0
- aip_agents/ptc/custom_tools_templates/custom_init.py.template +26 -0
- aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +149 -0
- aip_agents/ptc/custom_tools_templates/custom_registry.py.template +112 -0
- aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
- aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +87 -0
- aip_agents/ptc/doc_gen.py +122 -0
- aip_agents/ptc/exceptions.py +57 -0
- aip_agents/ptc/executor.py +627 -0
- aip_agents/ptc/mcp/__init__.py +45 -0
- aip_agents/ptc/mcp/sandbox_bridge.py +679 -0
- aip_agents/ptc/mcp/templates/__init__.py +1 -0
- aip_agents/ptc/mcp/templates/mcp_client.py.template +240 -0
- aip_agents/ptc/naming.py +430 -0
- aip_agents/ptc/payload.py +26 -0
- aip_agents/ptc/prompt_builder.py +674 -0
- aip_agents/ptc/ptc_helper.py +16 -0
- aip_agents/ptc/sandbox_bridge.py +297 -0
- aip_agents/ptc/template_utils.py +33 -0
- aip_agents/ptc/templates/__init__.py +1 -0
- aip_agents/ptc/templates/ptc_helper.py.template +134 -0
- aip_agents/ptc/tool_def_helpers.py +101 -0
- aip_agents/ptc/tool_enrichment.py +163 -0
- aip_agents/resilience/__init__.py +10 -0
- aip_agents/resilience/base.py +46 -0
- aip_agents/resilience/tool.py +1661 -0
- aip_agents/sandbox/__init__.py +62 -0
- aip_agents/sandbox/defaults.py +220 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1-datasaur.Dockerfile +120 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1.Dockerfile +100 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2-datasaur.Dockerfile +143 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2.Dockerfile +119 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3-datasaur.Dockerfile +173 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3.Dockerfile +130 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.Dockerfile +164 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.patch_js_context.py +41 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.requirements.txt +29 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2.Dockerfile +87 -0
- aip_agents/sandbox/e2b_runtime.py +27 -0
- aip_agents/sandbox/image_registry.py +98 -0
- aip_agents/sandbox/providers/__init__.py +76 -0
- aip_agents/sandbox/providers/base.py +392 -0
- aip_agents/sandbox/providers/bedrock.py +28 -0
- aip_agents/sandbox/providers/e2b.py +230 -0
- aip_agents/sandbox/providers/opensandbox.py +163 -0
- aip_agents/sandbox/sandbox_runtime.py +967 -0
- aip_agents/sandbox/template_builder.py +240 -0
- aip_agents/sandbox/types.py +141 -0
- aip_agents/sandbox/validation.py +50 -0
- aip_agents/schema/__init__.py +106 -0
- aip_agents/schema/a2a.py +58 -0
- aip_agents/schema/agent.py +117 -0
- aip_agents/schema/artifacts.py +75 -0
- aip_agents/schema/attachments.py +425 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +47 -0
- aip_agents/schema/model_id.py +103 -0
- aip_agents/schema/model_usage.py +50 -0
- aip_agents/schema/runtime_metadata.py +78 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/schema/tool_payloads.py +68 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +267 -0
- aip_agents/skills/__init__.py +30 -0
- aip_agents/skills/errors.py +110 -0
- aip_agents/skills/installer.py +479 -0
- aip_agents/skills/models.py +769 -0
- aip_agents/skills/stager.py +224 -0
- aip_agents/skills/validation.py +53 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +65 -0
- aip_agents/tools/agent_schedule_tool.py +392 -0
- aip_agents/tools/browser_use/__init__.py +83 -0
- aip_agents/tools/browser_use/action_parser.py +104 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1118 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +203 -0
- aip_agents/tools/browser_use/schemas.py +121 -0
- aip_agents/tools/browser_use/session.py +13 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +321 -0
- aip_agents/tools/browser_use/streaming.py +263 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +37 -0
- aip_agents/tools/code_sandbox/__init__.py +48 -0
- aip_agents/tools/code_sandbox/code_sandbox_tool.py +350 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +346 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +65 -0
- aip_agents/tools/computer_use/__init__.py +75 -0
- aip_agents/tools/computer_use/computer_use_tool.py +910 -0
- aip_agents/tools/computer_use/schemas.py +78 -0
- aip_agents/tools/computer_use/streaming.py +198 -0
- aip_agents/tools/computer_use/types.py +32 -0
- aip_agents/tools/constants.py +177 -0
- aip_agents/tools/date_range_tool.py +554 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/execute_ptc_code.py +582 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +518 -0
- aip_agents/tools/gl_connector_tools.py +170 -0
- aip_agents/tools/memory_search/__init__.py +29 -0
- aip_agents/tools/memory_search/base.py +213 -0
- aip_agents/tools/memory_search/mem0.py +424 -0
- aip_agents/tools/memory_search/schema.py +81 -0
- aip_agents/tools/memory_search_tool.py +34 -0
- aip_agents/tools/streaming_common.py +577 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +73 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +91 -0
- aip_agents/utils/a2a_connector.py +1758 -0
- aip_agents/utils/artifact_helpers.py +542 -0
- aip_agents/utils/artifacts/__init__.py +20 -0
- aip_agents/utils/artifacts/tracker.py +367 -0
- aip_agents/utils/attachment_helpers.py +178 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +276 -0
- aip_agents/utils/file_watchers/__init__.py +33 -0
- aip_agents/utils/file_watchers/base.py +46 -0
- aip_agents/utils/file_watchers/e2b.py +128 -0
- aip_agents/utils/file_watchers/factory.py +39 -0
- aip_agents/utils/file_watchers/ls_diff.py +88 -0
- aip_agents/utils/file_watchers/sandbox.py +66 -0
- aip_agents/utils/file_watchers/watchdog.py +97 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +258 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +399 -0
- aip_agents/utils/langgraph/delegation_context.py +22 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1329 -0
- aip_agents/utils/langgraph/tool_output_management.py +1047 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +822 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +363 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +29 -0
- aip_agents/utils/pii/pii_handler.py +555 -0
- aip_agents/utils/pii/pii_helper.py +225 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +836 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +531 -0
- aip_agents_binary-0.0.0.dist-info/METADATA +825 -0
- aip_agents_binary-0.0.0.dist-info/RECORD +447 -0
- aip_agents_binary-0.0.0.dist-info/WHEEL +5 -0
- aip_agents_binary-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
"""Provider-agnostic sandbox runtime for PTC.
|
|
2
|
+
|
|
3
|
+
Orchestrates per-run sandbox lifecycle (create → reuse → cleanup) against the
|
|
4
|
+
generic ``CodeInterpreterSandbox`` interface. Backend selection and all
|
|
5
|
+
create-time asymmetry live behind the provider factory (``sandbox/providers``);
|
|
6
|
+
this runtime never imports a specific backend SDK.
|
|
7
|
+
|
|
8
|
+
Authors:
|
|
9
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import Awaitable, Callable
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from gllm_inference.schema import Attachment
|
|
21
|
+
from gllm_tools.code_interpreter.code_sandbox.models import ExecutionResult, ExecutionStatus
|
|
22
|
+
from gllm_tools.code_interpreter.code_sandbox.sandbox import CodeInterpreterSandbox
|
|
23
|
+
|
|
24
|
+
from aip_agents.sandbox.defaults import (
|
|
25
|
+
DEFAULT_SANDBOX_PACKAGES,
|
|
26
|
+
DEFAULT_SANDBOX_TEMPLATE,
|
|
27
|
+
)
|
|
28
|
+
from aip_agents.sandbox.providers import SandboxConfig, get_provider
|
|
29
|
+
from aip_agents.sandbox.types import SandboxExecutionResult
|
|
30
|
+
from aip_agents.sandbox.validation import validate_package_names
|
|
31
|
+
from aip_agents.utils.logger import get_logger
|
|
32
|
+
|
|
33
|
+
# Type alias for package selector callback.
|
|
34
|
+
# Called after sandbox creation with actual template status (None if template failed).
|
|
35
|
+
PackageSelector = Callable[[str | None], list[str] | None]
|
|
36
|
+
|
|
37
|
+
logger = get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
SANDBOX_NOT_INITIALIZED_ERROR = "Sandbox not initialized"
|
|
40
|
+
DEFAULT_PACKAGE_INSTALL_TIMEOUT_SECONDS = 300.0
|
|
41
|
+
E2B_COMMANDS_BACKEND = "e2b_commands"
|
|
42
|
+
E2B_CODE_INTERPRETER_BACKEND = "e2b_code_interpreter"
|
|
43
|
+
E2B_COMMAND_TIMEOUT_SOURCE = "e2b_command_timeout"
|
|
44
|
+
PTC_EXECUTION_TIMEOUT_SOURCE = "ptc_execution_timeout"
|
|
45
|
+
LOCAL_ASYNCIO_TIMEOUT_SOURCE = "local_asyncio_timeout"
|
|
46
|
+
CODE_INTERPRETER_TIMEOUT_SOURCE = "e2b_code_interpreter_status_timeout"
|
|
47
|
+
CODE_INTERPRETER_BACKEND_TIMEOUT_SOURCE = "e2b_code_interpreter_backend_timeout"
|
|
48
|
+
STALE_RUNTIME_TRANSPORT_DIAGNOSTIC = "stale_runtime_transport"
|
|
49
|
+
BACKEND_TIMEOUT_MARKERS = (
|
|
50
|
+
"unexpectedendofexecution",
|
|
51
|
+
"deadline exceeded",
|
|
52
|
+
"timed out",
|
|
53
|
+
)
|
|
54
|
+
BOUND_TO_DIFFERENT_EVENT_LOOP_MARKER = "bound to a different event loop"
|
|
55
|
+
|
|
56
|
+
_HANDLER_IS_CLOSED_MARKER = "handler is closed"
|
|
57
|
+
_CLOSEABLE_TRANSPORT_ERROR_MARKERS = (
|
|
58
|
+
_HANDLER_IS_CLOSED_MARKER,
|
|
59
|
+
"tcptransport closed",
|
|
60
|
+
"event loop is closed",
|
|
61
|
+
BOUND_TO_DIFFERENT_EVENT_LOOP_MARKER,
|
|
62
|
+
"transport closed",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_STALE_RUNTIME_RESULT_MARKER_GROUPS = (
|
|
66
|
+
("asyncio.locks.event", BOUND_TO_DIFFERENT_EVENT_LOOP_MARKER),
|
|
67
|
+
("tcptransport closed", _HANDLER_IS_CLOSED_MARKER),
|
|
68
|
+
("unable to perform operation on", _HANDLER_IS_CLOSED_MARKER),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SandboxRuntime:
|
|
73
|
+
"""Sandbox runtime for executing code in isolated environments.
|
|
74
|
+
|
|
75
|
+
This runtime manages per-run sandbox lifecycle:
|
|
76
|
+
- Create sandbox on first execute
|
|
77
|
+
- Reuse sandbox for subsequent executes
|
|
78
|
+
- Destroy sandbox on cleanup
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
runtime = SandboxRuntime()
|
|
82
|
+
result = await runtime.execute(
|
|
83
|
+
code="print('Hello')",
|
|
84
|
+
timeout=60.0,
|
|
85
|
+
files={"tools/mcp.py": "# MCP client code"},
|
|
86
|
+
)
|
|
87
|
+
await runtime.cleanup()
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
prefer_python_file_execution = True
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
template: str | None = None,
|
|
95
|
+
packages: list[str] | None = None,
|
|
96
|
+
package_selector: PackageSelector | None = None,
|
|
97
|
+
sandbox_timeout: int = 300,
|
|
98
|
+
package_install_timeout: float | None = None,
|
|
99
|
+
auto_build_template: bool = False,
|
|
100
|
+
build_base_template: str = "code-interpreter-v1",
|
|
101
|
+
api_key: str | None = None,
|
|
102
|
+
domain: str | None = None,
|
|
103
|
+
provider: Any | None = None,
|
|
104
|
+
# Backward compatibility: accept old parameter name
|
|
105
|
+
ptc_packages: list[str] | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Initialize sandbox runtime.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
template: Optional E2B template ID for custom sandbox environments.
|
|
111
|
+
packages: Packages to install in sandbox. If None or empty, skip install.
|
|
112
|
+
Overwritten by package_selector if provided.
|
|
113
|
+
package_selector: Optional callback to select packages after sandbox creation.
|
|
114
|
+
Called with actual template (None if template failed) to enable smart
|
|
115
|
+
package selection based on whether template was successfully created.
|
|
116
|
+
sandbox_timeout: Timeout in seconds for the sandbox session. Defaults to 300.
|
|
117
|
+
This prevents "port is not open" errors by keeping the sandbox alive.
|
|
118
|
+
package_install_timeout: Timeout in seconds for package installation.
|
|
119
|
+
When None, uses the runtime default. Values <= 0 disable the E2B
|
|
120
|
+
command timeout for package installation.
|
|
121
|
+
auto_build_template: If True and the requested template returns 404, attempt
|
|
122
|
+
to build it automatically before falling back to the default sandbox.
|
|
123
|
+
Building can take several minutes. Defaults to False.
|
|
124
|
+
build_base_template: Base E2B template to build from when auto-building.
|
|
125
|
+
Defaults to ``"code-interpreter-v1"``.
|
|
126
|
+
api_key: Provider API key override. E2B falls back to ``E2B_API_KEY``
|
|
127
|
+
when not provided.
|
|
128
|
+
domain: Optional provider domain override. E2B falls back to
|
|
129
|
+
``E2B_DOMAIN`` when not provided.
|
|
130
|
+
provider: Sandbox provider name/enum. Defaults to ``SANDBOX_PROVIDER``
|
|
131
|
+
then ``"e2b"``.
|
|
132
|
+
ptc_packages: Deprecated. Use `packages` instead.
|
|
133
|
+
"""
|
|
134
|
+
self._template = template
|
|
135
|
+
# Handle backward compatibility
|
|
136
|
+
if ptc_packages is not None and packages is None:
|
|
137
|
+
packages = ptc_packages
|
|
138
|
+
self._packages = packages
|
|
139
|
+
self._package_selector = package_selector
|
|
140
|
+
self._provider = provider if provider is not None else os.environ.get("SANDBOX_PROVIDER", "e2b")
|
|
141
|
+
if self._provider_name == "e2b":
|
|
142
|
+
self._api_key = api_key if api_key is not None else os.environ.get("E2B_API_KEY")
|
|
143
|
+
self._domain = domain if domain is not None else os.environ.get("E2B_DOMAIN")
|
|
144
|
+
else:
|
|
145
|
+
self._api_key = api_key
|
|
146
|
+
self._domain = domain
|
|
147
|
+
self._sandbox: CodeInterpreterSandbox | None = None
|
|
148
|
+
self._sandbox_created_with_template = False
|
|
149
|
+
self._reset_listeners: list[Callable[[], None]] = []
|
|
150
|
+
self._sandbox_timeout = sandbox_timeout
|
|
151
|
+
self._package_install_timeout = (
|
|
152
|
+
DEFAULT_PACKAGE_INSTALL_TIMEOUT_SECONDS if package_install_timeout is None else package_install_timeout
|
|
153
|
+
)
|
|
154
|
+
self._auto_build_template = auto_build_template
|
|
155
|
+
self._build_base_template = build_base_template
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def _provider_name(self) -> str:
|
|
159
|
+
"""Return the configured provider registry name."""
|
|
160
|
+
return str(getattr(self._provider, "value", self._provider)).lower()
|
|
161
|
+
|
|
162
|
+
async def execute(
|
|
163
|
+
self,
|
|
164
|
+
code: str,
|
|
165
|
+
*,
|
|
166
|
+
timeout: float = 300, # NOSONAR - public API; execution uses asyncio.timeout below.
|
|
167
|
+
files: dict[str, str] | None = None,
|
|
168
|
+
env: dict[str, str] | None = None,
|
|
169
|
+
template: str | None = None,
|
|
170
|
+
) -> SandboxExecutionResult:
|
|
171
|
+
"""Execute code inside the sandbox.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
code: Python code to execute.
|
|
175
|
+
timeout: Execution timeout in seconds.
|
|
176
|
+
files: Files to upload to the sandbox (path -> content).
|
|
177
|
+
env: Environment variables to set.
|
|
178
|
+
template: Optional template override for this execution.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
SandboxExecutionResult with stdout, stderr, and exit_code.
|
|
182
|
+
"""
|
|
183
|
+
return await self._run_with_stale_runtime_recovery(
|
|
184
|
+
lambda: self._execute_impl(
|
|
185
|
+
code,
|
|
186
|
+
execution_timeout=timeout,
|
|
187
|
+
files=files,
|
|
188
|
+
env=env,
|
|
189
|
+
template=template,
|
|
190
|
+
),
|
|
191
|
+
operation="execute",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def _execute_impl(
|
|
195
|
+
self,
|
|
196
|
+
code: str,
|
|
197
|
+
*,
|
|
198
|
+
execution_timeout: float,
|
|
199
|
+
files: dict[str, str] | None,
|
|
200
|
+
env: dict[str, str] | None,
|
|
201
|
+
template: str | None,
|
|
202
|
+
) -> SandboxExecutionResult:
|
|
203
|
+
"""Execute one code attempt without stale-runtime recovery."""
|
|
204
|
+
if self._sandbox is None:
|
|
205
|
+
await self._ensure_sandbox(template or self._template, timeout_budget_seconds=execution_timeout)
|
|
206
|
+
|
|
207
|
+
await self._apply_sandbox_timeout()
|
|
208
|
+
|
|
209
|
+
if files:
|
|
210
|
+
await self._upload_files(files)
|
|
211
|
+
|
|
212
|
+
async with asyncio.timeout(execution_timeout):
|
|
213
|
+
return await self._run_code(code, env, timeout=execution_timeout)
|
|
214
|
+
|
|
215
|
+
async def execute_command(
|
|
216
|
+
self,
|
|
217
|
+
command: str,
|
|
218
|
+
*,
|
|
219
|
+
timeout: float = 30, # NOSONAR - public API; execution uses asyncio.timeout below.
|
|
220
|
+
env: dict[str, str] | None = None,
|
|
221
|
+
) -> SandboxExecutionResult:
|
|
222
|
+
"""Execute a shell command in the sandbox.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
command: Shell command to execute.
|
|
226
|
+
timeout: Command timeout in seconds.
|
|
227
|
+
env: Optional environment variables.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
SandboxExecutionResult containing stdout, stderr, and exit_code.
|
|
231
|
+
|
|
232
|
+
Notes:
|
|
233
|
+
Delegates to the provider-neutral sandbox command surface when
|
|
234
|
+
available, otherwise falls back to ``execute_code`` with
|
|
235
|
+
``subprocess.run`` inside the sandbox.
|
|
236
|
+
"""
|
|
237
|
+
return await self._run_with_stale_runtime_recovery(
|
|
238
|
+
lambda: self._execute_command_impl(command, command_timeout=timeout, env=env),
|
|
239
|
+
operation="execute_command",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def _execute_command_impl(
|
|
243
|
+
self,
|
|
244
|
+
command: str,
|
|
245
|
+
*,
|
|
246
|
+
command_timeout: float,
|
|
247
|
+
env: dict[str, str] | None,
|
|
248
|
+
) -> SandboxExecutionResult:
|
|
249
|
+
"""Execute one shell command attempt without stale-runtime recovery."""
|
|
250
|
+
if self._sandbox is None:
|
|
251
|
+
await self._ensure_sandbox(self._template, timeout_budget_seconds=command_timeout)
|
|
252
|
+
|
|
253
|
+
if self._sandbox is None:
|
|
254
|
+
raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
|
|
255
|
+
|
|
256
|
+
# Refresh sandbox lease before execution so the VM doesn't expire mid-run
|
|
257
|
+
await self._apply_sandbox_timeout()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
# Use asyncio.timeout context manager for cancellation (S6966)
|
|
261
|
+
async with asyncio.timeout(command_timeout):
|
|
262
|
+
execute_command = getattr(self._sandbox, "execute_command", None)
|
|
263
|
+
if callable(execute_command):
|
|
264
|
+
result = await execute_command(command, env=env, timeout=command_timeout)
|
|
265
|
+
return self._to_command_result(
|
|
266
|
+
result,
|
|
267
|
+
timeout_seconds=self._normalize_command_timeout(command_timeout),
|
|
268
|
+
timeout_source=E2B_COMMAND_TIMEOUT_SOURCE,
|
|
269
|
+
)
|
|
270
|
+
return await self._execute_command_via_code(command, env=env, timeout=command_timeout)
|
|
271
|
+
except TimeoutError:
|
|
272
|
+
return self._timeout_result(
|
|
273
|
+
execution_backend=E2B_COMMANDS_BACKEND,
|
|
274
|
+
timeout_source=LOCAL_ASYNCIO_TIMEOUT_SOURCE,
|
|
275
|
+
timeout_seconds=command_timeout,
|
|
276
|
+
stderr=f"Command execution exceeded local timeout of {command_timeout}s",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
async def execute_python_file(
|
|
280
|
+
self,
|
|
281
|
+
*,
|
|
282
|
+
code: str,
|
|
283
|
+
entrypoint_path: str,
|
|
284
|
+
timeout: float = 300, # NOSONAR - public API; execution uses asyncio.timeout below.
|
|
285
|
+
files: dict[str, str] | None = None,
|
|
286
|
+
env: dict[str, str] | None = None,
|
|
287
|
+
template: str | None = None,
|
|
288
|
+
) -> SandboxExecutionResult:
|
|
289
|
+
"""Upload and execute a Python entrypoint through the generic sandbox runtime."""
|
|
290
|
+
return await self._run_with_stale_runtime_recovery(
|
|
291
|
+
lambda: self._execute_python_file_impl(
|
|
292
|
+
code=code,
|
|
293
|
+
entrypoint_path=entrypoint_path,
|
|
294
|
+
execution_timeout=timeout,
|
|
295
|
+
files=files,
|
|
296
|
+
env=env,
|
|
297
|
+
template=template,
|
|
298
|
+
),
|
|
299
|
+
operation="execute_python_file",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
async def _execute_python_file_impl(
|
|
303
|
+
self,
|
|
304
|
+
*,
|
|
305
|
+
code: str,
|
|
306
|
+
entrypoint_path: str,
|
|
307
|
+
execution_timeout: float,
|
|
308
|
+
files: dict[str, str] | None,
|
|
309
|
+
env: dict[str, str] | None,
|
|
310
|
+
template: str | None,
|
|
311
|
+
) -> SandboxExecutionResult:
|
|
312
|
+
"""Execute one Python entrypoint attempt without stale-runtime recovery."""
|
|
313
|
+
if self._sandbox is None:
|
|
314
|
+
await self._ensure_sandbox(template or self._template, timeout_budget_seconds=execution_timeout)
|
|
315
|
+
|
|
316
|
+
if self._sandbox is None:
|
|
317
|
+
raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
|
|
318
|
+
|
|
319
|
+
await self._apply_sandbox_timeout()
|
|
320
|
+
|
|
321
|
+
files_to_upload = dict(files or {})
|
|
322
|
+
files_to_upload[entrypoint_path] = code
|
|
323
|
+
if files_to_upload:
|
|
324
|
+
await self._upload_files(files_to_upload)
|
|
325
|
+
|
|
326
|
+
command = "python " + json.dumps(entrypoint_path)
|
|
327
|
+
try:
|
|
328
|
+
async with asyncio.timeout(execution_timeout):
|
|
329
|
+
return await self._execute_command_via_code(command, env=env, timeout=execution_timeout)
|
|
330
|
+
except TimeoutError:
|
|
331
|
+
return self._timeout_result(
|
|
332
|
+
execution_backend=E2B_COMMANDS_BACKEND,
|
|
333
|
+
timeout_source=LOCAL_ASYNCIO_TIMEOUT_SOURCE,
|
|
334
|
+
timeout_seconds=execution_timeout,
|
|
335
|
+
stderr=f"Python entrypoint execution exceeded local timeout of {execution_timeout}s",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
async def _run_with_stale_runtime_recovery(
|
|
339
|
+
self,
|
|
340
|
+
attempt: Callable[[], Awaitable[SandboxExecutionResult]],
|
|
341
|
+
*,
|
|
342
|
+
operation: str,
|
|
343
|
+
) -> SandboxExecutionResult:
|
|
344
|
+
"""Run one sandbox operation and retry once after stale transport reset."""
|
|
345
|
+
try:
|
|
346
|
+
result = await attempt()
|
|
347
|
+
except Exception as exc:
|
|
348
|
+
if self._is_stale_runtime_error(exc):
|
|
349
|
+
logger.info("%s: detected stale runtime/transport (%r), resetting and retrying once", operation, exc)
|
|
350
|
+
self._reset_for_new_loop()
|
|
351
|
+
return await attempt()
|
|
352
|
+
raise
|
|
353
|
+
if self._is_stale_runtime_result(
|
|
354
|
+
stdout=result.stdout,
|
|
355
|
+
stderr=result.stderr,
|
|
356
|
+
exit_code=result.exit_code,
|
|
357
|
+
diagnostics=result.diagnostics,
|
|
358
|
+
):
|
|
359
|
+
logger.info("%s: detected stale runtime/transport result, resetting and retrying once", operation)
|
|
360
|
+
self._reset_for_new_loop()
|
|
361
|
+
return await attempt()
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
@staticmethod
|
|
365
|
+
def _normalize_e2b_timeout(execution_timeout: float) -> int:
|
|
366
|
+
timeout_seconds = float(execution_timeout)
|
|
367
|
+
if timeout_seconds <= 0:
|
|
368
|
+
return 0
|
|
369
|
+
return math.ceil(timeout_seconds)
|
|
370
|
+
|
|
371
|
+
@classmethod
|
|
372
|
+
def _normalize_command_timeout(cls, execution_timeout: float) -> int:
|
|
373
|
+
return cls._normalize_e2b_timeout(execution_timeout)
|
|
374
|
+
|
|
375
|
+
def _command_diagnostics(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
timeout_seconds: float | int,
|
|
379
|
+
timeout_source: str,
|
|
380
|
+
timed_out: bool,
|
|
381
|
+
) -> dict[str, Any]:
|
|
382
|
+
active_timeout_seconds = float(timeout_seconds) if timed_out else None
|
|
383
|
+
diagnostics = self._execution_diagnostics(
|
|
384
|
+
execution_backend=E2B_COMMANDS_BACKEND,
|
|
385
|
+
timeout_source=timeout_source if timed_out else None,
|
|
386
|
+
timeout_seconds=active_timeout_seconds,
|
|
387
|
+
)
|
|
388
|
+
diagnostics["e2b_command_timeout_seconds"] = None if timeout_seconds == 0 else float(timeout_seconds)
|
|
389
|
+
diagnostics["e2b_request_timeout_seconds"] = None
|
|
390
|
+
return diagnostics
|
|
391
|
+
|
|
392
|
+
def _to_command_result(
|
|
393
|
+
self,
|
|
394
|
+
result: Any,
|
|
395
|
+
*,
|
|
396
|
+
timeout_seconds: float | int,
|
|
397
|
+
timeout_source: str,
|
|
398
|
+
) -> SandboxExecutionResult:
|
|
399
|
+
stderr = str(getattr(result, "stderr", "") or "")
|
|
400
|
+
error = str(getattr(result, "error", "") or "")
|
|
401
|
+
if error:
|
|
402
|
+
stderr = f"{stderr}\n{error}" if stderr else error
|
|
403
|
+
|
|
404
|
+
exit_code = getattr(result, "exit_code", None)
|
|
405
|
+
if exit_code is None:
|
|
406
|
+
exit_code = getattr(result, "returncode", 0)
|
|
407
|
+
exit_code = int(exit_code or 0)
|
|
408
|
+
timed_out = getattr(result, "status", None) == ExecutionStatus.TIMEOUT or (
|
|
409
|
+
exit_code != 0 and self._looks_like_timeout(stderr)
|
|
410
|
+
)
|
|
411
|
+
diagnostics = self._command_diagnostics(
|
|
412
|
+
timeout_seconds=timeout_seconds,
|
|
413
|
+
timeout_source=timeout_source,
|
|
414
|
+
timed_out=timed_out,
|
|
415
|
+
)
|
|
416
|
+
self._mark_stale_runtime_diagnostics(diagnostics, stderr=stderr, error_text=error, exit_code=exit_code)
|
|
417
|
+
sandbox_result = SandboxExecutionResult(
|
|
418
|
+
stdout=str(getattr(result, "stdout", "") or ""),
|
|
419
|
+
stderr=stderr,
|
|
420
|
+
exit_code=exit_code,
|
|
421
|
+
timed_out=timed_out,
|
|
422
|
+
execution_backend=E2B_COMMANDS_BACKEND,
|
|
423
|
+
timeout_source=timeout_source if timed_out else None,
|
|
424
|
+
timeout_seconds=float(timeout_seconds) if timed_out else None,
|
|
425
|
+
diagnostics=diagnostics,
|
|
426
|
+
)
|
|
427
|
+
return sandbox_result
|
|
428
|
+
|
|
429
|
+
def _timeout_result(
|
|
430
|
+
self,
|
|
431
|
+
*,
|
|
432
|
+
execution_backend: str,
|
|
433
|
+
timeout_source: str,
|
|
434
|
+
timeout_seconds: float,
|
|
435
|
+
stderr: str,
|
|
436
|
+
) -> SandboxExecutionResult:
|
|
437
|
+
return SandboxExecutionResult(
|
|
438
|
+
stdout="",
|
|
439
|
+
stderr=stderr,
|
|
440
|
+
exit_code=1,
|
|
441
|
+
timed_out=True,
|
|
442
|
+
execution_backend=execution_backend,
|
|
443
|
+
timeout_source=timeout_source,
|
|
444
|
+
timeout_seconds=float(timeout_seconds),
|
|
445
|
+
diagnostics=self._execution_diagnostics(
|
|
446
|
+
execution_backend=execution_backend,
|
|
447
|
+
timeout_source=timeout_source,
|
|
448
|
+
timeout_seconds=float(timeout_seconds),
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def _execution_diagnostics(
|
|
453
|
+
self,
|
|
454
|
+
*,
|
|
455
|
+
execution_backend: str,
|
|
456
|
+
timeout_source: str | None = None,
|
|
457
|
+
timeout_seconds: float | None = None,
|
|
458
|
+
) -> dict[str, Any]:
|
|
459
|
+
diagnostics: dict[str, Any] = {
|
|
460
|
+
"execution_backend": execution_backend,
|
|
461
|
+
"sandbox_timeout_seconds": float(self._sandbox_timeout),
|
|
462
|
+
}
|
|
463
|
+
if self._sandbox is not None:
|
|
464
|
+
diagnostics["sandbox_id"] = self._sandbox_identifier(self._sandbox)
|
|
465
|
+
if timeout_source is not None:
|
|
466
|
+
diagnostics["timeout_source"] = timeout_source
|
|
467
|
+
if timeout_seconds is not None:
|
|
468
|
+
diagnostics["timeout_seconds"] = float(timeout_seconds)
|
|
469
|
+
return diagnostics
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def _looks_like_timeout(text: str) -> bool:
|
|
473
|
+
lowered = text.lower()
|
|
474
|
+
return any(marker in lowered for marker in BACKEND_TIMEOUT_MARKERS)
|
|
475
|
+
|
|
476
|
+
async def cleanup(self) -> None:
|
|
477
|
+
"""Destroy the sandbox and release resources."""
|
|
478
|
+
if self._sandbox is not None:
|
|
479
|
+
try:
|
|
480
|
+
logger.info("Destroying sandbox")
|
|
481
|
+
await self._sandbox.terminate()
|
|
482
|
+
except Exception as e:
|
|
483
|
+
logger.warning(f"Error destroying sandbox: {e}")
|
|
484
|
+
finally:
|
|
485
|
+
# Reset any process-wide SDK transport state bound to this event loop
|
|
486
|
+
# (no-op for backends without one). Called via the generic interface.
|
|
487
|
+
self._sandbox.reset_transport()
|
|
488
|
+
self._sandbox = None
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def _is_stale_runtime_error(error: BaseException) -> bool:
|
|
492
|
+
"""Return True when *error* looks like a closed-transport or closed-loop failure.
|
|
493
|
+
|
|
494
|
+
Covers uvloop ``TCPTransport closed=True ... handler is closed``,
|
|
495
|
+
asyncio ``Event loop is closed`` / cross-loop binding errors,
|
|
496
|
+
and anyio/httpcore closed-resource markers.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
error: The exception raised by a sandbox or transport operation.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
True when the error matches a known stale-transport/loop signature.
|
|
503
|
+
"""
|
|
504
|
+
current: BaseException | None = error
|
|
505
|
+
seen: set[int] = set()
|
|
506
|
+
while current is not None and id(current) not in seen:
|
|
507
|
+
seen.add(id(current))
|
|
508
|
+
message = str(current).lower()
|
|
509
|
+
if any(marker in message for marker in _CLOSEABLE_TRANSPORT_ERROR_MARKERS):
|
|
510
|
+
return True
|
|
511
|
+
current = current.__cause__ or current.__context__
|
|
512
|
+
return False
|
|
513
|
+
|
|
514
|
+
@staticmethod
|
|
515
|
+
def _is_stale_runtime_result(
|
|
516
|
+
*,
|
|
517
|
+
stdout: str = "",
|
|
518
|
+
stderr: str = "",
|
|
519
|
+
exit_code: int = 0,
|
|
520
|
+
diagnostics: dict[str, Any] | None = None,
|
|
521
|
+
) -> bool:
|
|
522
|
+
"""Return True when a failed result was produced by stale transport state."""
|
|
523
|
+
_ = stdout, stderr
|
|
524
|
+
if exit_code == 0:
|
|
525
|
+
return False
|
|
526
|
+
return diagnostics is not None and diagnostics.get(STALE_RUNTIME_TRANSPORT_DIAGNOSTIC) is True
|
|
527
|
+
|
|
528
|
+
@staticmethod
|
|
529
|
+
def _looks_like_stale_runtime_output(text: str) -> bool:
|
|
530
|
+
"""Return True when non-user backend error text matches stale transport output."""
|
|
531
|
+
lowered = text.lower()
|
|
532
|
+
return any(all(marker in lowered for marker in group) for group in _STALE_RUNTIME_RESULT_MARKER_GROUPS)
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def _looks_like_stale_runtime_stderr(text: str) -> bool:
|
|
536
|
+
"""Return True when backend stderr matches the observed stale-loop result shape."""
|
|
537
|
+
lowered = text.lower()
|
|
538
|
+
return "asyncio.locks.event" in lowered and BOUND_TO_DIFFERENT_EVENT_LOOP_MARKER in lowered
|
|
539
|
+
|
|
540
|
+
def _sandbox_result_timeout_source(self, status: ExecutionStatus, stderr: str) -> str | None:
|
|
541
|
+
"""Return the timeout source for a code-interpreter result."""
|
|
542
|
+
if status == ExecutionStatus.TIMEOUT:
|
|
543
|
+
return CODE_INTERPRETER_TIMEOUT_SOURCE
|
|
544
|
+
if self._looks_like_timeout(stderr):
|
|
545
|
+
return CODE_INTERPRETER_BACKEND_TIMEOUT_SOURCE
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
def _mark_stale_runtime_diagnostics(
|
|
549
|
+
self,
|
|
550
|
+
diagnostics: dict[str, Any],
|
|
551
|
+
*,
|
|
552
|
+
error: BaseException | None = None,
|
|
553
|
+
error_text: str = "",
|
|
554
|
+
stderr: str = "",
|
|
555
|
+
exit_code: int | None = None,
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Mark diagnostics when the failure looks like a stale runtime transport."""
|
|
558
|
+
if error is not None:
|
|
559
|
+
if type(error).__name__ != "CommandExitException" and self._is_stale_runtime_error(error):
|
|
560
|
+
diagnostics[STALE_RUNTIME_TRANSPORT_DIAGNOSTIC] = True
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
if exit_code is None or exit_code == 0:
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
if (error_text and self._looks_like_stale_runtime_output(error_text)) or (
|
|
567
|
+
stderr and self._looks_like_stale_runtime_stderr(stderr)
|
|
568
|
+
):
|
|
569
|
+
diagnostics[STALE_RUNTIME_TRANSPORT_DIAGNOSTIC] = True
|
|
570
|
+
|
|
571
|
+
def _reset_for_new_loop(self) -> None:
|
|
572
|
+
"""Clear loop-bound sandbox state so the next call reconnects on the current loop.
|
|
573
|
+
|
|
574
|
+
Does NOT terminate the remote sandbox — that VM may still be alive. Only
|
|
575
|
+
the local SDK client state is discarded. Callers must re-initialise
|
|
576
|
+
through the normal ``_create_sandbox`` path on the next operation.
|
|
577
|
+
"""
|
|
578
|
+
sandbox = self._sandbox
|
|
579
|
+
self._sandbox = None
|
|
580
|
+
if sandbox is not None:
|
|
581
|
+
try:
|
|
582
|
+
# Clear any process-wide SDK transport singleton bound to the dead loop
|
|
583
|
+
# (no-op for backends without one). Called via the generic interface.
|
|
584
|
+
sandbox.reset_transport()
|
|
585
|
+
except Exception as exc:
|
|
586
|
+
logger.warning("Ignoring transport reset failure during runtime reset: %s", exc)
|
|
587
|
+
for listener in tuple(self._reset_listeners):
|
|
588
|
+
try:
|
|
589
|
+
listener()
|
|
590
|
+
except Exception as exc:
|
|
591
|
+
logger.warning("Ignoring runtime reset listener failure: %s", exc)
|
|
592
|
+
|
|
593
|
+
def _register_reset_listener(self, callback: Callable[[], None]) -> None:
|
|
594
|
+
"""Register a callback that should run when loop-bound runtime state is reset."""
|
|
595
|
+
if callback not in self._reset_listeners:
|
|
596
|
+
self._reset_listeners.append(callback)
|
|
597
|
+
|
|
598
|
+
@staticmethod
|
|
599
|
+
def _sandbox_identifier(sandbox: Any) -> str:
|
|
600
|
+
"""Return a best-effort sandbox identifier from the sandbox interface."""
|
|
601
|
+
return str(getattr(sandbox, "sandbox_id", None) or "unknown")
|
|
602
|
+
|
|
603
|
+
async def download_file(self, file_path: str) -> bytes | None:
|
|
604
|
+
"""Download a file's bytes from the sandbox (binary-safe)."""
|
|
605
|
+
if self._sandbox is None:
|
|
606
|
+
raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
|
|
607
|
+
return await self._sandbox.download_file(file_path)
|
|
608
|
+
|
|
609
|
+
@property
|
|
610
|
+
def is_active(self) -> bool:
|
|
611
|
+
"""Check if a sandbox is currently active."""
|
|
612
|
+
return self._sandbox is not None
|
|
613
|
+
|
|
614
|
+
@property
|
|
615
|
+
def sandbox(self) -> CodeInterpreterSandbox | None:
|
|
616
|
+
"""Return the underlying sandbox instance, if active."""
|
|
617
|
+
return self._sandbox
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def created_with_template(self) -> bool:
|
|
621
|
+
"""Whether the active sandbox was created from the requested template."""
|
|
622
|
+
return self._sandbox_created_with_template
|
|
623
|
+
|
|
624
|
+
def _should_skip_default_package_install(self, template: str | None) -> bool:
|
|
625
|
+
"""Determine whether to skip package installation for the default template."""
|
|
626
|
+
if not self._sandbox_created_with_template:
|
|
627
|
+
return False
|
|
628
|
+
|
|
629
|
+
if template != DEFAULT_SANDBOX_TEMPLATE:
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
return self._packages == list(DEFAULT_SANDBOX_PACKAGES)
|
|
633
|
+
|
|
634
|
+
async def _create_sandbox(
|
|
635
|
+
self, template: str | None = None, *, timeout_budget_seconds: float | None = None
|
|
636
|
+
) -> None:
|
|
637
|
+
"""Create a new sandbox via the selected provider factory.
|
|
638
|
+
|
|
639
|
+
Implements canonical runtime rules:
|
|
640
|
+
- If template provided, try creating sandbox with template
|
|
641
|
+
- Transient placement failures are retried inside the execution budget
|
|
642
|
+
- Real template/configuration errors fail fast (provider-owned policy)
|
|
643
|
+
- Install packages regardless of template usage
|
|
644
|
+
|
|
645
|
+
Note: Package installation occurs after sandbox creation.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
template: Optional template ID.
|
|
649
|
+
timeout_budget_seconds: Remaining execution budget for transient
|
|
650
|
+
placement retries. None disables budget-gating.
|
|
651
|
+
"""
|
|
652
|
+
logger.info("Creating sandbox (provider=%s, template=%s)", self._provider_name, template)
|
|
653
|
+
provider = get_provider(self._provider)
|
|
654
|
+
result = await provider.create(
|
|
655
|
+
SandboxConfig(
|
|
656
|
+
template=template,
|
|
657
|
+
api_key=self._api_key,
|
|
658
|
+
domain=self._domain,
|
|
659
|
+
sandbox_timeout=self._normalize_e2b_timeout(self._sandbox_timeout),
|
|
660
|
+
auto_build_template=self._auto_build_template,
|
|
661
|
+
build_base_template=self._build_base_template,
|
|
662
|
+
packages=self._packages,
|
|
663
|
+
timeout_budget_seconds=timeout_budget_seconds,
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
self._sandbox = result.sandbox
|
|
667
|
+
self._sandbox_created_with_template = result.created_with_template
|
|
668
|
+
await self._apply_sandbox_timeout()
|
|
669
|
+
await self._post_sandbox_setup(template)
|
|
670
|
+
|
|
671
|
+
async def _ensure_sandbox(self, template: str | None, *, timeout_budget_seconds: float | None = None) -> None:
|
|
672
|
+
"""Create a sandbox on demand via the configured provider factory."""
|
|
673
|
+
if self._sandbox is not None:
|
|
674
|
+
return
|
|
675
|
+
await self._create_sandbox(template, timeout_budget_seconds=timeout_budget_seconds)
|
|
676
|
+
|
|
677
|
+
async def _post_sandbox_setup(self, template: str | None) -> None:
|
|
678
|
+
"""Run package-selector and package installation after sandbox creation."""
|
|
679
|
+
actual_template = template if self._sandbox_created_with_template else None
|
|
680
|
+
if self._package_selector is not None:
|
|
681
|
+
try:
|
|
682
|
+
self._packages = self._package_selector(actual_template)
|
|
683
|
+
logger.info(
|
|
684
|
+
f"Package selector called with actual_template={actual_template}, "
|
|
685
|
+
f"selected packages: {self._packages}"
|
|
686
|
+
)
|
|
687
|
+
except Exception as e:
|
|
688
|
+
logger.exception("Package selector failed: %s", e)
|
|
689
|
+
await self.cleanup()
|
|
690
|
+
raise
|
|
691
|
+
|
|
692
|
+
await self.configure_packages(self._packages, template=actual_template)
|
|
693
|
+
|
|
694
|
+
async def configure_packages(
|
|
695
|
+
self,
|
|
696
|
+
packages: list[str] | None,
|
|
697
|
+
*,
|
|
698
|
+
template: str | None,
|
|
699
|
+
package_install_timeout: float | None = None,
|
|
700
|
+
) -> None:
|
|
701
|
+
"""Configure and install packages in the current sandbox.
|
|
702
|
+
|
|
703
|
+
This public seam lets shared-runtime callers, including PTC adapters,
|
|
704
|
+
reuse the same package compatibility, skip, validation, and install rules
|
|
705
|
+
as runtime-owned sandbox setup.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
packages: Packages to install. ``None`` or an empty list skips install.
|
|
709
|
+
template: Effective template used to create the sandbox, or ``None``
|
|
710
|
+
when the sandbox uses the default/fallback E2B environment.
|
|
711
|
+
package_install_timeout: Optional package install timeout override.
|
|
712
|
+
``None`` keeps the runtime's existing timeout.
|
|
713
|
+
"""
|
|
714
|
+
self._packages = packages
|
|
715
|
+
if package_install_timeout is not None:
|
|
716
|
+
self._package_install_timeout = package_install_timeout
|
|
717
|
+
|
|
718
|
+
# Validate native-tool compatibility after packages are selected
|
|
719
|
+
await self._validate_native_tool_compatibility(template)
|
|
720
|
+
|
|
721
|
+
if self._packages:
|
|
722
|
+
if self._should_skip_default_package_install(template):
|
|
723
|
+
logger.info("Skipping package install (default template already includes defaults)")
|
|
724
|
+
else:
|
|
725
|
+
await self._install_packages()
|
|
726
|
+
|
|
727
|
+
async def configure_sandbox_timeout(self, sandbox_timeout: float) -> None:
|
|
728
|
+
"""Update the sandbox lease timeout used by this runtime."""
|
|
729
|
+
self._sandbox_timeout = sandbox_timeout
|
|
730
|
+
await self._apply_sandbox_timeout()
|
|
731
|
+
|
|
732
|
+
async def _validate_native_tool_compatibility(self, template: str | None) -> None:
|
|
733
|
+
"""Validate that native AIP tools are only used with the compatible baseline template.
|
|
734
|
+
|
|
735
|
+
Native AIP tools require ``DEFAULT_SANDBOX_TEMPLATE``.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
template: The template alias requested by the caller.
|
|
739
|
+
|
|
740
|
+
Raises:
|
|
741
|
+
RuntimeError: If native AIP tools are requested but the effective template
|
|
742
|
+
is fallback/default or a non-compatible alias.
|
|
743
|
+
"""
|
|
744
|
+
if not self._packages:
|
|
745
|
+
return
|
|
746
|
+
|
|
747
|
+
# Check if native AIP tools package is in the selected packages
|
|
748
|
+
has_native_tools = any(pkg.startswith(("aip-agents", "aip-agents-binary")) for pkg in self._packages)
|
|
749
|
+
|
|
750
|
+
if has_native_tools and not self._sandbox_created_with_template:
|
|
751
|
+
error_msg = (
|
|
752
|
+
f"Native AIP tools require a compatible sandbox template "
|
|
753
|
+
f"('{DEFAULT_SANDBOX_TEMPLATE}'), but sandbox fell back to the default "
|
|
754
|
+
f"E2B template which uses an incompatible Python version. "
|
|
755
|
+
f"Ensure the template '{DEFAULT_SANDBOX_TEMPLATE}' is built and available, "
|
|
756
|
+
f"or disable native tools."
|
|
757
|
+
)
|
|
758
|
+
logger.error(f"ptc_native_runtime_incompatible: template_fallback=True, requested_template={template}")
|
|
759
|
+
await self.cleanup()
|
|
760
|
+
raise RuntimeError(error_msg)
|
|
761
|
+
|
|
762
|
+
if has_native_tools and template is not None and template != DEFAULT_SANDBOX_TEMPLATE:
|
|
763
|
+
error_msg = (
|
|
764
|
+
f"Native AIP tools require template '{DEFAULT_SANDBOX_TEMPLATE}', "
|
|
765
|
+
f"but received template '{template}'. "
|
|
766
|
+
f"Use template='{DEFAULT_SANDBOX_TEMPLATE}' or omit template to use the default."
|
|
767
|
+
)
|
|
768
|
+
logger.error(
|
|
769
|
+
f"ptc_native_runtime_incompatible: template_alias={template}, expected_alias={DEFAULT_SANDBOX_TEMPLATE}"
|
|
770
|
+
)
|
|
771
|
+
await self.cleanup()
|
|
772
|
+
raise RuntimeError(error_msg)
|
|
773
|
+
|
|
774
|
+
async def _install_packages(self) -> None:
|
|
775
|
+
"""Install packages in the sandbox."""
|
|
776
|
+
if self._sandbox is None:
|
|
777
|
+
raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
|
|
778
|
+
|
|
779
|
+
packages = self._packages
|
|
780
|
+
if packages is None or not packages:
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
# Validate all packages before constructing command
|
|
784
|
+
validate_package_names(packages)
|
|
785
|
+
|
|
786
|
+
# Note: packages_str is safe because packages is a controlled list from
|
|
787
|
+
# configuration, not user input.
|
|
788
|
+
packages_str = " ".join(packages)
|
|
789
|
+
logger.info(f"Installing packages in sandbox: {packages_str}")
|
|
790
|
+
|
|
791
|
+
try:
|
|
792
|
+
command = f"pip install -q {packages_str}"
|
|
793
|
+
timeout = self._package_install_timeout if self._package_install_timeout > 0 else 0
|
|
794
|
+
execute_command = getattr(self._sandbox, "execute_command", None)
|
|
795
|
+
if callable(execute_command):
|
|
796
|
+
result = await execute_command(command, timeout=timeout)
|
|
797
|
+
else:
|
|
798
|
+
result = await self._execute_command_via_code(command, env=None, timeout=timeout)
|
|
799
|
+
except Exception as e:
|
|
800
|
+
logger.exception("Error installing packages: %s", e)
|
|
801
|
+
raise
|
|
802
|
+
|
|
803
|
+
if result.exit_code != 0:
|
|
804
|
+
logger.error(f"Failed to install packages: {result.stderr}")
|
|
805
|
+
raise RuntimeError(f"Failed to install PTC packages: {result.stderr}")
|
|
806
|
+
|
|
807
|
+
logger.info("Packages installed successfully")
|
|
808
|
+
|
|
809
|
+
# Backward compatibility alias
|
|
810
|
+
_install_ptc_packages = _install_packages
|
|
811
|
+
|
|
812
|
+
async def _upload_files(self, files: dict[str, str]) -> None:
|
|
813
|
+
"""Upload files to the sandbox working directory via the generic code API.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
files: Mapping of path -> content.
|
|
817
|
+
"""
|
|
818
|
+
if self._sandbox is None:
|
|
819
|
+
raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
|
|
820
|
+
|
|
821
|
+
attachments = [self._to_attachment(path, content) for path, content in files.items()]
|
|
822
|
+
logger.debug("Uploading %s file(s) to sandbox", len(attachments))
|
|
823
|
+
paths_literal = json.dumps(list(files.keys()))
|
|
824
|
+
verify_code = (
|
|
825
|
+
"import json, os\n"
|
|
826
|
+
f"missing = [p for p in json.loads({paths_literal!r}) if not os.path.exists(p)]\n"
|
|
827
|
+
"print('\\n'.join(missing))\n"
|
|
828
|
+
"if missing:\n"
|
|
829
|
+
" raise RuntimeError('sandbox upload verification failed')\n"
|
|
830
|
+
)
|
|
831
|
+
result = await self._sandbox.execute_code(verify_code, files=attachments, upload_dir="")
|
|
832
|
+
if result.exit_code != 0:
|
|
833
|
+
missing = (result.stdout or "").strip() or "unknown"
|
|
834
|
+
raise RuntimeError(f"Sandbox file upload verification failed; missing after upload: {missing}")
|
|
835
|
+
|
|
836
|
+
@staticmethod
|
|
837
|
+
def _to_attachment(path: str, content: str) -> Attachment:
|
|
838
|
+
"""Build an ``Attachment`` for *path*/*content* (mime/extension are unused by upload)."""
|
|
839
|
+
return Attachment(
|
|
840
|
+
filename=path,
|
|
841
|
+
data=content.encode("utf-8"),
|
|
842
|
+
mime_type="text/plain",
|
|
843
|
+
extension=Path(path).suffix.lstrip("."),
|
|
844
|
+
metadata={},
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
async def _apply_sandbox_timeout(self) -> None:
|
|
848
|
+
"""Apply the configured sandbox session timeout.
|
|
849
|
+
|
|
850
|
+
Lease refresh is best-effort: it no-ops for a sandbox without
|
|
851
|
+
``set_timeout`` and delegates underlying-SDK error swallowing to the
|
|
852
|
+
sandbox's ``set_timeout`` (logs a warning, never raises).
|
|
853
|
+
"""
|
|
854
|
+
if self._sandbox is None:
|
|
855
|
+
return
|
|
856
|
+
set_timeout = getattr(self._sandbox, "set_timeout", None)
|
|
857
|
+
if set_timeout is None:
|
|
858
|
+
return
|
|
859
|
+
await set_timeout(self._normalize_e2b_timeout(self._sandbox_timeout))
|
|
860
|
+
|
|
861
|
+
async def _run_code(
|
|
862
|
+
self,
|
|
863
|
+
code: str,
|
|
864
|
+
env: dict[str, str] | None = None,
|
|
865
|
+
timeout: float = 300, # NOSONAR - forwarded to E2B SDK execute_code; not an async cancellation timeout
|
|
866
|
+
) -> SandboxExecutionResult:
|
|
867
|
+
"""Run code in the sandbox and map the result.
|
|
868
|
+
|
|
869
|
+
Delegates to the sandbox's ``execute_code`` (which captures stdout/stderr and
|
|
870
|
+
the formatted error and never raises for execution failures), then maps
|
|
871
|
+
the returned ``ExecutionResult`` to ``SandboxExecutionResult``.
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
code: Code to execute.
|
|
875
|
+
env: Environment variables.
|
|
876
|
+
timeout: Execution timeout in seconds, forwarded to the sandbox.
|
|
877
|
+
|
|
878
|
+
Returns:
|
|
879
|
+
SandboxExecutionResult with execution output.
|
|
880
|
+
"""
|
|
881
|
+
if self._sandbox is None:
|
|
882
|
+
raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
|
|
883
|
+
|
|
884
|
+
result = await self._sandbox.execute_code(code, timeout=max(1, int(timeout)), env=env)
|
|
885
|
+
return self._to_sandbox_result(result, timeout_seconds=timeout)
|
|
886
|
+
|
|
887
|
+
async def _execute_command_via_code(
|
|
888
|
+
self,
|
|
889
|
+
command: str,
|
|
890
|
+
*,
|
|
891
|
+
env: dict[str, str] | None,
|
|
892
|
+
timeout: float,
|
|
893
|
+
) -> SandboxExecutionResult:
|
|
894
|
+
"""Execute a shell command through ``execute_code`` for newer E2B wrappers."""
|
|
895
|
+
if self._sandbox is None:
|
|
896
|
+
raise RuntimeError(SANDBOX_NOT_INITIALIZED_ERROR)
|
|
897
|
+
|
|
898
|
+
env_literal = json.dumps(env or {})
|
|
899
|
+
command_literal = json.dumps(command)
|
|
900
|
+
code = f"""
|
|
901
|
+
import json
|
|
902
|
+
import os
|
|
903
|
+
import subprocess
|
|
904
|
+
import sys
|
|
905
|
+
|
|
906
|
+
env = os.environ.copy()
|
|
907
|
+
env.update(json.loads({env_literal!r}))
|
|
908
|
+
completed = subprocess.run(
|
|
909
|
+
{command_literal},
|
|
910
|
+
shell=True,
|
|
911
|
+
text=True,
|
|
912
|
+
capture_output=True,
|
|
913
|
+
timeout={float(timeout)!r},
|
|
914
|
+
env=env,
|
|
915
|
+
)
|
|
916
|
+
sys.stdout.write(completed.stdout)
|
|
917
|
+
sys.stderr.write(completed.stderr)
|
|
918
|
+
if completed.returncode:
|
|
919
|
+
raise SystemExit(completed.returncode)
|
|
920
|
+
"""
|
|
921
|
+
result = await self._run_code(code, timeout=timeout)
|
|
922
|
+
result.timeout_source = result.timeout_source or (
|
|
923
|
+
CODE_INTERPRETER_BACKEND_TIMEOUT_SOURCE if result.timed_out else None
|
|
924
|
+
)
|
|
925
|
+
if result.timed_out and result.timeout_seconds is None:
|
|
926
|
+
result.timeout_seconds = float(timeout)
|
|
927
|
+
if result.diagnostics:
|
|
928
|
+
result.diagnostics.setdefault("command_fallback", "execute_code_subprocess")
|
|
929
|
+
return result
|
|
930
|
+
|
|
931
|
+
def _to_sandbox_result(
|
|
932
|
+
self,
|
|
933
|
+
result: ExecutionResult,
|
|
934
|
+
*,
|
|
935
|
+
timeout_seconds: float | None = None,
|
|
936
|
+
) -> SandboxExecutionResult:
|
|
937
|
+
"""Map a gllm-tools ``ExecutionResult`` to ``SandboxExecutionResult``.
|
|
938
|
+
|
|
939
|
+
The formatted error (traceback + ``name: value``) is appended to stderr so
|
|
940
|
+
callers that surface stderr to the model keep seeing the full traceback.
|
|
941
|
+
"""
|
|
942
|
+
stderr = result.stderr or ""
|
|
943
|
+
if result.error:
|
|
944
|
+
stderr = f"{stderr}\n{result.error}" if stderr else result.error
|
|
945
|
+
timeout_source = self._sandbox_result_timeout_source(result.status, stderr)
|
|
946
|
+
timed_out = timeout_source is not None
|
|
947
|
+
diagnostics = self._execution_diagnostics(
|
|
948
|
+
execution_backend=E2B_CODE_INTERPRETER_BACKEND,
|
|
949
|
+
timeout_source=timeout_source,
|
|
950
|
+
timeout_seconds=float(timeout_seconds) if timed_out and timeout_seconds is not None else None,
|
|
951
|
+
)
|
|
952
|
+
self._mark_stale_runtime_diagnostics(
|
|
953
|
+
diagnostics,
|
|
954
|
+
error_text=result.error or "",
|
|
955
|
+
stderr=result.stderr or "",
|
|
956
|
+
exit_code=result.exit_code,
|
|
957
|
+
)
|
|
958
|
+
return SandboxExecutionResult(
|
|
959
|
+
stdout=result.stdout or "",
|
|
960
|
+
stderr=stderr,
|
|
961
|
+
exit_code=result.exit_code,
|
|
962
|
+
timed_out=timed_out,
|
|
963
|
+
execution_backend=E2B_CODE_INTERPRETER_BACKEND,
|
|
964
|
+
timeout_source=timeout_source,
|
|
965
|
+
timeout_seconds=float(timeout_seconds) if timed_out and timeout_seconds is not None else None,
|
|
966
|
+
diagnostics=diagnostics,
|
|
967
|
+
)
|