solace-agent-mesh 1.0.9__py3-none-any.whl → 1.3.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (220) hide show
  1. solace_agent_mesh/agent/adk/adk_llm.txt +182 -42
  2. solace_agent_mesh/agent/adk/artifacts/artifacts_llm.txt +171 -0
  3. solace_agent_mesh/agent/adk/callbacks.py +165 -104
  4. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +0 -18
  5. solace_agent_mesh/agent/adk/models/models_llm.txt +104 -55
  6. solace_agent_mesh/agent/adk/runner.py +25 -17
  7. solace_agent_mesh/agent/adk/services.py +3 -3
  8. solace_agent_mesh/agent/adk/setup.py +11 -0
  9. solace_agent_mesh/agent/adk/stream_parser.py +8 -1
  10. solace_agent_mesh/agent/adk/tool_wrapper.py +10 -3
  11. solace_agent_mesh/agent/agent_llm.txt +355 -18
  12. solace_agent_mesh/agent/protocol/event_handlers.py +460 -317
  13. solace_agent_mesh/agent/protocol/protocol_llm.txt +54 -7
  14. solace_agent_mesh/agent/sac/app.py +2 -2
  15. solace_agent_mesh/agent/sac/component.py +211 -517
  16. solace_agent_mesh/agent/sac/sac_llm.txt +133 -63
  17. solace_agent_mesh/agent/testing/testing_llm.txt +25 -58
  18. solace_agent_mesh/agent/tools/peer_agent_tool.py +15 -11
  19. solace_agent_mesh/agent/tools/tools_llm.txt +234 -69
  20. solace_agent_mesh/agent/utils/artifact_helpers.py +35 -1
  21. solace_agent_mesh/agent/utils/utils_llm.txt +90 -105
  22. solace_agent_mesh/assets/docs/404.html +3 -3
  23. solace_agent_mesh/assets/docs/assets/js/6e0db977.39a79ca9.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/{75384d09.ccd480c4.js → 75384d09.bf78fbdb.js} +1 -1
  25. solace_agent_mesh/assets/docs/assets/js/90dd9cf6.88f385ea.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/f284c35a.fb68323a.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/main.08d30374.js +2 -0
  28. solace_agent_mesh/assets/docs/assets/js/runtime~main.458efb1d.js +1 -0
  29. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +4 -4
  30. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +4 -4
  31. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +4 -4
  32. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +4 -4
  33. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +4 -4
  34. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +4 -4
  35. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
  36. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +4 -4
  37. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
  38. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +4 -4
  39. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +4 -4
  40. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/migration-guides/a2a-upgrade-to-0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +105 -0
  44. solace_agent_mesh/assets/docs/docs/documentation/migration-guides/a2a-upgrade-to-0.3.0/a2a-technical-migration-map/index.html +53 -0
  45. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +4 -4
  51. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +4 -4
  52. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +4 -4
  53. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  54. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +4 -4
  55. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +4 -4
  56. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
  58. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +4 -4
  59. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +4 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
  61. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +4 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
  64. solace_agent_mesh/assets/docs/lunr-index-1757433031159.json +1 -0
  65. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  66. solace_agent_mesh/assets/docs/search-doc-1757433031159.json +1 -0
  67. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  68. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  69. solace_agent_mesh/cli/__init__.py +1 -1
  70. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +125 -48
  71. solace_agent_mesh/cli/commands/eval_cmd.py +14 -0
  72. solace_agent_mesh/cli/commands/init_cmd/__init__.py +53 -31
  73. solace_agent_mesh/cli/commands/init_cmd/database_step.py +91 -0
  74. solace_agent_mesh/cli/commands/init_cmd/env_step.py +19 -8
  75. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +80 -25
  76. solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +32 -10
  77. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +74 -15
  78. solace_agent_mesh/cli/commands/plugin_cmd/create_cmd.py +0 -2
  79. solace_agent_mesh/cli/commands/run_cmd.py +5 -3
  80. solace_agent_mesh/cli/utils.py +68 -12
  81. solace_agent_mesh/client/webui/frontend/static/assets/authCallback-vY5eu2lI.js +1 -0
  82. solace_agent_mesh/client/webui/frontend/static/assets/client-BeBkzgWW.js +25 -0
  83. solace_agent_mesh/client/webui/frontend/static/assets/main-Bjys1KQs.js +339 -0
  84. solace_agent_mesh/client/webui/frontend/static/assets/main-C03yrETa.css +1 -0
  85. solace_agent_mesh/client/webui/frontend/static/assets/vendor-CE0AeXyK.js +395 -0
  86. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -2
  87. solace_agent_mesh/client/webui/frontend/static/index.html +4 -3
  88. solace_agent_mesh/common/a2a/__init__.py +213 -0
  89. solace_agent_mesh/common/a2a/a2a_llm.txt +182 -0
  90. solace_agent_mesh/common/a2a/artifact.py +328 -0
  91. solace_agent_mesh/common/a2a/events.py +183 -0
  92. solace_agent_mesh/common/a2a/message.py +307 -0
  93. solace_agent_mesh/common/a2a/protocol.py +513 -0
  94. solace_agent_mesh/common/a2a/task.py +127 -0
  95. solace_agent_mesh/common/a2a/translation.py +653 -0
  96. solace_agent_mesh/common/a2a/types.py +54 -0
  97. solace_agent_mesh/common/a2a_spec/a2a.json +2576 -0
  98. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +407 -0
  99. solace_agent_mesh/common/a2a_spec/schemas/agent_progress_update.json +18 -0
  100. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +31 -0
  101. solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +18 -0
  102. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +235 -0
  103. solace_agent_mesh/common/a2a_spec/schemas/tool_invocation_start.json +26 -0
  104. solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +25 -0
  105. solace_agent_mesh/common/agent_registry.py +1 -1
  106. solace_agent_mesh/common/common_llm.txt +192 -70
  107. solace_agent_mesh/common/data_parts.py +99 -0
  108. solace_agent_mesh/common/middleware/middleware_llm.txt +17 -17
  109. solace_agent_mesh/common/sac/__init__.py +0 -0
  110. solace_agent_mesh/common/sac/sac_llm.txt +71 -0
  111. solace_agent_mesh/common/sac/sam_component_base.py +252 -0
  112. solace_agent_mesh/common/services/providers/providers_llm.txt +51 -84
  113. solace_agent_mesh/common/services/services_llm.txt +206 -26
  114. solace_agent_mesh/common/utils/artifact_utils.py +29 -0
  115. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +176 -80
  116. solace_agent_mesh/common/utils/embeds/resolver.py +1 -0
  117. solace_agent_mesh/common/utils/utils_llm.txt +323 -42
  118. solace_agent_mesh/config_portal/backend/common.py +2 -2
  119. solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +1 -1
  120. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-bFMKlzKf.js +98 -0
  121. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-d845808d.js → manifest-89db7c30.js} +1 -1
  122. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  123. solace_agent_mesh/core_a2a/core_a2a_llm.txt +10 -8
  124. solace_agent_mesh/core_a2a/service.py +20 -44
  125. solace_agent_mesh/evaluation/message_organizer.py +35 -56
  126. solace_agent_mesh/evaluation/run.py +26 -5
  127. solace_agent_mesh/evaluation/subscriber.py +35 -10
  128. solace_agent_mesh/evaluation/summary_builder.py +27 -34
  129. solace_agent_mesh/gateway/base/app.py +27 -1
  130. solace_agent_mesh/gateway/base/base_llm.txt +177 -72
  131. solace_agent_mesh/gateway/base/component.py +294 -523
  132. solace_agent_mesh/gateway/gateway_llm.txt +299 -58
  133. solace_agent_mesh/gateway/http_sse/ARCHITECTURE_GUIDE.md +676 -0
  134. solace_agent_mesh/gateway/http_sse/alembic/env.py +85 -0
  135. solace_agent_mesh/gateway/http_sse/alembic/script.py.mako +28 -0
  136. solace_agent_mesh/gateway/http_sse/alembic/versions/b1c2d3e4f5g6_add_database_indexes.py +83 -0
  137. solace_agent_mesh/gateway/http_sse/alembic/versions/d5b3f8f2e9a0_create_initial_database.py +58 -0
  138. solace_agent_mesh/gateway/http_sse/alembic.ini +147 -0
  139. solace_agent_mesh/gateway/http_sse/api/__init__.py +11 -0
  140. solace_agent_mesh/gateway/http_sse/api/controllers/__init__.py +9 -0
  141. solace_agent_mesh/gateway/http_sse/api/controllers/session_controller.py +355 -0
  142. solace_agent_mesh/gateway/http_sse/api/controllers/task_controller.py +279 -0
  143. solace_agent_mesh/gateway/http_sse/api/controllers/user_controller.py +35 -0
  144. solace_agent_mesh/gateway/http_sse/api/dto/__init__.py +10 -0
  145. solace_agent_mesh/gateway/http_sse/api/dto/requests/__init__.py +37 -0
  146. solace_agent_mesh/gateway/http_sse/api/dto/requests/session_requests.py +49 -0
  147. solace_agent_mesh/gateway/http_sse/api/dto/requests/task_requests.py +66 -0
  148. solace_agent_mesh/gateway/http_sse/api/dto/responses/__init__.py +43 -0
  149. solace_agent_mesh/gateway/http_sse/api/dto/responses/session_responses.py +68 -0
  150. solace_agent_mesh/gateway/http_sse/api/dto/responses/task_responses.py +74 -0
  151. solace_agent_mesh/gateway/http_sse/app.py +31 -1
  152. solace_agent_mesh/gateway/http_sse/application/__init__.py +3 -0
  153. solace_agent_mesh/gateway/http_sse/application/services/__init__.py +3 -0
  154. solace_agent_mesh/gateway/http_sse/application/services/session_service.py +135 -0
  155. solace_agent_mesh/gateway/http_sse/component.py +371 -236
  156. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +29 -29
  157. solace_agent_mesh/gateway/http_sse/dependencies.py +142 -39
  158. solace_agent_mesh/gateway/http_sse/domain/entities/__init__.py +3 -0
  159. solace_agent_mesh/gateway/http_sse/domain/entities/session.py +90 -0
  160. solace_agent_mesh/gateway/http_sse/domain/repositories/__init__.py +3 -0
  161. solace_agent_mesh/gateway/http_sse/domain/repositories/session_repository.py +54 -0
  162. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +272 -36
  163. solace_agent_mesh/gateway/http_sse/infrastructure/__init__.py +4 -0
  164. solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/__init__.py +3 -0
  165. solace_agent_mesh/gateway/http_sse/infrastructure/dependency_injection/container.py +123 -0
  166. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/__init__.py +4 -0
  167. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_persistence_service.py +16 -0
  168. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/database_service.py +119 -0
  169. solace_agent_mesh/gateway/http_sse/infrastructure/persistence/models.py +31 -0
  170. solace_agent_mesh/gateway/http_sse/infrastructure/persistence_service.py +12 -0
  171. solace_agent_mesh/gateway/http_sse/infrastructure/repositories/__init__.py +3 -0
  172. solace_agent_mesh/gateway/http_sse/infrastructure/repositories/session_repository.py +174 -0
  173. solace_agent_mesh/gateway/http_sse/main.py +293 -91
  174. solace_agent_mesh/gateway/http_sse/routers/agents.py +1 -1
  175. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +137 -56
  176. solace_agent_mesh/gateway/http_sse/routers/config.py +3 -1
  177. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +231 -5
  178. solace_agent_mesh/gateway/http_sse/routers/tasks.py +199 -171
  179. solace_agent_mesh/gateway/http_sse/routers/visualization.py +7 -7
  180. solace_agent_mesh/gateway/http_sse/services/agent_service.py +1 -1
  181. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +89 -135
  182. solace_agent_mesh/gateway/http_sse/services/task_service.py +2 -5
  183. solace_agent_mesh/gateway/http_sse/session_manager.py +64 -30
  184. solace_agent_mesh/gateway/http_sse/shared/__init__.py +9 -0
  185. solace_agent_mesh/gateway/http_sse/shared/auth_utils.py +29 -0
  186. solace_agent_mesh/gateway/http_sse/shared/enums.py +45 -0
  187. solace_agent_mesh/gateway/http_sse/shared/types.py +45 -0
  188. solace_agent_mesh/solace_agent_mesh_llm.txt +362 -0
  189. solace_agent_mesh/templates/gateway_component_template.py +149 -98
  190. solace_agent_mesh/templates/shared_config.yaml +4 -5
  191. solace_agent_mesh/templates/webui.yaml +8 -10
  192. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/METADATA +9 -6
  193. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/RECORD +197 -141
  194. solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +0 -1
  195. solace_agent_mesh/assets/docs/assets/js/main.3d0e7879.js +0 -2
  196. solace_agent_mesh/assets/docs/assets/js/runtime~main.05d19492.js +0 -1
  197. solace_agent_mesh/assets/docs/lunr-index-1757091012487.json +0 -1
  198. solace_agent_mesh/assets/docs/search-doc-1757091012487.json +0 -1
  199. solace_agent_mesh/client/webui/frontend/static/assets/authCallback-BmF2l6vg.js +0 -1
  200. solace_agent_mesh/client/webui/frontend/static/assets/client-D881Dttc.js +0 -49
  201. solace_agent_mesh/client/webui/frontend/static/assets/main-D0FnP_W4.css +0 -1
  202. solace_agent_mesh/client/webui/frontend/static/assets/main-Do32sFPX.js +0 -708
  203. solace_agent_mesh/common/a2a_protocol.py +0 -564
  204. solace_agent_mesh/common/client/__init__.py +0 -4
  205. solace_agent_mesh/common/client/card_resolver.py +0 -21
  206. solace_agent_mesh/common/client/client.py +0 -85
  207. solace_agent_mesh/common/client/client_llm.txt +0 -133
  208. solace_agent_mesh/common/server/__init__.py +0 -4
  209. solace_agent_mesh/common/server/server.py +0 -122
  210. solace_agent_mesh/common/server/server_llm.txt +0 -169
  211. solace_agent_mesh/common/server/task_manager.py +0 -291
  212. solace_agent_mesh/common/server/utils.py +0 -28
  213. solace_agent_mesh/common/types.py +0 -411
  214. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-Bym6YkMd.js +0 -98
  215. solace_agent_mesh/gateway/http_sse/routers/sessions.py +0 -80
  216. solace_agent_mesh/gateway/http_sse/routers/users.py +0 -59
  217. /solace_agent_mesh/assets/docs/assets/js/{main.3d0e7879.js.LICENSE.txt → main.08d30374.js.LICENSE.txt} +0 -0
  218. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/WHEEL +0 -0
  219. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/entry_points.txt +0 -0
  220. {solace_agent_mesh-1.0.9.dist-info → solace_agent_mesh-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,50 +3,39 @@ Contains event handling logic for the A2A_ADK_HostComponent.
3
3
  """
4
4
 
5
5
  import json
6
- import yaml
7
6
  import asyncio
8
- from typing import Union, TYPE_CHECKING, List, Dict, Any
7
+ from typing import TYPE_CHECKING, Dict, Any
9
8
  import fnmatch
10
9
  from solace_ai_connector.common.log import log
11
10
  from solace_ai_connector.common.message import Message as SolaceMessage
11
+ from ...agent.adk.callbacks import _publish_data_part_status_update
12
+ from ...common.data_parts import ToolResultData
13
+ from ...common.a2a.types import ToolsExtensionParams
12
14
  from solace_ai_connector.common.event import Event, EventType
13
- from ...common.types import (
14
- Message as A2AMessage,
15
- SendTaskRequest,
16
- SendTaskStreamingRequest,
17
- CancelTaskRequest,
18
- GetTaskRequest,
19
- SetTaskPushNotificationRequest,
20
- GetTaskPushNotificationRequest,
21
- TaskResubscriptionRequest,
22
- TaskIdParams,
23
- JSONParseError,
24
- InvalidRequestError,
25
- InternalError,
26
- JSONRPCResponse,
15
+ from a2a.types import (
16
+ A2ARequest,
27
17
  AgentCard,
28
18
  AgentCapabilities,
19
+ AgentExtension,
20
+ DataPart,
21
+ JSONRPCResponse,
29
22
  Task,
30
- TaskStatusUpdateEvent,
31
23
  TaskArtifactUpdateEvent,
32
- TaskStatus,
33
- TaskState,
34
- DataPart,
24
+ TaskStatusUpdateEvent,
35
25
  TextPart,
36
- A2ARequest,
37
26
  )
38
- from ...common.a2a_protocol import (
27
+ from ...common import a2a
28
+ from ...common.a2a import (
39
29
  get_agent_request_topic,
40
30
  get_discovery_topic,
41
31
  translate_a2a_to_adk_content,
42
32
  get_client_response_topic,
43
33
  get_agent_response_subscription_topic,
44
34
  get_agent_status_subscription_topic,
45
- _extract_text_from_parts,
35
+ get_text_from_message,
46
36
  )
47
37
  from ...agent.utils.artifact_helpers import (
48
38
  generate_artifact_metadata_summary,
49
- load_artifact_content_or_metadata,
50
39
  )
51
40
  from ...agent.adk.runner import run_adk_async_task_thread_wrapper
52
41
  from ..sac.task_execution_context import TaskExecutionContext
@@ -55,10 +44,6 @@ from google.adk.agents import RunConfig
55
44
  if TYPE_CHECKING:
56
45
  from ..sac.component import SamAgentComponent
57
46
  from google.adk.agents.run_config import StreamingMode
58
- from google.adk.events import Event as ADKEvent
59
- from google.genai import types as adk_types
60
-
61
-
62
47
 
63
48
 
64
49
  def _register_peer_artifacts_in_parent_context(
@@ -119,7 +104,9 @@ async def process_event(component, event: Event):
119
104
  )
120
105
  else:
121
106
  log.warning(
122
- f"{component.log_identifier} InvocationMonitor not available in component for event on topic {topic}"
107
+ "%s InvocationMonitor not available in component for event on topic %s",
108
+ component.log_identifier,
109
+ topic,
123
110
  )
124
111
  namespace = component.get_config("namespace")
125
112
  agent_name = component.get_config("agent_name")
@@ -186,6 +173,50 @@ async def process_event(component, event: Event):
186
173
  component.handle_error(e, event)
187
174
 
188
175
 
176
+ async def _publish_peer_tool_result_notification(
177
+ component: "SamAgentComponent",
178
+ correlation_data: Dict[str, Any],
179
+ payload_to_queue: Any,
180
+ log_identifier: str,
181
+ ):
182
+ """Publishes a ToolResultData status update for a completed peer tool call."""
183
+ peer_tool_name = correlation_data.get("peer_tool_name")
184
+ function_call_id = correlation_data.get("adk_function_call_id")
185
+ original_task_context_data = correlation_data.get("original_task_context")
186
+
187
+ if not (peer_tool_name and function_call_id and original_task_context_data):
188
+ log.warning(
189
+ "%s Missing data in correlation_data. Cannot publish peer tool result notification.",
190
+ log_identifier,
191
+ )
192
+ return
193
+
194
+ log.info(
195
+ "%s Publishing tool_result notification for completed peer task '%s'.",
196
+ log_identifier,
197
+ peer_tool_name,
198
+ )
199
+ try:
200
+ tool_result_notification = ToolResultData(
201
+ tool_name=peer_tool_name,
202
+ result_data=payload_to_queue,
203
+ function_call_id=function_call_id,
204
+ )
205
+ await _publish_data_part_status_update(
206
+ host_component=component,
207
+ a2a_context=original_task_context_data,
208
+ data_part_model=tool_result_notification,
209
+ )
210
+ except Exception as e:
211
+ log.error(
212
+ "%s Failed to publish peer tool result notification for '%s': %s",
213
+ log_identifier,
214
+ peer_tool_name,
215
+ e,
216
+ exc_info=True,
217
+ )
218
+
219
+
189
220
  async def handle_a2a_request(component, message: SolaceMessage):
190
221
  """
191
222
  Handles an incoming A2A request message.
@@ -198,44 +229,29 @@ async def handle_a2a_request(component, message: SolaceMessage):
198
229
  component.log_identifier,
199
230
  message.get_topic(),
200
231
  )
