solace-agent-mesh 1.6.0__py3-none-any.whl → 1.6.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (127) hide show
  1. solace_agent_mesh/agent/adk/app_llm_agent.py +26 -0
  2. solace_agent_mesh/agent/adk/artifacts/filesystem_artifact_service.py +1 -1
  3. solace_agent_mesh/agent/adk/embed_resolving_mcp_toolset.py +135 -31
  4. solace_agent_mesh/agent/adk/models/lite_llm.py +5 -0
  5. solace_agent_mesh/agent/adk/runner.py +10 -12
  6. solace_agent_mesh/agent/adk/services.py +50 -14
  7. solace_agent_mesh/agent/adk/setup.py +66 -38
  8. solace_agent_mesh/agent/protocol/event_handlers.py +416 -152
  9. solace_agent_mesh/agent/proxies/a2a/app.py +3 -2
  10. solace_agent_mesh/agent/proxies/base/app.py +3 -2
  11. solace_agent_mesh/agent/proxies/base/component.py +35 -4
  12. solace_agent_mesh/agent/sac/app.py +97 -9
  13. solace_agent_mesh/agent/sac/component.py +284 -145
  14. solace_agent_mesh/agent/sac/task_execution_context.py +79 -2
  15. solace_agent_mesh/agent/tools/tool_config_types.py +3 -0
  16. solace_agent_mesh/agent/utils/artifact_helpers.py +1 -1
  17. solace_agent_mesh/assets/docs/404.html +3 -3
  18. solace_agent_mesh/assets/docs/assets/js/240a0364.c39f8388.js +1 -0
  19. solace_agent_mesh/assets/docs/assets/js/631738c7.7c4594c9.js +1 -0
  20. solace_agent_mesh/assets/docs/assets/js/66d4869e.830d443f.js +1 -0
  21. solace_agent_mesh/assets/docs/assets/js/71da7b71.ddbdfbe2.js +1 -0
  22. solace_agent_mesh/assets/docs/assets/js/{e3d9abda.2b916f9e.js → e3d9abda.6b9493d0.js} +1 -1
  23. solace_agent_mesh/assets/docs/assets/js/e92d0134.4f395c6b.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/f284c35a.720d2ef2.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/main.d1643f0b.js +2 -0
  26. solace_agent_mesh/assets/docs/assets/js/runtime~main.97f920d4.js +1 -0
  27. solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +3 -3
  28. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +3 -3
  29. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +3 -3
  30. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +3 -3
  31. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +3 -3
  32. solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +3 -3
  33. solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +3 -3
  34. solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +3 -3
  35. solace_agent_mesh/assets/docs/docs/documentation/components/index.html +3 -3
  36. solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +3 -3
  37. solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +3 -3
  38. solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +3 -3
  39. solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +4 -25
  40. solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/deploying/logging/index.html +76 -0
  43. solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +5 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +3 -3
  45. solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +3 -3
  46. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +3 -3
  47. solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +3 -3
  48. solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +3 -3
  49. solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +3 -3
  50. solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +3 -3
  51. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +3 -3
  52. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +3 -3
  53. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +3 -3
  54. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +3 -3
  55. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +3 -3
  56. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +3 -3
  57. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +3 -3
  58. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +3 -3
  59. solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +3 -3
  60. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
  61. solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
  62. solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
  63. solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
  64. solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
  65. solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
  66. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  67. solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
  68. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +3 -6
  69. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
  70. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
  71. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +3 -3
  72. solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
  73. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  74. solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
  75. solace_agent_mesh/assets/docs/lunr-index-1761663789856.json +1 -0
  76. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  77. solace_agent_mesh/assets/docs/search-doc-1761663789856.json +1 -0
  78. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  79. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  80. solace_agent_mesh/cli/__init__.py +1 -1
  81. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-BTf6dqwp.js → authCallback-D4_RMYRh.js} +1 -1
  82. solace_agent_mesh/client/webui/frontend/static/assets/{client-CaY59VuC.js → client-UZ3qU6Bq.js} +1 -1
  83. solace_agent_mesh/client/webui/frontend/static/assets/main--3yJYl7S.css +1 -0
  84. solace_agent_mesh/client/webui/frontend/static/assets/main-DojKHS49.js +342 -0
  85. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BEmvJSYz.js → vendor-DSqhjwq_.js} +1 -1
  86. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  87. solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
  88. solace_agent_mesh/common/a2a/events.py +2 -1
  89. solace_agent_mesh/common/a2a/protocol.py +78 -0
  90. solace_agent_mesh/common/sac/sam_component_base.py +406 -21
  91. solace_agent_mesh/common/utils/pydantic_utils.py +90 -3
  92. solace_agent_mesh/gateway/base/app.py +15 -0
  93. solace_agent_mesh/gateway/base/component.py +116 -46
  94. solace_agent_mesh/gateway/http_sse/app.py +7 -0
  95. solace_agent_mesh/gateway/http_sse/component.py +18 -10
  96. solace_agent_mesh/gateway/http_sse/dependencies.py +83 -59
  97. solace_agent_mesh/gateway/http_sse/main.py +5 -4
  98. solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +1 -1
  99. solace_agent_mesh/gateway/http_sse/routers/auth.py +103 -6
  100. solace_agent_mesh/gateway/http_sse/routers/config.py +1 -1
  101. solace_agent_mesh/gateway/http_sse/routers/sessions.py +1 -1
  102. solace_agent_mesh/gateway/http_sse/routers/sse.py +15 -5
  103. solace_agent_mesh/gateway/http_sse/routers/tasks.py +3 -3
  104. solace_agent_mesh/gateway/http_sse/routers/users.py +47 -1
  105. solace_agent_mesh/gateway/http_sse/routers/visualization.py +90 -8
  106. solace_agent_mesh/gateway/http_sse/services/session_service.py +1 -1
  107. solace_agent_mesh/gateway/http_sse/session_manager.py +15 -15
  108. solace_agent_mesh/gateway/http_sse/shared/exception_handlers.py +16 -1
  109. solace_agent_mesh/gateway/http_sse/sse_manager.py +15 -6
  110. solace_agent_mesh/templates/logging_config_template.ini +2 -2
  111. {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/METADATA +2 -2
  112. {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/RECORD +116 -114
  113. solace_agent_mesh/assets/docs/assets/js/240a0364.7eac6021.js +0 -1
  114. solace_agent_mesh/assets/docs/assets/js/631738c7.a8b1ef8b.js +0 -1
  115. solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +0 -1
  116. solace_agent_mesh/assets/docs/assets/js/e92d0134.cf6d6522.js +0 -1
  117. solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +0 -1
  118. solace_agent_mesh/assets/docs/assets/js/main.20feee82.js +0 -2
  119. solace_agent_mesh/assets/docs/assets/js/runtime~main.0d198646.js +0 -1
  120. solace_agent_mesh/assets/docs/lunr-index-1761165361160.json +0 -1
  121. solace_agent_mesh/assets/docs/search-doc-1761165361160.json +0 -1
  122. solace_agent_mesh/client/webui/frontend/static/assets/main-BGTaW0uv.js +0 -342
  123. solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +0 -1
  124. /solace_agent_mesh/assets/docs/assets/js/{main.20feee82.js.LICENSE.txt → main.d1643f0b.js.LICENSE.txt} +0 -0
  125. {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/WHEEL +0 -0
  126. {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/entry_points.txt +0 -0
  127. {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.2.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,8 @@ import logging
6
6
  import abc
7
7
  import asyncio
8
8
  import threading
9
- from typing import Any
9
+ import functools
10
+ from typing import Any, Optional
10
11
 
11
12
  from solace_ai_connector.components.component_base import ComponentBase
12
13
 
@@ -14,6 +15,8 @@ from ..exceptions import MessageSizeExceededError
14
15
  from ..utils.message_utils import validate_message_size
15
16
 
16
17
  log = logging.getLogger(__name__)
18
+ trace_logger = logging.getLogger("sam_trace")
19
+
17
20
 
18
21
  class SamComponentBase(ComponentBase, abc.ABC):
19
22
  """
@@ -54,7 +57,293 @@ class SamComponentBase(ComponentBase, abc.ABC):
54
57
 
55
58
  self._async_loop: asyncio.AbstractEventLoop | None = None
56
59
  self._async_thread: threading.Thread | None = None
57
- log.info("%s SamComponentBase initialized successfully.", self.log_identifier)
60
+
61
+ # Timer callback registry
62
+ self._timer_callbacks: dict[str, Any] = {}
63
+ self._timer_callbacks_lock = threading.Lock()
64
+
65
+ # Trust Manager integration (enterprise feature) - initialized as part of _late_init
66
+ self.trust_manager: Optional[Any] = None
67
+
68
+ log.info("%s Initialized SamComponentBase", self.log_identifier)
69
+
70
+ def add_timer(
71
+ self,
72
+ delay_ms: int,
73
+ timer_id: str,
74
+ interval_ms: int = 0,
75
+ callback: Optional[Any] = None,
76
+ ):
77
+ """
78
+ Add a timer with optional callback.
79
+
80
+ Args:
81
+ delay_ms: Initial delay in milliseconds
82
+ timer_id: Unique timer identifier
83
+ interval_ms: Repeat interval in milliseconds (0 for one-shot)
84
+ callback: Optional callback function to invoke when timer fires.
85
+ If provided, callback will be invoked when timer event occurs.
86
+ Callback receives timer_data dict as argument.
87
+ Callback should be thread-safe or schedule work appropriately.
88
+ """
89
+ # Register callback if provided
90
+ if callback:
91
+ with self._timer_callbacks_lock:
92
+ if timer_id in self._timer_callbacks:
93
+ log.warning(
94
+ "%s Timer ID '%s' already has a registered callback. Overwriting.",
95
+ self.log_identifier,
96
+ timer_id,
97
+ )
98
+ self._timer_callbacks[timer_id] = callback
99
+ log.debug(
100
+ "%s Registered callback for timer: %s",
101
+ self.log_identifier,
102
+ timer_id,
103
+ )
104
+
105
+ # Call parent implementation to actually create the timer
106
+ super().add_timer(delay_ms=delay_ms, timer_id=timer_id, interval_ms=interval_ms)
107
+
108
+ def cancel_timer(self, timer_id: str):
109
+ """
110
+ Cancel a timer and remove its callback if registered.
111
+
112
+ Args:
113
+ timer_id: Timer identifier to cancel
114
+ """
115
+ # Remove callback registration
116
+ with self._timer_callbacks_lock:
117
+ if timer_id in self._timer_callbacks:
118
+ del self._timer_callbacks[timer_id]
119
+ log.debug(
120
+ "%s Unregistered callback for timer: %s",
121
+ self.log_identifier,
122
+ timer_id,
123
+ )
124
+
125
+ # Call parent implementation to actually cancel the timer
126
+ super().cancel_timer(timer_id)
127
+
128
+ def process_event(self, event):
129
+ """
130
+ Process incoming events by routing to appropriate handlers.
131
+
132
+ This base implementation handles MESSAGE and TIMER events:
133
+ - MESSAGE events are routed to _handle_message() abstract method
134
+ - TIMER events are routed to registered callbacks
135
+ - Other events are passed to parent class
136
+
137
+ Args:
138
+ event: Event object from SAC framework
139
+ """
140
+ from solace_ai_connector.common.event import Event, EventType
141
+ from solace_ai_connector.common.message import Message as SolaceMessage
142
+
143
+ if event.event_type == EventType.MESSAGE:
144
+ message: SolaceMessage = event.data
145
+ topic = message.get_topic()
146
+
147
+ if not topic:
148
+ log.warning(
149
+ "%s Received message without topic. Ignoring.",
150
+ self.log_identifier,
151
+ )
152
+ try:
153
+ message.call_negative_acknowledgements()
154
+ except Exception as nack_e:
155
+ log.error(
156
+ "%s Failed to NACK message without topic: %s",
157
+ self.log_identifier,
158
+ nack_e,
159
+ )
160
+ return
161
+
162
+ try:
163
+ # Delegate to abstract method implemented by subclass
164
+ self._handle_message(message, topic)
165
+ except Exception as e:
166
+ log.error(
167
+ "%s Error in _handle_message for topic %s: %s",
168
+ self.log_identifier,
169
+ topic,
170
+ e,
171
+ exc_info=True,
172
+ )
173
+ try:
174
+ message.call_negative_acknowledgements()
175
+ except Exception as nack_e:
176
+ log.error(
177
+ "%s Failed to NACK message after error: %s",
178
+ self.log_identifier,
179
+ nack_e,
180
+ )
181
+ self.handle_error(e, event)
182
+
183
+ elif event.event_type == EventType.TIMER:
184
+ # Handle timer events via callback registry
185
+ timer_data = event.data
186
+ timer_id = timer_data.get("timer_id")
187
+
188
+ if not timer_id:
189
+ log.warning(
190
+ "%s Timer event missing timer_id: %s",
191
+ self.log_identifier,
192
+ timer_data,
193
+ )
194
+ return
195
+
196
+ # Look up registered callback
197
+ with self._timer_callbacks_lock:
198
+ callback = self._timer_callbacks.get(timer_id)
199
+
200
+ if callback:
201
+ try:
202
+ log.debug(
203
+ "%s Invoking registered callback for timer: %s",
204
+ self.log_identifier,
205
+ timer_id,
206
+ )
207
+ callback(timer_data)
208
+ except Exception as e:
209
+ log.error(
210
+ "%s Error in timer callback for %s: %s",
211
+ self.log_identifier,
212
+ timer_id,
213
+ e,
214
+ exc_info=True,
215
+ )
216
+ else:
217
+ log.warning(
218
+ "%s No callback registered for timer: %s. Timer event ignored.",
219
+ self.log_identifier,
220
+ timer_id,
221
+ )
222
+ else:
223
+ # Pass other event types to parent class
224
+ super().process_event(event)
225
+
226
+ def _handle_message(self, message, topic: str) -> None:
227
+ """
228
+ Handle an incoming message by routing to async handler.
229
+
230
+ This base implementation schedules async processing on the component's
231
+ event loop. Subclasses can override this for custom sync handling,
232
+ or implement _handle_message_async() for async handling.
233
+
234
+ Args:
235
+ message: The Solace message (SolaceMessage instance)
236
+ topic: The topic the message was received on
237
+ """
238
+ loop = self.get_async_loop()
239
+ if loop and loop.is_running():
240
+ # Schedule async processing
241
+ coro = self._handle_message_async(message, topic)
242
+ future = asyncio.run_coroutine_threadsafe(coro, loop)
243
+ future.add_done_callback(
244
+ functools.partial(self._handle_async_message_completion, topic=topic)
245
+ )
246
+ else:
247
+ log.error(
248
+ "%s Async loop not available. Cannot process message on topic: %s",
249
+ self.log_identifier,
250
+ topic,
251
+ )
252
+ raise RuntimeError("Async loop not available for message processing")
253
+
254
+ def _handle_async_message_completion(self, future: asyncio.Future, topic: str):
255
+ """Callback to handle completion of async message processing."""
256
+ try:
257
+ if future.cancelled():
258
+ log.warning(
259
+ "%s Message processing for topic %s was cancelled.",
260
+ self.log_identifier,
261
+ topic,
262
+ )
263
+ elif future.done():
264
+ exception = future.exception()
265
+ if exception is not None:
266
+ log.error(
267
+ "%s Message processing for topic %s failed: %s",
268
+ self.log_identifier,
269
+ topic,
270
+ exception,
271
+ exc_info=exception,
272
+ )
273
+ else:
274
+ # Handle successful completion
275
+ try:
276
+ _ = future.result()
277
+ log.debug(
278
+ "%s Message processing for topic %s completed successfully.",
279
+ self.log_identifier,
280
+ topic,
281
+ )
282
+ # Optional: Process the result if needed
283
+ # self._process_successful_result(result, topic)
284
+ except Exception as result_exception:
285
+ # This catches exceptions that might occur when getting the result
286
+ log.error(
287
+ "%s Error retrieving result for topic %s: %s",
288
+ self.log_identifier,
289
+ topic,
290
+ result_exception,
291
+ exc_info=result_exception,
292
+ )
293
+ else:
294
+ # This case shouldn't normally occur in a completion callback,
295
+ # but it's good defensive programming
296
+ log.warning(
297
+ "%s Future for topic %s is not done in completion handler.",
298
+ self.log_identifier,
299
+ topic,
300
+ )
301
+ except Exception as e:
302
+ log.error(
303
+ "%s Error in async message completion handler for topic %s: %s",
304
+ self.log_identifier,
305
+ topic,
306
+ e,
307
+ exc_info=True,
308
+ )
309
+
310
+ @abc.abstractmethod
311
+ async def _handle_message_async(self, message, topic: str) -> None:
312
+ """
313
+ Async handler for incoming messages.
314
+
315
+ Subclasses must implement this to process messages asynchronously.
316
+ This runs on the component's dedicated async event loop.
317
+
318
+ Args:
319
+ message: The Solace message (SolaceMessage instance)
320
+ topic: The topic the message was received on
321
+ """
322
+ pass
323
+
324
+ def _late_init(self):
325
+ """Late initialization hook called after the component is fully set up."""
326
+
327
+ # Setup the Trust Manager if present (enterprise feature)
328
+ # NOTE: The Trust Manager should use component.get_broker_username() to retrieve
329
+ # the actual broker client-username for trust card topic construction. This is
330
+ # critical because trust cards MUST be published on topics that match the actual
331
+ # authentication identity (client-username) used to connect to the broker.
332
+ try:
333
+ from solace_agent_mesh_enterprise.common.trust import (
334
+ initialize_trust_manager,
335
+ )
336
+
337
+ trust_config = self.get_config("trust_manager")
338
+ if trust_config and trust_config.get("enabled", False):
339
+ self.trust_manager = initialize_trust_manager(self)
340
+ log.info("%s Enterprise Trust Manager initialized", self.log_identifier)
341
+ except ImportError:
342
+ log.debug("%s Enterprise Trust Manager not available", self.log_identifier)
343
+ except Exception as e:
344
+ log.error(
345
+ "%s Failed to initialize Trust Manager: %s", self.log_identifier, e
346
+ )
58
347
 
59
348
  def publish_a2a_message(
60
349
  self, payload: dict, topic: str, user_properties: dict | None = None
@@ -62,7 +351,10 @@ class SamComponentBase(ComponentBase, abc.ABC):
62
351
  """Helper to publish A2A messages via the SAC App with size validation."""
63
352
  try:
64
353
  log.debug(
65
- f"{self.log_identifier} [publish_a2a_message] Starting - topic: {topic}, payload keys: {list(payload.keys()) if isinstance(payload, dict) else 'not_dict'}"
354
+ "%s [publish_a2a_message] Starting - topic: %s, payload keys: %s",
355
+ self.log_identifier,
356
+ topic,
357
+ list(payload.keys()) if isinstance(payload, dict) else "not_dict"
66
358
  )
67
359
 
68
360
  # Validate message size
@@ -75,14 +367,14 @@ class SamComponentBase(ComponentBase, abc.ABC):
75
367
  f"Message size validation failed: payload size ({actual_size} bytes) "
76
368
  f"exceeds maximum allowed size ({self.max_message_size_bytes} bytes)"
77
369
  )
78
- log.error("%s %s", self.log_identifier, error_msg)
370
+ log.error("%s [publish_a2a_message] %s", self.log_identifier, error_msg)
79
371
  raise MessageSizeExceededError(
80
372
  actual_size, self.max_message_size_bytes, error_msg
81
373
  )
82
374
 
83
375
  # Debug logging to show message size when publishing
84
376
  log.debug(
85
- "%s Publishing message to topic %s (size: %d bytes)",
377
+ "%s [publish_a2a_message] Publishing message to topic %s (size: %d bytes)",
86
378
  self.log_identifier,
87
379
  topic,
88
380
  actual_size,
@@ -91,7 +383,8 @@ class SamComponentBase(ComponentBase, abc.ABC):
91
383
  app = self.get_app()
92
384
  if app:
93
385
  log.debug(
94
- f"{self.log_identifier} [publish_a2a_message] Got app instance, about to call app.send_message"
386
+ "%s [publish_a2a_message] Got app instance, about to call app.send_message",
387
+ self.log_identifier
95
388
  )
96
389
 
97
390
  # Conditionally log to invocation monitor if it exists (i.e., on an agent)
@@ -103,19 +396,24 @@ class SamComponentBase(ComponentBase, abc.ABC):
103
396
  component_identifier=self.log_identifier,
104
397
  )
105
398
 
106
- log.debug(
107
- f"{self.log_identifier} [publish_a2a_message] About to call app.send_message with payload: {payload}"
108
- )
109
- log.debug(
110
- f"{self.log_identifier} [publish_a2a_message] App send_message params - topic: {topic}, user_properties: {user_properties}"
111
- )
399
+ if trace_logger.isEnabledFor(logging.DEBUG):
400
+ trace_logger.debug(
401
+ "%s [publish_a2a_message] About to call app.send_message on topic '%s'\nwith payload: %s\nwith user_properties: %s",
402
+ self.log_identifier, topic, payload, user_properties
403
+ )
404
+ else:
405
+ log.debug(
406
+ "%s [publish_a2a_message] About to call app.send_message on topic '%s' (for more details, enable TRACE logging)",
407
+ self.log_identifier, topic
408
+ )
112
409
 
113
410
  app.send_message(
114
411
  payload=payload, topic=topic, user_properties=user_properties
115
412
  )
116
413
 
117
414
  log.debug(
118
- f"{self.log_identifier} [publish_a2a_message] Successfully called app.send_message"
415
+ "%s [publish_a2a_message] Successfully called app.send_message on topic '%s'",
416
+ self.log_identifier, topic
119
417
  )
120
418
  else:
121
419
  log.error(
@@ -146,15 +444,11 @@ class SamComponentBase(ComponentBase, abc.ABC):
146
444
  main_task = None
147
445
  try:
148
446
  log.info(
149
- "%s Starting _async_setup_and_run as an asyncio task.",
447
+ "%s Starting _async_setup_and_run as an asyncio task. Will run event loop forever (or until stop_signal).",
150
448
  self.log_identifier,
151
449
  )
152
450
  main_task = self._async_loop.create_task(self._async_setup_and_run())
153
451
 
154
- log.info(
155
- "%s Running asyncio event loop forever (or until stop_signal).",
156
- self.log_identifier,
157
- )
158
452
  self._async_loop.run_forever()
159
453
 
160
454
  except Exception as e:
@@ -197,6 +491,10 @@ class SamComponentBase(ComponentBase, abc.ABC):
197
491
  def run(self):
198
492
  """Starts the component's dedicated async thread."""
199
493
  log.info("%s Starting SamComponentBase run method.", self.log_identifier)
494
+
495
+ # Do all initialization that needs to be done after we are fully setup
496
+ self._late_init()
497
+
200
498
  if not self._async_thread or not self._async_thread.is_alive():
201
499
  self._async_thread = threading.Thread(
202
500
  target=self._run_async_operations,
@@ -256,14 +554,101 @@ class SamComponentBase(ComponentBase, abc.ABC):
256
554
  """Returns the dedicated asyncio event loop for this component's async tasks."""
257
555
  return self._async_loop
258
556
 
557
+ def get_broker_username(self) -> Optional[str]:
558
+ """
559
+ Returns the broker username (client-username) that this component uses
560
+ to authenticate with the Solace broker.
561
+
562
+ This is critical for trust card publishing and verification, as the
563
+ trust card topic must match the actual authentication identity.
564
+
565
+ Returns:
566
+ The broker username if available, None otherwise.
567
+ """
568
+ try:
569
+ app = self.get_app()
570
+ if app and hasattr(app, "app_info"):
571
+ broker_config = app.app_info.get("broker", {})
572
+ broker_username = broker_config.get("broker_username")
573
+ if broker_username:
574
+ log.debug(
575
+ "%s Retrieved broker username: %s",
576
+ self.log_identifier,
577
+ broker_username,
578
+ )
579
+ return broker_username
580
+ else:
581
+ log.warning(
582
+ "%s Broker username not found in broker configuration",
583
+ self.log_identifier,
584
+ )
585
+ else:
586
+ log.warning(
587
+ "%s Unable to access app or app_info to retrieve broker username",
588
+ self.log_identifier,
589
+ )
590
+ except Exception as e:
591
+ log.error(
592
+ "%s Error retrieving broker username: %s",
593
+ self.log_identifier,
594
+ e,
595
+ exc_info=True,
596
+ )
597
+ return None
598
+
259
599
  @abc.abstractmethod
260
- async def _async_setup_and_run(self) -> None:
600
+ def _get_component_id(self) -> str:
261
601
  """
262
- Abstract method for subclasses to implement their main asynchronous logic.
263
- This coroutine is executed within the managed event loop.
602
+ Returns unique identifier for this component instance.
603
+ Must be implemented by subclasses.
604
+
605
+ Returns:
606
+ Unique component identifier (e.g., agent_name, gateway_id)
264
607
  """
265
608
  pass
266
609
 
610
+ @abc.abstractmethod
611
+ def _get_component_type(self) -> str:
612
+ """
613
+ Returns component type string.
614
+ Must be implemented by subclasses.
615
+
616
+ Returns:
617
+ Component type ("gateway", "agent", etc.)
618
+ """
619
+ pass
620
+
621
+ async def _async_setup_and_run(self) -> None:
622
+ """
623
+ Base async setup that initializes Trust Manager if present.
624
+ Subclasses should override and call super() first, then add their logic.
625
+ """
626
+ # Initialize Trust Manager if present (ENTERPRISE FEATURE)
627
+ if self.trust_manager:
628
+ try:
629
+ log.info(
630
+ "%s Initializing Trust Manager with periodic publishing...",
631
+ self.log_identifier,
632
+ )
633
+ # Pass event loop and add_timer method to Trust Manager
634
+ await self.trust_manager.initialize(
635
+ add_timer_callback=self.add_timer,
636
+ event_loop=self.get_async_loop(),
637
+ )
638
+ log.info(
639
+ "%s Initialized Trust Manager", self.log_identifier
640
+ )
641
+ except Exception as e:
642
+ log.error(
643
+ "%s Failed to initialize Trust Manager: %s",
644
+ self.log_identifier,
645
+ e,
646
+ exc_info=True,
647
+ )
648
+ # Trust Manager failure should not prevent component startup
649
+ # Set to None to disable trust manager for this session
650
+ self.trust_manager = None
651
+
267
652
  @abc.abstractmethod
268
653
  def _pre_async_cleanup(self) -> None:
269
654
  """
@@ -1,6 +1,7 @@
1
1
  """Provides a Pydantic BaseModel for SAM configuration with dict-like access."""
2
- from pydantic import BaseModel
3
- from typing import Any, Dict, Type, TypeVar
2
+ from pydantic import BaseModel, ValidationError
3
+ from typing import Any, TypeVar, Union, get_args, get_origin
4
+ from types import UnionType
4
5
 
5
6
  T = TypeVar("T", bound="SamConfigBase")
6
7
 
@@ -13,7 +14,7 @@ class SamConfigBase(BaseModel):
13
14
  """
14
15
 
15
16
  @classmethod
16
- def model_validate_and_clean(cls: Type[T], obj: Any) -> T:
17
+ def model_validate_and_clean(cls: type[T], obj: Any) -> T:
17
18
  """
18
19
  Validates a dictionary, first removing any keys with None values.
19
20
  This allows Pydantic's default values to be applied correctly when
@@ -24,6 +25,92 @@ class SamConfigBase(BaseModel):
24
25
  return cls.model_validate(cleaned_obj)
25
26
  return cls.model_validate(obj)
26
27
 
28
+ @classmethod
29
+ def format_validation_error_message(cls: type[T], error: ValidationError, app_name: str | None, agent_name: str | None = None) -> str:
30
+ """
31
+ Formats Pydantic validation error messages into a clear, actionable format.
32
+
33
+ Example output:
34
+ ---- Configuration validation failed for 'my-agent-app' ----
35
+
36
+ Agent Name: AgentConfig
37
+
38
+ ERROR 1:
39
+ Missing required field: 'namespace'
40
+ Location: app_config.namespace
41
+ Description: Absolute topic prefix for A2A communication (e.g., 'myorg/dev')
42
+
43
+ ---- Please update your YAML configuration ----
44
+ """
45
+
46
+ error_lines = [
47
+ f"\n---- Configuration validation failed for {app_name or 'UNKNOWN'} ----",
48
+ ""
49
+ ]
50
+
51
+ if agent_name:
52
+ error_lines.append(f" Agent Name: {agent_name}\n")
53
+
54
+ def get_nested_field_description(model_class: type[BaseModel], path: list[str | int]) -> str | None:
55
+ """Recursively get field description from nested models"""
56
+ if not path:
57
+ return None
58
+
59
+ current_field = path[0]
60
+ if str(current_field) not in model_class.model_fields:
61
+ return None
62
+
63
+ field_info = model_class.model_fields[str(current_field)]
64
+
65
+ if len(path) == 1:
66
+ return field_info.description
67
+
68
+ annotation = field_info.annotation
69
+
70
+ # Handle Optional/Union types
71
+ if annotation is not None:
72
+ origin = get_origin(annotation)
73
+ if origin is Union or origin is UnionType:
74
+ types = get_args(annotation)
75
+ annotation = next((t for t in types if t is not type(None)), None)
76
+ elif origin is list:
77
+ inner_type = get_args(annotation)[0]
78
+ if len(path) > 1 and isinstance(path[1], int):
79
+ if isinstance(inner_type, type) and issubclass(inner_type, BaseModel):
80
+ return get_nested_field_description(inner_type, path[2:])
81
+ return None
82
+ annotation = inner_type
83
+
84
+ if annotation is not None and isinstance(annotation, type) and issubclass(annotation, BaseModel):
85
+ return get_nested_field_description(annotation, path[1:])
86
+
87
+ return None
88
+
89
+
90
+ for index, err in enumerate(error.errors()):
91
+ error_type = err.get('type')
92
+ loc = err['loc']
93
+ msg = err['msg']
94
+
95
+ error_lines.append(f"ERROR {index + 1}:")
96
+
97
+ absolute_path = '.'.join(str(item) for item in loc)
98
+ description = get_nested_field_description(cls, list(loc))
99
+ if error_type == 'missing':
100
+ error_lines.extend([
101
+ f" Missing required field: '{loc[-1]}'",
102
+ ])
103
+ else:
104
+ error_lines.extend([
105
+ f" Error: {msg}",
106
+ ])
107
+ error_lines.append(f" Location: app_config.{absolute_path}")
108
+ error_lines.append(f" Description: {description or 'UNKNOWN'}")
109
+ error_lines.append("")
110
+
111
+ error_lines.append('---- Please update your YAML configuration ----')
112
+ return '\n'.join(error_lines) + "\n"
113
+
27
114
  def get(self, key: str, default: Any = None) -> Any:
28
115
  """Provides dict-like .get() method."""
29
116
  return getattr(self, key, default)
@@ -19,6 +19,7 @@ from ...common.a2a import (
19
19
 
20
20
  log = logging.getLogger(__name__)
21
21
 
22
+
22
23
  class BaseGatewayComponent(ComponentBase):
23
24
  pass
24
25
 
@@ -244,6 +245,20 @@ class BaseGatewayApp(App):
244
245
  )
245
246
  },
246
247
  ]
248
+
249
+ # Add trust card subscription if trust manager is enabled
250
+ trust_config = resolved_app_config_block.get("trust_manager")
251
+ if trust_config and trust_config.get("enabled", False):
252
+ from ...common.a2a.protocol import get_trust_card_subscription_topic
253
+
254
+ trust_card_topic = get_trust_card_subscription_topic(self.namespace)
255
+ subscriptions.append({"topic": trust_card_topic})
256
+ log.info(
257
+ "Trust Manager enabled for gateway '%s', added trust card subscription: %s",
258
+ self.gateway_id,
259
+ trust_card_topic,
260
+ )
261
+
247
262
  log.info(
248
263
  "Generated Solace subscriptions for gateway '%s': %s",
249
264
  self.gateway_id,