bedrock-agentcore-starter-toolkit 0.1.13__py3-none-any.whl → 0.1.15__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 bedrock-agentcore-starter-toolkit might be problematic. Click here for more details.

Files changed (30) hide show
  1. bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +35 -2
  2. bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +127 -1
  3. bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +3 -0
  4. bedrock_agentcore_starter_toolkit/operations/memory/README.md +1109 -0
  5. bedrock_agentcore_starter_toolkit/operations/memory/constants.py +1 -9
  6. bedrock_agentcore_starter_toolkit/operations/memory/manager.py +248 -57
  7. bedrock_agentcore_starter_toolkit/operations/memory/models/__init__.py +106 -0
  8. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/__init__.py +52 -0
  9. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/base.py +77 -0
  10. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/custom.py +194 -0
  11. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/semantic.py +35 -0
  12. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/summary.py +35 -0
  13. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/user_preference.py +34 -0
  14. bedrock_agentcore_starter_toolkit/operations/memory/strategy_validator.py +395 -0
  15. bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +78 -0
  16. bedrock_agentcore_starter_toolkit/operations/runtime/destroy.py +43 -3
  17. bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +45 -0
  18. bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +164 -0
  19. bedrock_agentcore_starter_toolkit/operations/runtime/models.py +7 -0
  20. bedrock_agentcore_starter_toolkit/operations/runtime/status.py +62 -0
  21. bedrock_agentcore_starter_toolkit/utils/runtime/container.py +4 -0
  22. bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +27 -1
  23. bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +10 -3
  24. bedrock_agentcore_starter_toolkit/utils/runtime/templates/execution_role_policy.json.j2 +31 -0
  25. {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/METADATA +1 -1
  26. {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/RECORD +30 -21
  27. {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/WHEEL +0 -0
  28. {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/entry_points.txt +0 -0
  29. {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/licenses/LICENSE.txt +0 -0
  30. {bedrock_agentcore_starter_toolkit-0.1.13.dist-info → bedrock_agentcore_starter_toolkit-0.1.15.dist-info}/licenses/NOTICE.txt +0 -0
@@ -0,0 +1,395 @@
1
+ """Strategy validation utilities for memory operations."""
2
+
3
+ import logging
4
+ import re
5
+ from typing import Any, Dict, List, Union
6
+
7
+ from .constants import StrategyType
8
+ from .models import convert_strategies_to_dicts
9
+ from .models.strategies import BaseStrategy
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class UniversalComparator:
15
+ """Universal comparison utility for deep strategy validation."""
16
+
17
+ @staticmethod
18
+ def _camel_to_snake(name: str) -> str:
19
+ """Convert camelCase to snake_case."""
20
+ # Handle sequences of uppercase letters (like XMLHttpRequest -> xml_http_request)
21
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
22
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
23
+
24
+ @staticmethod
25
+ def normalize_field_names(data: Any) -> Any:
26
+ """Recursively normalize field names from camelCase to snake_case."""
27
+ if isinstance(data, dict):
28
+ normalized = {}
29
+ for key, value in data.items():
30
+ normalized_key = UniversalComparator._camel_to_snake(key)
31
+ normalized[normalized_key] = UniversalComparator.normalize_field_names(value)
32
+ return normalized
33
+ elif isinstance(data, list):
34
+ return [UniversalComparator.normalize_field_names(item) for item in data]
35
+ else:
36
+ return data
37
+
38
+ @staticmethod
39
+ def deep_compare(dict1: Dict[str, Any], dict2: Dict[str, Any], path: str = "") -> tuple[bool, str]:
40
+ """Deep compare two dictionaries with detailed error reporting."""
41
+ # Normalize both dictionaries
42
+ norm1 = UniversalComparator.normalize_field_names(dict1)
43
+ norm2 = UniversalComparator.normalize_field_names(dict2)
44
+
45
+ return UniversalComparator._deep_compare_normalized(norm1, norm2, path)
46
+
47
+ @staticmethod
48
+ def _deep_compare_normalized(obj1: Any, obj2: Any, path: str = "") -> tuple[bool, str]:
49
+ """Compare normalized objects recursively."""
50
+ # Handle None equivalence - treat None and empty values as equivalent
51
+ if obj1 is None and obj2 is None:
52
+ return True, ""
53
+ if obj1 is None and (obj2 == "" or obj2 == [] or obj2 == {}):
54
+ return True, ""
55
+ if obj2 is None and (obj1 == "" or obj1 == [] or obj1 == {}):
56
+ return True, ""
57
+
58
+ # Type comparison
59
+ if type(obj1) is not type(obj2):
60
+ return False, f"{path}: type mismatch ({type(obj1).__name__} vs {type(obj2).__name__})"
61
+
62
+ if isinstance(obj1, dict):
63
+ # Get all keys from both dictionaries
64
+ all_keys = set(obj1.keys()) | set(obj2.keys())
65
+
66
+ for key in all_keys:
67
+ key_path = f"{path}.{key}" if path else key
68
+
69
+ val1 = obj1.get(key)
70
+ val2 = obj2.get(key)
71
+
72
+ # Special handling for namespaces - compare as sets (order-independent)
73
+ if key == "namespaces" and isinstance(val1, list) and isinstance(val2, list):
74
+ set1 = set(val1) if val1 else set()
75
+ set2 = set(val2) if val2 else set()
76
+ if set1 != set2:
77
+ return False, f"{key_path}: mismatch ({sorted(set1)} vs {sorted(set2)})"
78
+ continue
79
+
80
+ matches, error = UniversalComparator._deep_compare_normalized(val1, val2, key_path)
81
+ if not matches:
82
+ return False, error
83
+
84
+ return True, ""
85
+
86
+ elif isinstance(obj1, list):
87
+ # Special case: if this is a namespaces list at the root level, compare as sets
88
+ if path == "namespaces":
89
+ set1 = set(obj1) if obj1 else set()
90
+ set2 = set(obj2) if obj2 else set()
91
+ if set1 != set2:
92
+ return False, f"{path}: mismatch ({sorted(set1)} vs {sorted(set2)})"
93
+ return True, ""
94
+
95
+ if len(obj1) != len(obj2):
96
+ return False, f"{path}: list length mismatch ({len(obj1)} vs {len(obj2)})"
97
+
98
+ for i, (item1, item2) in enumerate(zip(obj1, obj2, strict=False)):
99
+ item_path = f"{path}[{i}]" if path else f"[{i}]"
100
+ matches, error = UniversalComparator._deep_compare_normalized(item1, item2, item_path)
101
+ if not matches:
102
+ return False, error
103
+
104
+ return True, ""
105
+
106
+ else:
107
+ # Direct value comparison
108
+ if obj1 != obj2:
109
+ return False, f"{path}: value mismatch ('{obj1}' vs '{obj2}')"
110
+ return True, ""
111
+
112
+
113
+ class StrategyComparator:
114
+ """Utility class for comparing memory strategies in detail."""
115
+
116
+ @staticmethod
117
+ def normalize_strategy(strategy: Union[Dict[str, Any], Dict[str, Dict[str, Any]]]) -> Dict[str, Any]:
118
+ """Normalize a strategy to a standard format with universal field normalization.
119
+
120
+ Args:
121
+ strategy: Strategy dictionary (either from memory response or request format)
122
+
123
+ Returns:
124
+ Normalized strategy dictionary with snake_case field names
125
+ """
126
+ # Check if this is already a normalized strategy (from memory response)
127
+ if "type" in strategy or "memoryStrategyType" in strategy:
128
+ return StrategyComparator._normalize_memory_strategy(strategy)
129
+
130
+ # Otherwise, it's a request format strategy
131
+ return StrategyComparator._normalize_request_strategy(strategy)
132
+
133
+ @staticmethod
134
+ def _normalize_memory_strategy(strategy: Dict[str, Any]) -> Dict[str, Any]:
135
+ """Normalize a strategy from memory response, including only fields relevant for comparison."""
136
+ # Handle different field name variations
137
+ strategy_type = strategy.get("type", strategy.get("memoryStrategyType"))
138
+
139
+ # Only include the core fields that should be compared
140
+ normalized = {
141
+ "type": strategy_type,
142
+ "name": strategy.get("name"),
143
+ "description": strategy.get("description"),
144
+ "namespaces": strategy.get("namespaces", []),
145
+ }
146
+
147
+ # Add configuration if present and normalize it
148
+ if "configuration" in strategy and strategy["configuration"]:
149
+ config = strategy["configuration"]
150
+ normalized_config = StrategyComparator._transform_memory_configuration(config, strategy_type)
151
+ normalized["configuration"] = UniversalComparator.normalize_field_names(normalized_config)
152
+
153
+ # Don't include any other fields from memory responses (like status, strategyId, etc.)
154
+ # as they are not relevant for strategy comparison
155
+
156
+ return normalized
157
+
158
+ @staticmethod
159
+ def _transform_memory_configuration(config: Dict[str, Any], strategy_type: str) -> Dict[str, Any]:
160
+ """Transform memory configuration from stored format to match requested format.
161
+
162
+ This handles the structural differences between how configurations are stored
163
+ in memory vs how they're provided through typed strategy objects.
164
+
165
+ Args:
166
+ config: Configuration from memory response
167
+ strategy_type: Strategy type (e.g., 'CUSTOM', 'SEMANTIC', etc.)
168
+
169
+ Returns:
170
+ Transformed configuration matching the requested format
171
+ """
172
+ if not config:
173
+ return config
174
+
175
+ # Handle CUSTOM strategy configurations that need transformation
176
+ if strategy_type == "CUSTOM" and config.get("type") in [
177
+ "SEMANTIC_OVERRIDE",
178
+ "USER_PREFERENCE_OVERRIDE",
179
+ "SUMMARY_OVERRIDE",
180
+ ]:
181
+ override_type = config.get("type")
182
+ transformed_config = {}
183
+
184
+ # Determine the override key name based on type
185
+ if override_type == "SEMANTIC_OVERRIDE":
186
+ override_key = "semanticOverride"
187
+ elif override_type == "USER_PREFERENCE_OVERRIDE":
188
+ override_key = "userPreferenceOverride"
189
+ elif override_type == "SUMMARY_OVERRIDE":
190
+ override_key = "summaryOverride"
191
+ else:
192
+ # Fallback - return original config
193
+ return config
194
+
195
+ transformed_config[override_key] = {}
196
+
197
+ # Transform extraction configuration
198
+ if "extraction" in config:
199
+ extraction = config["extraction"]
200
+ if "customExtractionConfiguration" in extraction:
201
+ custom_extraction = extraction["customExtractionConfiguration"]
202
+
203
+ # Find the override key and extract the actual config
204
+ for key, value in custom_extraction.items():
205
+ if key.endswith("Override"):
206
+ transformed_config[override_key]["extraction"] = value
207
+ break
208
+ elif "custom_extraction_configuration" in extraction:
209
+ # Handle snake_case version
210
+ custom_extraction = extraction["custom_extraction_configuration"]
211
+
212
+ # Find the override key and extract the actual config
213
+ for key, value in custom_extraction.items():
214
+ if key.endswith("_override"):
215
+ transformed_config[override_key]["extraction"] = value
216
+ break
217
+ else:
218
+ # Direct extraction config (no wrapper)
219
+ transformed_config[override_key]["extraction"] = extraction
220
+
221
+ # Transform consolidation configuration
222
+ if "consolidation" in config:
223
+ consolidation = config["consolidation"]
224
+ if "customConsolidationConfiguration" in consolidation:
225
+ custom_consolidation = consolidation["customConsolidationConfiguration"]
226
+
227
+ # Find the override key and extract the actual config
228
+ for key, value in custom_consolidation.items():
229
+ if key.endswith("Override"):
230
+ transformed_config[override_key]["consolidation"] = value
231
+ break
232
+ elif "custom_consolidation_configuration" in consolidation:
233
+ # Handle snake_case version
234
+ custom_consolidation = consolidation["custom_consolidation_configuration"]
235
+
236
+ # Find the override key and extract the actual config
237
+ for key, value in custom_consolidation.items():
238
+ if key.endswith("_override"):
239
+ transformed_config[override_key]["consolidation"] = value
240
+ break
241
+ else:
242
+ # Direct consolidation config (no wrapper)
243
+ transformed_config[override_key]["consolidation"] = consolidation
244
+
245
+ # Copy any other fields that don't need transformation
246
+ for key, value in config.items():
247
+ if key not in ["type", "extraction", "consolidation"]:
248
+ transformed_config[key] = value
249
+
250
+ return transformed_config
251
+
252
+ # For non-CUSTOM strategies or configurations that don't need transformation, return as-is
253
+ return config
254
+
255
+ @staticmethod
256
+ def _normalize_request_strategy(strategy_dict: Dict[str, Any]) -> Dict[str, Any]:
257
+ """Normalize a strategy from request format."""
258
+ # Find the strategy type key in the dictionary
259
+ strategy_type = None
260
+ strategy_config = None
261
+
262
+ for key, config in strategy_dict.items():
263
+ if key.endswith("MemoryStrategy") or key in [
264
+ StrategyType.SEMANTIC.value,
265
+ StrategyType.SUMMARY.value,
266
+ StrategyType.USER_PREFERENCE.value,
267
+ StrategyType.CUSTOM.value,
268
+ ]:
269
+ strategy_config = config
270
+ # Map strategy keys to standard types using the constants
271
+ if key == "semanticMemoryStrategy" or key == StrategyType.SEMANTIC.value:
272
+ strategy_type = StrategyType.SEMANTIC.get_memory_strategy()
273
+ elif key == "summaryMemoryStrategy" or key == StrategyType.SUMMARY.value:
274
+ strategy_type = StrategyType.SUMMARY.get_memory_strategy()
275
+ elif key == "userPreferenceMemoryStrategy" or key == StrategyType.USER_PREFERENCE.value:
276
+ strategy_type = StrategyType.USER_PREFERENCE.get_memory_strategy()
277
+ elif key == "customMemoryStrategy" or key == StrategyType.CUSTOM.value:
278
+ strategy_type = StrategyType.CUSTOM.get_memory_strategy()
279
+ elif key.endswith("MemoryStrategy"):
280
+ # Handle future strategy types following naming convention
281
+ # e.g., "newTypeMemoryStrategy" -> "NEW_TYPE"
282
+ type_name = key.replace("MemoryStrategy", "")
283
+ strategy_type = UniversalComparator._camel_to_snake(type_name).upper()
284
+ break
285
+
286
+ if not strategy_config:
287
+ raise ValueError(f"Invalid strategy format: {strategy_dict}")
288
+
289
+ normalized = {
290
+ "type": strategy_type,
291
+ "name": strategy_config.get("name"),
292
+ "description": strategy_config.get("description"),
293
+ "namespaces": strategy_config.get("namespaces", []),
294
+ }
295
+
296
+ # Add configuration if present and normalize it
297
+ if "configuration" in strategy_config and strategy_config["configuration"]:
298
+ normalized["configuration"] = UniversalComparator.normalize_field_names(strategy_config["configuration"])
299
+
300
+ # Normalize any additional fields in the strategy config (but exclude common metadata fields)
301
+ excluded_fields = {"name", "description", "namespaces", "configuration", "status", "strategyId"}
302
+ for key, value in strategy_config.items():
303
+ if key not in excluded_fields:
304
+ normalized_key = UniversalComparator._camel_to_snake(key)
305
+ normalized[normalized_key] = UniversalComparator.normalize_field_names(value)
306
+
307
+ return normalized
308
+
309
+ @staticmethod
310
+ def compare_strategies(
311
+ existing_strategies: List[Dict[str, Any]], requested_strategies: List[Union[BaseStrategy, Dict[str, Any]]]
312
+ ) -> tuple[bool, str]:
313
+ """Compare existing memory strategies with requested strategies using universal comparison.
314
+
315
+ Args:
316
+ existing_strategies: List of strategy dictionaries from memory response
317
+ requested_strategies: List of requested strategy objects or dictionaries
318
+
319
+ Returns:
320
+ Tuple of (matches, error_message). If matches is False, error_message contains details.
321
+ """
322
+ # Convert requested strategies to dictionaries for comparison
323
+ requested_dict_strategies = convert_strategies_to_dicts(requested_strategies)
324
+
325
+ # Normalize both sets of strategies
326
+ normalized_existing = []
327
+ for strategy in existing_strategies:
328
+ try:
329
+ normalized_existing.append(StrategyComparator.normalize_strategy(strategy))
330
+ except Exception as e:
331
+ logger.warning("Failed to normalize existing strategy: %s, error: %s", strategy, e)
332
+ continue
333
+
334
+ normalized_requested = []
335
+ for strategy in requested_dict_strategies:
336
+ try:
337
+ normalized_requested.append(StrategyComparator.normalize_strategy(strategy))
338
+ except Exception as e:
339
+ logger.warning("Failed to normalize requested strategy: %s, error: %s", strategy, e)
340
+ continue
341
+
342
+ # Sort both lists by type and name for consistent comparison
343
+ normalized_existing.sort(key=lambda x: (x.get("type", ""), x.get("name", "")))
344
+ normalized_requested.sort(key=lambda x: (x.get("type", ""), x.get("name", "")))
345
+
346
+ # Check if counts match
347
+ if len(normalized_existing) != len(normalized_requested):
348
+ existing_types = [s.get("type") for s in normalized_existing]
349
+ requested_types = [s.get("type") for s in normalized_requested]
350
+ return False, (
351
+ f"Strategy count mismatch. "
352
+ f"Existing memory has {len(normalized_existing)} strategies: {existing_types}, "
353
+ f"but {len(normalized_requested)} strategies were requested: {requested_types}."
354
+ )
355
+
356
+ # Use universal comparison for each strategy pair
357
+ for i, (existing, requested) in enumerate(zip(normalized_existing, normalized_requested, strict=False)):
358
+ logger.info("Existing %s\nRequested %s", existing, requested)
359
+ matches, error = UniversalComparator.deep_compare(existing, requested)
360
+ if not matches:
361
+ return False, f"Strategy {i + 1} mismatch: {error}"
362
+
363
+ return True, ""
364
+
365
+
366
+ def validate_existing_memory_strategies(
367
+ memory_strategies: List[Dict[str, Any]],
368
+ requested_strategies: List[Union[BaseStrategy, Dict[str, Any]]],
369
+ memory_name: str,
370
+ ) -> None:
371
+ """Validate that existing memory strategies match the requested strategies using universal comparison.
372
+
373
+ Args:
374
+ memory_strategies: List of strategy dictionaries from memory response
375
+ requested_strategies: List of requested strategy objects or dictionaries
376
+ memory_name: Memory name for error messages
377
+
378
+ Raises:
379
+ ValueError: If the strategies don't match with detailed explanation
380
+ """
381
+ matches, error_message = StrategyComparator.compare_strategies(memory_strategies, requested_strategies)
382
+
383
+ if not matches:
384
+ raise ValueError(
385
+ f"Strategy mismatch for memory '{memory_name}'. {error_message} "
386
+ f"Cannot use existing memory with different strategy configuration."
387
+ )
388
+
389
+ # Log successful validation
390
+ strategy_types = [s.get("type", s.get("memoryStrategyType", "unknown")) for s in memory_strategies]
391
+ logger.info(
392
+ "Universal strategy validation passed for memory %s. Strategies match: [%s]",
393
+ memory_name,
394
+ ", ".join(strategy_types),
395
+ )
@@ -5,6 +5,7 @@ import re
5
5
  from pathlib import Path
6
6
  from typing import Any, Dict, Optional, Tuple
7
7
 
8
+ from ...cli.runtime.configuration_manager import ConfigurationManager
8
9
  from ...services.ecr import get_account_id, get_region
9
10
  from ...utils.runtime.config import merge_agent_config, save_config
10
11
  from ...utils.runtime.container import ContainerRuntime
@@ -12,6 +13,8 @@ from ...utils.runtime.schema import (
12
13
  AWSConfig,
13
14
  BedrockAgentCoreAgentSchema,
14
15
  BedrockAgentCoreDeploymentInfo,
16
+ CodeBuildConfig,
17
+ MemoryConfig,
15
18
  NetworkConfiguration,
16
19
  ObservabilityConfig,
17
20
  ProtocolConfiguration,
@@ -25,6 +28,7 @@ def configure_bedrock_agentcore(
25
28
  agent_name: str,
26
29
  entrypoint_path: Path,
27
30
  execution_role: Optional[str] = None,
31
+ code_build_execution_role: Optional[str] = None,
28
32
  ecr_repository: Optional[str] = None,
29
33
  container_runtime: Optional[str] = None,
30
34
  auto_create_ecr: bool = True,
@@ -36,6 +40,7 @@ def configure_bedrock_agentcore(
36
40
  verbose: bool = False,
37
41
  region: Optional[str] = None,
38
42
  protocol: Optional[str] = None,
43
+ non_interactive: bool = False,
39
44
  ) -> ConfigureResult:
40
45
  """Configure Bedrock AgentCore application with deployment settings.
41
46
 
@@ -43,6 +48,7 @@ def configure_bedrock_agentcore(
43
48
  agent_name: name of the agent,
44
49
  entrypoint_path: Path to the entrypoint file
45
50
  execution_role: AWS execution role ARN or name (auto-created if not provided)
51
+ code_build_execution_role: CodeBuild execution role ARN or name (uses execution_role if not provided)
46
52
  ecr_repository: ECR repository URI
47
53
  container_runtime: Container runtime to use
48
54
  auto_create_ecr: Whether to auto-create ECR repository
@@ -54,6 +60,7 @@ def configure_bedrock_agentcore(
54
60
  verbose: Whether to provide verbose output during configuration
55
61
  region: AWS region for deployment
56
62
  protocol: agent server protocol, must be either HTTP or MCP
63
+ non_interactive: Skip interactive prompts and use defaults
57
64
 
58
65
  Returns:
59
66
  ConfigureResult model with configuration details
@@ -109,6 +116,69 @@ def configure_bedrock_agentcore(
109
116
  else:
110
117
  log.debug("No execution role provided and auto-create disabled")
111
118
 
119
+ if verbose:
120
+ log.debug("Prompting for memory configuration")
121
+
122
+ config_manager = ConfigurationManager(build_dir / ".bedrock_agentcore.yaml")
123
+
124
+ # New memory selection flow
125
+ action, value = config_manager.prompt_memory_selection()
126
+
127
+ memory_config = MemoryConfig()
128
+ if action == "USE_EXISTING":
129
+ # Using existing memory - just store the ID
130
+ memory_config.memory_id = value
131
+ memory_config.mode = "STM_AND_LTM" # Assume existing has strategies
132
+ memory_config.memory_name = f"{agent_name}_memory"
133
+ log.info("Using existing memory resource: %s", value)
134
+ elif action == "CREATE_NEW":
135
+ # Create new with specified mode
136
+ memory_config.mode = value # This is the mode (STM_ONLY, STM_AND_LTM, NO_MEMORY)
137
+ memory_config.event_expiry_days = 30
138
+ memory_config.memory_name = f"{agent_name}_memory"
139
+ log.info("Will create new memory with mode: %s", value)
140
+
141
+ if memory_config.mode == "STM_AND_LTM":
142
+ log.info("Memory configuration: Short-term + Long-term memory enabled")
143
+ elif memory_config.mode == "STM_ONLY":
144
+ log.info("Memory configuration: Short-term memory only")
145
+
146
+ # Check for existing memory configuration from previous launch
147
+ config_path = build_dir / ".bedrock_agentcore.yaml"
148
+ memory_id = None
149
+ memory_name = None
150
+
151
+ if config_path.exists():
152
+ try:
153
+ from ...utils.runtime.config import load_config
154
+
155
+ existing_config = load_config(config_path)
156
+ existing_agent = existing_config.get_agent_config(agent_name)
157
+ if existing_agent and existing_agent.memory and existing_agent.memory.memory_id:
158
+ memory_id = existing_agent.memory.memory_id
159
+ memory_name = existing_agent.memory.memory_name
160
+ log.info("Found existing memory ID from previous launch: %s", memory_id)
161
+ except Exception as e:
162
+ log.debug("Unable to read existing memory configuration: %s", e)
163
+
164
+ # Handle CodeBuild execution role - use separate role if provided, otherwise use execution_role
165
+ codebuild_execution_role_arn = None
166
+ if code_build_execution_role:
167
+ # User provided a separate CodeBuild role
168
+ if code_build_execution_role.startswith("arn:aws:iam::"):
169
+ codebuild_execution_role_arn = code_build_execution_role
170
+ else:
171
+ codebuild_execution_role_arn = f"arn:aws:iam::{account_id}:role/{code_build_execution_role}"
172
+
173
+ if verbose:
174
+ log.debug("Using separate CodeBuild execution role: %s", codebuild_execution_role_arn)
175
+ else:
176
+ # No separate CodeBuild role provided - use None
177
+ codebuild_execution_role_arn = None
178
+
179
+ if verbose and execution_role_arn:
180
+ log.debug("Using same role for CodeBuild: %s", codebuild_execution_role_arn)
181
+
112
182
  # Generate Dockerfile and .dockerignore
113
183
  bedrock_agentcore_name = None
114
184
  # Try to find the variable name for the Bedrock AgentCore instance in the file
@@ -123,6 +193,8 @@ def configure_bedrock_agentcore(
123
193
  log.debug(" Region: %s", region)
124
194
  log.debug(" Enable observability: %s", enable_observability)
125
195
  log.debug(" Requirements file: %s", requirements_file)
196
+ if memory_id:
197
+ log.debug(" Memory ID: %s", memory_id)
126
198
 
127
199
  dockerfile_path = runtime.generate_dockerfile(
128
200
  entrypoint_path,
@@ -131,6 +203,8 @@ def configure_bedrock_agentcore(
131
203
  region,
132
204
  enable_observability,
133
205
  requirements_file,
206
+ memory_id,
207
+ memory_name,
134
208
  )
135
209
 
136
210
  # Check if .dockerignore was created
@@ -195,8 +269,12 @@ def configure_bedrock_agentcore(
195
269
  observability=ObservabilityConfig(enabled=enable_observability),
196
270
  ),
197
271
  bedrock_agentcore=BedrockAgentCoreDeploymentInfo(),
272
+ codebuild=CodeBuildConfig(
273
+ execution_role=codebuild_execution_role_arn,
274
+ ),
198
275
  authorizer_configuration=authorizer_configuration,
199
276
  request_header_configuration=request_header_configuration,
277
+ memory=memory_config,
200
278
  )
201
279
 
202
280
  # Use simplified config merging
@@ -7,6 +7,7 @@ from typing import Optional
7
7
  import boto3
8
8
  from botocore.exceptions import ClientError
9
9
 
10
+ from ...operations.memory.manager import MemoryManager
10
11
  from ...services.runtime import BedrockAgentCoreClient
11
12
  from ...utils.runtime.config import load_config, save_config
12
13
  from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
@@ -77,13 +78,16 @@ def destroy_bedrock_agentcore(
77
78
  # 4. Remove CodeBuild project
78
79
  _destroy_codebuild_project(session, agent_config, result, dry_run)
79
80
 
80
- # 5. Remove CodeBuild IAM Role
81
+ # 5. Remove memory resource
82
+ _destroy_memory(session, agent_config, result, dry_run)
83
+
84
+ # 6. Remove CodeBuild IAM Role
81
85
  _destroy_codebuild_iam_role(session, agent_config, result, dry_run)
82
86
 
83
- # 6. Remove IAM execution role (if not used by other agents)
87
+ # 7. Remove IAM execution role (if not used by other agents)
84
88
  _destroy_iam_role(session, project_config, agent_config, result, dry_run)
85
89
 
86
- # 7. Clean up configuration
90
+ # 8. Clean up configuration
87
91
  if not dry_run and not result.errors:
88
92
  _cleanup_agent_config(config_path, project_config, agent_config.name, result)
89
93
 
@@ -386,6 +390,42 @@ def _destroy_codebuild_project(
386
390
  log.warning("Error during CodeBuild cleanup: %s", e)
387
391
 
388
392
 
393
+ def _destroy_memory(
394
+ session: boto3.Session,
395
+ agent_config: BedrockAgentCoreAgentSchema,
396
+ result: DestroyResult,
397
+ dry_run: bool,
398
+ ) -> None:
399
+ """Remove memory resource for this agent."""
400
+ if not agent_config.memory or not agent_config.memory.memory_id:
401
+ result.warnings.append("No memory configured, skipping memory cleanup")
402
+ return
403
+
404
+ try:
405
+ memory_manager = MemoryManager(region_name=agent_config.aws.region)
406
+ memory_id = agent_config.memory.memory_id
407
+
408
+ if dry_run:
409
+ result.resources_removed.append(f"Memory: {memory_id} (DRY RUN)")
410
+ return
411
+
412
+ try:
413
+ # Use the manager's delete method which handles the deletion properly
414
+ memory_manager.delete_memory(memory_id=memory_id)
415
+ result.resources_removed.append(f"Memory: {memory_id}")
416
+ log.info("Deleted memory: %s", memory_id)
417
+ except ClientError as e:
418
+ if e.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
419
+ result.warnings.append(f"Failed to delete memory {memory_id}: {e}")
420
+ log.warning("Failed to delete memory: %s", e)
421
+ else:
422
+ result.warnings.append(f"Memory {memory_id} not found (may have been deleted already)")
423
+
424
+ except Exception as e:
425
+ result.warnings.append(f"Error during memory cleanup: {e}")
426
+ log.warning("Error during memory cleanup: %s", e)
427
+
428
+
389
429
  def _destroy_codebuild_iam_role(
390
430
  session: boto3.Session,
391
431
  agent_config: BedrockAgentCoreAgentSchema,
@@ -29,6 +29,51 @@ def invoke_bedrock_agentcore(
29
29
  # Load project configuration
30
30
  project_config = load_config(config_path)
31
31
  agent_config = project_config.get_agent_config(agent_name)
32
+
33
+ # Check memory status on first invoke if LTM is enabled
34
+ if (
35
+ agent_config.memory
36
+ and agent_config.memory.has_ltm
37
+ and agent_config.memory.memory_id
38
+ and not agent_config.memory.first_invoke_memory_check_done
39
+ ):
40
+ try:
41
+ from ...operations.memory.constants import MemoryStatus
42
+ from ...operations.memory.manager import MemoryManager
43
+
44
+ memory_manager = MemoryManager(region_name=agent_config.aws.region)
45
+ memory_status = memory_manager.get_memory_status(agent_config.memory.memory_id)
46
+
47
+ if memory_status != MemoryStatus.ACTIVE.value:
48
+ # Provide graceful error message
49
+ error_message = (
50
+ f"Memory is still provisioning (current status: {memory_status}). "
51
+ f"Long-term memory extraction takes 60-180 seconds to activate.\n\n"
52
+ f"Please wait and check status with:\n"
53
+ f" agentcore status{f' --agent {agent_name}' if agent_name else ''}"
54
+ )
55
+
56
+ # Log the message for visibility
57
+ log.warning("Memory not yet active for agent '%s': %s", agent_config.name, memory_status)
58
+
59
+ raise ValueError(error_message)
60
+
61
+ # Memory is active, mark check as done
62
+ agent_config.memory.first_invoke_memory_check_done = True
63
+ project_config.agents[agent_config.name] = agent_config
64
+ save_config(project_config, config_path)
65
+ log.info("Memory is active, proceeding with invoke")
66
+
67
+ except ImportError as e:
68
+ log.error("Failed to import MemoryManager: %s", e)
69
+ # Continue without check if import fails
70
+ except Exception as e:
71
+ # If it's our ValueError, re-raise it
72
+ if "Memory is still provisioning" in str(e):
73
+ raise
74
+ # For other errors, log but continue
75
+ log.warning("Could not check memory status: %s", e)
76
+
32
77
  # Log which agent is being invoked
33
78
  mode = "locally" if local_mode else "via cloud endpoint"
34
79
  log.debug("Invoking BedrockAgentCore agent '%s' %s", agent_config.name, mode)