201
- a2a_context = {}
202
- adk_session = None
203
- jsonrpc_request_id = None
204
- logical_task_id = None
205
- client_id = message.get_user_properties().get("clientId", "default_client")
206
- status_topic_from_peer = message.get_user_properties().get("a2aStatusTopic")
207
- reply_topic_from_peer = message.get_user_properties().get("replyTo")
208
- namespace = component.get_config("namespace")
209
- a2a_user_config = message.get_user_properties().get("a2aUserConfig", {})
210
- if not isinstance(a2a_user_config, dict):
211
- log.warning(
212
- "%s 'a2aUserConfig' user property is not a dictionary, received: %s. Defaulting to empty dict.",
213
- component.log_identifier,
214
- type(a2a_user_config),
215
- )
216
- a2a_user_config = {}
217
- log.debug(
218
- "%s Extracted 'a2aUserConfig': %s",
219
- component.log_identifier,
220
- a2a_user_config,
221
- )
222
232
  try:
223
233
  payload_dict = message.get_payload()
224
234
  if not isinstance(payload_dict, dict):
225
235
  raise ValueError("Payload is not a dictionary.")
226
- jsonrpc_request_id = payload_dict.get("id")
227
- a2a_request: Union[
228
- SendTaskRequest,
229
- SendTaskStreamingRequest,
230
- CancelTaskRequest,
231
- GetTaskRequest,
232
- SetTaskPushNotificationRequest,
233
- GetTaskPushNotificationRequest,
234
- TaskResubscriptionRequest,
235
- ] = A2ARequest.validate_python(payload_dict)
236
- jsonrpc_request_id = a2a_request.id
237
- logical_task_id = a2a_request.params.id
238
- if isinstance(a2a_request, CancelTaskRequest):
236
+ a2a_request: A2ARequest = A2ARequest.model_validate(payload_dict)
237
+ jsonrpc_request_id = a2a.get_request_id(a2a_request)
238
+
239
+ # Extract properties from message user properties
240
+ client_id = message.get_user_properties().get("clientId", "default_client")
241
+ status_topic_from_peer = message.get_user_properties().get("a2aStatusTopic")
242
+ reply_topic_from_peer = message.get_user_properties().get("replyTo")
243
+ namespace = component.get_config("namespace")
244
+ a2a_user_config = message.get_user_properties().get("a2aUserConfig", {})
245
+ if not isinstance(a2a_user_config, dict):
246
+ log.warning("a2aUserConfig is not a dict, using empty dict instead")
247
+ a2a_user_config = {}
248
+
249
+ # The concept of logical_task_id changes. For Cancel, it's in params.id.
250
+ # For Send, we will generate it.
251
+ logical_task_id = None
252
+ method = a2a.get_request_method(a2a_request)
253
+ if method == "tasks/cancel":
254
+ logical_task_id = a2a.get_task_id_from_cancel_request(a2a_request)
239
255
  log.info(
240
256
  "%s Received CancelTaskRequest for Task ID: %s.",
241
257
  component.log_identifier,
@@ -253,28 +269,38 @@ async def handle_a2a_request(component, message: SolaceMessage):
253
269
  logical_task_id,
254
270
  )
