bedrock-agentcore-starter-toolkit 0.1.21__py3-none-any.whl → 0.1.23__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 (25) hide show
  1. bedrock_agentcore_starter_toolkit/cli/common.py +1 -1
  2. bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +167 -51
  3. bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +45 -17
  4. bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +30 -1
  5. bedrock_agentcore_starter_toolkit/operations/memory/manager.py +1 -1
  6. bedrock_agentcore_starter_toolkit/operations/runtime/__init__.py +12 -1
  7. bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +182 -29
  8. bedrock_agentcore_starter_toolkit/operations/runtime/destroy.py +24 -7
  9. bedrock_agentcore_starter_toolkit/operations/runtime/exceptions.py +27 -0
  10. bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +12 -3
  11. bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +99 -44
  12. bedrock_agentcore_starter_toolkit/operations/runtime/status.py +5 -4
  13. bedrock_agentcore_starter_toolkit/services/codebuild.py +53 -26
  14. bedrock_agentcore_starter_toolkit/services/ecr.py +44 -2
  15. bedrock_agentcore_starter_toolkit/utils/runtime/config.py +43 -1
  16. bedrock_agentcore_starter_toolkit/utils/runtime/container.py +89 -30
  17. bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +43 -4
  18. bedrock_agentcore_starter_toolkit/utils/runtime/templates/Dockerfile.j2 +1 -6
  19. bedrock_agentcore_starter_toolkit/utils/runtime/templates/dockerignore.template +1 -0
  20. {bedrock_agentcore_starter_toolkit-0.1.21.dist-info → bedrock_agentcore_starter_toolkit-0.1.23.dist-info}/METADATA +2 -2
  21. {bedrock_agentcore_starter_toolkit-0.1.21.dist-info → bedrock_agentcore_starter_toolkit-0.1.23.dist-info}/RECORD +25 -24
  22. {bedrock_agentcore_starter_toolkit-0.1.21.dist-info → bedrock_agentcore_starter_toolkit-0.1.23.dist-info}/WHEEL +0 -0
  23. {bedrock_agentcore_starter_toolkit-0.1.21.dist-info → bedrock_agentcore_starter_toolkit-0.1.23.dist-info}/entry_points.txt +0 -0
  24. {bedrock_agentcore_starter_toolkit-0.1.21.dist-info → bedrock_agentcore_starter_toolkit-0.1.23.dist-info}/licenses/LICENSE.txt +0 -0
  25. {bedrock_agentcore_starter_toolkit-0.1.21.dist-info → bedrock_agentcore_starter_toolkit-0.1.23.dist-info}/licenses/NOTICE.txt +0 -0
@@ -1,14 +1,16 @@
1
1
  """Configure operation - creates BedrockAgentCore configuration and Dockerfile."""
2
2
 
3
3
  import logging
4
+ import os
4
5
  import re
5
6
  from pathlib import Path
6
- from typing import Any, Dict, Optional, Tuple
7
+ from typing import Any, Dict, Literal, Optional, Tuple
7
8
 
8
9
  from ...cli.runtime.configuration_manager import ConfigurationManager
9
10
  from ...services.ecr import get_account_id, get_region
10
11
  from ...utils.runtime.config import merge_agent_config, save_config
11
12
  from ...utils.runtime.container import ContainerRuntime
