bedrock-agentcore-starter-toolkit 0.1.26__py3-none-any.whl → 0.1.28__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 (23) hide show
  1. bedrock_agentcore_starter_toolkit/cli/cli.py +9 -1
  2. bedrock_agentcore_starter_toolkit/cli/runtime/commands.py +263 -12
  3. bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py +31 -7
  4. bedrock_agentcore_starter_toolkit/notebook/runtime/bedrock_agentcore.py +240 -2
  5. bedrock_agentcore_starter_toolkit/operations/memory/manager.py +20 -33
  6. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/base.py +2 -0
  7. bedrock_agentcore_starter_toolkit/operations/memory/models/strategies/self_managed.py +107 -0
  8. bedrock_agentcore_starter_toolkit/operations/runtime/__init__.py +4 -0
  9. bedrock_agentcore_starter_toolkit/operations/runtime/configure.py +130 -11
  10. bedrock_agentcore_starter_toolkit/operations/runtime/invoke.py +0 -53
  11. bedrock_agentcore_starter_toolkit/operations/runtime/launch.py +213 -16
  12. bedrock_agentcore_starter_toolkit/operations/runtime/models.py +19 -0
  13. bedrock_agentcore_starter_toolkit/operations/runtime/status.py +30 -0
  14. bedrock_agentcore_starter_toolkit/operations/runtime/stop_session.py +123 -0
  15. bedrock_agentcore_starter_toolkit/operations/runtime/vpc_validation.py +196 -0
  16. bedrock_agentcore_starter_toolkit/services/runtime.py +43 -1
  17. bedrock_agentcore_starter_toolkit/utils/runtime/schema.py +44 -1
  18. {bedrock_agentcore_starter_toolkit-0.1.26.dist-info → bedrock_agentcore_starter_toolkit-0.1.28.dist-info}/METADATA +8 -8
  19. {bedrock_agentcore_starter_toolkit-0.1.26.dist-info → bedrock_agentcore_starter_toolkit-0.1.28.dist-info}/RECORD +23 -20
  20. {bedrock_agentcore_starter_toolkit-0.1.26.dist-info → bedrock_agentcore_starter_toolkit-0.1.28.dist-info}/WHEEL +0 -0
  21. {bedrock_agentcore_starter_toolkit-0.1.26.dist-info → bedrock_agentcore_starter_toolkit-0.1.28.dist-info}/entry_points.txt +0 -0
  22. {bedrock_agentcore_starter_toolkit-0.1.26.dist-info → bedrock_agentcore_starter_toolkit-0.1.28.dist-info}/licenses/LICENSE.txt +0 -0
  23. {bedrock_agentcore_starter_toolkit-0.1.26.dist-info → bedrock_agentcore_starter_toolkit-0.1.28.dist-info}/licenses/NOTICE.txt +0 -0
@@ -4,11 +4,13 @@ import logging
4
4
  import os
5
5
  import re
6
6
  from pathlib import Path
7
- from typing import Any, Dict, Literal, Optional, Tuple
7
+ from typing import Any, Dict, List, Literal, Optional, Tuple
8
+
9
+ import boto3
8
10
 
9
11
  from ...cli.runtime.configuration_manager import ConfigurationManager
10
12
  from ...services.ecr import get_account_id, get_region
11
- from ...utils.runtime.config import merge_agent_config, save_config
13
+ from ...utils.runtime.config import load_config_if_exists, merge_agent_config, save_config
12
14
  from ...utils.runtime.container import ContainerRuntime
13
15
  from ...utils.runtime.entrypoint import detect_dependencies
