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,1656 @@
1
+ """Task delegation middleware for dynamic worker spawning.
2
+
3
+ This module provides a ``task`` tool that allows a parent agent to delegate
4
+ one bounded sub-task to a worker agent at runtime, while enforcing delegation
5
+ depth, concurrency, and tool-scope constraints.
6
+
7
+ Authors:
8
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import threading
15
+ import time
16
+ import uuid
17
+ from collections.abc import AsyncIterator
18
+ from contextvars import ContextVar
19
+ from dataclasses import dataclass
20
+ from typing import Any, cast
21
+
22
+ from langchain_core.runnables import RunnableConfig
23
+ from langchain_core.tools import ArgsSchema, BaseTool
24
+ from langgraph.config import get_stream_writer
25
+ from langgraph.types import Command, StreamWriter
26
+ from pydantic import BaseModel, Field, SkipValidation
27
+
28
+ from aip_agents.middleware.base import AgentMiddleware, ModelRequest
29
+ from aip_agents.middleware.schema import MiddlewareToolConfig
30
+ from aip_agents.schema.step_limit import StepLimitConfig
31
+ from aip_agents.utils.langgraph.tool_managers.delegation_tool_manager import (
32
+ DELEGATED_AGENT_TIMEOUT_CONFIG_ERROR_KEY,
33
+ INVALID_DELEGATED_AGENT_TIMEOUT_MESSAGE,
34
+ )
35
+ from aip_agents.utils.logger import get_logger
36
+ from aip_agents.utils.metadata_helper import MetadataFieldKeys, get_next_step_number
37
+ from aip_agents.utils.pii.pii_helper import extract_pii_mapping_from_agent_response
38
+ from aip_agents.utils.step_limit_manager import (
39
+ _DELEGATION_CHAIN_CVAR,
40
+ _DELEGATION_DEPTH_CVAR,
41
+ _REMAINING_STEP_BUDGET_CVAR,
42
+ _STEP_LIMIT_CONFIG_CVAR,
43
+ StepLimitManager,
44
+ )
45
+ from aip_agents.utils.token_usage_helper import (
46
+ MODEL_USAGE_DELTA_KEY,
47
+ MODEL_USAGE_KEY,
48
+ STEP_USAGE_KEY,
49
+ TOTAL_USAGE_KEY,
50
+ USAGE_METADATA_KEY,
51
+ )
52
+
53
+ logger = get_logger(__name__)
54
+
55
+ TASK_DELEGATION_SYSTEM_PROMPT = (
56
+ "You are a Coordinator using the `task` tool for bounded delegation.\n"
57
+ "Default strategy: decompose multi-part or research-heavy requests into delegated tracks, then synthesize worker outputs.\n"
58
+ "When a request has independent sections or subtasks, delegate each section with `task(...)` before final synthesis.\n"
59
+ "Keep parent execution focused on orchestration, validation, and final artifact assembly.\n"
60
+ "After delegating a track, do not repeat materially equivalent tool workflows in the parent; issue a follow-up task instead.\n"
61
+ "Write task calls with explicit objective, scope, constraints, and output format."
62
+ )
63
+
64
+ WORKER_DELEGATION_NOT_ALLOWED = "worker_delegation_not_allowed"
65
+ WORKER_CONCURRENCY_LIMIT_REACHED = "worker concurrency limit reached"
66
+ UNKNOWN_OR_DISALLOWED_TOOLS = "unknown_or_disallowed_tools"
67
+ INSUFFICIENT_STEP_BUDGET = "insufficient_step_budget"
68
+ DEFAULT_MAX_CONCURRENT_WORKERS = 5
69
+ DEFAULT_MAX_PENDING_WORKERS = 20
70
+ MAX_ERROR_MESSAGE_LENGTH = 400
71
+ ALLOWED_TOOL_NAME_PREFIXES = ("functions.", "function.", "tools.")
72
+ EVENT_TYPE_TOOL_CALL = "tool_call"
73
+ EVENT_TYPE_TOOL_RESULT = "tool_result"
74
+ EVENT_TYPE_FINAL_RESPONSE = "final_response"
75
+ EVENT_TYPE_CONTENT_CHUNK = "content_chunk"
76
+ EVENT_TYPE_STATUS_UPDATE = "status_update"
77
+ SUB_AGENT_EVENT_METADATA_KEY = "is_sub_agent_event"
78
+ DELEGATION_METADATA_KEY = "delegation_worker_timeout"
79
+ DELEGATION_FAILED_STATUS = "failed"
80
+ DELEGATION_TOOL_TIMEOUT_REASON = "tool_timeout"
81
+ SUB_AGENT_TERMINAL_EVENT_REMAP = {
82
+ EVENT_TYPE_FINAL_RESPONSE: EVENT_TYPE_CONTENT_CHUNK,
83
+ "error": EVENT_TYPE_STATUS_UPDATE,
84
+ "step_limit_exceeded": EVENT_TYPE_STATUS_UPDATE,
85
+ }
86
+ METADATA_INTERNAL_PREFIXES = ("__", "langgraph_", "langchain_")
87
+ METADATA_INTERNAL_KEYS = {"step_id", "previous_step_ids", "agent_name"}
88
+ _TASK_SUB_START_STEP_CVAR: ContextVar[dict[str, str] | None] = ContextVar("_task_sub_start_step_cvar", default=None)
89
+
90
+
91
+ class TaskDelegationConfig(BaseModel):
92
+ """Configuration for dynamic task delegation middleware."""
93
+
94
+ max_concurrent_workers: int = DEFAULT_MAX_CONCURRENT_WORKERS
95
+ max_pending_workers: int = DEFAULT_MAX_PENDING_WORKERS
96
+ timeout_seconds: float | None = None
97
+
98
+ def create_middleware(self, parent_agent: Any) -> TaskDelegationMiddleware:
99
+ """Create task delegation middleware for one parent agent.
100
+
101
+ Returns:
102
+ TaskDelegationMiddleware: Configured middleware instance.
103
+ """
104
+ return TaskDelegationMiddleware(
105
+ parent_agent=parent_agent,
106
+ max_concurrent_workers=self.max_concurrent_workers,
107
+ max_pending_workers=self.max_pending_workers,
108
+ worker_timeout_seconds=self.timeout_seconds,
109
+ )
110
+
111
+
112
+ class TaskToolSchema(BaseModel):
113
+ """Schema for task tool inputs.
114
+
115
+ Attributes:
116
+ query (str): Delegated task mission text.
117
+ prompt (str | None): Optional additional instruction context.
118
+ allowed_tools (list[str] | None): Optional narrowing-only allowlist.
119
+ """
120
+
121
+ query: str = Field(..., description="Delegated task mission text")
122
+ prompt: str | None = Field(default=None, description="Optional extra instruction context")
123
+ allowed_tools: list[str] | None = Field(default=None, description="Optional narrowing-only tool allowlist")
124
+
125
+
126
+ class TaskResult(BaseModel):
127
+ """Deterministic task execution result.
128
+
129
+ Attributes:
130
+ success (bool): True when delegated execution completed successfully.
131
+ result (str): Worker output text, or empty string on failure.
132
+ error (str | None): Failure reason when ``success`` is False.
133
+ """
134
+
135
+ success: bool
136
+ result: str
137
+ error: str | None = None
138
+ worker_response: dict[str, Any] | None = Field(default=None, exclude=True, repr=False)
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class WorkerExecutionPlan:
143
+ """Precomputed worker execution inputs used by ``run_task``.
144
+
145
+ Attributes:
146
+ worker_tools (list[BaseTool]): Effective toolset for worker execution.
147
+ filesystem_backend (Any | None): Shared filesystem backend when enabled.
148
+ use_shared_filesystem_middleware (bool): Whether worker should reuse parent
149
+ filesystem middleware backend.
150
+ child_budget (int): Derived worker step budget.
151
+ worker_instruction (str): Final worker system prompt.
152
+ worker_kwargs (dict[str, Any]): Child runnable kwargs.
153
+ """
154
+
155
+ worker_tools: list[BaseTool]
156
+ filesystem_backend: Any | None
157
+ use_shared_filesystem_middleware: bool
158
+ child_budget: int
159
+ worker_instruction: str
160
+ worker_kwargs: dict[str, Any]
161
+
162
+
163
+ class TaskDelegationTool(BaseTool):
164
+ """LangChain-compatible ``task`` tool wrapper.
165
+
166
+ Attributes:
167
+ name (str): Tool name exposed to the model.
168
+ description (str): Tool behavior summary and argument contract.
169
+ args_schema (ArgsSchema): Input schema used for argument validation.
170
+ middleware (TaskDelegationMiddleware): Owning middleware instance.
171
+ """
172
+
173
+ name: str = "task"
174
+ description: str = (
175
+ "Spawn an ephemeral worker for one delegated task. "
176
+ "Arguments: query (required), prompt (optional), allowed_tools (optional narrowing-only)."
177
+ )
178
+ args_schema: ArgsSchema = TaskToolSchema
179
+ tool_config_schema: type[BaseModel] = MiddlewareToolConfig
180
+ middleware: SkipValidation[TaskDelegationMiddleware] = Field(exclude=True)
181
+ metadata: dict[str, Any] | None = Field(default=None)
182
+
183
+ def _run(
184
+ self,
185
+ query: str,
186
+ prompt: str | None = None,
187
+ allowed_tools: list[str] | None = None,
188
+ config: RunnableConfig | None = None,
189
+ ) -> str | dict[str, Any] | Command:
190
+ """Execute delegated task synchronously.
191
+
192
+ Args:
193
+ query (str): Delegated task mission text.
194
+ prompt (str | None, optional): Additional worker instructions.
195
+ Defaults to None.
196
+ allowed_tools (list[str] | None, optional): Narrowing-only allowlist
197
+ for worker tools. Defaults to None.
198
+ config (RunnableConfig | None, optional): Runnable execution config.
199
+ Defaults to None.
200
+
201
+ Returns:
202
+ str | dict[str, Any] | Command: Delegation-style tool output.
203
+ """
204
+ try:
205
+ asyncio.get_running_loop()
206
+ except RuntimeError:
207
+ return asyncio.run(self._arun(query=query, prompt=prompt, allowed_tools=allowed_tools, config=config))
208
+
209
+ return self._run_arun_in_thread(
210
+ query=query,
211
+ prompt=prompt,
212
+ allowed_tools=allowed_tools,
213
+ config=config,
214
+ )
215
+
216
+ def _run_arun_in_thread(
217
+ self,
218
+ *,
219
+ query: str,
220
+ prompt: str | None,
221
+ allowed_tools: list[str] | None,
222
+ config: RunnableConfig | None,
223
+ ) -> str | dict[str, Any] | Command:
224
+ """Execute async task delegation from sync context with active event loop.
225
+
226
+ Args:
227
+ query (str): Delegated task mission text.
228
+ prompt (str | None): Optional additional worker instructions.
229
+ allowed_tools (list[str] | None): Optional narrowing-only allowlist.
230
+ config (RunnableConfig | None): Runnable execution config.
231
+
232
+ Returns:
233
+ str | dict[str, Any] | Command: Delegation-style tool output.
234
+
235
+ Raises:
236
+ RuntimeError: If thread execution returns no result.
237
+ Exception: Re-raises exceptions captured from async execution.
238
+ """
239
+ result_holder: list[str | dict[str, Any] | Command] = []
240
+ error_holder: list[Exception] = []
241
+
242
+ def _runner() -> None:
243
+ try:
244
+ result_holder.append(
245
+ asyncio.run(self._arun(query=query, prompt=prompt, allowed_tools=allowed_tools, config=config))
246
+ )
247
+ except Exception as exc:
248
+ error_holder.append(exc)
249
+
250
+ thread = threading.Thread(target=_runner, daemon=True)
251
+ thread.start()
252
+ thread.join()
253
+
254
+ if error_holder:
255
+ raise error_holder[0]
256
+ if not result_holder:
257
+ raise RuntimeError("TaskDelegationTool._run produced no result")
258
+
259
+ return result_holder[0]
260
+
261
+ async def _arun(
262
+ self,
263
+ query: str,
264
+ prompt: str | None = None,
265
+ allowed_tools: list[str] | None = None,
266
+ config: RunnableConfig | None = None,
267
+ ) -> str | dict[str, Any] | Command:
268
+ """Execute delegated task asynchronously.
269
+
270
+ Args:
271
+ query (str): Delegated task mission text.
272
+ prompt (str | None, optional): Additional worker instructions.
273
+ Defaults to None.
274
+ allowed_tools (list[str] | None, optional): Narrowing-only allowlist
275
+ for worker tools. Defaults to None.
276
+ config (RunnableConfig | None, optional): Runnable execution config.
277
+ Defaults to None.
278
+
279
+ Returns:
280
+ str | dict[str, Any] | Command: Delegation-style tool output.
281
+ """
282
+ result = await self.middleware.run_task(
283
+ query=query,
284
+ prompt=prompt,
285
+ allowed_tools=allowed_tools,
286
+ config=config,
287
+ stream_writer=self._resolve_stream_writer(),
288
+ )
289
+ if not result.success:
290
+ return result.error or "task delegation failed"
291
+
292
+ if not isinstance(result.worker_response, dict):
293
+ return result.result
294
+
295
+ return self._format_task_response_with_extras(result.result, result.worker_response)
296
+
297
+ def _format_task_response_with_extras(
298
+ self,
299
+ text_response: str,
300
+ worker_response: dict[str, Any],
301
+ ) -> str | Command:
302
+ """Format worker response into delegation-compatible tool output.
303
+
304
+ Args:
305
+ text_response (str): Worker text output.
306
+ worker_response (dict[str, Any]): Worker response payload.
307
+
308
+ Returns:
309
+ str | Command: Plain text when no extras are present; otherwise
310
+ ``Command(update=...)`` with metadata and artifacts.
311
+ """
312
+ full_state = worker_response.get("full_final_state")
313
+ if not isinstance(full_state, dict):
314
+ return text_response
315
+
316
+ metadata = full_state.get("metadata")
317
+ metadata_update = self.middleware._filter_worker_metadata(metadata) if isinstance(metadata, dict) else {}
318
+
319
+ total_usage = full_state.get("total_usage")
320
+ token_usage = total_usage if isinstance(total_usage, dict) and total_usage else None
321
+
322
+ references = full_state.get("references")
323
+ references_list = references if isinstance(references, list) else []
324
+
325
+ artifacts = full_state.get("artifacts")
326
+ artifacts_list = artifacts if isinstance(artifacts, list) else []
327
+
328
+ pii_mapping = extract_pii_mapping_from_agent_response(worker_response)
329
+
330
+ has_extras = any((metadata_update, token_usage, references_list, artifacts_list, pii_mapping))
331
+ if not has_extras:
332
+ return text_response
333
+
334
+ update_dict: dict[str, Any] = {"result": text_response}
335
+ if metadata_update:
336
+ update_dict["metadata"] = metadata_update
337
+ if token_usage:
338
+ update_dict[USAGE_METADATA_KEY] = token_usage
339
+ if references_list:
340
+ update_dict[MetadataFieldKeys.REFERENCES] = references_list
341
+ if artifacts_list:
342
+ update_dict["artifacts"] = artifacts_list
343
+ if pii_mapping:
344
+ update_dict[MetadataFieldKeys.PII_MAPPING] = pii_mapping
345
+
346
+ return Command(update=update_dict)
347
+
348
+ @staticmethod
349
+ def _resolve_stream_writer() -> StreamWriter:
350
+ """Resolve current LangGraph stream writer with no-op fallback.
351
+
352
+ Returns:
353
+ StreamWriter: Active stream writer or fallback no-op callable.
354
+ """
355
+ try:
356
+ return cast(StreamWriter, get_stream_writer())
357
+ except Exception:
358
+ logger.debug("task_delegation_stream_writer_unavailable", exc_info=True)
359
+
360
+ def _noop_writer(_: Any) -> None:
361
+ """Fallback no-op stream writer used outside streaming context."""
362
+ return None
363
+
364
+ return _noop_writer
365
+
366
+
367
+ class TaskDelegationMiddleware(AgentMiddleware):
368
+ """Middleware that injects runtime task delegation support.
369
+
370
+ The middleware adds a ``task`` tool and enforces bounded worker delegation.
371
+ It prevents nested worker delegation, applies queue/concurrency limits, and
372
+ narrows worker tools when an allowlist is provided.
373
+ """
374
+
375
+ def __init__(
376
+ self,
377
+ parent_agent: Any,
378
+ *,
379
+ max_concurrent_workers: int = DEFAULT_MAX_CONCURRENT_WORKERS,
380
+ max_pending_workers: int = DEFAULT_MAX_PENDING_WORKERS,
381
+ worker_timeout_seconds: float | None = None,
382
+ ) -> None:
383
+ """Initialize task delegation middleware.
384
+
385
+ Args:
386
+ parent_agent (Any): Parent agent instance that owns this middleware.
387
+ max_concurrent_workers (int, optional): Maximum concurrently running
388
+ workers. Defaults to 5.
389
+ max_pending_workers (int, optional): Maximum pending worker requests
390
+ allowed in addition to active workers. Defaults to 20.
391
+ worker_timeout_seconds (float | None, optional): Parent-owned timeout
392
+ budget for each dynamic worker run. Defaults to None.
393
+
394
+ Raises:
395
+ ValueError: If concurrency or pending limits are invalid.
396
+ """
397
+ if max_concurrent_workers < 1:
398
+ raise ValueError("max_concurrent_workers must be >= 1")
399
+ if max_pending_workers < 0:
400
+ raise ValueError("max_pending_workers must be >= 0")
401
+
402
+ self.parent_agent = parent_agent
403
+ self.max_concurrent_workers = max_concurrent_workers
404
+ self.max_pending_workers = max_pending_workers
405
+ self.worker_timeout_seconds = worker_timeout_seconds
406
+ self.system_prompt_additions = TASK_DELEGATION_SYSTEM_PROMPT
407
+
408
+ self._semaphore = asyncio.Semaphore(max_concurrent_workers)
409
+ self._pending_lock = asyncio.Lock()
410
+ self._pending_workers = 0
411
+ self._active_workers = 0
412
+
413
+ self.tools: list[BaseTool] = [TaskDelegationTool(middleware=self, metadata=self._build_task_tool_metadata())]
414
+
415
+ def _build_task_tool_metadata(self) -> dict[str, Any]:
416
+ """Build delegation-style timeout metadata for the dynamic task tool.
417
+
418
+ Returns:
419
+ dict[str, Any]: Tool metadata consumed by delegation timeout policy.
420
+ """
421
+ metadata: dict[str, Any] = {
422
+ "is_delegation_tool": True,
423
+ "delegated_agent_name": f"{self.parent_agent.name}_task_worker",
424
+ "tool_type": "delegation",
425
+ "delegation_manager": self.__class__.__name__,
426
+ }
427
+
428
+ if self.worker_timeout_seconds is None:
429
+ return metadata
430
+
431
+ try:
432
+ timeout_seconds = float(self.worker_timeout_seconds)
433
+ except (TypeError, ValueError):
434
+ metadata[DELEGATED_AGENT_TIMEOUT_CONFIG_ERROR_KEY] = INVALID_DELEGATED_AGENT_TIMEOUT_MESSAGE
435
+ return metadata
436
+
437
+ if timeout_seconds <= 0:
438
+ metadata[DELEGATED_AGENT_TIMEOUT_CONFIG_ERROR_KEY] = INVALID_DELEGATED_AGENT_TIMEOUT_MESSAGE
439
+ return metadata
440
+
441
+ metadata["delegated_agent_timeout_seconds"] = timeout_seconds
442
+ return metadata
443
+
444
+ def before_model(self, state: dict[str, Any]) -> dict[str, Any]:
445
+ """No-op hook before model invocation.
446
+
447
+ Args:
448
+ state (dict[str, Any]): Current graph state.
449
+
450
+ Returns:
451
+ dict[str, Any]: Empty update.
452
+ """
453
+ return {}
454
+
455
+ def modify_model_request(self, request: ModelRequest, state: dict[str, Any]) -> ModelRequest:
456
+ """Pass through model request unchanged.
457
+
458
+ Args:
459
+ request (ModelRequest): Outgoing model request payload.
460
+ state (dict[str, Any]): Current graph state.
461
+
462
+ Returns:
463
+ ModelRequest: Unmodified request.
464
+ """
465
+ return request
466
+
467
+ def after_model(self, state: dict[str, Any]) -> dict[str, Any]:
468
+ """No-op hook after model invocation.
469
+
470
+ Args:
471
+ state (dict[str, Any]): Current graph state.
472
+
473
+ Returns:
474
+ dict[str, Any]: Empty update.
475
+ """
476
+ return {}
477
+
478
+ async def run_task(
479
+ self,
480
+ *,
481
+ query: str,
482
+ prompt: str | None,
483
+ allowed_tools: list[str] | None,
484
+ config: RunnableConfig | None,
485
+ stream_writer: StreamWriter | None = None,
486
+ ) -> TaskResult:
487
+ """Run one delegated worker task with safety constraints.
488
+
489
+ Args:
490
+ query (str): Delegated task mission text.
491
+ prompt (str | None): Optional additional worker instructions.
492
+ allowed_tools (list[str] | None): Optional narrowing-only allowlist.
493
+ config (RunnableConfig | None): Runnable execution config.
494
+ stream_writer (StreamWriter | None, optional): Parent stream writer
495
+ receiving forwarded sub-agent events. Defaults to None.
496
+
497
+ Returns:
498
+ TaskResult: Execution result with success flag and error context.
499
+
500
+ Raises:
501
+ None: All internal failures are converted into ``TaskResult`` with
502
+ ``success=False``.
503
+ """
504
+ start_time = time.perf_counter()
505
+ task_id = uuid.uuid4().hex[:12]
506
+ queue_reserved = False
507
+ slot_acquired = False
508
+ sub_start_token = _TASK_SUB_START_STEP_CVAR.set({})
509
+
510
+ try:
511
+ preflight_failure = await self._validate_task_entry(task_id, start_time)
512
+ if preflight_failure is not None:
513
+ return preflight_failure
514
+ queue_reserved = True
515
+
516
+ concurrency_slot = await self._acquire_worker_slot()
517
+ slot_acquired = True
518
+
519
+ execution_plan_or_failure = self._build_worker_execution_plan(
520
+ task_id=task_id,
521
+ prompt=prompt,
522
+ allowed_tools=allowed_tools,
523
+ config=config,
524
+ start_time=start_time,
525
+ )
526
+ if isinstance(execution_plan_or_failure, TaskResult):
527
+ return execution_plan_or_failure
528
+
529
+ execution_plan = execution_plan_or_failure
530
+ self._log_worker_spawn(task_id, query, execution_plan, concurrency_slot)
531
+ return await self._execute_worker_task(
532
+ task_id=task_id,
533
+ query=query,
534
+ execution_plan=execution_plan,
535
+ stream_writer=stream_writer,
536
+ start_time=start_time,
537
+ )
538
+ finally:
539
+ if slot_acquired:
540
+ await self._release_worker_slot()
541
+ elif queue_reserved:
542
+ await self._release_pending_slot()
543
+ _TASK_SUB_START_STEP_CVAR.reset(sub_start_token)
544
+
545
+ async def _validate_task_entry(self, task_id: str, start_time: float) -> TaskResult | None:
546
+ """Validate delegation entry constraints before worker acquisition.
547
+
548
+ Args:
549
+ task_id (str): Delegation task identifier.
550
+ start_time (float): Perf counter timestamp at task start.
551
+
552
+ Returns:
553
+ TaskResult | None: Failure result when entry checks fail; otherwise
554
+ ``None``.
555
+ """
556
+ current_depth = _DELEGATION_DEPTH_CVAR.get() or 0
557
+ if current_depth > 0:
558
+ return self._failure(task_id, WORKER_DELEGATION_NOT_ALLOWED, start_time)
559
+
560
+ if not await self._reserve_queue_slot():
561
+ return self._failure(task_id, WORKER_CONCURRENCY_LIMIT_REACHED, start_time)
562
+
563
+ return None
564
+
565
+ async def _acquire_worker_slot(self) -> int:
566
+ """Acquire semaphore and convert one pending slot into active execution.
567
+
568
+ Returns:
569
+ int: Active worker count after acquisition.
570
+ """
571
+ semaphore_acquired = False
572
+ try:
573
+ await self._semaphore.acquire()
574
+ semaphore_acquired = True
575
+ async with self._pending_lock:
576
+ self._pending_workers = max(0, self._pending_workers - 1)
577
+ self._active_workers += 1
578
+ return self._active_workers
579
+ except asyncio.CancelledError:
580
+ if semaphore_acquired:
581
+ self._semaphore.release()
582
+ raise
583
+
584
+ async def _release_pending_slot(self) -> None:
585
+ """Release one reserved pending slot when worker acquisition is aborted."""
586
+ async with self._pending_lock:
587
+ self._pending_workers = max(0, self._pending_workers - 1)
588
+
589
+ async def _release_worker_slot(self) -> None:
590
+ """Release one active worker slot and return semaphore permit."""
591
+ async with self._pending_lock:
592
+ self._active_workers = max(0, self._active_workers - 1)
593
+ self._semaphore.release()
594
+
595
+ def _build_worker_execution_plan(
596
+ self,
597
+ *,
598
+ task_id: str,
599
+ prompt: str | None,
600
+ allowed_tools: list[str] | None,
601
+ config: RunnableConfig | None,
602
+ start_time: float,
603
+ ) -> WorkerExecutionPlan | TaskResult:
604
+ """Build deterministic worker execution inputs.
605
+
606
+ Args:
607
+ task_id (str): Delegation task identifier.
608
+ prompt (str | None): Optional additional worker instructions.
609
+ allowed_tools (list[str] | None): Optional narrowing-only allowlist.
610
+ config (RunnableConfig | None): Parent runnable config.
611
+ start_time (float): Perf counter timestamp at task start.
612
+
613
+ Returns:
614
+ WorkerExecutionPlan | TaskResult: Worker plan on success, otherwise
615
+ a failure ``TaskResult``.
616
+ """
617
+ worker_tools, tools_error = self._resolve_worker_tools(allowed_tools)
618
+ if tools_error is not None:
619
+ return self._failure(task_id, tools_error, start_time)
620
+
621
+ filesystem_backend, filesystem_tool_names = self._get_parent_filesystem_context()
622
+ use_shared_filesystem_middleware = filesystem_backend is not None and allowed_tools is None
623
+ if use_shared_filesystem_middleware and filesystem_tool_names:
624
+ worker_tools = self._exclude_parent_filesystem_tools(worker_tools, filesystem_tool_names)
625
+
626
+ child_budget = self._derive_child_budget()
627
+ if child_budget <= 0:
628
+ return self._failure(task_id, INSUFFICIENT_STEP_BUDGET, start_time)
629
+
630
+ return WorkerExecutionPlan(
631
+ worker_tools=worker_tools,
632
+ filesystem_backend=filesystem_backend,
633
+ use_shared_filesystem_middleware=use_shared_filesystem_middleware,
634
+ child_budget=child_budget,
635
+ worker_instruction=self._build_worker_instruction(prompt),
636
+ worker_kwargs=self._build_child_run_kwargs(config),
637
+ )
638
+
639
+ @staticmethod
640
+ def _exclude_parent_filesystem_tools(
641
+ worker_tools: list[BaseTool], filesystem_tool_names: set[str]
642
+ ) -> list[BaseTool]:
643
+ """Remove filesystem tools already provided by shared middleware.
644
+
645
+ Args:
646
+ worker_tools (list[BaseTool]): Candidate worker tools.
647
+ filesystem_tool_names (set[str]): Parent filesystem tool names.
648
+
649
+ Returns:
650
+ list[BaseTool]: Filtered worker toolset.
651
+ """
652
+ return [
653
+ tool
654
+ for tool in worker_tools
655
+ if isinstance(getattr(tool, "name", None), str) and getattr(tool, "name") not in filesystem_tool_names
656
+ ]
657
+
658
+ def _log_worker_spawn(
659
+ self,
660
+ task_id: str,
661
+ query: str,
662
+ execution_plan: WorkerExecutionPlan,
663
+ concurrency_slot: int,
664
+ ) -> None:
665
+ """Emit deterministic worker spawn telemetry.
666
+
667
+ Args:
668
+ task_id (str): Delegation task identifier.
669
+ query (str): Delegated task mission text.
670
+ execution_plan (WorkerExecutionPlan): Planned worker execution inputs.
671
+ concurrency_slot (int): Active worker slot number.
672
+ """
673
+ logger.info(
674
+ "task_delegation_spawn task_id=%s parent_thread_id=%s tools_count=%s query_length=%s concurrency_slot=%s",
675
+ task_id,
676
+ execution_plan.worker_kwargs.get("configurable", {}).get(self.parent_agent.thread_id_key),
677
+ len(execution_plan.worker_tools),
678
+ len(query),
679
+ concurrency_slot,
680
+ )
681
+
682
+ async def _execute_worker_task(
683
+ self,
684
+ *,
685
+ task_id: str,
686
+ query: str,
687
+ execution_plan: WorkerExecutionPlan,
688
+ stream_writer: StreamWriter | None,
689
+ start_time: float,
690
+ ) -> TaskResult:
691
+ """Execute worker agent using a precomputed execution plan.
692
+
693
+ Args:
694
+ task_id (str): Delegation task identifier.
695
+ query (str): Delegated task mission text.
696
+ execution_plan (WorkerExecutionPlan): Worker execution inputs.
697
+ stream_writer (StreamWriter | None): Optional parent stream writer.
698
+ start_time (float): Perf counter timestamp at task start.
699
+
700
+ Returns:
701
+ TaskResult: Success or failure result.
702
+ """
703
+ try:
704
+ from aip_agents.agent.langgraph_react_agent import LangGraphReactAgent
705
+
706
+ worker_model = self.parent_agent.model
707
+ if worker_model is None:
708
+ worker_model = self.parent_agent.lm_invoker
709
+
710
+ worker = LangGraphReactAgent(
711
+ name=f"{self.parent_agent.name}_worker_{task_id}",
712
+ instruction=execution_plan.worker_instruction,
713
+ model=worker_model,
714
+ tools=execution_plan.worker_tools,
715
+ filesystem=(
716
+ execution_plan.filesystem_backend if execution_plan.use_shared_filesystem_middleware else None
717
+ ),
718
+ task_delegation=False,
719
+ step_limit_config=StepLimitConfig(max_steps=max(1, execution_plan.child_budget)),
720
+ )
721
+ response = await self._run_worker_with_stream_trace(
722
+ worker,
723
+ query=query,
724
+ worker_kwargs=execution_plan.worker_kwargs,
725
+ stream_writer=stream_writer,
726
+ )
727
+ worker_output = response.get("output", "") if isinstance(response, dict) else str(response)
728
+ result = TaskResult(
729
+ success=True,
730
+ result=str(worker_output),
731
+ error=None,
732
+ worker_response=response if isinstance(response, dict) else None,
733
+ )
734
+
735
+ logger.debug(
736
+ "task_delegation_output task_id=%s worker_response=%r",
737
+ task_id,
738
+ response,
739
+ )
740
+ return self._success(task_id, result, start_time)
741
+ except asyncio.CancelledError:
742
+ raise
743
+ except Exception as exc:
744
+ logger.exception("task_delegation_exception task_id=%s error=%s", task_id, str(exc))
745
+ return self._failure(task_id, str(exc), start_time)
746
+
747
+ async def _reserve_queue_slot(self) -> bool:
748
+ """Reserve a bounded slot for pending work.
749
+
750
+ Returns:
751
+ bool: True when combined active and pending capacity remains
752
+ available, False when delegation capacity is saturated.
753
+ """
754
+ async with self._pending_lock:
755
+ if self._active_workers + self._pending_workers >= self.max_concurrent_workers + self.max_pending_workers:
756
+ return False
757
+ self._pending_workers += 1
758
+ return True
759
+
760
+ def _derive_child_budget(self) -> int:
761
+ """Compute worker step budget from current delegation context.
762
+
763
+ Returns:
764
+ int: Child step budget. Values less than or equal to zero are
765
+ considered insufficient for worker execution.
766
+ """
767
+ current_depth = _DELEGATION_DEPTH_CVAR.get() or 0
768
+ current_chain = list(_DELEGATION_CHAIN_CVAR.get() or ())
769
+ remaining_budget = _REMAINING_STEP_BUDGET_CVAR.get()
770
+ parent_config = _STEP_LIMIT_CONFIG_CVAR.get()
771
+
772
+ manager = StepLimitManager(
773
+ config=parent_config,
774
+ initial_delegation_depth=current_depth,
775
+ parent_step_budget=remaining_budget,
776
+ )
777
+ manager.context.delegation_chain = current_chain
778
+ return manager.get_child_budget()
779
+
780
+ def _resolve_worker_tools(self, allowed_tools: list[str] | None) -> tuple[list[BaseTool], str | None]:
781
+ """Resolve effective worker tools under narrowing constraints.
782
+
783
+ Args:
784
+ allowed_tools (list[str] | None): Optional narrowing-only allowlist.
785
+
786
+ Returns:
787
+ tuple[list[BaseTool], str | None]: A tuple of ``(tools, error)``.
788
+ ``error`` is ``None`` on success; otherwise it contains a
789
+ deterministic error message.
790
+ """
791
+ candidate_tools = self._collect_candidate_worker_tools()
792
+
793
+ tool_by_name = {tool.name: tool for tool in candidate_tools}
794
+
795
+ if allowed_tools is None:
796
+ return candidate_tools, None
797
+
798
+ canonical_names, unknown = self._resolve_requested_allowed_tool_names(allowed_tools, tool_by_name)
799
+
800
+ if unknown:
801
+ return [], f"{UNKNOWN_OR_DISALLOWED_TOOLS}: {', '.join(unknown)}"
802
+
803
+ return [tool_by_name[name] for name in canonical_names], None
804
+
805
+ def _collect_candidate_worker_tools(self) -> list[BaseTool]:
806
+ """Collect parent tools eligible for worker execution.
807
+
808
+ Returns:
809
+ list[BaseTool]: Parent tools excluding delegation entries.
810
+ """
811
+ resolved_tools = getattr(self.parent_agent, "resolved_tools", []) or []
812
+ candidate_tools: list[BaseTool] = []
813
+ for tool in resolved_tools:
814
+ if self._is_worker_eligible_tool(tool):
815
+ candidate_tools.append(tool)
816
+ return candidate_tools
817
+
818
+ @staticmethod
819
+ def _is_worker_eligible_tool(tool: Any) -> bool:
820
+ """Return whether one parent tool is eligible for worker runs.
821
+
822
+ Args:
823
+ tool (Any): Candidate tool.
824
+
825
+ Returns:
826
+ bool: True when the tool is non-delegation with valid name.
827
+ """
828
+ tool_name = getattr(tool, "name", "")
829
+ if not isinstance(tool_name, str) or not tool_name:
830
+ return False
831
+ if tool_name == "task" or tool_name.startswith("delegate_to_"):
832
+ return False
833
+ tool_metadata = getattr(tool, "metadata", None)
834
+ return not (isinstance(tool_metadata, dict) and tool_metadata.get("is_delegation_tool") is True)
835
+
836
+ def _resolve_requested_allowed_tool_names(
837
+ self,
838
+ allowed_tools: list[str],
839
+ tool_by_name: dict[str, BaseTool],
840
+ ) -> tuple[list[str], list[str]]:
841
+ """Resolve requested allowlist names into canonical tool names.
842
+
843
+ Args:
844
+ allowed_tools (list[str]): Requested tool names.
845
+ tool_by_name (dict[str, BaseTool]): Candidate tools keyed by canonical name.
846
+
847
+ Returns:
848
+ tuple[list[str], list[str]]: Canonical names and unknown names.
849
+ """
850
+ canonical_names: list[str] = []
851
+ unknown: list[str] = []
852
+ seen: set[str] = set()
853
+
854
+ normalized_names = [name for name in allowed_tools if isinstance(name, str) and name]
855
+ for requested_name in normalized_names:
856
+ canonical_name = self._normalize_allowed_tool_name(requested_name, tool_by_name)
857
+ if canonical_name is None:
858
+ unknown.append(requested_name)
859
+ continue
860
+ if canonical_name in seen:
861
+ continue # pragma: no cover
862
+ seen.add(canonical_name)
863
+ canonical_names.append(canonical_name)
864
+ return canonical_names, unknown
865
+
866
+ @staticmethod
867
+ def _normalize_allowed_tool_name(requested_name: str, tool_by_name: dict[str, BaseTool]) -> str | None:
868
+ """Normalize requested tool name against known tool map.
869
+
870
+ Args:
871
+ requested_name (str): Raw tool name requested by caller.
872
+ tool_by_name (dict[str, BaseTool]): Candidate tools keyed by name.
873
+
874
+ Returns:
875
+ str | None: Canonical tool name or ``None`` when not resolvable.
876
+ """
877
+ if requested_name in tool_by_name:
878
+ return requested_name
879
+
880
+ for prefix in ALLOWED_TOOL_NAME_PREFIXES:
881
+ if requested_name.startswith(prefix):
882
+ stripped_name = requested_name[len(prefix) :]
883
+ if stripped_name in tool_by_name:
884
+ return stripped_name
885
+
886
+ return None
887
+
888
+ def _get_parent_filesystem_context(self) -> tuple[Any | None, set[str]]:
889
+ """Return parent filesystem backend and filesystem tool names.
890
+
891
+ Returns:
892
+ tuple[Any | None, set[str]]: Parent filesystem backend (if any) and
893
+ the set of filesystem tool names exposed by parent middleware.
894
+ """
895
+ backend = getattr(self.parent_agent, "_filesystem_backend", None)
896
+ middleware = getattr(self.parent_agent, "_filesystem_middleware", None)
897
+
898
+ if backend is None or middleware is None:
899
+ return None, set()
900
+
901
+ names: set[str] = set()
902
+ for tool in getattr(middleware, "tools", []) or []:
903
+ name = getattr(tool, "name", None)
904
+ if isinstance(name, str) and name:
905
+ names.add(name)
906
+
907
+ return backend, names
908
+
909
+ def _build_worker_instruction(self, prompt: str | None) -> str:
910
+ """Build worker instruction text.
911
+
912
+ Args:
913
+ prompt (str | None): Optional additional instruction context.
914
+
915
+ Returns:
916
+ str: Worker system instruction.
917
+ """
918
+ base = (
919
+ f"You are a Worker for parent agent '{self.parent_agent.name}'.\n"
920
+ "Execute exactly one delegated track with bounded tools and limits.\n"
921
+ "Focus on direct execution and concise, source-backed output for the requested track.\n"
922
+ "Do not delegate further tasks."
923
+ )
924
+ if prompt:
925
+ return f"{base}\n\nTASK INSTRUCTION CONTEXT:\n{prompt}"
926
+ return base
927
+
928
+ def _build_child_run_kwargs(self, config: RunnableConfig | None) -> dict[str, Any]:
929
+ """Build worker run kwargs from parent config.
930
+
931
+ Args:
932
+ config (RunnableConfig | None): Parent runnable config.
933
+
934
+ Returns:
935
+ dict[str, Any]: Child run kwargs with ``configurable.thread_id`` and
936
+ optional metadata subset.
937
+ """
938
+ thread_key = getattr(self.parent_agent, "thread_id_key", "thread_id")
939
+ parent_thread_id = self._resolve_parent_thread_id(config, thread_key)
940
+ metadata_subset = self._extract_child_metadata_subset(config)
941
+
942
+ run_kwargs: dict[str, Any] = {
943
+ "configurable": {thread_key: parent_thread_id},
944
+ }
945
+ if metadata_subset:
946
+ run_kwargs["metadata"] = metadata_subset
947
+
948
+ return run_kwargs
949
+
950
+ def _resolve_parent_thread_id(self, config: RunnableConfig | None, thread_key: str) -> str:
951
+ """Resolve parent thread id from config, parent state, or fallback uuid.
952
+
953
+ Args:
954
+ config (RunnableConfig | None): Parent runnable config.
955
+ thread_key (str): Configurable thread-id key.
956
+
957
+ Returns:
958
+ str: Parent thread id.
959
+ """
960
+ config_thread_id = self._extract_config_thread_id(config, thread_key)
961
+ if config_thread_id is not None:
962
+ return config_thread_id
963
+
964
+ parent_thread_candidate = getattr(self.parent_agent, thread_key, None)
965
+ if isinstance(parent_thread_candidate, str) and parent_thread_candidate:
966
+ return parent_thread_candidate
967
+ return str(uuid.uuid4())
968
+
969
+ @staticmethod
970
+ def _extract_config_thread_id(config: RunnableConfig | None, thread_key: str) -> str | None:
971
+ """Extract thread id from runnable config configurable section.
972
+
973
+ Args:
974
+ config (RunnableConfig | None): Parent runnable config.
975
+ thread_key (str): Thread key to lookup.
976
+
977
+ Returns:
978
+ str | None: Config thread id when present and non-empty.
979
+ """
980
+ if not isinstance(config, dict):
981
+ return None
982
+ configurable = config.get("configurable")
983
+ if not isinstance(configurable, dict):
984
+ return None # pragma: no cover
985
+ candidate = configurable.get(thread_key)
986
+ if isinstance(candidate, str) and candidate:
987
+ return candidate
988
+ return None # pragma: no cover
989
+
990
+ @staticmethod
991
+ def _extract_child_metadata_subset(config: RunnableConfig | None) -> dict[str, Any]:
992
+ """Extract safe metadata keys forwarded to worker run kwargs.
993
+
994
+ Args:
995
+ config (RunnableConfig | None): Parent runnable config.
996
+
997
+ Returns:
998
+ dict[str, Any]: Metadata subset containing trace/request/run ids.
999
+ """
1000
+ if not isinstance(config, dict):
1001
+ return {}
1002
+ metadata = config.get("metadata")
1003
+ if not isinstance(metadata, dict):
1004
+ return {}
1005
+
1006
+ metadata_subset: dict[str, Any] = {}
1007
+ for key in ("trace_id", "request_id", "run_id"):
1008
+ value = metadata.get(key)
1009
+ if value is not None:
1010
+ metadata_subset[key] = value
1011
+ return metadata_subset
1012
+
1013
+ @staticmethod
1014
+ def _filter_worker_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
1015
+ """Filter worker metadata keys before propagating to parent update.
1016
+
1017
+ Args:
1018
+ metadata (dict[str, Any]): Raw worker metadata dictionary.
1019
+
1020
+ Returns:
1021
+ dict[str, Any]: Filtered metadata preserving safe linkage fields.
1022
+ """
1023
+ filtered: dict[str, Any] = {
1024
+ key: value
1025
+ for key, value in metadata.items()
1026
+ if not any(key.startswith(prefix) for prefix in METADATA_INTERNAL_PREFIXES)
1027
+ and key not in METADATA_INTERNAL_KEYS
1028
+ }
1029
+ previous_step_ids = metadata.get("previous_step_ids")
1030
+ if isinstance(previous_step_ids, list) and previous_step_ids:
1031
+ filtered["previous_step_ids"] = list(previous_step_ids)
1032
+ step_id = metadata.get("step_id")
1033
+ if step_id:
1034
+ filtered.setdefault("step_id", step_id)
1035
+ return filtered
1036
+
1037
+ def _success(self, task_id: str, result: TaskResult, start_time: float) -> TaskResult:
1038
+ """Log and return a successful task result.
1039
+
1040
+ Args:
1041
+ task_id (str): Delegation task identifier.
1042
+ result (TaskResult): Successful result payload.
1043
+ start_time (float): Perf counter timestamp at task start.
1044
+
1045
+ Returns:
1046
+ TaskResult: Unmodified successful result.
1047
+ """
1048
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
1049
+ logger.info(
1050
+ "task_delegation_result task_id=%s success=%s duration_ms=%s",
1051
+ task_id,
1052
+ result.success,
1053
+ duration_ms,
1054
+ )
1055
+ return result
1056
+
1057
+ def _failure(self, task_id: str, error: str, start_time: float) -> TaskResult:
1058
+ """Log and return a failure task result.
1059
+
1060
+ Args:
1061
+ task_id (str): Delegation task identifier.
1062
+ error (str): Failure reason text.
1063
+ start_time (float): Perf counter timestamp at task start.
1064
+
1065
+ Returns:
1066
+ TaskResult: Failure result with bounded error text.
1067
+ """
1068
+ bounded_error = error[:MAX_ERROR_MESSAGE_LENGTH]
1069
+ duration_ms = int((time.perf_counter() - start_time) * 1000)
1070
+ logger.error(
1071
+ "task_delegation_result task_id=%s success=%s duration_ms=%s error=%s",
1072
+ task_id,
1073
+ False,
1074
+ duration_ms,
1075
+ bounded_error,
1076
+ )
1077
+ return TaskResult(success=False, result="", error=bounded_error)
1078
+
1079
+ async def _run_worker_with_stream_trace(
1080
+ self,
1081
+ worker: Any,
1082
+ *,
1083
+ query: str,
1084
+ worker_kwargs: dict[str, Any],
1085
+ stream_writer: StreamWriter | None,
1086
+ ) -> dict[str, Any]:
1087
+ """Execute worker stream and collect normalized final response payload.
1088
+
1089
+ Args:
1090
+ worker (Any): Worker agent exposing ``arun_a2a_stream``.
1091
+ query (str): Delegated task mission text.
1092
+ worker_kwargs (dict[str, Any]): Child runnable kwargs.
1093
+ stream_writer (StreamWriter | None): Optional parent stream writer.
1094
+
1095
+ Returns:
1096
+ dict[str, Any]: Worker response with ``output`` and ``full_final_state``.
1097
+
1098
+ Raises:
1099
+ RuntimeError: If worker stream API is missing, invalid, or lacks
1100
+ ``final_response`` event.
1101
+ """
1102
+ stream_method = getattr(worker, "arun_a2a_stream", None)
1103
+ if not callable(stream_method):
1104
+ raise RuntimeError("Worker does not support arun_a2a_stream; task delegation requires stream mode")
1105
+
1106
+ stream = stream_method(query, **worker_kwargs)
1107
+ if not hasattr(stream, "__aiter__"):
1108
+ raise RuntimeError("Worker stream is not async-iterable; task delegation requires async stream mode")
1109
+ async_stream = cast(AsyncIterator[Any], stream)
1110
+
1111
+ stream_records: list[dict[str, Any]] = []
1112
+ final_event: dict[str, Any] | None = None
1113
+ parent_thread_id = self._extract_parent_thread_id(worker_kwargs)
1114
+
1115
+ async for chunk in async_stream:
1116
+ if not isinstance(chunk, dict):
1117
+ continue
1118
+ normalized = self._normalize_worker_stream_chunk(chunk)
1119
+ stream_records.append(normalized)
1120
+ self._forward_sub_agent_chunk(normalized, stream_writer, parent_thread_id)
1121
+
1122
+ event_type = self._normalize_event_type(normalized.get("event_type"))
1123
+ if event_type == EVENT_TYPE_FINAL_RESPONSE:
1124
+ final_event = normalized
1125
+
1126
+ if not isinstance(final_event, dict):
1127
+ raise RuntimeError(
1128
+ "No final_response event captured from worker stream (likely model/context/tool execution failure)"
1129
+ )
1130
+
1131
+ response = self._build_worker_response_from_stream_final(final_event, stream_records)
1132
+ return response
1133
+
1134
+ def _forward_sub_agent_chunk(
1135
+ self,
1136
+ chunk: dict[str, Any],
1137
+ writer: StreamWriter | None,
1138
+ parent_thread_id: str | None,
1139
+ ) -> None:
1140
+ """Forward one normalized sub-agent chunk to the parent stream.
1141
+
1142
+ Args:
1143
+ chunk (dict[str, Any]): Normalized worker stream chunk.
1144
+ writer (StreamWriter | None): Parent stream writer.
1145
+ parent_thread_id (str | None): Parent thread identifier.
1146
+ """
1147
+ if writer is None:
1148
+ return
1149
+
1150
+ event_type = self._normalize_event_type(chunk.get("event_type"))
1151
+ if event_type == EVENT_TYPE_TOOL_CALL:
1152
+ self._forward_tool_call_event(chunk, writer, parent_thread_id)
1153
+ return
1154
+ if event_type == EVENT_TYPE_TOOL_RESULT:
1155
+ self._forward_tool_result_event(chunk, writer)
1156
+ return
1157
+
1158
+ marked_chunk = dict(chunk)
1159
+ metadata = chunk.get("metadata")
1160
+ marked_metadata = dict(metadata) if isinstance(metadata, dict) else {}
1161
+ marked_metadata[SUB_AGENT_EVENT_METADATA_KEY] = True
1162
+ marked_chunk["metadata"] = marked_metadata
1163
+ normalized_chunk = self._normalize_forwarded_sub_agent_chunk(marked_chunk)
1164
+ writer(normalized_chunk)
1165
+
1166
+ @staticmethod
1167
+ def _extract_parent_thread_id(worker_kwargs: dict[str, Any]) -> str | None:
1168
+ """Extract parent thread id from child runnable kwargs.
1169
+
1170
+ Args:
1171
+ worker_kwargs (dict[str, Any]): Child runnable kwargs.
1172
+
1173
+ Returns:
1174
+ str | None: Parent thread id when present.
1175
+ """
1176
+ configurable = worker_kwargs.get("configurable") if isinstance(worker_kwargs.get("configurable"), dict) else {}
1177
+ for value in configurable.values():
1178
+ if isinstance(value, str) and value:
1179
+ return value
1180
+ return None
1181
+
1182
+ def _forward_tool_call_event(
1183
+ self,
1184
+ chunk: dict[str, Any],
1185
+ writer: StreamWriter,
1186
+ parent_thread_id: str | None,
1187
+ ) -> None:
1188
+ """Forward tool-call event using delegation-compatible envelope.
1189
+
1190
+ Args:
1191
+ chunk (dict[str, Any]): Normalized stream chunk.
1192
+ writer (StreamWriter): Parent stream writer.
1193
+ parent_thread_id (str | None): Parent thread identifier.
1194
+ """
1195
+ tool_info = chunk.get("tool_info") if isinstance(chunk.get("tool_info"), dict) else {}
1196
+ message = self._create_tool_call_message(tool_info)
1197
+ metadata = self._prepare_tool_call_metadata(chunk, tool_info, parent_thread_id)
1198
+
1199
+ a2a_event: dict[str, Any] = {
1200
+ "event_type": EVENT_TYPE_TOOL_CALL,
1201
+ "content": message,
1202
+ "metadata": metadata,
1203
+ "tool_info": tool_info,
1204
+ "is_final": False,
1205
+ "artifacts": chunk.get("artifacts"),
1206
+ MetadataFieldKeys.REFERENCES: chunk.get(MetadataFieldKeys.REFERENCES),
1207
+ STEP_USAGE_KEY: chunk.get(STEP_USAGE_KEY),
1208
+ TOTAL_USAGE_KEY: chunk.get(TOTAL_USAGE_KEY),
1209
+ MODEL_USAGE_DELTA_KEY: chunk.get(MODEL_USAGE_DELTA_KEY),
1210
+ MODEL_USAGE_KEY: chunk.get(MODEL_USAGE_KEY),
1211
+ }
1212
+ writer(a2a_event)
1213
+
1214
+ def _prepare_tool_call_metadata(
1215
+ self,
1216
+ chunk: dict[str, Any],
1217
+ tool_info: dict[str, Any],
1218
+ parent_thread_id: str | None,
1219
+ ) -> dict[str, Any]:
1220
+ """Prepare tool-call metadata with linkage and generated step id.
1221
+
1222
+ Args:
1223
+ chunk (dict[str, Any]): Normalized stream chunk.
1224
+ tool_info (dict[str, Any]): Tool info payload.
1225
+ parent_thread_id (str | None): Parent thread identifier.
1226
+
1227
+ Returns:
1228
+ dict[str, Any]: Enriched metadata for forwarded event.
1229
+ """
1230
+ metadata = dict(chunk.get("metadata") or {})
1231
+ metadata[SUB_AGENT_EVENT_METADATA_KEY] = True
1232
+ self._ensure_step_id_in_metadata(metadata, tool_info)
1233
+
1234
+ agent_name = metadata.get("agent_name")
1235
+ if isinstance(agent_name, str) and agent_name:
1236
+ self._setup_agent_linkage(metadata, agent_name, tool_info, parent_thread_id)
1237
+ else:
1238
+ self._handle_missing_agent_name(metadata)
1239
+ return metadata
1240
+
1241
+ def _ensure_step_id_in_metadata(self, metadata: dict[str, Any], tool_info: dict[str, Any]) -> None:
1242
+ """Ensure metadata has ``step_id``, generating one when missing.
1243
+
1244
+ Args:
1245
+ metadata (dict[str, Any]): Event metadata to mutate.
1246
+ tool_info (dict[str, Any]): Tool info for deterministic id generation.
1247
+ """
1248
+ if "step_id" in metadata:
1249
+ return
1250
+ raw_agent_name = metadata.get("agent_name")
1251
+ agent_name_for_id = raw_agent_name if isinstance(raw_agent_name, str) and raw_agent_name else "anon_agent"
1252
+ metadata["step_id"] = self._generate_delegation_step_id(EVENT_TYPE_TOOL_CALL, agent_name_for_id, tool_info)
1253
+
1254
+ def _generate_delegation_step_id(self, event_type: str, agent_name: str, tool_info: dict[str, Any]) -> str:
1255
+ """Generate deterministic fallback step identifiers for delegated events.
1256
+
1257
+ Args:
1258
+ event_type (str): Event type name.
1259
+ agent_name (str): Sub-agent name.
1260
+ tool_info (dict[str, Any]): Tool info payload.
1261
+
1262
+ Returns:
1263
+ str: Generated step id.
1264
+ """
1265
+ try:
1266
+ counter = get_next_step_number()
1267
+ if event_type == EVENT_TYPE_TOOL_CALL:
1268
+ candidate = tool_info.get("tool_calls")
1269
+ if isinstance(candidate, list) and candidate:
1270
+ first_call = candidate[0] if isinstance(candidate[0], dict) else {}
1271
+ tool_name = first_call.get("name") if isinstance(first_call.get("name"), str) else "tool"
1272
+ return f"{tool_name}_{counter:03d}"
1273
+ if event_type == EVENT_TYPE_TOOL_RESULT:
1274
+ tool_name = tool_info.get("name") if isinstance(tool_info.get("name"), str) else "result"
1275
+ return f"{tool_name}_{counter:03d}"
1276
+ fallback_prefix = self._build_fallback_prefix(agent_name)
1277
+ return f"{fallback_prefix}_{event_type}_{counter:03d}"
1278
+ except Exception:
1279
+ return f"task_{uuid.uuid4().hex[:8]}"
1280
+
1281
+ @staticmethod
1282
+ def _build_fallback_prefix(agent_name: str) -> str:
1283
+ """Build sanitized fallback prefix from agent name.
1284
+
1285
+ Args:
1286
+ agent_name (str): Agent name candidate.
1287
+
1288
+ Returns:
1289
+ str: Prefix used for generated fallback ids.
1290
+ """
1291
+ if not agent_name:
1292
+ return "task"
1293
+ sanitized = "".join(ch for ch in agent_name.lower() if ch.isalnum())[:8]
1294
+ return f"task_{sanitized}" if sanitized else "task"
1295
+
1296
+ def _setup_agent_linkage(
1297
+ self,
1298
+ metadata: dict[str, Any],
1299
+ agent_name: str,
1300
+ tool_info: dict[str, Any],
1301
+ parent_thread_id: str | None,
1302
+ ) -> None:
1303
+ """Attach parent/sub-agent linkage fields for tool-call events.
1304
+
1305
+ Args:
1306
+ metadata (dict[str, Any]): Metadata to mutate.
1307
+ agent_name (str): Sub-agent name.
1308
+ tool_info (dict[str, Any]): Tool info payload.
1309
+ parent_thread_id (str | None): Parent thread identifier.
1310
+ """
1311
+ parent_step_id = self._get_parent_step_id(tool_info, parent_thread_id)
1312
+ metadata["previous_step_ids"] = [parent_step_id] if parent_step_id else []
1313
+
1314
+ sub_start_map = _TASK_SUB_START_STEP_CVAR.get() or {}
1315
+ step_id = metadata.get("step_id")
1316
+ if isinstance(step_id, str) and step_id:
1317
+ sub_start_map[agent_name] = step_id
1318
+ _TASK_SUB_START_STEP_CVAR.set(sub_start_map)
1319
+
1320
+ def _get_parent_step_id(self, tool_info: dict[str, Any], parent_thread_id: str | None) -> str | None:
1321
+ """Resolve parent step id from tool parent map when available.
1322
+
1323
+ Args:
1324
+ tool_info (dict[str, Any]): Tool info payload.
1325
+ parent_thread_id (str | None): Parent thread identifier.
1326
+
1327
+ Returns:
1328
+ str | None: Parent step id when found.
1329
+ """
1330
+ if (
1331
+ self.parent_agent is None
1332
+ or not hasattr(self.parent_agent, "_tool_parent_map_by_thread")
1333
+ or not parent_thread_id
1334
+ ):
1335
+ return None
1336
+ return self._lookup_parent_step_from_agent(tool_info, parent_thread_id)
1337
+
1338
+ def _lookup_parent_step_from_agent(self, tool_info: dict[str, Any], parent_thread_id: str) -> str | None:
1339
+ """Lookup parent step id using parent agent thread-scoped map.
1340
+
1341
+ Args:
1342
+ tool_info (dict[str, Any]): Tool info payload.
1343
+ parent_thread_id (str): Parent thread identifier.
1344
+
1345
+ Returns:
1346
+ str | None: Mapped parent step id when found.
1347
+ """
1348
+ try:
1349
+ parent_map = self.parent_agent._tool_parent_map_by_thread.get(parent_thread_id, {})
1350
+ if not isinstance(parent_map, dict):
1351
+ return None
1352
+
1353
+ tool_call_id = tool_info.get("id")
1354
+ if not isinstance(tool_call_id, str) and isinstance(tool_info.get("tool_calls"), list):
1355
+ for call in tool_info.get("tool_calls", []):
1356
+ if isinstance(call, dict) and isinstance(call.get("id"), str):
1357
+ tool_call_id = call.get("id")
1358
+ break
1359
+
1360
+ if isinstance(tool_call_id, str) and tool_call_id:
1361
+ parent_step = parent_map.get(str(tool_call_id))
1362
+ if isinstance(parent_step, str) and parent_step:
1363
+ return parent_step
1364
+ except Exception:
1365
+ logger.debug("task_delegation_parent_step_lookup_failed", exc_info=True)
1366
+ return None
1367
+
1368
+ @staticmethod
1369
+ def _handle_missing_agent_name(metadata: dict[str, Any]) -> None:
1370
+ """Mark metadata for missing agent-name linkage scenario.
1371
+
1372
+ Args:
1373
+ metadata (dict[str, Any]): Metadata to mutate.
1374
+ """
1375
+ metadata["previous_step_ids"] = []
1376
+ metadata["agent_name_missing"] = True
1377
+
1378
+ def _forward_tool_result_event(self, chunk: dict[str, Any], writer: StreamWriter) -> None:
1379
+ """Forward tool-result event using delegation-compatible envelope.
1380
+
1381
+ Args:
1382
+ chunk (dict[str, Any]): Normalized stream chunk.
1383
+ writer (StreamWriter): Parent stream writer.
1384
+ """
1385
+ tool_info = chunk.get("tool_info") if isinstance(chunk.get("tool_info"), dict) else {}
1386
+ deduped_tool_names = self._extract_tool_result_names(tool_info)
1387
+ metadata = self._build_tool_result_metadata(chunk)
1388
+
1389
+ content = self._build_completion_content(deduped_tool_names)
1390
+ a2a_event: dict[str, Any] = {
1391
+ "event_type": EVENT_TYPE_TOOL_RESULT,
1392
+ "content": content,
1393
+ "metadata": metadata,
1394
+ "tool_info": tool_info,
1395
+ "is_final": False,
1396
+ "artifacts": chunk.get("artifacts"),
1397
+ MetadataFieldKeys.REFERENCES: chunk.get(MetadataFieldKeys.REFERENCES),
1398
+ STEP_USAGE_KEY: chunk.get(STEP_USAGE_KEY),
1399
+ TOTAL_USAGE_KEY: chunk.get(TOTAL_USAGE_KEY),
1400
+ MODEL_USAGE_DELTA_KEY: chunk.get(MODEL_USAGE_DELTA_KEY),
1401
+ MODEL_USAGE_KEY: chunk.get(MODEL_USAGE_KEY),
1402
+ }
1403
+ writer(a2a_event)
1404
+
1405
+ @staticmethod
1406
+ def _extract_tool_result_names(tool_info: dict[str, Any]) -> list[str]:
1407
+ """Extract deterministic tool-name list from tool-result payload.
1408
+
1409
+ Args:
1410
+ tool_info (dict[str, Any]): Tool result payload.
1411
+
1412
+ Returns:
1413
+ list[str]: Deduplicated non-empty tool names.
1414
+ """
1415
+ primary_name = tool_info.get("name")
1416
+ if isinstance(primary_name, str) and primary_name:
1417
+ return [primary_name]
1418
+
1419
+ tool_calls = tool_info.get("tool_calls")
1420
+ if not isinstance(tool_calls, list):
1421
+ return ["unknown_tool"] # pragma: no cover
1422
+
1423
+ tool_names: list[str] = []
1424
+ for call in tool_calls:
1425
+ if not isinstance(call, dict):
1426
+ continue
1427
+ name = call.get("name")
1428
+ if isinstance(name, str) and name:
1429
+ tool_names.append(name)
1430
+
1431
+ deduped_names = list(dict.fromkeys(tool_names))
1432
+ return deduped_names or ["unknown_tool"]
1433
+
1434
+ def _build_tool_result_metadata(self, chunk: dict[str, Any]) -> dict[str, Any]:
1435
+ """Build delegated metadata envelope for forwarded tool-result event.
1436
+
1437
+ Args:
1438
+ chunk (dict[str, Any]): Normalized worker chunk.
1439
+
1440
+ Returns:
1441
+ dict[str, Any]: Metadata with sub-agent linkage markers.
1442
+ """
1443
+ metadata = dict(chunk.get("metadata") or {})
1444
+ metadata[SUB_AGENT_EVENT_METADATA_KEY] = True
1445
+
1446
+ agent_name = metadata.get("agent_name")
1447
+ if not isinstance(agent_name, str) or not agent_name:
1448
+ self._handle_missing_agent_name(metadata)
1449
+ return metadata
1450
+
1451
+ sub_start_map = _TASK_SUB_START_STEP_CVAR.get() or {}
1452
+ start_step_id = sub_start_map.get(agent_name)
1453
+ metadata["previous_step_ids"] = [start_step_id] if isinstance(start_step_id, str) and start_step_id else []
1454
+ return metadata
1455
+
1456
+ def _build_completion_content(self, tool_names: list[str]) -> str:
1457
+ """Build parent-facing completion content for tool-result events.
1458
+
1459
+ Args:
1460
+ tool_names (list[str]): Completed tool names.
1461
+
1462
+ Returns:
1463
+ str: Completion text matching delegation manager style.
1464
+ """
1465
+ if self.parent_agent and hasattr(self.parent_agent, "_get_tool_completion_content"):
1466
+ try:
1467
+ return self.parent_agent._get_tool_completion_content(tool_names)
1468
+ except Exception:
1469
+ logger.debug("task_delegation_parent_completion_format_failed", exc_info=True)
1470
+
1471
+ has_delegation = any(name.startswith("delegate_to") for name in tool_names)
1472
+ prefix = "Completed sub-agents:" if has_delegation else "Completed tools:"
1473
+ return f"{prefix} {', '.join(tool_names)}"
1474
+
1475
+ @staticmethod
1476
+ def _create_tool_call_message(tool_info: dict[str, Any]) -> str:
1477
+ """Build human-readable message for forwarded tool-call events.
1478
+
1479
+ Args:
1480
+ tool_info (dict[str, Any]): Tool info payload.
1481
+
1482
+ Returns:
1483
+ str: Formatted message with tool names.
1484
+ """
1485
+ tool_calls = tool_info.get("tool_calls", [])
1486
+ tool_names = [tc.get("name", "unknown") for tc in tool_calls if isinstance(tc, dict)]
1487
+ if not tool_names:
1488
+ tool_names = ["unknown"]
1489
+ return f"Processing with tools: {', '.join(tool_names)}"
1490
+
1491
+ def _normalize_forwarded_sub_agent_chunk(self, chunk: dict[str, Any]) -> dict[str, Any]:
1492
+ """Normalize forwarded sub-agent chunk for parent stream safety.
1493
+
1494
+ Args:
1495
+ chunk (dict[str, Any]): Marked sub-agent chunk.
1496
+
1497
+ Returns:
1498
+ dict[str, Any]: Normalized chunk with terminal remapping applied.
1499
+ """
1500
+ normalized_chunk = dict(chunk)
1501
+ if normalized_chunk.get("is_final") is True:
1502
+ normalized_chunk["is_final"] = False
1503
+ normalized_chunk.pop("final", None)
1504
+
1505
+ event_type = self._normalize_event_type(normalized_chunk.get("event_type"))
1506
+ remapped_event_type = SUB_AGENT_TERMINAL_EVENT_REMAP.get(event_type)
1507
+ if remapped_event_type is not None:
1508
+ normalized_chunk["event_type"] = remapped_event_type
1509
+ return normalized_chunk
1510
+
1511
+ @staticmethod
1512
+ def _build_worker_response_from_stream_final(
1513
+ final_event: dict[str, Any],
1514
+ stream_records: list[dict[str, Any]],
1515
+ ) -> dict[str, Any]:
1516
+ """Build canonical worker response payload from final stream event.
1517
+
1518
+ Args:
1519
+ final_event (dict[str, Any]): Final stream event payload.
1520
+ stream_records (list[dict[str, Any]]): Normalized stream records.
1521
+
1522
+ Returns:
1523
+ dict[str, Any]: Canonical worker response with ``full_final_state``.
1524
+ """
1525
+ final_metadata = final_event.get("metadata") if isinstance(final_event.get("metadata"), dict) else {}
1526
+ merged_metadata = dict(final_metadata)
1527
+
1528
+ timeout_delegation = TaskDelegationMiddleware._extract_timeout_delegation_metadata(stream_records)
1529
+ if timeout_delegation is not None:
1530
+ existing_delegation = merged_metadata.get(DELEGATION_METADATA_KEY)
1531
+ if isinstance(existing_delegation, dict):
1532
+ merged_delegation = dict(existing_delegation) # pragma: no cover
1533
+ merged_delegation.update(timeout_delegation) # pragma: no cover
1534
+ merged_metadata[DELEGATION_METADATA_KEY] = merged_delegation # pragma: no cover
1535
+ else:
1536
+ merged_metadata[DELEGATION_METADATA_KEY] = timeout_delegation
1537
+
1538
+ return {
1539
+ "output": str(final_event.get("content", "")),
1540
+ "full_final_state": {
1541
+ "total_usage": final_event.get("total_usage")
1542
+ if isinstance(final_event.get("total_usage"), dict)
1543
+ else {},
1544
+ "references": final_event.get("references") if isinstance(final_event.get("references"), list) else [],
1545
+ "metadata": merged_metadata,
1546
+ "stream_trace": stream_records,
1547
+ "artifacts": final_event.get("artifacts") if isinstance(final_event.get("artifacts"), list) else [],
1548
+ },
1549
+ }
1550
+
1551
+ @staticmethod
1552
+ def _extract_timeout_delegation_metadata(stream_records: list[dict[str, Any]]) -> dict[str, Any] | None:
1553
+ """Extract timeout delegation metadata from normalized worker stream records."""
1554
+ timeout_execution = TaskDelegationMiddleware._extract_timeout_tool_execution(stream_records)
1555
+ if timeout_execution is None:
1556
+ return None
1557
+
1558
+ return {
1559
+ "status": DELEGATION_FAILED_STATUS,
1560
+ "reason": DELEGATION_TOOL_TIMEOUT_REASON,
1561
+ "tool_execution": timeout_execution,
1562
+ }
1563
+
1564
+ @staticmethod
1565
+ def _extract_timeout_tool_execution(stream_records: list[dict[str, Any]]) -> dict[str, Any] | None:
1566
+ """Return the last timeout-marked tool_execution payload from stream records."""
1567
+ for record in reversed(stream_records):
1568
+ if not isinstance(record, dict):
1569
+ continue # pragma: no cover
1570
+ if record.get("event_type") != EVENT_TYPE_TOOL_RESULT:
1571
+ continue
1572
+
1573
+ metadata = record.get("metadata")
1574
+ if not isinstance(metadata, dict):
1575
+ continue # pragma: no cover
1576
+
1577
+ tool_execution = metadata.get("tool_execution")
1578
+ if not isinstance(tool_execution, dict):
1579
+ continue
1580
+ if tool_execution.get("status") != "timeout":
1581
+ continue # pragma: no cover
1582
+
1583
+ return dict(tool_execution)
1584
+
1585
+ return None
1586
+
1587
+ @staticmethod
1588
+ def _normalize_event_type(raw: Any) -> str:
1589
+ """Normalize event type values from enum or arbitrary objects.
1590
+
1591
+ Args:
1592
+ raw (Any): Raw event type value.
1593
+
1594
+ Returns:
1595
+ str: Normalized event type string.
1596
+ """
1597
+ if hasattr(raw, "value"):
1598
+ return str(raw.value)
1599
+ if isinstance(raw, str):
1600
+ return raw
1601
+ return str(raw)
1602
+
1603
+ def _normalize_worker_stream_chunk(self, chunk: dict[str, Any]) -> dict[str, Any]:
1604
+ """Normalize raw worker stream chunk into stable internal structure.
1605
+
1606
+ Args:
1607
+ chunk (dict[str, Any]): Raw worker stream chunk.
1608
+
1609
+ Returns:
1610
+ dict[str, Any]: Normalized stream chunk.
1611
+ """
1612
+ event_type = self._normalize_event_type(chunk.get("event_type"))
1613
+ metadata = chunk.get("metadata") if isinstance(chunk.get("metadata"), dict) else {}
1614
+ tool_info = chunk.get("tool_info") if isinstance(chunk.get("tool_info"), dict) else {}
1615
+ record: dict[str, Any] = {
1616
+ "event_type": event_type,
1617
+ "step_id": metadata.get("step_id"),
1618
+ "previous_step_ids": metadata.get("previous_step_ids"),
1619
+ "is_final": bool(chunk.get("is_final")),
1620
+ "content": chunk.get("content", ""),
1621
+ "metadata": metadata,
1622
+ "tool_info": tool_info,
1623
+ "step_usage": chunk.get("step_usage") if isinstance(chunk.get("step_usage"), dict) else None,
1624
+ "total_usage": chunk.get("total_usage") if isinstance(chunk.get("total_usage"), dict) else None,
1625
+ MODEL_USAGE_DELTA_KEY: chunk.get(MODEL_USAGE_DELTA_KEY)
1626
+ if isinstance(chunk.get(MODEL_USAGE_DELTA_KEY), dict)
1627
+ else None,
1628
+ MODEL_USAGE_KEY: chunk.get(MODEL_USAGE_KEY) if isinstance(chunk.get(MODEL_USAGE_KEY), list) else None,
1629
+ "references": chunk.get("references") if isinstance(chunk.get("references"), list) else None,
1630
+ "artifacts": chunk.get("artifacts") if isinstance(chunk.get("artifacts"), list) else None,
1631
+ }
1632
+
1633
+ if event_type == EVENT_TYPE_TOOL_CALL:
1634
+ tool_calls: list[dict[str, Any]] = (
1635
+ [tc for tc in tool_info.get("tool_calls", []) if isinstance(tc, dict)]
1636
+ if isinstance(tool_info.get("tool_calls"), list)
1637
+ else []
1638
+ )
1639
+ record["tool_calls"] = [
1640
+ {
1641
+ "id": tc.get("id"),
1642
+ "name": tc.get("name"),
1643
+ "args": tc.get("args"),
1644
+ }
1645
+ for tc in tool_calls
1646
+ ]
1647
+ elif event_type == EVENT_TYPE_TOOL_RESULT:
1648
+ record["tool_result"] = {
1649
+ "id": tool_info.get("id"),
1650
+ "name": tool_info.get("name"),
1651
+ "execution_time": tool_info.get("execution_time"),
1652
+ "args": tool_info.get("args"),
1653
+ "output": tool_info.get("output"),
1654
+ }
1655
+
1656
+ return record