13
+ from ...utils.runtime.entrypoint import detect_dependencies
12
14
  from ...utils.runtime.schema import (
13
15
  AWSConfig,
14
16
  BedrockAgentCoreAgentSchema,
@@ -24,6 +26,103 @@ from .models import ConfigureResult
24
26
  log = logging.getLogger(__name__)
25
27
 
26
28
 
29
+ def get_relative_path(path: Path, base: Optional[Path] = None) -> str:
30
+ """Convert path to relative format with OS-native separators.
31
+
32
+ Args:
33
+ path: Absolute or relative path
34
+ base: Base directory (defaults to current working directory)
35
+
36
+ Returns:
37
+ Path relative to base with OS-native separators
38
+
39
+ Raises:
40
+ ValueError: If path is empty or invalid
41
+ """
42
+ # Validate input
43
+ if not path or str(path).strip() == "":
44
+ raise ValueError("Path cannot be empty")
45
+
46
+ # Ensure path is a Path object
47
+ path_obj = Path(path) if not isinstance(path, Path) else path
48
+ base = base or Path.cwd()
49
+
50
+ try:
51
+ rel_path = path_obj.relative_to(base)
52
+ return str(rel_path)
53
+ except ValueError:
54
+ # Path is outside base - keep full path for clarity
55
+ # Don't lose directory structure by showing just the filename
56
+ return str(path_obj)
57
+
58
+
59
+ def detect_entrypoint(source_path: Path) -> Optional[Path]:
60
+ """Detect entrypoint file in source directory.
61
+
62
+ Args:
63
+ source_path: Directory to search for entrypoint
64
+
65
+ Returns:
66
+ Path to detected entrypoint file, or None if not found
67
+ """
68
+ ENTRYPOINT_CANDIDATES = ["agent.py", "app.py", "main.py", "__main__.py"]
69
+
70
+ source_dir = Path(source_path)
71
+ for candidate in ENTRYPOINT_CANDIDATES:
72
+ candidate_path = source_dir / candidate
73
+ if candidate_path.exists():
74
+ log.debug("Detected entrypoint: %s", candidate_path)
75
+ return candidate_path
76
+
77
+ log.debug("No entrypoint found in %s", source_path)
78
+ return None
79
+
80
+
81
+ def detect_requirements(source_path: Path):
82
+ """Detect requirements file in the source directory.
83
+
84
+ Args:
85
+ source_path: Source directory (where entrypoint is located)
86
+
87
+ Returns:
88
+ DependencyInfo object with detection results
89
+ """
90
+ # Resolve to absolute path for consistent behavior
91
+ source_path_resolved = Path(source_path).resolve()
92
+ log.debug("Checking for requirements in source directory: %s", source_path_resolved)
93
+
94
+ deps = detect_dependencies(source_path_resolved)
95
+ if deps.found:
96
+ log.debug("Found requirements in source directory: %s", deps.resolved_path)
97
+ else:
98
+ log.debug("No requirements file found in source directory: %s", source_path_resolved)
99
+
100
+ return deps
101
+
102
+
103
+ def infer_agent_name(entrypoint_path: Path, base: Optional[Path] = None) -> str:
104
+ """Infer agent name from entrypoint path.
105
+
106
+ Args:
107
+ entrypoint_path: Path to agent entrypoint file
108
+ base: Base directory for relative path (defaults to cwd)
109
+
110
+ Returns:
111
+ Suggested agent name (e.g., 'agents_writer_main' from 'agents/writer/main.py')
112
+ """
113
+ rel_entrypoint = get_relative_path(entrypoint_path, base)
114
+
115
+ # Remove .py extension if present (only at the end)
116
+ if rel_entrypoint.endswith(".py"):
117
+ rel_entrypoint = rel_entrypoint[:-3]
118
+
119
+ # Replace spaces and OS path separators with underscores
120
+ suggested_name = rel_entrypoint.replace(" ", "_").replace(os.sep, "_")
121
+
122
+ log.debug("Inferred agent name: %s from %s", suggested_name, get_relative_path(entrypoint_path, base))
123
+ return suggested_name
124
+
125
+
27
126
  def configure_bedrock_agentcore(
28
127
  agent_name: str,
29
128
  entrypoint_path: Path,
@@ -34,6 +133,7 @@ def configure_bedrock_agentcore(
34
133
  auto_create_ecr: bool = True,
35
134
  auto_create_execution_role: bool = True,
36
135
  enable_observability: bool = True,
136
+ memory_mode: Literal["NO_MEMORY", "STM_ONLY", "STM_AND_LTM"] = "STM_ONLY",
37
137
  requirements_file: Optional[str] = None,
38
138
  authorizer_configuration: Optional[Dict[str, Any]] = None,
39
139
  request_header_configuration: Optional[Dict[str, Any]] = None,
@@ -41,6 +141,7 @@ def configure_bedrock_agentcore(
41
141
  region: Optional[str] = None,
42
142
  protocol: Optional[str] = None,
43
143
  non_interactive: bool = False,
144
+ source_path: Optional[str] = None,
44
145
  ) -> ConfigureResult:
45
146
  """Configure Bedrock AgentCore application with deployment settings.
46
147
 
@@ -54,6 +155,7 @@ def configure_bedrock_agentcore(
54
155
  auto_create_ecr: Whether to auto-create ECR repository
55
156
  auto_create_execution_role: Whether to auto-create execution role if not provided
56
157
  enable_observability: Whether to enable observability
158
+ memory_mode: Memory configuration mode - "NO_MEMORY", "STM_ONLY" (default), or "STM_AND_LTM"
57
159
  requirements_file: Path to requirements file
58
160
  authorizer_configuration: JWT authorizer configuration dictionary
59
161
  request_header_configuration: Request header configuration dictionary
@@ -61,6 +163,7 @@ def configure_bedrock_agentcore(
61
163
  region: AWS region for deployment
62
164
  protocol: agent server protocol, must be either HTTP or MCP or A2A
63
165
  non_interactive: Skip interactive prompts and use defaults
166
+ source_path: Optional path to agent source code directory
64
167
 
65
168
  Returns:
66
169
  ConfigureResult model with configuration details
@@ -73,10 +176,13 @@ def configure_bedrock_agentcore(
73
176
  log.setLevel(logging.INFO)
74
177
  # Log agent name at the start of configuration
75
178
  log.info("Configuring BedrockAgentCore agent: %s", agent_name)
179
+
180
+ # Build directory is always project root for module validation and dependency detection
76
181
  build_dir = Path.cwd()
77
182
 
78
183
  if verbose:
79
184
  log.debug("Build directory: %s", build_dir)
185
+ log.debug("Source path: %s", source_path or "None (using build directory)")
80
186
  log.debug("Bedrock AgentCore name: %s", agent_name)
81
187
  log.debug("Entrypoint path: %s", entrypoint_path)
82
188
 
@@ -116,32 +222,52 @@ def configure_bedrock_agentcore(
116
222
  else:
117
223
  log.debug("No execution role provided and auto-create disabled")
118
224
 
119
- if verbose:
120
- log.debug("Prompting for memory configuration")
121
-
225
+ # Pass region to ConfigurationManager so it can check for existing memories
122
226
  config_manager = ConfigurationManager(build_dir / ".bedrock_agentcore.yaml", non_interactive)
123
227
 
124
- # New memory selection flow
125
- action, value = config_manager.prompt_memory_selection()
126
-
228
+ # Handle memory configuration
127
229
  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)
230
+
231
+ # Check if memory is explicitly disabled FIRST (works in both interactive and non-interactive modes)
232
+ if memory_mode == "NO_MEMORY":
233
+ memory_config.mode = "NO_MEMORY"
234
+ log.info("Memory disabled")
235
+ elif non_interactive:
236
+ # Non-interactive mode: use explicit memory_mode parameter
237
+ memory_config.mode = memory_mode
137
238
  memory_config.event_expiry_days = 30
138
239
  memory_config.memory_name = f"{agent_name}_memory"
139
- log.info("Will create new memory with mode: %s", value)
240
+ log.info("Will create new memory with mode: %s", memory_mode)
140
241
 
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")
242
+ if memory_mode == "STM_AND_LTM":
243
+ log.info("Memory configuration: Short-term + Long-term memory enabled")
244
+ else: # STM_ONLY
245
+ log.info("Memory configuration: Short-term memory only")
246
+ else:
247
+ # Interactive mode: prompt user (only if memory not explicitly disabled)
248
+ action, value = config_manager.prompt_memory_selection()
249
+
250
+ if action == "USE_EXISTING":
251
+ # Using existing memory - just store the ID
252
+ memory_config.memory_id = value
253
+ memory_config.mode = "STM_AND_LTM" # Assume existing has strategies
254
+ memory_config.memory_name = f"{agent_name}_memory"
255
+ log.info("Using existing memory resource: %s", value)
256
+ elif action == "CREATE_NEW":
257
+ # Create new with specified mode
258
+ memory_config.mode = value
259
+ memory_config.event_expiry_days = 30
260
+ memory_config.memory_name = f"{agent_name}_memory"
261
+ log.info("Will create new memory with mode: %s", value)
262
+
263
+ if value == "STM_AND_LTM":
264
+ log.info("Memory configuration: Short-term + Long-term memory enabled")
265
+ else: # STM_ONLY
266
+ log.info("Memory configuration: Short-term memory only")
267
+ elif action == "SKIP":
268
+ # User chose to skip memory setup
269
+ memory_config.mode = "NO_MEMORY"
270
+ log.info("Memory disabled by user choice")
145
271
 
146
272
  # Check for existing memory configuration from previous launch
147
273
  config_path = build_dir / ".bedrock_agentcore.yaml"
@@ -196,24 +322,50 @@ def configure_bedrock_agentcore(
196
322
  if memory_id:
197
323
  log.debug(" Memory ID: %s", memory_id)
198
324
 
325
+ # Determine output directory for Dockerfile based on source_path
326
+ # If source_path provided: write to .bedrock_agentcore/{agent_name}/ directly
327
+ # Otherwise: write to project root (legacy)
328
+ if source_path:
329
+ from ...utils.runtime.config import get_agentcore_directory
330
+
331
+ dockerfile_output_dir = get_agentcore_directory(Path.cwd(), agent_name, source_path)
332
+ else:
333
+ dockerfile_output_dir = build_dir
334
+
335
+ # Generate Dockerfile in the correct location (no moving needed)
199
336
  dockerfile_path = runtime.generate_dockerfile(
200
337
  entrypoint_path,
201
- build_dir,
338
+ dockerfile_output_dir,
202
339
  bedrock_agentcore_name or "bedrock_agentcore",
203
340
  region,
204
341
  enable_observability,
205
342
  requirements_file,
206
343
  memory_id,
207
344
  memory_name,
345
+ source_path,
208
346
  protocol,
209
347
  )
210
-
211
- # Check if .dockerignore was created
212
- dockerignore_path = build_dir / ".dockerignore"
213
-
214
- log.info("Generated Dockerfile: %s", dockerfile_path)
215
- if dockerignore_path.exists():
216
- log.info("Generated .dockerignore: %s", dockerignore_path)
348
+ # Log with relative path for better readability
349
+ rel_dockerfile_path = get_relative_path(Path(dockerfile_path))
350
+ log.info("Generated Dockerfile: %s", rel_dockerfile_path)
351
+
352
+ # Ensure .dockerignore exists at Docker build context location
353
+ if source_path:
354
+ # For source_path: .dockerignore at source directory (Docker build context)
355
+ source_dockerignore = Path(source_path) / ".dockerignore"
356
+ if not source_dockerignore.exists():
357
+ template_path = (
358
+ Path(__file__).parent.parent.parent / "utils" / "runtime" / "templates" / "dockerignore.template"
359
+ )
360
+ if template_path.exists():
361
+ source_dockerignore.write_text(template_path.read_text())
362
+ log.info("Generated .dockerignore: %s", source_dockerignore)
363
+ dockerignore_path = source_dockerignore
364
+ else:
365
+ # Legacy: .dockerignore at project root
366
+ dockerignore_path = build_dir / ".dockerignore"
367
+ if dockerignore_path.exists():
368
+ log.info("Generated .dockerignore: %s", dockerignore_path)
217
369
 
218
370
  # Handle project configuration (named agents)
219
371
  config_path = build_dir / ".bedrock_agentcore.yaml"
@@ -258,6 +410,7 @@ def configure_bedrock_agentcore(
258
410
  entrypoint=entrypoint,
259
411
  platform=ContainerRuntime.DEFAULT_PLATFORM,
260
412
  container_runtime=runtime.runtime,
413
+ source_path=str(Path(source_path).resolve()) if source_path else None,
261
414
  aws=AWSConfig(
262
415
  execution_role=execution_role_arn,
263
416
  execution_role_auto_create=execution_role_auto_create,
@@ -11,6 +11,7 @@ from ...operations.memory.manager import MemoryManager
11
11
  from ...services.runtime import BedrockAgentCoreClient
12
12
  from ...utils.runtime.config import load_config, save_config
13
13
  from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
14
+ from .exceptions import RuntimeToolkitException
14
15
  from .models import DestroyResult
15
16
 
16
17
  log = logging.getLogger(__name__)
@@ -79,7 +80,16 @@ def destroy_bedrock_agentcore(
79
80
  _destroy_codebuild_project(session, agent_config, result, dry_run)
80
81
 
81
82
  # 5. Remove memory resource
82
- _destroy_memory(session, agent_config, result, dry_run)
83
+ if agent_config.memory and agent_config.memory.memory_id and agent_config.memory.mode != "NO_MEMORY":
84
+ if agent_config.memory.was_created_by_toolkit:
85
+ # Memory was created by toolkit during configure/launch - delete it
86
+ _destroy_memory(session, agent_config, result, dry_run)
87
+ if not dry_run:
88
+ log.info("Deleted memory (was created by toolkit): %s", agent_config.memory.memory_id)
89
+ else:
90
+ # Memory was pre-existing - preserve it
91
+ result.warnings.append(f"Memory {agent_config.memory.memory_id} preserved (was pre-existing)")
92
+ log.info("Preserving pre-existing memory: %s", agent_config.memory.memory_id)
83
93
 
84
94
  # 6. Remove CodeBuild IAM Role
85
95
  _destroy_codebuild_iam_role(session, agent_config, result, dry_run)
@@ -102,7 +112,7 @@ def destroy_bedrock_agentcore(
102
112
 
103
113
  except Exception as e:
104
114
  log.error("Destroy operation failed: %s", str(e))
105
- raise RuntimeError(f"Destroy operation failed: {e}") from e
115
+ raise RuntimeToolkitException(f"Destroy operation failed: {e}") from e
106
116
 
107
117
 
108
118
  def _destroy_agentcore_endpoint(
@@ -129,10 +139,9 @@ def _destroy_agentcore_endpoint(
129
139
  endpoint_name = endpoint_response.get("name", "DEFAULT")
130
140
  endpoint_arn = endpoint_response.get("agentRuntimeEndpointArn")
131
141
 
132
- # Special case: DEFAULT endpoint cannot be explicitly deleted
142
+ # DEFAULT endpoint is automatically deleted when agent is deleted
133
143
  if endpoint_name == "DEFAULT":
134
- result.warnings.append("DEFAULT endpoint cannot be explicitly deleted, skipping")
135
- log.info("Skipping deletion of DEFAULT endpoint")
144
+ log.info("DEFAULT endpoint will be automatically deleted with agent")
136
145
  return
137
146
 
138
147
  if dry_run:
@@ -146,7 +155,13 @@ def _destroy_agentcore_endpoint(
146
155
  result.resources_removed.append(f"AgentCore endpoint: {endpoint_arn}")
147
156
  log.info("Deleted AgentCore endpoint: %s", endpoint_arn)
148
157
  except ClientError as delete_error:
149
- if delete_error.response["Error"]["Code"] not in ["ResourceNotFoundException", "NotFound"]:
158
+ error_code = delete_error.response["Error"]["Code"]
159
+
160
+ # Handle ConflictException for DEFAULT endpoint gracefully
161
+ if error_code == "ConflictException":
162
+ log.info("DEFAULT endpoint will be automatically deleted with agent")
163
+ return
164
+ elif error_code not in ["ResourceNotFoundException", "NotFound"]:
150
165
  result.errors.append(f"Failed to delete endpoint {endpoint_arn}: {delete_error}")
151
166
  log.error("Failed to delete endpoint: %s", delete_error)
152
167
  else:
@@ -368,7 +383,9 @@ def _destroy_codebuild_project(
368
383
  """Remove CodeBuild project for this agent."""
369
384
  try:
370
385
  codebuild_client = session.client("codebuild", region_name=agent_config.aws.region)
371
- project_name = f"bedrock-agentcore-{agent_config.name}-builder"
386
+ from ...services.ecr import sanitize_ecr_repo_name
387
+
388
+ project_name = f"bedrock-agentcore-{sanitize_ecr_repo_name(agent_config.name)}-builder"
372
389
 
373
390
  if dry_run:
374
391
  result.resources_removed.append(f"CodeBuild project: {project_name} (DRY RUN)")
@@ -0,0 +1,27 @@
1
+ """Exceptions for the Bedrock AgentCore Runtime module."""
2
+
3
+ from typing import List, Optional
4
+
5
+
6
+ class RuntimeException(Exception):
7
+ """Base exception for all Runtime SDK errors."""
8
+
9
+ pass
10
+
11
+
12
+ class RuntimeToolkitException(RuntimeException):
13
+ """Raised when runtime operations fail with resource tracking."""
14
+
15
+ def __init__(self, message: str, created_resources: Optional[List[str]] = None):
16
+ """Initialize RuntimeToolkitException with optional resource tracking.
17
+
18
+ Args:
19
+ message: Error message
20
+ created_resources: List of resources created before failure
21
+ """
22
+ self.created_resources = created_resources or []
23
+ if created_resources:
24
+ full_message = f"{message}. Resources created: {created_resources}"
25
+ else:
26
+ full_message = message
27
+ super().__init__(full_message)
@@ -30,10 +30,10 @@ def invoke_bedrock_agentcore(
30
30
  project_config = load_config(config_path)
31
31
  agent_config = project_config.get_agent_config(agent_name)
32
32
 
33
- # Check memory status on first invoke if LTM is enabled
33
+ # Check memory status on first invoke if memory is enabled (STM or LTM)
34
34
  if (
35
35
  agent_config.memory
36
- and agent_config.memory.has_ltm
36
+ and agent_config.memory.mode != "NO_MEMORY"
37
37
  and agent_config.memory.memory_id
38
38
  and not agent_config.memory.first_invoke_memory_check_done
39
39
  ):
@@ -45,10 +45,19 @@ def invoke_bedrock_agentcore(
45
45
  memory_status = memory_manager.get_memory_status(agent_config.memory.memory_id)
46
46
 
47
47
  if memory_status != MemoryStatus.ACTIVE.value:
48
+ # Determine memory type for better messaging
49
+ memory_type = "Memory"
50
+ if agent_config.memory.has_ltm:
51
+ memory_type = "Long-term memory"
52
+ time_estimate = "60-180 seconds"
53
+ else:
54
+ memory_type = "Short-term memory"
55
+ time_estimate = "30-90 seconds"
56
+
48
57
  # Provide graceful error message
49
58
  error_message = (
50
59
  f"Memory is still provisioning (current status: {memory_status}). "
51
- f"Long-term memory extraction takes 60-180 seconds to activate.\n\n"
60
+ f"{memory_type} takes {time_estimate} to activate.\n\n"
52
61
  f"Please wait and check status with:\n"
53
62
  f" agentcore status{f' --agent {agent_name}' if agent_name else ''}"
54
63
  )
@@ -19,6 +19,7 @@ from ...utils.runtime.container import ContainerRuntime
19
19
  from ...utils.runtime.logs import get_genai_observability_url
20
20
  from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
21
21
  from .create_role import get_or_create_runtime_execution_role
22
+ from .exceptions import RuntimeToolkitException
22
23
  from .models import LaunchResult
23
24
 
24
25
  log = logging.getLogger(__name__)
@@ -139,7 +140,13 @@ def _ensure_memory_for_agent(
139
140
  """Ensure memory resource exists for agent. Returns memory_id or None.
140
141
 
141
142
  This function is idempotent - it creates memory if needed or reuses existing.
143
+ CRITICAL: Never overwrites was_created_by_toolkit flag - that's set by configure.
142
144
  """
145
+ # Check if memory is disabled
146
+ if agent_config.memory and agent_config.memory.mode == "NO_MEMORY":
147
+ log.info("Memory disabled - skipping memory creation")
148
+ return None
149
+
143
150
  # If memory already exists, return it
144
151
  if agent_config.memory and agent_config.memory.memory_id:
145
152
  log.info("Using existing memory: %s", agent_config.memory.memory_id)
@@ -157,14 +164,16 @@ def _ensure_memory_for_agent(
157
164
  memory_manager = MemoryManager(region_name=agent_config.aws.region)
158
165
  memory_name = f"{agent_name}_mem" # Short name under 48 char limit
159
166
 
160
- # Check if memory already exists
167
+ # Check if memory already exists in cloud
161
168
  existing_memory = None
162
169
  try:
163
170
  memories = memory_manager.list_memories()
164
171
  for m in memories:
165
172
  if m.id.startswith(memory_name):
166
173
  existing_memory = memory_manager.get_memory(m.id)
167
- log.info("Found existing memory: %s", m.id)
174
+ log.info("Found existing memory in cloud: %s", m.id)
175
+ # DO NOT OVERWRITE was_created_by_toolkit flag
176
+ # The flag from configure tells us the user's intent
168
177
  break
169
178
  except Exception as e:
170
179
  log.debug("Error checking for existing memory: %s", e)
@@ -250,9 +259,16 @@ def _ensure_memory_for_agent(
250
259
  event_expiry_days=agent_config.memory.event_expiry_days or 30,
251
260
  memory_execution_role_arn=None,
252
261
  )
262
+
263
+ # Ensure was_created_by_toolkit is True since we just created it
264
+ # (Should already be True from configure if user chose CREATE_NEW)
265
+ if not agent_config.memory.was_created_by_toolkit:
266
+ log.warning("Memory created but flag was False - correcting to True")
267
+ agent_config.memory.was_created_by_toolkit = True
268
+
253
269
  log.info("✅ New memory created: %s (provisioning in background)", memory.id)
254
270
 
255
- # Save memory configuration
271
+ # Save memory configuration (preserving was_created_by_toolkit flag)
256
272
  agent_config.memory.memory_id = memory.id
257
273
  agent_config.memory.memory_arn = memory.arn
258
274
  agent_config.memory.memory_name = memory_name
@@ -479,14 +495,24 @@ def launch_bedrock_agentcore(
479
495
  "💡 For local builds, please install Docker, Finch, or Podman"
480
496
  )
481
497
 
482
- # Get build context - always use project root (where config and Dockerfile are)
483
- build_dir = config_path.parent
498
+ # Get build context - use source_path if configured, otherwise use project root
499
+ build_dir = Path(agent_config.source_path) if agent_config.source_path else config_path.parent
500
+ log.info("Using build directory: %s", build_dir)
484
501
 
485
502
  bedrock_agentcore_name = agent_config.name
486
503
  tag = f"bedrock_agentcore-{bedrock_agentcore_name}:latest"
487
504
 
488
505
  # Step 1: Build Docker image (only if we need it)
489
- success, output = runtime.build(build_dir, tag)
506
+ # When using source_path, Dockerfile is in .bedrock_agentcore/{agent_name}/ directory
507
+ from ...utils.runtime.config import get_agentcore_directory
508
+
509
+ dockerfile_dir = get_agentcore_directory(config_path.parent, agent_config.name, agent_config.source_path)
510
+ dockerfile_path = dockerfile_dir / "Dockerfile"
511
+
512
+ if not dockerfile_path.exists():
513
+ raise RuntimeError(f"Dockerfile not found at {dockerfile_path}. Please run 'agentcore configure' first.")
514
+
515
+ success, output = runtime.build(build_dir, tag, dockerfile_path=dockerfile_path)
490
516
  if not success:
491
517
  error_lines = output[-10:] if len(output) > 10 else output
492
518
  error_message = " ".join(error_lines)
@@ -573,53 +599,82 @@ def _execute_codebuild_workflow(
573
599
  agent_config.aws.account,
574
600
  agent_config.aws.region,
575
601
  )
576
- # Validate configuration
577
- errors = agent_config.validate(for_local=False)
578
- if errors:
579
- raise ValueError(f"Invalid configuration: {', '.join(errors)}")
580
602
 
581
- region = agent_config.aws.region
582
- if not region:
583
- raise ValueError("Region not found in configuration")
603
+ # Track created resources for error context
604
+ created_resources = []
584
605
 
585
- session = boto3.Session(region_name=region)
586
- account_id = agent_config.aws.account # Use existing account from config
606
+ try:
607
+ # Validate configuration
608
+ errors = agent_config.validate(for_local=False)
609
+ if errors:
610
+ raise ValueError(f"Invalid configuration: {', '.join(errors)}")
611
+
612
+ region = agent_config.aws.region
613
+ if not region:
614
+ raise ValueError("Region not found in configuration")
615
+
616
+ session = boto3.Session(region_name=region)
617
+ account_id = agent_config.aws.account # Use existing account from config
618
+
619
+ # Setup AWS resources
620
+ log.info("Setting up AWS resources (ECR repository%s)...", "" if ecr_only else ", execution roles")
621
+ ecr_uri = _ensure_ecr_repository(agent_config, project_config, config_path, agent_name, region)
622
+ if ecr_uri:
623
+ created_resources.append(f"ECR Repository: {ecr_uri}")
624
+ ecr_repository_arn = f"arn:aws:ecr:{region}:{account_id}:repository/{ecr_uri.split('/')[-1]}"
625
+
626
+ # Setup execution role only if not ECR-only mode
627
+ if not ecr_only:
628
+ _ensure_execution_role(agent_config, project_config, config_path, agent_name, region, account_id)
629
+ if agent_config.aws.execution_role:
630
+ created_resources.append(f"Runtime Execution Role: {agent_config.aws.execution_role}")
631
+
632
+ # Prepare CodeBuild
633
+ log.info("Preparing CodeBuild project and uploading source...")
634
+ codebuild_service = CodeBuildService(session)
635
+
636
+ # Use cached CodeBuild role from config if available
637
+ if hasattr(agent_config, "codebuild") and agent_config.codebuild.execution_role:
638
+ log.info("Using CodeBuild role from config: %s", agent_config.codebuild.execution_role)
639
+ codebuild_execution_role = agent_config.codebuild.execution_role
640
+ else:
641
+ codebuild_execution_role = codebuild_service.create_codebuild_execution_role(
642
+ account_id=account_id, ecr_repository_arn=ecr_repository_arn, agent_name=agent_name
643
+ )
644
+ if codebuild_execution_role:
645
+ created_resources.append(f"CodeBuild Execution Role: {codebuild_execution_role}")
587
646
 
588
- # Setup AWS resources
589
- log.info("Setting up AWS resources (ECR repository%s)...", "" if ecr_only else ", execution roles")
590
- ecr_uri = _ensure_ecr_repository(agent_config, project_config, config_path, agent_name, region)
591
- ecr_repository_arn = f"arn:aws:ecr:{region}:{account_id}:repository/{ecr_uri.split('/')[-1]}"
647
+ # Get source directory - use source_path if configured, otherwise use current directory
648
+ source_dir = str(Path(agent_config.source_path)) if agent_config.source_path else "."
592
649
 
593
- # Setup execution role only if not ECR-only mode
594
- if not ecr_only:
595
- _ensure_execution_role(agent_config, project_config, config_path, agent_name, region, account_id)
650
+ # Get Dockerfile directory - use agentcore directory if source_path provided
651
+ from ...utils.runtime.config import get_agentcore_directory
596
652
 
597
- # Prepare CodeBuild
598
- log.info("Preparing CodeBuild project and uploading source...")
599
- codebuild_service = CodeBuildService(session)
653
+ dockerfile_dir = get_agentcore_directory(config_path.parent, agent_name, agent_config.source_path)
600
654
 
601
- # Use cached CodeBuild role from config if available
602
- if hasattr(agent_config, "codebuild") and agent_config.codebuild.execution_role:
603
- log.info("Using CodeBuild role from config: %s", agent_config.codebuild.execution_role)
604
- codebuild_execution_role = agent_config.codebuild.execution_role
605
- else:
606
- codebuild_execution_role = codebuild_service.create_codebuild_execution_role(
607
- account_id=account_id, ecr_repository_arn=ecr_repository_arn, agent_name=agent_name
655
+ source_location = codebuild_service.upload_source(
656
+ agent_name=agent_name, source_dir=source_dir, dockerfile_dir=str(dockerfile_dir)
608
657
  )
609
658
 
610
- source_location = codebuild_service.upload_source(agent_name=agent_name)
659
+ # Use cached project name from config if available
660
+ if hasattr(agent_config, "codebuild") and agent_config.codebuild.project_name:
661
+ log.info("Using CodeBuild project from config: %s", agent_config.codebuild.project_name)
662
+ project_name = agent_config.codebuild.project_name
663
+ else:
664
+ project_name = codebuild_service.create_or_update_project(
665
+ agent_name=agent_name,
666
+ ecr_repository_uri=ecr_uri,
667
+ execution_role=codebuild_execution_role,
668
+ source_location=source_location,
669
+ )
670
+ if project_name:
671
+ created_resources.append(f"CodeBuild Project: {project_name}")
611
672
 
612
- # Use cached project name from config if available
613
- if hasattr(agent_config, "codebuild") and agent_config.codebuild.project_name:
614
- log.info("Using CodeBuild project from config: %s", agent_config.codebuild.project_name)
615
- project_name = agent_config.codebuild.project_name
616
- else:
617
- project_name = codebuild_service.create_or_update_project(
618
- agent_name=agent_name,
619
- ecr_repository_uri=ecr_uri,
620
- execution_role=codebuild_execution_role,
621
- source_location=source_location,
622
- )
673
+ except Exception as e:
674
+ if created_resources:
675
+ log.error("Launch failed after creating the following resources: %s. Error: %s", created_resources, str(e))
676
+ raise RuntimeToolkitException("Launch failed", created_resources) from e
677
+ raise
623
678
 
624
679
  # Execute CodeBuild
625
680
  log.info("Starting CodeBuild build (this may take several minutes)...")