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.
Files changed (447) hide show
  1. aip_agents/__init__.py +66 -0
  2. aip_agents/a2a/__init__.py +19 -0
  3. aip_agents/a2a/server/__init__.py +10 -0
  4. aip_agents/a2a/server/base_executor.py +1125 -0
  5. aip_agents/a2a/server/google_adk_executor.py +198 -0
  6. aip_agents/a2a/server/langflow_executor.py +180 -0
  7. aip_agents/a2a/server/langgraph_executor.py +270 -0
  8. aip_agents/a2a/types.py +232 -0
  9. aip_agents/a2ui/__init__.py +26 -0
  10. aip_agents/a2ui/catalog_provider.py +109 -0
  11. aip_agents/a2ui/examples/0.8/booking_form.json +1 -0
  12. aip_agents/a2ui/examples/0.8/confirmation.json +1 -0
  13. aip_agents/a2ui/examples/0.8/single_column_list.json +1 -0
  14. aip_agents/a2ui/examples/0.9/booking_form.json +1 -0
  15. aip_agents/a2ui/examples/0.9/confirmation.json +1 -0
  16. aip_agents/a2ui/examples/0.9/single_column_list.json +1 -0
  17. aip_agents/a2ui/middleware.py +754 -0
  18. aip_agents/a2ui/models.py +153 -0
  19. aip_agents/a2ui/public_url.py +138 -0
  20. aip_agents/a2ui/stream_filter.py +67 -0
  21. aip_agents/agent/__init__.py +72 -0
  22. aip_agents/agent/base_agent.py +1437 -0
  23. aip_agents/agent/base_langgraph_agent.py +4048 -0
  24. aip_agents/agent/google_adk_agent.py +932 -0
  25. aip_agents/agent/google_adk_constants.py +11 -0
  26. aip_agents/agent/graph_runtime.py +185 -0
  27. aip_agents/agent/hitl/__init__.py +24 -0
  28. aip_agents/agent/hitl/config.py +28 -0
  29. aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
  30. aip_agents/agent/hitl/manager.py +532 -0
  31. aip_agents/agent/hitl/models.py +18 -0
  32. aip_agents/agent/hitl/prompt/__init__.py +9 -0
  33. aip_agents/agent/hitl/prompt/base.py +42 -0
  34. aip_agents/agent/hitl/prompt/deferred.py +73 -0
  35. aip_agents/agent/hitl/registry.py +149 -0
  36. aip_agents/agent/interface.py +138 -0
  37. aip_agents/agent/interfaces.py +65 -0
  38. aip_agents/agent/langflow_agent.py +464 -0
  39. aip_agents/agent/langgraph_react_agent.py +3741 -0
  40. aip_agents/agent/model_invocation_adapter.py +346 -0
  41. aip_agents/agent/system_instruction_context.py +34 -0
  42. aip_agents/agent/tool_execution_engine.py +587 -0
  43. aip_agents/audio_interface/__init__.py +30 -0
  44. aip_agents/audio_interface/audio_agent_adapter.py +254 -0
  45. aip_agents/audio_interface/config.py +146 -0
  46. aip_agents/audio_interface/errors.py +15 -0
  47. aip_agents/audio_interface/livekit_audio_session.py +504 -0
  48. aip_agents/audio_interface/livekit_realtime_audio_session.py +142 -0
  49. aip_agents/audio_interface/session_factory.py +40 -0
  50. aip_agents/clients/__init__.py +10 -0
  51. aip_agents/clients/langflow/__init__.py +10 -0
  52. aip_agents/clients/langflow/client.py +477 -0
  53. aip_agents/clients/langflow/types.py +18 -0
  54. aip_agents/common/__init__.py +5 -0
  55. aip_agents/common/callable_introspection.py +17 -0
  56. aip_agents/constants.py +23 -0
  57. aip_agents/context/__init__.py +5 -0
  58. aip_agents/context/budget.py +237 -0
  59. aip_agents/context/compaction.py +339 -0
  60. aip_agents/context/metadata.py +169 -0
  61. aip_agents/context/offload.py +449 -0
  62. aip_agents/context/preparation.py +196 -0
  63. aip_agents/context/prompt.py +61 -0
  64. aip_agents/context/runtime.py +149 -0
  65. aip_agents/context/session_recall.py +69 -0
  66. aip_agents/context/state.py +48 -0
  67. aip_agents/context/utils.py +44 -0
  68. aip_agents/credentials/manager.py +132 -0
  69. aip_agents/examples/__init__.py +5 -0
  70. aip_agents/examples/audio_livekit_session.py +116 -0
  71. aip_agents/examples/compare_streaming_client.py +783 -0
  72. aip_agents/examples/compare_streaming_server.py +141 -0
  73. aip_agents/examples/gl_connectors_smoke.py +148 -0
  74. aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
  75. aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
  76. aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
  77. aip_agents/examples/hello_world_a2a_google_adk_server.py +78 -0
  78. aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
  79. aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
  80. aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
  81. aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
  82. aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
  83. aip_agents/examples/hello_world_a2a_langchain_reference_server.py +104 -0
  84. aip_agents/examples/hello_world_a2a_langchain_server.py +78 -0
  85. aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +77 -0
  86. aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
  87. aip_agents/examples/hello_world_a2a_langflow_server.py +80 -0
  88. aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
  89. aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
  90. aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +91 -0
  91. aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
  92. aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
  93. aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
  94. aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
  95. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
  96. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
  97. aip_agents/examples/hello_world_a2a_langgraph_server.py +83 -0
  98. aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +78 -0
  99. aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +131 -0
  100. aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
  101. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
  102. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +250 -0
  103. aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
  104. aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +79 -0
  105. aip_agents/examples/hello_world_filesystem.py +162 -0
  106. aip_agents/examples/hello_world_google_adk.py +41 -0
  107. aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
  108. aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
  109. aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
  110. aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
  111. aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
  112. aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
  113. aip_agents/examples/hello_world_google_adk_stream.py +44 -0
  114. aip_agents/examples/hello_world_langchain.py +28 -0
  115. aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
  116. aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
  117. aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
  118. aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
  119. aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
  120. aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
  121. aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
  122. aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
  123. aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
  124. aip_agents/examples/hello_world_langchain_stream.py +36 -0
  125. aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
  126. aip_agents/examples/hello_world_langflow_agent.py +163 -0
  127. aip_agents/examples/hello_world_langgraph.py +39 -0
  128. aip_agents/examples/hello_world_langgraph_gl_connector_twitter.py +44 -0
  129. aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
  130. aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
  131. aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
  132. aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
  133. aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
  134. aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
  135. aip_agents/examples/hello_world_langgraph_skills.py +62 -0
  136. aip_agents/examples/hello_world_langgraph_stream.py +43 -0
  137. aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
  138. aip_agents/examples/hello_world_model_switch_cli.py +210 -0
  139. aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
  140. aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
  141. aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
  142. aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
  143. aip_agents/examples/hello_world_multi_agent_tool_output_client.py +57 -0
  144. aip_agents/examples/hello_world_multi_agent_tool_output_server.py +131 -0
  145. aip_agents/examples/hello_world_pii_logger.py +21 -0
  146. aip_agents/examples/hello_world_ptc.py +49 -0
  147. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  148. aip_agents/examples/hello_world_sentry.py +133 -0
  149. aip_agents/examples/hello_world_step_limits.py +273 -0
  150. aip_agents/examples/hello_world_stock_a2a_server.py +100 -0
  151. aip_agents/examples/hello_world_tool_output_client.py +55 -0
  152. aip_agents/examples/hello_world_tool_output_server.py +113 -0
  153. aip_agents/examples/hitl_demo.py +724 -0
  154. aip_agents/examples/mcp_configs/configs.py +63 -0
  155. aip_agents/examples/mcp_servers/common.py +76 -0
  156. aip_agents/examples/mcp_servers/mcp_name.py +29 -0
  157. aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
  158. aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
  159. aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
  160. aip_agents/examples/mcp_servers/mcp_time.py +10 -0
  161. aip_agents/examples/pii_demo_langgraph_client.py +69 -0
  162. aip_agents/examples/pii_demo_langgraph_server.py +125 -0
  163. aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
  164. aip_agents/examples/pii_demo_multi_agent_server.py +246 -0
  165. aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
  166. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +87 -0
  167. aip_agents/examples/tools/__init__.py +27 -0
  168. aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
  169. aip_agents/examples/tools/adk_weather_tool.py +60 -0
  170. aip_agents/examples/tools/data_generator_tool.py +103 -0
  171. aip_agents/examples/tools/data_visualization_tool.py +312 -0
  172. aip_agents/examples/tools/image_artifact_tool.py +136 -0
  173. aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
  174. aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
  175. aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
  176. aip_agents/examples/tools/langchain_weather_tool.py +48 -0
  177. aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
  178. aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
  179. aip_agents/examples/tools/multiply_tool.py +43 -0
  180. aip_agents/examples/tools/pii_demo_tools.py +189 -0
  181. aip_agents/examples/tools/random_chart_tool.py +142 -0
  182. aip_agents/examples/tools/serper_tool.py +202 -0
  183. aip_agents/examples/tools/stock_tools.py +82 -0
  184. aip_agents/examples/tools/table_generator_tool.py +167 -0
  185. aip_agents/examples/tools/time_tool.py +82 -0
  186. aip_agents/examples/tools/weather_forecast_tool.py +38 -0
  187. aip_agents/guardrails/__init__.py +83 -0
  188. aip_agents/guardrails/engines/__init__.py +69 -0
  189. aip_agents/guardrails/engines/base.py +90 -0
  190. aip_agents/guardrails/engines/nemo.py +101 -0
  191. aip_agents/guardrails/engines/phrase_matcher.py +113 -0
  192. aip_agents/guardrails/exceptions.py +39 -0
  193. aip_agents/guardrails/manager.py +163 -0
  194. aip_agents/guardrails/middleware.py +199 -0
  195. aip_agents/guardrails/schemas.py +63 -0
  196. aip_agents/guardrails/utils.py +45 -0
  197. aip_agents/integration/__init__.py +43 -0
  198. aip_agents/integration/agent.py +58 -0
  199. aip_agents/integration/guardrails.py +52 -0
  200. aip_agents/integration/hitl.py +71 -0
  201. aip_agents/integration/ptc.py +95 -0
  202. aip_agents/integration/skills.py +37 -0
  203. aip_agents/integration/storage.py +81 -0
  204. aip_agents/integration/version.py +103 -0
  205. aip_agents/mcp/__init__.py +1 -0
  206. aip_agents/mcp/client/__init__.py +60 -0
  207. aip_agents/mcp/client/base_mcp_client.py +377 -0
  208. aip_agents/mcp/client/connection_manager.py +231 -0
  209. aip_agents/mcp/client/errors.py +119 -0
  210. aip_agents/mcp/client/google_adk/__init__.py +45 -0
  211. aip_agents/mcp/client/google_adk/client.py +383 -0
  212. aip_agents/mcp/client/langchain/__init__.py +11 -0
  213. aip_agents/mcp/client/langchain/client.py +265 -0
  214. aip_agents/mcp/client/persistent_session.py +704 -0
  215. aip_agents/mcp/client/session_pool.py +358 -0
  216. aip_agents/mcp/client/transports.py +288 -0
  217. aip_agents/mcp/utils/__init__.py +7 -0
  218. aip_agents/mcp/utils/config_validator.py +139 -0
  219. aip_agents/memory/__init__.py +14 -0
  220. aip_agents/memory/adapters/__init__.py +10 -0
  221. aip_agents/memory/adapters/base_adapter.py +815 -0
  222. aip_agents/memory/adapters/mem0.py +286 -0
  223. aip_agents/memory/base.py +84 -0
  224. aip_agents/memory/constants.py +49 -0
  225. aip_agents/memory/factory.py +86 -0
  226. aip_agents/memory/guidance.py +20 -0
  227. aip_agents/memory/simple_memory.py +47 -0
  228. aip_agents/middleware/__init__.py +65 -0
  229. aip_agents/middleware/backends/__init__.py +72 -0
  230. aip_agents/middleware/backends/in_memory.py +578 -0
  231. aip_agents/middleware/backends/local_disk.py +1203 -0
  232. aip_agents/middleware/backends/protocol.py +353 -0
  233. aip_agents/middleware/backends/sandbox.py +1431 -0
  234. aip_agents/middleware/backends/sandbox_files.py +113 -0
  235. aip_agents/middleware/backends/utils.py +293 -0
  236. aip_agents/middleware/base.py +239 -0
  237. aip_agents/middleware/filesystem.py +690 -0
  238. aip_agents/middleware/manager.py +291 -0
  239. aip_agents/middleware/memory.py +532 -0
  240. aip_agents/middleware/pii_middleware.py +192 -0
  241. aip_agents/middleware/schema.py +19 -0
  242. aip_agents/middleware/skills.py +872 -0
  243. aip_agents/middleware/task_delegation.py +1656 -0
  244. aip_agents/middleware/todolist.py +276 -0
  245. aip_agents/middleware/tools/__init__.py +23 -0
  246. aip_agents/middleware/tools/edit_file.py +70 -0
  247. aip_agents/middleware/tools/execute.py +428 -0
  248. aip_agents/middleware/tools/grep_file.py +79 -0
  249. aip_agents/middleware/tools/ls.py +71 -0
  250. aip_agents/middleware/tools/read_file.py +239 -0
  251. aip_agents/middleware/tools/skill_load.py +193 -0
  252. aip_agents/middleware/tools/skill_resource.py +338 -0
  253. aip_agents/middleware/tools/write_file.py +114 -0
  254. aip_agents/middleware/vision.py +322 -0
  255. aip_agents/ptc/__init__.py +87 -0
  256. aip_agents/ptc/custom_tools.py +511 -0
  257. aip_agents/ptc/custom_tools_payload.py +499 -0
  258. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  259. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +24 -0
  260. aip_agents/ptc/custom_tools_templates/custom_init.py.template +26 -0
  261. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +149 -0
  262. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +112 -0
  263. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  264. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +87 -0
  265. aip_agents/ptc/doc_gen.py +122 -0
  266. aip_agents/ptc/exceptions.py +57 -0
  267. aip_agents/ptc/executor.py +627 -0
  268. aip_agents/ptc/mcp/__init__.py +45 -0
  269. aip_agents/ptc/mcp/sandbox_bridge.py +679 -0
  270. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  271. aip_agents/ptc/mcp/templates/mcp_client.py.template +240 -0
  272. aip_agents/ptc/naming.py +430 -0
  273. aip_agents/ptc/payload.py +26 -0
  274. aip_agents/ptc/prompt_builder.py +674 -0
  275. aip_agents/ptc/ptc_helper.py +16 -0
  276. aip_agents/ptc/sandbox_bridge.py +297 -0
  277. aip_agents/ptc/template_utils.py +33 -0
  278. aip_agents/ptc/templates/__init__.py +1 -0
  279. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  280. aip_agents/ptc/tool_def_helpers.py +101 -0
  281. aip_agents/ptc/tool_enrichment.py +163 -0
  282. aip_agents/resilience/__init__.py +10 -0
  283. aip_agents/resilience/base.py +46 -0
  284. aip_agents/resilience/tool.py +1661 -0
  285. aip_agents/sandbox/__init__.py +62 -0
  286. aip_agents/sandbox/defaults.py +220 -0
  287. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1-datasaur.Dockerfile +120 -0
  288. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1.Dockerfile +100 -0
  289. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2-datasaur.Dockerfile +143 -0
  290. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2.Dockerfile +119 -0
  291. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3-datasaur.Dockerfile +173 -0
  292. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3.Dockerfile +130 -0
  293. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.Dockerfile +164 -0
  294. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.patch_js_context.py +41 -0
  295. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.requirements.txt +29 -0
  296. aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2.Dockerfile +87 -0
  297. aip_agents/sandbox/e2b_runtime.py +27 -0
  298. aip_agents/sandbox/image_registry.py +98 -0
  299. aip_agents/sandbox/providers/__init__.py +76 -0
  300. aip_agents/sandbox/providers/base.py +392 -0
  301. aip_agents/sandbox/providers/bedrock.py +28 -0
  302. aip_agents/sandbox/providers/e2b.py +230 -0
  303. aip_agents/sandbox/providers/opensandbox.py +163 -0
  304. aip_agents/sandbox/sandbox_runtime.py +967 -0
  305. aip_agents/sandbox/template_builder.py +240 -0
  306. aip_agents/sandbox/types.py +141 -0
  307. aip_agents/sandbox/validation.py +50 -0
  308. aip_agents/schema/__init__.py +106 -0
  309. aip_agents/schema/a2a.py +58 -0
  310. aip_agents/schema/agent.py +117 -0
  311. aip_agents/schema/artifacts.py +75 -0
  312. aip_agents/schema/attachments.py +425 -0
  313. aip_agents/schema/hitl.py +157 -0
  314. aip_agents/schema/langgraph.py +47 -0
  315. aip_agents/schema/model_id.py +103 -0
  316. aip_agents/schema/model_usage.py +50 -0
  317. aip_agents/schema/runtime_metadata.py +78 -0
  318. aip_agents/schema/step_limit.py +108 -0
  319. aip_agents/schema/storage.py +40 -0
  320. aip_agents/schema/tool_payloads.py +68 -0
  321. aip_agents/sentry/__init__.py +11 -0
  322. aip_agents/sentry/sentry.py +267 -0
  323. aip_agents/skills/__init__.py +30 -0
  324. aip_agents/skills/errors.py +110 -0
  325. aip_agents/skills/installer.py +479 -0
  326. aip_agents/skills/models.py +769 -0
  327. aip_agents/skills/stager.py +224 -0
  328. aip_agents/skills/validation.py +53 -0
  329. aip_agents/storage/__init__.py +41 -0
  330. aip_agents/storage/base.py +85 -0
  331. aip_agents/storage/clients/__init__.py +12 -0
  332. aip_agents/storage/clients/minio_client.py +318 -0
  333. aip_agents/storage/config.py +62 -0
  334. aip_agents/storage/providers/__init__.py +15 -0
  335. aip_agents/storage/providers/base.py +106 -0
  336. aip_agents/storage/providers/memory.py +114 -0
  337. aip_agents/storage/providers/object_storage.py +214 -0
  338. aip_agents/tools/__init__.py +65 -0
  339. aip_agents/tools/agent_schedule_tool.py +392 -0
  340. aip_agents/tools/browser_use/__init__.py +83 -0
  341. aip_agents/tools/browser_use/action_parser.py +104 -0
  342. aip_agents/tools/browser_use/browser_use_tool.py +1118 -0
  343. aip_agents/tools/browser_use/llm_config.py +120 -0
  344. aip_agents/tools/browser_use/minio_storage.py +203 -0
  345. aip_agents/tools/browser_use/schemas.py +121 -0
  346. aip_agents/tools/browser_use/session.py +13 -0
  347. aip_agents/tools/browser_use/session_errors.py +132 -0
  348. aip_agents/tools/browser_use/steel_session_recording.py +321 -0
  349. aip_agents/tools/browser_use/streaming.py +263 -0
  350. aip_agents/tools/browser_use/structured_data_parser.py +257 -0
  351. aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
  352. aip_agents/tools/browser_use/types.py +37 -0
  353. aip_agents/tools/code_sandbox/__init__.py +48 -0
  354. aip_agents/tools/code_sandbox/code_sandbox_tool.py +350 -0
  355. aip_agents/tools/code_sandbox/constant.py +13 -0
  356. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +346 -0
  357. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +65 -0
  358. aip_agents/tools/computer_use/__init__.py +75 -0
  359. aip_agents/tools/computer_use/computer_use_tool.py +910 -0
  360. aip_agents/tools/computer_use/schemas.py +78 -0
  361. aip_agents/tools/computer_use/streaming.py +198 -0
  362. aip_agents/tools/computer_use/types.py +32 -0
  363. aip_agents/tools/constants.py +177 -0
  364. aip_agents/tools/date_range_tool.py +554 -0
  365. aip_agents/tools/document_loader/__init__.py +44 -0
  366. aip_agents/tools/document_loader/base_reader.py +302 -0
  367. aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
  368. aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
  369. aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
  370. aip_agents/tools/document_loader/pdf_splitter.py +169 -0
  371. aip_agents/tools/execute_ptc_code.py +582 -0
  372. aip_agents/tools/gl_connector/__init__.py +5 -0
  373. aip_agents/tools/gl_connector/tool.py +518 -0
  374. aip_agents/tools/gl_connector_tools.py +170 -0
  375. aip_agents/tools/memory_search/__init__.py +29 -0
  376. aip_agents/tools/memory_search/base.py +213 -0
  377. aip_agents/tools/memory_search/mem0.py +424 -0
  378. aip_agents/tools/memory_search/schema.py +81 -0
  379. aip_agents/tools/memory_search_tool.py +34 -0
  380. aip_agents/tools/streaming_common.py +577 -0
  381. aip_agents/tools/time_tool.py +117 -0
  382. aip_agents/tools/tool_config_injector.py +300 -0
  383. aip_agents/tools/web_search/__init__.py +15 -0
  384. aip_agents/tools/web_search/serper_tool.py +187 -0
  385. aip_agents/types/__init__.py +73 -0
  386. aip_agents/types/a2a_events.py +13 -0
  387. aip_agents/utils/__init__.py +91 -0
  388. aip_agents/utils/a2a_connector.py +1758 -0
  389. aip_agents/utils/artifact_helpers.py +542 -0
  390. aip_agents/utils/artifacts/__init__.py +20 -0
  391. aip_agents/utils/artifacts/tracker.py +367 -0
  392. aip_agents/utils/attachment_helpers.py +178 -0
  393. aip_agents/utils/constants.py +22 -0
  394. aip_agents/utils/datetime/__init__.py +34 -0
  395. aip_agents/utils/datetime/normalization.py +231 -0
  396. aip_agents/utils/datetime/timezone.py +206 -0
  397. aip_agents/utils/env_loader.py +27 -0
  398. aip_agents/utils/event_handler_registry.py +58 -0
  399. aip_agents/utils/file_prompt_utils.py +276 -0
  400. aip_agents/utils/file_watchers/__init__.py +33 -0
  401. aip_agents/utils/file_watchers/base.py +46 -0
  402. aip_agents/utils/file_watchers/e2b.py +128 -0
  403. aip_agents/utils/file_watchers/factory.py +39 -0
  404. aip_agents/utils/file_watchers/ls_diff.py +88 -0
  405. aip_agents/utils/file_watchers/sandbox.py +66 -0
  406. aip_agents/utils/file_watchers/watchdog.py +97 -0
  407. aip_agents/utils/final_response_builder.py +211 -0
  408. aip_agents/utils/formatter_llm_client.py +258 -0
  409. aip_agents/utils/langgraph/__init__.py +19 -0
  410. aip_agents/utils/langgraph/converter.py +399 -0
  411. aip_agents/utils/langgraph/delegation_context.py +22 -0
  412. aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
  413. aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
  414. aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
  415. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1329 -0
  416. aip_agents/utils/langgraph/tool_output_management.py +1047 -0
  417. aip_agents/utils/logger.py +195 -0
  418. aip_agents/utils/metadata/__init__.py +27 -0
  419. aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
  420. aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
  421. aip_agents/utils/metadata/activity_narrative/builder.py +822 -0
  422. aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
  423. aip_agents/utils/metadata/activity_narrative/context.py +49 -0
  424. aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
  425. aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
  426. aip_agents/utils/metadata/schemas/__init__.py +16 -0
  427. aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
  428. aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
  429. aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
  430. aip_agents/utils/metadata_helper.py +363 -0
  431. aip_agents/utils/name_preprocessor/__init__.py +17 -0
  432. aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
  433. aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
  434. aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
  435. aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
  436. aip_agents/utils/pii/__init__.py +29 -0
  437. aip_agents/utils/pii/pii_handler.py +555 -0
  438. aip_agents/utils/pii/pii_helper.py +225 -0
  439. aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
  440. aip_agents/utils/reference_helper.py +273 -0
  441. aip_agents/utils/sse_chunk_transformer.py +836 -0
  442. aip_agents/utils/step_limit_manager.py +265 -0
  443. aip_agents/utils/token_usage_helper.py +531 -0
  444. aip_agents_binary-0.0.0.dist-info/METADATA +825 -0
  445. aip_agents_binary-0.0.0.dist-info/RECORD +447 -0
  446. aip_agents_binary-0.0.0.dist-info/WHEEL +5 -0
  447. 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
+ )