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
@@ -0,0 +1,5 @@
1
+ """
2
+ Solace Agent Mesh Initialization
3
+ """
4
+ from .common.utils.initializer import initialize
5
+ initialize()
@@ -785,6 +785,26 @@ It can span multiple lines.
785
785
 
786
786
  The system will automatically save the content and give you a confirmation in the next turn."""
787
787
 
788
+ def _generate_artifact_creation_instruction() -> str:
789
+ return """
790
+ **Creating Text-Based Artifacts:**
791
+
792
+ **When to Create Text-based Artifacts:**
793
+ Create an artifact when the content provides value as a standalone file:
794
+ - Content with special formatting (HTML, Markdown, CSS, structured markup) that requires proper rendering
795
+ - Content explicitly intended for use outside this conversation (reports, emails, presentations, reference documents)
796
+ - Structured reference content users will save or follow (schedules, guides, templates)
797
+ - Content that will be edited, expanded, or reused
798
+ - Substantial text documents
799
+ - Technical documentation meant as reference material
800
+
801
+ **When NOT to Create Text-based Artifacts:**
802
+ - Simple answers, explanations, or conversational responses
803
+ - Brief advice, opinions, or quick information
804
+ - Short lists, summaries, or single paragraphs
805
+ - Temporary content only relevant to the immediate conversation
806
+ - Basic explanations that don't require reference material
807
+ """
788
808
 
789
809
  def _generate_embed_instruction(
790
810
  include_artifact_content: bool,
@@ -918,7 +938,7 @@ When faced with a complex goal or request that involves multiple steps, data ret
918
938
  Simple, direct requests like 'create an image of a dog' or 'write an email to thank my boss' do not require a plan.
919
939
 
920
940
  If a plan is created:
921
- 1. It should be a terse, hierarchical list describing the steps needed.
941
+ 1. It should be a terse, hierarchical list describing the steps needed, with each checkbox item on its own line.
922
942
  2. Use '☐' (empty checkbox emoji) for pending items and '☑' (checked checkbox emoji) for completed items.
923
943
  3. If the plan changes significantly during execution, restate the updated plan.
924
944
  4. As items are completed, update the plan to check them off.
@@ -926,6 +946,8 @@ If a plan is created:
926
946
  """
927
947
  injected_instructions.append(planning_instruction)
928
948
  log.debug("%s Added hardcoded planning instructions.", log_identifier)
949
+ artifact_creation_instruction = _generate_artifact_creation_instruction()
950
+ injected_instructions.append(artifact_creation_instruction)
929
951
  fenced_artifact_instruction = _generate_fenced_artifact_instruction()
930
952
  injected_instructions.append(fenced_artifact_instruction)
931
953
 
@@ -23,49 +23,40 @@ class FilesystemArtifactService(BaseArtifactService):
23
23
  """
24
24
  An artifact service implementation using the local filesystem.
25
25
 
26
- Stores artifacts in a structured directory based on a configured scope
27
- (namespace, app name, or custom), user ID, session ID (or 'user' namespace),
26
+ Stores artifacts in a structured directory based on the effective app name
27
+ (which represents the scope), user ID, session ID (or 'user' namespace),
28
28
  filename, and version. Metadata (like mime_type) is stored in a companion file.
29
29
  """
30
30
 
31
- def __init__(self, base_path: str, scope_identifier: str):
31
+ def __init__(self, base_path: str):
32
32
  """
33
33
  Initializes the FilesystemArtifactService.
34
34
 
35
35
  Args:
36
36
  base_path: The root directory where all artifacts will be stored.
37
- scope_identifier: The sanitized identifier representing the storage scope
38
- (e.g., sanitized namespace, app name, or custom value).
39
37
 
40
38
  Raises:
41
- ValueError: If base_path or scope_identifier is not provided or the
42
- scoped base path cannot be created.
39
+ ValueError: If base_path is not provided or cannot be created.
43
40
  """
44
41
  if not base_path:
45
42
  raise ValueError("base_path cannot be empty for FilesystemArtifactService")
46
- if not scope_identifier:
47
- raise ValueError(
48
- "scope_identifier cannot be empty for FilesystemArtifactService"
49
- )
50
43
 
