solace-agent-mesh 1.0.8__py3-none-any.whl → 1.1.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 (162) 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 +7 -5
  7. solace_agent_mesh/agent/adk/setup.py +11 -0
  8. solace_agent_mesh/agent/adk/stream_parser.py +8 -1
  9. solace_agent_mesh/agent/adk/tool_wrapper.py +10 -3
  10. solace_agent_mesh/agent/agent_llm.txt +355 -18
  11. solace_agent_mesh/agent/protocol/event_handlers.py +433 -296
  12. solace_agent_mesh/agent/protocol/protocol_llm.txt +54 -7
  13. solace_agent_mesh/agent/sac/app.py +1 -1
  14. solace_agent_mesh/agent/sac/component.py +212 -517
  15. solace_agent_mesh/agent/sac/sac_llm.txt +133 -63
  16. solace_agent_mesh/agent/testing/testing_llm.txt +25 -58
  17. solace_agent_mesh/agent/tools/peer_agent_tool.py +15 -11
  18. solace_agent_mesh/agent/tools/tools_llm.txt +234 -69
  19. solace_agent_mesh/agent/utils/artifact_helpers.py +35 -1
  20. solace_agent_mesh/agent/utils/utils_llm.txt +90 -105
  21. solace_agent_mesh/assets/docs/404.html +3 -3
  22. solace_agent_mesh/assets/docs/assets/js/{3d406171.7d02a73b.js → 3d406171.0b9eeed1.js} +1 -1
  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.a75ecc0d.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 +8 -8
  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-1756992446316.json +1 -0
  65. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  66. solace_agent_mesh/assets/docs/search-doc-1756992446316.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/web_add_agent_step.py +12 -3
  71. solace_agent_mesh/cli/commands/add_cmd/web_add_gateway_step.py +10 -14
  72. solace_agent_mesh/cli/commands/init_cmd/web_init_step.py +2 -15
  73. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +6 -2
  74. solace_agent_mesh/cli/utils.py +15 -0
  75. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DvlO62me.js → authCallback-BmF2l6vg.js} +1 -1
  76. solace_agent_mesh/client/webui/frontend/static/assets/{client-bp6u3qVZ.js → client-D881Dttc.js} +4 -4
  77. solace_agent_mesh/client/webui/frontend/static/assets/main-C0jZjYa8.js +699 -0
  78. solace_agent_mesh/client/webui/frontend/static/assets/main-CCeG324-.css +1 -0
  79. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +2 -2
  80. solace_agent_mesh/client/webui/frontend/static/index.html +3 -3
  81. solace_agent_mesh/common/a2a/__init__.py +213 -0
  82. solace_agent_mesh/common/a2a/a2a_llm.txt +182 -0
  83. solace_agent_mesh/common/a2a/artifact.py +328 -0
  84. solace_agent_mesh/common/a2a/events.py +183 -0
  85. solace_agent_mesh/common/a2a/message.py +307 -0
  86. solace_agent_mesh/common/a2a/protocol.py +513 -0
  87. solace_agent_mesh/common/a2a/task.py +127 -0
  88. solace_agent_mesh/common/a2a/translation.py +653 -0
  89. solace_agent_mesh/common/a2a/types.py +54 -0
  90. solace_agent_mesh/common/a2a_spec/a2a.json +2576 -0
  91. solace_agent_mesh/common/a2a_spec/a2a_spec_llm.txt +407 -0
  92. solace_agent_mesh/common/a2a_spec/schemas/agent_progress_update.json +18 -0
  93. solace_agent_mesh/common/a2a_spec/schemas/artifact_creation_progress.json +31 -0
  94. solace_agent_mesh/common/a2a_spec/schemas/llm_invocation.json +18 -0
  95. solace_agent_mesh/common/a2a_spec/schemas/schemas_llm.txt +235 -0
  96. solace_agent_mesh/common/a2a_spec/schemas/tool_invocation_start.json +26 -0
  97. solace_agent_mesh/common/a2a_spec/schemas/tool_result.json +25 -0
  98. solace_agent_mesh/common/agent_registry.py +1 -1
  99. solace_agent_mesh/common/common_llm.txt +192 -70
  100. solace_agent_mesh/common/data_parts.py +99 -0
  101. solace_agent_mesh/common/middleware/middleware_llm.txt +17 -17
  102. solace_agent_mesh/common/sac/__init__.py +0 -0
  103. solace_agent_mesh/common/sac/sac_llm.txt +71 -0
  104. solace_agent_mesh/common/sac/sam_component_base.py +252 -0
  105. solace_agent_mesh/common/services/providers/providers_llm.txt +51 -84
  106. solace_agent_mesh/common/services/services_llm.txt +206 -26
  107. solace_agent_mesh/common/utils/artifact_utils.py +29 -0
  108. solace_agent_mesh/common/utils/embeds/embeds_llm.txt +176 -80
  109. solace_agent_mesh/common/utils/utils_llm.txt +323 -42
  110. solace_agent_mesh/config_portal/backend/common.py +1 -1
  111. solace_agent_mesh/config_portal/frontend/static/client/assets/{_index-MqsrTd6g.js → _index-Bym6YkMd.js} +74 -24
  112. solace_agent_mesh/config_portal/frontend/static/client/assets/{components-B7lKcHVY.js → components-Rk0n-9cK.js} +1 -1
  113. solace_agent_mesh/config_portal/frontend/static/client/assets/{entry.client-CEumGClk.js → entry.client-mvZjNKiz.js} +1 -1
  114. solace_agent_mesh/config_portal/frontend/static/client/assets/{index-DSo1AH_7.js → index-DzNKzXrc.js} +1 -1
  115. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-d845808d.js +1 -0
  116. solace_agent_mesh/config_portal/frontend/static/client/assets/{root-C4XmHinv.js → root-BWvk5-gF.js} +1 -1
  117. solace_agent_mesh/config_portal/frontend/static/client/index.html +3 -3
  118. solace_agent_mesh/core_a2a/core_a2a_llm.txt +10 -8
  119. solace_agent_mesh/core_a2a/service.py +20 -44
  120. solace_agent_mesh/gateway/base/app.py +27 -1
  121. solace_agent_mesh/gateway/base/base_llm.txt +177 -72
  122. solace_agent_mesh/gateway/base/component.py +294 -523
  123. solace_agent_mesh/gateway/gateway_llm.txt +299 -58
  124. solace_agent_mesh/gateway/http_sse/component.py +156 -183
  125. solace_agent_mesh/gateway/http_sse/components/components_llm.txt +29 -29
  126. solace_agent_mesh/gateway/http_sse/http_sse_llm.txt +272 -36
  127. solace_agent_mesh/gateway/http_sse/main.py +8 -10
  128. solace_agent_mesh/gateway/http_sse/routers/agents.py +1 -1
  129. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +18 -4
  130. solace_agent_mesh/gateway/http_sse/routers/routers_llm.txt +231 -5
  131. solace_agent_mesh/gateway/http_sse/routers/sessions.py +12 -7
  132. solace_agent_mesh/gateway/http_sse/routers/tasks.py +116 -169
  133. solace_agent_mesh/gateway/http_sse/services/agent_service.py +1 -1
  134. solace_agent_mesh/gateway/http_sse/services/services_llm.txt +89 -135
  135. solace_agent_mesh/gateway/http_sse/services/task_service.py +2 -5
  136. solace_agent_mesh/solace_agent_mesh_llm.txt +362 -0
  137. solace_agent_mesh/templates/gateway_component_template.py +149 -98
  138. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/METADATA +5 -4
  139. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/RECORD +143 -126
  140. solace_agent_mesh/assets/docs/assets/js/f284c35a.731836ad.js +0 -1
  141. solace_agent_mesh/assets/docs/assets/js/main.6dba4a66.js +0 -2
  142. solace_agent_mesh/assets/docs/assets/js/runtime~main.6415ad00.js +0 -1
  143. solace_agent_mesh/assets/docs/lunr-index-1756153049706.json +0 -1
  144. solace_agent_mesh/assets/docs/search-doc-1756153049706.json +0 -1
  145. solace_agent_mesh/client/webui/frontend/static/assets/main-BCpII1-0.css +0 -1
  146. solace_agent_mesh/client/webui/frontend/static/assets/main-BucUdn9m.js +0 -673
  147. solace_agent_mesh/common/a2a_protocol.py +0 -564
  148. solace_agent_mesh/common/client/__init__.py +0 -4
  149. solace_agent_mesh/common/client/card_resolver.py +0 -21
  150. solace_agent_mesh/common/client/client.py +0 -85
  151. solace_agent_mesh/common/client/client_llm.txt +0 -133
  152. solace_agent_mesh/common/server/__init__.py +0 -4
  153. solace_agent_mesh/common/server/server.py +0 -122
  154. solace_agent_mesh/common/server/server_llm.txt +0 -169
  155. solace_agent_mesh/common/server/task_manager.py +0 -291
  156. solace_agent_mesh/common/server/utils.py +0 -28
  157. solace_agent_mesh/common/types.py +0 -411
  158. solace_agent_mesh/config_portal/frontend/static/client/assets/manifest-28271392.js +0 -1
  159. /solace_agent_mesh/assets/docs/assets/js/{main.6dba4a66.js.LICENSE.txt → main.a75ecc0d.js.LICENSE.txt} +0 -0
  160. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/WHEEL +0 -0
  161. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.0.dist-info}/entry_points.txt +0 -0
  162. {solace_agent_mesh-1.0.8.dist-info → solace_agent_mesh-1.1.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.
@@ -223,19 +254,15 @@ async def handle_a2a_request(component, message: SolaceMessage):
223
254
  payload_dict = message.get_payload()
224
255
  if not isinstance(payload_dict, dict):
225
256
  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):