14
16
  from ...utils.runtime.schema import (
@@ -16,8 +18,10 @@ from ...utils.runtime.schema import (
16
18
  BedrockAgentCoreAgentSchema,
17
19
  BedrockAgentCoreDeploymentInfo,
18
20
  CodeBuildConfig,
21
+ LifecycleConfiguration,
19
22
  MemoryConfig,
20
23
  NetworkConfiguration,
24
+ NetworkModeConfig,
21
25
  ObservabilityConfig,
22
26
  ProtocolConfiguration,
23
27
  )
@@ -56,26 +60,30 @@ def get_relative_path(path: Path, base: Optional[Path] = None) -> str:
56
60
  return str(path_obj)
57
61
 
58
62
 
59
- def detect_entrypoint(source_path: Path) -> Optional[Path]:
60
- """Detect entrypoint file in source directory.
63
+ def detect_entrypoint(source_path: Path) -> List[Path]:
64
+ """Detect entrypoint files in source directory.
61
65
 
62
66
  Args:
63
67
  source_path: Directory to search for entrypoint
64
68
 
65
69
  Returns:
66
- Path to detected entrypoint file, or None if not found
70
+ List of detected entrypoint files (empty list if none found)
67
71
  """
68
72
  ENTRYPOINT_CANDIDATES = ["agent.py", "app.py", "main.py", "__main__.py"]
69
73
 
70
74
  source_dir = Path(source_path)
75
+ found_files = []
76
+
71
77
  for candidate in ENTRYPOINT_CANDIDATES:
72
78
  candidate_path = source_dir / candidate
73
79
  if candidate_path.exists():
80
+ found_files.append(candidate_path)
74
81
  log.debug("Detected entrypoint: %s", candidate_path)
75
- return candidate_path
76
82
 
77
- log.debug("No entrypoint found in %s", source_path)
78
- return None
83
+ if not found_files:
84
+ log.debug("No entrypoint found in %s", source_path)
85
+
86
+ return found_files
79
87
 
80
88
 
81
89
  def detect_requirements(source_path: Path):
@@ -133,7 +141,7 @@ def configure_bedrock_agentcore(
133
141
  auto_create_ecr: bool = True,
134
142
  auto_create_execution_role: bool = True,
135
143
  enable_observability: bool = True,
136
- memory_mode: Literal["NO_MEMORY", "STM_ONLY", "STM_AND_LTM"] = "STM_ONLY",
144
+ memory_mode: Literal["NO_MEMORY", "STM_ONLY", "STM_AND_LTM"] = "NO_MEMORY",
137
145
  requirements_file: Optional[str] = None,
138
146
  authorizer_configuration: Optional[Dict[str, Any]] = None,
139
147
  request_header_configuration: Optional[Dict[str, Any]] = None,
@@ -142,6 +150,11 @@ def configure_bedrock_agentcore(
142
150
  protocol: Optional[str] = None,
143
151
  non_interactive: bool = False,
144
152
  source_path: Optional[str] = None,
153
+ vpc_enabled: bool = False,
154
+ vpc_subnets: Optional[List[str]] = None,
155
+ vpc_security_groups: Optional[List[str]] = None,
156
+ idle_timeout: Optional[int] = None,
157
+ max_lifetime: Optional[int] = None,
145
158
  ) -> ConfigureResult:
146
159
  """Configure Bedrock AgentCore application with deployment settings.
147
160
 
@@ -164,6 +177,13 @@ def configure_bedrock_agentcore(
164
177
  protocol: agent server protocol, must be either HTTP or MCP or A2A
165
178
  non_interactive: Skip interactive prompts and use defaults
166
179
  source_path: Optional path to agent source code directory
180
+ vpc_enabled: Whether to enable VPC networking mode
181
+ vpc_subnets: List of subnet IDs for VPC mode
182
+ vpc_security_groups: List of security group IDs for VPC mode
183
+ idle_timeout: Idle runtime session timeout in seconds (60-28800).
184
+ If not specified, AWS API default (900s / 15 minutes) is used.
185
+ max_lifetime: Maximum instance lifetime in seconds (60-28800).
186
+ If not specified, AWS API default (28800s / 8 hours) is used.
167
187
 
168
188
  Returns:
169
189
  ConfigureResult model with configuration details
@@ -244,7 +264,7 @@ def configure_bedrock_agentcore(
244
264
  else: # STM_ONLY
245
265
  log.info("Memory configuration: Short-term memory only")
246
266
  else:
247
- # Interactive mode: prompt user (only if memory not explicitly disabled)
267
+ # Interactive mode - let user choose
248
268
  action, value = config_manager.prompt_memory_selection()
249
269
 
250
270
  if action == "USE_EXISTING":
@@ -274,6 +294,21 @@ def configure_bedrock_agentcore(
274
294
  memory_id = None
275
295
  memory_name = None
276
296
 
297
+ # Handle lifecycle configuration
298
+ lifecycle_config = LifecycleConfiguration()
299
+ if idle_timeout is not None or max_lifetime is not None:
300
+ lifecycle_config = LifecycleConfiguration(
301
+ idle_runtime_session_timeout=idle_timeout,
302
+ max_lifetime=max_lifetime,
303
+ )
304
+
305
+ if verbose:
306
+ log.debug("Lifecycle configuration:")
307
+ if idle_timeout:
308
+ log.debug(" Idle timeout: %ds (%d minutes)", idle_timeout, idle_timeout / 60)
309
+ if max_lifetime:
310
+ log.debug(" Max lifetime: %ds (%d hours)", max_lifetime, max_lifetime / 3600)
311
+
277
312
  if config_path.exists():
278
313
  try:
279
314
  from ...utils.runtime.config import load_config
@@ -305,6 +340,42 @@ def configure_bedrock_agentcore(
305
340
  if verbose and execution_role_arn:
306
341
  log.debug("Using same role for CodeBuild: %s", codebuild_execution_role_arn)
307
342
 
343
+ if vpc_enabled:
344
+ if not vpc_subnets or not vpc_security_groups:
345
+ raise ValueError("VPC mode requires both subnets and security groups")
346
+
347
+ for subnet_id in vpc_subnets:
348
+ if not subnet_id.startswith("subnet-"):
349
+ raise ValueError(
350
+ f"Invalid subnet ID format: {subnet_id}\nSubnet IDs must start with 'subnet-' (e.g., subnet-abc123)"
351
+ )
352
+ if len(subnet_id) < 15: # "subnet-" (7) + 8 chars = 15
353
+ raise ValueError(
354
+ f"Invalid subnet ID format: {subnet_id}\nSubnet ID is too short. Expected format: subnet-xxxxxxxx"
355
+ )
356
+
357
+ # Validate security group IDs format
358
+ for sg_id in vpc_security_groups:
359
+ if not sg_id.startswith("sg-"):
360
+ raise ValueError(
361
+ f"Invalid security group ID format: {sg_id}\n"
362
+ f"Security group IDs must start with 'sg-' (e.g., sg-abc123)"
363
+ )
364
+ if len(sg_id) < 11: # "sg-" (3) + 8 chars = 11
365
+ raise ValueError(
366
+ f"Invalid security group ID format: {sg_id}\n"
367
+ f"Security group ID is too short. Expected format: sg-xxxxxxxx"
368
+ )
369
+
370
+ network_config = NetworkConfiguration(
371
+ network_mode="VPC",
372
+ network_mode_config=NetworkModeConfig(subnets=vpc_subnets, security_groups=vpc_security_groups),
373
+ )
374
+ log.info("Network mode: VPC with %d subnets and %d security groups", len(vpc_subnets), len(vpc_security_groups))
375
+ else:
376
+ network_config = NetworkConfiguration(network_mode="PUBLIC")
377
+ log.info("Network mode: PUBLIC")
378
+
308
379
  # Generate Dockerfile and .dockerignore
309
380
  bedrock_agentcore_name = None
310
381
  # Try to find the variable name for the Bedrock AgentCore instance in the file
@@ -332,6 +403,11 @@ def configure_bedrock_agentcore(
332
403
  else:
333
404
  dockerfile_output_dir = build_dir
334
405
 
406
+ if memory_config.mode == "NO_MEMORY":
407
+ memory_id = None
408
+ memory_name = None
409
+ log.debug("Cleared memory_id/name for Dockerfile generation (memory disabled)")
410
+
335
411
  # Generate Dockerfile in the correct location (no moving needed)
336
412
  dockerfile_path = runtime.generate_dockerfile(
337
413
  entrypoint_path,
@@ -374,6 +450,32 @@ def configure_bedrock_agentcore(
374
450
  log.debug("Agent name from BedrockAgentCoreApp: %s", agent_name)
375
451
  log.debug("Config path: %s", config_path)
376
452
 
453
+ existing_project_config = load_config_if_exists(config_path)
454
+
455
+ if existing_project_config and agent_name in existing_project_config.agents:
456
+ existing_agent = existing_project_config.agents[agent_name]
457
+ existing_network = existing_agent.aws.network_configuration
458
+
459
+ # Import validation helper
460
+ from .vpc_validation import check_network_immutability
461
+
462
+ # Check if network config is being changed
463
+ error = check_network_immutability(
464
+ existing_network_mode=existing_network.network_mode,
465
+ existing_subnets=existing_network.network_mode_config.subnets
466
+ if existing_network.network_mode_config
467
+ else None,
468
+ existing_security_groups=existing_network.network_mode_config.security_groups
469
+ if existing_network.network_mode_config
470
+ else None,
471
+ new_network_mode="VPC" if vpc_enabled else "PUBLIC",
472
+ new_subnets=vpc_subnets,
473
+ new_security_groups=vpc_security_groups,
474
+ )
475
+
476
+ if error:
477
+ raise ValueError(error)
478
+
377
479
  # Convert to POSIX for cross-platform compatibility
378
480
  entrypoint_path_str = entrypoint_path.as_posix()
379
481
 
@@ -418,9 +520,10 @@ def configure_bedrock_agentcore(
418
520
  region=region,
419
521
  ecr_repository=ecr_repository,
420
522
  ecr_auto_create=ecr_auto_create_value,
421
- network_configuration=NetworkConfiguration(network_mode="PUBLIC"),
523
+ network_configuration=network_config,
422
524
  protocol_configuration=ProtocolConfiguration(server_protocol=protocol or "HTTP"),
423
525
  observability=ObservabilityConfig(enabled=enable_observability),
526
+ lifecycle_configuration=lifecycle_config,
424
527
  ),
425
528
  bedrock_agentcore=BedrockAgentCoreDeploymentInfo(),
426
529
  codebuild=CodeBuildConfig(
@@ -438,6 +541,18 @@ def configure_bedrock_agentcore(
438
541
  if verbose:
439
542
  log.debug("Configuration saved with agent: %s", agent_name)
440
543
 
544
+ # Get VPC ID for display if VPC mode
545
+ vpc_id = None
546
+ if vpc_enabled and vpc_subnets:
547
+ try:
548
+ session = boto3.Session(region_name=region)
549
+ ec2_client = session.client("ec2", region_name=region)
550
+ subnet_response = ec2_client.describe_subnets(SubnetIds=[vpc_subnets[0]])
551
+ if subnet_response["Subnets"]:
552
+ vpc_id = subnet_response["Subnets"][0]["VpcId"]
553
+ except Exception:
554
+ pass # nosec B110
555
+
441
556
  return ConfigureResult(
442
557
  config_path=config_path,
443
558
  dockerfile_path=dockerfile_path,
@@ -448,6 +563,10 @@ def configure_bedrock_agentcore(
448
563
  execution_role=execution_role_arn,
449
564
  ecr_repository=ecr_repository,
450
565
  auto_create_ecr=auto_create_ecr and not ecr_repository,
566
+ network_mode="VPC" if vpc_enabled else "PUBLIC",
567
+ network_subnets=vpc_subnets if vpc_enabled else None,
568
+ network_security_groups=vpc_security_groups if vpc_enabled else None,
569
+ network_vpc_id=vpc_id,
451
570
  )
452
571
 
453
572
 
@@ -31,59 +31,6 @@ def invoke_bedrock_agentcore(
31
31
  project_config = load_config(config_path)
32
32
  agent_config = project_config.get_agent_config(agent_name)
33
33
 
34
- # Check memory status on first invoke if memory is enabled (STM or LTM)
35
- if (
36
- agent_config.memory
37
- and agent_config.memory.mode != "NO_MEMORY"
38
- and agent_config.memory.memory_id
39
- and not agent_config.memory.first_invoke_memory_check_done
40
- ):
41
- try:
42
- from ...operations.memory.constants import MemoryStatus
43
- from ...operations.memory.manager import MemoryManager
44
-
45
- memory_manager = MemoryManager(region_name=agent_config.aws.region)
46
- memory_status = memory_manager.get_memory_status(agent_config.memory.memory_id)
47
-
48
- if memory_status != MemoryStatus.ACTIVE.value:
49
- # Determine memory type for better messaging
50
- memory_type = "Memory"
51
- if agent_config.memory.has_ltm:
52
- memory_type = "Long-term memory"
53
- time_estimate = "60-180 seconds"
54
- else:
55
- memory_type = "Short-term memory"
56
- time_estimate = "30-90 seconds"
57
-
58
- # Provide graceful error message
59
- error_message = (
60
- f"Memory is still provisioning (current status: {memory_status}). "
61
- f"{memory_type} takes {time_estimate} to activate.\n\n"
62
- f"Please wait and check status with:\n"
63
- f" agentcore status{f' --agent {agent_name}' if agent_name else ''}"
64
- )
65
-
66
- # Log the message for visibility
67
- log.warning("Memory not yet active for agent '%s': %s", agent_config.name, memory_status)
68
-
69
- raise ValueError(error_message)
70
-
71
- # Memory is active, mark check as done
72
- agent_config.memory.first_invoke_memory_check_done = True
73
- project_config.agents[agent_config.name] = agent_config
74
- save_config(project_config, config_path)
75
- log.info("Memory is active, proceeding with invoke")
76
-
77
- except ImportError as e:
78
- log.error("Failed to import MemoryManager: %s", e)
79
- # Continue without check if import fails
80
- except Exception as e:
81
- # If it's our ValueError, re-raise it
82
- if "Memory is still provisioning" in str(e):
83
- raise
84
- # For other errors, log but continue
85
- log.warning("Could not check memory status: %s", e)
86
-
87
34
  # Log which agent is being invoked
88
35
  mode = "locally" if local_mode else "via cloud endpoint"
89
36
  log.debug("Invoking BedrockAgentCore agent '%s' %s", agent_config.name, mode)
@@ -5,10 +5,11 @@ import logging
5
5
  import time
6
6
  import urllib.parse
7
7
  from pathlib import Path
8
- from typing import Optional
8
+ from typing import List, Optional
9
9
 
10
10
  import boto3
11
11
  from botocore.exceptions import ClientError
12
+ from rich.console import Console
12
13
 
13
14
  from ...services.codebuild import CodeBuildService
14
15
  from ...services.ecr import deploy_to_ecr, get_or_create_ecr_repository
@@ -22,9 +23,122 @@ from .create_role import get_or_create_runtime_execution_role
22
23
  from .exceptions import RuntimeToolkitException
23
24
  from .models import LaunchResult
24
25
 
26
+ # console = Console()
27
+
25
28
  log = logging.getLogger(__name__)
26
29
 
27
30
 
31
+ def _validate_vpc_resources(session: boto3.Session, agent_config, region: str) -> None:
32
+ """Validate VPC resources exist and are in the same VPC.
33
+
34
+ Args:
35
+ session: Boto3 session
36
+ agent_config: Agent configuration
37
+ region: AWS region
38
+
39
+ Raises:
40
+ ValueError: If validation fails
41
+ """
42
+ network_config = agent_config.aws.network_configuration
43
+
44
+ if network_config.network_mode != "VPC":
45
+ return # Nothing to validate for PUBLIC mode
46
+
47
+ if not network_config.network_mode_config:
48
+ raise ValueError("VPC mode requires network configuration")
49
+
50
+ subnets = network_config.network_mode_config.subnets
51
+ security_groups = network_config.network_mode_config.security_groups
52
+
53
+ if not subnets or not security_groups:
54
+ raise ValueError("VPC mode requires both subnets and security groups")
55
+
56
+ ec2_client = session.client("ec2", region_name=region)
57
+
58
+ # Validate subnets exist and get their VPC IDs
59
+ try:
60
+ subnet_response = ec2_client.describe_subnets(SubnetIds=subnets)
61
+ subnet_vpcs = {subnet["VpcId"] for subnet in subnet_response["Subnets"]}
62
+
63
+ if len(subnet_vpcs) > 1:
64
+ raise ValueError(
65
+ f"All subnets must be in the same VPC. "
66
+ f"Found subnets in {len(subnet_vpcs)} different VPCs: {subnet_vpcs}"
67
+ )
68
+
69
+ vpc_id = subnet_vpcs.pop()
70
+ log.info("✓ All %d subnets are in VPC: %s", len(subnets), vpc_id)
71
+
72
+ except ClientError as e:
73
+ if e.response["Error"]["Code"] == "InvalidSubnetID.NotFound":
74
+ raise ValueError(f"One or more subnet IDs not found: {subnets}") from e
75
+ raise ValueError(f"Failed to validate subnets: {e}") from e
76
+
77
+ # Validate security groups exist and are in the same VPC
78
+ try:
79
+ sg_response = ec2_client.describe_security_groups(GroupIds=security_groups)
80
+ sg_vpcs = {sg["VpcId"] for sg in sg_response["SecurityGroups"]}
81
+
82
+ if len(sg_vpcs) > 1:
83
+ raise ValueError(
84
+ f"All security groups must be in the same VPC. Found {len(sg_vpcs)} different VPCs: {sg_vpcs}"
85
+ )
86
+
87
+ sg_vpc_id = sg_vpcs.pop()
88
+
89
+ if sg_vpc_id != vpc_id:
90
+ raise ValueError(
91
+ f"Security groups must be in the same VPC as subnets. "
92
+ f"Subnets are in VPC {vpc_id}, but security groups are in VPC {sg_vpc_id}"
93
+ )
94
+
95
+ log.info("✓ All %d security groups are in VPC: %s", len(security_groups), vpc_id)
96
+
97
+ except ClientError as e:
98
+ if e.response["Error"]["Code"] == "InvalidGroup.NotFound":
99
+ raise ValueError(f"One or more security group IDs not found: {security_groups}") from e
100
+ raise ValueError(f"Failed to validate security groups: {e}") from e
101
+
102
+ log.info("✓ VPC configuration validated successfully")
103
+
104
+
105
+ def _ensure_network_service_linked_role(session: boto3.Session, logger) -> None:
106
+ """Ensure the AgentCore Network service-linked role exists."""
107
+ iam_client = session.client("iam")
108
+ role_name = "AWSServiceRoleForBedrockAgentCoreNetwork"
109
+
110
+ try:
111
+ # Check if role exists
112
+ iam_client.get_role(RoleName=role_name)
113
+ logger.info("✓ VPC service-linked role verified: %s", role_name)
114
+
115
+ except ClientError as e:
116
+ if e.response["Error"]["Code"] != "NoSuchEntity":
117
+ raise
118
+
119
+ logger.info("Creating VPC service-linked role...")
120
+
121
+ try:
122
+ iam_client.create_service_linked_role(
123
+ AWSServiceName="network.bedrock-agentcore.amazonaws.com",
124
+ Description="Service-linked role for Amazon Bedrock AgentCore VPC networking",
125
+ )
126
+ logger.info("✓ VPC service-linked role created: %s", role_name)
127
+
128
+ # Wait for propagation
129
+ import time
130
+
131
+ logger.info(" Waiting 10 seconds for IAM propagation...")
132
+ time.sleep(10)
133
+
134
+ except ClientError as e:
135
+ if e.response["Error"]["Code"] == "InvalidInput":
136
+ logger.info("✓ VPC service-linked role verified (created by another process)")
137
+ else:
138
+ logger.error("✗ Failed to create service-linked role: %s", e)
139
+ raise
140
+
141
+
28
142
  def _ensure_ecr_repository(agent_config, project_config, config_path, agent_name, region):
29
143
  """Ensure ECR repository exists (idempotent)."""
30
144
  ecr_uri = agent_config.aws.ecr_repository
@@ -136,6 +250,7 @@ def _ensure_memory_for_agent(
136
250
  project_config: BedrockAgentCoreConfigSchema,
137
251
  config_path: Path,
138
252
  agent_name: str,
253
+ console: Optional[Console] = None,
139
254
  ) -> Optional[str]:
140
255
  """Ensure memory resource exists for agent. Returns memory_id or None.
141
256
 
@@ -161,7 +276,10 @@ def _ensure_memory_for_agent(
161
276
  from ...operations.memory.constants import StrategyType
162
277
  from ...operations.memory.manager import MemoryManager
163
278
 
164
- memory_manager = MemoryManager(region_name=agent_config.aws.region)
279
+ memory_manager = MemoryManager(
280
+ region_name=agent_config.aws.region,
281
+ console=console, # ADD THIS
282
+ )
165
283
  memory_name = f"{agent_name}_mem" # Short name under 48 char limit
166
284
 
167
285
  # Check if memory already exists in cloud
@@ -190,6 +308,7 @@ def _ensure_memory_for_agent(
190
308
  # If LTM is enabled but no strategies exist, add them
191
309
  if agent_config.memory.has_ltm and len(existing_strategies) == 0:
192
310
  log.info("Adding LTM strategies to existing memory...")
311
+ console.print("⏳ Adding long-term memory strategies (this may take 30-180 seconds)...")
193
312
  memory_manager.update_memory_strategies_and_wait(
194
313
  memory_id=existing_memory.id,
195
314
  add_strategies=[
@@ -212,13 +331,21 @@ def _ensure_memory_for_agent(
212
331
  }
213
332
  },
214
333
  ],
215
- max_wait=30,
334
+ max_wait=300, # CHANGE: Increased from 30 to 300
216
335
  poll_interval=5,
217
336
  )
218
337
  memory = existing_memory
219
338
  log.info("✅ LTM strategies added to existing memory")
220
339
  else:
221
- memory = existing_memory
340
+ # CHANGE: ADD THIS BLOCK - Wait for existing memory to become ACTIVE
341
+ console.print("⏳ Waiting for existing memory to become ACTIVE...")
342
+ memory = memory_manager._wait_for_memory_active(
343
+ existing_memory.id,
344
+ max_wait=300,
345
+ poll_interval=5,
346
+ )
347
+ # END CHANGE
348
+
222
349
  if agent_config.memory.has_ltm and len(existing_strategies) > 0:
223
350
  log.info("✅ Using existing memory with %d strategies", len(existing_strategies))
224
351
  else:
@@ -251,28 +378,29 @@ def _ensure_memory_for_agent(
251
378
  else:
252
379
  log.info("Creating new STM-only memory...")
253
380
 
254
- # Use private method to avoid waiting
255
- memory = memory_manager._create_memory(
381
+ # CHANGE: Use create_memory_and_wait instead of _create_memory
382
+ console.print("⏳ Creating memory resource (this may take 30-180 seconds)...")
383
+ memory = memory_manager.create_memory_and_wait(
256
384
  name=memory_name,
257
385
  description=f"Memory for agent {agent_name} with {'STM+LTM' if strategies else 'STM only'}",
258
386
  strategies=strategies,
259
387
  event_expiry_days=agent_config.memory.event_expiry_days or 30,
260
- memory_execution_role_arn=None,
388
+ max_wait=300, # 5 minutes
389
+ poll_interval=5,
261
390
  )
391
+ log.info("✅ Memory created and active: %s", memory.id)
392
+ # END CHANGE
262
393
 
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)
394
+ # CHANGE: ADD THIS - Mark as created by toolkit since we just created it
265
395
  if not agent_config.memory.was_created_by_toolkit:
266
- log.warning("Memory created but flag was False - correcting to True")
267
396
  agent_config.memory.was_created_by_toolkit = True
268
-
269
- log.info("✅ New memory created: %s (provisioning in background)", memory.id)
397
+ # END CHANGE
270
398
 
271
399
  # Save memory configuration (preserving was_created_by_toolkit flag)
272
400
  agent_config.memory.memory_id = memory.id
273
401
  agent_config.memory.memory_arn = memory.arn
274
402
  agent_config.memory.memory_name = memory_name
275
- agent_config.memory.first_invoke_memory_check_done = False
403
+ agent_config.memory.first_invoke_memory_check_done = True # CHANGE: Set to True since memory is now ACTIVE
276
404
 
277
405
  project_config.agents[agent_config.name] = agent_config
278
406
  save_config(project_config, config_path)
@@ -303,8 +431,8 @@ def _deploy_to_bedrock_agentcore(
303
431
  if env_vars is None:
304
432
  env_vars = {}
305
433
 
306
- # Add memory configuration to env_vars if it exists
307
- if agent_config.memory and agent_config.memory.memory_id:
434
+ # Add memory configuration to env_vars only if memory is enabled
435
+ if agent_config.memory and agent_config.memory.mode != "NO_MEMORY" and agent_config.memory.memory_id:
308
436
  env_vars["BEDROCK_AGENTCORE_MEMORY_ID"] = agent_config.memory.memory_id
309
437
  env_vars["BEDROCK_AGENTCORE_MEMORY_NAME"] = agent_config.memory.memory_name
310
438
  log.info("Passing memory configuration to agent: %s", agent_config.memory.memory_id)
@@ -315,6 +443,15 @@ def _deploy_to_bedrock_agentcore(
315
443
  network_config = agent_config.aws.network_configuration.to_aws_dict()
316
444
  protocol_config = agent_config.aws.protocol_configuration.to_aws_dict()
317
445
 
446
+ lifecycle_config = None
447
+ if agent_config.aws.lifecycle_configuration.has_custom_settings:
448
+ lifecycle_config = agent_config.aws.lifecycle_configuration.to_aws_dict()
449
+ log.info(
450
+ "Applying custom lifecycle settings: idle=%s, max=%s",
451
+ agent_config.aws.lifecycle_configuration.idle_runtime_session_timeout,
452
+ agent_config.aws.lifecycle_configuration.max_lifetime,
453
+ )
454
+
318
455
  # Execution role should be available by now (either provided or auto-created)
319
456
  if not agent_config.aws.execution_role:
320
457
  raise ValueError(
@@ -340,6 +477,7 @@ def _deploy_to_bedrock_agentcore(
340
477
  protocol_config=protocol_config,
341
478
  env_vars=env_vars,
342
479
  auto_update_on_conflict=auto_update_on_conflict,
480
+ lifecycle_config=lifecycle_config,
343
481
  )
344
482
  break # Success! Exit retry loop
345
483
 
@@ -414,9 +552,45 @@ def _deploy_to_bedrock_agentcore(
414
552
  result = bedrock_agentcore_client.wait_for_agent_endpoint_ready(agent_id)
415
553
  log.info("Agent endpoint: %s", result)
416
554
 
555
+ if agent_config.aws.network_configuration.network_mode == "VPC":
556
+ vpc_subnets = agent_config.aws.network_configuration.network_mode_config.subnets
557
+ session = boto3.Session(region_name=region)
558
+ _check_vpc_deployment(session, agent_id, vpc_subnets, region)
559
+
417
560
  return agent_id, agent_arn
418
561
 
419
562
 
563
+ def _check_vpc_deployment(session: boto3.Session, agent_id: str, vpc_subnets: List[str], region: str) -> None:
564
+ """Verify VPC deployment created ENIs in the specified subnets."""
565
+ ec2_client = session.client("ec2", region_name=region)
566
+
567
+ try:
568
+ # Look for ENIs in our subnets with AgentCore description
569
+ response = ec2_client.describe_network_interfaces(
570
+ Filters=[
571
+ {"Name": "subnet-id", "Values": vpc_subnets},
572
+ {"Name": "description", "Values": ["*AgentCore*", "*bedrock-agentcore*"]},
573
+ ]
574
+ )
575
+
576
+ all_enis = response.get("NetworkInterfaces", [])
577
+ our_enis = [eni for eni in all_enis if eni.get("SubnetId") in vpc_subnets]
578
+
579
+ if our_enis:
580
+ log.info("✓ Found %d ENI(s) in configured subnets:", len(our_enis))
581
+ for eni in our_enis:
582
+ log.info(" - ENI ID: %s", eni["NetworkInterfaceId"])
583
+ log.info(" Subnet: %s", eni["SubnetId"])
584
+ log.info(" Private IP: %s", eni.get("PrivateIpAddress", "N/A"))
585
+ log.info(" Status: %s", eni["Status"])
586
+ log.info(" Security Groups: %s", [sg["GroupId"] for sg in eni.get("Groups", [])])
587
+ else:
588
+ log.info(":information_source: VPC network interfaces will be created on first invocation")
589
+
590
+ except Exception as e:
591
+ log.error("Error checking ENIs: %s", e)
592
+
593
+
420
594
  def launch_bedrock_agentcore(
421
595
  config_path: Path,
422
596
  agent_name: Optional[str] = None,
@@ -424,6 +598,7 @@ def launch_bedrock_agentcore(
424
598
  use_codebuild: bool = True,
425
599
  env_vars: Optional[dict] = None,
426
600
  auto_update_on_conflict: bool = False,
601
+ console: Optional[Console] = None,
427
602
  ) -> LaunchResult:
428
603
  """Launch Bedrock AgentCore locally or to cloud.
429
604
 
@@ -434,10 +609,14 @@ def launch_bedrock_agentcore(
434
609
  use_codebuild: Whether to use CodeBuild for ARM64 builds
435
610
  env_vars: Environment variables to pass to local container (dict of key-value pairs)
436
611
  auto_update_on_conflict: Whether to automatically update when agent already exists (default: False)
612
+ console: Optional Rich Console instance for progress output. Used to maintain
613
+ output hierarchy with CLI status contexts.
437
614
 
438
615
  Returns:
439
616
  LaunchResult model with launch details
440
617
  """
618
+ if console is None:
619
+ console = Console()
441
620
  # Load project configuration
442
621
  project_config = load_config(config_path)
443
622
  agent_config = project_config.get_agent_config(agent_name)
@@ -445,10 +624,25 @@ def launch_bedrock_agentcore(
445
624
  if env_vars is None:
446
625
  env_vars = {}
447
626
 
627
+ if agent_config.aws.network_configuration.network_mode == "VPC":
628
+ if local:
629
+ log.warning("⚠️ VPC configuration detected but running in local mode. VPC settings will be ignored.")
630
+ else:
631
+ log.info("Validating VPC resources...")
632
+ session = boto3.Session(region_name=agent_config.aws.region)
633
+ _validate_vpc_resources(session, agent_config, agent_config.aws.region)
634
+
635
+ # Ensure service-linked role exists for VPC networking
636
+ _ensure_network_service_linked_role(session, log)
637
+
448
638
  # Ensure memory exists for non-CodeBuild paths
449
639
  if not use_codebuild:
450
640
  _ensure_memory_for_agent(agent_config, project_config, config_path, agent_config.name)
451
641
 
642
+ # Ensure memory exists for non-CodeBuild paths
643
+ if not use_codebuild:
644
+ _ensure_memory_for_agent(agent_config, project_config, config_path, agent_config.name, console=console)
645
+
452
646
  # Add memory configuration to environment variables if available
453
647
  if agent_config.memory and agent_config.memory.memory_id:
454
648
  env_vars["BEDROCK_AGENTCORE_MEMORY_ID"] = agent_config.memory.memory_id
@@ -705,10 +899,13 @@ def _launch_with_codebuild(
705
899
  project_config,
706
900
  auto_update_on_conflict: bool = False,
707
901
  env_vars: Optional[dict] = None,
902
+ console: Optional[Console] = None,
708
903
  ) -> LaunchResult:
709
904
  """Launch using CodeBuild for ARM64 builds."""
905
+ if console is None:
906
+ console = Console()
710
907
  # Create memory if configured
711
- _ensure_memory_for_agent(agent_config, project_config, config_path, agent_name)
908
+ _ensure_memory_for_agent(agent_config, project_config, config_path, agent_name, console=console)
712
909
 
713
910
  # Execute shared CodeBuild workflow with full deployment mode
714
911
  build_id, ecr_uri, region, account_id = _execute_codebuild_workflow(