51
44
  self.base_path = os.path.abspath(base_path)
52
- self.scope_identifier = scope_identifier
53
- self.scope_base_path = os.path.join(self.base_path, self.scope_identifier)
54
45
 
55
46
  try:
56
- os.makedirs(self.scope_base_path, exist_ok=True)
47
+ os.makedirs(self.base_path, exist_ok=True)
57
48
  logger.info(
58
- "FilesystemArtifactService initialized. Scoped base path: %s",
59
- self.scope_base_path,
49
+ "FilesystemArtifactService initialized. Base path: %s",
50
+ self.base_path,
60
51
  )
61
52
  except OSError as e:
62
53
  logger.error(
63
- "Failed to create scoped base directory '%s': %s",
64
- self.scope_base_path,
54
+ "Failed to create base directory '%s': %s",
55
+ self.base_path,
65
56
  e,
66
57
  )
67
58
  raise ValueError(
68
- f"Could not create or access scoped base_path '{self.scope_base_path}': {e}"
59
+ f"Could not create or access base_path '{self.base_path}': {e}"
69
60
  ) from e
70
61
 
71
62
  def _file_has_user_namespace(self, filename: str) -> bool:
@@ -76,10 +67,10 @@ class FilesystemArtifactService(BaseArtifactService):
76
67
  self, app_name: str, user_id: str, session_id: str, filename: str
77
68
  ) -> str:
78
69
  """
79
- Constructs the directory path for a specific artifact (all versions)
80
- within the configured scope.
81
- The app_name parameter is ignored for path construction but kept for signature compatibility.
70
+ Constructs the directory path for a specific artifact (all versions).
71
+ The `app_name` is now the effective scope identifier, resolved by the caller.
82
72
  """
73
+ app_name_sanitized = os.path.basename(app_name)
83
74
  user_id_sanitized = os.path.basename(user_id)
84
75
  session_id_sanitized = os.path.basename(session_id)
85
76
  filename_sanitized = os.path.basename(filename)
@@ -87,11 +78,16 @@ class FilesystemArtifactService(BaseArtifactService):
87
78
  if self._file_has_user_namespace(filename):
88
79
  filename_dir = os.path.basename(filename.split(":", 1)[1])
89
80
  return os.path.join(
90
- self.scope_base_path, user_id_sanitized, "user", filename_dir
81
+ self.base_path,
82
+ app_name_sanitized,
83
+ user_id_sanitized,
84
+ "user",
85
+ filename_dir,
91
86
  )
92
87
  else:
93
88
  return os.path.join(
94
- self.scope_base_path,
89
+ self.base_path,
90
+ app_name_sanitized,
95
91
  user_id_sanitized,
96
92
  session_id_sanitized,
97
93
  filename_sanitized,
@@ -115,7 +111,7 @@ class FilesystemArtifactService(BaseArtifactService):
115
111
  filename: str,
116
112
  artifact: adk_types.Part,
117
113
  ) -> int:
118
- log_prefix = f"[FSArtifact:Save:{filename}] "
114
+ log_prefix = "[FSArtifact:Save] "
119
115
 
120
116
  filename = self._normalize_filename_unicode(filename)
121
117
  artifact_dir = self._get_artifact_dir(app_name, user_id, session_id, filename)
@@ -192,7 +188,7 @@ class FilesystemArtifactService(BaseArtifactService):
192
188
  filename: str,
193
189
  version: Optional[int] = None,
194
190
  ) -> Optional[adk_types.Part]:
195
- log_prefix = f"[FSArtifact:Load:{filename}] "
191
+ log_prefix = "[FSArtifact:Load] "
196
192
  filename = self._normalize_filename_unicode(filename)
197
193
  artifact_dir = self._get_artifact_dir(app_name, user_id, session_id, filename)
198
194
 
@@ -271,13 +267,14 @@ class FilesystemArtifactService(BaseArtifactService):
271
267
  async def list_artifact_keys(
272
268
  self, *, app_name: str, user_id: str, session_id: str
273
269
  ) -> List[str]:
274
- log_prefix = f"[FSArtifact:ListKeys] "
270
+ log_prefix = "[FSArtifact:ListKeys] "
275
271
  filenames = set()
