Skip to content

Commit d58b61c

Browse files
sundargthbSundar Raghavan
andauthored
feat: Add automatic memory provisioning to Bedrock AgentCore CLI (#204)
* feat: Add automatic memory provisioning to Bedrock AgentCore CLI - Integrate AgentCore Memory Manager for STM and LTM configuration - Add memory type selection prompt during agent configuration - Implement automatic memory resource creation with appropriate strategies - Support both short-term memory (30-day retention) and long-term memory extraction - Pass memory configuration to container runtime via environment variables - Handle idempotent memory operations for existing resources * fix: resolve conflict * fix: Optimize memory provisioning and add status display Remove blocking wait for LTM strategy provisioning during launch Use batch strategy updates to reduce provisioning time from ~3 minutes to seconds Add memory provisioning status to agentcore status command Fix status panel formatting with proper newline characters Use async memory creation for faster agent deployment * fix: Optimize memory provisioning and add status display Remove blocking wait for LTM strategy provisioning during launch Use batch strategy updates to reduce provisioning time from ~3 minutes to seconds Add memory provisioning status to agentcore status command Fix status panel formatting with proper newline characters Use async memory creation for faster agent deployment * complete unit test files * improve test coverage * improve test coverage * resolve conflicts * resolve conflicts * docs: add quickstart example * update execution role policy for code interpreter * feat: Add graceful memory provisioning check on first invoke. Check LTM memory status on first invoke. * feat: address memory configuration feedback and improve UX Addresses PR review comments: - Replace boolean flags (enabled/enable_ltm) with single Literal enum type (STM_ONLY, STM_AND_LTM, NO_MEMORY) for clearer memory mode configuration - Add ListMemories integration in configure flow allowing users to select existing memory resources or create new ones with guided prompts - Enhance agentcore status output to display comprehensive memory resource details including all fields, strategies, namespaces, and provisioning status * update unit test files * fix formatting errors * update quickstart example * add non-interactive for memory * fix lint error * complete PR feedback updates * complete PR feedback updates --------- Signed-off-by: Sundar Raghavan <[email protected]> Co-authored-by: Sundar Raghavan <[email protected]>
1 parent 6721d12 commit d58b61c

File tree

22 files changed

+3906
-89
lines changed

22 files changed

+3906
-89
lines changed

documentation/docs/examples/agentcore-quickstart-example.md

Lines changed: 643 additions & 0 deletions
Large diffs are not rendered by default.

src/bedrock_agentcore_starter_toolkit/cli/runtime/commands.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ def configure(
280280
verbose=verbose,
281281
region=region,
282282
protocol=protocol.upper() if protocol else None,
283+
non_interactive=non_interactive,
283284
)
284285

285286
# Prepare authorization info for summary
@@ -306,8 +307,9 @@ def configure(
306307
f"ECR Repository: [dim]"
307308
f"{'Auto-create' if result.auto_create_ecr else result.ecr_repository or 'N/A'}"
308309
f"[/dim]\n"
309-
f"Authorization: [dim]{auth_info}[/dim]\n"
310+
f"Authorization: [dim]{auth_info}[/dim]\n\n"
310311
f"{headers_info}\n"
312+
f"Memory: [dim]Short-term memory (30-day retention)[/dim]\n\n"
311313
f"📄 Config saved to: [dim]{result.config_path}[/dim]\n\n"
312314
f"[bold]Next Steps:[/bold]\n"
313315
f" [cyan]agentcore launch[/cyan]",
@@ -328,7 +330,7 @@ def launch(
328330
None, "--agent", "-a", help="Agent name (use 'agentcore configure list' to see available agents)"
329331
),
330332
local: bool = typer.Option(
331-
False, "--local", "-l", help="Build locally and run container locally - requires Docker/Finch/Podman"
333+
False, "--local", "-l", help="Local build + local runtime - requires Docker/Finch/Podman"
332334
),
333335
local_build: bool = typer.Option(
334336
False,
@@ -852,6 +854,11 @@ def status(
852854

853855
# Determine overall status
854856
endpoint_status = endpoint_data.get("status", "Unknown") if endpoint_data else "Not Ready"
857+
# memory_info = ""
858+
# if hasattr(status_json["config"], "memory_id") and status_json["config"].get("memory_id"):
859+
# memory_type = status_json["config"].get("memory_type", "Short-term")
860+
# memory_id = status_json["config"].get("memory_id")
861+
# memory_info = f"Memory: [cyan]{memory_type}[/cyan] ([dim]{memory_id}[/dim])\n"
855862
if endpoint_status == "READY":
856863
status_text = "Ready - Agent deployed and endpoint available"
857864
else:
@@ -867,6 +874,30 @@ def status(
867874
f"([cyan]{endpoint_status}[/cyan])\n"
868875
f"Region: [cyan]{status_json['config']['region']}[/cyan] | "
869876
f"Account: [dim]{status_json['config'].get('account', 'Not available')}[/dim]\n\n"
877+
)
878+
879+
# Add memory status with proper provisioning indication
880+
if "memory_id" in status_json.get("config", {}) and status_json["config"]["memory_id"]:
881+
memory_type = status_json["config"].get("memory_type", "Unknown")
882+
memory_id = status_json["config"]["memory_id"]
883+
memory_status = status_json["config"].get("memory_status", "Unknown")
884+
885+
# Color-code based on status
886+
if memory_status == "ACTIVE":
887+
panel_content += f"Memory: [green]{memory_type}[/green] ([dim]{memory_id}[/dim])\n"
888+
elif memory_status in ["CREATING", "UPDATING"]:
889+
panel_content += f"Memory: [yellow]{memory_type}[/yellow] ([dim]{memory_id}[/dim])\n"
890+
panel_content += (
891+
" [yellow]⚠️ Memory is provisioning. "
892+
"STM will be available once ACTIVE.[/yellow]\n"
893+
)
894+
else:
895+
panel_content += f"Memory: [red]{memory_type}[/red] ([dim]{memory_id}[/dim])\n"
896+
897+
panel_content += "\n"
898+
899+
# Continue building the panel
900+
panel_content += (
870901
f"[bold]Deployment Info:[/bold]\n"
871902
f"Created: [dim]{agent_data.get('createdAt', 'Not available')}[/dim]\n"
872903
f"Last Updated: [dim]"

src/bedrock_agentcore_starter_toolkit/cli/runtime/configuration_manager.py

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os
44
from pathlib import Path
5-
from typing import Dict, Optional
5+
from typing import Dict, Optional, Tuple
66

77
from ..common import _handle_error, _print_success, _prompt_with_default, console
88

@@ -198,3 +198,129 @@ def _configure_request_header_allowlist(self, existing_headers: str = "") -> dic
198198
_print_success(f"Request header allowlist configured with {len(headers)} headers")
199199

200200
return {"requestHeaderAllowlist": headers}
201+
202+
def prompt_memory_type(self) -> tuple[bool, bool]:
203+
"""Prompt user for memory configuration preference.
204+
205+
Returns:
206+
Tuple of (enable_memory, enable_ltm)
207+
"""
208+
console.print("\n🧠 [cyan]Memory Configuration[/cyan]")
209+
console.print("Short-term memory stores conversation within sessions.")
210+
console.print("Long-term memory extracts preferences and facts across sessions.")
211+
console.print()
212+
213+
# First ask if they want memory at all
214+
enable_memory_response = _prompt_with_default("Enable memory for your agent? (yes/no)", "yes").strip().lower()
215+
216+
enable_memory = enable_memory_response in ["yes", "y"]
217+
218+
if not enable_memory:
219+
_print_success("Memory disabled")
220+
return False, False
221+
222+
# If memory is enabled, ask about long-term memory
223+
console.print("\n[dim]Long-term memory extracts:[/dim]")
224+
console.print(" • User preferences (e.g., 'I prefer Python')")
225+
console.print(" • Semantic facts (e.g., 'My birthday is in January')")
226+
console.print(" • Session summaries")
227+
console.print()
228+
229+
enable_ltm_response = _prompt_with_default("Enable long-term memory extraction? (yes/no)", "no").strip().lower()
230+
231+
enable_ltm = enable_ltm_response in ["yes", "y"]
232+
233+
if enable_ltm:
234+
_print_success("Long-term memory will be configured")
235+
else:
236+
_print_success("Using short-term memory only")
237+
238+
return enable_memory, enable_ltm
239+
240+
def prompt_memory_selection(self) -> Tuple[str, str]:
241+
"""Prompt user to select existing memory or create new.
242+
243+
Returns:
244+
Tuple of (action, value) where:
245+
- action is "USE_EXISTING", "CREATE_NEW"
246+
- value is memory_id for USE_EXISTING, mode for CREATE_NEW
247+
"""
248+
if self.non_interactive:
249+
# In non-interactive mode, default to creating new STM
250+
return ("CREATE_NEW", "STM_ONLY")
251+
252+
console.print("\n🧠 [cyan]Memory Configuration[/cyan]")
253+
254+
# Try to list existing memories
255+
try:
256+
from ...operations.memory.manager import MemoryManager
257+
258+
# Need region - will be passed from configure.py
259+
region = self.existing_config.aws.region if self.existing_config else None
260+
if not region:
261+
# Fall back to new memory creation if no region
262+
return self._prompt_new_memory_config()
263+
264+
memory_manager = MemoryManager(region_name=region)
265+
existing_memories = memory_manager.list_memories(max_results=10)
266+
267+
if existing_memories:
268+
console.print("\n[cyan]Existing memory resources found:[/cyan]")
269+
for i, mem in enumerate(existing_memories, 1):
270+
# Display memory summary
271+
mem_id = mem.get("id", "unknown")
272+
mem_name = mem.get("name", "")
273+
if "memory-" in mem_id:
274+
display_name = mem_id.split("memory-")[0] + "memory"
275+
else:
276+
display_name = mem_name or mem_id[:40]
277+
278+
console.print(f" {i}. [bold]{display_name}[/bold]")
279+
if mem.get("description"):
280+
console.print(f" [dim]{mem.get('description')}[/dim]")
281+
console.print(f" [dim]ID: {mem_id}[/dim]")
282+
283+
console.print("\n[dim]Options:[/dim]")
284+
console.print("[dim] • Enter a number to use existing memory[/dim]")
285+
console.print("[dim] • Press Enter to create new memory[/dim]")
286+
console.print("[dim] • Type 's' to skip memory setup[/dim]")
287+
288+
response = _prompt_with_default("Your choice", "").strip().lower()
289+
290+
if response.isdigit():
291+
idx = int(response) - 1
292+
if 0 <= idx < len(existing_memories):
293+
selected = existing_memories[idx]
294+
_print_success(f"Using existing memory: {selected.get('name', selected.get('id'))}")
295+
return ("USE_EXISTING", selected.get("id"))
296+
elif response == "s":
297+
_print_success("Skipping memory configuration")
298+
return ("SKIP", None)
299+
except Exception as e:
300+
console.print(f"[dim]Could not list existing memories: {e}[/dim]")
301+
302+
# Fall back to creating new memory
303+
return self._prompt_new_memory_config()
304+
305+
def _prompt_new_memory_config(self) -> Tuple[str, str]:
306+
"""Prompt for new memory configuration."""
307+
console.print("\n🧠 [cyan]Memory Configuration[/cyan]")
308+
console.print("[green]✓ Short-term memory is enabled by default[/green]")
309+
console.print(" • Stores conversations within sessions")
310+
console.print(" • Provides immediate context recall")
311+
console.print()
312+
console.print("[cyan]Optional: Long-term memory[/cyan]")
313+
console.print(" • Extracts user preferences across sessions")
314+
console.print(" • Remembers facts and patterns")
315+
console.print(" • Creates session summaries")
316+
console.print(" • [dim]Note: Takes 60-90 seconds to process[/dim]")
317+
console.print()
318+
319+
response = _prompt_with_default("Enable long-term memory extraction? (yes/no)", "no").strip().lower()
320+
321+
if response in ["yes", "y"]:
322+
_print_success("Configuring short-term + long-term memory")
323+
return ("CREATE_NEW", "STM_AND_LTM")
324+
else:
325+
_print_success("Using short-term memory only")
326+
return ("CREATE_NEW", "STM_ONLY")

src/bedrock_agentcore_starter_toolkit/operations/runtime/configure.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from typing import Any, Dict, Optional, Tuple
77

8+
from ...cli.runtime.configuration_manager import ConfigurationManager
89
from ...services.ecr import get_account_id, get_region
910
from ...utils.runtime.config import merge_agent_config, save_config
1011
from ...utils.runtime.container import ContainerRuntime
@@ -13,6 +14,7 @@
1314
BedrockAgentCoreAgentSchema,
1415
BedrockAgentCoreDeploymentInfo,
1516
CodeBuildConfig,
17+
MemoryConfig,
1618
NetworkConfiguration,
1719
ObservabilityConfig,
1820
ProtocolConfiguration,
@@ -38,6 +40,7 @@ def configure_bedrock_agentcore(
3840
verbose: bool = False,
3941
region: Optional[str] = None,
4042
protocol: Optional[str] = None,
43+
non_interactive: bool = False,
4144
) -> ConfigureResult:
4245
"""Configure Bedrock AgentCore application with deployment settings.
4346
@@ -57,6 +60,7 @@ def configure_bedrock_agentcore(
5760
verbose: Whether to provide verbose output during configuration
5861
region: AWS region for deployment
5962
protocol: agent server protocol, must be either HTTP or MCP
63+
non_interactive: Skip interactive prompts and use defaults
6064
6165
Returns:
6266
ConfigureResult model with configuration details
@@ -112,6 +116,51 @@ def configure_bedrock_agentcore(
112116
else:
113117
log.debug("No execution role provided and auto-create disabled")
114118

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+
115164
# Handle CodeBuild execution role - use separate role if provided, otherwise use execution_role
116165
codebuild_execution_role_arn = None
117166
if code_build_execution_role:
@@ -144,6 +193,8 @@ def configure_bedrock_agentcore(
144193
log.debug(" Region: %s", region)
145194
log.debug(" Enable observability: %s", enable_observability)
146195
log.debug(" Requirements file: %s", requirements_file)
196+
if memory_id:
197+
log.debug(" Memory ID: %s", memory_id)
147198

148199
dockerfile_path = runtime.generate_dockerfile(
149200
entrypoint_path,
@@ -152,6 +203,8 @@ def configure_bedrock_agentcore(
152203
region,
153204
enable_observability,
154205
requirements_file,
206+
memory_id,
207+
memory_name,
155208
)
156209

157210
# Check if .dockerignore was created
@@ -221,6 +274,7 @@ def configure_bedrock_agentcore(
221274
),
222275
authorizer_configuration=authorizer_configuration,
223276
request_header_configuration=request_header_configuration,
277+
memory=memory_config,
224278
)
225279

226280
# Use simplified config merging

src/bedrock_agentcore_starter_toolkit/operations/runtime/destroy.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import boto3
88
from botocore.exceptions import ClientError
99

10+
from ...operations.memory.manager import MemoryManager
1011
from ...services.runtime import BedrockAgentCoreClient
1112
from ...utils.runtime.config import load_config, save_config
1213
from ...utils.runtime.schema import BedrockAgentCoreAgentSchema, BedrockAgentCoreConfigSchema
@@ -77,13 +78,16 @@ def destroy_bedrock_agentcore(
7778
# 4. Remove CodeBuild project
7879
_destroy_codebuild_project(session, agent_config, result, dry_run)
7980

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
8185
_destroy_codebuild_iam_role(session, agent_config, result, dry_run)
8286

83-
# 6. Remove IAM execution role (if not used by other agents)
87+
# 7. Remove IAM execution role (if not used by other agents)
8488
_destroy_iam_role(session, project_config, agent_config, result, dry_run)
8589

86-
# 7. Clean up configuration
90+
# 8. Clean up configuration
8791
if not dry_run and not result.errors:
8892
_cleanup_agent_config(config_path, project_config, agent_config.name, result)
8993

@@ -386,6 +390,42 @@ def _destroy_codebuild_project(
386390
log.warning("Error during CodeBuild cleanup: %s", e)
387391

388392

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+
389429
def _destroy_codebuild_iam_role(
390430
session: boto3.Session,
391431
agent_config: BedrockAgentCoreAgentSchema,

0 commit comments

Comments
 (0)