257
+ a2a_request: A2ARequest = A2ARequest.model_validate(payload_dict)
258
+ jsonrpc_request_id = a2a.get_request_id(a2a_request)
259
+
260
+ # The concept of logical_task_id changes. For Cancel, it's in params.id.
261
+ # For Send, we will generate it.
262
+ logical_task_id = None
263
+ method = a2a.get_request_method(a2a_request)
264
+ if method == "tasks/cancel":
265
+ logical_task_id = a2a.get_task_id_from_cancel_request(a2a_request)
239
266
  log.info(
240
267
  "%s Received CancelTaskRequest for Task ID: %s.",
241
268
  component.log_identifier,
@@ -253,28 +280,38 @@ async def handle_a2a_request(component, message: SolaceMessage):
253
280
  logical_task_id,
254
281
  )
255
282
 
256
- peer_sub_tasks = task_context.active_peer_sub_tasks
283
+ peer_sub_tasks = task_context.active_peer_sub_tasks.copy()
257
284
  if peer_sub_tasks:
258
- for sub_task_info in peer_sub_tasks:
259
- sub_task_id = sub_task_info.get("sub_task_id")
285
+ for sub_task_id, sub_task_info in peer_sub_tasks.items():
260
286
  target_peer_agent_name = sub_task_info.get("peer_agent_name")
