solace-agent-mesh 1.5.1__py3-none-any.whl → 1.6.1__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 (184) hide show
  1. solace_agent_mesh/agent/adk/callbacks.py +0 -5
  2. solace_agent_mesh/agent/adk/models/lite_llm.py +123 -8
  3. solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +245 -0
  4. solace_agent_mesh/agent/protocol/event_handlers.py +213 -31
  5. solace_agent_mesh/agent/proxies/__init__.py +0 -0
  6. solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
  7. solace_agent_mesh/agent/proxies/a2a/app.py +55 -0
  8. solace_agent_mesh/agent/proxies/a2a/component.py +1115 -0
  9. solace_agent_mesh/agent/proxies/a2a/config.py +140 -0
  10. solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
  11. solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
  12. solace_agent_mesh/agent/proxies/base/app.py +99 -0
  13. solace_agent_mesh/agent/proxies/base/component.py +650 -0
  14. solace_agent_mesh/agent/proxies/base/config.py +85 -0
  15. solace_agent_mesh/agent/proxies/base/proxy_task_context.py +17 -0
  16. solace_agent_mesh/agent/sac/app.py +58 -5
  17. solace_agent_mesh/agent/sac/component.py +238 -75
  18. solace_agent_mesh/agent/sac/task_execution_context.py +46 -0
  19. solace_agent_mesh/agent/tools/audio_tools.py +125 -8
  20. solace_agent_mesh/agent/tools/web_tools.py +10 -5
  21. solace_agent_mesh/agent/utils/artifact_helpers.py +141 -3
  22. solace_agent_mesh/assets/docs/404.html +3 -3
  23. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +1 -0
  26. solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
  29. solace_agent_mesh/assets/docs/assets/js/{ad71b5ed.60668e9e.js → ad71b5ed.af3ecfd1.js} +1 -1
  30. solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
  31. solace_agent_mesh/assets/docs/assets/js/{da0b5bad.9d369087.js → da0b5bad.d08a9466.js} +1 -1
  32. solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
  33. solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +1 -0
  34. solace_agent_mesh/assets/docs/assets/js/{e3d9abda.2b916f9e.js → e3d9abda.6b9493d0.js} +1 -1
  35. solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +1 -0
  36. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +1 -0
  37. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +1 -0
  38. solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js → main.b12eac43.js} +2 -2
  39. solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +1 -0
  40. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +15 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
  51. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +262 -0
  52. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
  53. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +31 -3
  54. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
  55. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
  56. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
  57. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +5 -5
  58. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
  59. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
  60. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
  61. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +6 -4
  62. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
  63. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
  64. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +4 -4
  65. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +5 -5
  66. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
  67. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
  68. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
  69. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
  70. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
  71. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
  72. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
  73. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
  74. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
  75. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
  76. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
  77. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
  78. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  79. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
  80. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +6 -5
  81. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
  82. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
  83. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +100 -3
  84. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
  85. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  86. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
  87. solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +1 -0
  88. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  89. solace_agent_mesh/assets/docs/search-doc-1761248203150.json +1 -0
  90. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  91. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  92. solace_agent_mesh/cli/__init__.py +1 -1
  93. solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +2 -69
  94. solace_agent_mesh/cli/commands/eval_cmd.py +11 -49
  95. solace_agent_mesh/cli/commands/init_cmd/__init__.py +0 -5
  96. solace_agent_mesh/cli/commands/init_cmd/env_step.py +10 -12
  97. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +9 -61
  98. solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +9 -49
  99. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +1 -2
  100. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DwrxZE0E.js → authCallback-BTf6dqwp.js} +1 -1
  101. solace_agent_mesh/client/webui/frontend/static/assets/{client-DarGQzyw.js → client-CaY59VuC.js} +1 -1
  102. solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +342 -0
  103. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +1 -0
  104. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BKIeiHj_.js → vendor-BEmvJSYz.js} +1 -1
  105. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  106. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  107. solace_agent_mesh/common/a2a/__init__.py +24 -0
  108. solace_agent_mesh/common/a2a/artifact.py +39 -0
  109. solace_agent_mesh/common/a2a/events.py +29 -0
  110. solace_agent_mesh/common/a2a/message.py +68 -0
  111. solace_agent_mesh/common/a2a/protocol.py +151 -1
  112. solace_agent_mesh/common/agent_registry.py +83 -3
  113. solace_agent_mesh/common/constants.py +3 -1
  114. solace_agent_mesh/common/sac/sam_component_base.py +383 -4
  115. solace_agent_mesh/common/utils/pydantic_utils.py +12 -0
  116. solace_agent_mesh/config_portal/backend/common.py +1 -1
  117. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +98 -0
  118. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-44d62be6.js → manifest-61038fc6.js} +1 -1
  119. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  120. solace_agent_mesh/evaluation/evaluator.py +128 -104
  121. solace_agent_mesh/evaluation/message_organizer.py +116 -110
  122. solace_agent_mesh/evaluation/report_data_processor.py +84 -86
  123. solace_agent_mesh/evaluation/report_generator.py +73 -79
  124. solace_agent_mesh/evaluation/run.py +421 -235
  125. solace_agent_mesh/evaluation/shared/__init__.py +92 -0
  126. solace_agent_mesh/evaluation/shared/constants.py +47 -0
  127. solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
  128. solace_agent_mesh/evaluation/shared/helpers.py +35 -0
  129. solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
  130. solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
  131. solace_agent_mesh/evaluation/subscriber.py +111 -232
  132. solace_agent_mesh/evaluation/summary_builder.py +227 -117
  133. solace_agent_mesh/gateway/base/app.py +16 -1
  134. solace_agent_mesh/gateway/base/component.py +112 -39
  135. solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
  136. solace_agent_mesh/gateway/http_sse/component.py +99 -3
  137. solace_agent_mesh/gateway/http_sse/dependencies.py +4 -4
  138. solace_agent_mesh/gateway/http_sse/main.py +1 -0
  139. solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +12 -13
  140. solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +15 -18
  141. solace_agent_mesh/gateway/http_sse/repository/interfaces.py +25 -18
  142. solace_agent_mesh/gateway/http_sse/repository/session_repository.py +30 -26
  143. solace_agent_mesh/gateway/http_sse/repository/task_repository.py +35 -44
  144. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +4 -3
  145. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +95 -203
  146. solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +4 -3
  147. solace_agent_mesh/gateway/http_sse/routers/sessions.py +2 -2
  148. solace_agent_mesh/gateway/http_sse/routers/tasks.py +33 -41
  149. solace_agent_mesh/gateway/http_sse/routers/users.py +47 -1
  150. solace_agent_mesh/gateway/http_sse/routers/visualization.py +17 -11
  151. solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +4 -4
  152. solace_agent_mesh/gateway/http_sse/services/feedback_service.py +51 -43
  153. solace_agent_mesh/gateway/http_sse/services/session_service.py +20 -20
  154. solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +8 -8
  155. solace_agent_mesh/gateway/http_sse/shared/base_repository.py +45 -71
  156. solace_agent_mesh/gateway/http_sse/shared/types.py +0 -18
  157. solace_agent_mesh/templates/gateway_config_template.yaml +0 -5
  158. solace_agent_mesh/templates/logging_config_template.ini +10 -6
  159. solace_agent_mesh/templates/plugin_gateway_config_template.yaml +0 -3
  160. solace_agent_mesh/templates/shared_config.yaml +40 -0
  161. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/METADATA +47 -21
  162. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/RECORD +166 -145
  163. solace_agent_mesh/assets/docs/assets/js/5c2bd65f.e49689dd.js +0 -1
  164. solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.39d5851d.js +0 -1
  165. solace_agent_mesh/assets/docs/assets/js/71da7b71.804d6567.js +0 -1
  166. solace_agent_mesh/assets/docs/assets/js/77cf947d.64c9bd6c.js +0 -1
  167. solace_agent_mesh/assets/docs/assets/js/9e9d0a82.dd810042.js +0 -1
  168. solace_agent_mesh/assets/docs/assets/js/db924877.cbc66f02.js +0 -1
  169. solace_agent_mesh/assets/docs/assets/js/de915948.139b4b9c.js +0 -1
  170. solace_agent_mesh/assets/docs/assets/js/e6f9706b.582a78ca.js +0 -1
  171. solace_agent_mesh/assets/docs/assets/js/f284c35a.5766a13d.js +0 -1
  172. solace_agent_mesh/assets/docs/assets/js/ff4d71f2.9c0297a6.js +0 -1
  173. solace_agent_mesh/assets/docs/assets/js/runtime~main.18dc45dd.js +0 -1
  174. solace_agent_mesh/assets/docs/lunr-index-1760121512891.json +0 -1
  175. solace_agent_mesh/assets/docs/search-doc-1760121512891.json +0 -1
  176. solace_agent_mesh/client/webui/frontend/static/assets/main-2nd1gbaH.js +0 -339
  177. solace_agent_mesh/client/webui/frontend/static/assets/main-DoKXctCM.css +0 -1
  178. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-BNuqpWDc.js +0 -98
  179. solace_agent_mesh/evaluation/config_loader.py +0 -657
  180. solace_agent_mesh/evaluation/test_case_loader.py +0 -714
  181. /solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js.LICENSE.txt → main.b12eac43.js.LICENSE.txt} +0 -0
  182. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/WHEEL +0 -0
  183. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/entry_points.txt +0 -0
  184. {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -14,6 +14,8 @@ from .artifact import (
14
14
  get_artifact_id,
15
15
  get_artifact_name,
16
16
  get_parts_from_artifact,
17
+ get_text_content_from_artifact,
18
+ is_text_only_artifact,
17
19
  update_artifact_parts,
18
20
  prepare_file_part_for_publishing,
19
21
  resolve_file_part_uri,
@@ -25,6 +27,8 @@ from .events import (
25
27
  get_artifact_from_artifact_update,
26
28
  get_data_parts_from_status_update,
27
29
  get_message_from_status_update,
30
+ is_task_artifact_update,
31
+ is_task_status_update,
28
32
  )
29
33
  from .message import (
30
34
  create_agent_data_message,
@@ -51,6 +55,11 @@ from .message import (
51
55
  get_text_from_message,
52
56
  get_text_from_text_part,
53
57
  get_uri_from_file_part,
58
+ is_data_part,
59
+ is_file_part,
60
+ is_file_part_bytes,
61
+ is_file_part_uri,
62
+ is_text_part,
54
63
  )
55
64
  from .protocol import (
56
65
  create_cancel_task_request,
@@ -92,8 +101,11 @@ from .protocol import (
92
101
  get_response_result,
93
102
  get_task_id_from_cancel_request,
94
103
  extract_task_id_from_topic,
104
+ is_client_event,
105
+ is_message_object,
95
106
  subscription_to_regex,
96
107
  topic_matches_subscription,
108
+ unpack_client_event,
97
109
  )
98
110
  from .task import (
99
111
  create_final_task,
@@ -122,6 +134,8 @@ __all__ = [
122
134
  "get_artifact_id",
123
135
  "get_artifact_name",
124
136
  "get_parts_from_artifact",
137
+ "get_text_content_from_artifact",
138
+ "is_text_only_artifact",
125
139
  "update_artifact_parts",
126
140
  "prepare_file_part_for_publishing",
127
141
  "resolve_file_part_uri",
@@ -132,6 +146,8 @@ __all__ = [
132
146
  "get_artifact_from_artifact_update",
133
147
  "get_data_parts_from_status_update",
134
148
  "get_message_from_status_update",
149
+ "is_task_artifact_update",
150
+ "is_task_status_update",
135
151
  # message.py
136
152
  "create_agent_data_message",
137
153
  "create_agent_parts_message",
@@ -157,6 +173,11 @@ __all__ = [
157
173
  "get_text_from_message",
158
174
  "get_uri_from_file_part",
159
175
  "get_text_from_text_part",
176
+ "is_data_part",
177
+ "is_file_part",
178
+ "is_file_part_bytes",
179
+ "is_file_part_uri",
180
+ "is_text_part",
160
181
  # protocol.py
161
182
  "create_cancel_task_request",
162
183
  "create_error_response",
@@ -197,8 +218,11 @@ __all__ = [
197
218
  "get_response_result",
198
219
  "get_task_id_from_cancel_request",
199
220
  "extract_task_id_from_topic",
221
+ "is_client_event",
222
+ "is_message_object",
200
223
  "subscription_to_regex",
201
224
  "topic_matches_subscription",
225
+ "unpack_client_event",
202
226
  # task.py
203
227
  "create_final_task",
204
228
  "create_initial_task",
@@ -327,3 +327,42 @@ def get_parts_from_artifact(artifact: Artifact) -> List[ContentPart]:
327
327
  A list of the unwrapped content parts.
328
328
  """
329
329
  return [part.root for part in artifact.parts]
330
+
331
+
332
+ def is_text_only_artifact(artifact: Artifact) -> bool:
333
+ """
334
+ Checks if an artifact contains only TextParts.
335
+
336
+ Args:
337
+ artifact: The Artifact object to check.
338
+
339
+ Returns:
340
+ True if all parts are TextParts, False otherwise.
341
+ """
342
+ if not artifact.parts:
343
+ return False
344
+
345
+ for part in artifact.parts:
346
+ if not isinstance(part.root, TextPart):
347
+ return False
348
+
349
+ return True
350
+
351
+
352
+ def get_text_content_from_artifact(artifact: Artifact) -> List[str]:
353
+ """
354
+ Extracts all text content from TextParts in an artifact.
355
+
356
+ Args:
357
+ artifact: The Artifact object to extract text from.
358
+
359
+ Returns:
360
+ A list of text strings from all TextParts. Returns empty list if no TextParts found.
361
+ """
362
+ text_content = []
363
+
364
+ for part in artifact.parts:
365
+ if isinstance(part.root, TextPart):
366
+ text_content.append(part.root.text)
367
+
368
+ return text_content
@@ -181,3 +181,32 @@ def get_artifact_from_artifact_update(
181
181
  if event:
182
182
  return event.artifact
183
183
  return None
184
+
185
+
186
+ # --- Type Checking Helpers ---
187
+
188
+
189
+ def is_task_status_update(obj: Any) -> bool:
190
+ """
191
+ Checks if an object is a TaskStatusUpdateEvent.
192
+
193
+ Args:
194
+ obj: The object to check.
195
+
196
+ Returns:
197
+ True if the object is a TaskStatusUpdateEvent, False otherwise.
198
+ """
199
+ return isinstance(obj, TaskStatusUpdateEvent)
200
+
201
+
202
+ def is_task_artifact_update(obj: Any) -> bool:
203
+ """
204
+ Checks if an object is a TaskArtifactUpdateEvent.
205
+
206
+ Args:
207
+ obj: The object to check.
208
+
209
+ Returns:
210
+ True if the object is a TaskArtifactUpdateEvent, False otherwise.
211
+ """
212
+ return isinstance(obj, TaskArtifactUpdateEvent)
@@ -305,3 +305,71 @@ def get_filename_from_file_part(part: FilePart) -> Optional[str]:
305
305
  def get_mimetype_from_file_part(part: FilePart) -> Optional[str]:
306
306
  """Safely retrieves the MIME type from a FilePart."""
307
307
  return part.file.mime_type
308
+
309
+
310
+ # --- Type Checking Helpers ---
311
+
312
+
313
+ def is_text_part(part: Part) -> bool:
314
+ """
315
+ Checks if a Part contains a TextPart.
316
+
317
+ Args:
318
+ part: The Part object to check.
319
+
320
+ Returns:
321
+ True if the part contains a TextPart, False otherwise.
322
+ """
323
+ return isinstance(part.root, TextPart)
324
+
325
+
326
+ def is_file_part(part: Part) -> bool:
327
+ """
328
+ Checks if a Part contains a FilePart.
329
+
330
+ Args:
331
+ part: The Part object to check.
332
+
333
+ Returns:
334
+ True if the part contains a FilePart, False otherwise.
335
+ """
336
+ return isinstance(part.root, FilePart)
337
+
338
+
339
+ def is_data_part(part: Part) -> bool:
340
+ """
341
+ Checks if a Part contains a DataPart.
342
+
343
+ Args:
344
+ part: The Part object to check.
345
+
346
+ Returns:
347
+ True if the part contains a DataPart, False otherwise.
348
+ """
349
+ return isinstance(part.root, DataPart)
350
+
351
+
352
+ def is_file_part_bytes(part: FilePart) -> bool:
353
+ """
354
+ Checks if a FilePart uses FileWithBytes (embedded content).
355
+
356
+ Args:
357
+ part: The FilePart object to check.
358
+
359
+ Returns:
360
+ True if the file content is embedded as bytes, False otherwise.
361
+ """
362
+ return isinstance(part.file, FileWithBytes)
363
+
364
+
365
+ def is_file_part_uri(part: FilePart) -> bool:
366
+ """
367
+ Checks if a FilePart uses FileWithUri (reference to external content).
368
+
369
+ Args:
370
+ part: The FilePart object to check.
371
+
372
+ Returns:
373
+ True if the file content is a URI reference, False otherwise.
374
+ """
375
+ return isinstance(part.file, FileWithUri)
@@ -5,7 +5,7 @@ parsing of JSON-RPC requests and responses.
5
5
  import logging
6
6
  import re
7
7
  import uuid
8
- from typing import Any, Dict, Optional, Union
8
+ from typing import Any, Dict, Optional, Tuple, Union
9
9
 
10
10
  from a2a.types import (
11
11
  A2ARequest,
@@ -197,6 +197,84 @@ def get_sam_events_subscription_topic(namespace: str, category: str) -> str:
197
197
  return f"{namespace.rstrip('/')}/sam/events/{category}/>"
198
198
 
199
199
 
200
+ def get_trust_card_topic(namespace: str, component_type: str, component_id: str) -> str:
201
+ """
202
+ Returns the topic for publishing a Trust Card.
203
+
204
+ IMPORTANT: The component_id parameter MUST be the exact broker client-username
205
+ that the component uses to authenticate with the Solace broker. This is critical
206
+ for trust verification - trust cards are validated against the actual broker
207
+ authentication identity.
208
+
209
+ Args:
210
+ namespace: SAM namespace
211
+ component_type: Type of component ("gateway", "agent", etc.)
212
+ component_id: MUST be the broker client-username (from broker_username config).
213
+ DO NOT use arbitrary IDs like agent_name or gateway_id unless they
214
+ match the broker_username exactly.
215
+
216
+ Returns:
217
+ Topic string: {namespace}/a2a/v1/trust/{component_type}/{component_id}
218
+
219
+ Raises:
220
+ ValueError: If any parameter is empty
221
+
222
+ Security Note:
223
+ Trust card verification relies on matching the topic component_id with the
224
+ authenticated broker client-username. Using a different value breaks the
225
+ security model and trust chain verification.
226
+ """
227
+ if not namespace:
228
+ raise ValueError("Namespace cannot be empty.")
229
+ if not component_type:
230
+ raise ValueError("Component type cannot be empty.")
231
+ if not component_id:
232
+ raise ValueError("Component ID cannot be empty.")
233
+ return f"{get_a2a_base_topic(namespace)}/trust/{component_type}/{component_id}"
234
+
235
+
236
+ def get_trust_card_subscription_topic(namespace: str, component_type: Optional[str] = None) -> str:
237
+ """
238
+ Returns subscription pattern for Trust Cards.
239
+
240
+ Args:
241
+ namespace: SAM namespace
242
+ component_type: Optional - subscribe to specific type, or None for all types
243
+
244
+ Returns:
245
+ Subscription pattern
246
+ """
247
+ if not namespace:
248
+ raise ValueError("Namespace cannot be empty.")
249
+
250
+ if component_type:
251
+ return f"{get_a2a_base_topic(namespace)}/trust/{component_type}/*"
252
+ else:
253
+ return f"{get_a2a_base_topic(namespace)}/trust/*/*"
254
+
255
+
256
+ def extract_trust_card_info_from_topic(topic: str) -> tuple[str, str]:
257
+ """
258
+ Extracts component type and ID from trust card topic.
259
+
260
+ Args:
261
+ topic: Trust card topic
262
+
263
+ Returns:
264
+ Tuple of (component_type, component_id)
265
+
266
+ Raises:
267
+ ValueError: If topic format is invalid
268
+ """
269
+ parts = topic.split('/')
270
+ if len(parts) < 6 or parts[1] != 'a2a' or parts[2] != 'v1' or parts[3] != 'trust':
271
+ raise ValueError(f"Invalid trust card topic format: {topic}")
272
+
273
+ component_type = parts[4]
274
+ component_id = parts[5]
275
+ return component_type, component_id
276
+
277
+
200
278
  def subscription_to_regex(subscription: str) -> str:
201
279
  """Converts a Solace topic subscription string to a regex pattern."""
202
280
  # Escape regex special characters except for Solace wildcards
@@ -532,3 +610,75 @@ def extract_task_id_from_topic(
532
610
  subscription_pattern,
533
611
  )
534
612
  return None
613
+
614
+
615
+ # --- Client Event Helpers ---
616
+
617
+
618
+ def is_client_event(obj: Any) -> bool:
619
+ """
620
+ Checks if an object is a ClientEvent tuple (Task, UpdateEvent).
621
+
622
+ A ClientEvent is a tuple with 2 elements where the first element is a Task
623
+ and the second is either a TaskStatusUpdateEvent, TaskArtifactUpdateEvent, or None.
624
+
625
+ Args:
626
+ obj: The object to check.
627
+
628
+ Returns:
629
+ True if the object is a ClientEvent tuple, False otherwise.
630
+ """
631
+ if not isinstance(obj, tuple) or len(obj) != 2:
632
+ return False
633
+
634
+ task, update_event = obj
635
+
636
+ # First element must be a Task
637
+ if not isinstance(task, Task):
638
+ return False
639
+
640
+ # Second element must be an update event or None
641
+ if update_event is not None and not isinstance(
642
+ update_event, (TaskStatusUpdateEvent, TaskArtifactUpdateEvent)
643
+ ):
644
+ return False
645
+
646
+ return True
647
+
648
+
649
+ def is_message_object(obj: Any) -> bool:
650
+ """
651
+ Checks if an object is a Message.
652
+
653
+ Args:
654
+ obj: The object to check.
655
+
656
+ Returns:
657
+ True if the object is a Message, False otherwise.
658
+ """
659
+ return isinstance(obj, Message)
660
+
661
+
662
+ def unpack_client_event(
663
+ event: tuple,
664
+ ) -> Tuple[Task, Optional[Union[TaskStatusUpdateEvent, TaskArtifactUpdateEvent]]]:
665
+ """
666
+ Safely unpacks a ClientEvent tuple into its components.
667
+
668
+ Args:
669
+ event: A ClientEvent tuple (Task, UpdateEvent).
670
+
671
+ Returns:
672
+ A tuple of (Task, Optional[UpdateEvent]) where UpdateEvent can be
673
+ TaskStatusUpdateEvent, TaskArtifactUpdateEvent, or None.
674
+
675
+ Raises:
676
+ ValueError: If the event is not a valid ClientEvent tuple.
677
+ """
678
+ if not is_client_event(event):
679
+ raise ValueError(
680
+ f"Expected a ClientEvent tuple, got {type(event).__name__}"
681
+ )
682
+
683
+ task, update_event = event
684
+ return task, update_event
@@ -4,26 +4,37 @@ Consolidated from src/tools/common/agent_registry.py and src/tools/a2a_cli_clien
4
4
  """
5
5
 
6
6
  import threading
7
- from typing import Dict, List, Optional
7
+ import time
8
+ from typing import Dict, List, Optional, Tuple
9
+ import logging
8
10
 
9
11
  from a2a.types import AgentCard
10
12
 
13
+ log = logging.getLogger(__name__)
11
14
 
12
15
  class AgentRegistry:
13
- """Stores and manages discovered AgentCards."""
16
+ """Stores and manages discovered AgentCards with health tracking."""
14
17
 
15
18
  def __init__(self):
16
19
  self._agents: Dict[str, AgentCard] = {}
20
+ self._last_seen: Dict[str, float] = {} # Timestamp of last agent card received
17
21
  self._lock = threading.Lock()
18
22
 
19
23
  def add_or_update_agent(self, agent_card: AgentCard):
20
24
  """Adds a new agent or updates an existing one."""
25
+
21
26
  if not agent_card or not agent_card.name:
22
- return
27
+ log.warning("Attempted to register agent with invalid agent card or missing name")
28
+ return False
23
29
 
24
30
  with self._lock:
25
31
  is_new = agent_card.name not in self._agents
32
+ current_time = time.time()
33
+
34
+ # Store the agent information
26
35
  self._agents[agent_card.name] = agent_card
36
+ self._last_seen[agent_card.name] = current_time
37
+
27
38
  return is_new
28
39
 
29
40
  def get_agent(self, agent_name: str) -> Optional[AgentCard]:
@@ -35,8 +46,77 @@ class AgentRegistry:
35
46
  """Returns a sorted list of discovered agent names."""
36
47
  with self._lock:
37
48
  return sorted(list(self._agents.keys()))
49
+
50
+ def get_last_seen(self, agent_name: str) -> Optional[float]:
51
+ """Returns the timestamp when the agent was last seen."""
52
+ with self._lock:
53
+ return self._last_seen.get(agent_name)
54
+
55
+ def check_ttl_expired(self, agent_name: str, ttl_seconds: int) -> Tuple[bool, int]:
56
+ """
57
+ Checks if an agent's TTL has expired.
58
+
59
+ Args:
60
+ agent_name: The name of the agent to check
61
+ ttl_seconds: The TTL in seconds
62
+
63
+ Returns:
64
+ A tuple of (is_expired, seconds_since_last_seen)
65
+ """
66
+
67
+ with self._lock:
68
+ if agent_name not in self._last_seen:
69
+ log.debug("Attempted to check TTL for non-existent agent '%s'", agent_name)
70
+ return False, 0
71
+
72
+ last_seen_time = self._last_seen.get(agent_name)
73
+ current_time = time.time()
74
+ time_since_last_seen = int(current_time - last_seen_time) if last_seen_time else 0
75
+
76
+ is_expired = time_since_last_seen > ttl_seconds
77
+
78
+ if is_expired:
79
+ log.warning(
80
+ "AGENT HEALTH CRITICAL: Agent '%s' TTL expired. "
81
+ "Last seen: %s seconds ago, TTL: %d seconds",
82
+ agent_name,
83
+ time_since_last_seen,
84
+ ttl_seconds
85
+ )
86
+
87
+ return is_expired, time_since_last_seen
88
+
89
+ def remove_agent(self, agent_name: str) -> bool:
90
+ """Removes an agent from the registry."""
91
+
92
+ with self._lock:
93
+ if agent_name in self._agents:
94
+ # Get agent details before removal for logging
95
+ last_seen_time = self._last_seen.get(agent_name)
96
+ current_time = time.time()
97
+ time_since_last_seen = int(current_time - last_seen_time) if last_seen_time else "unknown"
98
+
99
+ # Log detailed information about the agent being removed
100
+ log.warning(
101
+ "AGENT DE-REGISTRATION: Removing agent '%s' from registry. "
102
+ "Last seen: %s seconds ago",
103
+ agent_name,
104
+ time_since_last_seen
105
+ )
106
+
107
+ # Remove the agent from all tracking dictionaries
108
+ del self._agents[agent_name]
109
+ if agent_name in self._last_seen:
110
+ del self._last_seen[agent_name]
111
+
112
+ log.info("Agent '%s' successfully removed from registry", agent_name)
113
+ return True
114
+ else:
115
+ log.debug("Attempted to remove non-existent agent '%s' from registry", agent_name)
116
+ return False
38
117
 
39
118
  def clear(self):
40
119
  """Clears all registered agents."""
41
120
  with self._lock:
42
121
  self._agents.clear()
122
+ self._last_seen.clear()
@@ -1,4 +1,6 @@
1
1
 
2
2
  DEFAULT_COMMUNICATION_TIMEOUT = 600 # 10 minutes
3
+ HEALTH_CHECK_TTL_SECONDS = 60 # 60 seconds - time after which a health check is considered stale
4
+ HEALTH_CHECK_INTERVAL_SECONDS = 10 # 10 seconds - interval between health checks
3
5
  TEXT_ARTIFACT_CONTEXT_MAX_LENGTH_CAPACITY = 200000 # maximum number of characters that can be loaded from a text artifact
4
- TEXT_ARTIFACT_CONTEXT_DEFAULT_LENGTH = 100000 # default number of characters to load from a text artifact
6
+ TEXT_ARTIFACT_CONTEXT_DEFAULT_LENGTH = 100000 # default number of characters to load from a text artifact