255
271
 
256
- peer_sub_tasks = task_context.active_peer_sub_tasks
272
+ peer_sub_tasks = task_context.active_peer_sub_tasks.copy()
257
273
  if peer_sub_tasks:
258
- for sub_task_info in peer_sub_tasks:
259
- sub_task_id = sub_task_info.get("sub_task_id")
274
+ for sub_task_id, sub_task_info in peer_sub_tasks.items():
260
275
  target_peer_agent_name = sub_task_info.get("peer_agent_name")
261
- if sub_task_id and target_peer_agent_name:
276
+ peer_task_id_to_cancel = sub_task_info.get("peer_task_id")
277
+
278
+ if not peer_task_id_to_cancel:
279
+ log.warning(
280
+ "%s Cannot cancel peer sub-task %s for main task %s because the peer's taskId is not yet known.",
281
+ component.log_identifier,
282
+ sub_task_id,
283
+ logical_task_id,
284
+ )
285
+ continue
286
+
287
+ if peer_task_id_to_cancel and target_peer_agent_name:
262
288
  log.info(
263
- "%s Attempting to cancel peer sub-task %s for agent %s (main task %s).",
289
+ "%s Attempting to cancel peer sub-task %s (Peer Task ID: %s) for agent %s (main task %s).",
264
290
  component.log_identifier,
265
291
  sub_task_id,
292
+ peer_task_id_to_cancel,
266
293
  target_peer_agent_name,
267
294
  logical_task_id,
268
295
  )