272
+ app_name_sanitized = os.path.basename(app_name)
276
273
  user_id_sanitized = os.path.basename(user_id)
277
274
  session_id_sanitized = os.path.basename(session_id)
278
275
 
279
276
  session_base_dir = os.path.join(
280
- self.scope_base_path, user_id_sanitized, session_id_sanitized
277
+ self.base_path, app_name_sanitized, user_id_sanitized, session_id_sanitized
281
278
  )
282
279
  if await asyncio.to_thread(os.path.isdir, session_base_dir):
283
280
  try:
@@ -293,7 +290,9 @@ class FilesystemArtifactService(BaseArtifactService):
293
290
  e,
294
291
  )
295
292
 
296
- user_base_dir = os.path.join(self.scope_base_path, user_id_sanitized, "user")
293
+ user_base_dir = os.path.join(
294
+ self.base_path, app_name_sanitized, user_id_sanitized, "user"
295
+ )
297
296
  if await asyncio.to_thread(os.path.isdir, user_base_dir):
298
297
  try:
299
298
  for item in await asyncio.to_thread(os.listdir, user_base_dir):
@@ -316,7 +315,7 @@ class FilesystemArtifactService(BaseArtifactService):
316
315
  async def delete_artifact(
317
316
  self, *, app_name: str, user_id: str, session_id: str, filename: str
318
317
  ) -> None:
319
- log_prefix = f"[FSArtifact:Delete:{filename}] "
318
+ log_prefix = "[FSArtifact:Delete] "
320
319
  artifact_dir = self._get_artifact_dir(app_name, user_id, session_id, filename)
321
320
 
322
321
  if not await asyncio.to_thread(os.path.isdir, artifact_dir):
@@ -332,9 +331,8 @@ class FilesystemArtifactService(BaseArtifactService):
332
331
  )
333
332
  except OSError as e:
334
333
  logger.error(
335
- "%sError deleting artifact directory '%s': %s",
334
+ "%sError deleting artifact directory '%s'",
336
335
  log_prefix,
337
- artifact_dir,
338
336
  e,
339
337
  )
340
338
 
@@ -342,7 +340,7 @@ class FilesystemArtifactService(BaseArtifactService):
342
340
  async def list_versions(
343
341
  self, *, app_name: str, user_id: str, session_id: str, filename: str
344
342
  ) -> List[int]:
345
- log_prefix = f"[FSArtifact:ListVersions:{filename}] "
343
+ log_prefix = "[FSArtifact:ListVersions] "
346
344
  artifact_dir = self._get_artifact_dir(app_name, user_id, session_id, filename)
347
345
  versions = []
348
346
 
@@ -360,22 +358,17 @@ class FilesystemArtifactService(BaseArtifactService):
360
358
  ):
361
359
  versions.append(int(item))
362
360
  except OSError as e:
363
- logger.error(
364
- "%sError listing versions in directory '%s': %s",
365
- log_prefix,
366
- artifact_dir,
367
- e,
368
- )
361
+ logger.error("%sError listing versions in directory '%s'", log_prefix, e)
369
362
  return []
370
363
 
371
364
  sorted_versions = sorted(versions)
372
365
  logger.debug("%sFound versions: %s", log_prefix, sorted_versions)
373
366
  return sorted_versions
374
-
367
+
375
368
  def _normalize_filename_unicode(self, filename: str) -> str:
376
369
  """
377
370
  Normalizes Unicode characters in a filename to their standard form.
378
371
  Specifically targets compatibility characters like non-breaking spaces (\u202f)
379
372
  and converts them to their regular ASCII equivalents (a standard space).
380
373
  """
381
- return unicodedata.normalize('NFKC', filename)
374
+ return unicodedata.normalize("NFKC", filename)
@@ -132,7 +132,7 @@ async def run_adk_async_task_thread_wrapper(
132
132
  logical_task_id,
133
133
  tce,
134
134
  )
135
- sub_tasks_to_cancel = task_context.peer_sub_tasks if task_context else []
135
+ sub_tasks_to_cancel = task_context.active_peer_sub_tasks if task_context else {}
136
136
 