261
- if sub_task_id and target_peer_agent_name:
287
+ peer_task_id_to_cancel = sub_task_info.get("peer_task_id")
288
+
289
+ if not peer_task_id_to_cancel:
290
+ log.warning(
291
+ "%s Cannot cancel peer sub-task %s for main task %s because the peer's taskId is not yet known.",
292
+ component.log_identifier,
293
+ sub_task_id,
294
+ logical_task_id,
295
+ )
296
+ continue
297
+
298
+ if peer_task_id_to_cancel and target_peer_agent_name:
262
299
  log.info(
263
- "%s Attempting to cancel peer sub-task %s for agent %s (main task %s).",
300
+ "%s Attempting to cancel peer sub-task %s (Peer Task ID: %s) for agent %s (main task %s).",
264
301
  component.log_identifier,
265
302
  sub_task_id,
303
+ peer_task_id_to_cancel,
266
304
  target_peer_agent_name,
267
305
  logical_task_id,
268
306
  )
269
307
  try:
270
- peer_cancel_params = TaskIdParams(id=sub_task_id)
271
- peer_cancel_request = CancelTaskRequest(
272
- params=peer_cancel_params
308
+ peer_cancel_request = a2a.create_cancel_task_request(
309
+ task_id=peer_task_id_to_cancel
273
310
  )
274
311
  peer_cancel_user_props = {
275
312
  "clientId": component.agent_name
276
313
  }
277
- component._publish_a2a_message(
314
+ component.publish_a2a_message(
278
315
  payload=peer_cancel_request.model_dump(
279
316
  exclude_none=True
280
317
  ),
@@ -284,17 +321,17 @@ async def handle_a2a_request(component, message: SolaceMessage):
284
321
  user_properties=peer_cancel_user_props,
285
322
  )
286
323
  log.info(
287
- "%s Sent CancelTaskRequest to peer %s for sub-task %s.",
324
+ "%s Sent CancelTaskRequest to peer %s for its task %s.",
288
325
  component.log_identifier,
289
326
  target_peer_agent_name,
290
- sub_task_id,
327
+ peer_task_id_to_cancel,
291
328
  )
292
329
  except Exception as e_peer_cancel:
293
330
  log.error(
294
- "%s Failed to send CancelTaskRequest to peer %s for sub-task %s: %s",
331
+ "%s Failed to send CancelTaskRequest to peer %s for task %s: %s",
295
332
  component.log_identifier,
296
333
  target_peer_agent_name,
297
- sub_task_id,
334
+ peer_task_id_to_cancel,
298
335
  e_peer_cancel,
299
336
  )
300
337
  else:
@@ -325,10 +362,18 @@ async def handle_a2a_request(component, message: SolaceMessage):
325
362
  ack_e,
326
363
  )
327
364
  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 {}
365
+ elif method in ["message/send", "message/stream"]:
366
+ a2a_message = a2a.get_message_from_send_request(a2a_request)
367
+ if not a2a_message:
368
+ raise ValueError("Could not extract message from SendMessageRequest")
369
+
370
+ # The gateway/client is the source of truth for the task ID.
371
+ # The agent adopts the ID from the JSON-RPC request envelope.
372
+ logical_task_id = str(a2a.get_request_id(a2a_request))
373
+ # The session id is now contextId on the message
374
+ original_session_id = a2a_message.context_id
375
+ message_id = a2a_message.message_id
376
+ task_metadata = a2a_message.metadata or {}
332
377
  system_purpose = task_metadata.get("system_purpose")
333
378
  response_format = task_metadata.get("response_format")
334
379
  session_behavior_from_meta = task_metadata.get("sessionBehavior")
@@ -357,7 +402,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
357
402
  )
358
403
  user_id = message.get_user_properties().get("userId", "default_user")
359
404
  agent_name = component.get_config("agent_name")
360
- is_streaming_request = isinstance(a2a_request, SendTaskStreamingRequest)
405
+ is_streaming_request = method == "message/stream"
361
406
  host_supports_streaming = component.get_config("supports_streaming", False)
362
407
  if is_streaming_request and not host_supports_streaming:
363
408
  raise ValueError(
@@ -368,14 +413,14 @@ async def handle_a2a_request(component, message: SolaceMessage):
368
413
  temporary_run_session_id_for_cleanup = None
369
414
  if session_behavior == "RUN_BASED":
370
415
  is_run_based_session = True
371
- effective_session_id = f"{original_session_id}:{task_id}:run"
416
+ effective_session_id = f"{original_session_id}:{logical_task_id}:run"
372
417
  temporary_run_session_id_for_cleanup = effective_session_id
373
418
  log.info(
374
419
  "%s Session behavior is RUN_BASED. OriginalID='%s', EffectiveID for this run='%s', TaskID='%s'.",
375
420
  component.log_identifier,
376
421
  original_session_id,
377
422
  effective_session_id,
378
- task_id,
423
+ logical_task_id,
379
424
  )
380
425
  else:
381
426
  is_run_based_session = False
@@ -385,7 +430,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
385
430
  "%s Session behavior is PERSISTENT. EffectiveID='%s' for TaskID='%s'.",
386
431
  component.log_identifier,
387
432
  effective_session_id,
388
- task_id,
433
+ logical_task_id,
389
434
  )
