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,1125 @@
1
+ """Defines the base abstract class for A2A server-side executors.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ Reinhart Linanda (reinhart.linanda@gdplabs.id)
6
+ """
7
+
8
+ import asyncio
9
+ import time
10
+ import uuid
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Awaitable, Callable
13
+ from dataclasses import dataclass
14
+ from datetime import UTC, datetime
15
+ from typing import Any
16
+
17
+ from a2a.server.agent_execution import AgentExecutor as A2ASDKExecutor
18
+ from a2a.server.agent_execution import RequestContext
19
+ from a2a.server.events.event_queue import EventQueue
20
+ from a2a.server.tasks import TaskUpdater
21
+ from a2a.types import (
22
+ FilePart,
23
+ FileWithBytes,
24
+ Message,
25
+ Part,
26
+ Role,
27
+ TaskArtifactUpdateEvent,
28
+ TaskState,
29
+ TaskStatus,
30
+ TaskStatusUpdateEvent,
31
+ TextPart,
32
+ )
33
+ from a2a.utils import get_text_parts, new_agent_text_message, new_text_artifact
34
+ from a2a.utils.artifact import new_artifact
35
+
36
+ from aip_agents.types import A2AEvent, A2AStreamEventType
37
+ from aip_agents.utils import serialize_references_for_metadata
38
+ from aip_agents.utils.artifact_helpers import ArtifactHandler
39
+ from aip_agents.utils.logger import get_logger
40
+ from aip_agents.utils.metadata_helper import MetadataFieldKeys
41
+
42
+ logger = get_logger(__name__)
43
+ SUB_AGENT_EVENT_METADATA_KEY = "is_sub_agent_event"
44
+ SUB_AGENT_TERMINAL_EVENT_REMAP = {
45
+ A2AStreamEventType.FINAL_RESPONSE: A2AStreamEventType.CONTENT_CHUNK,
46
+ A2AStreamEventType.ERROR: A2AStreamEventType.STATUS_UPDATE,
47
+ A2AStreamEventType.STEP_LIMIT_EXCEEDED: A2AStreamEventType.STATUS_UPDATE,
48
+ }
49
+
50
+
51
+ @dataclass
52
+ class StatusUpdateParams:
53
+ """Parameters for status updates."""
54
+
55
+ metadata: dict[str, Any] | None = None
56
+ final: bool = False
57
+ task_id: str | None = None
58
+ context_id: str | None = None
59
+
60
+
61
+ class BaseA2AExecutor(A2ASDKExecutor, ABC):
62
+ """Abstract base class for GLLM Agent framework's A2A server-side executors.
63
+
64
+ This class extends the A2A SDK's `AgentExecutor`. It serves as a common
65
+ foundation for specific executors tailored to different agent types within the
66
+ `aip-agents` framework, such as `LangGraphA2AExecutor` or
67
+ `GoogleADKA2AExecutor`.
68
+
69
+ Subclasses are required to implement the `execute` method to handle A2A
70
+ requests. The `cancel` method has a common implementation.
71
+
72
+ Attributes:
73
+ _active_tasks (dict[str, asyncio.Task]): A dictionary mapping task IDs to
74
+ their corresponding asyncio.Task instances for active agent executions.
75
+ """
76
+
77
+ def __init__(self) -> None:
78
+ """Initializes the BaseA2AExecutor."""
79
+ self._active_tasks: dict[str, asyncio.Task] = {}
80
+ self._processed_artifacts: dict[str, set[str]] = {} # task_id -> set of artifact hashes
81
+ self._streaming_artifacts: dict[str, bool] = {} # task_id -> has_streaming_content
82
+ self._streaming_artifact_ids: dict[str, str] = {} # task_id -> artifact_id for consistent streaming
83
+ # Track cumulative time per task (monotonic seconds) for metadata.time
84
+ self._task_start_times: dict[str, float] = {}
85
+
86
+ def _remove_active_task(self, task_id: str) -> None:
87
+ """Removes an active task from the internal tracking dictionary.
88
+
89
+ Args:
90
+ task_id (str): The ID of the task to remove.
91
+ """
92
+ if task_id in self._active_tasks:
93
+ self._active_tasks.pop(task_id)
94
+ # Clean up processed artifacts for this task
95
+ if task_id in self._processed_artifacts:
96
+ self._processed_artifacts.pop(task_id)
97
+ # Clean up streaming artifacts tracking
98
+ if task_id in self._streaming_artifacts:
99
+ self._streaming_artifacts.pop(task_id)
100
+ # Clean up streaming artifact IDs
101
+ if task_id in self._streaming_artifact_ids:
102
+ self._streaming_artifact_ids.pop(task_id)
103
+ # Clean up task start time tracking
104
+ if task_id in self._task_start_times:
105
+ self._task_start_times.pop(task_id)
106
+
107
+ def _apply_cumulative_time(self, task_id: str, metadata: dict[str, Any] | None) -> None:
108
+ """Ensure metadata.time is cumulative since first status event for the task.
109
+
110
+ Args:
111
+ task_id: The A2A task ID.
112
+ metadata: The metadata dict to mutate.
113
+ """
114
+ if metadata is None:
115
+ return
116
+ now = time.monotonic()
117
+ start = self._task_start_times.get(task_id)
118
+ if start is None:
119
+ self._task_start_times[task_id] = now
120
+ elapsed = 0.0
121
+ else:
122
+ elapsed = max(0.0, now - start)
123
+ # Always use string keys in metadata to ensure JSON-serializable output
124
+ metadata[MetadataFieldKeys.TIME] = elapsed
125
+
126
+ async def _handle_initial_execute_checks(
127
+ self, context: RequestContext, event_queue: EventQueue
128
+ ) -> tuple[TaskUpdater | None, str | None, dict[str, Any] | None]:
129
+ """Performs initial validation and setup for an incoming agent request.
130
+
131
+ This method checks for the presence of message content, extracts the query and metadata,
132
+ and initializes the task status with the A2A server.
133
+
134
+ Args:
135
+ context (RequestContext): The A2A request context, containing message
136
+ details and task information.
137
+ event_queue (EventQueue): The queue for sending task status updates back
138
+ to the A2A server.
139
+
140
+ Returns:
141
+ tuple[TaskUpdater | None, str | None, dict[str, typing.Any] | None]: A tuple containing a `TaskUpdater`
142
+ instance, the extracted query string, and extracted metadata dictionary if initial checks pass. If checks
143
+ fail (e.g., no message content), it returns (None, None, None) and will have
144
+ already sent a failure status through the updater.
145
+ """
146
+ if not context.message or not context.message.parts:
147
+ updater = TaskUpdater(event_queue, context.task_id, context.context_id)
148
+ await updater.failed(message=new_agent_text_message("No message content provided."))
149
+ return None, None, None
150
+
151
+ query_parts = get_text_parts(context.message.parts)
152
+ query = "\n".join(query_parts)
153
+
154
+ if not query:
155
+ updater = TaskUpdater(event_queue, context.task_id, context.context_id)
156
+ await updater.failed(message=new_agent_text_message("Extracted query is empty."))
157
+ return None, None, None
158
+
159
+ # Extract metadata from both message and request params
160
+ metadata = self._extract_metadata(context)
161
+
162
+ updater = TaskUpdater(event_queue, context.task_id, context.context_id)
163
+ if not context.current_task:
164
+ await updater.submit()
165
+ await updater.start_work()
166
+ return updater, query, metadata
167
+
168
+ def _extract_metadata(self, context: RequestContext) -> dict[str, Any]:
169
+ """Extracts metadata from the request context.
170
+
171
+ This method combines metadata from both the message and the request parameters
172
+ to provide a comprehensive metadata dictionary for the agent.
173
+
174
+ Args:
175
+ context (RequestContext): The A2A request context containing message and params.
176
+
177
+ Returns:
178
+ dict[str, Any]: A dictionary containing all available metadata.
179
+ """
180
+ metadata = {}
181
+
182
+ if context._params and context._params.metadata:
183
+ metadata.update(context._params.metadata)
184
+
185
+ logger.debug(f"Final extracted metadata: {metadata}")
186
+ return metadata
187
+
188
+ async def _update_status(
189
+ self,
190
+ updater: TaskUpdater,
191
+ state: TaskState,
192
+ message: Message,
193
+ params: StatusUpdateParams | None = None,
194
+ ) -> None:
195
+ """Update task status with metadata placed in TaskStatusUpdateEvent according to A2A spec.
196
+
197
+ This method creates a TaskStatusUpdateEvent with metadata in the correct location
198
+ (the event's metadata field) rather than in the message metadata field.
199
+
200
+ Args:
201
+ updater (TaskUpdater): The TaskUpdater instance for sending status updates.
202
+ state (TaskState): The new task state.
203
+ message (Message): The message associated with the status update.
204
+ params (StatusUpdateParams | None): Parameters for the status update.
205
+ """
206
+ current_timestamp = datetime.now(UTC).isoformat()
207
+
208
+ # Use defaults if params not provided
209
+ if params is None:
210
+ params = StatusUpdateParams()
211
+
212
+ # Use provided task_id and context_id, or extract from message/updater
213
+ task_id = params.task_id
214
+ if task_id is None:
215
+ task_id = message.task_id
216
+ context_id = params.context_id
217
+ if context_id is None:
218
+ context_id = message.context_id
219
+
220
+ # Ensure we have valid IDs
221
+ if task_id is None or context_id is None:
222
+ raise ValueError("task_id and context_id must be provided or available in the message")
223
+
224
+ # Ensure metadata exists and apply cumulative time for this task
225
+ metadata = params.metadata or {}
226
+ try:
227
+ self._apply_cumulative_time(task_id, metadata)
228
+ except Exception as e:
229
+ logger.warning(f"Failed to apply cumulative time for task {task_id}: {e}")
230
+
231
+ event = TaskStatusUpdateEvent(
232
+ task_id=task_id,
233
+ context_id=context_id,
234
+ final=params.final,
235
+ status=TaskStatus(
236
+ state=state,
237
+ message=message,
238
+ timestamp=current_timestamp,
239
+ ),
240
+ metadata=metadata,
241
+ )
242
+
243
+ await updater.event_queue.enqueue_event(event)
244
+
245
+ async def _execute_agent_processing(
246
+ self,
247
+ agent_processing_coro: Awaitable[None],
248
+ updater: TaskUpdater,
249
+ task_id: str,
250
+ context_id: str | None = None,
251
+ ) -> None:
252
+ """Manages the execution lifecycle of an agent processing coroutine.
253
+
254
+ This method creates an asyncio task for the provided agent processing
255
+ coroutine, stores it for potential cancellation, and awaits its completion.
256
+ It handles `asyncio.CancelledError` to update task status to cancelled
257
+ and logs other exceptions, marking the task as failed.
258
+
259
+ Args:
260
+ agent_processing_coro (typing.Awaitable[None]): The coroutine that
261
+ performs the agent-specific processing (e.g., streaming results).
262
+ updater (TaskUpdater): The TaskUpdater instance for sending status updates.
263
+ task_id (str): The unique ID of the A2A task.
264
+ context_id (str | None): The context ID. Defaults to None.
265
+ """
266
+ task = asyncio.create_task(agent_processing_coro)
267
+ self._active_tasks[task_id] = task
268
+
269
+ try:
270
+ await task
271
+ except asyncio.CancelledError:
272
+ # This specific CancelledError is raised if the task created from
273
+ # agent_processing_coro is cancelled externally (e.g., by the cancel method).
274
+ logger.info(f"Agent processing task {task_id} was cancelled by client request.")
275
+ await self._update_status(
276
+ updater,
277
+ TaskState.canceled,
278
+ new_agent_text_message("Task was cancelled by client."),
279
+ params=StatusUpdateParams(task_id=task_id, context_id=context_id),
280
+ )
281
+ raise
282
+ except Exception as e:
283
+ self._remove_active_task(task_id)
284
+ logger.error(f"Error during agent execution for task {task_id}: {e}", exc_info=True)
285
+ await self._update_status(
286
+ updater,
287
+ TaskState.failed,
288
+ new_agent_text_message(f"Error during execution: {str(e)}"),
289
+ params=StatusUpdateParams(final=True, task_id=task_id, context_id=context_id),
290
+ )
291
+ finally:
292
+ self._remove_active_task(task_id)
293
+
294
+ async def _handle_artifact_event(
295
+ self,
296
+ payload: dict[str, Any],
297
+ updater: TaskUpdater,
298
+ task_id: str | None = None,
299
+ ) -> bool:
300
+ """Handles an artifact event from the agent stream.
301
+
302
+ Args:
303
+ payload (dict[str, typing.Any]): The artifact payload containing data, name, etc.
304
+ updater (TaskUpdater): The TaskUpdater instance for sending artifact updates.
305
+ task_id (str | None): The task ID for deduplication tracking.
306
+
307
+ Returns:
308
+ bool: False to continue stream, True if there was an error that should stop processing.
309
+ """
310
+ try:
311
+ # Validate and extract artifact data
312
+ artifact_data_b64 = payload.get("data")
313
+ if not artifact_data_b64:
314
+ logger.warning("Artifact payload missing 'data' field")
315
+ return False
316
+
317
+ # Check for duplicates
318
+ if task_id and self._is_duplicate_artifact(payload, task_id):
319
+ return False
320
+
321
+ # Create A2A-compliant artifact
322
+ artifact_to_send = self._create_a2a_artifact(payload)
323
+ if not artifact_to_send:
324
+ return False
325
+
326
+ # Merge payload metadata (if any) and apply cumulative time
327
+ event_metadata: dict[str, Any] | None = None
328
+ try:
329
+ raw_md = payload.get("metadata") if isinstance(payload, dict) else None
330
+ event_metadata = raw_md.copy() if isinstance(raw_md, dict) else {}
331
+ except Exception as e:
332
+ logger.warning(f"Failed to copy artifact metadata for task {task_id}: {e}")
333
+ event_metadata = {}
334
+ if task_id:
335
+ try:
336
+ self._apply_cumulative_time(task_id, event_metadata)
337
+ except Exception as e:
338
+ logger.warning(f"Failed to apply cumulative time to artifact metadata for task {task_id}: {e}")
339
+
340
+ # Send artifact to client with metadata
341
+ return await self._send_artifact_to_client(artifact_to_send, updater, metadata=event_metadata)
342
+
343
+ except Exception as e:
344
+ logger.error(f"Failed to process artifact payload: {e}", exc_info=True)
345
+ return False
346
+
347
+ def _is_duplicate_artifact(self, payload: dict[str, Any], task_id: str) -> bool:
348
+ """Check if artifact is a duplicate and handle deduplication tracking.
349
+
350
+ Args:
351
+ payload: The artifact payload.
352
+ task_id: The task ID for tracking.
353
+
354
+ Returns:
355
+ True if artifact is a duplicate, False otherwise.
356
+ """
357
+ artifact_hash = ArtifactHandler.generate_artifact_hash(
358
+ payload.get("data", ""),
359
+ payload.get("name", ""),
360
+ payload.get("mime_type", ""),
361
+ )
362
+
363
+ if task_id not in self._processed_artifacts:
364
+ self._processed_artifacts[task_id] = set()
365
+
366
+ if artifact_hash in self._processed_artifacts[task_id]:
367
+ logger.warning(
368
+ f"Skipping duplicate artifact: {payload.get('name', 'unnamed')} (hash: {artifact_hash[:8]}...)"
369
+ )
370
+ return True
371
+
372
+ self._processed_artifacts[task_id].add(artifact_hash)
373
+ logger.info(f"Processing new artifact: {payload.get('name', 'unnamed')} (hash: {artifact_hash[:8]}...)")
374
+ return False
375
+
376
+ def _create_a2a_artifact(self, payload: dict[str, Any]) -> Any | None:
377
+ """Create A2A-compliant artifact from payload with flexible part type support.
378
+
379
+ Args:
380
+ payload: The artifact payload.
381
+
382
+ Returns:
383
+ A2A artifact object or None if creation failed.
384
+ """
385
+ try:
386
+ # Determine the appropriate part type based on payload content
387
+ part = self._create_artifact_part(payload)
388
+ if not part:
389
+ return None
390
+
391
+ artifact_to_send = new_artifact(
392
+ parts=[Part(root=part)],
393
+ name=payload.get("name", "Generated Artifact"),
394
+ description=payload.get("description", ""),
395
+ )
396
+ return artifact_to_send
397
+
398
+ except Exception as artifact_error:
399
+ logger.error(f"Failed to create artifact: {artifact_error}")
400
+ return None
401
+
402
+ def _create_artifact_part(self, payload: dict[str, Any]) -> Any | None:
403
+ """Create the appropriate part type based on payload content.
404
+
405
+ Args:
406
+ payload: The artifact payload.
407
+
408
+ Returns:
409
+ A Part object (TextPart, FilePart, etc.) or None if creation failed.
410
+ """
411
+ try:
412
+ # Check if this is text content (common for streaming responses)
413
+ if "text" in payload or ("mime_type" in payload and payload["mime_type"].startswith("text/")):
414
+ return TextPart(
415
+ kind="text",
416
+ text=payload.get("text", payload.get("data", "")),
417
+ metadata=payload.get("metadata"),
418
+ )
419
+
420
+ # Check if this has binary data (files, images, etc.)
421
+ elif "data" in payload:
422
+ return FilePart(
423
+ kind="file",
424
+ file=FileWithBytes(
425
+ bytes=payload.get("data"),
426
+ name=payload.get("name", "artifact"),
427
+ mime_type=payload.get("mime_type", "application/octet-stream"),
428
+ ),
429
+ metadata=payload.get("metadata"),
430
+ )
431
+
432
+ # Fallback to text part for any other content
433
+ else:
434
+ content = str(payload.get("content", payload.get("data", "")))
435
+ return TextPart(
436
+ kind="text",
437
+ text=content,
438
+ metadata=payload.get("metadata"),
439
+ )
440
+
441
+ except Exception as part_error:
442
+ logger.error(f"Failed to create artifact part: {part_error}")
443
+ return None
444
+
445
+ async def _send_artifact_to_client(
446
+ self,
447
+ artifact: Any,
448
+ updater: TaskUpdater,
449
+ metadata: dict[str, Any] | None = None,
450
+ ) -> bool:
451
+ """Send artifact to client as a TaskArtifactUpdateEvent with event metadata.
452
+
453
+ Args:
454
+ artifact: The A2A artifact to send.
455
+ updater: The TaskUpdater instance used to enqueue artifact events.
456
+ metadata: Optional event-level metadata to include with the update
457
+ (e.g., cumulative time, tracing fields). This is attached to the
458
+ TaskArtifactUpdateEvent so clients receive it on the event envelope.
459
+
460
+ Returns:
461
+ False to continue stream on success, True if there was an error.
462
+ """
463
+ try:
464
+ event = TaskArtifactUpdateEvent(
465
+ task_id=updater.task_id,
466
+ context_id=updater.context_id,
467
+ artifact=artifact,
468
+ append=None,
469
+ last_chunk=None,
470
+ metadata=metadata,
471
+ )
472
+ await updater.event_queue.enqueue_event(event)
473
+ logger.info(f"Successfully sent artifact '{artifact.name}' to client")
474
+ return False # Continue stream
475
+
476
+ except Exception as send_error:
477
+ logger.error(f"Failed to send artifact to client: {send_error}")
478
+ return True # Error occurred, stop processing
479
+
480
+ async def _send_content_as_artifact( # noqa: PLR0913
481
+ self,
482
+ content: str,
483
+ event_queue: EventQueue,
484
+ task_id: str,
485
+ context_id: str,
486
+ append: bool = True,
487
+ last_chunk: bool = False,
488
+ artifact_name: str = "streaming_response",
489
+ metadata: dict[str, Any] | None = None,
490
+ ) -> None:
491
+ """Send content as a streaming artifact update event.
492
+
493
+ This method creates and sends a TaskArtifactUpdateEvent for content delivery,
494
+ which is the correct way to stream content according to A2A protocol.
495
+
496
+ Args:
497
+ content (str): The content to send as an artifact.
498
+ event_queue (EventQueue): The event queue for sending the artifact event.
499
+ task_id (str): The task ID.
500
+ context_id (str): The context ID.
501
+ append (bool): Whether this content should be appended to previous chunks.
502
+ Defaults to True for streaming content.
503
+ last_chunk (bool): Whether this is the final chunk. Defaults to False.
504
+ artifact_name (str): Name for the artifact. Defaults to "streaming_response".
505
+ metadata (dict[str, Any] | None): Optional metadata to include with the artifact.
506
+ """
507
+ try:
508
+ # Get or create consistent artifact ID for this streaming task
509
+ if task_id not in self._streaming_artifact_ids:
510
+ # Create new artifact and store its ID
511
+ artifact_payload = new_text_artifact(
512
+ name=artifact_name,
513
+ description="Streaming response from the agent.",
514
+ text=content,
515
+ )
516
+ self._streaming_artifact_ids[task_id] = artifact_payload.artifact_id
517
+ else:
518
+ # Reuse existing artifact ID for consistency
519
+ artifact_payload = new_text_artifact(
520
+ name=artifact_name,
521
+ description="Streaming response from the agent.",
522
+ text=content,
523
+ )
524
+ # Override the auto-generated ID with our consistent one
525
+ artifact_payload.artifact_id = self._streaming_artifact_ids[task_id]
526
+
527
+ artifact_event = TaskArtifactUpdateEvent(
528
+ append=append,
529
+ context_id=context_id,
530
+ task_id=task_id,
531
+ last_chunk=last_chunk,
532
+ artifact=artifact_payload,
533
+ metadata=metadata,
534
+ )
535
+
536
+ await event_queue.enqueue_event(artifact_event)
537
+ logger.debug(f"Sent content as artifact: {artifact_name} (append={append}, last_chunk={last_chunk})")
538
+
539
+ except Exception as e:
540
+ logger.error(f"Failed to send content as artifact: {e}", exc_info=True)
541
+
542
+ async def _handle_stream_event( # noqa: PLR0913, PLR0911 TODO: Refactor this
543
+ self,
544
+ chunk: A2AEvent,
545
+ updater: TaskUpdater,
546
+ task_id: str,
547
+ context_id: str,
548
+ event_queue: EventQueue,
549
+ metadata: dict[str, Any] | None = None,
550
+ ) -> bool:
551
+ """Handle semantically typed A2A events with type-based dispatching.
552
+
553
+ This method processes A2AEvent objects using their semantic event types,
554
+ eliminating the need for string parsing and JSON decoding. Each event type
555
+ is handled by a dedicated method for better maintainability.
556
+
557
+ Args:
558
+ chunk: The A2AEvent to process with semantic type information.
559
+ updater: TaskUpdater instance for sending A2A status updates.
560
+ task_id: Unique identifier for the A2A task.
561
+ context_id: Context identifier for the A2A session.
562
+ event_queue: Event queue for sending artifact update events.
563
+ metadata: Optional metadata to merge with chunk metadata.
564
+
565
+ Returns:
566
+ bool: True if stream processing should terminate, False to continue.
567
+ """
568
+ event_type = chunk.get("event_type")
569
+
570
+ # Convert string event type to Enum if possible to match handler_map keys
571
+ if isinstance(event_type, str):
572
+ try:
573
+ event_type = A2AStreamEventType(event_type)
574
+ except ValueError:
575
+ # Keep as string if not a valid enum member (will likely fall through to unknown)
576
+ event_type = chunk.get("event_type")
577
+
578
+ chunk, event_type = self._normalize_sub_agent_terminal_event(chunk, event_type)
579
+
580
+ # Prepare metadata and handle artifacts
581
+ final_metadata = self._prepare_event_metadata(chunk, metadata)
582
+ self._apply_cumulative_time(task_id, final_metadata)
583
+ await self._process_event_artifacts(chunk, updater, task_id)
584
+
585
+ # Dispatch to appropriate handler based on event type
586
+ handler_map: dict[Any, Callable[[], Awaitable[bool]]] = {
587
+ A2AStreamEventType.TOOL_CALL: lambda: self._handle_tool_call_event(
588
+ chunk, updater, task_id, context_id, metadata
589
+ ),
590
+ A2AStreamEventType.TOOL_RESULT: lambda: self._handle_tool_result_event(
591
+ chunk, updater, task_id, context_id, metadata
592
+ ),
593
+ A2AStreamEventType.CONTENT_CHUNK: lambda: self._handle_content_chunk_event(
594
+ chunk, event_queue, task_id, context_id, final_metadata
595
+ ),
596
+ A2AStreamEventType.FINAL_RESPONSE: lambda: self._handle_final_response_event(
597
+ chunk, updater, event_queue, task_id, context_id, final_metadata
598
+ ),
599
+ A2AStreamEventType.STATUS_UPDATE: lambda: self._handle_status_update_event(
600
+ chunk, updater, task_id, context_id
601
+ ),
602
+ A2AStreamEventType.STEP_LIMIT_EXCEEDED: lambda: self._handle_step_limit_exceeded_event(
603
+ chunk, updater, task_id, context_id
604
+ ),
605
+ A2AStreamEventType.ERROR: lambda: self._handle_error_event(chunk, updater, task_id, context_id),
606
+ }
607
+
608
+ handler = handler_map.get(event_type)
609
+ if handler:
610
+ return await handler()
611
+
612
+ logger.warning(f"Unknown event type: {event_type}")
613
+ return False
614
+
615
+ @staticmethod
616
+ def _is_forwarded_sub_agent_event(chunk: A2AEvent) -> bool:
617
+ """Return True when chunk metadata marks it as forwarded from a sub-agent."""
618
+ metadata = chunk.get("metadata")
619
+ return isinstance(metadata, dict) and metadata.get(SUB_AGENT_EVENT_METADATA_KEY) is True
620
+
621
+ def _normalize_sub_agent_terminal_event(
622
+ self,
623
+ chunk: A2AEvent,
624
+ event_type: A2AStreamEventType | str | None,
625
+ ) -> tuple[A2AEvent, A2AStreamEventType | str | None]:
626
+ """Remap sub-agent terminal events to non-terminal stream events.
627
+
628
+ Sub-agent terminal events are intermediate from the top-level perspective.
629
+ They should be rendered as progress/content updates, not terminate the
630
+ parent stream.
631
+ """
632
+ if not isinstance(event_type, A2AStreamEventType):
633
+ return chunk, event_type
634
+ if not self._is_forwarded_sub_agent_event(chunk):
635
+ return chunk, event_type
636
+
637
+ remapped_event_type = SUB_AGENT_TERMINAL_EVENT_REMAP.get(event_type)
638
+ if remapped_event_type is None:
639
+ return chunk, event_type
640
+
641
+ normalized_chunk: A2AEvent = dict(chunk)
642
+ normalized_chunk["event_type"] = remapped_event_type
643
+ normalized_chunk["is_final"] = False
644
+
645
+ metadata = normalized_chunk.get("metadata")
646
+ normalized_metadata = dict(metadata) if isinstance(metadata, dict) else {}
647
+ normalized_chunk["metadata"] = normalized_metadata
648
+ return normalized_chunk, remapped_event_type
649
+
650
+ def _prepare_event_metadata(self, chunk: A2AEvent, metadata: dict[str, Any] | None) -> dict[str, Any]:
651
+ """Prepare final metadata by merging chunk and provided metadata.
652
+
653
+ Args:
654
+ chunk: A2AEvent containing chunk metadata.
655
+ metadata: Optional additional metadata to merge.
656
+
657
+ Returns:
658
+ dict[str, Any]: Merged metadata dictionary.
659
+ """
660
+ final_metadata = {}
661
+ if metadata:
662
+ final_metadata.update(metadata)
663
+
664
+ chunk_metadata = chunk.get("metadata", {})
665
+ if chunk_metadata:
666
+ final_metadata.update(chunk_metadata)
667
+
668
+ if chunk.get(MetadataFieldKeys.REFERENCES):
669
+ final_metadata[MetadataFieldKeys.REFERENCES] = serialize_references_for_metadata(
670
+ chunk[MetadataFieldKeys.REFERENCES]
671
+ )
672
+
673
+ event_type_value = chunk.get("event_type")
674
+ if isinstance(event_type_value, A2AStreamEventType):
675
+ final_metadata["event_type"] = event_type_value.value
676
+ elif isinstance(event_type_value, str):
677
+ final_metadata["event_type"] = event_type_value
678
+
679
+ # Merge selected top-level fields from chunk into metadata using string keys
680
+ metadata_fields = [
681
+ MetadataFieldKeys.TOOL_INFO,
682
+ MetadataFieldKeys.STEP_USAGE,
683
+ MetadataFieldKeys.TOTAL_USAGE,
684
+ MetadataFieldKeys.THINKING_AND_ACTIVITY_INFO,
685
+ ]
686
+
687
+ for key in metadata_fields:
688
+ value = chunk.get(key)
689
+ if value is not None and value: # Only overwrite if value is truthy
690
+ final_metadata[key] = value
691
+
692
+ return final_metadata
693
+
694
+ async def _process_event_artifacts(self, chunk: A2AEvent, updater: TaskUpdater, task_id: str) -> None:
695
+ """Process any artifacts attached to the event.
696
+
697
+ Args:
698
+ chunk: A2AEvent that may contain artifacts.
699
+ updater: TaskUpdater for handling artifact events.
700
+ task_id: Task identifier for artifact processing.
701
+ """
702
+ if "artifacts" in chunk and chunk["artifacts"]:
703
+ for artifact_data in chunk["artifacts"]:
704
+ await self._handle_artifact_event(artifact_data, updater, task_id)
705
+
706
+ async def _handle_tool_call_event(
707
+ self,
708
+ chunk: A2AEvent,
709
+ updater: TaskUpdater,
710
+ task_id: str,
711
+ context_id: str,
712
+ metadata: dict[str, Any] | None = None,
713
+ ) -> bool:
714
+ """Handle TOOL_CALL event by sending appropriate status update.
715
+
716
+ Args:
717
+ chunk: A2AEvent with TOOL_CALL type and tool information.
718
+ updater: TaskUpdater for sending status updates.
719
+ task_id: Task identifier.
720
+ context_id: Context identifier.
721
+ metadata: Optional metadata to include with the status update.
722
+
723
+ Returns:
724
+ bool: False to continue stream processing.
725
+ """
726
+ status_message = chunk["content"]
727
+ final_metadata = self._prepare_event_metadata(chunk, metadata)
728
+ message = Message(
729
+ role=Role.agent,
730
+ parts=[Part(root=TextPart(text=status_message))],
731
+ message_id=str(uuid.uuid4()),
732
+ task_id=task_id,
733
+ context_id=context_id,
734
+ )
735
+
736
+ await self._update_status(
737
+ updater,
738
+ TaskState.working,
739
+ message,
740
+ StatusUpdateParams(metadata=final_metadata, task_id=task_id, context_id=context_id),
741
+ )
742
+ return False
743
+
744
+ async def _handle_tool_result_event(
745
+ self,
746
+ chunk: A2AEvent,
747
+ updater: TaskUpdater,
748
+ task_id: str,
749
+ context_id: str,
750
+ metadata: dict[str, Any] | None = None,
751
+ ) -> bool:
752
+ """Handle TOOL_RESULT event by sending completion status update.
753
+
754
+ Args:
755
+ chunk: A2AEvent with TOOL_RESULT type and execution details.
756
+ updater: TaskUpdater for sending status updates.
757
+ task_id: Task identifier.
758
+ context_id: Context identifier.
759
+ metadata: Optional metadata to include with the status update.
760
+
761
+ Returns:
762
+ bool: False to continue stream processing.
763
+ """
764
+ status_message = self._extract_tool_result_status_message(chunk)
765
+ final_metadata = self._prepare_event_metadata(chunk, metadata)
766
+ message = Message(
767
+ role=Role.agent,
768
+ parts=[Part(root=TextPart(text=status_message))],
769
+ message_id=str(uuid.uuid4()),
770
+ task_id=task_id,
771
+ context_id=context_id,
772
+ )
773
+
774
+ await self._update_status(
775
+ updater,
776
+ TaskState.working,
777
+ message,
778
+ StatusUpdateParams(metadata=final_metadata, task_id=task_id, context_id=context_id),
779
+ )
780
+ return False
781
+
782
+ async def _handle_content_chunk_event(
783
+ self, chunk: A2AEvent, event_queue: EventQueue, task_id: str, context_id: str, final_metadata: dict[str, Any]
784
+ ) -> bool:
785
+ """Handle CONTENT_CHUNK event by streaming content as artifact.
786
+
787
+ Args:
788
+ chunk: A2AEvent with CONTENT_CHUNK type and user content.
789
+ event_queue: Event queue for artifact updates.
790
+ task_id: Task identifier.
791
+ context_id: Context identifier.
792
+ final_metadata: Merged metadata for the artifact.
793
+
794
+ Returns:
795
+ bool: False to continue stream processing.
796
+ """
797
+ is_first_chunk = task_id not in self._streaming_artifacts
798
+ self._apply_cumulative_time(task_id, final_metadata)
799
+ await self._send_content_as_artifact(
800
+ content=chunk["content"],
801
+ event_queue=event_queue,
802
+ task_id=task_id,
803
+ context_id=context_id,
804
+ append=not is_first_chunk,
805
+ last_chunk=False,
806
+ metadata=final_metadata,
807
+ )
808
+ self._streaming_artifacts[task_id] = True
809
+ return False
810
+
811
+ async def _handle_final_response_event( # noqa: PLR0913
812
+ self,
813
+ chunk: A2AEvent,
814
+ updater: TaskUpdater,
815
+ event_queue: EventQueue,
816
+ task_id: str,
817
+ context_id: str,
818
+ final_metadata: dict[str, Any],
819
+ ) -> bool:
820
+ """Handle FINAL_RESPONSE event by sending final artifact and completing task.
821
+
822
+ Args:
823
+ chunk: A2AEvent with FINAL_RESPONSE type and final content.
824
+ updater: TaskUpdater for task completion.
825
+ event_queue: Event queue for artifact updates.
826
+ task_id: Task identifier.
827
+ context_id: Context identifier.
828
+ final_metadata: Merged metadata for the artifact.
829
+
830
+ Returns:
831
+ bool: True to terminate stream processing.
832
+ """
833
+ content = chunk["content"]
834
+ has_streaming_content = task_id in self._streaming_artifacts
835
+
836
+ if content is not None:
837
+ self._apply_cumulative_time(task_id, final_metadata)
838
+ artifact_name = "final_response" if not has_streaming_content else "streaming_response"
839
+ await self._send_content_as_artifact(
840
+ content=content,
841
+ event_queue=event_queue,
842
+ task_id=task_id,
843
+ context_id=context_id,
844
+ append=has_streaming_content,
845
+ last_chunk=True,
846
+ artifact_name=artifact_name,
847
+ metadata=final_metadata,
848
+ )
849
+
850
+ # Complete the task via status enqueue to preserve metadata
851
+ completion_message = "Task completed successfully."
852
+ completion_metadata = final_metadata.copy()
853
+ completion_metadata.pop(MetadataFieldKeys.TOOL_INFO, None)
854
+ completion_metadata.pop(MetadataFieldKeys.THINKING_AND_ACTIVITY_INFO, None)
855
+ await self._update_status(
856
+ updater,
857
+ TaskState.completed,
858
+ new_agent_text_message(completion_message, context_id=context_id, task_id=task_id),
859
+ params=StatusUpdateParams(metadata=completion_metadata, final=True, task_id=task_id, context_id=context_id),
860
+ )
861
+ return True
862
+
863
+ async def _handle_status_update_event(
864
+ self, chunk: A2AEvent, updater: TaskUpdater, task_id: str, context_id: str
865
+ ) -> bool:
866
+ """Handle STATUS_UPDATE event by sending generic status message.
867
+
868
+ Args:
869
+ chunk: A2AEvent with STATUS_UPDATE type.
870
+ updater: TaskUpdater for sending status updates.
871
+ task_id: Task identifier.
872
+ context_id: Context identifier.
873
+
874
+ Returns:
875
+ bool: False to continue stream processing.
876
+ """
877
+ # Include metadata for status updates as well, so clients can trace step_ids
878
+ final_metadata = self._prepare_event_metadata(chunk, None)
879
+ message = Message(
880
+ role=Role.agent,
881
+ parts=[Part(root=TextPart(text=chunk["content"]))],
882
+ message_id=str(uuid.uuid4()),
883
+ task_id=task_id,
884
+ context_id=context_id,
885
+ )
886
+ await self._update_status(
887
+ updater,
888
+ TaskState.working,
889
+ message,
890
+ StatusUpdateParams(metadata=final_metadata, task_id=task_id, context_id=context_id),
891
+ )
892
+ return False
893
+
894
+ async def _handle_error_event(self, chunk: A2AEvent, updater: TaskUpdater, task_id: str, context_id: str) -> bool:
895
+ """Handle ERROR event by failing the task.
896
+
897
+ Args:
898
+ chunk: A2AEvent with ERROR type and error details.
899
+ updater: TaskUpdater for task failure.
900
+ task_id: Task identifier.
901
+ context_id: Context identifier.
902
+
903
+ Returns:
904
+ bool: True to terminate stream processing.
905
+ """
906
+ await self._update_status(
907
+ updater,
908
+ TaskState.failed,
909
+ new_agent_text_message(chunk["content"], context_id=context_id, task_id=task_id),
910
+ params=StatusUpdateParams(final=True),
911
+ )
912
+ return True
913
+
914
+ async def _handle_step_limit_exceeded_event(
915
+ self, chunk: A2AEvent, updater: TaskUpdater, task_id: str, context_id: str
916
+ ) -> bool:
917
+ """Handle step limit exceeded events by failing the task with metadata.
918
+
919
+ Args:
920
+ chunk: A2AEvent payload describing the step limit exceed event,
921
+ including content and optional metadata.
922
+ updater: TaskUpdater used to emit status updates to the A2A server.
923
+ task_id: Identifier of the task whose step limit was exceeded.
924
+ context_id: Context identifier associated with the task.
925
+
926
+ Returns:
927
+ bool: True to terminate further stream processing for the task.
928
+ """
929
+ final_metadata = self._prepare_event_metadata(chunk, None)
930
+ message_text = chunk.get("content") or "Agent exceeded the configured step limit."
931
+
932
+ await self._update_status(
933
+ updater,
934
+ TaskState.failed,
935
+ new_agent_text_message(message_text, context_id=context_id, task_id=task_id),
936
+ params=StatusUpdateParams(final=True, metadata=final_metadata, task_id=task_id, context_id=context_id),
937
+ )
938
+ return True
939
+
940
+ def _extract_tool_result_status_message(self, chunk: A2AEvent) -> str:
941
+ """Extract status message for tool completion from A2AEvent.
942
+
943
+ Args:
944
+ chunk: A2AEvent with TOOL_RESULT type and execution details.
945
+
946
+ Returns:
947
+ str: Human-readable status message for tool completion.
948
+ """
949
+ content = chunk.get("content")
950
+ if isinstance(content, str) and content.strip():
951
+ return content
952
+
953
+ tool_info = chunk.get(MetadataFieldKeys.TOOL_INFO)
954
+ if tool_info and tool_info.get("name"):
955
+ return f"Completed {tool_info['name']}"
956
+ # Fall back to the generic completion message used when tasks end silently.
957
+ return "Task completed successfully."
958
+
959
+ @abstractmethod
960
+ async def execute(
961
+ self,
962
+ context: RequestContext,
963
+ event_queue: EventQueue,
964
+ ) -> None:
965
+ """Processes an incoming agent request and manages its execution.
966
+
967
+ Implementations should interact with the underlying agent (e.g., a LangGraph
968
+ or Google ADK agent) based on the provided `context`. All communications
969
+ regarding task status, artifacts, and completion must be sent through
970
+ the `event_queue`.
971
+
972
+ This method typically involves:
973
+ 1. Calling `_handle_initial_execute_checks` for validation and setup.
974
+ 2. Defining an agent-specific coroutine for processing (e.g., `_process_stream`).
975
+ 3. Calling `_execute_agent_processing` to manage the lifecycle of this coroutine.
976
+
977
+ Args:
978
+ context (RequestContext): The request context containing information about the incoming
979
+ message, task, and other relevant data.
980
+ event_queue (EventQueue): The queue used to send events (e.g., task status updates,
981
+ artifacts) back to the A2A server infrastructure.
982
+ """
983
+ raise NotImplementedError("Concrete A2A executors must implement the 'execute' method.")
984
+
985
+ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
986
+ """Handles a request to cancel an ongoing agent task.
987
+
988
+ This method attempts to cancel an active asyncio.Task associated with the
989
+ given `context.task_id`. It waits for a short period for the task to handle
990
+ the cancellation gracefully. The `event_queue` is used to report the
991
+ outcome of the cancellation attempt (e.g., success, error during cleanup).
992
+
993
+ Args:
994
+ context (RequestContext): The request context for the task to be cancelled,
995
+ primarily used to get the `task_id` and `context_id`.
996
+ event_queue (EventQueue): The queue for sending cancellation status events.
997
+ """
998
+ task_id = context.task_id
999
+ task = self._active_tasks.get(task_id)
1000
+ updater = TaskUpdater(event_queue, task_id, context.context_id)
1001
+
1002
+ cancelled_by_client = False
1003
+ handled = False
1004
+ cancelled_error: asyncio.CancelledError | None = None
1005
+ if task and not task.done():
1006
+ logger.info(f"Attempting to cancel task {task_id} due to client request.")
1007
+ task.cancel()
1008
+ cancelled_by_client = True
1009
+ cancelled_error, handled = await self._request_task_cancellation(task, task_id, context, updater)
1010
+ if handled:
1011
+ return
1012
+
1013
+ self._remove_active_task(task_id)
1014
+ await self._emit_cancellation_status(context, updater, task, cancelled_by_client)
1015
+
1016
+ if cancelled_error is not None:
1017
+ raise cancelled_error
1018
+
1019
+ async def _request_task_cancellation(
1020
+ self,
1021
+ task: asyncio.Task,
1022
+ task_id: str,
1023
+ context: RequestContext,
1024
+ updater: TaskUpdater,
1025
+ ) -> tuple[asyncio.CancelledError | None, bool]:
1026
+ """Wait for a cancelled task to finish any cleanup.
1027
+
1028
+ Args:
1029
+ task: The asyncio.Task that was cancelled.
1030
+ task_id: The ID of the task being cancelled.
1031
+ context: The RequestContext for the cancellation request.
1032
+ updater: The TaskUpdater for sending status updates.
1033
+
1034
+ Returns:
1035
+ A tuple containing:
1036
+ - The asyncio.CancelledError if the task surfaced one while waiting.
1037
+ - A boolean indicating whether the caller should stop further processing
1038
+ because the helper already handled status updates and cleanup.
1039
+ """
1040
+ try:
1041
+ await asyncio.wait_for(asyncio.shield(task), timeout=5.0)
1042
+ logger.info(f"Task {task_id} completed its execution or cleanup after cancellation request.")
1043
+ return None, False
1044
+ except asyncio.CancelledError as exc:
1045
+ logger.info(f"Task {task_id} was externally cancelled and finished its cancellation logic or timed out.")
1046
+ return exc, False
1047
+ except TimeoutError:
1048
+ logger.warning(
1049
+ f"Timeout waiting for task {task_id} to complete after cancellation request. "
1050
+ "It might still be running cleanup in the background or may not have handled cancellation properly."
1051
+ )
1052
+ return None, False
1053
+ except Exception as error: # noqa: BLE001
1054
+ logger.error(
1055
+ f"Error encountered while waiting for cancelled task {task_id} to finish: {error}",
1056
+ exc_info=True,
1057
+ )
1058
+ await self._update_status(
1059
+ updater,
1060
+ TaskState.canceled,
1061
+ message=new_agent_text_message(
1062
+ f"Task cancelled, but an error occurred during its cleanup: {str(error)}"
1063
+ ),
1064
+ params=StatusUpdateParams(task_id=task_id, context_id=context.context_id),
1065
+ )
1066
+ self._remove_active_task(task_id)
1067
+ return None, True
1068
+
1069
+ async def _emit_cancellation_status(
1070
+ self,
1071
+ context: RequestContext,
1072
+ updater: TaskUpdater,
1073
+ task: asyncio.Task | None,
1074
+ cancelled_by_client: bool,
1075
+ ) -> None:
1076
+ """Emit the final cancellation status based on task state.
1077
+
1078
+ Args:
1079
+ context: The RequestContext for the cancellation request.
1080
+ updater: The TaskUpdater for sending status updates.
1081
+ task: The asyncio.Task that was cancelled, or None.
1082
+ cancelled_by_client: Whether the cancellation was requested by the client.
1083
+ """
1084
+ if cancelled_by_client:
1085
+ await self._update_status(
1086
+ updater,
1087
+ TaskState.canceled,
1088
+ message=new_agent_text_message("Task cancelled successfully by client request."),
1089
+ params=StatusUpdateParams(task_id=context.task_id, context_id=context.context_id),
1090
+ )
1091
+ return
1092
+
1093
+ if task and task.cancelled():
1094
+ await self._update_status(
1095
+ updater,
1096
+ TaskState.canceled,
1097
+ message=new_agent_text_message("Task was found to be already cancelled."),
1098
+ params=StatusUpdateParams(task_id=context.task_id, context_id=context.context_id),
1099
+ )
1100
+ return
1101
+
1102
+ if task and task.done() and not task.cancelled():
1103
+ logger.info(f"Task {context.task_id} was already done (completed/failed) when cancel was processed.")
1104
+ current_task = context.current_task
1105
+ if current_task and current_task.status not in (
1106
+ TaskState.completed,
1107
+ TaskState.failed,
1108
+ TaskState.canceled,
1109
+ ):
1110
+ await self._update_status(
1111
+ updater,
1112
+ TaskState.canceled,
1113
+ message=new_agent_text_message("Task was already done but marked as cancelled per request."),
1114
+ params=StatusUpdateParams(task_id=context.task_id, context_id=context.context_id),
1115
+ )
1116
+ return
1117
+
1118
+ await self._update_status(
1119
+ updater,
1120
+ TaskState.canceled,
1121
+ message=new_agent_text_message(
1122
+ "Task cancellation processed; task was not actively running or already handled."
1123
+ ),
1124
+ params=StatusUpdateParams(task_id=context.task_id, context_id=context.context_id),
1125
+ )