solace-agent-mesh 1.0.1__py3-none-any.whl → 1.0.3__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 (117) hide show
  1. solace_agent_mesh/__init__.py +5 -0
  2. solace_agent_mesh/agent/adk/callbacks.py +23 -1
  3. solace_agent_mesh/agent/adk/filesystem_artifact_service.py +34 -41
  4. solace_agent_mesh/agent/adk/runner.py +10 -6
  5. solace_agent_mesh/agent/adk/services.py +141 -60
  6. solace_agent_mesh/agent/protocol/event_handlers.py +84 -155
  7. solace_agent_mesh/agent/sac/app.py +12 -2
  8. solace_agent_mesh/agent/sac/component.py +182 -38
  9. solace_agent_mesh/agent/sac/task_execution_context.py +15 -6
  10. solace_agent_mesh/agent/tools/builtin_artifact_tools.py +8 -1
  11. solace_agent_mesh/agent/tools/general_agent_tools.py +4 -2
  12. solace_agent_mesh/agent/tools/peer_agent_tool.py +97 -88
  13. solace_agent_mesh/agent/utils/artifact_helpers.py +148 -14
  14. solace_agent_mesh/assets/docs/404.html +3 -3
  15. solace_agent_mesh/assets/docs/assets/js/{04989206.674a8007.js → 04989206.da8246cd.js} +1 -1
  16. solace_agent_mesh/assets/docs/assets/js/1023fc19.8e6d174c.js +1 -0
  17. solace_agent_mesh/assets/docs/assets/js/1c6e87d2.a8c5ce5a.js +1 -0
  18. solace_agent_mesh/assets/docs/assets/js/{3d406171.f722eaf5.js → 3d406171.9b081d5f.js} +1 -1
  19. solace_agent_mesh/assets/docs/assets/js/75384d09.c3991823.js +1 -0
  20. solace_agent_mesh/assets/docs/assets/js/9eff14a2.036c35ea.js +1 -0
  21. solace_agent_mesh/assets/docs/assets/js/{aba87c2f.d3e2dcc3.js → aba87c2f.a6b84da6.js} +1 -1
  22. solace_agent_mesh/assets/docs/assets/js/{ae4415af.8e279b5d.js → ae4415af.96189a93.js} +1 -1
  23. solace_agent_mesh/assets/docs/assets/js/b7006a3a.38c0cf3d.js +1 -0
  24. solace_agent_mesh/assets/docs/assets/js/bb2ef573.56931473.js +1 -0
  25. solace_agent_mesh/assets/docs/assets/js/{cc969b05.954186d4.js → cc969b05.bd3e0d6c.js} +1 -1
  26. solace_agent_mesh/assets/docs/assets/js/f284c35a.5aff74ab.js +1 -0
  27. solace_agent_mesh/assets/docs/assets/js/f897a61a.862b0514.js +1 -0
  28. solace_agent_mesh/assets/docs/assets/js/main.dc7db184.js +2 -0
  29. solace_agent_mesh/assets/docs/assets/js/runtime~main.aa687c82.js +1 -0
  30. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
  31. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
  32. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +8 -6
  33. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +4 -4
  34. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
  35. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
  36. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +21 -4
  37. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
  38. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
  39. solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
  40. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
  41. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
  42. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  43. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +10 -10
  44. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +5 -5
  46. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +5 -5
  49. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +140 -0
  50. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +4 -4
  51. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +4 -4
  52. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  53. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +4 -4
  54. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
  55. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +5 -5
  56. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
  57. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
  58. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
  59. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
  60. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
  61. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
  62. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
  63. solace_agent_mesh/assets/docs/img/solace-logo-text.svg +18 -0
  64. solace_agent_mesh/assets/docs/lunr-index-1755120326601.json +1 -0
  65. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  66. solace_agent_mesh/assets/docs/search-doc-1755120326601.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/init_cmd/env_step.py +8 -0
  71. solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +21 -1
  72. solace_agent_mesh/cli/commands/plugin_cmd/catalog_cmd.py +1 -0
  73. solace_agent_mesh/cli/commands/plugin_cmd/official_registry.py +2 -1
  74. solace_agent_mesh/cli/commands/run_cmd.py +42 -3
  75. solace_agent_mesh/cli/main.py +1 -3
  76. solace_agent_mesh/client/webui/frontend/static/assets/main-BCpII1-0.css +1 -0
  77. solace_agent_mesh/client/webui/frontend/static/assets/main-DzKPMTRs.js +673 -0
  78. solace_agent_mesh/client/webui/frontend/static/index.html +2 -2
  79. solace_agent_mesh/common/exceptions.py +25 -0
  80. solace_agent_mesh/common/utils/initializer.py +51 -0
  81. solace_agent_mesh/common/utils/message_utils.py +79 -0
  82. solace_agent_mesh/config_portal/backend/common.py +1 -1
  83. solace_agent_mesh/config_portal/backend/plugin_catalog/constants.py +2 -1
  84. solace_agent_mesh/config_portal/backend/plugin_catalog/registry_manager.py +6 -2
  85. solace_agent_mesh/config_portal/backend/plugin_catalog/scraper.py +1 -5
  86. solace_agent_mesh/config_portal/backend/plugin_catalog_server.py +1 -0
  87. solace_agent_mesh/config_portal/backend/server.py +1 -0
  88. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-xSu2leR8.js +48 -0
  89. solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-d2b54a97.js → manifest-950eb3be.js} +1 -1
  90. solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
  91. solace_agent_mesh/gateway/base/component.py +15 -2
  92. solace_agent_mesh/gateway/http_sse/routers/artifacts.py +9 -3
  93. solace_agent_mesh/gateway/http_sse/sse_manager.py +23 -1
  94. solace_agent_mesh/templates/gateway_component_template.py +75 -44
  95. solace_agent_mesh/templates/logging_config_template.ini +64 -0
  96. solace_agent_mesh/templates/plugin_agent_config_template.yaml +1 -1
  97. {solace_agent_mesh-1.0.1.dist-info → solace_agent_mesh-1.0.3.dist-info}/METADATA +56 -46
  98. {solace_agent_mesh-1.0.1.dist-info → solace_agent_mesh-1.0.3.dist-info}/RECORD +102 -96
  99. solace_agent_mesh/assets/docs/assets/js/1023fc19.015679ca.js +0 -1
  100. solace_agent_mesh/assets/docs/assets/js/1c6e87d2.23bccffb.js +0 -1
  101. solace_agent_mesh/assets/docs/assets/js/9eff14a2.1bf8f61c.js +0 -1
  102. solace_agent_mesh/assets/docs/assets/js/b7006a3a.40b10c9d.js +0 -1
  103. solace_agent_mesh/assets/docs/assets/js/bb2ef573.207e6990.js +0 -1
  104. solace_agent_mesh/assets/docs/assets/js/f284c35a.ecc3d195.js +0 -1
  105. solace_agent_mesh/assets/docs/assets/js/f897a61a.2c2e152c.js +0 -1
  106. solace_agent_mesh/assets/docs/assets/js/main.7ed3319f.js +0 -2
  107. solace_agent_mesh/assets/docs/assets/js/runtime~main.d9520ae2.js +0 -1
  108. solace_agent_mesh/assets/docs/img/Solace_AI_Framework_README.png +0 -0
  109. solace_agent_mesh/assets/docs/lunr-index-1753813536522.json +0 -1
  110. solace_agent_mesh/assets/docs/search-doc-1753813536522.json +0 -1
  111. solace_agent_mesh/client/webui/frontend/static/assets/main-An0a5j5k.js +0 -663
  112. solace_agent_mesh/client/webui/frontend/static/assets/main-Bu5-4Bac.css +0 -1
  113. solace_agent_mesh/config_portal/frontend/static/client/assets/_index-DNxCwAGB.js +0 -48
  114. /solace_agent_mesh/assets/docs/assets/js/{main.7ed3319f.js.LICENSE.txt → main.dc7db184.js.LICENSE.txt} +0 -0
  115. {solace_agent_mesh-1.0.1.dist-info → solace_agent_mesh-1.0.3.dist-info}/WHEEL +0 -0
  116. {solace_agent_mesh-1.0.1.dist-info → solace_agent_mesh-1.0.3.dist-info}/entry_points.txt +0 -0
  117. {solace_agent_mesh-1.0.1.dist-info → solace_agent_mesh-1.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -32,6 +32,7 @@ from ...common.types import (
32
32
  TaskStatus,
33
33
  TaskState,
34
34
  DataPart,
35
+ TextPart,
35
36
  A2ARequest,
36
37
  )
37
38
  from ...common.a2a_protocol import (
@@ -44,6 +45,7 @@ from ...common.a2a_protocol import (
44
45
  _extract_text_from_parts,
45
46
  )
46
47
  from ...agent.utils.artifact_helpers import (
48
+ generate_artifact_metadata_summary,
47
49
  load_artifact_content_or_metadata,
48
50
  )
49
51
  from ...agent.adk.runner import run_adk_async_task_thread_wrapper
@@ -57,114 +59,6 @@ from google.adk.events import Event as ADKEvent
57
59
  from google.genai import types as adk_types
58
60
 
59
61
 
60
- async def _format_artifact_summary_from_manifest(
61
- component: "SamAgentComponent",
62
- produced_artifacts: List[Dict[str, Any]],
63
- peer_agent_name: str,
64
- correlation_data: Dict[str, Any],
65
- ) -> str:
66
- """
67
- Loads metadata for a list of produced artifacts and formats it into a
68
- human-readable YAML summary string.
69
- """
70
- if not produced_artifacts:
71
- return ""
72
-
73
- artifact_summary_lines = [
74
- f"Peer agent `{peer_agent_name}` created {len(produced_artifacts)} artifact(s):"
75
- ]
76
-
77
- original_task_context = correlation_data.get("original_task_context", {})
78
- user_id = original_task_context.get("user_id")
79
- session_id = original_task_context.get("session_id")
80
-
81
- if not (component.artifact_service and user_id and session_id):
82
- log.warning(
83
- "%s Cannot load artifact metadata: missing artifact_service or context.",
84
- component.log_identifier,
85
- )
86
- for artifact_ref in produced_artifacts:
87
- artifact_summary_lines.append(
88
- f"- `{artifact_ref.get('filename')}` (v{artifact_ref.get('version')})"
89
- )
90
- return "\n".join(artifact_summary_lines)
91
-
92
- peer_agent_name_for_artifact = peer_agent_name
93
- if (
94
- not peer_agent_name_for_artifact
95
- or peer_agent_name_for_artifact == "A peer agent"
96
- ):
97
- log.warning(
98
- "%s Peer agent name not in task metadata, using self agent name for artifact loading.",
99
- component.log_identifier,
100
- )
101
- peer_agent_name_for_artifact = component.agent_name
102
-
103
- for artifact_ref in produced_artifacts:
104
- filename = artifact_ref.get("filename")
105
- version = artifact_ref.get("version")
106
- if not filename or version is None:
107
- continue
108
-
109
- try:
110
- metadata_result = await load_artifact_content_or_metadata(
111
- artifact_service=component.artifact_service,
112
- app_name=peer_agent_name_for_artifact,
113
- user_id=user_id,
114
- session_id=session_id,
115
- filename=filename,
116
- version=version,
117
- load_metadata_only=True,
118
- )
119
- if metadata_result.get("status") == "success":
120
- metadata = metadata_result.get("metadata", {})
121
- TRUNCATION_LIMIT_BYTES = 1024
122
- TRUNCATION_MESSAGE = "\n... [truncated] ..."
123
-
124
- try:
125
- formatted_metadata_str = yaml.safe_dump(
126
- metadata,
127
- default_flow_style=False,
128
- sort_keys=False,
129
- allow_unicode=True,
130
- )
131
-
132
- if (
133
- len(formatted_metadata_str.encode("utf-8"))
134
- > TRUNCATION_LIMIT_BYTES
135
- ):
136
- cutoff = TRUNCATION_LIMIT_BYTES - len(
137
- TRUNCATION_MESSAGE.encode("utf-8")
138
- )
139
- formatted_metadata_str = (
140
- formatted_metadata_str[:cutoff] + TRUNCATION_MESSAGE
141
- )
142
-
143
- summary_line = f"- `{filename}` (v{version}):\n ```yaml\n{formatted_metadata_str}\n ```"
144
- artifact_summary_lines.append(summary_line)
145
- except Exception as e_format:
146
- log.error(
147
- "Error formatting metadata for %s v%s: %s",
148
- filename,
149
- version,
150
- e_format,
151
- )
152
- artifact_summary_lines.append(
153
- f"- `{filename}` (v{version}): Error formatting metadata."
154
- )
155
- else:
156
- artifact_summary_lines.append(
157
- f"- `{filename}` (v{version}): Could not load metadata."
158
- )
159
- except Exception as e_meta:
160
- log.error(
161
- "Error loading metadata for %s v%s: %s", filename, version, e_meta
162
- )
163
- artifact_summary_lines.append(
164
- f"- `{filename}` (v{version}): Error loading metadata."
165
- )
166
-
167
- return "\n".join(artifact_summary_lines)
168
62
 
169
63
 
170
64
  def _register_peer_artifacts_in_parent_context(
@@ -248,7 +142,7 @@ async def process_event(component, event: Event):
248
142
  elif topic.startswith(agent_response_sub_prefix) or topic.startswith(
249
143
  agent_status_sub_prefix
250
144
  ):
251
- handle_a2a_response(component, message)
145
+ await handle_a2a_response(component, message)
252
146
  else:
253
147
  log.warning(
254
148
  "%s Received message on unhandled topic: %s",
@@ -264,31 +158,8 @@ async def process_event(component, event: Event):
264
158
  if timer_data.get("timer_id") == component._card_publish_timer_id:
265
159
  publish_agent_card(component)
266
160
  elif event.event_type == EventType.CACHE_EXPIRY:
267
- cache_data = event.data
268
- log.info(
269
- "%s Received cache expiry event: %s",
270
- component.log_identifier,
271
- cache_data,
272
- )
273
- sub_task_id = cache_data.get("key")
274
- if sub_task_id and sub_task_id.startswith(
275
- component.CORRELATION_DATA_PREFIX
276
- ):
277
- expired_data = cache_data.get("expired_data")
278
- if expired_data:
279
- await component._handle_peer_timeout(sub_task_id, expired_data)
280
- else:
281
- log.error(
282
- "%s Missing expired_data in cache expiry event for sub-task %s. Cannot process timeout.",
283
- component.log_identifier,
284
- sub_task_id,
285
- )
286
- else:
287
- log.debug(
288
- "%s Cache expiry for key '%s' is not a peer sub-task timeout.",
289
- component.log_identifier,
290
- sub_task_id,
291
- )
161
+ # Delegate cache expiry handling to the component itself.
162
+ await component.handle_cache_expiry_event(event.data)
292
163
  else:
293
164
  log.warning(
294
165
  "%s Received unknown event type: %s",
@@ -633,8 +504,51 @@ async def handle_a2a_request(component, message: SolaceMessage):
633
504
  logical_task_id,
634
505
  )
635
506
 
507
+ a2a_message_for_adk = a2a_request.params.message
508
+ invoked_artifacts = (
509
+ a2a_message_for_adk.metadata.get("invoked_with_artifacts", [])
510
+ if a2a_message_for_adk.metadata
511
+ else []
512
+ )
513
+
514
+ if invoked_artifacts:
515
+ log.info(
516
+ "%s Task %s invoked with %d artifact(s). Preparing context from metadata.",
517
+ component.log_identifier,
518
+ task_id,
519
+ len(invoked_artifacts),
520
+ )
521
+ header_text = (
522
+ "The user has provided the following artifacts as context for your task. "
523
+ "Use the information contained within their metadata to complete your objective."
524
+ )
525
+ artifact_summary = await generate_artifact_metadata_summary(
526
+ component=component,
527
+ artifact_identifiers=invoked_artifacts,
528
+ user_id=user_id,
529
+ session_id=effective_session_id,
530
+ app_name=agent_name,
531
+ header_text=header_text,
532
+ )
533
+
534
+ task_description = _extract_text_from_parts(
535
+ a2a_message_for_adk.parts
536
+ )
537
+ final_prompt = f"{task_description}\n\n{artifact_summary}"
538
+
539
+ a2a_message_for_adk = A2AMessage(
540
+ role="user",
541
+ parts=[TextPart(text=final_prompt)],
542
+ metadata=a2a_message_for_adk.metadata,
543
+ )
544
+ log.debug(
545
+ "%s Generated new prompt for task %s with artifact context.",
546
+ component.log_identifier,
547
+ task_id,
548
+ )
549
+
636
550
  adk_content = translate_a2a_to_adk_content(
637
- a2a_request.params.message, component.log_identifier
551
+ a2a_message_for_adk, component.log_identifier
638
552
  )
639
553
 
640
554
  adk_session = await component.session_service.get_session(
@@ -854,10 +768,9 @@ def handle_agent_card_message(component, message: SolaceMessage):
854
768
  component.handle_error(e, Event(EventType.MESSAGE, message))
855
769
 
856
770
 
857
- def handle_a2a_response(component, message: SolaceMessage):
771
+ async def handle_a2a_response(component, message: SolaceMessage):
858
772
  """Handles incoming responses/status updates from peer agents."""
859
773
  sub_task_id = None
860
- agent_name = component.get_config("agent_name")
861
774
  payload_to_queue = None
862
775
  is_final_response = False
863
776
 
@@ -932,10 +845,8 @@ def handle_a2a_response(component, message: SolaceMessage):
932
845
  component.log_identifier,
933
846
  sub_task_id,
934
847
  )
935
- correlation_data = (
936
- component.cache_service.get_data(
937
- sub_task_id
938
- )
848
+ correlation_data = await component._get_correlation_data_for_sub_task(
849
+ sub_task_id
939
850
  )
940
851
  if not correlation_data:
941
852
  log.warning(
@@ -1227,7 +1138,12 @@ def handle_a2a_response(component, message: SolaceMessage):
1227
1138
  "error": f"Failed to parse response from peer: {parse_error}",
1228
1139
  "code": "PEER_PARSE_ERROR",
1229
1140
  }
1230
- is_final_response = True
1141
+ # Print out the stack trace for debugging
1142
+ log.exception(
1143
+ "%s Exception stack trace: %s",
1144
+ component.log_identifier,
1145
+ parse_error,
1146
+ )
1231
1147
 
1232
1148
  if not is_final_response:
1233
1149
  # This is an intermediate status update for monitoring.
@@ -1240,13 +1156,9 @@ def handle_a2a_response(component, message: SolaceMessage):
1240
1156
  message.call_acknowledgements()
1241
1157
  return
1242
1158
 
1243
- correlation_data = component.cache_service.get_data(sub_task_id)
1159
+ correlation_data = await component._claim_peer_sub_task_completion(sub_task_id)
1244
1160
  if not correlation_data:
1245
- log.warning(
1246
- "%s No correlation data found for sub-task %s. Cannot process response. Ignoring.",
1247
- component.log_identifier,
1248
- sub_task_id,
1249
- )
1161
+ # The helper method logs the reason (timeout, already claimed, etc.)
1250
1162
  message.call_acknowledgements()
1251
1163
  return
1252
1164
 
@@ -1309,14 +1221,31 @@ def handle_a2a_response(component, message: SolaceMessage):
1309
1221
  peer_agent_name = task_obj.metadata.get(
1310
1222
  "agent_name", "A peer agent"
1311
1223
  )
1312
- artifact_summary = (
1313
- await _format_artifact_summary_from_manifest(
1314
- component,
1315
- produced_artifacts,
1316
- peer_agent_name,
1317
- correlation_data,
1318
- )
1224
+ original_task_context = correlation_data.get(
1225
+ "original_task_context", {}
1319
1226
  )
1227
+ user_id = original_task_context.get("user_id")
1228
+ session_id = original_task_context.get("session_id")
1229
+
1230
+ header_text = f"Peer agent `{peer_agent_name}` created {len(produced_artifacts)} artifact(s):"
1231
+
1232
+ if user_id and session_id:
1233
+ artifact_summary = (
1234
+ await generate_artifact_metadata_summary(
1235
+ component=component,
1236
+ artifact_identifiers=produced_artifacts,
1237
+ user_id=user_id,
1238
+ session_id=session_id,
1239
+ app_name=peer_agent_name,
1240
+ header_text=header_text,
1241
+ )
1242
+ )
1243
+ else:
1244
+ log.warning(
1245
+ "%s Could not generate artifact summary: missing user_id or session_id in correlation data.",
1246
+ log_retrigger,
1247
+ )
1248
+ artifact_summary = ""
1320
1249
  # Bubble up the peer's artifacts to the parent context
1321
1250
  _register_peer_artifacts_in_parent_context(
1322
1251
  task_context, task_obj, log_retrigger
@@ -246,8 +246,8 @@ class SamAgentApp(App):
246
246
  "type": "string",
247
247
  "required": False,
248
248
  "default": "namespace",
249
- "enum": ["namespace", "app", "custom"],
250
- "description": "Scope for filesystem artifacts: 'namespace' (default, shared by namespace), 'app' (isolated by app name), 'custom' (use artifact_scope_value).",
249
+ "enum": ["namespace", "app"],
250
+ "description": "Process-wide scope for all artifact services. 'namespace' (default): shared by all components in the namespace. 'app': isolated by agent/gateway name. This setting must be consistent for all components in the same process.",
251
251
  },
252
252
  "artifact_scope_value": {
253
253
  "type": "string",
@@ -340,6 +340,13 @@ class SamAgentApp(App):
340
340
  "default": 0,
341
341
  "description": "Minimum size in bytes for accumulated text from LLM stream before sending a status update. If 0 or less, batching is disabled and updates are sent per chunk. Final LLM chunks are always sent regardless of this threshold.",
342
342
  },
343
+ {
344
+ "name": "max_message_size_bytes",
345
+ "required": False,
346
+ "type": "integer",
347
+ "default": 10_000_000,
348
+ "description": "Maximum allowed message size in bytes before rejecting publication to prevent broker disconnections. Default: 10MB",
349
+ },
343
350
  {
344
351
  "name": "enable_artifact_content_instruction",
345
352
  "required": False,
@@ -551,6 +558,7 @@ class SamAgentApp(App):
551
558
  app_config = app_info.get("app_config", {})
552
559
  namespace = app_config.get("namespace")
553
560
  agent_name = app_config.get("agent_name")
561
+ broker_request_response = app_info.get("broker_request_response")
554
562
 
555
563
  if not namespace or not isinstance(namespace, str):
556
564
  raise ValueError(
@@ -619,6 +627,8 @@ class SamAgentApp(App):
619
627
  "component_config": {},
620
628
  "subscriptions": generated_subs,
621
629
  }
630
+ if broker_request_response:
631
+ component_definition["broker_request_response"] = broker_request_response
622
632
 
623
633
  app_info["components"] = [component_definition]
624
634
  log.debug("Replaced 'components' in app_info with programmatic definition.")
@@ -53,6 +53,8 @@ from ...common.types import (
53
53
  TaskStatusUpdateEvent,
54
54
  TaskArtifactUpdateEvent,
55
55
  SendTaskRequest,
56
+ CancelTaskRequest,
57
+ TaskIdParams,
56
58
  )
57
59
  from ...common.a2a_protocol import (
58
60
  get_a2a_base_topic,
@@ -65,7 +67,9 @@ from ...common.a2a_protocol import (
65
67
  get_gateway_status_topic,
66
68
  )
67
69
  from ...agent.utils.config_parser import resolve_instruction_provider
68
- from ...agent.utils.artifact_helpers import get_latest_artifact_version
70
+ from ...agent.utils.artifact_helpers import (
71
+ get_latest_artifact_version,
72
+ )
69
73
  from ...agent.adk.services import (
70
74
  initialize_session_service,
71
75
  initialize_artifact_service,
@@ -90,6 +94,8 @@ from ...agent.adk.invocation_monitor import InvocationMonitor
90
94
  from ...common.middleware.registry import MiddlewareRegistry
91
95
  from ...common.constants import DEFAULT_COMMUNICATION_TIMEOUT
92
96
  from ...agent.tools.registry import tool_registry
97
+ from ...common.utils.message_utils import validate_message_size
98
+ from ...common.exceptions import MessageSizeExceededError
93
99
 
94
100
  if TYPE_CHECKING:
95
101
  from .task_execution_context import TaskExecutionContext
@@ -230,6 +236,11 @@ class SamAgentComponent(ComponentBase):
230
236
  raise ValueError(
231
237
  "Internal Error: Inter-agent comms config missing after validation."
232
238
  )
239
+
240
+ self.max_message_size_bytes = self.get_config(
241
+ "max_message_size_bytes", 10_000_000
242
+ )
243
+
233
244
  log.info("%s Configuration retrieved successfully.", self.log_identifier)
234
245
  except Exception as e:
235
246
  log.error(
@@ -392,6 +403,7 @@ class SamAgentComponent(ComponentBase):
392
403
  self.session_service = initialize_session_service(self)
393
404
  self.artifact_service = initialize_artifact_service(self)
394
405
  self.memory_service = initialize_memory_service(self)
406
+
395
407
  log.info(
396
408
  "%s Synchronous ADK services initialized.", self.log_identifier
397
409
  )
@@ -536,49 +548,123 @@ class SamAgentComponent(ComponentBase):
536
548
  if timer_data.get("timer_id") == self._card_publish_timer_id:
537
549
  publish_agent_card(self)
538
550
 
539
- def handle_cache_expiry_event(self, cache_data: Dict[str, Any]):
540
- """Handles cache expiry events, specifically for peer timeouts."""
551
+ async def handle_cache_expiry_event(self, cache_data: Dict[str, Any]):
552
+ """
553
+ Handles cache expiry events for peer timeouts by calling the atomic claim helper.
554
+ """
541
555
  log.debug("%s Received cache expiry event: %s", self.log_identifier, cache_data)
542
- expired_key = cache_data.get("key")
543
- expired_data = cache_data.get("expired_data")
556
+ sub_task_id = cache_data.get("key")
557
+ logical_task_id = cache_data.get("expired_data")
558
+
559
+ if not (
560
+ sub_task_id
561
+ and sub_task_id.startswith(CORRELATION_DATA_PREFIX)
562
+ and logical_task_id
563
+ ):
564
+ log.debug(
565
+ "%s Cache expiry for key '%s' is not a peer sub-task timeout or is missing data.",
566
+ self.log_identifier,
567
+ sub_task_id,
568
+ )
569
+ return
570
+
571
+ correlation_data = await self._claim_peer_sub_task_completion(
572
+ sub_task_id=sub_task_id, logical_task_id_from_event=logical_task_id
573
+ )
544
574
 
545
- if expired_key and expired_key.startswith(CORRELATION_DATA_PREFIX):
546
- sub_task_id = expired_key
575
+ if correlation_data:
547
576
  log.warning(
548
- "%s Detected timeout for sub-task ID: %s",
577
+ "%s Detected timeout for sub-task %s (Main Task: %s). Claimed successfully.",
549
578
  self.log_identifier,
550
579
  sub_task_id,
580
+ logical_task_id,
551
581
  )
552
- if expired_data:
553
- try:
554
- original_task_context = expired_data.get("original_task_context")
555
- if original_task_context:
556
- self._handle_peer_timeout(sub_task_id, expired_data)
557
- else:
558
- log.error(
559
- "%s Missing 'original_task_context' in expired cache data for sub-task %s. Cannot process timeout.",
560
- self.log_identifier,
561
- sub_task_id,
562
- )
563
- except Exception as e:
564
- log.exception(
565
- "%s Error handling peer timeout for sub-task %s: %s",
566
- self.log_identifier,
567
- sub_task_id,
568
- e,
569
- )
570
- else:
571
- log.error(
572
- "%s Missing expired_data in cache expiry event for sub-task %s. Cannot process timeout.",
573
- self.log_identifier,
574
- sub_task_id,
575
- )
582
+ await self._handle_peer_timeout(sub_task_id, correlation_data)
576
583
  else:
577
- log.debug(
578
- "%s Cache expiry for key '%s' is not a peer sub-task timeout.",
584
+ log.info(
585
+ "%s Ignoring timeout event for sub-task %s as it was already completed.",
586
+ self.log_identifier,
587
+ sub_task_id,
588
+ )
589
+
590
+ async def _get_correlation_data_for_sub_task(
591
+ self, sub_task_id: str
592
+ ) -> Optional[Dict[str, Any]]:
593
+ """
594
+ Non-destructively retrieves correlation data for a sub-task.
595
+ Used for intermediate events where the sub-task should remain active.
596
+ """
597
+ logical_task_id = self.cache_service.get_data(sub_task_id)
598
+ if not logical_task_id:
599
+ log.warning(
600
+ "%s No cache entry for sub-task %s. Cannot get correlation data.",
579
601
  self.log_identifier,
580
- expired_key,
602
+ sub_task_id,
603
+ )
604
+ return None
605
+
606
+ with self.active_tasks_lock:
607
+ task_context = self.active_tasks.get(logical_task_id)
608
+
609
+ if not task_context:
610
+ log.error(
611
+ "%s TaskExecutionContext not found for task %s, but cache entry existed for sub-task %s. This may indicate a cleanup issue.",
612
+ self.log_identifier,
613
+ logical_task_id,
614
+ sub_task_id,
615
+ )
616
+ return None
617
+
618
+ with task_context.lock:
619
+ return task_context.active_peer_sub_tasks.get(sub_task_id)
620
+
621
+ async def _claim_peer_sub_task_completion(
622
+ self, sub_task_id: str, logical_task_id_from_event: Optional[str] = None
623
+ ) -> Optional[Dict[str, Any]]:
624
+ """
625
+ Atomically claims a sub-task as complete, preventing race conditions.
626
+ This is a destructive operation that removes state.
627
+
628
+ Args:
629
+ sub_task_id: The ID of the sub-task to claim.
630
+ logical_task_id_from_event: The parent task ID, if provided by the event (e.g., a timeout).
631
+ If not provided, it will be looked up from the cache.
632
+ """
633
+ log_id = f"{self.log_identifier}[ClaimSubTask:{sub_task_id}]"
634
+ logical_task_id = logical_task_id_from_event
635
+
636
+ if not logical_task_id:
637
+ logical_task_id = self.cache_service.get_data(sub_task_id)
638
+ if not logical_task_id:
639
+ log.warning(
640
+ "%s No cache entry found. Task has likely timed out and been cleaned up. Cannot claim.",
641
+ log_id,
642
+ )
643
+ return None
644
+
645
+ with self.active_tasks_lock:
646
+ task_context = self.active_tasks.get(logical_task_id)
647
+
648
+ if not task_context:
649
+ log.error(
650
+ "%s TaskExecutionContext not found for task %s. Cleaning up stale cache entry.",
651
+ log_id,
652
+ logical_task_id,
581
653
  )
654
+ self.cache_service.remove_data(sub_task_id)
655
+ return None
656
+
657
+ correlation_data = task_context.claim_sub_task_completion(sub_task_id)
658
+
659
+ if correlation_data:
660
+ # If we successfully claimed the task, remove the timeout tracker from the cache.
661
+ self.cache_service.remove_data(sub_task_id)
662
+ log.info("%s Successfully claimed completion.", log_id)
663
+ return correlation_data
664
+ else:
665
+ # This means the task was already claimed by a competing event (e.g., timeout vs. response).
666
+ log.warning("%s Failed to claim; it was already completed.", log_id)
667
+ return None
582
668
 
583
669
  async def _retrigger_agent_with_peer_responses(
584
670
  self,
@@ -686,8 +772,9 @@ class SamAgentComponent(ComponentBase):
686
772
  correlation_data: Dict[str, Any],
687
773
  ):
688
774
  """
689
- Handles the timeout of a peer agent task by updating the completion counter
690
- and potentially re-triggering the runner if all parallel tasks are now complete.
775
+ Handles the timeout of a peer agent task. It sends a cancellation request
776
+ to the peer, updates the local completion counter, and potentially
777
+ re-triggers the runner if all parallel tasks are now complete.
691
778
  """
692
779
  logical_task_id = correlation_data.get("logical_task_id")
693
780
  invocation_id = correlation_data.get("invocation_id")
@@ -700,6 +787,36 @@ class SamAgentComponent(ComponentBase):
700
787
  invocation_id,
701
788
  )
702
789
 
790
+ # Proactively send a cancellation request to the peer agent.
791
+ peer_agent_name = correlation_data.get("peer_agent_name")
792
+ if peer_agent_name:
793
+ try:
794
+ log.info(
795
+ "%s Sending CancelTaskRequest to peer '%s' for timed-out sub-task %s.",
796
+ log_retrigger,
797
+ peer_agent_name,
798
+ sub_task_id,
799
+ )
800
+ task_id_for_peer = sub_task_id.replace(CORRELATION_DATA_PREFIX, "", 1)
801
+ cancel_params = TaskIdParams(id=task_id_for_peer)
802
+ cancel_request = CancelTaskRequest(params=cancel_params)
803
+ user_props = {"clientId": self.agent_name}
804
+ peer_topic = self._get_agent_request_topic(peer_agent_name)
805
+ self._publish_a2a_message(
806
+ payload=cancel_request.model_dump(exclude_none=True),
807
+ topic=peer_topic,
808
+ user_properties=user_props,
809
+ )
810
+ except Exception as e:
811
+ log.error(
812
+ "%s Failed to send CancelTaskRequest to peer '%s' for sub-task %s: %s",
813
+ log_retrigger,
814
+ peer_agent_name,
815
+ sub_task_id,
816
+ e,
817
+ )
818
+
819
+ # Process the timeout locally.
703
820
  with self.active_tasks_lock:
704
821
  task_context = self.active_tasks.get(logical_task_id)
705
822
 
@@ -1841,7 +1958,8 @@ class SamAgentComponent(ComponentBase):
1841
1958
  len(unprocessed_tail.encode("utf-8")),
1842
1959
  )
1843
1960
  else:
1844
- resolved_text = resolved_text + unprocessed_tail
1961
+ if unprocessed_tail is not None and unprocessed_tail != "":
1962
+ resolved_text = resolved_text + unprocessed_tail
1845
1963
 
1846
1964
  if signals_found:
1847
1965
  log.info(
@@ -2860,6 +2978,29 @@ class SamAgentComponent(ComponentBase):
2860
2978
  ):
2861
2979
  """Helper to publish A2A messages via the SAC App."""
2862
2980
  try:
2981
+ max_size_bytes = self.max_message_size_bytes
2982
+
2983
+ # Validate message size
2984
+ is_valid, actual_size = validate_message_size(
2985
+ payload, max_size_bytes, self.log_identifier
2986
+ )
2987
+
2988
+ if not is_valid:
2989
+ error_msg = (
2990
+ f"Message size validation failed: payload size ({actual_size} bytes) "
2991
+ f"exceeds maximum allowed size ({max_size_bytes} bytes)"
2992
+ )
2993
+ log.error("%s %s", self.log_identifier, error_msg)
2994
+ raise MessageSizeExceededError(actual_size, max_size_bytes, error_msg)
2995
+
2996
+ # Debug logging to show message size when publishing
2997
+ log.debug(
2998
+ "%s Publishing message to topic %s (size: %d bytes)",
2999
+ self.log_identifier,
3000
+ topic,
3001
+ actual_size,
3002
+ )
3003
+
2863
3004
  app = self.get_app()
2864
3005
  if app:
2865
3006
  if self.invocation_monitor:
@@ -2877,6 +3018,9 @@ class SamAgentComponent(ComponentBase):
2877
3018
  "%s Cannot publish message: Not running within a SAC App context.",
2878
3019
  self.log_identifier,
2879
3020
  )
3021
+ except MessageSizeExceededError:
3022
+ # Re-raise MessageSizeExceededError without wrapping
3023
+ raise
2880
3024
  except Exception as e:
2881
3025
  log.exception(
2882
3026
  "%s Failed to publish A2A message to topic %s: %s",