269
296
  try:
270
- peer_cancel_params = TaskIdParams(id=sub_task_id)
271
- peer_cancel_request = CancelTaskRequest(
272
- params=peer_cancel_params
297
+ peer_cancel_request = a2a.create_cancel_task_request(
298
+ task_id=peer_task_id_to_cancel
273
299
  )
274
300
  peer_cancel_user_props = {
275
301
  "clientId": component.agent_name
276
302
  }
277
- component._publish_a2a_message(
303
+ component.publish_a2a_message(
278
304
  payload=peer_cancel_request.model_dump(
279
305
  exclude_none=True
280
306
  ),
@@ -284,17 +310,17 @@ async def handle_a2a_request(component, message: SolaceMessage):
284
310
  user_properties=peer_cancel_user_props,
285
311
  )
286
312
  log.info(
287
- "%s Sent CancelTaskRequest to peer %s for sub-task %s.",
313
+ "%s Sent CancelTaskRequest to peer %s for its task %s.",
288
314
  component.log_identifier,
289
315
  target_peer_agent_name,
290
- sub_task_id,
316
+ peer_task_id_to_cancel,
291
317
  )
292
318
  except Exception as e_peer_cancel:
293
319
  log.error(
294
- "%s Failed to send CancelTaskRequest to peer %s for sub-task %s: %s",
320
+ "%s Failed to send CancelTaskRequest to peer %s for task %s: %s",
295
321
  component.log_identifier,
296
322
  target_peer_agent_name,
297
- sub_task_id,
323
+ peer_task_id_to_cancel,
298
324
  e_peer_cancel,
299
325
  )
300
326
  else:
@@ -325,10 +351,18 @@ async def handle_a2a_request(component, message: SolaceMessage):
325
351
  ack_e,
326
352
  )
327
353
  return None
328
- elif isinstance(a2a_request, (SendTaskRequest, SendTaskStreamingRequest)):
329
- original_session_id = a2a_request.params.sessionId
330
- task_id = a2a_request.params.id
331
- task_metadata = a2a_request.params.metadata or {}
354
+ elif method in ["message/send", "message/stream"]:
355
+ a2a_message = a2a.get_message_from_send_request(a2a_request)
356
+ if not a2a_message:
357
+ raise ValueError("Could not extract message from SendMessageRequest")
358
+
359
+ # The gateway/client is the source of truth for the task ID.
360
+ # The agent adopts the ID from the JSON-RPC request envelope.
361
+ logical_task_id = str(a2a.get_request_id(a2a_request))
362
+ # The session id is now contextId on the message
363
+ original_session_id = a2a_message.context_id
364
+ message_id = a2a_message.message_id
365
+ task_metadata = a2a_message.metadata or {}
332
366
  system_purpose = task_metadata.get("system_purpose")
333
367
  response_format = task_metadata.get("response_format")
334
368
  session_behavior_from_meta = task_metadata.get("sessionBehavior")
@@ -357,7 +391,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
357
391
  )
358
392
  user_id = message.get_user_properties().get("userId", "default_user")
359
393
  agent_name = component.get_config("agent_name")
360
- is_streaming_request = isinstance(a2a_request, SendTaskStreamingRequest)
394
+ is_streaming_request = method == "message/stream"
361
395
  host_supports_streaming = component.get_config("supports_streaming", False)
362
396
  if is_streaming_request and not host_supports_streaming:
363
397
  raise ValueError(
@@ -366,16 +400,30 @@ async def handle_a2a_request(component, message: SolaceMessage):
366
400
  effective_session_id = original_session_id
367
401
  is_run_based_session = False
368
402
  temporary_run_session_id_for_cleanup = None
403
+
404
+ session_id_from_data = None
405
+ if a2a_message and a2a_message.parts:
406
+ for part in a2a_message.parts:
407
+ if isinstance(part, DataPart) and "session_id" in part.data:
408
+ session_id_from_data = part.data["session_id"]
409
+ log.info(
410
+ f"Extracted session_id '{session_id_from_data}' from DataPart."
411
+ )
412
+ break
413
+
414
+ if session_id_from_data:
415
+ original_session_id = session_id_from_data
416
+
369
417
  if session_behavior == "RUN_BASED":
370
418
  is_run_based_session = True
371
- effective_session_id = f"{original_session_id}:{task_id}:run"
419
+ effective_session_id = f"{original_session_id}:{logical_task_id}:run"
372
420
  temporary_run_session_id_for_cleanup = effective_session_id
373
421
  log.info(
374
422
  "%s Session behavior is RUN_BASED. OriginalID='%s', EffectiveID for this run='%s', TaskID='%s'.",
375
423
  component.log_identifier,
376
424
  original_session_id,
377
425
  effective_session_id,
378
- task_id,
426
+ logical_task_id,
379
427
  )
380
428
  else:
381
429
  is_run_based_session = False
@@ -385,8 +433,9 @@ async def handle_a2a_request(component, message: SolaceMessage):
385
433
  "%s Session behavior is PERSISTENT. EffectiveID='%s' for TaskID='%s'.",
386
434
  component.log_identifier,
387
435
  effective_session_id,
388
- task_id,
436
+ logical_task_id,
389
437
  )
438
+
390
439
  adk_session_for_run = await component.session_service.get_session(
391
440
  app_name=agent_name, user_id=user_id, session_id=effective_session_id
392
441
  )
@@ -400,15 +449,17 @@ async def handle_a2a_request(component, message: SolaceMessage):
400
449
  "%s Created new ADK session '%s' for task '%s'.",
401
450
  component.log_identifier,
402
451
  effective_session_id,
403
- task_id,
452
+ logical_task_id,
404
453
  )
454
+
405
455
  else:
406
456
  log.info(
407
457
  "%s Reusing existing ADK session '%s' for task '%s'.",
408
458
  component.log_identifier,
409
459
  effective_session_id,
410
- task_id,
460
+ logical_task_id,
411
461
  )
462
+
412
463
  if is_run_based_session:
413
464
  try:
414
465
  original_adk_session_data = (
@@ -447,27 +498,29 @@ async def handle_a2a_request(component, message: SolaceMessage):
447
498
  "%s No history to copy from original session '%s' for run-based task '%s'.",
448
499
  component.log_identifier,
449
500
  original_session_id,
450
- task_id,
501
+ logical_task_id,
451
502
  )
452
503
  else:
453
504
  log.debug(
454
505
  "%s Original session '%s' not found or has no history, cannot copy for run-based task '%s'.",
455
506
  component.log_identifier,
456
507
  original_session_id,
457
- task_id,
508
+ logical_task_id,
458
509
  )
459
510
  except Exception as e_copy:
460
511
  log.error(
461
512
  "%s Error copying history for run-based session '%s' (task '%s'): %s. Proceeding with empty session.",
462
513
  component.log_identifier,
463
514
  effective_session_id,
464
- task_id,
515
+ logical_task_id,
465
516
  e_copy,
466
517
  )
467
518
  a2a_context = {
468
519
  "jsonrpc_request_id": jsonrpc_request_id,
469
520
  "logical_task_id": logical_task_id,
470
- "session_id": original_session_id,
521
+ "contextId": original_session_id,
522
+ "messageId": message_id,
523
+ "session_id": original_session_id, # Keep for now for compatibility
471
524
  "user_id": user_id,
472
525
  "client_id": client_id,
473
526
  "is_streaming": is_streaming_request,
@@ -504,7 +557,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
504
557
  logical_task_id,
505
558
  )
506
559
 
507
- a2a_message_for_adk = a2a_request.params.message
560
+ a2a_message_for_adk = a2a_message
508
561
  invoked_artifacts = (
509
562
  a2a_message_for_adk.metadata.get("invoked_with_artifacts", [])
510
563
  if a2a_message_for_adk.metadata
@@ -515,7 +568,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
515
568
  log.info(
516
569
  "%s Task %s invoked with %d artifact(s). Preparing context from metadata.",
517
570
  component.log_identifier,
518
- task_id,
571
+ logical_task_id,
519
572
  len(invoked_artifacts),
520
573
  )
521
574
  header_text = (
@@ -531,24 +584,24 @@ async def handle_a2a_request(component, message: SolaceMessage):
531
584
  header_text=header_text,
532
585
  )
533
586
 
534
- task_description = _extract_text_from_parts(
535
- a2a_message_for_adk.parts
536
- )
587
+ task_description = get_text_from_message(a2a_message_for_adk)
537
588
  final_prompt = f"{task_description}\n\n{artifact_summary}"
538
589
 
539
- a2a_message_for_adk = A2AMessage(
540
- role="user",
541
- parts=[TextPart(text=final_prompt)],
542
- metadata=a2a_message_for_adk.metadata,
590
+ a2a_message_for_adk = a2a.update_message_parts(
591
+ message=a2a_message_for_adk,
592
+ new_parts=[a2a.create_text_part(text=final_prompt)],
543
593
  )
544
594
  log.debug(
545
595
  "%s Generated new prompt for task %s with artifact context.",
546
596
  component.log_identifier,
547
- task_id,
597
+ logical_task_id,
548
598
  )
549
599
 
550
- adk_content = translate_a2a_to_adk_content(
551
- a2a_message_for_adk, component.log_identifier
600
+ adk_content = await translate_a2a_to_adk_content(
601
+ a2a_message=a2a_message_for_adk,
602
+ component=component,
603
+ user_id=user_id,
604
+ session_id=effective_session_id,
552
605
  )
553
606
 
554
607
  adk_session = await component.session_service.get_session(
@@ -621,7 +674,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
621
674
  log.warning(
622
675
  "%s Received unhandled A2A request type: %s. Acknowledging.",
623
676
  component.log_identifier,
624
- type(a2a_request).__name__,
677
+ method,
625
678
  )
626
679
  try:
627
680
  message.call_acknowledgements()
@@ -629,7 +682,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
629
682
  log.error(
630
683
  "%s Failed to ACK unhandled request type %s: %s",
631
684
  component.log_identifier,
632
- type(a2a_request).__name__,
685
+ method,
633
686
  ack_e,
634
687
  )
635
688
  return None
@@ -641,22 +694,15 @@ async def handle_a2a_request(component, message: SolaceMessage):
641
694
  e,
642
695
  )
643
696
  error_data = {"taskId": logical_task_id} if logical_task_id else None
644
- if isinstance(e, ValueError):
645
- error_response = JSONRPCResponse(
646
- id=jsonrpc_request_id,
647
- error=InvalidRequestError(message=str(e), data=error_data),
648
- )
649
- else:
650
- error_response = JSONRPCResponse(
651
- id=jsonrpc_request_id,
652
- error=JSONParseError(message=str(e), data=error_data),
653
- )
697
+ error_response = a2a.create_internal_error_response(
698
+ message=str(e), request_id=jsonrpc_request_id, data=error_data
699
+ )
654
700
 
655
701
  target_topic = reply_topic_from_peer or (
656
702
  get_client_response_topic(namespace, client_id) if client_id else None
657
703
  )
658
704
  if target_topic:
659
- component._publish_a2a_message(
705
+ component.publish_a2a_message(
660
706
  error_response.model_dump(exclude_none=True),
661
707
  target_topic,
662
708
  )
@@ -681,18 +727,16 @@ async def handle_a2a_request(component, message: SolaceMessage):
681
727
  log.exception(
682
728
  "%s Unexpected error handling A2A request: %s", component.log_identifier, e
683
729
  )
684
- error_response = JSONRPCResponse(
685
- id=jsonrpc_request_id,
686
- error=InternalError(
687
- message=f"Unexpected server error: {e}",
688
- data={"taskId": logical_task_id},
689
- ),
730
+ error_response = a2a.create_internal_error_response(
731
+ message=f"Unexpected server error: {e}",
732
+ request_id=jsonrpc_request_id,
733
+ data={"taskId": logical_task_id},
690
734
  )
691
735
  target_topic = reply_topic_from_peer or (
692
736
  get_client_response_topic(namespace, client_id) if client_id else None
693
737
  )
694
738
  if target_topic:
695
- component._publish_a2a_message(
739
+ component.publish_a2a_message(
696
740
  error_response.model_dump(exclude_none=True),
697
741
  target_topic,
698
742
  )
@@ -755,7 +799,7 @@ def handle_agent_card_message(component, message: SolaceMessage):
755
799
  break
756
800
 
757
801
  if is_allowed:
758
- agent_card.peer_agents = {}
802
+ # The received card is stored as-is. We don't need to modify it.
759
803
  component.peer_agents[agent_name] = agent_card
760
804
 
761
805
  message.call_acknowledgements()
@@ -776,18 +820,25 @@ async def handle_a2a_response(component, message: SolaceMessage):
776
820
 
777
821
  try:
778
822
  topic = message.get_topic()
779
- topic_parts = topic.split("/")
780
- if len(topic_parts) > 0:
781
- sub_task_id = topic_parts[-1]
782
- if not sub_task_id.startswith(component.CORRELATION_DATA_PREFIX):
783
- log.warning(
784
- "%s Topic %s does not end with expected sub-task ID format. Ignoring.",
785
- component.log_identifier,
786
- topic,
787
- )
788
- message.call_acknowledgements()
789
- return
823
+ agent_response_sub = a2a.get_agent_response_subscription_topic(
824
+ component.namespace, component.agent_name
825
+ )
826
+ agent_status_sub = a2a.get_agent_status_subscription_topic(
827
+ component.namespace, component.agent_name
828
+ )
829
+
830
+ if a2a.topic_matches_subscription(topic, agent_response_sub):
831
+ sub_task_id = a2a.extract_task_id_from_topic(
832
+ topic, agent_response_sub, component.log_identifier
833
+ )
834
+ elif a2a.topic_matches_subscription(topic, agent_status_sub):
835
+ sub_task_id = a2a.extract_task_id_from_topic(
836
+ topic, agent_status_sub, component.log_identifier
837
+ )
790
838
  else:
839
+ sub_task_id = None
840
+
841
+ if not sub_task_id:
791
842
  log.error(
792
843
  "%s Could not extract sub-task ID from topic: %s",
793
844
  component.log_identifier,
@@ -813,159 +864,185 @@ async def handle_a2a_response(component, message: SolaceMessage):
813
864
  is_final_response = True
814
865
  else:
815
866
  try:
816
- a2a_response = JSONRPCResponse(**payload_dict)
867
+ a2a_response = JSONRPCResponse.model_validate(payload_dict)
868
+
869
+ result = a2a.get_response_result(a2a_response)
870
+ if result:
871
+ payload_data = result
872
+
873
+ # Store the peer's task ID if we see it for the first time
874
+ peer_task_id = getattr(payload_data, "task_id", None)
875
+ if peer_task_id:
876
+ correlation_data = (
877
+ await component._get_correlation_data_for_sub_task(
878
+ sub_task_id
879
+ )
880
+ )
881
+ if correlation_data and "peer_task_id" not in correlation_data:
882
+ log.info(
883
+ "%s Received first response for sub-task %s. Storing peer taskId: %s",
884
+ component.log_identifier,
885
+ sub_task_id,
886
+ peer_task_id,
887
+ )
888
+ main_logical_task_id = correlation_data.get(
889
+ "logical_task_id"
890
+ )
891
+ with component.active_tasks_lock:
892
+ task_context = component.active_tasks.get(
893
+ main_logical_task_id
894
+ )
895
+ if task_context:
896
+ with task_context.lock:
897
+ if (
898
+ sub_task_id
899
+ in task_context.active_peer_sub_tasks
900
+ ):
901
+ task_context.active_peer_sub_tasks[
902
+ sub_task_id
903
+ ]["peer_task_id"] = peer_task_id
817
904
 
818
- if a2a_response.result and isinstance(a2a_response.result, dict):
819
- payload_data = a2a_response.result
820
905
  parsed_successfully = False
821
906
  is_final_response = False
822
907
  payload_to_queue = None
823
908
 
824
- if (
825
- "final" in payload_data
826
- and "status" in payload_data
827
- and isinstance(payload_data.get("final"), bool)
828
- ):
909
+ if isinstance(payload_data, TaskStatusUpdateEvent):
829
910
  try:
830
- status_event = TaskStatusUpdateEvent(**payload_data)
831
-
832
- if (
833
- status_event.status
834
- and status_event.status.message
835
- and status_event.status.message.parts
836
- ):
837
- for part_from_peer in status_event.status.message.parts:
838
- if (
839
- isinstance(part_from_peer, DataPart)
840
- and part_from_peer.data.get("a2a_signal_type")
841
- == "agent_status_message"
842
- ):
843
- log.info(
844
- "%s Received agent_status_message signal from peer for sub-task %s.",
911
+ status_event = payload_data
912
+ data_parts = a2a.get_data_parts_from_status_update(
913
+ status_event
914
+ )
915
+ if data_parts:
916
+ for data_part in data_parts:
917
+ log.info(
918
+ "%s Received DataPart signal from peer for sub-task %s. Forwarding...",
919
+ component.log_identifier,
920
+ sub_task_id,
921
+ )
922
+ correlation_data = await component._get_correlation_data_for_sub_task(
923
+ sub_task_id
924
+ )
925
+ if not correlation_data:
926
+ log.warning(
927
+ "%s Correlation data not found for sub-task %s. Cannot forward status signal.",
845
928
  component.log_identifier,
846
929
  sub_task_id,
847
930
  )
848
- correlation_data = await component._get_correlation_data_for_sub_task(
849
- sub_task_id
850
- )
851
- if not correlation_data:
852
- log.warning(
853
- "%s Correlation data not found for sub-task %s. Cannot forward status signal.",
854
- component.log_identifier,
855
- sub_task_id,
856
- )
857
- message.call_acknowledgements()
858
- return
859
-
860
- original_task_context = correlation_data.get(
861
- "original_task_context"
862
- )
863
- if not original_task_context:
864
- log.warning(
865
- "%s original_task_context not found in correlation data for sub-task %s. Cannot forward status signal.",
866
- component.log_identifier,
867
- sub_task_id,
868
- )
869
- message.call_acknowledgements()
870
- return
871
-
872
- main_logical_task_id = (
873
- original_task_context.get("logical_task_id")
874
- )
875
- original_jsonrpc_request_id = (
876
- original_task_context.get(
877
- "jsonrpc_request_id"
878
- )
931
+ message.call_acknowledgements()
932
+ return
933
+
934
+ original_task_context = correlation_data.get(
935
+ "original_task_context"
936
+ )
937
+ if not original_task_context:
938
+ log.warning(
939
+ "%s original_task_context not found in correlation data for sub-task %s. Cannot forward status signal.",
940
+ component.log_identifier,
941
+ sub_task_id,
879
942
  )
943
+ message.call_acknowledgements()
944
+ return
945
+
946
+ main_logical_task_id = original_task_context.get(
947
+ "logical_task_id"
948
+ )
949
+ original_jsonrpc_request_id = (
950
+ original_task_context.get("jsonrpc_request_id")
951
+ )
952
+ main_context_id = original_task_context.get(
953
+ "contextId"
954
+ )
880
955
 
881
- target_topic_for_forward = (
882
- original_task_context.get("statusTopic")
956
+ target_topic_for_forward = (
957
+ original_task_context.get("statusTopic")
958
+ )
959
+
960
+ if (
961
+ not main_logical_task_id
962
+ or not original_jsonrpc_request_id
963
+ or not target_topic_for_forward
964
+ ):
965
+ log.error(
966
+ "%s Missing critical info (main_task_id, original_rpc_id, or target_status_topic) in context for sub-task %s. Cannot forward. Context: %s",
967
+ component.log_identifier,
968
+ sub_task_id,
969
+ original_task_context,
883
970
  )
971
+ message.call_acknowledgements()
972
+ return
884
973
 
885
- if (
886
- not main_logical_task_id
887
- or not original_jsonrpc_request_id
888
- or not target_topic_for_forward
889
- ):
890
- log.error(
891
- "%s Missing critical info (main_task_id, original_rpc_id, or target_status_topic) in context for sub-task %s. Cannot forward. Context: %s",
892
- component.log_identifier,
893
- sub_task_id,
894
- original_task_context,
895
- )
896
- message.call_acknowledgements()
897
- return
898
-
899
- peer_agent_name = (
900
- status_event.metadata.get(
901
- "agent_name", "UnknownPeer"
902
- )
903
- if status_event.metadata
904
- else "UnknownPeer"
974
+ peer_agent_name = (
975
+ status_event.metadata.get(
976
+ "agent_name", "UnknownPeer"
905
977
  )
978
+ if status_event.metadata
979
+ else "UnknownPeer"
980
+ )
906
981
 
907
- forwarded_message = A2AMessage(
908
- role="agent",
909
- parts=[part_from_peer],
910
- metadata={
911
- "agent_name": component.agent_name,
912
- "forwarded_from_peer": peer_agent_name,
913
- "original_peer_event_id": status_event.id,
914
- "original_peer_event_timestamp": (
915
- status_event.status.timestamp.isoformat()
916
- if status_event.status
917
- and status_event.status.timestamp
918
- else None
919
- ),
920
- "function_call_id": correlation_data.get(
921
- "adk_function_call_id", None
922
- ),
923
- },
982
+ forwarded_message = a2a.create_agent_parts_message(
983
+ parts=[data_part],
984
+ metadata={
985
+ "agent_name": component.agent_name,
986
+ "forwarded_from_peer": peer_agent_name,
987
+ "original_peer_event_taskId": status_event.task_id,
988
+ "original_peer_event_timestamp": (
989
+ status_event.status.timestamp
990
+ if status_event.status
991
+ and status_event.status.timestamp
992
+ else None
993
+ ),
994
+ "function_call_id": correlation_data.get(
995
+ "adk_function_call_id", None
996
+ ),
997
+ },
998
+ )
999
+ forwarded_event = a2a.create_status_update(
1000
+ task_id=main_logical_task_id,
1001
+ context_id=main_context_id,
1002
+ message=forwarded_message,
1003
+ is_final=False,
1004
+ )
1005
+ if (
1006
+ status_event.status
1007
+ and status_event.status.timestamp
1008
+ ):
1009
+ forwarded_event.status.timestamp = (
1010
+ status_event.status.timestamp
924
1011
  )
925
- forwarded_status = TaskStatus(
926
- state=TaskState.WORKING,
927
- message=forwarded_message,
928
- timestamp=status_event.status.timestamp,
1012
+ forwarded_rpc_response = JSONRPCResponse(
1013
+ id=original_jsonrpc_request_id,
1014
+ result=forwarded_event,
1015
+ )
1016
+ payload_to_publish = (
1017
+ forwarded_rpc_response.model_dump(
1018
+ by_alias=True, exclude_none=True
929
1019
  )
930
- forwarded_event = TaskStatusUpdateEvent(
931
- id=main_logical_task_id,
932
- status=forwarded_status,
933
- final=False,
1020
+ )
1021
+
1022
+ try:
1023
+ component.publish_a2a_message(
1024
+ payload_to_publish,
1025
+ target_topic_for_forward,
934
1026
  )
935
- forwarded_rpc_response = JSONRPCResponse(
936
- id=original_jsonrpc_request_id,
937
- result=forwarded_event,
1027
+ log.info(
1028
+ "%s Forwarded DataPart signal for main task %s (from peer %s) to %s.",
1029
+ component.log_identifier,
1030
+ main_logical_task_id,
1031
+ peer_agent_name,
1032
+ target_topic_for_forward,
938
1033
  )
939
- payload_to_publish = (
940
- forwarded_rpc_response.model_dump(
941
- exclude_none=True
942
- )
1034
+ except Exception as pub_err:
1035
+ log.exception(
1036
+ "%s Failed to publish forwarded status signal for main task %s: %s",
1037
+ component.log_identifier,
1038
+ main_logical_task_id,
1039
+ pub_err,
943
1040
  )
944
-
945
- try:
946
- component._publish_a2a_message(
947
- payload_to_publish,
948
- target_topic_for_forward,
949
- )
950
- log.info(
951
- "%s Forwarded agent_status_message signal for main task %s (from peer %s) to %s.",
952
- component.log_identifier,
953
- main_logical_task_id,
954
- peer_agent_name,
955
- target_topic_for_forward,
956
- )
957
- except Exception as pub_err:
958
- log.exception(
959
- "%s Failed to publish forwarded status signal for main task %s: %s",
960
- component.log_identifier,
961
- main_logical_task_id,
962
- pub_err,
963
- )
964
- message.call_acknowledgements()
965
- return
1041
+ message.call_acknowledgements()
1042
+ return
966
1043
 
967
1044
  payload_to_queue = status_event.model_dump(
968
- exclude_none=True
1045
+ by_alias=True, exclude_none=True
969
1046
  )
970
1047
  if status_event.final:
971
1048
  log.debug(
@@ -974,22 +1051,15 @@ async def handle_a2a_response(component, message: SolaceMessage):
974
1051
  sub_task_id,
975
1052
  )
976
1053
 
977
- if (
978
- status_event.status
979
- and status_event.status.message
980
- and status_event.status.message.parts
981
- ):
1054
+ if status_event.status and status_event.status.message:
982
1055
  response_parts_data = []
983
- for part in status_event.status.message.parts:
984
- if (
985
- hasattr(part, "text")
986
- and part.text is not None
987
- ):
1056
+ unwrapped_parts = a2a.get_parts_from_message(
1057
+ status_event.status.message
1058
+ )
1059
+ for part in unwrapped_parts:
1060
+ if isinstance(part, TextPart):
988
1061
  response_parts_data.append(str(part.text))
989
- elif (
990
- hasattr(part, "data")
991
- and part.data is not None
992
- ):
1062
+ elif isinstance(part, DataPart):
993
1063
  try:
994
1064
  response_parts_data.append(
995
1065
  json.dumps(part.data)
@@ -1023,7 +1093,7 @@ async def handle_a2a_response(component, message: SolaceMessage):
1023
1093
  parsed_successfully = True
1024
1094
  except Exception as e:
1025
1095
  log.warning(
1026
- "%s Failed to parse payload as TaskStatusUpdateEvent for sub-task %s. Payload: %s. Error: %s",
1096
+ "%s Failed to process payload as TaskStatusUpdateEvent for sub-task %s. Payload: %s. Error: %s",
1027
1097
  component.log_identifier,
1028
1098
  sub_task_id,
1029
1099
  payload_data,
@@ -1031,15 +1101,11 @@ async def handle_a2a_response(component, message: SolaceMessage):
1031
1101
  )
1032
1102
  payload_to_queue = None
1033
1103
 
1034
- if (
1035
- not parsed_successfully
1036
- and "artifact" in payload_data
1037
- and isinstance(payload_data.get("artifact"), dict)
1038
- ):
1104
+ elif isinstance(payload_data, TaskArtifactUpdateEvent):
1039
1105
  try:
1040
- artifact_event = TaskArtifactUpdateEvent(**payload_data)
1106
+ artifact_event = payload_data
1041
1107
  payload_to_queue = artifact_event.model_dump(
1042
- exclude_none=True
1108
+ by_alias=True, exclude_none=True
1043
1109
  )
1044
1110
  is_final_response = False
1045
1111
  log.debug(
@@ -1058,10 +1124,12 @@ async def handle_a2a_response(component, message: SolaceMessage):
1058
1124
  )
1059
1125
  payload_to_queue = None
1060
1126
 
1061
- if not parsed_successfully:
1127
+ elif isinstance(payload_data, Task):
1062
1128
  try:
1063
- final_task = Task(**payload_data)
1064
- payload_to_queue = final_task.model_dump(exclude_none=True)
1129
+ final_task = payload_data
1130
+ payload_to_queue = final_task.model_dump(
1131
+ by_alias=True, exclude_none=True
1132
+ )
1065
1133
  is_final_response = True
1066
1134
  log.debug(
1067
1135
  "%s Parsed final Task object from peer for sub-task %s.",
@@ -1071,26 +1139,31 @@ async def handle_a2a_response(component, message: SolaceMessage):
1071
1139
  parsed_successfully = True
1072
1140
  except Exception as task_parse_error:
1073
1141
  log.error(
1074
- "%s Failed to parse peer response for sub-task %s as any known type. Payload: %s. Error: %s",
1142
+ "%s Failed to parse peer response for sub-task %s as Task. Payload: %s. Error: %s",
1075
1143
  component.log_identifier,
1076
1144
  sub_task_id,
1077
1145
  payload_data,
1078
1146
  task_parse_error,
1079
1147
  )
1080
- if not a2a_response.error:
1081
- a2a_response.error = InternalError(
1148
+ if not a2a.get_response_error(a2a_response):
1149
+ error = a2a.create_internal_error(
1082
1150
  message=f"Failed to parse response from peer agent for sub-task {sub_task_id}",
1083
1151
  data={
1084
- "original_payload": payload_data,
1152
+ "original_payload": payload_data.model_dump(
1153
+ by_alias=True, exclude_none=True
1154
+ ),
1085
1155
  "error": str(task_parse_error),
1086
1156
  },
1087
1157
  )
1158
+ a2a_response = a2a.create_error_response(
1159
+ error, a2a.get_response_id(a2a_response)
1160
+ )
1088
1161
  payload_to_queue = None
1089
1162
  is_final_response = True
1090
1163
 
1091
1164
  if (
1092
1165
  not parsed_successfully
1093
- and not a2a_response.error
1166
+ and not a2a.get_response_error(a2a_response)
1094
1167
  and payload_to_queue is None
1095
1168
  ):
1096
1169
  log.error(
@@ -1099,23 +1172,30 @@ async def handle_a2a_response(component, message: SolaceMessage):
1099
1172
  sub_task_id,
1100
1173
  payload_data,
1101
1174
  )
1102
- a2a_response.error = InternalError(
1175
+ error = a2a.create_internal_error(
1103
1176
  message=f"Unknown response structure from peer agent for sub-task {sub_task_id}",
1104
- data={"original_payload": payload_data},
1177
+ data={
1178
+ "original_payload": payload_data.model_dump(
1179
+ by_alias=True, exclude_none=True
1180
+ )
1181
+ },
1182
+ )
1183
+ a2a_response = a2a.create_error_response(
1184
+ error, a2a.get_response_id(a2a_response)
1105
1185
  )
1106
1186
  is_final_response = True
1107
1187
 
1108
- elif a2a_response.error:
1188
+ elif error := a2a.get_response_error(a2a_response):
1109
1189
  log.warning(
1110
1190
  "%s Received error response from peer for sub-task %s: %s",
1111
1191
  component.log_identifier,
1112
1192
  sub_task_id,
1113
- a2a_response.error,
1193
+ error,
1114
1194
  )
1115
1195
  payload_to_queue = {
1116
- "error": a2a_response.error.message,
1117
- "code": a2a_response.error.code,
1118
- "data": a2a_response.error.data,
1196
+ "error": error.message,
1197
+ "code": error.code,
1198
+ "data": error.data,
1119
1199
  }
1120
1200
  is_final_response = True
1121
1201
  else:
@@ -1206,9 +1286,7 @@ async def handle_a2a_response(component, message: SolaceMessage):
1206
1286
  try:
1207
1287
  task_obj = Task(**payload_to_queue)
1208
1288
  if task_obj.status and task_obj.status.message:
1209
- final_text = _extract_text_from_parts(
1210
- task_obj.status.message.parts
1211
- )
1289
+ final_text = get_text_from_message(task_obj.status.message)
1212
1290
 
1213
1291
  if (
1214
1292
  task_obj.metadata
@@ -1264,6 +1342,13 @@ async def handle_a2a_response(component, message: SolaceMessage):
1264
1342
  if artifact_summary:
1265
1343
  full_response_text = f"{artifact_summary}\n\n{full_response_text}"
1266
1344
 
1345
+ await _publish_peer_tool_result_notification(
1346
+ component=component,
1347
+ correlation_data=correlation_data,
1348
+ payload_to_queue=payload_to_queue,
1349
+ log_identifier=log_retrigger,
1350
+ )
1351
+
1267
1352
  current_result = {
1268
1353
  "adk_function_call_id": correlation_data.get("adk_function_call_id"),
1269
1354
  "peer_tool_name": correlation_data.get("peer_tool_name"),
@@ -1355,34 +1440,92 @@ def publish_agent_card(component):
1355
1440
  agent_request_topic = get_agent_request_topic(namespace, agent_name)
1356
1441
  dynamic_url = f"solace:{agent_request_topic}"
1357
1442
 
1443
+ # Define unique URIs for our custom extensions.
1444
+ PEER_TOPOLOGY_EXTENSION_URI = (
1445
+ "https://solace.com/a2a/extensions/peer-agent-topology"
1446
+ )
1447
+ DISPLAY_NAME_EXTENSION_URI = "https://solace.com/a2a/extensions/display-name"
1448
+ TOOLS_EXTENSION_URI = "https://solace.com/a2a/extensions/sam/tools"
1449
+
1450
+ extensions_list = []
1451
+
1452
+ # Create the extension object for peer agents.
1453
+ if peer_agents:
1454
+ peer_topology_extension = AgentExtension(
1455
+ uri=PEER_TOPOLOGY_EXTENSION_URI,
1456
+ description="A list of peer agents this agent is configured to communicate with.",
1457
+ params={"peer_agent_names": list(peer_agents.keys())},
1458
+ )
1459
+ extensions_list.append(peer_topology_extension)
1460
+
1461
+ # Create the extension object for the UI display name.
1462
+ if display_name:
1463
+ display_name_extension = AgentExtension(
1464
+ uri=DISPLAY_NAME_EXTENSION_URI,
1465
+ description="A UI-friendly display name for the agent.",
1466
+ params={"display_name": display_name},
1467
+ )
1468
+ extensions_list.append(display_name_extension)
1469
+
1470
+ # Create the extension object for the agent's tools.
1471
+ dynamic_tools = getattr(component, "agent_card_tool_manifest", [])
1472
+ if dynamic_tools:
1473
+ # Ensure all tools have a 'tags' field to prevent validation errors.
1474
+ processed_tools = []
1475
+ for tool in dynamic_tools:
1476
+ if "tags" not in tool:
1477
+ log.debug(
1478
+ "%s Tool '%s' in manifest is missing 'tags' field. Defaulting to empty list.",
1479
+ component.log_identifier,
1480
+ tool.get("id", "unknown"),
1481
+ )
1482
+ tool["tags"] = []
1483
+ processed_tools.append(tool)
1484
+
1485
+ tools_params = ToolsExtensionParams(tools=processed_tools)
1486
+ tools_extension = AgentExtension(
1487
+ uri=TOOLS_EXTENSION_URI,
1488
+ description="A list of tools available to the agent.",
1489
+ params=tools_params.model_dump(exclude_none=True),
1490
+ )
1491
+ extensions_list.append(tools_extension)
1492
+
1493
+ # Build the capabilities object, including our custom extensions.
1358
1494
  capabilities = AgentCapabilities(
1359
1495
  streaming=supports_streaming,
1360
- pushNotifications=False,
1361
- stateTransitionHistory=False,
1496
+ push_notifications=False,
1497
+ state_transition_history=False,
1498
+ extensions=extensions_list if extensions_list else None,
1362
1499
  )
1363
1500
 
1364
- skills = card_config.get("skills", [])
1365
- dynamic_tools = getattr(component, "agent_card_tool_manifest", [])
1501
+ skills_from_config = card_config.get("skills", [])
1502
+ # The 'tools' field is not part of the official AgentCard spec.
1503
+ # The tools are now included as an extension.
1504
+
1505
+ # Ensure all skills have a 'tags' field to prevent validation errors.
1506
+ processed_skills = []
1507
+ for skill in skills_from_config:
1508
+ if "tags" not in skill:
1509
+ skill["tags"] = []
1510
+ processed_skills.append(skill)
1366
1511
 
1367
1512
  agent_card = AgentCard(
1368
1513
  name=agent_name,
1369
- display_name=display_name,
1514
+ protocol_version=card_config.get("protocolVersion", "0.3.0"),
1370
1515
  version=component.HOST_COMPONENT_VERSION,
1371
1516
  url=dynamic_url,
1372
1517
  capabilities=capabilities,
1373
1518
  description=card_config.get("description", ""),
1374
- skills=skills,
1375
- tools=dynamic_tools,
1376
- defaultInputModes=card_config.get("defaultInputModes", ["text"]),
1377
- defaultOutputModes=card_config.get("defaultOutputModes", ["text"]),
1378
- documentationUrl=card_config.get("documentationUrl"),
1519
+ skills=processed_skills,
1520
+ default_input_modes=card_config.get("defaultInputModes", ["text"]),
1521
+ default_output_modes=card_config.get("defaultOutputModes", ["text"]),
1522
+ documentation_url=card_config.get("documentationUrl"),
1379
1523
  provider=card_config.get("provider"),
1380
- peer_agents=peer_agents,
1381
1524
  )
1382
1525
 
1383
1526
  discovery_topic = get_discovery_topic(namespace)
1384
1527
 
1385
- component._publish_a2a_message(
1528
+ component.publish_a2a_message(
1386
1529
  agent_card.model_dump(exclude_none=True), discovery_topic
1387
1530
  )
1388
1531
  log.debug(