390
435
  adk_session_for_run = await component.session_service.get_session(
391
436
  app_name=agent_name, user_id=user_id, session_id=effective_session_id
@@ -400,14 +445,14 @@ async def handle_a2a_request(component, message: SolaceMessage):
400
445
  "%s Created new ADK session '%s' for task '%s'.",
401
446
  component.log_identifier,
402
447
  effective_session_id,
403
- task_id,
448
+ logical_task_id,
404
449
  )
405
450
  else:
406
451
  log.info(
407
452
  "%s Reusing existing ADK session '%s' for task '%s'.",
408
453
  component.log_identifier,
409
454
  effective_session_id,
410
- task_id,
455
+ logical_task_id,
411
456
  )
412
457
  if is_run_based_session:
413
458
  try:
@@ -447,27 +492,29 @@ async def handle_a2a_request(component, message: SolaceMessage):
447
492
  "%s No history to copy from original session '%s' for run-based task '%s'.",
448
493
  component.log_identifier,
449
494
  original_session_id,
450
- task_id,
495
+ logical_task_id,
451
496
  )
452
497
  else:
453
498
  log.debug(
454
499
  "%s Original session '%s' not found or has no history, cannot copy for run-based task '%s'.",
455
500
  component.log_identifier,
456
501
  original_session_id,
457
- task_id,
502
+ logical_task_id,
458
503
  )
459
504
  except Exception as e_copy:
460
505
  log.error(
461
506
  "%s Error copying history for run-based session '%s' (task '%s'): %s. Proceeding with empty session.",
462
507
  component.log_identifier,
463
508
  effective_session_id,
464
- task_id,
509
+ logical_task_id,
465
510
  e_copy,
466
511
  )
467
512
  a2a_context = {
468
513
  "jsonrpc_request_id": jsonrpc_request_id,
469
514
  "logical_task_id": logical_task_id,
470
- "session_id": original_session_id,
515
+ "contextId": original_session_id,
516
+ "messageId": message_id,
517
+ "session_id": original_session_id, # Keep for now for compatibility
471
518
  "user_id": user_id,
472
519
  "client_id": client_id,
473
520
  "is_streaming": is_streaming_request,
@@ -504,7 +551,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
504
551
  logical_task_id,
505
552
  )
506
553
 