137
137
  if sub_tasks_to_cancel:
138
138
  log.info(
@@ -141,18 +141,22 @@ async def run_adk_async_task_thread_wrapper(
141
141
  len(sub_tasks_to_cancel),
142
142
  logical_task_id,
143
143
  )
144
- for sub_task_info in sub_tasks_to_cancel:
144
+ for sub_task_id, sub_task_info in sub_tasks_to_cancel.items():
145
145
  try:
146
- sub_task_id = sub_task_info.get("sub_task_id")
147
146
  target_peer_agent_name = sub_task_info.get("peer_agent_name")
148
147
  if not sub_task_id or not target_peer_agent_name:
149
148
  log.warning(
150
- "%s Incomplete sub-task info found, cannot cancel: %s",
149
+ "%s Incomplete sub-task info found for sub-task %s, cannot cancel: %s",
151
150
  component.log_identifier,
151
+ sub_task_id,
152
152
  sub_task_info,
153
153
  )
154
154
  continue
155
- peer_cancel_params = TaskIdParams(id=sub_task_id)
155
+
156
+ task_id_for_peer = sub_task_id.replace(
157
+ component.CORRELATION_DATA_PREFIX, "", 1
158
+ )
159
+ peer_cancel_params = TaskIdParams(id=task_id_for_peer)
156
160
  peer_cancel_request = CancelTaskRequest(params=peer_cancel_params)
157
161
  peer_cancel_user_props = {"clientId": component.agent_name}
158
162
  peer_request_topic = component._get_agent_request_topic(
@@ -167,7 +171,7 @@ async def run_adk_async_task_thread_wrapper(
167
171
  log.error(
168
172
  "%s Failed to send CancelTaskRequest for sub-task %s: %s",
169
173
  component.log_identifier,
170
- sub_task_info.get("sub_task_id"),
174
+ sub_task_id,
171
175
  e_peer_cancel,
172
176
  exc_info=True,
173
177
  )
@@ -4,8 +4,10 @@ Initializes ADK Services based on configuration.
4
4
 
5
5
  import os
6
6
  import re
7
- from typing import Dict
7
+ from typing import Dict, Optional, List, Any
8
+ from typing_extensions import override
8
9
 
10
+ from google.genai import types as adk_types
9
11
  from solace_ai_connector.common.log import log
10
12
 
11
13
  from google.adk.sessions import (
@@ -28,13 +30,126 @@ from google.adk.memory import (
28
30
  from .filesystem_artifact_service import FilesystemArtifactService
29
31
 
30
32
  try:
31
- from tests.integration.infrastructure.artifact_service.service import (
33
+ from sam_test_infrastructure.artifact_service.service import (
32
34
  TestInMemoryArtifactService,
33
35
  )
34
36
  except ImportError:
35
37
  TestInMemoryArtifactService = None
36
38
 
37
39
 
40
+ class ScopedArtifactServiceWrapper(BaseArtifactService):
41
+ """
42
+ A wrapper for an artifact service that transparently applies a configured scope.
43
+ This ensures all artifact operations respect either 'namespace' or 'app' scoping
44
+ without requiring changes at the call site. It dynamically checks the component's
45
+ configuration on each call to support test-specific overrides.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ wrapped_service: BaseArtifactService,
51
+ component: Any,
52
+ ):
53
+ """
54
+ Initializes the ScopedArtifactServiceWrapper.
55
+
56
+ Args:
57
+ wrapped_service: The concrete artifact service instance (e.g., InMemory, GCS).
58
+ component: The component instance (agent or gateway) that owns this service.
59
+ """
60
+ self.wrapped_service = wrapped_service
61
+ self.component = component
62
+
63
+ def _get_scoped_app_name(self, app_name: str) -> str:
64
+ """
65
+ Determines the effective app_name for an artifact operation by dynamically
66
+ checking the component's configuration.
67
+ """
68
+ # The component's get_config will handle test-injected overrides.
69
+ # The default scope is 'namespace' as defined in the app schema.
70
+ scope_type = self.component.get_config("artifact_scope", "namespace")
71
+
72
+ if scope_type == "namespace":
73
+ # For namespace scope, the value is always the component's namespace.
74
+ return self.component.namespace
75
+
76
+ # For 'app' scope, use the app_name that was passed into the method, which is
77
+ # typically the agent_name or gateway_id.
78
+ return app_name
79
+
80
+ @override
81
+ async def save_artifact(
82
+ self,
83
+ *,
84
+ app_name: str,
85
+ user_id: str,
86
+ session_id: str,
87
+ filename: str,
88
+ artifact: adk_types.Part,
89
+ ) -> int:
90
+ scoped_app_name = self._get_scoped_app_name(app_name)
91
+ return await self.wrapped_service.save_artifact(
92
+ app_name=scoped_app_name,
93
+ user_id=user_id,
94
+ session_id=session_id,
95
+ filename=filename,
96
+ artifact=artifact,
97
+ )
98
+
99
+ @override
100
+ async def load_artifact(
101
+ self,
102
+ *,
103
+ app_name: str,
104
+ user_id: str,
105
+ session_id: str,
106
+ filename: str,
107
+ version: Optional[int] = None,
108
+ ) -> Optional[adk_types.Part]:
109
+ scoped_app_name = self._get_scoped_app_name(app_name)
110
+ return await self.wrapped_service.load_artifact(
111
+ app_name=scoped_app_name,
112
+ user_id=user_id,
113
+ session_id=session_id,
114
+ filename=filename,
115
+ version=version,
116
+ )
117
+
118
+ @override
119
+ async def list_artifact_keys(
120
+ self, *, app_name: str, user_id: str, session_id: str
121
+ ) -> List[str]:
122
+ scoped_app_name = self._get_scoped_app_name(app_name)
123
+ return await self.wrapped_service.list_artifact_keys(
124
+ app_name=scoped_app_name, user_id=user_id, session_id=session_id
125
+ )
126
+
127
+ @override
128
+ async def delete_artifact(
129
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
130
+ ) -> None:
131
+ scoped_app_name = self._get_scoped_app_name(app_name)
132
+ await self.wrapped_service.delete_artifact(
133
+ app_name=scoped_app_name,
134
+ user_id=user_id,
135
+ session_id=session_id,
136
+ filename=filename,
137
+ )
138
+ return
139
+
140
+ @override
141
+ async def list_versions(
142
+ self, *, app_name: str, user_id: str, session_id: str, filename: str
143
+ ) -> List[int]:
144
+ scoped_app_name = self._get_scoped_app_name(app_name)
145
+ return await self.wrapped_service.list_versions(
146
+ app_name=scoped_app_name,
147
+ user_id=user_id,
148
+ session_id=session_id,
149
+ filename=filename,
150
+ )
151
+
152
+
38
153
  def _sanitize_for_path(identifier: str) -> str:
39
154
  """Sanitizes a string to be safe for use as a directory name."""
40
155
  if not identifier:
@@ -88,7 +203,11 @@ def initialize_session_service(component) -> BaseSessionService:
88
203
 
89
204
 
90
205
  def initialize_artifact_service(component) -> BaseArtifactService:
91
- """Initializes the ADK Artifact Service based on configuration."""
206
+ """
207
+ Initializes the ADK Artifact Service based on configuration.
208
+ This factory creates the concrete service instance and then wraps it with
209
+ the ScopedArtifactServiceWrapper to enforce artifact scoping rules dynamically.
210
+ """
92
211
  config: Dict = component.get_config("artifact_service", {"type": "memory"})
93
212
  service_type = config.get("type", "memory").lower()
94
213
  log.info(
@@ -97,8 +216,9 @@ def initialize_artifact_service(component) -> BaseArtifactService:
97
216
  service_type,
98
217
  )
99
218
 
219
+ concrete_service: BaseArtifactService
100
220
  if service_type == "memory":
101
- return InMemoryArtifactService()
221
+ concrete_service = InMemoryArtifactService()
102
222
  elif service_type == "gcs":
103
223
  bucket_name = config.get("bucket_name")
104
224
  if not bucket_name:
@@ -107,9 +227,11 @@ def initialize_artifact_service(component) -> BaseArtifactService:
107
227
  )
108
228
  try:
109
229
  gcs_args = {
110
- k: v for k, v in config.items() if k not in ["type", "bucket_name"]
230
+ k: v
231
+ for k, v in config.items()
232
+ if k not in ["type", "bucket_name", "artifact_scope"]
111
233
  }
112
- return GcsArtifactService(bucket_name=bucket_name, **gcs_args)
234
+ concrete_service = GcsArtifactService(bucket_name=bucket_name, **gcs_args)
113
235
  except ImportError:
114
236
  log.error(
115
237
  "%s google-cloud-storage not installed. Please install 'google-adk[gcs]' or 'google-cloud-storage'.",
@@ -123,60 +245,8 @@ def initialize_artifact_service(component) -> BaseArtifactService:
123
245
  f"{component.log_identifier} 'base_path' is required for filesystem artifact service."
124
246
  )
125
247
 
126
- artifact_scope = config.get("artifact_scope", "namespace").lower()
127
- scope_identifier_raw = None
128
-
129
- if artifact_scope == "app":
130
- app_instance = component.get_app()
131
- if not app_instance or not app_instance.name:
132
- raise ValueError(
133
- f"{component.log_identifier} Cannot determine app name for 'app' scope."
134
- )
135
- scope_identifier_raw = app_instance.name
136
- log.info(
137
- "%s Using 'app' scope for filesystem artifacts: %s",
138
- component.log_identifier,
139
- scope_identifier_raw,
140
- )
141
- elif artifact_scope == "namespace":
142
- scope_identifier_raw = component.get_config("namespace")
143
- log.info(
144
- "%s Using 'namespace' scope for filesystem artifacts: %s",
145
- component.log_identifier,
146
- scope_identifier_raw,
147
- )
148
- elif artifact_scope == "custom":
149
- scope_identifier_raw = config.get("artifact_scope_value")
150
- if not scope_identifier_raw:
151
- raise ValueError(
152
- f"{component.log_identifier} 'artifact_scope_value' is required when artifact_scope is 'custom'."
153
- )
154
- log.info(
155
- "%s Using 'custom' scope for filesystem artifacts: %s",
156
- component.log_identifier,
157
- scope_identifier_raw,
158
- )
159
- else:
160
- raise ValueError(
161
- f"{component.log_identifier} Invalid 'artifact_scope' value: {artifact_scope}"
162
- )
163
-
164
- if not scope_identifier_raw:
165
- raise ValueError(
166
- f"{component.log_identifier} Failed to determine scope identifier for filesystem artifacts."
167
- )
168
-
169
- scope_identifier_sanitized = _sanitize_for_path(scope_identifier_raw)
170
- log.info(
171
- "%s Sanitized scope identifier: %s",
172
- component.log_identifier,
173
- scope_identifier_sanitized,
174
- )
175
-
176
248
  try:
177
- return FilesystemArtifactService(
178
- base_path=base_path, scope_identifier=scope_identifier_sanitized
179
- )
249
+ concrete_service = FilesystemArtifactService(base_path=base_path)
180
250
  except Exception as e:
181
251
  log.error(
182
252
  "%s Failed to initialize FilesystemArtifactService: %s",
@@ -196,12 +266,23 @@ def initialize_artifact_service(component) -> BaseArtifactService:
196
266
  "%s Using TestInMemoryArtifactService for testing.",
197
267
  component.log_identifier,
198
268
  )
199
- return TestInMemoryArtifactService()
269
+ concrete_service = TestInMemoryArtifactService()
200
270
  else:
201
271
  raise ValueError(
202
272
  f"{component.log_identifier} Unsupported artifact service type: {service_type}"
203
273
  )
204
274
 
275
+ # Wrap the concrete service to enforce scoping dynamically.
276
+ # The wrapper will query the component's config at runtime.
277
+ log.info(
278
+ "%s Wrapping artifact service with dynamic ScopedArtifactServiceWrapper.",
279
+ component.log_identifier,
280
+ )
281
+ return ScopedArtifactServiceWrapper(
282
+ wrapped_service=concrete_service,
283
+ component=component,
284
+ )
285
+
205
286
 
206
287
  def initialize_memory_service(component) -> BaseMemoryService:
207
288
  """Initializes the ADK Memory Service based on configuration."""