solace-agent-mesh 0.2.4__py3-none-any.whl → 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (521) hide show
  1. solace_agent_mesh/__init__.py +5 -0
  2. solace_agent_mesh/agent/adk/adk_llm.txt +93 -0
  3. solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
  4. solace_agent_mesh/agent/adk/callbacks.py +1716 -0
  5. solace_agent_mesh/agent/adk/filesystem_artifact_service.py +381 -0
  6. solace_agent_mesh/agent/adk/invocation_monitor.py +295 -0
  7. solace_agent_mesh/agent/adk/models/lite_llm.py +872 -0
  8. solace_agent_mesh/agent/adk/models/models_llm.txt +94 -0
  9. solace_agent_mesh/agent/adk/runner.py +357 -0
  10. solace_agent_mesh/agent/adk/services.py +240 -0
  11. solace_agent_mesh/agent/adk/setup.py +751 -0
  12. solace_agent_mesh/agent/adk/stream_parser.py +214 -0
  13. solace_agent_mesh/agent/adk/tool_wrapper.py +139 -0
  14. solace_agent_mesh/agent/agent_llm.txt +41 -0
  15. solace_agent_mesh/agent/protocol/event_handlers.py +1444 -0
  16. solace_agent_mesh/agent/protocol/protocol_llm.txt +21 -0
  17. solace_agent_mesh/agent/sac/app.py +640 -0
  18. solace_agent_mesh/agent/sac/component.py +3496 -0
  19. solace_agent_mesh/agent/sac/patch_adk.py +111 -0
  20. solace_agent_mesh/agent/sac/sac_llm.txt +105 -0
  21. solace_agent_mesh/agent/sac/task_execution_context.py +185 -0
  22. solace_agent_mesh/agent/testing/__init__.py +3 -0
  23. solace_agent_mesh/agent/testing/debug_utils.py +135 -0
  24. solace_agent_mesh/agent/testing/testing_llm.txt +90 -0
  25. solace_agent_mesh/agent/tools/__init__.py +14 -0
  26. solace_agent_mesh/agent/tools/audio_tools.py +1622 -0
  27. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +1954 -0
  28. solace_agent_mesh/agent/tools/builtin_data_analysis_tools.py +238 -0
  29. solace_agent_mesh/agent/tools/general_agent_tools.py +571 -0
  30. solace_agent_mesh/agent/tools/image_tools.py +1184 -0
  31. solace_agent_mesh/agent/tools/peer_agent_tool.py +290 -0
  32. solace_agent_mesh/agent/tools/registry.py +36 -0
  33. solace_agent_mesh/agent/tools/test_tools.py +135 -0
  34. solace_agent_mesh/agent/tools/tool_definition.py +45 -0
  35. solace_agent_mesh/agent/tools/tools_llm.txt +104 -0
  36. solace_agent_mesh/agent/tools/web_tools.py +381 -0
  37. solace_agent_mesh/agent/utils/artifact_helpers.py +927 -0
  38. solace_agent_mesh/agent/utils/config_parser.py +47 -0
  39. solace_agent_mesh/agent/utils/context_helpers.py +60 -0
  40. solace_agent_mesh/agent/utils/utils_llm.txt +153 -0
  41. solace_agent_mesh/assets/docs/404.html +16 -0
  42. solace_agent_mesh/assets/docs/assets/css/styles.906a1503.css +1 -0
  43. solace_agent_mesh/assets/docs/assets/images/Solace_AI_Framework_With_Broker-85f0a306a9bcdd20b390b7a949f6d862.png +0 -0
  44. solace_agent_mesh/assets/docs/assets/images/sac-flows-80d5b603c6aafd33e87945680ce0abf3.png +0 -0
  45. solace_agent_mesh/assets/docs/assets/images/sac_parts_of_a_component-cb3d0424b1d0c17734c5435cca6b4082.png +0 -0
  46. solace_agent_mesh/assets/docs/assets/js/04989206.674a8007.js +1 -0
  47. solace_agent_mesh/assets/docs/assets/js/0e682baa.79f0ab22.js +1 -0
  48. solace_agent_mesh/assets/docs/assets/js/1001.0182a8bd.js +1 -0
  49. solace_agent_mesh/assets/docs/assets/js/1023fc19.015679ca.js +1 -0
  50. solace_agent_mesh/assets/docs/assets/js/1039.0bd46aa1.js +1 -0
  51. solace_agent_mesh/assets/docs/assets/js/149.b797a808.js +1 -0
  52. solace_agent_mesh/assets/docs/assets/js/1523c6b4.91c7bc01.js +1 -0
  53. solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js +2 -0
  54. solace_agent_mesh/assets/docs/assets/js/165.6a39807d.js.LICENSE.txt +9 -0
  55. solace_agent_mesh/assets/docs/assets/js/166ab619.7d97ccaf.js +1 -0
  56. solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js +2 -0
  57. solace_agent_mesh/assets/docs/assets/js/17896441.a5e82f9b.js.LICENSE.txt +7 -0
  58. solace_agent_mesh/assets/docs/assets/js/1c6e87d2.a8c5ce5a.js +1 -0
  59. solace_agent_mesh/assets/docs/assets/js/2130.ab9fd314.js +1 -0
  60. solace_agent_mesh/assets/docs/assets/js/21ceee5f.614fa8dd.js +1 -0
  61. solace_agent_mesh/assets/docs/assets/js/2237.5e477fc6.js +1 -0
  62. solace_agent_mesh/assets/docs/assets/js/2334.622a6395.js +1 -0
  63. solace_agent_mesh/assets/docs/assets/js/2a9cab12.8909df92.js +1 -0
  64. solace_agent_mesh/assets/docs/assets/js/3219.adc1d663.js +1 -0
  65. solace_agent_mesh/assets/docs/assets/js/332e10b5.7a103f42.js +1 -0
  66. solace_agent_mesh/assets/docs/assets/js/3624.b524e433.js +1 -0
  67. solace_agent_mesh/assets/docs/assets/js/375.708d48db.js +1 -0
  68. solace_agent_mesh/assets/docs/assets/js/3834.b6cd790e.js +1 -0
  69. solace_agent_mesh/assets/docs/assets/js/3d406171.f722eaf5.js +1 -0
  70. solace_agent_mesh/assets/docs/assets/js/4250.95455b28.js +1 -0
  71. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.36090198.js +1 -0
  72. solace_agent_mesh/assets/docs/assets/js/4356.d169ab5b.js +1 -0
  73. solace_agent_mesh/assets/docs/assets/js/442a8107.5ba94b65.js +1 -0
  74. solace_agent_mesh/assets/docs/assets/js/4458.518e66fa.js +1 -0
  75. solace_agent_mesh/assets/docs/assets/js/4488.c7cc3442.js +1 -0
  76. solace_agent_mesh/assets/docs/assets/js/4494.6ee23046.js +1 -0
  77. solace_agent_mesh/assets/docs/assets/js/4855.fc4444b6.js +1 -0
  78. solace_agent_mesh/assets/docs/assets/js/4866.22daefc0.js +1 -0
  79. solace_agent_mesh/assets/docs/assets/js/4950.ca4caeda.js +1 -0
  80. solace_agent_mesh/assets/docs/assets/js/4c2787c2.66ee00e9.js +1 -0
  81. solace_agent_mesh/assets/docs/assets/js/5388.7a136447.js +1 -0
  82. solace_agent_mesh/assets/docs/assets/js/55f47984.c484bf96.js +1 -0
  83. solace_agent_mesh/assets/docs/assets/js/5607.081356f8.js +1 -0
  84. solace_agent_mesh/assets/docs/assets/js/5864.b0d0e9de.js +1 -0
  85. solace_agent_mesh/assets/docs/assets/js/5b4258a4.bda20761.js +1 -0
  86. solace_agent_mesh/assets/docs/assets/js/5e95c892.558d5167.js +1 -0
  87. solace_agent_mesh/assets/docs/assets/js/6143.0a1464c9.js +1 -0
  88. solace_agent_mesh/assets/docs/assets/js/6395.e9c73649.js +1 -0
  89. solace_agent_mesh/assets/docs/assets/js/6796.51d2c9b7.js +1 -0
  90. solace_agent_mesh/assets/docs/assets/js/6976.379be23b.js +1 -0
  91. solace_agent_mesh/assets/docs/assets/js/6978.ee0b945c.js +1 -0
  92. solace_agent_mesh/assets/docs/assets/js/7040.cb436723.js +1 -0
  93. solace_agent_mesh/assets/docs/assets/js/7195.412f418a.js +1 -0
  94. solace_agent_mesh/assets/docs/assets/js/7280.3fb73bdb.js +1 -0
  95. solace_agent_mesh/assets/docs/assets/js/768e31b0.a12673db.js +1 -0
  96. solace_agent_mesh/assets/docs/assets/js/7845.e33e7c4c.js +1 -0
  97. solace_agent_mesh/assets/docs/assets/js/7900.69516146.js +1 -0
  98. solace_agent_mesh/assets/docs/assets/js/8356.8a379c04.js +1 -0
  99. solace_agent_mesh/assets/docs/assets/js/85387663.6bf41934.js +1 -0
  100. solace_agent_mesh/assets/docs/assets/js/8567.4732c6b7.js +1 -0
  101. solace_agent_mesh/assets/docs/assets/js/8573.cb04eda5.js +1 -0
  102. solace_agent_mesh/assets/docs/assets/js/8577.1d54e766.js +1 -0
  103. solace_agent_mesh/assets/docs/assets/js/8591.d7c16be6.js +2 -0
  104. solace_agent_mesh/assets/docs/assets/js/8591.d7c16be6.js.LICENSE.txt +61 -0
  105. solace_agent_mesh/assets/docs/assets/js/8709.7ecd4047.js +1 -0
  106. solace_agent_mesh/assets/docs/assets/js/8731.49e930c2.js +1 -0
  107. solace_agent_mesh/assets/docs/assets/js/8908.f9d1b506.js +1 -0
  108. solace_agent_mesh/assets/docs/assets/js/9157.b4093d07.js +1 -0
  109. solace_agent_mesh/assets/docs/assets/js/9278.a4fd875d.js +1 -0
  110. solace_agent_mesh/assets/docs/assets/js/945fb41e.74d728aa.js +1 -0
  111. solace_agent_mesh/assets/docs/assets/js/9616.b75c2f6d.js +1 -0
  112. solace_agent_mesh/assets/docs/assets/js/9793.c6d16376.js +1 -0
  113. solace_agent_mesh/assets/docs/assets/js/9eff14a2.1bf8f61c.js +1 -0
  114. solace_agent_mesh/assets/docs/assets/js/a3a92b25.26ca071f.js +1 -0
  115. solace_agent_mesh/assets/docs/assets/js/a7bd4aaa.2204d2f7.js +1 -0
  116. solace_agent_mesh/assets/docs/assets/js/a94703ab.0438dbc2.js +1 -0
  117. solace_agent_mesh/assets/docs/assets/js/aba21aa0.c42a534c.js +1 -0
  118. solace_agent_mesh/assets/docs/assets/js/aba87c2f.d3e2dcc3.js +1 -0
  119. solace_agent_mesh/assets/docs/assets/js/ae4415af.8e279b5d.js +1 -0
  120. solace_agent_mesh/assets/docs/assets/js/b7006a3a.40b10c9d.js +1 -0
  121. solace_agent_mesh/assets/docs/assets/js/bac0be12.f50d9bac.js +1 -0
  122. solace_agent_mesh/assets/docs/assets/js/bb2ef573.207e6990.js +1 -0
  123. solace_agent_mesh/assets/docs/assets/js/c2c06897.63b76e9e.js +1 -0
  124. solace_agent_mesh/assets/docs/assets/js/cc969b05.954186d4.js +1 -0
  125. solace_agent_mesh/assets/docs/assets/js/cd3d4052.ca6eed8c.js +1 -0
  126. solace_agent_mesh/assets/docs/assets/js/ced92a13.fb92e7ca.js +1 -0
  127. solace_agent_mesh/assets/docs/assets/js/cee5d587.f5b73ca1.js +1 -0
  128. solace_agent_mesh/assets/docs/assets/js/f284c35a.ecc3d195.js +1 -0
  129. solace_agent_mesh/assets/docs/assets/js/f897a61a.f8c53b0f.js +1 -0
  130. solace_agent_mesh/assets/docs/assets/js/fbfa3e75.aca209c9.js +1 -0
  131. solace_agent_mesh/assets/docs/assets/js/main.c6286d7c.js +2 -0
  132. solace_agent_mesh/assets/docs/assets/js/main.c6286d7c.js.LICENSE.txt +81 -0
  133. solace_agent_mesh/assets/docs/assets/js/runtime~main.d5133813.js +1 -0
  134. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +128 -0
  135. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +91 -0
  136. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +201 -0
  137. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +91 -0
  138. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +55 -0
  139. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +82 -0
  140. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +77 -0
  141. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +48 -0
  142. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +54 -0
  143. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +17 -0
  144. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +45 -0
  145. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +76 -0
  146. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +150 -0
  147. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +54 -0
  148. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +267 -0
  149. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +136 -0
  150. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +116 -0
  151. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +80 -0
  152. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +164 -0
  153. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +57 -0
  154. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +72 -0
  155. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +102 -0
  156. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +99 -0
  157. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +90 -0
  158. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +107 -0
  159. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +152 -0
  160. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +103 -0
  161. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +170 -0
  162. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +200 -0
  163. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +54 -0
  164. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +69 -0
  165. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +59 -0
  166. solace_agent_mesh/assets/docs/img/Solace_AI_Framework_README.png +0 -0
  167. solace_agent_mesh/assets/docs/img/Solace_AI_Framework_With_Broker.png +0 -0
  168. solace_agent_mesh/assets/docs/img/logo.png +0 -0
  169. solace_agent_mesh/assets/docs/img/sac-flows.png +0 -0
  170. solace_agent_mesh/assets/docs/img/sac_parts_of_a_component.png +0 -0
  171. solace_agent_mesh/assets/docs/img/solace-logo.png +0 -0
  172. solace_agent_mesh/assets/docs/lunr-index-1754075282978.json +1 -0
  173. solace_agent_mesh/assets/docs/lunr-index.json +1 -0
  174. solace_agent_mesh/assets/docs/search-doc-1754075282978.json +1 -0
  175. solace_agent_mesh/assets/docs/search-doc.json +1 -0
  176. solace_agent_mesh/assets/docs/sitemap.xml +1 -0
  177. solace_agent_mesh/cli/__init__.py +1 -1
  178. solace_agent_mesh/cli/commands/add_cmd/__init__.py +15 -0
  179. solace_agent_mesh/cli/commands/add_cmd/add_cmd_llm.txt +250 -0
  180. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +659 -0
  181. solace_agent_mesh/cli/commands/add_cmd/gateway_cmd.py +322 -0
  182. solace_agent_mesh/cli/commands/add_cmd/web_add_agent_step.py +93 -0
  183. solace_agent_mesh/cli/commands/add_cmd/web_add_gateway_step.py +118 -0
  184. solace_agent_mesh/cli/commands/docs_cmd.py +57 -0
  185. solace_agent_mesh/cli/commands/eval_cmd.py +64 -0
  186. solace_agent_mesh/cli/commands/init_cmd/__init__.py +404 -0
  187. solace_agent_mesh/cli/commands/init_cmd/broker_step.py +201 -0
  188. solace_agent_mesh/cli/commands/init_cmd/directory_step.py +28 -0
  189. solace_agent_mesh/cli/commands/init_cmd/env_step.py +205 -0
  190. solace_agent_mesh/cli/commands/init_cmd/init_cmd_llm.txt +365 -0
  191. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +407 -0
  192. solace_agent_mesh/cli/commands/init_cmd/project_files_step.py +38 -0
  193. solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +110 -0
  194. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +183 -0
  195. solace_agent_mesh/cli/commands/plugin_cmd/__init__.py +18 -0
  196. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +372 -0
  197. solace_agent_mesh/cli/commands/plugin_cmd/build_cmd.py +86 -0
  198. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +139 -0
  199. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +309 -0
  200. solace_agent_mesh/cli/commands/plugin_cmd/official_registry.py +175 -0
  201. solace_agent_mesh/cli/commands/plugin_cmd/plugin_cmd_llm.txt +305 -0
  202. solace_agent_mesh/cli/commands/run_cmd.py +158 -0
  203. solace_agent_mesh/cli/main.py +17 -294
  204. solace_agent_mesh/cli/utils.py +135 -204
  205. solace_agent_mesh/client/webui/frontend/static/assets/authCallback-DvlO62me.js +1 -0
  206. solace_agent_mesh/client/webui/frontend/static/assets/client-bp6u3qVZ.js +49 -0
  207. solace_agent_mesh/client/webui/frontend/static/assets/favicon-BLgzUch9.ico +0 -0
  208. solace_agent_mesh/client/webui/frontend/static/assets/main-D11Lmy9p.css +1 -0
  209. solace_agent_mesh/client/webui/frontend/static/assets/main-Gfk3BYn5.js +663 -0
  210. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +14 -0
  211. solace_agent_mesh/client/webui/frontend/static/index.html +15 -0
  212. solace_agent_mesh/common/__init__.py +1 -0
  213. solace_agent_mesh/common/a2a_protocol.py +564 -0
  214. solace_agent_mesh/common/agent_registry.py +42 -0
  215. solace_agent_mesh/common/client/__init__.py +4 -0
  216. solace_agent_mesh/common/client/card_resolver.py +21 -0
  217. solace_agent_mesh/common/client/client.py +85 -0
  218. solace_agent_mesh/common/client/client_llm.txt +133 -0
  219. solace_agent_mesh/common/common_llm.txt +144 -0
  220. solace_agent_mesh/common/constants.py +1 -14
  221. solace_agent_mesh/common/middleware/__init__.py +12 -0
  222. solace_agent_mesh/common/middleware/config_resolver.py +130 -0
  223. solace_agent_mesh/common/middleware/middleware_llm.txt +174 -0
  224. solace_agent_mesh/common/middleware/registry.py +125 -0
  225. solace_agent_mesh/common/server/__init__.py +4 -0
  226. solace_agent_mesh/common/server/server.py +122 -0
  227. solace_agent_mesh/common/server/server_llm.txt +169 -0
  228. solace_agent_mesh/common/server/task_manager.py +291 -0
  229. solace_agent_mesh/common/server/utils.py +28 -0
  230. solace_agent_mesh/common/services/__init__.py +4 -0
  231. solace_agent_mesh/common/services/employee_service.py +162 -0
  232. solace_agent_mesh/common/services/identity_service.py +129 -0
  233. solace_agent_mesh/common/services/providers/__init__.py +4 -0
  234. solace_agent_mesh/common/services/providers/local_file_identity_service.py +148 -0
  235. solace_agent_mesh/common/services/providers/providers_llm.txt +113 -0
  236. solace_agent_mesh/common/services/services_llm.txt +132 -0
  237. solace_agent_mesh/common/types.py +411 -0
  238. solace_agent_mesh/common/utils/__init__.py +7 -0
  239. solace_agent_mesh/common/utils/asyncio_macos_fix.py +86 -0
  240. solace_agent_mesh/common/utils/embeds/__init__.py +33 -0
  241. solace_agent_mesh/common/utils/embeds/constants.py +55 -0
  242. solace_agent_mesh/common/utils/embeds/converter.py +452 -0
  243. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +124 -0
  244. solace_agent_mesh/common/utils/embeds/evaluators.py +394 -0
  245. solace_agent_mesh/common/utils/embeds/modifiers.py +816 -0
  246. solace_agent_mesh/common/utils/embeds/resolver.py +865 -0
  247. solace_agent_mesh/common/utils/embeds/types.py +14 -0
  248. solace_agent_mesh/common/utils/in_memory_cache.py +108 -0
  249. solace_agent_mesh/common/utils/initializer.py +51 -0
  250. solace_agent_mesh/common/utils/log_formatters.py +44 -0
  251. solace_agent_mesh/common/utils/mime_helpers.py +106 -0
  252. solace_agent_mesh/common/utils/push_notification_auth.py +134 -0
  253. solace_agent_mesh/common/utils/utils_llm.txt +67 -0
  254. solace_agent_mesh/config_portal/backend/common.py +66 -24
  255. solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +24 -0
  256. solace_agent_mesh/config_portal/backend/plugin_catalog/models.py +49 -0
  257. solace_agent_mesh/config_portal/backend/plugin_catalog/registry_manager.py +164 -0
  258. solace_agent_mesh/config_portal/backend/plugin_catalog/scraper.py +521 -0
  259. solace_agent_mesh/config_portal/backend/plugin_catalog_server.py +217 -0
  260. solace_agent_mesh/config_portal/backend/server.py +551 -181
  261. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-_7yox_eh.js +48 -0
  262. solace_agent_mesh/config_portal/frontend/static/client/assets/components-B7lKcHVY.js +140 -0
  263. solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-DX1misIU.js → entry.client-CEumGClk.js} +3 -3
  264. solace_agent_mesh/config_portal/frontend/static/client/assets/index-DSo1AH_7.js +68 -0
  265. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-e5c3acfe.js +1 -0
  266. solace_agent_mesh/config_portal/frontend/static/client/assets/{root-BApq5dPK.js → root-C4XmHinv.js} +2 -2
  267. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DxRwaWiE.css +1 -0
  268. solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
  269. solace_agent_mesh/core_a2a/__init__.py +1 -0
  270. solace_agent_mesh/core_a2a/core_a2a_llm.txt +88 -0
  271. solace_agent_mesh/core_a2a/service.py +331 -0
  272. solace_agent_mesh/evaluation/config_loader.py +657 -0
  273. solace_agent_mesh/evaluation/evaluator.py +667 -0
  274. solace_agent_mesh/evaluation/message_organizer.py +568 -0
  275. solace_agent_mesh/evaluation/report/benchmark_info.html +35 -0
  276. solace_agent_mesh/evaluation/report/chart_section.html +141 -0
  277. solace_agent_mesh/evaluation/report/detailed_breakdown.html +28 -0
  278. solace_agent_mesh/evaluation/report/modal.html +59 -0
  279. solace_agent_mesh/evaluation/report/modal_chart_functions.js +411 -0
  280. solace_agent_mesh/evaluation/report/modal_script.js +296 -0
  281. solace_agent_mesh/evaluation/report/modal_styles.css +340 -0
  282. solace_agent_mesh/evaluation/report/performance_metrics_styles.css +93 -0
  283. solace_agent_mesh/evaluation/report/templates/footer.html +2 -0
  284. solace_agent_mesh/evaluation/report/templates/header.html +340 -0
  285. solace_agent_mesh/evaluation/report_data_processor.py +972 -0
  286. solace_agent_mesh/evaluation/report_generator.py +613 -0
  287. solace_agent_mesh/evaluation/run.py +613 -0
  288. solace_agent_mesh/evaluation/subscriber.py +872 -0
  289. solace_agent_mesh/evaluation/summary_builder.py +775 -0
  290. solace_agent_mesh/evaluation/test_case_loader.py +714 -0
  291. solace_agent_mesh/gateway/base/__init__.py +1 -0
  292. solace_agent_mesh/gateway/base/app.py +266 -0
  293. solace_agent_mesh/gateway/base/base_llm.txt +119 -0
  294. solace_agent_mesh/gateway/base/component.py +1542 -0
  295. solace_agent_mesh/gateway/base/task_context.py +74 -0
  296. solace_agent_mesh/gateway/gateway_llm.txt +125 -0
  297. solace_agent_mesh/gateway/http_sse/app.py +190 -0
  298. solace_agent_mesh/gateway/http_sse/component.py +1602 -0
  299. solace_agent_mesh/gateway/http_sse/components/__init__.py +7 -0
  300. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +65 -0
  301. solace_agent_mesh/gateway/http_sse/components/visualization_forwarder_component.py +108 -0
  302. solace_agent_mesh/gateway/http_sse/dependencies.py +316 -0
  303. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +63 -0
  304. solace_agent_mesh/gateway/http_sse/main.py +442 -0
  305. solace_agent_mesh/gateway/http_sse/routers/__init__.py +4 -0
  306. solace_agent_mesh/gateway/http_sse/routers/agents.py +41 -0
  307. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +827 -0
  308. solace_agent_mesh/gateway/http_sse/routers/auth.py +212 -0
  309. solace_agent_mesh/gateway/http_sse/routers/config.py +55 -0
  310. solace_agent_mesh/gateway/http_sse/routers/people.py +69 -0
  311. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +37 -0
  312. solace_agent_mesh/gateway/http_sse/routers/sessions.py +80 -0
  313. solace_agent_mesh/gateway/http_sse/routers/sse.py +138 -0
  314. solace_agent_mesh/gateway/http_sse/routers/tasks.py +294 -0
  315. solace_agent_mesh/gateway/http_sse/routers/users.py +59 -0
  316. solace_agent_mesh/gateway/http_sse/routers/visualization.py +1131 -0
  317. solace_agent_mesh/gateway/http_sse/services/__init__.py +4 -0
  318. solace_agent_mesh/gateway/http_sse/services/agent_service.py +69 -0
  319. solace_agent_mesh/gateway/http_sse/services/people_service.py +158 -0
  320. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +179 -0
  321. solace_agent_mesh/gateway/http_sse/services/task_service.py +121 -0
  322. solace_agent_mesh/gateway/http_sse/session_manager.py +187 -0
  323. solace_agent_mesh/gateway/http_sse/sse_manager.py +328 -0
  324. solace_agent_mesh/llm.txt +228 -0
  325. solace_agent_mesh/llm_detail.txt +2835 -0
  326. solace_agent_mesh/templates/agent_template.yaml +53 -0
  327. solace_agent_mesh/templates/eval_backend_template.yaml +54 -0
  328. solace_agent_mesh/templates/gateway_app_template.py +73 -0
  329. solace_agent_mesh/templates/gateway_component_template.py +431 -0
  330. solace_agent_mesh/templates/gateway_config_template.yaml +43 -0
  331. solace_agent_mesh/templates/logging_config_template.ini +64 -0
  332. solace_agent_mesh/templates/main_orchestrator.yaml +55 -0
  333. solace_agent_mesh/templates/plugin_agent_config_template.yaml +122 -0
  334. solace_agent_mesh/templates/plugin_custom_config_template.yaml +27 -0
  335. solace_agent_mesh/templates/plugin_custom_template.py +10 -0
  336. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +63 -0
  337. solace_agent_mesh/templates/plugin_pyproject_template.toml +33 -0
  338. solace_agent_mesh/templates/plugin_readme_template.md +34 -0
  339. solace_agent_mesh/templates/plugin_tools_template.py +224 -0
  340. solace_agent_mesh/templates/shared_config.yaml +66 -0
  341. solace_agent_mesh/templates/templates_llm.txt +147 -0
  342. solace_agent_mesh/templates/webui.yaml +53 -0
  343. solace_agent_mesh-1.0.2.dist-info/METADATA +432 -0
  344. solace_agent_mesh-1.0.2.dist-info/RECORD +361 -0
  345. solace_agent_mesh-1.0.2.dist-info/entry_points.txt +3 -0
  346. {solace_agent_mesh-0.2.4.dist-info → solace_agent_mesh-1.0.2.dist-info}/licenses/LICENSE +1 -1
  347. solace_agent_mesh/agents/base_agent_component.py +0 -256
  348. solace_agent_mesh/agents/global/actions/agent_state_change.py +0 -54
  349. solace_agent_mesh/agents/global/actions/clear_history.py +0 -32
  350. solace_agent_mesh/agents/global/actions/convert_file_to_markdown.py +0 -160
  351. solace_agent_mesh/agents/global/actions/create_file.py +0 -70
  352. solace_agent_mesh/agents/global/actions/error_action.py +0 -45
  353. solace_agent_mesh/agents/global/actions/plantuml_diagram.py +0 -163
  354. solace_agent_mesh/agents/global/actions/plotly_graph.py +0 -152
  355. solace_agent_mesh/agents/global/actions/retrieve_file.py +0 -51
  356. solace_agent_mesh/agents/global/global_agent_component.py +0 -38
  357. solace_agent_mesh/agents/image_processing/actions/create_image.py +0 -75
  358. solace_agent_mesh/agents/image_processing/actions/describe_image.py +0 -115
  359. solace_agent_mesh/agents/image_processing/image_processing_agent_component.py +0 -23
  360. solace_agent_mesh/agents/slack/__init__.py +0 -1
  361. solace_agent_mesh/agents/slack/actions/__init__.py +0 -1
  362. solace_agent_mesh/agents/slack/actions/post_message.py +0 -177
  363. solace_agent_mesh/agents/slack/slack_agent_component.py +0 -59
  364. solace_agent_mesh/agents/web_request/actions/do_image_search.py +0 -84
  365. solace_agent_mesh/agents/web_request/actions/do_news_search.py +0 -47
  366. solace_agent_mesh/agents/web_request/actions/do_suggestion_search.py +0 -34
  367. solace_agent_mesh/agents/web_request/actions/do_web_request.py +0 -135
  368. solace_agent_mesh/agents/web_request/actions/download_file.py +0 -69
  369. solace_agent_mesh/agents/web_request/web_request_agent_component.py +0 -33
  370. solace_agent_mesh/assets/web-visualizer/assets/index-D0qORgkg.css +0 -1
  371. solace_agent_mesh/assets/web-visualizer/assets/index-DnDr1pnu.js +0 -109
  372. solace_agent_mesh/assets/web-visualizer/index.html +0 -14
  373. solace_agent_mesh/assets/web-visualizer/vite.svg +0 -1
  374. solace_agent_mesh/cli/commands/add/__init__.py +0 -3
  375. solace_agent_mesh/cli/commands/add/add.py +0 -88
  376. solace_agent_mesh/cli/commands/add/agent.py +0 -110
  377. solace_agent_mesh/cli/commands/add/copy_from_plugin.py +0 -92
  378. solace_agent_mesh/cli/commands/add/gateway.py +0 -374
  379. solace_agent_mesh/cli/commands/build.py +0 -670
  380. solace_agent_mesh/cli/commands/chat/__init__.py +0 -3
  381. solace_agent_mesh/cli/commands/chat/chat.py +0 -361
  382. solace_agent_mesh/cli/commands/config.py +0 -29
  383. solace_agent_mesh/cli/commands/init/__init__.py +0 -3
  384. solace_agent_mesh/cli/commands/init/ai_provider_step.py +0 -93
  385. solace_agent_mesh/cli/commands/init/broker_step.py +0 -99
  386. solace_agent_mesh/cli/commands/init/builtin_agent_step.py +0 -83
  387. solace_agent_mesh/cli/commands/init/check_if_already_done.py +0 -13
  388. solace_agent_mesh/cli/commands/init/create_config_file_step.py +0 -65
  389. solace_agent_mesh/cli/commands/init/create_other_project_files_step.py +0 -147
  390. solace_agent_mesh/cli/commands/init/file_service_step.py +0 -73
  391. solace_agent_mesh/cli/commands/init/init.py +0 -92
  392. solace_agent_mesh/cli/commands/init/project_structure_step.py +0 -16
  393. solace_agent_mesh/cli/commands/init/web_init_step.py +0 -32
  394. solace_agent_mesh/cli/commands/plugin/__init__.py +0 -3
  395. solace_agent_mesh/cli/commands/plugin/add.py +0 -100
  396. solace_agent_mesh/cli/commands/plugin/build.py +0 -268
  397. solace_agent_mesh/cli/commands/plugin/create.py +0 -117
  398. solace_agent_mesh/cli/commands/plugin/plugin.py +0 -124
  399. solace_agent_mesh/cli/commands/plugin/remove.py +0 -73
  400. solace_agent_mesh/cli/commands/run.py +0 -68
  401. solace_agent_mesh/cli/commands/visualizer.py +0 -138
  402. solace_agent_mesh/cli/config.py +0 -85
  403. solace_agent_mesh/common/action.py +0 -91
  404. solace_agent_mesh/common/action_list.py +0 -37
  405. solace_agent_mesh/common/action_response.py +0 -340
  406. solace_agent_mesh/common/mysql_database.py +0 -40
  407. solace_agent_mesh/common/postgres_database.py +0 -85
  408. solace_agent_mesh/common/prompt_templates.py +0 -28
  409. solace_agent_mesh/common/stimulus_utils.py +0 -152
  410. solace_agent_mesh/common/time.py +0 -24
  411. solace_agent_mesh/common/utils.py +0 -712
  412. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-a-zJ6rLx.js +0 -46
  413. solace_agent_mesh/config_portal/frontend/static/client/assets/components-ZIfdTbrV.js +0 -191
  414. solace_agent_mesh/config_portal/frontend/static/client/assets/index-BJHAE5s4.js +0 -17
  415. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-44c41103.js +0 -1
  416. solace_agent_mesh/config_portal/frontend/static/client/assets/root-DX4gQ516.css +0 -1
  417. solace_agent_mesh/configs/agent_global.yaml +0 -74
  418. solace_agent_mesh/configs/agent_image_processing.yaml +0 -82
  419. solace_agent_mesh/configs/agent_slack.yaml +0 -64
  420. solace_agent_mesh/configs/agent_web_request.yaml +0 -75
  421. solace_agent_mesh/configs/conversation_to_file.yaml +0 -56
  422. solace_agent_mesh/configs/error_catcher.yaml +0 -56
  423. solace_agent_mesh/configs/monitor.yaml +0 -0
  424. solace_agent_mesh/configs/monitor_stim_and_errors_to_slack.yaml +0 -109
  425. solace_agent_mesh/configs/monitor_user_feedback.yaml +0 -58
  426. solace_agent_mesh/configs/orchestrator.yaml +0 -241
  427. solace_agent_mesh/configs/service_embedding.yaml +0 -81
  428. solace_agent_mesh/configs/service_llm.yaml +0 -265
  429. solace_agent_mesh/configs/visualize_websocket.yaml +0 -55
  430. solace_agent_mesh/gateway/components/gateway_base.py +0 -47
  431. solace_agent_mesh/gateway/components/gateway_input.py +0 -278
  432. solace_agent_mesh/gateway/components/gateway_output.py +0 -298
  433. solace_agent_mesh/gateway/identity/bamboohr_identity.py +0 -18
  434. solace_agent_mesh/gateway/identity/identity_base.py +0 -10
  435. solace_agent_mesh/gateway/identity/identity_provider.py +0 -60
  436. solace_agent_mesh/gateway/identity/no_identity.py +0 -9
  437. solace_agent_mesh/gateway/identity/passthru_identity.py +0 -9
  438. solace_agent_mesh/monitors/base_monitor_component.py +0 -26
  439. solace_agent_mesh/monitors/feedback/user_feedback_monitor.py +0 -75
  440. solace_agent_mesh/monitors/stim_and_errors/stim_and_error_monitor.py +0 -560
  441. solace_agent_mesh/orchestrator/__init__.py +0 -0
  442. solace_agent_mesh/orchestrator/action_manager.py +0 -237
  443. solace_agent_mesh/orchestrator/components/__init__.py +0 -0
  444. solace_agent_mesh/orchestrator/components/orchestrator_action_manager_timeout_component.py +0 -58
  445. solace_agent_mesh/orchestrator/components/orchestrator_action_response_component.py +0 -179
  446. solace_agent_mesh/orchestrator/components/orchestrator_register_component.py +0 -107
  447. solace_agent_mesh/orchestrator/components/orchestrator_stimulus_processor_component.py +0 -527
  448. solace_agent_mesh/orchestrator/components/orchestrator_streaming_output_component.py +0 -260
  449. solace_agent_mesh/orchestrator/orchestrator_main.py +0 -172
  450. solace_agent_mesh/orchestrator/orchestrator_prompt.py +0 -539
  451. solace_agent_mesh/services/__init__.py +0 -0
  452. solace_agent_mesh/services/authorization/providers/base_authorization_provider.py +0 -56
  453. solace_agent_mesh/services/bamboo_hr_service/__init__.py +0 -3
  454. solace_agent_mesh/services/bamboo_hr_service/bamboo_hr.py +0 -182
  455. solace_agent_mesh/services/common/__init__.py +0 -4
  456. solace_agent_mesh/services/common/auto_expiry.py +0 -45
  457. solace_agent_mesh/services/common/singleton.py +0 -18
  458. solace_agent_mesh/services/file_service/__init__.py +0 -14
  459. solace_agent_mesh/services/file_service/file_manager/__init__.py +0 -0
  460. solace_agent_mesh/services/file_service/file_manager/bucket_file_manager.py +0 -149
  461. solace_agent_mesh/services/file_service/file_manager/file_manager_base.py +0 -162
  462. solace_agent_mesh/services/file_service/file_manager/memory_file_manager.py +0 -64
  463. solace_agent_mesh/services/file_service/file_manager/volume_file_manager.py +0 -106
  464. solace_agent_mesh/services/file_service/file_service.py +0 -437
  465. solace_agent_mesh/services/file_service/file_service_constants.py +0 -54
  466. solace_agent_mesh/services/file_service/file_transformations.py +0 -141
  467. solace_agent_mesh/services/file_service/file_utils.py +0 -324
  468. solace_agent_mesh/services/file_service/transformers/__init__.py +0 -5
  469. solace_agent_mesh/services/history_service/__init__.py +0 -3
  470. solace_agent_mesh/services/history_service/history_providers/__init__.py +0 -0
  471. solace_agent_mesh/services/history_service/history_providers/base_history_provider.py +0 -54
  472. solace_agent_mesh/services/history_service/history_providers/file_history_provider.py +0 -74
  473. solace_agent_mesh/services/history_service/history_providers/index.py +0 -40
  474. solace_agent_mesh/services/history_service/history_providers/memory_history_provider.py +0 -33
  475. solace_agent_mesh/services/history_service/history_providers/mongodb_history_provider.py +0 -66
  476. solace_agent_mesh/services/history_service/history_providers/redis_history_provider.py +0 -66
  477. solace_agent_mesh/services/history_service/history_providers/sql_history_provider.py +0 -93
  478. solace_agent_mesh/services/history_service/history_service.py +0 -413
  479. solace_agent_mesh/services/history_service/long_term_memory/__init__.py +0 -0
  480. solace_agent_mesh/services/history_service/long_term_memory/long_term_memory.py +0 -399
  481. solace_agent_mesh/services/llm_service/components/llm_request_component.py +0 -340
  482. solace_agent_mesh/services/llm_service/components/llm_service_component_base.py +0 -152
  483. solace_agent_mesh/services/middleware_service/__init__.py +0 -0
  484. solace_agent_mesh/services/middleware_service/middleware_service.py +0 -20
  485. solace_agent_mesh/templates/action.py +0 -38
  486. solace_agent_mesh/templates/agent.py +0 -29
  487. solace_agent_mesh/templates/agent.yaml +0 -70
  488. solace_agent_mesh/templates/gateway-config-template.yaml +0 -6
  489. solace_agent_mesh/templates/gateway-default-config.yaml +0 -28
  490. solace_agent_mesh/templates/gateway-flows.yaml +0 -78
  491. solace_agent_mesh/templates/gateway-header.yaml +0 -16
  492. solace_agent_mesh/templates/gateway_base.py +0 -15
  493. solace_agent_mesh/templates/gateway_input.py +0 -98
  494. solace_agent_mesh/templates/gateway_output.py +0 -71
  495. solace_agent_mesh/templates/plugin-gateway-default-config.yaml +0 -29
  496. solace_agent_mesh/templates/plugin-pyproject.toml +0 -30
  497. solace_agent_mesh/templates/rest-api-default-config.yaml +0 -31
  498. solace_agent_mesh/templates/rest-api-flows.yaml +0 -81
  499. solace_agent_mesh/templates/slack-default-config.yaml +0 -16
  500. solace_agent_mesh/templates/slack-flows.yaml +0 -81
  501. solace_agent_mesh/templates/solace-agent-mesh-default.yaml +0 -86
  502. solace_agent_mesh/templates/solace-agent-mesh-plugin-default.yaml +0 -8
  503. solace_agent_mesh/templates/web-default-config.yaml +0 -10
  504. solace_agent_mesh/templates/web-flows.yaml +0 -76
  505. solace_agent_mesh/tools/__init__.py +0 -0
  506. solace_agent_mesh/tools/components/__init__.py +0 -0
  507. solace_agent_mesh/tools/components/conversation_formatter.py +0 -111
  508. solace_agent_mesh/tools/components/file_resolver_component.py +0 -58
  509. solace_agent_mesh/tools/config/runtime_config.py +0 -26
  510. solace_agent_mesh-0.2.4.dist-info/METADATA +0 -176
  511. solace_agent_mesh-0.2.4.dist-info/RECORD +0 -193
  512. solace_agent_mesh-0.2.4.dist-info/entry_points.txt +0 -3
  513. /solace_agent_mesh/{agents → agent}/__init__.py +0 -0
  514. /solace_agent_mesh/{agents/global → agent/adk}/__init__.py +0 -0
  515. /solace_agent_mesh/{agents/global/actions → agent/protocol}/__init__.py +0 -0
  516. /solace_agent_mesh/{agents/image_processing → agent/sac}/__init__.py +0 -0
  517. /solace_agent_mesh/{agents/image_processing/actions → agent/utils}/__init__.py +0 -0
  518. /solace_agent_mesh/{agents/web_request → config_portal/backend/plugin_catalog}/__init__.py +0 -0
  519. /solace_agent_mesh/{agents/web_request/actions → evaluation}/__init__.py +0 -0
  520. /solace_agent_mesh/gateway/{components → http_sse}/__init__.py +0 -0
  521. {solace_agent_mesh-0.2.4.dist-info → solace_agent_mesh-1.0.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,3496 @@
1
+ """
2
+ Custom Solace AI Connector Component to Host Google ADK Agents via A2A Protocol.
3
+ """
4
+
5
+ from typing import Any, Dict, Optional, Union, Callable, List, Tuple, TYPE_CHECKING
6
+ import asyncio
7
+ import functools
8
+ import threading
9
+ import concurrent.futures
10
+ import fnmatch
11
+ import base64
12
+ from datetime import datetime, timezone
13
+ import json
14
+ from solace_ai_connector.components.component_base import ComponentBase
15
+ from solace_ai_connector.common.message import (
16
+ Message as SolaceMessage,
17
+ )
18
+ from solace_ai_connector.common.log import log
19
+ from solace_ai_connector.common.event import Event, EventType
20
+ from solace_ai_connector.common.utils import import_module
21
+ import inspect
22
+ from pydantic import BaseModel, ValidationError
23
+ from google.adk.agents.invocation_context import (
24
+ LlmCallsLimitExceededError,
25
+ )
26
+ from google.adk.agents import RunConfig
27
+ from google.adk.agents.run_config import StreamingMode
28
+ from google.adk.sessions import BaseSessionService
29
+ from google.adk.artifacts import BaseArtifactService
30
+ from google.adk.memory import BaseMemoryService
31
+ from google.adk.agents import LlmAgent
32
+ from google.adk.runners import Runner
33
+ from google.adk.models import LlmResponse
34
+ from google.adk.agents.readonly_context import ReadonlyContext
35
+ from google.adk.events import Event as ADKEvent
36
+ from google.adk.agents.callback_context import CallbackContext
37
+ from google.adk.models.llm_request import LlmRequest
38
+ from google.genai import types as adk_types
39
+ from google.adk.tools.mcp_tool import MCPToolset
40
+ from ...common.types import (
41
+ AgentCard,
42
+ Task,
43
+ TaskStatus,
44
+ TaskState,
45
+ Message as A2AMessage,
46
+ TextPart,
47
+ FilePart,
48
+ DataPart,
49
+ FileContent,
50
+ Artifact as A2AArtifact,
51
+ JSONRPCResponse,
52
+ InternalError,
53
+ TaskStatusUpdateEvent,
54
+ TaskArtifactUpdateEvent,
55
+ SendTaskRequest,
56
+ CancelTaskRequest,
57
+ TaskIdParams,
58
+ )
59
+ from ...common.a2a_protocol import (
60
+ get_a2a_base_topic,
61
+ get_discovery_topic,
62
+ get_agent_request_topic,
63
+ get_agent_response_topic,
64
+ get_client_response_topic,
65
+ get_peer_agent_status_topic,
66
+ format_and_route_adk_event,
67
+ get_gateway_status_topic,
68
+ )
69
+ from ...agent.utils.config_parser import resolve_instruction_provider
70
+ from ...agent.utils.artifact_helpers import get_latest_artifact_version
71
+ from ...agent.adk.services import (
72
+ initialize_session_service,
73
+ initialize_artifact_service,
74
+ initialize_memory_service,
75
+ )
76
+ from ...agent.adk.setup import (
77
+ load_adk_tools,
78
+ initialize_adk_agent,
79
+ initialize_adk_runner,
80
+ )
81
+ from ...agent.protocol.event_handlers import (
82
+ process_event,
83
+ publish_agent_card,
84
+ )
85
+ from ...agent.adk.runner import run_adk_async_task_thread_wrapper, TaskCancelledError
86
+ from ...agent.tools.peer_agent_tool import (
87
+ CORRELATION_DATA_PREFIX,
88
+ PeerAgentTool,
89
+ PEER_TOOL_PREFIX,
90
+ )
91
+ from ...agent.adk.invocation_monitor import InvocationMonitor
92
+ from ...common.middleware.registry import MiddlewareRegistry
93
+ from ...common.constants import DEFAULT_COMMUNICATION_TIMEOUT
94
+ from ...agent.tools.registry import tool_registry
95
+
96
+ if TYPE_CHECKING:
97
+ from .task_execution_context import TaskExecutionContext
98
+
99
+
100
+ info = {
101
+ "class_name": "SamAgentComponent",
102
+ "description": (
103
+ "Hosts a Google ADK agent and bridges communication via the A2A protocol over Solace. "
104
+ "NOTE: Configuration is defined in the app-level 'app_config' block "
105
+ "and validated by 'SamAgentApp.app_schema' when using the associated App class."
106
+ ),
107
+ "config_parameters": [],
108
+ "input_schema": {
109
+ "type": "object",
110
+ "description": "Not typically used; component reacts to events.",
111
+ "properties": {},
112
+ },
113
+ "output_schema": {
114
+ "type": "object",
115
+ "description": "Not typically used; component publishes results to Solace.",
116
+ "properties": {},
117
+ },
118
+ }
119
+ InstructionProvider = Callable[[ReadonlyContext], str]
120
+
121
+
122
+ class SamAgentComponent(ComponentBase):
123
+ """
124
+ A Solace AI Connector component that hosts a Google ADK agent,
125
+ communicating via the A2A protocol over Solace.
126
+ """
127
+
128
+ CORRELATION_DATA_PREFIX = CORRELATION_DATA_PREFIX
129
+ HOST_COMPONENT_VERSION = "1.0.0-alpha"
130
+
131
+ def __init__(self, **kwargs):
132
+ """
133
+ Initializes the A2A_ADK_HostComponent.
134
+ Args:
135
+ **kwargs: Configuration parameters passed from the SAC framework.
136
+ Expects configuration under app_config.
137
+ """
138
+ if "component_config" in kwargs and "app_config" in kwargs["component_config"]:
139
+ name = kwargs["component_config"]["app_config"].get("agent_name")
140
+ if name:
141
+ kwargs.setdefault("name", name)
142
+
143
+ super().__init__(info, **kwargs)
144
+ self.agent_name = self.get_config("agent_name")
145
+ log.info("%s Initializing A2A ADK Host Component...", self.log_identifier)
146
+ try:
147
+ self.namespace = self.get_config("namespace")
148
+ if not self.namespace:
149
+ raise ValueError("Internal Error: Namespace missing after validation.")
150
+ self.supports_streaming = self.get_config("supports_streaming", False)
151
+ self.stream_batching_threshold_bytes = self.get_config(
152
+ "stream_batching_threshold_bytes", 0
153
+ )
154
+ self.agent_name = self.get_config("agent_name")
155
+ if not self.agent_name:
156
+ raise ValueError("Internal Error: Agent name missing after validation.")
157
+ self.model_config = self.get_config("model")
158
+ if not self.model_config:
159
+ raise ValueError(
160
+ "Internal Error: Model config missing after validation."
161
+ )
162
+ self.instruction_config = self.get_config("instruction", "")
163
+ self.global_instruction_config = self.get_config("global_instruction", "")
164
+ self.tools_config = self.get_config("tools", [])
165
+ self.planner_config = self.get_config("planner")
166
+ self.code_executor_config = self.get_config("code_executor")
167
+ self.session_service_config = self.get_config("session_service")
168
+ if not self.session_service_config:
169
+ raise ValueError(
170
+ "Internal Error: Session service config missing after validation."
171
+ )
172
+ self.default_session_behavior = self.session_service_config.get(
173
+ "default_behavior", "PERSISTENT"
174
+ ).upper()
175
+ if self.default_session_behavior not in ["PERSISTENT", "RUN_BASED"]:
176
+ log.warning(
177
+ "%s Invalid 'default_behavior' in session_service_config: '%s'. Defaulting to PERSISTENT.",
178
+ self.log_identifier,
179
+ self.default_session_behavior,
180
+ )
181
+ self.default_session_behavior = "PERSISTENT"
182
+ log.info(
183
+ "%s Default session behavior set to: %s",
184
+ self.log_identifier,
185
+ self.default_session_behavior,
186
+ )
187
+ self.artifact_service_config = self.get_config(
188
+ "artifact_service", {"type": "memory"}
189
+ )
190
+ self.memory_service_config = self.get_config(
191
+ "memory_service", {"type": "memory"}
192
+ )
193
+ self.artifact_handling_mode = self.get_config(
194
+ "artifact_handling_mode", "ignore"
195
+ ).lower()
196
+ if self.artifact_handling_mode not in ["ignore", "embed", "reference"]:
197
+ log.warning(
198
+ "%s Invalid artifact_handling_mode '%s'. Defaulting to 'ignore'.",
199
+ self.log_identifier,
200
+ self.artifact_handling_mode,
201
+ )
202
+ self.artifact_handling_mode = "ignore"
203
+ log.info(
204
+ "%s Artifact Handling Mode: %s",
205
+ self.log_identifier,
206
+ self.artifact_handling_mode,
207
+ )
208
+ if self.artifact_handling_mode == "reference":
209
+ log.warning(
210
+ "%s Artifact handling mode 'reference' selected, but this component does not currently host an endpoint to serve artifacts. Clients may not be able to retrieve referenced artifacts.",
211
+ self.log_identifier,
212
+ )
213
+ self.agent_card_config = self.get_config("agent_card")
214
+ if not self.agent_card_config:
215
+ raise ValueError(
216
+ "Internal Error: Agent card config missing after validation."
217
+ )
218
+ self.agent_card_publishing_config = self.get_config("agent_card_publishing")
219
+ if not self.agent_card_publishing_config:
220
+ raise ValueError(
221
+ "Internal Error: Agent card publishing config missing after validation."
222
+ )
223
+ self.agent_discovery_config = self.get_config("agent_discovery")
224
+ if not self.agent_discovery_config:
225
+ raise ValueError(
226
+ "Internal Error: Agent discovery config missing after validation."
227
+ )
228
+ self.inter_agent_communication_config = self.get_config(
229
+ "inter_agent_communication"
230
+ )
231
+ if not self.inter_agent_communication_config:
232
+ raise ValueError(
233
+ "Internal Error: Inter-agent comms config missing after validation."
234
+ )
235
+ log.info("%s Configuration retrieved successfully.", self.log_identifier)
236
+ except Exception as e:
237
+ log.error(
238
+ "%s Failed to retrieve configuration via get_config: %s",
239
+ self.log_identifier,
240
+ e,
241
+ )
242
+ raise ValueError(f"Configuration retrieval error: {e}") from e
243
+ self.session_service: BaseSessionService = None
244
+ self.artifact_service: BaseArtifactService = None
245
+ self.memory_service: BaseMemoryService = None
246
+ self.adk_agent: LlmAgent = None
247
+ self.runner: Runner = None
248
+ self.agent_card_tool_manifest: List[Dict[str, Any]] = []
249
+ self.peer_agents: Dict[str, Any] = {}
250
+ self._card_publish_timer_id: str = f"publish_card_{self.agent_name}"
251
+ self._async_loop = None
252
+ self._async_thread = None
253
+ self._async_init_future = None
254
+ self.peer_response_queues: Dict[str, asyncio.Queue] = {}
255
+ self.peer_response_queue_lock = threading.Lock()
256
+ self.agent_specific_state: Dict[str, Any] = {}
257
+ self.active_tasks: Dict[str, "TaskExecutionContext"] = {}
258
+ self.active_tasks_lock = threading.Lock()
259
+ self._agent_system_instruction_string: Optional[str] = None
260
+ self._agent_system_instruction_callback: Optional[
261
+ Callable[[CallbackContext, LlmRequest], Optional[str]]
262
+ ] = None
263
+ self.invocation_monitor: Optional[InvocationMonitor] = None
264
+ self._active_background_tasks = set()
265
+ try:
266
+ self.agent_specific_state: Dict[str, Any] = {}
267
+ init_func_details = self.get_config("agent_init_function")
268
+ if init_func_details and isinstance(init_func_details, dict):
269
+ module_name = init_func_details.get("module")
270
+ func_name = init_func_details.get("name")
271
+ base_path = init_func_details.get("base_path")
272
+ specific_init_params_dict = init_func_details.get("config", {})
273
+ if module_name and func_name:
274
+ log.info(
275
+ "%s Attempting to load init_function: %s.%s",
276
+ self.log_identifier,
277
+ module_name,
278
+ func_name,
279
+ )
280
+ try:
281
+ module = import_module(module_name, base_path=base_path)
282
+ init_function = getattr(module, func_name)
283
+ if not callable(init_function):
284
+ raise TypeError(
285
+ f"Init function '{func_name}' in module '{module_name}' is not callable."
286
+ )
287
+ sig = inspect.signature(init_function)
288
+ pydantic_config_model = None
289
+ config_param_name = None
290
+ validated_config_arg = specific_init_params_dict
291
+ for param_name_sig, param_sig in sig.parameters.items():
292
+ if (
293
+ param_sig.annotation is not inspect.Parameter.empty
294
+ and isinstance(param_sig.annotation, type)
295
+ and issubclass(param_sig.annotation, BaseModel)
296
+ ):
297
+ pydantic_config_model = param_sig.annotation
298
+ config_param_name = param_name_sig
299
+ break
300
+ if pydantic_config_model and config_param_name:
301
+ log.info(
302
+ "%s Found Pydantic config model '%s' for init_function parameter '%s'.",
303
+ self.log_identifier,
304
+ pydantic_config_model.__name__,
305
+ config_param_name,
306
+ )
307
+ try:
308
+ validated_config_arg = pydantic_config_model(
309
+ **specific_init_params_dict
310
+ )
311
+ except ValidationError as ve:
312
+ log.error(
313
+ "%s Validation error for init_function config using Pydantic model '%s': %s",
314
+ self.log_identifier,
315
+ pydantic_config_model.__name__,
316
+ ve,
317
+ )
318
+ raise ValueError(
319
+ f"Invalid configuration for init_function '{func_name}': {ve}"
320
+ ) from ve
321
+ elif (
322
+ config_param_name
323
+ and param_sig.annotation is not inspect.Parameter.empty
324
+ ):
325
+ log.warning(
326
+ "%s Config parameter '%s' for init_function '%s' has a type hint '%s', but it's not a Pydantic BaseModel. Passing raw dict.",
327
+ self.log_identifier,
328
+ config_param_name,
329
+ func_name,
330
+ param_sig.annotation,
331
+ )
332
+ else:
333
+ log.info(
334
+ "%s No Pydantic model type hint found for a config parameter of init_function '%s'. Passing raw dict if a config param exists, or only host_component.",
335
+ self.log_identifier,
336
+ func_name,
337
+ )
338
+ func_params_list = list(sig.parameters.values())
339
+ num_actual_params = len(func_params_list)
340
+ if num_actual_params == 1:
341
+ if specific_init_params_dict:
342
+ log.warning(
343
+ "%s Init function '%s' takes 1 argument, but 'config' was provided in YAML. Config will be ignored.",
344
+ self.log_identifier,
345
+ func_name,
346
+ )
347
+ init_function(self)
348
+ elif num_actual_params == 2:
349
+ actual_config_param_name_in_signature = func_params_list[
350
+ 1
351
+ ].name
352
+ init_function(
353
+ self,
354
+ **{
355
+ actual_config_param_name_in_signature: validated_config_arg
356
+ },
357
+ )
358
+ else:
359
+ raise TypeError(
360
+ f"Init function '{func_name}' has an unsupported signature. "
361
+ f"Expected (host_component_instance) or (host_component_instance, config_param), "
362
+ f"but got {num_actual_params} parameters."
363
+ )
364
+ log.info(
365
+ "%s Successfully executed init_function: %s.%s",
366
+ self.log_identifier,
367
+ module_name,
368
+ func_name,
369
+ )
370
+ except Exception as e:
371
+ log.exception(
372
+ "%s Fatal error during agent initialization via init_function '%s.%s': %s",
373
+ self.log_identifier,
374
+ module_name,
375
+ func_name,
376
+ e,
377
+ )
378
+ raise RuntimeError(
379
+ f"Agent custom initialization failed: {e}"
380
+ ) from e
381
+ try:
382
+ self.invocation_monitor = InvocationMonitor()
383
+ except Exception as im_e:
384
+ log.error(
385
+ "%s Failed to initialize InvocationMonitor: %s",
386
+ self.log_identifier,
387
+ im_e,
388
+ )
389
+ self.invocation_monitor = None
390
+ try:
391
+ log.info(
392
+ "%s Initializing synchronous ADK services...", self.log_identifier
393
+ )
394
+ self.session_service = initialize_session_service(self)
395
+ self.artifact_service = initialize_artifact_service(self)
396
+ self.memory_service = initialize_memory_service(self)
397
+ log.info(
398
+ "%s Synchronous ADK services initialized.", self.log_identifier
399
+ )
400
+ except Exception as service_err:
401
+ log.exception(
402
+ "%s Failed to initialize synchronous ADK services: %s",
403
+ self.log_identifier,
404
+ service_err,
405
+ )
406
+ raise RuntimeError(
407
+ f"Failed to initialize synchronous ADK services: {service_err}"
408
+ ) from service_err
409
+ log.info(
410
+ "%s Starting dedicated async thread for MCP/ADK initialization...",
411
+ self.log_identifier,
412
+ )
413
+ self._async_loop = asyncio.new_event_loop()
414
+ self._async_init_future = concurrent.futures.Future()
415
+ self._async_thread = threading.Thread(
416
+ target=self._start_async_loop, daemon=True
417
+ )
418
+ self._async_thread.start()
419
+ init_coro_future = asyncio.run_coroutine_threadsafe(
420
+ self._perform_async_init(), self._async_loop
421
+ )
422
+ log.info(
423
+ "%s Waiting for async initialization to complete...",
424
+ self.log_identifier,
425
+ )
426
+ try:
427
+ init_coro_future.result(timeout=60)
428
+ self._async_init_future.result(timeout=1)
429
+ log.info(
430
+ "%s Async initialization completed successfully.",
431
+ self.log_identifier,
432
+ )
433
+ except Exception as init_err:
434
+ log.error(
435
+ "%s Async initialization failed during __init__: %s",
436
+ self.log_identifier,
437
+ init_err,
438
+ )
439
+ self.cleanup()
440
+ raise RuntimeError(
441
+ f"Failed to initialize component asynchronously: {init_err}"
442
+ ) from init_err
443
+ publish_interval_sec = self.agent_card_publishing_config.get(
444
+ "interval_seconds"
445
+ )
446
+ if publish_interval_sec and publish_interval_sec > 0:
447
+ log.info(
448
+ "%s Scheduling agent card publishing every %d seconds.",
449
+ self.log_identifier,
450
+ publish_interval_sec,
451
+ )
452
+ self.add_timer(
453
+ delay_ms=1000,
454
+ timer_id=self._card_publish_timer_id,
455
+ interval_ms=publish_interval_sec * 1000,
456
+ )
457
+ else:
458
+ log.warning(
459
+ "%s Agent card publishing interval not configured or invalid, card will not be published periodically.",
460
+ self.log_identifier,
461
+ )
462
+ log.info(
463
+ "%s Initialization complete for agent: %s",
464
+ self.log_identifier,
465
+ self.agent_name,
466
+ )
467
+ except Exception as e:
468
+ log.exception("%s Initialization failed: %s", self.log_identifier, e)
469
+ raise
470
+
471
+ def invoke(self, message: SolaceMessage, data: dict) -> dict:
472
+ """Placeholder invoke method. Primary logic resides in process_event."""
473
+ log.warning(
474
+ "%s 'invoke' method called, but primary logic resides in 'process_event'. This should not happen in normal operation.",
475
+ self.log_identifier,
476
+ )
477
+ return None
478
+
479
+ def process_event(self, event: Event):
480
+ """Processes incoming events (Messages, Timers, etc.)."""
481
+ try:
482
+ loop = self.get_async_loop()
483
+ is_loop_running = loop.is_running() if loop else False
484
+ if loop and is_loop_running:
485
+ coro = process_event(self, event)
486
+ future = asyncio.run_coroutine_threadsafe(coro, loop)
487
+ future.add_done_callback(
488
+ functools.partial(
489
+ self._handle_scheduled_task_completion,
490
+ event_type_for_log=event.event_type,
491
+ )
492
+ )
493
+ else:
494
+ log.error(
495
+ "%s Async loop not available or not running (loop is %s, is_running: %s). Cannot process event: %s",
496
+ self.log_identifier,
497
+ "present" if loop else "None",
498
+ is_loop_running,
499
+ event.event_type,
500
+ )
501
+ if event.event_type == EventType.MESSAGE:
502
+ try:
503
+ event.data.call_negative_acknowledgements()
504
+ log.warning(
505
+ "%s NACKed message due to unavailable async loop for event processing.",
506
+ self.log_identifier,
507
+ )
508
+ except Exception as nack_e:
509
+ log.error(
510
+ "%s Failed to NACK message after async loop issue: %s",
511
+ self.log_identifier,
512
+ nack_e,
513
+ )
514
+ except Exception as e:
515
+ log.error(
516
+ "%s Error processing event: %s. Exception: %s",
517
+ self.log_identifier,
518
+ event.event_type,
519
+ e,
520
+ )
521
+ if event.event_type == EventType.MESSAGE:
522
+ try:
523
+ event.data.call_negative_acknowledgements()
524
+ log.warning(
525
+ "%s NACKed message due to error in event processing.",
526
+ self.log_identifier,
527
+ )
528
+ except Exception as nack_e:
529
+ log.error(
530
+ "%s Failed to NACK message after error in event processing: %s",
531
+ self.log_identifier,
532
+ nack_e,
533
+ )
534
+
535
+ def handle_timer_event(self, timer_data: Dict[str, Any]):
536
+ """Handles timer events, specifically for agent card publishing."""
537
+ log.debug("%s Received timer event: %s", self.log_identifier, timer_data)
538
+ if timer_data.get("timer_id") == self._card_publish_timer_id:
539
+ publish_agent_card(self)
540
+
541
+ async def handle_cache_expiry_event(self, cache_data: Dict[str, Any]):
542
+ """
543
+ Handles cache expiry events for peer timeouts by calling the atomic claim helper.
544
+ """
545
+ log.debug("%s Received cache expiry event: %s", self.log_identifier, cache_data)
546
+ sub_task_id = cache_data.get("key")
547
+ logical_task_id = cache_data.get("expired_data")
548
+
549
+ if not (
550
+ sub_task_id
551
+ and sub_task_id.startswith(CORRELATION_DATA_PREFIX)
552
+ and logical_task_id
553
+ ):
554
+ log.debug(
555
+ "%s Cache expiry for key '%s' is not a peer sub-task timeout or is missing data.",
556
+ self.log_identifier,
557
+ sub_task_id,
558
+ )
559
+ return
560
+
561
+ correlation_data = await self._claim_peer_sub_task_completion(
562
+ sub_task_id=sub_task_id, logical_task_id_from_event=logical_task_id
563
+ )
564
+
565
+ if correlation_data:
566
+ log.warning(
567
+ "%s Detected timeout for sub-task %s (Main Task: %s). Claimed successfully.",
568
+ self.log_identifier,
569
+ sub_task_id,
570
+ logical_task_id,
571
+ )
572
+ await self._handle_peer_timeout(sub_task_id, correlation_data)
573
+ else:
574
+ log.info(
575
+ "%s Ignoring timeout event for sub-task %s as it was already completed.",
576
+ self.log_identifier,
577
+ sub_task_id,
578
+ )
579
+
580
+ async def _get_correlation_data_for_sub_task(
581
+ self, sub_task_id: str
582
+ ) -> Optional[Dict[str, Any]]:
583
+ """
584
+ Non-destructively retrieves correlation data for a sub-task.
585
+ Used for intermediate events where the sub-task should remain active.
586
+ """
587
+ logical_task_id = self.cache_service.get_data(sub_task_id)
588
+ if not logical_task_id:
589
+ log.warning(
590
+ "%s No cache entry for sub-task %s. Cannot get correlation data.",
591
+ self.log_identifier,
592
+ sub_task_id,
593
+ )
594
+ return None
595
+
596
+ with self.active_tasks_lock:
597
+ task_context = self.active_tasks.get(logical_task_id)
598
+
599
+ if not task_context:
600
+ log.error(
601
+ "%s TaskExecutionContext not found for task %s, but cache entry existed for sub-task %s. This may indicate a cleanup issue.",
602
+ self.log_identifier,
603
+ logical_task_id,
604
+ sub_task_id,
605
+ )
606
+ return None
607
+
608
+ with task_context.lock:
609
+ return task_context.active_peer_sub_tasks.get(sub_task_id)
610
+
611
+ async def _claim_peer_sub_task_completion(
612
+ self, sub_task_id: str, logical_task_id_from_event: Optional[str] = None
613
+ ) -> Optional[Dict[str, Any]]:
614
+ """
615
+ Atomically claims a sub-task as complete, preventing race conditions.
616
+ This is a destructive operation that removes state.
617
+
618
+ Args:
619
+ sub_task_id: The ID of the sub-task to claim.
620
+ logical_task_id_from_event: The parent task ID, if provided by the event (e.g., a timeout).
621
+ If not provided, it will be looked up from the cache.
622
+ """
623
+ log_id = f"{self.log_identifier}[ClaimSubTask:{sub_task_id}]"
624
+ logical_task_id = logical_task_id_from_event
625
+
626
+ if not logical_task_id:
627
+ logical_task_id = self.cache_service.get_data(sub_task_id)
628
+ if not logical_task_id:
629
+ log.warning(
630
+ "%s No cache entry found. Task has likely timed out and been cleaned up. Cannot claim.",
631
+ log_id,
632
+ )
633
+ return None
634
+
635
+ with self.active_tasks_lock:
636
+ task_context = self.active_tasks.get(logical_task_id)
637
+
638
+ if not task_context:
639
+ log.error(
640
+ "%s TaskExecutionContext not found for task %s. Cleaning up stale cache entry.",
641
+ log_id,
642
+ logical_task_id,
643
+ )
644
+ self.cache_service.remove_data(sub_task_id)
645
+ return None
646
+
647
+ correlation_data = task_context.claim_sub_task_completion(sub_task_id)
648
+
649
+ if correlation_data:
650
+ # If we successfully claimed the task, remove the timeout tracker from the cache.
651
+ self.cache_service.remove_data(sub_task_id)
652
+ log.info("%s Successfully claimed completion.", log_id)
653
+ return correlation_data
654
+ else:
655
+ # This means the task was already claimed by a competing event (e.g., timeout vs. response).
656
+ log.warning("%s Failed to claim; it was already completed.", log_id)
657
+ return None
658
+
659
+ async def _retrigger_agent_with_peer_responses(
660
+ self,
661
+ results_to_inject: list,
662
+ correlation_data: dict,
663
+ task_context: "TaskExecutionContext",
664
+ ):
665
+ """
666
+ Injects peer tool responses into the session history and re-triggers the ADK runner.
667
+ This function contains the logic to correctly merge parallel tool call responses.
668
+ """
669
+ original_task_context = correlation_data.get("original_task_context")
670
+ logical_task_id = correlation_data.get("logical_task_id")
671
+ paused_invocation_id = correlation_data.get("invocation_id")
672
+ log_retrigger = f"{self.log_identifier}[RetriggerManager:{logical_task_id}]"
673
+
674
+ try:
675
+ effective_session_id = original_task_context.get("effective_session_id")
676
+ user_id = original_task_context.get("user_id")
677
+ session = await self.session_service.get_session(
678
+ app_name=self.agent_name,
679
+ user_id=user_id,
680
+ session_id=effective_session_id,
681
+ )
682
+ if not session:
683
+ raise RuntimeError(
684
+ f"Could not find ADK session '{effective_session_id}'"
685
+ )
686
+
687
+ new_response_parts = []
688
+ for result in results_to_inject:
689
+ part = adk_types.Part.from_function_response(
690
+ name=result["peer_tool_name"],
691
+ response=result["payload"],
692
+ )
693
+ part.function_response.id = result["adk_function_call_id"]
694
+ new_response_parts.append(part)
695
+
696
+ # Always create a new event for the incoming peer responses.
697
+ # The ADK's `contents` processor is responsible for merging multiple
698
+ # tool responses into a single message before the next LLM call.
699
+ log.info(
700
+ "%s Creating a new tool response event for %d peer responses.",
701
+ log_retrigger,
702
+ len(new_response_parts),
703
+ )
704
+ new_adk_event = ADKEvent(
705
+ invocation_id=paused_invocation_id,
706
+ author=self.agent_name,
707
+ content=adk_types.Content(role="tool", parts=new_response_parts),
708
+ )
709
+ await self.session_service.append_event(
710
+ session=session, event=new_adk_event
711
+ )
712
+
713
+ # Always use SSE streaming mode for the ADK runner, even on re-trigger.
714
+ # This ensures that real-time callbacks for status updates and artifact
715
+ # creation can function correctly for all turns of a task.
716
+ streaming_mode = StreamingMode.SSE
717
+ max_llm_calls = self.get_config("max_llm_calls_per_task", 20)
718
+ run_config = RunConfig(
719
+ streaming_mode=streaming_mode, max_llm_calls=max_llm_calls
720
+ )
721
+
722
+ log.info(
723
+ "%s Re-triggering ADK runner for main task %s.",
724
+ log_retrigger,
725
+ logical_task_id,
726
+ )
727
+ try:
728
+ await run_adk_async_task_thread_wrapper(
729
+ self, session, None, run_config, original_task_context
730
+ )
731
+ finally:
732
+ log.info(
733
+ "%s Cleaning up parallel invocation state for invocation %s.",
734
+ log_retrigger,
735
+ paused_invocation_id,
736
+ )
737
+ task_context.clear_parallel_invocation_state(paused_invocation_id)
738
+
739
+ except Exception as e:
740
+ log.exception(
741
+ "%s Failed to re-trigger ADK runner for task %s: %s",
742
+ log_retrigger,
743
+ logical_task_id,
744
+ e,
745
+ )
746
+ if original_task_context:
747
+ loop = self.get_async_loop()
748
+ if loop and loop.is_running():
749
+ asyncio.run_coroutine_threadsafe(
750
+ self.finalize_task_error(e, original_task_context), loop
751
+ )
752
+ else:
753
+ log.error(
754
+ "%s Async loop not available. Cannot schedule error finalization for task %s.",
755
+ log_retrigger,
756
+ logical_task_id,
757
+ )
758
+
759
+ async def _handle_peer_timeout(
760
+ self,
761
+ sub_task_id: str,
762
+ correlation_data: Dict[str, Any],
763
+ ):
764
+ """
765
+ Handles the timeout of a peer agent task. It sends a cancellation request
766
+ to the peer, updates the local completion counter, and potentially
767
+ re-triggers the runner if all parallel tasks are now complete.
768
+ """
769
+ logical_task_id = correlation_data.get("logical_task_id")
770
+ invocation_id = correlation_data.get("invocation_id")
771
+ log_retrigger = f"{self.log_identifier}[RetriggerManager:{logical_task_id}]"
772
+
773
+ log.warning(
774
+ "%s Peer request timed out for sub-task: %s (Invocation: %s)",
775
+ log_retrigger,
776
+ sub_task_id,
777
+ invocation_id,
778
+ )
779
+
780
+ # Proactively send a cancellation request to the peer agent.
781
+ peer_agent_name = correlation_data.get("peer_agent_name")
782
+ if peer_agent_name:
783
+ try:
784
+ log.info(
785
+ "%s Sending CancelTaskRequest to peer '%s' for timed-out sub-task %s.",
786
+ log_retrigger,
787
+ peer_agent_name,
788
+ sub_task_id,
789
+ )
790
+ task_id_for_peer = sub_task_id.replace(CORRELATION_DATA_PREFIX, "", 1)
791
+ cancel_params = TaskIdParams(id=task_id_for_peer)
792
+ cancel_request = CancelTaskRequest(params=cancel_params)
793
+ user_props = {"clientId": self.agent_name}
794
+ peer_topic = self._get_agent_request_topic(peer_agent_name)
795
+ self._publish_a2a_message(
796
+ payload=cancel_request.model_dump(exclude_none=True),
797
+ topic=peer_topic,
798
+ user_properties=user_props,
799
+ )
800
+ except Exception as e:
801
+ log.error(
802
+ "%s Failed to send CancelTaskRequest to peer '%s' for sub-task %s: %s",
803
+ log_retrigger,
804
+ peer_agent_name,
805
+ sub_task_id,
806
+ e,
807
+ )
808
+
809
+ # Process the timeout locally.
810
+ with self.active_tasks_lock:
811
+ task_context = self.active_tasks.get(logical_task_id)
812
+
813
+ if not task_context:
814
+ log.warning(
815
+ "%s TaskExecutionContext not found for task %s. Ignoring timeout event.",
816
+ log_retrigger,
817
+ logical_task_id,
818
+ )
819
+ return
820
+
821
+ timeout_value = self.inter_agent_communication_config.get(
822
+ "request_timeout_seconds", DEFAULT_COMMUNICATION_TIMEOUT
823
+ )
824
+ all_sub_tasks_completed = task_context.handle_peer_timeout(
825
+ sub_task_id, correlation_data, timeout_value, invocation_id
826
+ )
827
+
828
+ if not all_sub_tasks_completed:
829
+ log.info(
830
+ "%s Waiting for more peer responses for invocation %s after timeout of sub-task %s.",
831
+ log_retrigger,
832
+ invocation_id,
833
+ sub_task_id,
834
+ )
835
+ return
836
+
837
+ log.info(
838
+ "%s All peer responses/timeouts received for invocation %s. Retriggering agent.",
839
+ log_retrigger,
840
+ invocation_id,
841
+ )
842
+ results_to_inject = task_context.parallel_tool_calls[invocation_id].get(
843
+ "results", []
844
+ )
845
+
846
+ await self._retrigger_agent_with_peer_responses(
847
+ results_to_inject, correlation_data, task_context
848
+ )
849
+
850
+ def _inject_peer_tools_callback(
851
+ self, callback_context: CallbackContext, llm_request: LlmRequest
852
+ ) -> Optional[LlmResponse]:
853
+ """
854
+ ADK before_model_callback to dynamically add PeerAgentTools to the LLM request
855
+ and generate the corresponding instruction text for the LLM.
856
+ """
857
+ log.debug("%s Running _inject_peer_tools_callback...", self.log_identifier)
858
+ if not self.peer_agents:
859
+ log.debug("%s No peer agents currently discovered.", self.log_identifier)
860
+ return None
861
+
862
+ a2a_context = callback_context.state.get("a2a_context", {})
863
+ user_config = (
864
+ a2a_context.get("a2a_user_config", {})
865
+ if isinstance(a2a_context, dict)
866
+ else {}
867
+ )
868
+
869
+ inter_agent_config = self.get_config("inter_agent_communication", {})
870
+ allow_list = inter_agent_config.get("allow_list", ["*"])
871
+ deny_list = set(self.get_config("deny_list", []))
872
+ self_name = self.get_config("agent_name")
873
+
874
+ peer_tools_to_add = []
875
+ allowed_peer_descriptions = []
876
+
877
+ for peer_name, agent_card in self.peer_agents.items():
878
+ if not isinstance(agent_card, AgentCard) or peer_name == self_name:
879
+ continue
880
+
881
+ is_allowed = any(
882
+ fnmatch.fnmatch(peer_name, p) for p in allow_list
883
+ ) and not any(fnmatch.fnmatch(peer_name, p) for p in deny_list)
884
+
885
+ if is_allowed:
886
+ config_resolver = MiddlewareRegistry.get_config_resolver()
887
+ operation_spec = {
888
+ "operation_type": "peer_delegation",
889
+ "target_agent": peer_name,
890
+ "delegation_context": "peer_discovery",
891
+ }
892
+ validation_context = {
893
+ "discovery_phase": "peer_enumeration",
894
+ "agent_context": {"component_type": "peer_discovery"},
895
+ }
896
+ validation_result = config_resolver.validate_operation_config(
897
+ user_config, operation_spec, validation_context
898
+ )
899
+ if not validation_result.get("valid", True):
900
+ log.debug(
901
+ "%s Peer agent '%s' filtered out by user configuration.",
902
+ self.log_identifier,
903
+ peer_name,
904
+ )
905
+ is_allowed = False
906
+
907
+ if not is_allowed:
908
+ continue
909
+
910
+ try:
911
+ peer_tool_instance = PeerAgentTool(
912
+ target_agent_name=peer_name, host_component=self
913
+ )
914
+ if peer_tool_instance.name not in llm_request.tools_dict:
915
+ peer_tools_to_add.append(peer_tool_instance)
916
+ description = (
917
+ getattr(agent_card, "description", "No description")
918
+ or "No description"
919
+ )
920
+ allowed_peer_descriptions.append(
921
+ f"- `peer_{peer_name}`: {description}"
922
+ )
923
+ except Exception as e:
924
+ log.error(
925
+ "%s Failed to create PeerAgentTool for '%s': %s",
926
+ self.log_identifier,
927
+ peer_name,
928
+ e,
929
+ )
930
+
931
+ if allowed_peer_descriptions:
932
+ peer_list_str = "\n".join(allowed_peer_descriptions)
933
+ instruction_text = (
934
+ "You can delegate tasks to other specialized agents if they are better suited.\n"
935
+ "Use the `peer_<agent_name>(task_description: str, user_query: str)` tool for delegation. "
936
+ "Replace `<agent_name>` with the actual name of the target agent.\n"
937
+ "Provide a clear `task_description` for the peer and include the original `user_query` for context.\n"
938
+ "Be aware that the peer agent may not have access to your session history, so you must provide all required context necessary to fulfill the request.\n\n"
939
+ "Available peer agents you can delegate to (use the `peer_...` tool name):\n"
940
+ f"{peer_list_str}"
941
+ )
942
+ callback_context.state["peer_tool_instructions"] = instruction_text
943
+ log.debug(
944
+ "%s Stored peer tool instructions in callback_context.state.",
945
+ self.log_identifier,
946
+ )
947
+
948
+ if peer_tools_to_add:
949
+ try:
950
+ if llm_request.config.tools is None:
951
+ llm_request.config.tools = []
952
+ if len(llm_request.config.tools) > 0:
953
+ for tool in peer_tools_to_add:
954
+ llm_request.tools_dict[tool.name] = tool
955
+ llm_request.config.tools[0].function_declarations.append(
956
+ tool._get_declaration()
957
+ )
958
+ else:
959
+ llm_request.append_tools(peer_tools_to_add)
960
+ log.debug(
961
+ "%s Dynamically added %d PeerAgentTool(s) to LLM request.",
962
+ self.log_identifier,
963
+ len(peer_tools_to_add),
964
+ )
965
+ except Exception as e:
966
+ log.error(
967
+ "%s Failed to append dynamic peer tools to LLM request: %s",
968
+ self.log_identifier,
969
+ e,
970
+ )
971
+ return None
972
+
973
+ def _filter_tools_by_capability_callback(
974
+ self, callback_context: CallbackContext, llm_request: LlmRequest
975
+ ) -> Optional[LlmResponse]:
976
+ """
977
+ ADK before_model_callback to filter tools in the LlmRequest based on user configuration.
978
+ This callback modifies `llm_request.config.tools` in place by potentially
979
+ removing individual FunctionDeclarations from genai.Tool objects or removing
980
+ entire genai.Tool objects if all their declarations are filtered out.
981
+ """
982
+ log_id_prefix = f"{self.log_identifier}[ToolCapabilityFilter]"
983
+ log.debug("%s Running _filter_tools_by_capability_callback...", log_id_prefix)
984
+
985
+ a2a_context = callback_context.state.get("a2a_context", {})
986
+ if not isinstance(a2a_context, dict):
987
+ log.warning(
988
+ "%s 'a2a_context' in session state is not a dictionary. Using empty configuration.",
989
+ log_id_prefix,
990
+ )
991
+ a2a_context = {}
992
+ user_config = a2a_context.get("a2a_user_config", {})
993
+ if not isinstance(user_config, dict):
994
+ log.warning(
995
+ "%s 'a2a_user_config' in a2a_context is not a dictionary. Using empty configuration.",
996
+ log_id_prefix,
997
+ )
998
+ user_config = {}
999
+
1000
+ log.debug(
1001
+ "%s User configuration for filtering: %s",
1002
+ log_id_prefix,
1003
+ {k: v for k, v in user_config.items() if not k.startswith("_")},
1004
+ )
1005
+
1006
+ config_resolver = MiddlewareRegistry.get_config_resolver()
1007
+
1008
+ if not llm_request.config or not llm_request.config.tools:
1009
+ log.debug("%s No tools in request to filter.", log_id_prefix)
1010
+ return None
1011
+
1012
+ explicit_tools_config = self.get_config("tools", [])
1013
+ final_filtered_genai_tools: List[adk_types.Tool] = []
1014
+ original_genai_tools_count = len(llm_request.config.tools)
1015
+ original_function_declarations_count = 0
1016
+
1017
+ for original_tool in llm_request.config.tools:
1018
+ if not original_tool.function_declarations:
1019
+ log.warning(
1020
+ "%s genai.Tool object has no function declarations. Keeping it.",
1021
+ log_id_prefix,
1022
+ )
1023
+ final_filtered_genai_tools.append(original_tool)
1024
+ continue
1025
+
1026
+ original_function_declarations_count += len(
1027
+ original_tool.function_declarations
1028
+ )
1029
+ permitted_declarations_for_this_tool: List[
1030
+ adk_types.FunctionDeclaration
1031
+ ] = []
1032
+
1033
+ for func_decl in original_tool.function_declarations:
1034
+ func_decl_name = func_decl.name
1035
+ tool_source_for_log = "unknown"
1036
+ tool_matched_for_capability_lookup = False
1037
+
1038
+ feature_descriptor = {
1039
+ "feature_type": "tool_function",
1040
+ "function_name": func_decl_name,
1041
+ "tool_source": self._determine_tool_source(func_decl_name),
1042
+ "tool_metadata": self._get_tool_metadata(func_decl_name),
1043
+ }
1044
+
1045
+ if func_decl_name.startswith(PEER_TOOL_PREFIX):
1046
+ peer_name = func_decl_name.replace(PEER_TOOL_PREFIX, "", 1)
1047
+ feature_descriptor["tool_metadata"]["peer_agent_name"] = peer_name
1048
+ tool_source_for_log = f"PeerAgentTool ({peer_name})"
1049
+ tool_matched_for_capability_lookup = True
1050
+
1051
+ if not tool_matched_for_capability_lookup:
1052
+ tool_def = tool_registry.get_tool_by_name(func_decl_name)
1053
+ if tool_def:
1054
+ feature_descriptor["tool_metadata"][
1055
+ "tool_category"
1056
+ ] = tool_def.category
1057
+ feature_descriptor["tool_metadata"]["builtin_tool"] = True
1058
+ tool_source_for_log = (
1059
+ f"Built-in Tool ({tool_def.category}/{func_decl_name})"
1060
+ )
1061
+ tool_matched_for_capability_lookup = True
1062
+
1063
+ if not tool_matched_for_capability_lookup:
1064
+ for tool_cfg in explicit_tools_config:
1065
+ cfg_tool_type = tool_cfg.get("tool_type")
1066
+ cfg_tool_name = tool_cfg.get("tool_name")
1067
+ cfg_func_name = tool_cfg.get("function_name")
1068
+ if (
1069
+ cfg_tool_type == "python"
1070
+ and cfg_func_name == func_decl_name
1071
+ ) or (
1072
+ cfg_tool_type in ["builtin", "mcp"]
1073
+ and cfg_tool_name == func_decl_name
1074
+ ):
1075
+ feature_descriptor["tool_metadata"][
1076
+ "tool_type"
1077
+ ] = cfg_tool_type
1078
+ feature_descriptor["tool_metadata"][
1079
+ "tool_config"
1080
+ ] = tool_cfg
1081
+ tool_source_for_log = f"Explicitly configured tool ({cfg_tool_type}: {cfg_tool_name or cfg_func_name})"
1082
+ tool_matched_for_capability_lookup = True
1083
+ break
1084
+
1085
+ if not tool_matched_for_capability_lookup:
1086
+ log.debug(
1087
+ "%s FunctionDeclaration '%s' not found in any known configuration for capability checking. Assuming feature is available.",
1088
+ log_id_prefix,
1089
+ func_decl_name,
1090
+ )
1091
+ tool_source_for_log = "Unmatched/Implicit FunctionDeclaration"
1092
+
1093
+ context = {
1094
+ "agent_context": self.get_agent_context(),
1095
+ "filter_phase": "pre_llm",
1096
+ "tool_configurations": {
1097
+ "explicit_tools": explicit_tools_config,
1098
+ },
1099
+ }
1100
+
1101
+ if config_resolver.is_feature_enabled(
1102
+ user_config, feature_descriptor, context
1103
+ ):
1104
+ permitted_declarations_for_this_tool.append(func_decl)
1105
+ log.debug(
1106
+ "%s FunctionDeclaration '%s' (Source: %s) permitted.",
1107
+ log_id_prefix,
1108
+ func_decl_name,
1109
+ tool_source_for_log,
1110
+ )
1111
+ else:
1112
+ log.info(
1113
+ "%s FunctionDeclaration '%s' (Source: %s) FILTERED OUT due to configuration restrictions.",
1114
+ log_id_prefix,
1115
+ func_decl_name,
1116
+ tool_source_for_log,
1117
+ )
1118
+
1119
+ if permitted_declarations_for_this_tool:
1120
+ scoped_tool = original_tool.model_copy(deep=True)
1121
+ scoped_tool.function_declarations = permitted_declarations_for_this_tool
1122
+
1123
+ final_filtered_genai_tools.append(scoped_tool)
1124
+ log.debug(
1125
+ "%s Keeping genai.Tool (original name/type preserved, declarations filtered) as it has %d permitted FunctionDeclaration(s).",
1126
+ log_id_prefix,
1127
+ len(permitted_declarations_for_this_tool),
1128
+ )
1129
+ else:
1130
+ log.info(
1131
+ "%s Entire genai.Tool (original declarations: %s) FILTERED OUT as all its FunctionDeclarations were denied by configuration.",
1132
+ log_id_prefix,
1133
+ [fd.name for fd in original_tool.function_declarations],
1134
+ )
1135
+
1136
+ final_function_declarations_count = sum(
1137
+ len(t.function_declarations)
1138
+ for t in final_filtered_genai_tools
1139
+ if t.function_declarations
1140
+ )
1141
+
1142
+ if final_function_declarations_count != original_function_declarations_count:
1143
+ log.info(
1144
+ "%s Tool list modified by capability filter. Original genai.Tools: %d (Total Declarations: %d). Filtered genai.Tools: %d (Total Declarations: %d).",
1145
+ log_id_prefix,
1146
+ original_genai_tools_count,
1147
+ original_function_declarations_count,
1148
+ len(final_filtered_genai_tools),
1149
+ final_function_declarations_count,
1150
+ )
1151
+ llm_request.config.tools = (
1152
+ final_filtered_genai_tools if final_filtered_genai_tools else None
1153
+ )
1154
+ else:
1155
+ log.debug(
1156
+ "%s Tool list and FunctionDeclarations unchanged after capability filtering.",
1157
+ log_id_prefix,
1158
+ )
1159
+
1160
+ return None
1161
+
1162
+ def _determine_tool_source(self, function_name: str) -> str:
1163
+ """Determine the source/type of a tool function."""
1164
+ if function_name.startswith("peer_"):
1165
+ return "peer_agent"
1166
+
1167
+ tool_def = tool_registry.get_tool_by_name(function_name)
1168
+ if tool_def:
1169
+ category_map = {
1170
+ "artifact_management": "builtin_artifact",
1171
+ "data_analysis": "builtin_data",
1172
+ }
1173
+ return category_map.get(tool_def.category, "builtin_other")
1174
+
1175
+ return "explicit_tool"
1176
+
1177
+ def _get_tool_metadata(self, function_name: str) -> Dict[str, Any]:
1178
+ """Get metadata for a tool function."""
1179
+ metadata = {"function_name": function_name}
1180
+
1181
+ if function_name.startswith("peer_"):
1182
+ peer_name = function_name.replace("peer_", "", 1)
1183
+ metadata.update(
1184
+ {"peer_agent_name": peer_name, "operation_type": "delegation"}
1185
+ )
1186
+ return metadata
1187
+
1188
+ tool_def = tool_registry.get_tool_by_name(function_name)
1189
+ if tool_def:
1190
+ metadata.update(
1191
+ {
1192
+ "tool_category": tool_def.category,
1193
+ "required_scopes": tool_def.required_scopes,
1194
+ "builtin_tool": True,
1195
+ }
1196
+ )
1197
+ return metadata
1198
+
1199
+ explicit_tools_config = self.get_config("tools", [])
1200
+ for tool_cfg in explicit_tools_config:
1201
+ cfg_tool_name = tool_cfg.get("tool_name")
1202
+ cfg_func_name = tool_cfg.get("function_name")
1203
+ if (
1204
+ tool_cfg.get("tool_type") == "python" and cfg_func_name == function_name
1205
+ ) or (
1206
+ tool_cfg.get("tool_type") in ["builtin", "mcp"]
1207
+ and cfg_tool_name == function_name
1208
+ ):
1209
+ metadata.update(
1210
+ {"tool_type": tool_cfg.get("tool_type"), "tool_config": tool_cfg}
1211
+ )
1212
+ break
1213
+
1214
+ return metadata
1215
+
1216
+ def get_agent_context(self) -> Dict[str, Any]:
1217
+ """Get agent context for middleware calls."""
1218
+ return {
1219
+ "agent_name": getattr(self, "agent_name", "unknown"),
1220
+ "component_type": "sac_agent",
1221
+ }
1222
+
1223
+ def _inject_gateway_instructions_callback(
1224
+ self, callback_context: CallbackContext, llm_request: LlmRequest
1225
+ ) -> Optional[LlmResponse]:
1226
+ """
1227
+ ADK before_model_callback to dynamically prepend gateway-defined system_purpose
1228
+ and response_format to the agent's llm_request.config.system_instruction.
1229
+ """
1230
+ log_id_prefix = f"{self.log_identifier}[GatewayInstrInject]"
1231
+ log.debug(
1232
+ "%s Running _inject_gateway_instructions_callback to modify system_instruction...",
1233
+ log_id_prefix,
1234
+ )
1235
+
1236
+ a2a_context = callback_context.state.get("a2a_context", {})
1237
+ if not isinstance(a2a_context, dict):
1238
+ log.warning(
1239
+ "%s 'a2a_context' in session state is not a dictionary. Skipping instruction injection.",
1240
+ log_id_prefix,
1241
+ )
1242
+ return None
1243
+
1244
+ system_purpose = a2a_context.get("system_purpose")
1245
+ response_format = a2a_context.get("response_format")
1246
+ user_profile = a2a_context.get("a2a_user_config", {}).get("user_profile")
1247
+
1248
+ inject_purpose = self.get_config("inject_system_purpose", False)
1249
+ inject_format = self.get_config("inject_response_format", False)
1250
+ inject_user_profile = self.get_config("inject_user_profile", False)
1251
+
1252
+ gateway_instructions_to_add = []
1253
+
1254
+ if (
1255
+ inject_purpose
1256
+ and system_purpose
1257
+ and isinstance(system_purpose, str)
1258
+ and system_purpose.strip()
1259
+ ):
1260
+ gateway_instructions_to_add.append(
1261
+ f"System Purpose:\n{system_purpose.strip()}"
1262
+ )
1263
+ log.debug(
1264
+ "%s Prepared system_purpose for system_instruction.", log_id_prefix
1265
+ )
1266
+
1267
+ if (
1268
+ inject_format
1269
+ and response_format
1270
+ and isinstance(response_format, str)
1271
+ and response_format.strip()
1272
+ ):
1273
+ gateway_instructions_to_add.append(
1274
+ f"Desired Response Format:\n{response_format.strip()}"
1275
+ )
1276
+ log.debug(
1277
+ "%s Prepared response_format for system_instruction.", log_id_prefix
1278
+ )
1279
+
1280
+ if (
1281
+ inject_user_profile
1282
+ and user_profile
1283
+ and (isinstance(user_profile, str) or isinstance(user_profile, dict))
1284
+ ):
1285
+ if isinstance(user_profile, dict):
1286
+ user_profile = json.dumps(user_profile, indent=2, default=str)
1287
+ gateway_instructions_to_add.append(
1288
+ f"Inquiring User Profile:\n{user_profile.strip()}\n"
1289
+ )
1290
+ log.debug("%s Prepared user_profile for system_instruction.", log_id_prefix)
1291
+
1292
+ if not gateway_instructions_to_add:
1293
+ log.debug(
1294
+ "%s No gateway instructions to inject into system_instruction.",
1295
+ log_id_prefix,
1296
+ )
1297
+ return None
1298
+
1299
+ if llm_request.config is None:
1300
+ log.warning(
1301
+ "%s llm_request.config is None, cannot append gateway instructions to system_instruction.",
1302
+ log_id_prefix,
1303
+ )
1304
+ return None
1305
+
1306
+ if llm_request.config.system_instruction is None:
1307
+ llm_request.config.system_instruction = ""
1308
+
1309
+ combined_new_instructions = "\n\n".join(gateway_instructions_to_add)
1310
+
1311
+ if llm_request.config.system_instruction:
1312
+ llm_request.config.system_instruction += (
1313
+ f"\n\n---\n\n{combined_new_instructions}"
1314
+ )
1315
+ else:
1316
+ llm_request.config.system_instruction = combined_new_instructions
1317
+
1318
+ log.info(
1319
+ "%s Injected %d gateway instruction block(s) into llm_request.config.system_instruction.",
1320
+ log_id_prefix,
1321
+ len(gateway_instructions_to_add),
1322
+ )
1323
+
1324
+ return None
1325
+
1326
+ async def _publish_text_as_partial_a2a_status_update(
1327
+ self,
1328
+ text_content: str,
1329
+ a2a_context: Dict,
1330
+ is_stream_terminating_content: bool = False,
1331
+ ):
1332
+ """
1333
+ Constructs and publishes a TaskStatusUpdateEvent for the given text.
1334
+ The 'final' flag is determined by is_stream_terminating_content.
1335
+ This method skips buffer flushing since it's used for LLM streaming text.
1336
+ """
1337
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
1338
+ log_identifier_helper = (
1339
+ f"{self.log_identifier}[PublishPartialText:{logical_task_id}]"
1340
+ )
1341
+
1342
+ if not text_content:
1343
+ log.debug(
1344
+ "%s No text content to publish as update (final=%s).",
1345
+ log_identifier_helper,
1346
+ is_stream_terminating_content,
1347
+ )
1348
+ return
1349
+
1350
+ try:
1351
+ a2a_message = A2AMessage(role="agent", parts=[TextPart(text=text_content)])
1352
+ task_status = TaskStatus(
1353
+ state=TaskState.WORKING,
1354
+ message=a2a_message,
1355
+ timestamp=datetime.now(timezone.utc),
1356
+ )
1357
+ event_metadata = {"agent_name": self.agent_name}
1358
+ status_update_event = TaskStatusUpdateEvent(
1359
+ id=logical_task_id,
1360
+ status=task_status,
1361
+ final=is_stream_terminating_content,
1362
+ metadata=event_metadata,
1363
+ )
1364
+
1365
+ await self._publish_status_update_with_buffer_flush(
1366
+ status_update_event,
1367
+ a2a_context,
1368
+ skip_buffer_flush=True,
1369
+ )
1370
+
1371
+ log.debug(
1372
+ "%s Published LLM streaming text (length: %d bytes, final: %s).",
1373
+ log_identifier_helper,
1374
+ len(text_content.encode("utf-8")),
1375
+ is_stream_terminating_content,
1376
+ )
1377
+
1378
+ except Exception as e:
1379
+ log.exception(
1380
+ "%s Error in _publish_text_as_partial_a2a_status_update: %s",
1381
+ log_identifier_helper,
1382
+ e,
1383
+ )
1384
+
1385
+ async def _publish_agent_status_signal_update(
1386
+ self, status_text: str, a2a_context: Dict
1387
+ ):
1388
+ """
1389
+ Constructs and publishes a TaskStatusUpdateEvent specifically for agent_status_message signals.
1390
+ This method will flush the buffer before publishing to maintain proper message ordering.
1391
+ """
1392
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
1393
+ log_identifier_helper = (
1394
+ f"{self.log_identifier}[PublishAgentSignal:{logical_task_id}]"
1395
+ )
1396
+
1397
+ if not status_text:
1398
+ log.debug(
1399
+ "%s No text content for agent status signal.", log_identifier_helper
1400
+ )
1401
+ return
1402
+
1403
+ try:
1404
+ signal_data_part = DataPart(
1405
+ data={
1406
+ "a2a_signal_type": "agent_status_message",
1407
+ "text": status_text,
1408
+ },
1409
+ metadata={"source_embed_type": "status_update"},
1410
+ )
1411
+ a2a_message = A2AMessage(role="agent", parts=[signal_data_part])
1412
+ task_status = TaskStatus(
1413
+ state=TaskState.WORKING,
1414
+ message=a2a_message,
1415
+ timestamp=datetime.now(timezone.utc),
1416
+ )
1417
+ event_metadata = {"agent_name": self.agent_name}
1418
+ status_update_event = TaskStatusUpdateEvent(
1419
+ id=logical_task_id,
1420
+ status=task_status,
1421
+ final=False,
1422
+ metadata=event_metadata,
1423
+ )
1424
+
1425
+ await self._publish_status_update_with_buffer_flush(
1426
+ status_update_event,
1427
+ a2a_context,
1428
+ skip_buffer_flush=False,
1429
+ )
1430
+
1431
+ log.debug(
1432
+ "%s Published agent_status_message signal ('%s').",
1433
+ log_identifier_helper,
1434
+ status_text,
1435
+ )
1436
+
1437
+ except Exception as e:
1438
+ log.exception(
1439
+ "%s Error in _publish_agent_status_signal_update: %s",
1440
+ log_identifier_helper,
1441
+ e,
1442
+ )
1443
+
1444
+ async def _flush_buffer_if_needed(
1445
+ self, a2a_context: Dict, reason: str = "status_update"
1446
+ ) -> bool:
1447
+ """
1448
+ Flushes streaming buffer if it contains content.
1449
+
1450
+ Args:
1451
+ a2a_context: The A2A context dictionary for the current task
1452
+ reason: The reason for flushing (for logging purposes)
1453
+
1454
+ Returns:
1455
+ bool: True if buffer was flushed, False if no content to flush
1456
+ """
1457
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
1458
+ log_identifier = f"{self.log_identifier}[BufferFlush:{logical_task_id}]"
1459
+
1460
+ with self.active_tasks_lock:
1461
+ task_context = self.active_tasks.get(logical_task_id)
1462
+
1463
+ if not task_context:
1464
+ log.warning(
1465
+ "%s TaskExecutionContext not found for task %s. Cannot flush buffer.",
1466
+ log_identifier,
1467
+ logical_task_id,
1468
+ )
1469
+ return False
1470
+
1471
+ buffer_content = task_context.get_streaming_buffer_content()
1472
+ if not buffer_content:
1473
+ log.debug(
1474
+ "%s No buffer content to flush (reason: %s).",
1475
+ log_identifier,
1476
+ reason,
1477
+ )
1478
+ return False
1479
+
1480
+ buffer_size = len(buffer_content.encode("utf-8"))
1481
+ log.info(
1482
+ "%s Flushing buffer content (size: %d bytes, reason: %s).",
1483
+ log_identifier,
1484
+ buffer_size,
1485
+ reason,
1486
+ )
1487
+
1488
+ try:
1489
+ resolved_text, unprocessed_tail = await self._flush_and_resolve_buffer(
1490
+ a2a_context, is_final=False
1491
+ )
1492
+
1493
+ if resolved_text:
1494
+ await self._publish_text_as_partial_a2a_status_update(
1495
+ resolved_text,
1496
+ a2a_context,
1497
+ is_stream_terminating_content=False,
1498
+ )
1499
+ log.debug(
1500
+ "%s Successfully flushed and published buffer content (resolved: %d bytes).",
1501
+ log_identifier,
1502
+ len(resolved_text.encode("utf-8")),
1503
+ )
1504
+ return True
1505
+ else:
1506
+ log.debug(
1507
+ "%s Buffer flush completed but no resolved text to publish.",
1508
+ log_identifier,
1509
+ )
1510
+ return False
1511
+
1512
+ except Exception as e:
1513
+ log.exception(
1514
+ "%s Error during buffer flush (reason: %s): %s",
1515
+ log_identifier,
1516
+ reason,
1517
+ e,
1518
+ )
1519
+ return False
1520
+
1521
+ async def _publish_status_update_with_buffer_flush(
1522
+ self,
1523
+ status_update_event: TaskStatusUpdateEvent,
1524
+ a2a_context: Dict,
1525
+ skip_buffer_flush: bool = False,
1526
+ ) -> None:
1527
+ """
1528
+ Central method for publishing status updates with automatic buffer flushing.
1529
+
1530
+ Args:
1531
+ status_update_event: The status update event to publish
1532
+ a2a_context: The A2A context dictionary for the current task
1533
+ skip_buffer_flush: If True, skip buffer flushing (used for LLM streaming text)
1534
+ """
1535
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
1536
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
1537
+ log_identifier = f"{self.log_identifier}[StatusUpdate:{logical_task_id}]"
1538
+
1539
+ status_type = "unknown"
1540
+ if status_update_event.metadata:
1541
+ if status_update_event.metadata.get("type") == "tool_invocation_start":
1542
+ status_type = "tool_invocation_start"
1543
+ elif "agent_name" in status_update_event.metadata:
1544
+ status_type = "agent_status"
1545
+
1546
+ if (
1547
+ status_update_event.status
1548
+ and status_update_event.status.message
1549
+ and status_update_event.status.message.parts
1550
+ ):
1551
+ for part in status_update_event.status.message.parts:
1552
+ if hasattr(part, "data") and part.data:
1553
+ if part.data.get("a2a_signal_type") == "agent_status_message":
1554
+ status_type = "agent_status_signal"
1555
+ break
1556
+ elif "tool_error" in part.data:
1557
+ status_type = "tool_failure"
1558
+ break
1559
+
1560
+ log.debug(
1561
+ "%s Publishing status update (type: %s, skip_buffer_flush: %s).",
1562
+ log_identifier,
1563
+ status_type,
1564
+ skip_buffer_flush,
1565
+ )
1566
+
1567
+ if not skip_buffer_flush:
1568
+ buffer_was_flushed = await self._flush_buffer_if_needed(
1569
+ a2a_context, reason=f"before_{status_type}_status"
1570
+ )
1571
+ if buffer_was_flushed:
1572
+ log.info(
1573
+ "%s Buffer flushed before %s status update.",
1574
+ log_identifier,
1575
+ status_type,
1576
+ )
1577
+
1578
+ try:
1579
+ rpc_response = JSONRPCResponse(
1580
+ id=jsonrpc_request_id, result=status_update_event
1581
+ )
1582
+ payload_to_publish = rpc_response.model_dump(exclude_none=True)
1583
+
1584
+ target_topic = a2a_context.get("statusTopic") or get_gateway_status_topic(
1585
+ self.namespace, self.get_gateway_id(), logical_task_id
1586
+ )
1587
+
1588
+ self._publish_a2a_event(payload_to_publish, target_topic, a2a_context)
1589
+
1590
+ log.info(
1591
+ "%s Published %s status update to %s.",
1592
+ log_identifier,
1593
+ status_type,
1594
+ target_topic,
1595
+ )
1596
+
1597
+ except Exception as e:
1598
+ log.exception(
1599
+ "%s Error publishing %s status update: %s",
1600
+ log_identifier,
1601
+ status_type,
1602
+ e,
1603
+ )
1604
+ raise
1605
+
1606
+ async def _translate_adk_part_to_a2a_filepart(
1607
+ self,
1608
+ adk_part: adk_types.Part,
1609
+ filename: str,
1610
+ a2a_context: Dict,
1611
+ version: Optional[int] = None,
1612
+ ) -> Optional[FilePart]:
1613
+ """
1614
+ Translates a loaded ADK Part (with inline_data) to an A2A FilePart
1615
+ based on the configured artifact_handling_mode.
1616
+ If version is not provided, it will be resolved to the latest.
1617
+ """
1618
+ if self.artifact_handling_mode == "ignore":
1619
+ log.debug(
1620
+ "%s Artifact handling mode is 'ignore'. Skipping translation for '%s'.",
1621
+ self.log_identifier,
1622
+ filename,
1623
+ )
1624
+ return None
1625
+
1626
+ if not adk_part or not adk_part.inline_data:
1627
+ log.warning(
1628
+ "%s Cannot translate artifact '%s': ADK Part is missing or has no inline_data.",
1629
+ self.log_identifier,
1630
+ filename,
1631
+ )
1632
+ return None
1633
+
1634
+ resolved_version = version
1635
+ if resolved_version is None:
1636
+ try:
1637
+ resolved_version = await get_latest_artifact_version(
1638
+ artifact_service=self.artifact_service,
1639
+ app_name=self.get_config("agent_name"),
1640
+ user_id=a2a_context.get("user_id"),
1641
+ session_id=a2a_context.get("session_id"),
1642
+ filename=filename,
1643
+ )
1644
+ if resolved_version is None:
1645
+ log.error(
1646
+ "%s Could not resolve latest version for artifact '%s'.",
1647
+ self.log_identifier,
1648
+ filename,
1649
+ )
1650
+ return None
1651
+ except Exception as e:
1652
+ log.exception(
1653
+ "%s Failed to resolve latest version for artifact '%s': %s",
1654
+ self.log_identifier,
1655
+ filename,
1656
+ e,
1657
+ )
1658
+ return None
1659
+
1660
+ mime_type = adk_part.inline_data.mime_type
1661
+ data_bytes = adk_part.inline_data.data
1662
+ file_content: Optional[FileContent] = None
1663
+
1664
+ try:
1665
+ if self.artifact_handling_mode == "embed":
1666
+ encoded_bytes = base64.b64encode(data_bytes).decode("utf-8")
1667
+ file_content = FileContent(
1668
+ name=filename, mimeType=mime_type, bytes=encoded_bytes
1669
+ )
1670
+ log.debug(
1671
+ "%s Embedding artifact '%s' (size: %d bytes) for A2A message.",
1672
+ self.log_identifier,
1673
+ filename,
1674
+ len(data_bytes),
1675
+ )
1676
+
1677
+ elif self.artifact_handling_mode == "reference":
1678
+ adk_app_name = self.get_config("agent_name")
1679
+ user_id = a2a_context.get("user_id")
1680
+ original_session_id = a2a_context.get("session_id")
1681
+
1682
+ if not all([adk_app_name, user_id, original_session_id]):
1683
+ log.error(
1684
+ "%s Cannot create artifact reference URI: missing context (app_name, user_id, or session_id).",
1685
+ self.log_identifier,
1686
+ )
1687
+ return None
1688
+
1689
+ artifact_uri = f"artifact://{adk_app_name}/{user_id}/{original_session_id}/{filename}?version={resolved_version}"
1690
+
1691
+ log.info(
1692
+ "%s Creating reference URI for artifact: %s",
1693
+ self.log_identifier,
1694
+ artifact_uri,
1695
+ )
1696
+ file_content = FileContent(
1697
+ name=filename, mimeType=mime_type, uri=artifact_uri
1698
+ )
1699
+
1700
+ if file_content:
1701
+ return FilePart(file=file_content)
1702
+ else:
1703
+ log.warning(
1704
+ "%s No FileContent created for artifact '%s' despite mode '%s'.",
1705
+ self.log_identifier,
1706
+ filename,
1707
+ self.artifact_handling_mode,
1708
+ )
1709
+ return None
1710
+
1711
+ except Exception as e:
1712
+ log.exception(
1713
+ "%s Error translating artifact '%s' to A2A FilePart (mode: %s): %s",
1714
+ self.log_identifier,
1715
+ filename,
1716
+ self.artifact_handling_mode,
1717
+ e,
1718
+ )
1719
+ return None
1720
+
1721
+ async def _filter_text_from_final_streaming_event(
1722
+ self, adk_event: ADKEvent, a2a_context: Dict
1723
+ ) -> ADKEvent:
1724
+ """
1725
+ Filters out text parts from the final ADKEvent of a turn for PERSISTENT streaming sessions.
1726
+ This prevents sending redundant, aggregated text that was already streamed.
1727
+ Non-text parts like function calls are preserved.
1728
+ """
1729
+ is_run_based_session = a2a_context.get("is_run_based_session", False)
1730
+ is_streaming = a2a_context.get("is_streaming", False)
1731
+ is_final_turn_event = not adk_event.partial
1732
+ has_content_parts = adk_event.content and adk_event.content.parts
1733
+
1734
+ # Only filter for PERSISTENT (not run-based) streaming sessions.
1735
+ if (
1736
+ not is_run_based_session
1737
+ and is_streaming
1738
+ and is_final_turn_event
1739
+ and has_content_parts
1740
+ ):
1741
+ log_id = f"{self.log_identifier}[FilterFinalStreamEvent:{a2a_context.get('logical_task_id', 'unknown')}]"
1742
+ log.debug(
1743
+ "%s Filtering final streaming event to remove redundant text.", log_id
1744
+ )
1745
+
1746
+ non_text_parts = [
1747
+ part for part in adk_event.content.parts if part.text is None
1748
+ ]
1749
+
1750
+ if len(non_text_parts) < len(adk_event.content.parts):
1751
+ event_copy = adk_event.model_copy(deep=True)
1752
+ event_copy.content = (
1753
+ adk_types.Content(parts=non_text_parts) if non_text_parts else None
1754
+ )
1755
+ log.info(
1756
+ "%s Removed text from final streaming event. Kept %d non-text part(s).",
1757
+ log_id,
1758
+ len(non_text_parts),
1759
+ )
1760
+ return event_copy
1761
+
1762
+ return adk_event
1763
+
1764
+ async def process_and_publish_adk_event(
1765
+ self, adk_event: ADKEvent, a2a_context: Dict
1766
+ ):
1767
+ """
1768
+ Main orchestrator for processing ADK events.
1769
+ Handles text buffering, embed resolution, and event routing based on
1770
+ whether the event is partial or the final event of a turn.
1771
+ """
1772
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
1773
+ log_id_main = (
1774
+ f"{self.log_identifier}[ProcessADKEvent:{logical_task_id}:{adk_event.id}]"
1775
+ )
1776
+ log.debug(
1777
+ "%s Received ADKEvent (Partial: %s, Final Turn: %s).",
1778
+ log_id_main,
1779
+ adk_event.partial,
1780
+ not adk_event.partial,
1781
+ )
1782
+
1783
+ if adk_event.content and adk_event.content.parts:
1784
+ if any(
1785
+ p.function_response
1786
+ and p.function_response.name == "_continue_generation"
1787
+ for p in adk_event.content.parts
1788
+ ):
1789
+ log.debug(
1790
+ "%s Discarding _continue_generation tool response event.",
1791
+ log_id_main,
1792
+ )
1793
+ return
1794
+
1795
+ if adk_event.custom_metadata and adk_event.custom_metadata.get(
1796
+ "was_interrupted"
1797
+ ):
1798
+ log.debug(
1799
+ "%s Found 'was_interrupted' signal. Skipping event.",
1800
+ log_id_main,
1801
+ )
1802
+ return
1803
+
1804
+ with self.active_tasks_lock:
1805
+ task_context = self.active_tasks.get(logical_task_id)
1806
+
1807
+ if not task_context:
1808
+ log.error(
1809
+ "%s TaskExecutionContext not found for task %s. Cannot process ADK event.",
1810
+ log_id_main,
1811
+ logical_task_id,
1812
+ )
1813
+ return
1814
+
1815
+ is_run_based_session = a2a_context.get("is_run_based_session", False)
1816
+ is_final_turn_event = not adk_event.partial
1817
+
1818
+ if not is_final_turn_event:
1819
+ if adk_event.content and adk_event.content.parts:
1820
+ for part in adk_event.content.parts:
1821
+ if part.text is not None:
1822
+ task_context.append_to_streaming_buffer(part.text)
1823
+ log.debug(
1824
+ "%s Appended text to buffer. New buffer size: %d bytes",
1825
+ log_id_main,
1826
+ len(
1827
+ task_context.get_streaming_buffer_content().encode(
1828
+ "utf-8"
1829
+ )
1830
+ ),
1831
+ )
1832
+
1833
+ buffer_content = task_context.get_streaming_buffer_content()
1834
+ batching_disabled = self.stream_batching_threshold_bytes <= 0
1835
+ buffer_has_content = bool(buffer_content)
1836
+ threshold_met = (
1837
+ buffer_has_content
1838
+ and not batching_disabled
1839
+ and (
1840
+ len(buffer_content.encode("utf-8"))
1841
+ >= self.stream_batching_threshold_bytes
1842
+ )
1843
+ )
1844
+
1845
+ if buffer_has_content and (batching_disabled or threshold_met):
1846
+ log.info(
1847
+ "%s Partial event triggered buffer flush due to size/batching config.",
1848
+ log_id_main,
1849
+ )
1850
+ resolved_text, _ = await self._flush_and_resolve_buffer(
1851
+ a2a_context, is_final=False
1852
+ )
1853
+
1854
+ if resolved_text:
1855
+ if is_run_based_session:
1856
+ task_context.append_to_run_based_buffer(resolved_text)
1857
+ log.debug(
1858
+ "%s [RUN_BASED] Appended %d bytes to run_based_response_buffer.",
1859
+ log_id_main,
1860
+ len(resolved_text.encode("utf-8")),
1861
+ )
1862
+ else:
1863
+ await self._publish_text_as_partial_a2a_status_update(
1864
+ resolved_text, a2a_context
1865
+ )
1866
+ else:
1867
+ buffer_content = task_context.get_streaming_buffer_content()
1868
+ if buffer_content:
1869
+ log.info(
1870
+ "%s Final event triggered flush of remaining buffer content.",
1871
+ log_id_main,
1872
+ )
1873
+ resolved_text, _ = await self._flush_and_resolve_buffer(
1874
+ a2a_context, is_final=True
1875
+ )
1876
+ if resolved_text:
1877
+ if is_run_based_session:
1878
+ task_context.append_to_run_based_buffer(resolved_text)
1879
+ log.debug(
1880
+ "%s [RUN_BASED] Appended final %d bytes to run_based_response_buffer.",
1881
+ log_id_main,
1882
+ len(resolved_text.encode("utf-8")),
1883
+ )
1884
+ else:
1885
+ await self._publish_text_as_partial_a2a_status_update(
1886
+ resolved_text, a2a_context
1887
+ )
1888
+
1889
+ # Prepare and publish the final event for observability
1890
+ event_to_publish = await self._filter_text_from_final_streaming_event(
1891
+ adk_event, a2a_context
1892
+ )
1893
+
1894
+ (
1895
+ a2a_payload,
1896
+ target_topic,
1897
+ user_properties,
1898
+ _,
1899
+ ) = await format_and_route_adk_event(event_to_publish, a2a_context, self)
1900
+
1901
+ if a2a_payload and target_topic:
1902
+ self._publish_a2a_event(a2a_payload, target_topic, a2a_context)
1903
+ log.info(
1904
+ "%s Published final turn event (e.g., tool call) to %s.",
1905
+ log_id_main,
1906
+ target_topic,
1907
+ )
1908
+ else:
1909
+ log.debug(
1910
+ "%s Final turn event did not result in a publishable A2A message.",
1911
+ log_id_main,
1912
+ )
1913
+
1914
+ await self._handle_artifact_return_signals(adk_event, a2a_context)
1915
+
1916
+ async def _flush_and_resolve_buffer(
1917
+ self, a2a_context: Dict, is_final: bool
1918
+ ) -> Tuple[str, str]:
1919
+ """Flushes buffer, resolves embeds, handles signals, returns (resolved_text, unprocessed_tail)."""
1920
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
1921
+ log_id = f"{self.log_identifier}[FlushBuffer:{logical_task_id}]"
1922
+
1923
+ with self.active_tasks_lock:
1924
+ task_context = self.active_tasks.get(logical_task_id)
1925
+
1926
+ if not task_context:
1927
+ log.error(
1928
+ "%s TaskExecutionContext not found for task %s. Cannot flush/resolve buffer.",
1929
+ log_id,
1930
+ logical_task_id,
1931
+ )
1932
+ return "", ""
1933
+
1934
+ text_to_process = task_context.flush_streaming_buffer()
1935
+
1936
+ resolved_text, signals_found, unprocessed_tail = (
1937
+ await self._resolve_early_embeds_and_handle_signals(
1938
+ text_to_process, a2a_context
1939
+ )
1940
+ )
1941
+
1942
+ if not is_final:
1943
+ if unprocessed_tail:
1944
+ task_context.append_to_streaming_buffer(unprocessed_tail)
1945
+ log.debug(
1946
+ "%s Placed unprocessed tail (length %d) back into buffer.",
1947
+ log_id,
1948
+ len(unprocessed_tail.encode("utf-8")),
1949
+ )
1950
+ else:
1951
+ if unprocessed_tail is not None and unprocessed_tail != "":
1952
+ resolved_text = resolved_text + unprocessed_tail
1953
+
1954
+ if signals_found:
1955
+ log.info(
1956
+ "%s Handling %d signals from buffer resolution.",
1957
+ log_id,
1958
+ len(signals_found),
1959
+ )
1960
+ for _signal_index, signal_data_tuple in signals_found:
1961
+ if (
1962
+ isinstance(signal_data_tuple, tuple)
1963
+ and len(signal_data_tuple) == 3
1964
+ and signal_data_tuple[0] is None
1965
+ and signal_data_tuple[1] == "SIGNAL_STATUS_UPDATE"
1966
+ ):
1967
+ status_text = signal_data_tuple[2]
1968
+ log.info(
1969
+ "%s Publishing SIGNAL_STATUS_UPDATE from buffer: '%s'",
1970
+ log_id,
1971
+ status_text,
1972
+ )
1973
+ await self._publish_agent_status_signal_update(
1974
+ status_text, a2a_context
1975
+ )
1976
+
1977
+ return resolved_text, unprocessed_tail
1978
+
1979
+ async def _handle_artifact_return_signals(
1980
+ self, adk_event: ADKEvent, a2a_context: Dict
1981
+ ):
1982
+ """
1983
+ Processes artifact return signals.
1984
+ This method is triggered by a placeholder in state_delta, but reads the
1985
+ actual list of signals from the TaskExecutionContext.
1986
+ """
1987
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
1988
+ log_id = f"{self.log_identifier}[ArtifactSignals:{logical_task_id}]"
1989
+
1990
+ # Check for the trigger in state_delta. The presence of any key is enough.
1991
+ has_signal_trigger = (
1992
+ adk_event.actions
1993
+ and adk_event.actions.state_delta
1994
+ and any(
1995
+ k.startswith("temp:a2a_return_artifact:")
1996
+ for k in adk_event.actions.state_delta
1997
+ )
1998
+ )
1999
+
2000
+ if not has_signal_trigger:
2001
+ return
2002
+
2003
+ with self.active_tasks_lock:
2004
+ task_context = self.active_tasks.get(logical_task_id)
2005
+
2006
+ if not task_context:
2007
+ log.warning(
2008
+ "%s No TaskExecutionContext found for task %s. Cannot process artifact signals.",
2009
+ log_id,
2010
+ logical_task_id,
2011
+ )
2012
+ return
2013
+
2014
+ all_signals = task_context.get_and_clear_artifact_signals()
2015
+
2016
+ if not all_signals:
2017
+ log.info(
2018
+ "%s Triggered for artifact signals, but none were found in the execution context.",
2019
+ log_id,
2020
+ )
2021
+ return
2022
+
2023
+ log.info(
2024
+ "%s Found %d artifact return signal(s) in the execution context.",
2025
+ log_id,
2026
+ len(all_signals),
2027
+ )
2028
+
2029
+ original_session_id = a2a_context.get("session_id")
2030
+ user_id = a2a_context.get("user_id")
2031
+ adk_app_name = self.get_config("agent_name")
2032
+
2033
+ peer_status_topic = a2a_context.get("statusTopic")
2034
+ namespace = self.get_config("namespace")
2035
+ gateway_id = self.get_gateway_id()
2036
+
2037
+ artifact_topic = peer_status_topic or get_gateway_status_topic(
2038
+ namespace, gateway_id, logical_task_id
2039
+ )
2040
+
2041
+ if not self.artifact_service:
2042
+ log.error("%s Artifact service not available.", log_id)
2043
+ return
2044
+ if not artifact_topic:
2045
+ log.error("%s Could not determine artifact topic.", log_id)
2046
+ return
2047
+
2048
+ for item in all_signals:
2049
+ try:
2050
+ filename = item["filename"]
2051
+ version = item["version"]
2052
+
2053
+ log.info(
2054
+ "%s Processing artifact return signal for '%s' v%d from context.",
2055
+ log_id,
2056
+ filename,
2057
+ version,
2058
+ )
2059
+
2060
+ loaded_adk_part = await self.artifact_service.load_artifact(
2061
+ app_name=adk_app_name,
2062
+ user_id=user_id,
2063
+ session_id=original_session_id,
2064
+ filename=filename,
2065
+ version=version,
2066
+ )
2067
+
2068
+ if not loaded_adk_part:
2069
+ log.warning(
2070
+ "%s Failed to load artifact '%s' v%d.",
2071
+ log_id,
2072
+ filename,
2073
+ version,
2074
+ )
2075
+ continue
2076
+
2077
+ a2a_file_part = await self._translate_adk_part_to_a2a_filepart(
2078
+ loaded_adk_part, filename, a2a_context, version=version
2079
+ )
2080
+
2081
+ if a2a_file_part:
2082
+ a2a_artifact = A2AArtifact(name=filename, parts=[a2a_file_part])
2083
+ artifact_update_event = TaskArtifactUpdateEvent(
2084
+ id=logical_task_id, artifact=a2a_artifact
2085
+ )
2086
+ artifact_payload = JSONRPCResponse(
2087
+ id=a2a_context.get("jsonrpc_request_id"),
2088
+ result=artifact_update_event,
2089
+ ).model_dump(exclude_none=True)
2090
+
2091
+ self._publish_a2a_event(
2092
+ artifact_payload, artifact_topic, a2a_context
2093
+ )
2094
+
2095
+ log.info(
2096
+ "%s Published TaskArtifactUpdateEvent for '%s' to %s",
2097
+ log_id,
2098
+ filename,
2099
+ artifact_topic,
2100
+ )
2101
+ else:
2102
+ log.warning(
2103
+ "%s Failed to translate artifact '%s' v%d to A2A FilePart.",
2104
+ log_id,
2105
+ filename,
2106
+ version,
2107
+ )
2108
+
2109
+ except Exception as e:
2110
+ log.exception(
2111
+ "%s Error processing artifact signal item %s from context: %s",
2112
+ log_id,
2113
+ item,
2114
+ e,
2115
+ )
2116
+
2117
+ def _format_final_task_status(self, last_event: ADKEvent) -> TaskStatus:
2118
+ """Helper to format the final TaskStatus based on the last ADK event."""
2119
+ log.debug(
2120
+ "%s Formatting final task status from last ADK event %s",
2121
+ self.log_identifier,
2122
+ last_event.id,
2123
+ )
2124
+ a2a_state = TaskState.COMPLETED
2125
+ a2a_parts = []
2126
+
2127
+ if last_event.content and last_event.content.parts:
2128
+ for part in last_event.content.parts:
2129
+ if part.text:
2130
+ a2a_parts.append(TextPart(text=part.text))
2131
+ elif part.function_response:
2132
+ try:
2133
+ response_data = part.function_response.response
2134
+ if isinstance(response_data, dict):
2135
+ a2a_parts.append(
2136
+ DataPart(
2137
+ data=response_data,
2138
+ metadata={"tool_name": part.function_response.name},
2139
+ )
2140
+ )
2141
+ else:
2142
+ a2a_parts.append(
2143
+ TextPart(
2144
+ text=f"Tool {part.function_response.name} result: {str(response_data)}"
2145
+ )
2146
+ )
2147
+ except Exception:
2148
+ a2a_parts.append(
2149
+ TextPart(
2150
+ text=f"[Tool {part.function_response.name} result omitted]"
2151
+ )
2152
+ )
2153
+
2154
+ elif last_event.actions:
2155
+ if last_event.actions.requested_auth_configs:
2156
+ a2a_state = TaskState.INPUT_REQUIRED
2157
+ a2a_parts.append(TextPart(text="[Agent requires input/authentication]"))
2158
+
2159
+ if not a2a_parts:
2160
+ a2a_parts.append(TextPart(text=""))
2161
+
2162
+ a2a_message = A2AMessage(role="agent", parts=a2a_parts)
2163
+ return TaskStatus(state=a2a_state, message=a2a_message)
2164
+
2165
+ async def finalize_task_success(self, a2a_context: Dict):
2166
+ """
2167
+ Finalizes a task successfully. Fetches final state, publishes final A2A response,
2168
+ and ACKs the original message.
2169
+ For RUN_BASED tasks, it uses the aggregated response buffer.
2170
+ For STREAMING tasks, it uses the content of the last ADK event.
2171
+ """
2172
+ logical_task_id = a2a_context.get("logical_task_id")
2173
+ original_message: Optional[SolaceMessage] = a2a_context.get(
2174
+ "original_solace_message"
2175
+ )
2176
+ log.info(
2177
+ "%s Finalizing task %s successfully.", self.log_identifier, logical_task_id
2178
+ )
2179
+ try:
2180
+ session_id_to_retrieve = a2a_context.get(
2181
+ "effective_session_id", a2a_context.get("session_id")
2182
+ )
2183
+ original_session_id = a2a_context.get("session_id")
2184
+ user_id = a2a_context.get("user_id")
2185
+ client_id = a2a_context.get("client_id")
2186
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
2187
+ peer_reply_topic = a2a_context.get("replyToTopic")
2188
+ namespace = self.get_config("namespace")
2189
+ agent_name = self.get_config("agent_name")
2190
+ is_run_based_session = a2a_context.get("is_run_based_session", False)
2191
+
2192
+ final_status: TaskStatus
2193
+
2194
+ with self.active_tasks_lock:
2195
+ task_context = self.active_tasks.get(logical_task_id)
2196
+
2197
+ final_adk_session = await self.session_service.get_session(
2198
+ app_name=agent_name,
2199
+ user_id=user_id,
2200
+ session_id=session_id_to_retrieve,
2201
+ )
2202
+ if not final_adk_session:
2203
+ raise RuntimeError(
2204
+ f"Could not retrieve final session state for {session_id_to_retrieve}"
2205
+ )
2206
+
2207
+ last_event = (
2208
+ final_adk_session.events[-1] if final_adk_session.events else None
2209
+ )
2210
+
2211
+ if is_run_based_session:
2212
+ aggregated_text = ""
2213
+ if task_context:
2214
+ aggregated_text = task_context.run_based_response_buffer
2215
+ log.info(
2216
+ "%s Using aggregated response buffer for RUN_BASED task %s (length: %d bytes).",
2217
+ self.log_identifier,
2218
+ logical_task_id,
2219
+ len(aggregated_text.encode("utf-8")),
2220
+ )
2221
+
2222
+ final_a2a_parts = []
2223
+ if aggregated_text:
2224
+ final_a2a_parts.append(TextPart(text=aggregated_text))
2225
+
2226
+ if last_event and last_event.content and last_event.content.parts:
2227
+ for part in last_event.content.parts:
2228
+ if part.text is None:
2229
+ if part.function_response:
2230
+ try:
2231
+ response_data = part.function_response.response
2232
+ if isinstance(response_data, dict):
2233
+ final_a2a_parts.append(
2234
+ DataPart(
2235
+ data=response_data,
2236
+ metadata={
2237
+ "tool_name": part.function_response.name
2238
+ },
2239
+ )
2240
+ )
2241
+ else:
2242
+ final_a2a_parts.append(
2243
+ TextPart(
2244
+ text=f"Tool {part.function_response.name} result: {str(response_data)}"
2245
+ )
2246
+ )
2247
+ except Exception:
2248
+ final_a2a_parts.append(
2249
+ TextPart(
2250
+ text=f"[Tool {part.function_response.name} result omitted]"
2251
+ )
2252
+ )
2253
+
2254
+ if not final_a2a_parts:
2255
+ final_a2a_parts.append(TextPart(text=""))
2256
+
2257
+ final_status = TaskStatus(
2258
+ state=TaskState.COMPLETED,
2259
+ message=A2AMessage(role="agent", parts=final_a2a_parts),
2260
+ )
2261
+ else:
2262
+ if last_event:
2263
+ final_status = self._format_final_task_status(last_event)
2264
+ else:
2265
+ final_status = TaskStatus(
2266
+ state=TaskState.COMPLETED,
2267
+ message=A2AMessage(
2268
+ role="agent", parts=[TextPart(text="Task completed.")]
2269
+ ),
2270
+ )
2271
+
2272
+ final_a2a_artifacts: List[A2AArtifact] = []
2273
+ log.debug(
2274
+ "%s Final artifact bundling is removed. Artifacts sent via TaskArtifactUpdateEvent.",
2275
+ self.log_identifier,
2276
+ )
2277
+
2278
+ final_task_metadata = {"agent_name": agent_name}
2279
+ if task_context and task_context.produced_artifacts:
2280
+ final_task_metadata["produced_artifacts"] = (
2281
+ task_context.produced_artifacts
2282
+ )
2283
+ log.info(
2284
+ "%s Attaching manifest of %d produced artifacts to final task metadata.",
2285
+ self.log_identifier,
2286
+ len(task_context.produced_artifacts),
2287
+ )
2288
+
2289
+ final_task = Task(
2290
+ id=logical_task_id,
2291
+ sessionId=original_session_id,
2292
+ status=final_status,
2293
+ artifacts=(final_a2a_artifacts if final_a2a_artifacts else None),
2294
+ metadata=final_task_metadata,
2295
+ )
2296
+ final_response = JSONRPCResponse(id=jsonrpc_request_id, result=final_task)
2297
+ a2a_payload = final_response.model_dump(exclude_none=True)
2298
+ target_topic = peer_reply_topic or get_client_response_topic(
2299
+ namespace, client_id
2300
+ )
2301
+
2302
+ self._publish_a2a_event(a2a_payload, target_topic, a2a_context)
2303
+ log.info(
2304
+ "%s Published final successful response for task %s to %s (Artifacts NOT bundled).",
2305
+ self.log_identifier,
2306
+ logical_task_id,
2307
+ target_topic,
2308
+ )
2309
+ if original_message:
2310
+ try:
2311
+ original_message.call_acknowledgements()
2312
+ log.info(
2313
+ "%s Called ACK for original message of task %s.",
2314
+ self.log_identifier,
2315
+ logical_task_id,
2316
+ )
2317
+ except Exception as ack_e:
2318
+ log.error(
2319
+ "%s Failed to call ACK for task %s: %s",
2320
+ self.log_identifier,
2321
+ logical_task_id,
2322
+ ack_e,
2323
+ )
2324
+ else:
2325
+ log.warning(
2326
+ "%s Original Solace message not found in context for task %s. Cannot ACK.",
2327
+ self.log_identifier,
2328
+ logical_task_id,
2329
+ )
2330
+
2331
+ except Exception as e:
2332
+ log.exception(
2333
+ "%s Error during successful finalization of task %s: %s",
2334
+ self.log_identifier,
2335
+ logical_task_id,
2336
+ e,
2337
+ )
2338
+ if original_message:
2339
+ try:
2340
+ original_message.call_negative_acknowledgements()
2341
+ log.warning(
2342
+ "%s Called NACK for original message of task %s due to finalization error.",
2343
+ self.log_identifier,
2344
+ logical_task_id,
2345
+ )
2346
+ except Exception as nack_e:
2347
+ log.error(
2348
+ "%s Failed to call NACK for task %s after finalization error: %s",
2349
+ self.log_identifier,
2350
+ logical_task_id,
2351
+ nack_e,
2352
+ )
2353
+ else:
2354
+ log.warning(
2355
+ "%s Original Solace message not found in context for task %s during finalization error. Cannot NACK.",
2356
+ self.log_identifier,
2357
+ logical_task_id,
2358
+ )
2359
+
2360
+ try:
2361
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
2362
+ client_id = a2a_context.get("client_id")
2363
+ peer_reply_topic = a2a_context.get("replyToTopic")
2364
+ namespace = self.get_config("namespace")
2365
+ error_response = JSONRPCResponse(
2366
+ id=jsonrpc_request_id,
2367
+ error=InternalError(
2368
+ message=f"Failed to finalize successful task: {e}",
2369
+ data={"taskId": logical_task_id},
2370
+ ),
2371
+ )
2372
+ target_topic = peer_reply_topic or get_client_response_topic(
2373
+ namespace, client_id
2374
+ )
2375
+ self._publish_a2a_message(
2376
+ error_response.model_dump(exclude_none=True), target_topic
2377
+ )
2378
+ except Exception as report_err:
2379
+ log.error(
2380
+ "%s Failed to report finalization error for task %s: %s",
2381
+ self.log_identifier,
2382
+ logical_task_id,
2383
+ report_err,
2384
+ )
2385
+
2386
+ def finalize_task_canceled(self, a2a_context: Dict):
2387
+ """
2388
+ Finalizes a task as CANCELED. Publishes A2A Task response with CANCELED state
2389
+ and ACKs the original message if available.
2390
+ Called by the background ADK thread wrapper when a task is cancelled.
2391
+ """
2392
+ logical_task_id = a2a_context.get("logical_task_id")
2393
+ original_message: Optional[SolaceMessage] = a2a_context.get(
2394
+ "original_solace_message"
2395
+ )
2396
+ log.info(
2397
+ "%s Finalizing task %s as CANCELED.", self.log_identifier, logical_task_id
2398
+ )
2399
+ try:
2400
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
2401
+ client_id = a2a_context.get("client_id")
2402
+ peer_reply_topic = a2a_context.get("replyToTopic")
2403
+ namespace = self.get_config("namespace")
2404
+
2405
+ canceled_status = TaskStatus(
2406
+ state=TaskState.CANCELED,
2407
+ message=A2AMessage(
2408
+ role="agent",
2409
+ parts=[TextPart(text="Task cancelled by request.")],
2410
+ ),
2411
+ )
2412
+ agent_name = self.get_config("agent_name")
2413
+ final_task = Task(
2414
+ id=logical_task_id,
2415
+ sessionId=a2a_context.get("session_id"),
2416
+ status=canceled_status,
2417
+ metadata={"agent_name": agent_name},
2418
+ )
2419
+ final_response = JSONRPCResponse(id=jsonrpc_request_id, result=final_task)
2420
+ a2a_payload = final_response.model_dump(exclude_none=True)
2421
+ target_topic = peer_reply_topic or get_client_response_topic(
2422
+ namespace, client_id
2423
+ )
2424
+
2425
+ self._publish_a2a_event(a2a_payload, target_topic, a2a_context)
2426
+ log.info(
2427
+ "%s Published final CANCELED response for task %s to %s.",
2428
+ self.log_identifier,
2429
+ logical_task_id,
2430
+ target_topic,
2431
+ )
2432
+
2433
+ if original_message:
2434
+ try:
2435
+ original_message.call_acknowledgements()
2436
+ log.info(
2437
+ "%s Called ACK for original message of cancelled task %s.",
2438
+ self.log_identifier,
2439
+ logical_task_id,
2440
+ )
2441
+ except Exception as ack_e:
2442
+ log.error(
2443
+ "%s Failed to call ACK for cancelled task %s: %s",
2444
+ self.log_identifier,
2445
+ logical_task_id,
2446
+ ack_e,
2447
+ )
2448
+ else:
2449
+ log.warning(
2450
+ "%s Original Solace message not found in context for cancelled task %s. Cannot ACK.",
2451
+ self.log_identifier,
2452
+ logical_task_id,
2453
+ )
2454
+
2455
+ except Exception as e:
2456
+ log.exception(
2457
+ "%s Error during CANCELED finalization of task %s: %s",
2458
+ self.log_identifier,
2459
+ logical_task_id,
2460
+ e,
2461
+ )
2462
+ if original_message:
2463
+ try:
2464
+ original_message.call_negative_acknowledgements()
2465
+ except Exception:
2466
+ pass
2467
+
2468
+ async def _publish_tool_failure_status(
2469
+ self, exception: Exception, a2a_context: Dict
2470
+ ):
2471
+ """
2472
+ Publishes an intermediate status update indicating a tool execution has failed.
2473
+ This method will flush the buffer before publishing to maintain proper message ordering.
2474
+ """
2475
+ logical_task_id = a2a_context.get("logical_task_id")
2476
+ log_identifier_helper = (
2477
+ f"{self.log_identifier}[ToolFailureStatus:{logical_task_id}]"
2478
+ )
2479
+ try:
2480
+ # Create the status update event
2481
+ tool_error_data_part = DataPart(
2482
+ data={
2483
+ "a2a_signal_type": "tool_execution_error",
2484
+ "error_message": str(exception),
2485
+ "details": "An unhandled exception occurred during tool execution.",
2486
+ }
2487
+ )
2488
+
2489
+ status_message = A2AMessage(role="agent", parts=[tool_error_data_part])
2490
+ intermediate_status = TaskStatus(
2491
+ state=TaskState.WORKING,
2492
+ message=status_message,
2493
+ timestamp=datetime.now(timezone.utc),
2494
+ )
2495
+
2496
+ status_update_event = TaskStatusUpdateEvent(
2497
+ id=logical_task_id,
2498
+ status=intermediate_status,
2499
+ final=False,
2500
+ metadata={"agent_name": self.get_config("agent_name")},
2501
+ )
2502
+
2503
+ await self._publish_status_update_with_buffer_flush(
2504
+ status_update_event,
2505
+ a2a_context,
2506
+ skip_buffer_flush=False,
2507
+ )
2508
+
2509
+ log.debug(
2510
+ "%s Published tool failure status update.",
2511
+ log_identifier_helper,
2512
+ )
2513
+
2514
+ except Exception as e:
2515
+ log.error(
2516
+ "%s Failed to publish intermediate tool failure status: %s",
2517
+ log_identifier_helper,
2518
+ e,
2519
+ )
2520
+
2521
+ async def _repair_session_history_on_error(
2522
+ self, exception: Exception, a2a_context: Dict
2523
+ ):
2524
+ """
2525
+ Reactively repairs the session history if the last event was a tool call.
2526
+ This is "the belt" in the belt-and-suspenders strategy.
2527
+ """
2528
+ log_identifier = f"{self.log_identifier}[HistoryRepair]"
2529
+ try:
2530
+ from ...agent.adk.callbacks import create_dangling_tool_call_repair_content
2531
+
2532
+ session_id = a2a_context.get("effective_session_id")
2533
+ user_id = a2a_context.get("user_id")
2534
+ agent_name = self.get_config("agent_name")
2535
+
2536
+ if not all([session_id, user_id, agent_name, self.session_service]):
2537
+ log.warning(
2538
+ "%s Skipping history repair due to missing context.", log_identifier
2539
+ )
2540
+ return
2541
+
2542
+ session = await self.session_service.get_session(
2543
+ app_name=agent_name, user_id=user_id, session_id=session_id
2544
+ )
2545
+
2546
+ if not session or not session.events:
2547
+ log.debug(
2548
+ "%s No session or events found for history repair.", log_identifier
2549
+ )
2550
+ return
2551
+
2552
+ last_event = session.events[-1]
2553
+ function_calls = last_event.get_function_calls()
2554
+
2555
+ if not function_calls:
2556
+ log.debug(
2557
+ "%s Last event was not a function call. No repair needed.",
2558
+ log_identifier,
2559
+ )
2560
+ return
2561
+
2562
+ log.info(
2563
+ "%s Last event contained function_call(s). Repairing session history.",
2564
+ log_identifier,
2565
+ )
2566
+
2567
+ repair_content = create_dangling_tool_call_repair_content(
2568
+ dangling_calls=function_calls,
2569
+ error_message=f"Tool execution failed with an unhandled exception: {str(exception)}",
2570
+ )
2571
+
2572
+ repair_event = ADKEvent(
2573
+ invocation_id=last_event.invocation_id,
2574
+ author=agent_name,
2575
+ content=repair_content,
2576
+ )
2577
+
2578
+ await self.session_service.append_event(session=session, event=repair_event)
2579
+ log.info(
2580
+ "%s Session history repaired successfully with an error function_response.",
2581
+ log_identifier,
2582
+ )
2583
+
2584
+ except Exception as e:
2585
+ log.exception(
2586
+ "%s Critical error during session history repair: %s", log_identifier, e
2587
+ )
2588
+
2589
+ def finalize_task_limit_reached(
2590
+ self, a2a_context: Dict, exception: LlmCallsLimitExceededError
2591
+ ):
2592
+ """
2593
+ Finalizes a task when the LLM call limit is reached, prompting the user to continue.
2594
+ Sends a COMPLETED status with an informative message.
2595
+ """
2596
+ logical_task_id = a2a_context.get("logical_task_id")
2597
+ original_message: Optional[SolaceMessage] = a2a_context.get(
2598
+ "original_solace_message"
2599
+ )
2600
+ log.info(
2601
+ "%s Finalizing task %s as COMPLETED (LLM call limit reached).",
2602
+ self.log_identifier,
2603
+ logical_task_id,
2604
+ )
2605
+ try:
2606
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
2607
+ client_id = a2a_context.get("client_id")
2608
+ peer_reply_topic = a2a_context.get("replyToTopic")
2609
+ namespace = self.get_config("namespace")
2610
+ agent_name = self.get_config("agent_name")
2611
+ original_session_id = a2a_context.get("session_id")
2612
+
2613
+ limit_message_text = (
2614
+ f"This interaction has reached its processing limit. "
2615
+ "If you'd like to continue this conversation, please type 'continue'. "
2616
+ "Otherwise, you can start a new topic."
2617
+ )
2618
+
2619
+ error_payload = InternalError(
2620
+ message=limit_message_text,
2621
+ data={"taskId": logical_task_id, "reason": "llm_call_limit_reached"},
2622
+ )
2623
+
2624
+ final_response = JSONRPCResponse(id=jsonrpc_request_id, error=error_payload)
2625
+ a2a_payload = final_response.model_dump(exclude_none=True)
2626
+
2627
+ target_topic = peer_reply_topic or get_client_response_topic(
2628
+ namespace, client_id
2629
+ )
2630
+
2631
+ self._publish_a2a_event(a2a_payload, target_topic, a2a_context)
2632
+ log.info(
2633
+ "%s Published ERROR response for task %s to %s (LLM limit reached, user guided to continue).",
2634
+ self.log_identifier,
2635
+ logical_task_id,
2636
+ target_topic,
2637
+ )
2638
+
2639
+ if original_message:
2640
+ try:
2641
+ original_message.call_acknowledgements()
2642
+ log.info(
2643
+ "%s Called ACK for original message of task %s (LLM limit reached).",
2644
+ self.log_identifier,
2645
+ logical_task_id,
2646
+ )
2647
+ except Exception as ack_e:
2648
+ log.error(
2649
+ "%s Failed to call ACK for task %s (LLM limit reached): %s",
2650
+ self.log_identifier,
2651
+ logical_task_id,
2652
+ ack_e,
2653
+ )
2654
+ else:
2655
+ log.warning(
2656
+ "%s Original Solace message not found in context for task %s (LLM limit reached). Cannot ACK.",
2657
+ self.log_identifier,
2658
+ logical_task_id,
2659
+ )
2660
+
2661
+ except Exception as e:
2662
+ log.exception(
2663
+ "%s Error during COMPLETED (LLM limit) finalization of task %s: %s",
2664
+ self.log_identifier,
2665
+ logical_task_id,
2666
+ e,
2667
+ )
2668
+ self.finalize_task_error(e, a2a_context)
2669
+
2670
+ async def finalize_task_error(self, exception: Exception, a2a_context: Dict):
2671
+ """
2672
+ Finalizes a task with an error. Publishes a final A2A Task with a FAILED
2673
+ status and NACKs the original message.
2674
+ Called by the background ADK thread wrapper.
2675
+ """
2676
+ logical_task_id = a2a_context.get("logical_task_id")
2677
+ original_message: Optional[SolaceMessage] = a2a_context.get(
2678
+ "original_solace_message"
2679
+ )
2680
+ log.error(
2681
+ "%s Finalizing task %s with error: %s",
2682
+ self.log_identifier,
2683
+ logical_task_id,
2684
+ exception,
2685
+ )
2686
+ try:
2687
+ await self._repair_session_history_on_error(exception, a2a_context)
2688
+
2689
+ await self._publish_tool_failure_status(exception, a2a_context)
2690
+
2691
+ client_id = a2a_context.get("client_id")
2692
+ jsonrpc_request_id = a2a_context.get("jsonrpc_request_id")
2693
+ peer_reply_topic = a2a_context.get("replyToTopic")
2694
+ namespace = self.get_config("namespace")
2695
+
2696
+ failed_status = TaskStatus(
2697
+ state=TaskState.FAILED,
2698
+ message=A2AMessage(
2699
+ role="agent",
2700
+ parts=[
2701
+ TextPart(
2702
+ text="An unexpected error occurred during tool execution. Please try your request again. If the problem persists, contact an administrator."
2703
+ )
2704
+ ],
2705
+ ),
2706
+ )
2707
+
2708
+ final_task = Task(
2709
+ id=logical_task_id,
2710
+ sessionId=a2a_context.get("session_id"),
2711
+ status=failed_status,
2712
+ metadata={"agent_name": self.get_config("agent_name")},
2713
+ )
2714
+
2715
+ final_response = JSONRPCResponse(id=jsonrpc_request_id, result=final_task)
2716
+ a2a_payload = final_response.model_dump(exclude_none=True)
2717
+ target_topic = peer_reply_topic or get_client_response_topic(
2718
+ namespace, client_id
2719
+ )
2720
+
2721
+ self._publish_a2a_event(a2a_payload, target_topic, a2a_context)
2722
+ log.info(
2723
+ "%s Published final FAILED Task response for task %s to %s",
2724
+ self.log_identifier,
2725
+ logical_task_id,
2726
+ target_topic,
2727
+ )
2728
+
2729
+ if original_message:
2730
+ try:
2731
+ original_message.call_negative_acknowledgements()
2732
+ log.info(
2733
+ "%s Called NACK for original message of failed task %s.",
2734
+ self.log_identifier,
2735
+ logical_task_id,
2736
+ )
2737
+ except Exception as nack_e:
2738
+ log.error(
2739
+ "%s Failed to call NACK for failed task %s: %s",
2740
+ self.log_identifier,
2741
+ logical_task_id,
2742
+ nack_e,
2743
+ )
2744
+ else:
2745
+ log.warning(
2746
+ "%s Original Solace message not found in context for failed task %s. Cannot NACK.",
2747
+ self.log_identifier,
2748
+ logical_task_id,
2749
+ )
2750
+
2751
+ except Exception as e:
2752
+ log.exception(
2753
+ "%s Error during error finalization of task %s: %s",
2754
+ self.log_identifier,
2755
+ logical_task_id,
2756
+ e,
2757
+ )
2758
+ if original_message:
2759
+ try:
2760
+ original_message.call_negative_acknowledgements()
2761
+ log.warning(
2762
+ "%s Called NACK for task %s during error finalization fallback.",
2763
+ self.log_identifier,
2764
+ logical_task_id,
2765
+ )
2766
+ except Exception as nack_e:
2767
+ log.error(
2768
+ "%s Failed to call NACK for task %s during error finalization fallback: %s",
2769
+ self.log_identifier,
2770
+ logical_task_id,
2771
+ nack_e,
2772
+ )
2773
+ else:
2774
+ log.warning(
2775
+ "%s Original Solace message not found for task %s during error finalization fallback. Cannot NACK.",
2776
+ self.log_identifier,
2777
+ logical_task_id,
2778
+ )
2779
+
2780
+ async def finalize_task_with_cleanup(
2781
+ self, a2a_context: Dict, is_paused: bool, exception: Optional[Exception] = None
2782
+ ):
2783
+ """
2784
+ Centralized async method to finalize a task and perform all necessary cleanup.
2785
+ This is scheduled on the component's event loop to ensure it runs after
2786
+ any pending status updates.
2787
+
2788
+ Args:
2789
+ a2a_context: The context dictionary for the task.
2790
+ is_paused: Boolean indicating if the task is paused for a long-running tool.
2791
+ exception: The exception that occurred, if any.
2792
+ """
2793
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
2794
+ log_id = f"{self.log_identifier}[FinalizeTask:{logical_task_id}]"
2795
+ log.info(
2796
+ "%s Starting finalization and cleanup. Paused: %s, Exception: %s",
2797
+ log_id,
2798
+ is_paused,
2799
+ type(exception).__name__ if exception else "None",
2800
+ )
2801
+
2802
+ try:
2803
+ if is_paused:
2804
+ log.info(
2805
+ "%s Task is paused for a long-running tool. Skipping finalization logic.",
2806
+ log_id,
2807
+ )
2808
+ else:
2809
+ try:
2810
+ if exception:
2811
+ if isinstance(exception, TaskCancelledError):
2812
+ self.finalize_task_canceled(a2a_context)
2813
+ elif isinstance(exception, LlmCallsLimitExceededError):
2814
+ self.finalize_task_limit_reached(a2a_context, exception)
2815
+ else:
2816
+ await self.finalize_task_error(exception, a2a_context)
2817
+ else:
2818
+ await self.finalize_task_success(a2a_context)
2819
+ except Exception as e:
2820
+ log.exception(
2821
+ "%s An unexpected error occurred during the finalization logic itself: %s",
2822
+ log_id,
2823
+ e,
2824
+ )
2825
+ original_message: Optional[SolaceMessage] = a2a_context.get(
2826
+ "original_solace_message"
2827
+ )
2828
+ if original_message:
2829
+ try:
2830
+ original_message.call_negative_acknowledgements()
2831
+ except Exception as nack_err:
2832
+ log.error(
2833
+ "%s Fallback NACK failed during finalization error: %s",
2834
+ log_id,
2835
+ nack_err,
2836
+ )
2837
+ finally:
2838
+ if not is_paused:
2839
+ # Cleanup for RUN_BASED sessions remains, as it's a service-level concern
2840
+ if a2a_context.get("is_run_based_session"):
2841
+ temp_session_id_to_delete = a2a_context.get(
2842
+ "temporary_run_session_id_for_cleanup"
2843
+ )
2844
+ agent_name_for_session = a2a_context.get("agent_name_for_session")
2845
+ user_id_for_session = a2a_context.get("user_id_for_session")
2846
+
2847
+ if (
2848
+ temp_session_id_to_delete
2849
+ and agent_name_for_session
2850
+ and user_id_for_session
2851
+ ):
2852
+ log.info(
2853
+ "%s Cleaning up RUN_BASED session (app: %s, user: %s, id: %s) from shared service for task_id='%s'",
2854
+ log_id,
2855
+ agent_name_for_session,
2856
+ user_id_for_session,
2857
+ temp_session_id_to_delete,
2858
+ logical_task_id,
2859
+ )
2860
+ try:
2861
+ if self.session_service:
2862
+ await self.session_service.delete_session(
2863
+ app_name=agent_name_for_session,
2864
+ user_id=user_id_for_session,
2865
+ session_id=temp_session_id_to_delete,
2866
+ )
2867
+ else:
2868
+ log.error(
2869
+ "%s self.session_service is None, cannot delete RUN_BASED session %s.",
2870
+ log_id,
2871
+ temp_session_id_to_delete,
2872
+ )
2873
+ except AttributeError:
2874
+ log.error(
2875
+ "%s self.session_service does not support 'delete_session'. Cleanup for RUN_BASED session (app: %s, user: %s, id: %s) skipped.",
2876
+ log_id,
2877
+ agent_name_for_session,
2878
+ user_id_for_session,
2879
+ temp_session_id_to_delete,
2880
+ )
2881
+ except Exception as e_cleanup:
2882
+ log.error(
2883
+ "%s Error cleaning up RUN_BASED session (app: %s, user: %s, id: %s) from shared service: %s",
2884
+ log_id,
2885
+ agent_name_for_session,
2886
+ user_id_for_session,
2887
+ temp_session_id_to_delete,
2888
+ e_cleanup,
2889
+ exc_info=True,
2890
+ )
2891
+ else:
2892
+ log.warning(
2893
+ "%s Could not clean up RUN_BASED session for task %s due to missing context (id_to_delete: %s, agent_name: %s, user_id: %s).",
2894
+ log_id,
2895
+ logical_task_id,
2896
+ temp_session_id_to_delete,
2897
+ agent_name_for_session,
2898
+ user_id_for_session,
2899
+ )
2900
+
2901
+ with self.active_tasks_lock:
2902
+ removed_task_context = self.active_tasks.pop(logical_task_id, None)
2903
+ if removed_task_context:
2904
+ log.debug(
2905
+ "%s Removed TaskExecutionContext for task %s.",
2906
+ log_id,
2907
+ logical_task_id,
2908
+ )
2909
+ else:
2910
+ log.warning(
2911
+ "%s TaskExecutionContext for task %s was already removed.",
2912
+ log_id,
2913
+ logical_task_id,
2914
+ )
2915
+ else:
2916
+ log.info(
2917
+ "%s Task %s is paused for a long-running tool. Skipping all cleanup.",
2918
+ log_id,
2919
+ logical_task_id,
2920
+ )
2921
+
2922
+ log.info(
2923
+ "%s Finalization and cleanup complete for task %s.",
2924
+ log_id,
2925
+ logical_task_id,
2926
+ )
2927
+
2928
+ def _resolve_instruction_provider(
2929
+ self, config_value: Any
2930
+ ) -> Union[str, InstructionProvider]:
2931
+ """Resolves instruction config using helper."""
2932
+ return resolve_instruction_provider(self, config_value)
2933
+
2934
+ def _get_a2a_base_topic(self) -> str:
2935
+ """Returns the base topic prefix using helper."""
2936
+ return get_a2a_base_topic(self.namespace)
2937
+
2938
+ def _get_discovery_topic(self) -> str:
2939
+ """Returns the discovery topic using helper."""
2940
+ return get_discovery_topic(self.namespace)
2941
+
2942
+ def _get_agent_request_topic(self, agent_id: str) -> str:
2943
+ """Returns the agent request topic using helper."""
2944
+ return get_agent_request_topic(self.namespace, agent_id)
2945
+
2946
+ def _get_agent_response_topic(
2947
+ self, delegating_agent_name: str, sub_task_id: str
2948
+ ) -> str:
2949
+ """Returns the agent response topic using helper."""
2950
+ return get_agent_response_topic(
2951
+ self.namespace, delegating_agent_name, sub_task_id
2952
+ )
2953
+
2954
+ def _get_peer_agent_status_topic(
2955
+ self, delegating_agent_name: str, sub_task_id: str
2956
+ ) -> str:
2957
+ """Returns the peer agent status topic using helper."""
2958
+ return get_peer_agent_status_topic(
2959
+ self.namespace, delegating_agent_name, sub_task_id
2960
+ )
2961
+
2962
+ def _get_client_response_topic(self, client_id: str) -> str:
2963
+ """Returns the client response topic using helper."""
2964
+ return get_client_response_topic(self.namespace, client_id)
2965
+
2966
+ def _publish_a2a_message(
2967
+ self, payload: Dict, topic: str, user_properties: Optional[Dict] = None
2968
+ ):
2969
+ """Helper to publish A2A messages via the SAC App."""
2970
+ try:
2971
+ app = self.get_app()
2972
+ if app:
2973
+ if self.invocation_monitor:
2974
+ self.invocation_monitor.log_message_event(
2975
+ direction="PUBLISHED",
2976
+ topic=topic,
2977
+ payload=payload,
2978
+ component_identifier=self.log_identifier,
2979
+ )
2980
+ app.send_message(
2981
+ payload=payload, topic=topic, user_properties=user_properties
2982
+ )
2983
+ else:
2984
+ log.error(
2985
+ "%s Cannot publish message: Not running within a SAC App context.",
2986
+ self.log_identifier,
2987
+ )
2988
+ except Exception as e:
2989
+ log.exception(
2990
+ "%s Failed to publish A2A message to topic %s: %s",
2991
+ self.log_identifier,
2992
+ topic,
2993
+ e,
2994
+ )
2995
+ raise
2996
+
2997
+ def _publish_a2a_event(self, payload: Dict, topic: str, a2a_context: Dict):
2998
+ """
2999
+ Centralized helper to publish an A2A event, ensuring user properties
3000
+ are consistently attached from the a2a_context.
3001
+ """
3002
+ user_properties = {}
3003
+ if a2a_context.get("a2a_user_config"):
3004
+ user_properties["a2aUserConfig"] = a2a_context["a2a_user_config"]
3005
+
3006
+ self._publish_a2a_message(payload, topic, user_properties)
3007
+
3008
+ def submit_a2a_task(
3009
+ self,
3010
+ target_agent_name: str,
3011
+ a2a_message: A2AMessage,
3012
+ original_session_id: str,
3013
+ main_logical_task_id: str,
3014
+ user_id: str,
3015
+ user_config: Dict[str, Any],
3016
+ sub_task_id: str,
3017
+ function_call_id: Optional[str] = None,
3018
+ ) -> str:
3019
+ """
3020
+ Submits a task to a peer agent in a non-blocking way.
3021
+ Returns the sub_task_id for correlation.
3022
+ """
3023
+ log_identifier_helper = (
3024
+ f"{self.log_identifier}[SubmitA2ATask:{target_agent_name}]"
3025
+ )
3026
+ log.debug(
3027
+ "%s Submitting non-blocking task for main task %s",
3028
+ log_identifier_helper,
3029
+ main_logical_task_id,
3030
+ )
3031
+
3032
+ peer_request_topic = self._get_agent_request_topic(target_agent_name)
3033
+
3034
+ a2a_request_params = {
3035
+ "id": sub_task_id,
3036
+ "sessionId": original_session_id,
3037
+ "message": a2a_message.model_dump(exclude_none=True),
3038
+ "metadata": {
3039
+ "sessionBehavior": "RUN_BASED",
3040
+ "parentTaskId": main_logical_task_id,
3041
+ "function_call_id": function_call_id,
3042
+ },
3043
+ }
3044
+ a2a_request = SendTaskRequest(params=a2a_request_params)
3045
+
3046
+ delegating_agent_name = self.get_config("agent_name")
3047
+ reply_to_topic = self._get_agent_response_topic(
3048
+ delegating_agent_name=delegating_agent_name,
3049
+ sub_task_id=sub_task_id,
3050
+ )
3051
+ status_topic = self._get_peer_agent_status_topic(
3052
+ delegating_agent_name=delegating_agent_name,
3053
+ sub_task_id=sub_task_id,
3054
+ )
3055
+
3056
+ user_properties = {
3057
+ "replyTo": reply_to_topic,
3058
+ "a2aStatusTopic": status_topic,
3059
+ "userId": user_id,
3060
+ }
3061
+ if isinstance(user_config, dict):
3062
+ user_properties["a2aUserConfig"] = user_config
3063
+
3064
+ self._publish_a2a_message(
3065
+ payload=a2a_request.model_dump(exclude_none=True),
3066
+ topic=peer_request_topic,
3067
+ user_properties=user_properties,
3068
+ )
3069
+ log.info(
3070
+ "%s Published delegation request to %s (Sub-Task ID: %s, ReplyTo: %s, StatusTo: %s)",
3071
+ log_identifier_helper,
3072
+ peer_request_topic,
3073
+ sub_task_id,
3074
+ reply_to_topic,
3075
+ status_topic,
3076
+ )
3077
+
3078
+ return sub_task_id
3079
+
3080
+ def _handle_scheduled_task_completion(
3081
+ self, future: concurrent.futures.Future, event_type_for_log: EventType
3082
+ ):
3083
+ """Callback to handle completion of futures from run_coroutine_threadsafe."""
3084
+ try:
3085
+ if future.cancelled():
3086
+ log.warning(
3087
+ "%s Coroutine for event type %s (scheduled via run_coroutine_threadsafe) was cancelled.",
3088
+ self.log_identifier,
3089
+ event_type_for_log,
3090
+ )
3091
+ elif future.done() and future.exception() is not None:
3092
+ exception = future.exception()
3093
+ log.error(
3094
+ "%s Coroutine for event type %s (scheduled via run_coroutine_threadsafe) failed with exception: %s",
3095
+ self.log_identifier,
3096
+ event_type_for_log,
3097
+ exception,
3098
+ exc_info=exception,
3099
+ )
3100
+ else:
3101
+ pass
3102
+ except Exception as e:
3103
+ log.error(
3104
+ "%s Error during _handle_scheduled_task_completion (for run_coroutine_threadsafe future) for event type %s: %s",
3105
+ self.log_identifier,
3106
+ event_type_for_log,
3107
+ e,
3108
+ exc_info=e,
3109
+ )
3110
+
3111
+ def _start_async_loop(self):
3112
+ """Target method for the dedicated async thread."""
3113
+ log.info("%s Dedicated async thread started.", self.log_identifier)
3114
+ try:
3115
+ asyncio.set_event_loop(self._async_loop)
3116
+ self._async_loop.run_forever()
3117
+ except Exception as e:
3118
+ log.exception(
3119
+ "%s Exception in dedicated async thread loop: %s",
3120
+ self.log_identifier,
3121
+ e,
3122
+ )
3123
+ if self._async_init_future and not self._async_init_future.done():
3124
+ self._async_init_future.set_exception(e)
3125
+ finally:
3126
+ log.info("%s Dedicated async thread loop finishing.", self.log_identifier)
3127
+ if self._async_loop.is_running():
3128
+ self._async_loop.call_soon_threadsafe(self._async_loop.stop)
3129
+
3130
+ async def _perform_async_init(self):
3131
+ """Coroutine executed on the dedicated loop to perform async initialization."""
3132
+ try:
3133
+ log.info(
3134
+ "%s Loading tools asynchronously in dedicated thread...",
3135
+ self.log_identifier,
3136
+ )
3137
+ loaded_tools, enabled_builtin_tools = await load_adk_tools(self)
3138
+ log.info(
3139
+ "%s Initializing ADK Agent/Runner asynchronously in dedicated thread...",
3140
+ self.log_identifier,
3141
+ )
3142
+ self.adk_agent = initialize_adk_agent(
3143
+ self, loaded_tools, enabled_builtin_tools
3144
+ )
3145
+ self.runner = initialize_adk_runner(self)
3146
+
3147
+ log.info("%s Populating agent card tool manifest...", self.log_identifier)
3148
+ tool_manifest = []
3149
+ for tool in loaded_tools:
3150
+ if isinstance(tool, MCPToolset):
3151
+ try:
3152
+ log.debug(
3153
+ "%s Retrieving tools from MCPToolset for Agent %s...",
3154
+ self.log_identifier,
3155
+ self.agent_name,
3156
+ )
3157
+ mcp_tools = await tool.get_tools()
3158
+ except Exception as e:
3159
+ log.error(
3160
+ "%s Error retrieving tools from MCPToolset for Agent Card %s: %s",
3161
+ self.log_identifier,
3162
+ self.agent_name,
3163
+ e,
3164
+ )
3165
+ continue
3166
+ for mcp_tool in mcp_tools:
3167
+ tool_manifest.append(
3168
+ {
3169
+ "id": mcp_tool.name,
3170
+ "name": mcp_tool.name,
3171
+ "description": mcp_tool.description
3172
+ or "No description available.",
3173
+ }
3174
+ )
3175
+ else:
3176
+ tool_name = getattr(tool, "name", getattr(tool, "__name__", None))
3177
+ if tool_name is not None:
3178
+ tool_manifest.append(
3179
+ {
3180
+ "id": tool_name,
3181
+ "name": tool_name,
3182
+ "description": getattr(
3183
+ tool, "description", getattr(tool, "__doc__", None)
3184
+ )
3185
+ or "No description available.",
3186
+ }
3187
+ )
3188
+
3189
+ self.agent_card_tool_manifest = tool_manifest
3190
+ log.info(
3191
+ "%s Agent card tool manifest populated with %d tools.",
3192
+ self.log_identifier,
3193
+ len(self.agent_card_tool_manifest),
3194
+ )
3195
+
3196
+ log.info(
3197
+ "%s Async initialization steps complete in dedicated thread.",
3198
+ self.log_identifier,
3199
+ )
3200
+ if self._async_init_future and not self._async_init_future.done():
3201
+ log.info(
3202
+ "%s _perform_async_init: Signaling success to main thread.",
3203
+ self.log_identifier,
3204
+ )
3205
+ self._async_loop.call_soon_threadsafe(
3206
+ self._async_init_future.set_result, True
3207
+ )
3208
+ else:
3209
+ log.warning(
3210
+ "%s _perform_async_init: _async_init_future is None or already done before signaling success.",
3211
+ self.log_identifier,
3212
+ )
3213
+ except Exception as e:
3214
+ log.exception(
3215
+ "%s _perform_async_init: Error during async initialization in dedicated thread: %s",
3216
+ self.log_identifier,
3217
+ e,
3218
+ )
3219
+ if self._async_init_future and not self._async_init_future.done():
3220
+ log.error(
3221
+ "%s _perform_async_init: Signaling failure to main thread.",
3222
+ self.log_identifier,
3223
+ )
3224
+ self._async_loop.call_soon_threadsafe(
3225
+ self._async_init_future.set_exception, e
3226
+ )
3227
+ else:
3228
+ log.warning(
3229
+ "%s _perform_async_init: _async_init_future is None or already done before signaling failure.",
3230
+ self.log_identifier,
3231
+ )
3232
+
3233
+ def cleanup(self):
3234
+ """Clean up resources on component shutdown."""
3235
+ log.info("%s Cleaning up A2A ADK Host Component.", self.log_identifier)
3236
+ self.cancel_timer(self._card_publish_timer_id)
3237
+
3238
+ cleanup_func_details = self.get_config("agent_cleanup_function")
3239
+ if cleanup_func_details and isinstance(cleanup_func_details, dict):
3240
+ module_name = cleanup_func_details.get("module")
3241
+ func_name = cleanup_func_details.get("name")
3242
+ base_path = cleanup_func_details.get("base_path")
3243
+
3244
+ if module_name and func_name:
3245
+ log.info(
3246
+ "%s Attempting to load and execute cleanup_function: %s.%s",
3247
+ self.log_identifier,
3248
+ module_name,
3249
+ func_name,
3250
+ )
3251
+ try:
3252
+ module = import_module(module_name, base_path=base_path)
3253
+ cleanup_function = getattr(module, func_name)
3254
+
3255
+ if not callable(cleanup_function):
3256
+ log.error(
3257
+ "%s Cleanup function '%s' in module '%s' is not callable. Skipping.",
3258
+ self.log_identifier,
3259
+ func_name,
3260
+ module_name,
3261
+ )
3262
+ else:
3263
+ cleanup_function(self)
3264
+ log.info(
3265
+ "%s Successfully executed cleanup_function: %s.%s",
3266
+ self.log_identifier,
3267
+ module_name,
3268
+ func_name,
3269
+ )
3270
+ except Exception as e:
3271
+ log.exception(
3272
+ "%s Error during agent cleanup via cleanup_function '%s.%s': %s",
3273
+ self.log_identifier,
3274
+ module_name,
3275
+ func_name,
3276
+ e,
3277
+ )
3278
+ if self.invocation_monitor:
3279
+ try:
3280
+ self.invocation_monitor.cleanup()
3281
+ except Exception as im_clean_e:
3282
+ log.error(
3283
+ "%s Error during InvocationMonitor cleanup: %s",
3284
+ self.log_identifier,
3285
+ im_clean_e,
3286
+ )
3287
+
3288
+ if self._async_loop and self._async_loop.is_running():
3289
+ log.info(
3290
+ "%s Performing async cleanup via dedicated thread...",
3291
+ self.log_identifier,
3292
+ )
3293
+
3294
+ async def _perform_async_cleanup():
3295
+ log.debug("%s Entering async cleanup coroutine...", self.log_identifier)
3296
+ pass
3297
+
3298
+ try:
3299
+ cleanup_future = asyncio.run_coroutine_threadsafe(
3300
+ _perform_async_cleanup(), self._async_loop
3301
+ )
3302
+ cleanup_future.result(timeout=30)
3303
+ log.info("%s Async cleanup completed.", self.log_identifier)
3304
+ except Exception as e:
3305
+ log.exception(
3306
+ "%s Error during async cleanup: %s", self.log_identifier, e
3307
+ )
3308
+ finally:
3309
+ if self._async_loop and self._async_loop.is_running():
3310
+ log.info(
3311
+ "%s Cleanup: Stopping dedicated async loop...",
3312
+ self.log_identifier,
3313
+ )
3314
+ self._async_loop.call_soon_threadsafe(self._async_loop.stop)
3315
+ else:
3316
+ log.info(
3317
+ "%s Cleanup: Dedicated async loop is None or not running, no need to stop.",
3318
+ self.log_identifier,
3319
+ )
3320
+ if self._async_thread and self._async_thread.is_alive():
3321
+ log.info(
3322
+ "%s Cleanup: Joining dedicated async thread...",
3323
+ self.log_identifier,
3324
+ )
3325
+ self._async_thread.join(timeout=5)
3326
+ if self._async_thread.is_alive():
3327
+ log.warning(
3328
+ "%s Dedicated async thread did not exit cleanly.",
3329
+ self.log_identifier,
3330
+ )
3331
+ log.info(
3332
+ "%s Dedicated async thread stopped and joined.", self.log_identifier
3333
+ )
3334
+ else:
3335
+ log.info(
3336
+ "%s Dedicated async loop not running, skipping async cleanup.",
3337
+ self.log_identifier,
3338
+ )
3339
+
3340
+ with self.active_tasks_lock:
3341
+ if self._async_loop and self._async_loop.is_running():
3342
+ for task_context in self.active_tasks.values():
3343
+ task_context.cancel()
3344
+ self.active_tasks.clear()
3345
+ log.debug("%s Cleared all active tasks.", self.log_identifier)
3346
+
3347
+ super().cleanup()
3348
+ log.info("%s Component cleanup finished.", self.log_identifier)
3349
+
3350
+ def set_agent_specific_state(self, key: str, value: Any):
3351
+ """
3352
+ Sets a key-value pair in the agent-specific state.
3353
+ Intended to be used by the custom init_function.
3354
+ """
3355
+ if not hasattr(self, "agent_specific_state"):
3356
+ self.agent_specific_state = {}
3357
+ self.agent_specific_state[key] = value
3358
+ log.debug("%s Set agent_specific_state['%s']", self.log_identifier, key)
3359
+
3360
+ def get_agent_specific_state(self, key: str, default: Optional[Any] = None) -> Any:
3361
+ """
3362
+ Gets a value from the agent-specific state.
3363
+ Intended to be used by tools and the custom cleanup_function.
3364
+ """
3365
+ if not hasattr(self, "agent_specific_state"):
3366
+ return default
3367
+ return self.agent_specific_state.get(key, default)
3368
+
3369
+ def get_async_loop(self) -> Optional[asyncio.AbstractEventLoop]:
3370
+ """Returns the dedicated asyncio event loop for this component's async tasks."""
3371
+ return self._async_loop
3372
+
3373
+ def set_agent_system_instruction_string(self, instruction_string: str) -> None:
3374
+ """
3375
+ Sets a static string to be injected into the LLM system prompt.
3376
+ Called by the agent's init_function.
3377
+ """
3378
+ if not isinstance(instruction_string, str):
3379
+ log.error(
3380
+ "%s Invalid type for instruction_string: %s. Must be a string.",
3381
+ self.log_identifier,
3382
+ type(instruction_string),
3383
+ )
3384
+ return
3385
+ self._agent_system_instruction_string = instruction_string
3386
+ self._agent_system_instruction_callback = None
3387
+ log.info("%s Static agent system instruction string set.", self.log_identifier)
3388
+
3389
+ def set_agent_system_instruction_callback(
3390
+ self,
3391
+ callback_function: Callable[[CallbackContext, LlmRequest], Optional[str]],
3392
+ ) -> None:
3393
+ """
3394
+ Sets a callback function to dynamically generate system prompt injections.
3395
+ Called by the agent's init_function.
3396
+ """
3397
+ if not callable(callback_function):
3398
+ log.error(
3399
+ "%s Invalid type for callback_function: %s. Must be callable.",
3400
+ self.log_identifier,
3401
+ type(callback_function),
3402
+ )
3403
+ return
3404
+ self._agent_system_instruction_callback = callback_function
3405
+ self._agent_system_instruction_string = None
3406
+ log.info("%s Agent system instruction callback set.", self.log_identifier)
3407
+
3408
+ def get_gateway_id(self) -> str:
3409
+ """
3410
+ Returns a unique identifier for this specific gateway/host instance.
3411
+ For now, using the agent name, but could be made more robust (e.g., hostname + agent name).
3412
+ """
3413
+ return self.agent_name
3414
+
3415
+ async def _resolve_early_embeds_and_handle_signals(
3416
+ self, raw_text: str, a2a_context: Dict
3417
+ ) -> Tuple[str, List[Tuple[int, Any]], str]:
3418
+ """
3419
+ Resolves early-stage embeds in raw text and extracts signals.
3420
+ Returns the resolved text, a list of signals, and any unprocessed tail.
3421
+ This is called by process_and_publish_adk_event.
3422
+ """
3423
+ logical_task_id = a2a_context.get("logical_task_id", "unknown_task")
3424
+ method_context_log_identifier = (
3425
+ f"{self.log_identifier}[ResolveEmbeds:{logical_task_id}]"
3426
+ )
3427
+ log.debug(
3428
+ "%s Resolving early embeds for text (length: %d).",
3429
+ method_context_log_identifier,
3430
+ len(raw_text),
3431
+ )
3432
+
3433
+ original_session_id = a2a_context.get("session_id")
3434
+ user_id = a2a_context.get("user_id")
3435
+ adk_app_name = self.get_config("agent_name")
3436
+
3437
+ if not all([self.artifact_service, original_session_id, user_id, adk_app_name]):
3438
+ log.error(
3439
+ "%s Missing necessary context for embed resolution (artifact_service, session_id, user_id, or adk_app_name). Skipping.",
3440
+ method_context_log_identifier,
3441
+ )
3442
+ return (
3443
+ raw_text,
3444
+ [],
3445
+ "",
3446
+ )
3447
+ context_for_embeds = {
3448
+ "artifact_service": self.artifact_service,
3449
+ "session_context": {
3450
+ "app_name": adk_app_name,
3451
+ "user_id": user_id,
3452
+ "session_id": original_session_id,
3453
+ },
3454
+ "config": {
3455
+ "gateway_max_artifact_resolve_size_bytes": self.get_config(
3456
+ "tool_output_llm_return_max_bytes", 4096
3457
+ ),
3458
+ "gateway_recursive_embed_depth": self.get_config(
3459
+ "gateway_recursive_embed_depth", 12
3460
+ ),
3461
+ },
3462
+ }
3463
+
3464
+ resolver_config = context_for_embeds["config"]
3465
+
3466
+ try:
3467
+ from ...common.utils.embeds.resolver import (
3468
+ resolve_embeds_in_string,
3469
+ evaluate_embed,
3470
+ )
3471
+ from ...common.utils.embeds.constants import EARLY_EMBED_TYPES
3472
+
3473
+ resolved_text, processed_until_index, signals_found = (
3474
+ await resolve_embeds_in_string(
3475
+ text=raw_text,
3476
+ context=context_for_embeds,
3477
+ resolver_func=evaluate_embed,
3478
+ types_to_resolve=EARLY_EMBED_TYPES,
3479
+ log_identifier=method_context_log_identifier,
3480
+ config=resolver_config,
3481
+ )
3482
+ )
3483
+ unprocessed_tail = raw_text[processed_until_index:]
3484
+ log.debug(
3485
+ "%s Embed resolution complete. Resolved text: '%s...', Signals found: %d, Unprocessed tail: '%s...'",
3486
+ method_context_log_identifier,
3487
+ resolved_text[:100],
3488
+ len(signals_found),
3489
+ unprocessed_tail[:100],
3490
+ )
3491
+ return resolved_text, signals_found, unprocessed_tail
3492
+ except Exception as e:
3493
+ log.exception(
3494
+ "%s Error during embed resolution: %s", method_context_log_identifier, e
3495
+ )
3496
+ return raw_text, [], ""