507
- a2a_message_for_adk = a2a_request.params.message
554
+ a2a_message_for_adk = a2a_message
508
555
  invoked_artifacts = (
509
556
  a2a_message_for_adk.metadata.get("invoked_with_artifacts", [])
510
557
  if a2a_message_for_adk.metadata
@@ -515,7 +562,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
515
562
  log.info(
516
563
  "%s Task %s invoked with %d artifact(s). Preparing context from metadata.",
517
564
  component.log_identifier,
518
- task_id,
565
+ logical_task_id,
519
566
  len(invoked_artifacts),
520
567
  )
521
568
  header_text = (
@@ -531,24 +578,24 @@ async def handle_a2a_request(component, message: SolaceMessage):
531
578
  header_text=header_text,
532
579
  )
533
580
 
534
- task_description = _extract_text_from_parts(
535
- a2a_message_for_adk.parts
536
- )
581
+ task_description = get_text_from_message(a2a_message_for_adk)
537
582
  final_prompt = f"{task_description}\n\n{artifact_summary}"
538
583
 
539
- a2a_message_for_adk = A2AMessage(
540
- role="user",
541
- parts=[TextPart(text=final_prompt)],
542
- metadata=a2a_message_for_adk.metadata,
584
+ a2a_message_for_adk = a2a.update_message_parts(
585
+ message=a2a_message_for_adk,
586
+ new_parts=[a2a.create_text_part(text=final_prompt)],
543
587
  )
544
588
  log.debug(
545
589
  "%s Generated new prompt for task %s with artifact context.",
546
590
  component.log_identifier,
547
- task_id,
591
+ logical_task_id,
548
592
  )
549
593
 
550
- adk_content = translate_a2a_to_adk_content(
551
- a2a_message_for_adk, component.log_identifier
594
+ adk_content = await translate_a2a_to_adk_content(
595
+ a2a_message=a2a_message_for_adk,
596
+ component=component,
597
+ user_id=user_id,
598
+ session_id=effective_session_id,
552
599
  )
553
600
 
554
601
  adk_session = await component.session_service.get_session(
@@ -621,7 +668,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
621
668
  log.warning(
622
669
  "%s Received unhandled A2A request type: %s. Acknowledging.",
623
670
  component.log_identifier,
624
- type(a2a_request).__name__,
671
+ method,
625
672
  )
626
673
  try:
627
674
  message.call_acknowledgements()
@@ -629,7 +676,7 @@ async def handle_a2a_request(component, message: SolaceMessage):
629
676
  log.error(
630
677
  "%s Failed to ACK unhandled request type %s: %s",
631
678
  component.log_identifier,
632
- type(a2a_request).__name__,
679
+ method,
633
680
  ack_e,
634
681
  )
635
682
  return None
@@ -641,22 +688,15 @@ async def handle_a2a_request(component, message: SolaceMessage):
641
688
  e,
642
689
  )
643
690
  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
- )
691
+ error_response = a2a.create_internal_error_response(
692
+ message=str(e), request_id=jsonrpc_request_id, data=error_data
693
+ )
654
694
 
655
695
  target_topic = reply_topic_from_peer or (
656
696
  get_client_response_topic(namespace, client_id) if client_id else None
657
697
  )
658
698
  if target_topic:
659
- component._publish_a2a_message(
699
+ component.publish_a2a_message(
660
700
  error_response.model_dump(exclude_none=True),
661
701
  target_topic,
662
702
  )
@@ -681,18 +721,16 @@ async def handle_a2a_request(component, message: SolaceMessage):
681
721
  log.exception(
682
722
  "%s Unexpected error handling A2A request: %s", component.log_identifier, e
683
723
  )
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
- ),
724
+ error_response = a2a.create_internal_error_response(
725
+ message=f"Unexpected server error: {e}",
726
+ request_id=jsonrpc_request_id,
727
+ data={"taskId": logical_task_id},
690
728
  )
691
729
  target_topic = reply_topic_from_peer or (
692
730
  get_client_response_topic(namespace, client_id) if client_id else None
693
731
  )
694
732
  if target_topic:
695
- component._publish_a2a_message(
733
+ component.publish_a2a_message(
696
734
  error_response.model_dump(exclude_none=True),
697
735
  target_topic,
698
736
  )
@@ -755,7 +793,7 @@ def handle_agent_card_message(component, message: SolaceMessage):
755
793
  break
756
794
 
757
795
  if is_allowed:
758
- agent_card.peer_agents = {}
796
+ # The received card is stored as-is. We don't need to modify it.
759
797
  component.peer_agents[agent_name] = agent_card
760
798
 
761
799
  message.call_acknowledgements()
@@ -776,18 +814,25 @@ async def handle_a2a_response(component, message: SolaceMessage):
776
814
 
777
815
  try:
778
816
  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
817
+ agent_response_sub = a2a.get_agent_response_subscription_topic(
818
+ component.namespace, component.agent_name
819
+ )
820
+ agent_status_sub = a2a.get_agent_status_subscription_topic(
821
+ component.namespace, component.agent_name
822
+ )
823
+
824
+ if a2a.topic_matches_subscription(topic, agent_response_sub):
825
+ sub_task_id = a2a.extract_task_id_from_topic(
826
+ topic, agent_response_sub, component.log_identifier
827
+ )
828
+ elif a2a.topic_matches_subscription(topic, agent_status_sub):
829
+ sub_task_id = a2a.extract_task_id_from_topic(
830
+ topic, agent_status_sub, component.log_identifier
831
+ )
790
832
  else:
833
+ sub_task_id = None
834
+
835
+ if not sub_task_id:
791
836
  log.error(
792
837
  "%s Could not extract sub-task ID from topic: %s",
793
838
  component.log_identifier,
@@ -813,159 +858,185 @@ async def handle_a2a_response(component, message: SolaceMessage):
813
858
  is_final_response = True
814
859
  else:
815
860
  try:
816
- a2a_response = JSONRPCResponse(**payload_dict)
861
+ a2a_response = JSONRPCResponse.model_validate(payload_dict)
862
+
863
+ result = a2a.get_response_result(a2a_response)
864
+ if result:
865
+ payload_data = result
866
+
867
+ # Store the peer's task ID if we see it for the first time
868
+ peer_task_id = getattr(payload_data, "task_id", None)
869
+ if peer_task_id:
870
+ correlation_data = (
871
+ await component._get_correlation_data_for_sub_task(
872
+ sub_task_id
873
+ )
874
+ )
875
+ if correlation_data and "peer_task_id" not in correlation_data:
876
+ log.info(
877
+ "%s Received first response for sub-task %s. Storing peer taskId: %s",
878
+ component.log_identifier,
879
+ sub_task_id,
880
+ peer_task_id,
881
+ )
882
+ main_logical_task_id = correlation_data.get(
883
+ "logical_task_id"
884
+ )
885
+ with component.active_tasks_lock:
886
+ task_context = component.active_tasks.get(
887
+ main_logical_task_id
888
+ )
889
+ if task_context:
890
+ with task_context.lock:
891
+ if (
892
+ sub_task_id
893
+ in task_context.active_peer_sub_tasks
894
+ ):
895
+ task_context.active_peer_sub_tasks[
896
+ sub_task_id
897
+ ]["peer_task_id"] = peer_task_id
817
898
 
818
- if a2a_response.result and isinstance(a2a_response.result, dict):
819
- payload_data = a2a_response.result
820
899
  parsed_successfully = False
821
900
  is_final_response = False
822
901
  payload_to_queue = None
823
902
 
824
- if (
825
- "final" in payload_data
826
- and "status" in payload_data
827
- and isinstance(payload_data.get("final"), bool)
828
- ):
903
+ if isinstance(payload_data, TaskStatusUpdateEvent):
829
904
  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.",
905
+ status_event = payload_data
906
+ data_parts = a2a.get_data_parts_from_status_update(
907
+ status_event
908
+ )
909
+ if data_parts:
910
+ for data_part in data_parts:
911
+ log.info(
912
+ "%s Received DataPart signal from peer for sub-task %s. Forwarding...",
913
+ component.log_identifier,
914
+ sub_task_id,
915
+ )
916
+ correlation_data = await component._get_correlation_data_for_sub_task(
917
+ sub_task_id
918
+ )
919
+ if not correlation_data:
920
+ log.warning(
921
+ "%s Correlation data not found for sub-task %s. Cannot forward status signal.",
845
922
  component.log_identifier,
846
923
  sub_task_id,
847
924
  )
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
- )
925
+ message.call_acknowledgements()
926
+ return
927
+
928
+ original_task_context = correlation_data.get(
929
+ "original_task_context"
930
+ )
931
+ if not original_task_context:
932
+ log.warning(
933
+ "%s original_task_context not found in correlation data for sub-task %s. Cannot forward status signal.",
934
+ component.log_identifier,
935
+ sub_task_id,
879
936
  )
937
+ message.call_acknowledgements()
938
+ return
939
+
940
+ main_logical_task_id = original_task_context.get(
941
+ "logical_task_id"
942
+ )
943
+ original_jsonrpc_request_id = (
944
+ original_task_context.get("jsonrpc_request_id")
945
+ )
946
+ main_context_id = original_task_context.get(
947
+ "contextId"
948
+ )
949
+
950
+ target_topic_for_forward = (
951
+ original_task_context.get("statusTopic")
952
+ )
880
953
 
881
- target_topic_for_forward = (
882
- original_task_context.get("statusTopic")
954
+ if (
955
+ not main_logical_task_id
956
+ or not original_jsonrpc_request_id
957
+ or not target_topic_for_forward
958
+ ):
959
+ log.error(
960
+ "%s Missing critical info (main_task_id, original_rpc_id, or target_status_topic) in context for sub-task %s. Cannot forward. Context: %s",
961
+ component.log_identifier,
962
+ sub_task_id,
963
+ original_task_context,
883
964
  )
965
+ message.call_acknowledgements()
966
+ return
884
967
 
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"
968
+ peer_agent_name = (
969
+ status_event.metadata.get(
970
+ "agent_name", "UnknownPeer"
905
971
  )
972
+ if status_event.metadata
973
+ else "UnknownPeer"
974
+ )
906
975
 
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
- },
976
+ forwarded_message = a2a.create_agent_parts_message(
977
+ parts=[data_part],
978
+ metadata={
979
+ "agent_name": component.agent_name,
980
+ "forwarded_from_peer": peer_agent_name,
981
+ "original_peer_event_taskId": status_event.task_id,
982
+ "original_peer_event_timestamp": (
983
+ status_event.status.timestamp
984
+ if status_event.status
985
+ and status_event.status.timestamp
986
+ else None
987
+ ),
988
+ "function_call_id": correlation_data.get(
989
+ "adk_function_call_id", None
990
+ ),
991
+ },
992
+ )
993
+ forwarded_event = a2a.create_status_update(
994
+ task_id=main_logical_task_id,
995
+ context_id=main_context_id,
996
+ message=forwarded_message,
997
+ is_final=False,
998
+ )
999
+ if (
1000
+ status_event.status
1001
+ and status_event.status.timestamp
1002
+ ):
1003
+ forwarded_event.status.timestamp = (
1004
+ status_event.status.timestamp
924
1005
  )
925
- forwarded_status = TaskStatus(
926
- state=TaskState.WORKING,
927
- message=forwarded_message,
928
- timestamp=status_event.status.timestamp,
1006
+ forwarded_rpc_response = JSONRPCResponse(
1007
+ id=original_jsonrpc_request_id,
1008
+ result=forwarded_event,
1009
+ )
1010
+ payload_to_publish = (
1011
+ forwarded_rpc_response.model_dump(
1012
+ by_alias=True, exclude_none=True
929
1013
  )
930
- forwarded_event = TaskStatusUpdateEvent(
931
- id=main_logical_task_id,
932
- status=forwarded_status,
933
- final=False,
1014
+ )
1015
+
1016
+ try:
1017
+ component.publish_a2a_message(
1018
+ payload_to_publish,
1019
+ target_topic_for_forward,
934
1020
  )
935
- forwarded_rpc_response = JSONRPCResponse(
936
- id=original_jsonrpc_request_id,
937
- result=forwarded_event,
1021
+ log.info(
1022
+ "%s Forwarded DataPart signal for main task %s (from peer %s) to %s.",
1023
+ component.log_identifier,
1024
+ main_logical_task_id,
1025
+ peer_agent_name,
1026
+ target_topic_for_forward,
938
1027
  )
939
- payload_to_publish = (
940
- forwarded_rpc_response.model_dump(
941
- exclude_none=True
942
- )
1028
+ except Exception as pub_err:
1029
+ log.exception(
1030
+ "%s Failed to publish forwarded status signal for main task %s: %s",
1031
+ component.log_identifier,
1032
+ main_logical_task_id,
1033
+ pub_err,
943
1034
  )
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
1035
+ message.call_acknowledgements()
1036
+ return
966
1037
 
967
1038
  payload_to_queue = status_event.model_dump(
968
- exclude_none=True
1039
+ by_alias=True, exclude_none=True
969
1040
  )
970
1041
  if status_event.final:
971
1042
  log.debug(
@@ -974,22 +1045,15 @@ async def handle_a2a_response(component, message: SolaceMessage):
974
1045
  sub_task_id,
975
1046
  )
976
1047
 
977
- if (
978
- status_event.status
979
- and status_event.status.message
980
- and status_event.status.message.parts
981
- ):
1048
+ if status_event.status and status_event.status.message:
982
1049
  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
- ):
1050
+ unwrapped_parts = a2a.get_parts_from_message(
1051
+ status_event.status.message
1052
+ )
1053
+ for part in unwrapped_parts:
1054
+ if isinstance(part, TextPart):
988
1055
  response_parts_data.append(str(part.text))
989
- elif (
990
- hasattr(part, "data")
991
- and part.data is not None
992
- ):
1056
+ elif isinstance(part, DataPart):
993
1057
  try:
994
1058
  response_parts_data.append(
995
1059
  json.dumps(part.data)
@@ -1023,7 +1087,7 @@ async def handle_a2a_response(component, message: SolaceMessage):
1023
1087
  parsed_successfully = True
1024
1088
  except Exception as e:
1025
1089
  log.warning(
1026
- "%s Failed to parse payload as TaskStatusUpdateEvent for sub-task %s. Payload: %s. Error: %s",
1090
+ "%s Failed to process payload as TaskStatusUpdateEvent for sub-task %s. Payload: %s. Error: %s",
1027
1091
  component.log_identifier,
1028
1092
  sub_task_id,
1029
1093
  payload_data,
@@ -1031,15 +1095,11 @@ async def handle_a2a_response(component, message: SolaceMessage):
1031
1095
  )
1032
1096
  payload_to_queue = None
1033
1097
 
1034
- if (
1035
- not parsed_successfully
1036
- and "artifact" in payload_data
1037
- and isinstance(payload_data.get("artifact"), dict)
1038
- ):
1098
+ elif isinstance(payload_data, TaskArtifactUpdateEvent):
1039
1099
  try:
1040
- artifact_event = TaskArtifactUpdateEvent(**payload_data)
1100
+ artifact_event = payload_data
1041
1101
  payload_to_queue = artifact_event.model_dump(
1042
- exclude_none=True
1102
+ by_alias=True, exclude_none=True
1043
1103
  )
1044
1104
  is_final_response = False
1045
1105
  log.debug(
@@ -1058,10 +1118,12 @@ async def handle_a2a_response(component, message: SolaceMessage):
1058
1118
  )
1059
1119
  payload_to_queue = None
1060
1120
 
1061
- if not parsed_successfully:
1121
+ elif isinstance(payload_data, Task):
1062
1122
  try:
1063
- final_task = Task(**payload_data)
1064
- payload_to_queue = final_task.model_dump(exclude_none=True)
1123
+ final_task = payload_data
1124
+ payload_to_queue = final_task.model_dump(
1125
+ by_alias=True, exclude_none=True
1126
+ )
1065
1127
  is_final_response = True
1066
1128
  log.debug(
1067
1129
  "%s Parsed final Task object from peer for sub-task %s.",
@@ -1071,26 +1133,31 @@ async def handle_a2a_response(component, message: SolaceMessage):
1071
1133
  parsed_successfully = True
1072
1134
  except Exception as task_parse_error:
1073
1135
  log.error(
1074
- "%s Failed to parse peer response for sub-task %s as any known type. Payload: %s. Error: %s",
1136
+ "%s Failed to parse peer response for sub-task %s as Task. Payload: %s. Error: %s",
1075
1137
  component.log_identifier,
1076
1138
  sub_task_id,
1077
1139
  payload_data,
1078
1140
  task_parse_error,
1079
1141
  )
1080
- if not a2a_response.error:
1081
- a2a_response.error = InternalError(
1142
+ if not a2a.get_response_error(a2a_response):
1143
+ error = a2a.create_internal_error(
1082
1144
  message=f"Failed to parse response from peer agent for sub-task {sub_task_id}",
1083
1145
  data={
1084
- "original_payload": payload_data,
1146
+ "original_payload": payload_data.model_dump(
1147
+ by_alias=True, exclude_none=True
1148
+ ),
1085
1149
  "error": str(task_parse_error),
1086
1150
  },
1087
1151
  )
1152
+ a2a_response = a2a.create_error_response(
1153
+ error, a2a.get_response_id(a2a_response)
1154
+ )
1088
1155
  payload_to_queue = None
1089
1156
  is_final_response = True
1090
1157
 
1091
1158
  if (
1092
1159
  not parsed_successfully
1093
- and not a2a_response.error
1160
+ and not a2a.get_response_error(a2a_response)
1094
1161
  and payload_to_queue is None
1095
1162
  ):
1096
1163
  log.error(
@@ -1099,23 +1166,30 @@ async def handle_a2a_response(component, message: SolaceMessage):
1099
1166
  sub_task_id,
1100
1167
  payload_data,
1101
1168
  )
1102
- a2a_response.error = InternalError(
1169
+ error = a2a.create_internal_error(
1103
1170
  message=f"Unknown response structure from peer agent for sub-task {sub_task_id}",
1104
- data={"original_payload": payload_data},
1171
+ data={
1172
+ "original_payload": payload_data.model_dump(
1173
+ by_alias=True, exclude_none=True
1174
+ )
1175
+ },
1176
+ )
1177
+ a2a_response = a2a.create_error_response(
1178
+ error, a2a.get_response_id(a2a_response)
1105
1179
  )
1106
1180
  is_final_response = True
1107
1181
 
1108
- elif a2a_response.error:
1182
+ elif error := a2a.get_response_error(a2a_response):
1109
1183
  log.warning(
1110
1184
  "%s Received error response from peer for sub-task %s: %s",
1111
1185
  component.log_identifier,
1112
1186
  sub_task_id,
1113
- a2a_response.error,
1187
+ error,
1114
1188
  )
1115
1189
  payload_to_queue = {
1116
- "error": a2a_response.error.message,
1117
- "code": a2a_response.error.code,
1118
- "data": a2a_response.error.data,
1190
+ "error": error.message,
1191
+ "code": error.code,
1192
+ "data": error.data,
1119
1193
  }
1120
1194
  is_final_response = True
1121
1195
  else:
@@ -1206,9 +1280,7 @@ async def handle_a2a_response(component, message: SolaceMessage):
1206
1280
  try:
1207
1281
  task_obj = Task(**payload_to_queue)
1208
1282
  if task_obj.status and task_obj.status.message:
1209
- final_text = _extract_text_from_parts(
1210
- task_obj.status.message.parts
1211
- )
1283
+ final_text = get_text_from_message(task_obj.status.message)
1212
1284
 
1213
1285
  if (
1214
1286
  task_obj.metadata
@@ -1264,6 +1336,13 @@ async def handle_a2a_response(component, message: SolaceMessage):
1264
1336
  if artifact_summary:
1265
1337
  full_response_text = f"{artifact_summary}\n\n{full_response_text}"
1266
1338
 
1339
+ await _publish_peer_tool_result_notification(
1340
+ component=component,
1341
+ correlation_data=correlation_data,
1342
+ payload_to_queue=payload_to_queue,
1343
+ log_identifier=log_retrigger,
1344
+ )
1345
+
1267
1346
  current_result = {
1268
1347
  "adk_function_call_id": correlation_data.get("adk_function_call_id"),
1269
1348
  "peer_tool_name": correlation_data.get("peer_tool_name"),
@@ -1355,34 +1434,92 @@ def publish_agent_card(component):
1355
1434
  agent_request_topic = get_agent_request_topic(namespace, agent_name)
1356
1435
  dynamic_url = f"solace:{agent_request_topic}"
1357
1436
 
1437
+ # Define unique URIs for our custom extensions.
1438
+ PEER_TOPOLOGY_EXTENSION_URI = (
1439
+ "https://solace.com/a2a/extensions/peer-agent-topology"
1440
+ )
1441
+ DISPLAY_NAME_EXTENSION_URI = "https://solace.com/a2a/extensions/display-name"
1442
+ TOOLS_EXTENSION_URI = "https://solace.com/a2a/extensions/sam/tools"
1443
+
1444
+ extensions_list = []
1445
+
1446
+ # Create the extension object for peer agents.
1447
+ if peer_agents:
1448
+ peer_topology_extension = AgentExtension(
1449
+ uri=PEER_TOPOLOGY_EXTENSION_URI,
1450
+ description="A list of peer agents this agent is configured to communicate with.",
1451
+ params={"peer_agent_names": list(peer_agents.keys())},
1452
+ )
1453
+ extensions_list.append(peer_topology_extension)
1454
+
1455
+ # Create the extension object for the UI display name.
1456
+ if display_name:
1457
+ display_name_extension = AgentExtension(
1458
+ uri=DISPLAY_NAME_EXTENSION_URI,
1459
+ description="A UI-friendly display name for the agent.",
1460
+ params={"display_name": display_name},
1461
+ )
1462
+ extensions_list.append(display_name_extension)
1463
+
1464
+ # Create the extension object for the agent's tools.
1465
+ dynamic_tools = getattr(component, "agent_card_tool_manifest", [])
1466
+ if dynamic_tools:
1467
+ # Ensure all tools have a 'tags' field to prevent validation errors.
1468
+ processed_tools = []
1469
+ for tool in dynamic_tools:
1470
+ if "tags" not in tool:
1471
+ log.debug(
1472
+ "%s Tool '%s' in manifest is missing 'tags' field. Defaulting to empty list.",
1473
+ component.log_identifier,
1474
+ tool.get("id", "unknown"),
1475
+ )
1476
+ tool["tags"] = []
1477
+ processed_tools.append(tool)
1478
+
1479
+ tools_params = ToolsExtensionParams(tools=processed_tools)
1480
+ tools_extension = AgentExtension(
1481
+ uri=TOOLS_EXTENSION_URI,
1482
+ description="A list of tools available to the agent.",
1483
+ params=tools_params.model_dump(exclude_none=True),
1484
+ )
1485
+ extensions_list.append(tools_extension)
1486
+
1487
+ # Build the capabilities object, including our custom extensions.
1358
1488
  capabilities = AgentCapabilities(
1359
1489
  streaming=supports_streaming,
1360
- pushNotifications=False,
1361
- stateTransitionHistory=False,
1490
+ push_notifications=False,
1491
+ state_transition_history=False,
1492
+ extensions=extensions_list if extensions_list else None,
1362
1493
  )
1363
1494
 
1364
- skills = card_config.get("skills", [])
1365
- dynamic_tools = getattr(component, "agent_card_tool_manifest", [])
1495
+ skills_from_config = card_config.get("skills", [])
1496
+ # The 'tools' field is not part of the official AgentCard spec.
1497
+ # The tools are now included as an extension.
1498
+
1499
+ # Ensure all skills have a 'tags' field to prevent validation errors.
1500
+ processed_skills = []
1501
+ for skill in skills_from_config:
1502
+ if "tags" not in skill:
1503
+ skill["tags"] = []
1504
+ processed_skills.append(skill)
1366
1505
 
1367
1506
  agent_card = AgentCard(
1368
1507
  name=agent_name,
1369
- display_name=display_name,
1508
+ protocol_version=card_config.get("protocolVersion", "0.3.0"),
1370
1509
  version=component.HOST_COMPONENT_VERSION,
1371
1510
  url=dynamic_url,
1372
1511
  capabilities=capabilities,
1373
1512
  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"),
1513
+ skills=processed_skills,
1514
+ default_input_modes=card_config.get("defaultInputModes", ["text"]),
1515
+ default_output_modes=card_config.get("defaultOutputModes", ["text"]),
1516
+ documentation_url=card_config.get("documentationUrl"),
1379
1517
  provider=card_config.get("provider"),
1380
- peer_agents=peer_agents,
1381
1518
  )
1382
1519
 
1383
1520
  discovery_topic = get_discovery_topic(namespace)
1384
1521
 
1385
- component._publish_a2a_message(
1522
+ component.publish_a2a_message(
1386
1523
  agent_card.model_dump(exclude_none=True), discovery_topic
1387
1524
  )
1388
1525
  log.debug(