diff --git a/.kiro/specs/agentcore-memory/.config.kiro b/.kiro/specs/agentcore-memory/.config.kiro
new file mode 100644
index 0000000..daf3917
--- /dev/null
+++ b/.kiro/specs/agentcore-memory/.config.kiro
@@ -0,0 +1 @@
+{"specId": "9f04c463-64ab-4cc2-8d4c-11793d775325", "workflowType": "requirements-first", "specType": "feature"}
diff --git a/.kiro/specs/agentcore-memory/design.md b/.kiro/specs/agentcore-memory/design.md
new file mode 100644
index 0000000..2080b78
--- /dev/null
+++ b/.kiro/specs/agentcore-memory/design.md
@@ -0,0 +1,633 @@
+# Design Document: AgentCore Memory Integration
+
+## Overview
+
+This design integrates the Amazon Bedrock AgentCore Memory service into AWS.AgentCore as a `ChatHistoryProvider` within the Microsoft Agent Framework pipeline. The provider automatically loads conversation history before each agent run and saves new messages after, giving agents persistent multi-turn memory that survives container restarts and scaling events.
+
+### Goals
+
+1. Implement `AgentCoreMemoryProvider` as a `ChatHistoryProvider` that bridges AgentCore Memory APIs (ListEvents/CreateEvent) into the MS AF pipeline
+2. Automatically register the provider via `AddAgentCore()` with zero additional user code
+3. Gracefully degrade to pass-through mode when MemoryId is not configured
+4. Filter tool-call/tool-result messages and empty-text messages from persistence
+5. Handle pagination for long conversation histories
+6. Maintain NativeAOT compatibility with source-generated JSON
+7. Ensure concurrent request isolation via per-request session state (no shared mutable state)
+
+### Non-Goals
+
+- Conversation windowing or summarization (separate future feature)
+- Long-term memory / semantic search (AgentCore Memory Records, separate feature)
+- Custom branching strategies (use default branch)
+- Exposing the Memory client directly to users (internal implementation detail)
+
+## Architecture
+
+```mermaid
+graph TD
+ subgraph "Request Pipeline"
+ A[POST /invocations] --> B[Extract Headers → AgentCoreRuntimeContext]
+ B --> C[Handler creates AgentSession]
+ C --> D[Store RuntimeContext in StateBag]
+ D --> E[agent.RunAsync]
+ end
+
+ subgraph "MS AF Agent Pipeline"
+ E --> F[AgentCoreRuntimeContextProvider]
+ F --> G[AgentCoreMemoryProvider.InvokingCoreAsync]
+ G --> H["ProvideChatHistoryAsync (load)"]
+ H --> I[LLM Invocation]
+ I --> J[AgentCoreMemoryProvider.InvokedCoreAsync]
+ J --> K["StoreChatHistoryAsync (save)"]
+ end
+
+ subgraph "AgentCore Memory Service"
+ H -->|ListEvents paginated| L[Memory API]
+ K -->|CreateEvent x2| L
+ end
+```
+
+### Data Flow: Load History
+
+```mermaid
+sequenceDiagram
+ participant Pipeline as MS AF Pipeline
+ participant Provider as AgentCoreMemoryProvider
+ participant Memory as AgentCore Memory API
+
+ Pipeline->>Provider: ProvideChatHistoryAsync(context)
+ Provider->>Provider: Get SessionId from StateBag
+ alt MemoryId not configured OR SessionId missing
+ Provider-->>Pipeline: return empty []
+ else MemoryId configured
+ loop Until no NextToken
+ Provider->>Memory: ListEvents(memoryId, actorId, sessionId, includePayloads=true)
+ Memory-->>Provider: events[] + nextToken?
+ end
+ Provider->>Provider: Convert events to ChatMessages (filter non-text)
+ Provider-->>Pipeline: return ChatMessage[]
+ end
+```
+
+### Data Flow: Save Messages
+
+```mermaid
+sequenceDiagram
+ participant Pipeline as MS AF Pipeline
+ participant Provider as AgentCoreMemoryProvider
+ participant Memory as AgentCore Memory API
+
+ Pipeline->>Provider: StoreChatHistoryAsync(context)
+ Provider->>Provider: Get SessionId from StateBag
+ alt MemoryId not configured OR SessionId missing
+ Provider-->>Pipeline: return (no-op)
+ else MemoryId configured
+ Provider->>Provider: Filter messages (skip tool-call, tool-result, empty text)
+ loop For each valid message
+ Provider->>Memory: CreateEvent(memoryId, sessionId, actorId, payload)
+ Memory-->>Provider: event created
+ end
+ Provider-->>Pipeline: return
+ end
+```
+
+## Components and Interfaces
+
+### New Classes
+
+#### `AgentCoreMemoryProvider` (public)
+
+The core component — a `ChatHistoryProvider` that bridges AgentCore Memory into the MS AF pipeline.
+
+```csharp
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Amazon.BedrockAgentCore;
+using Amazon.BedrockAgentCore.Model;
+
+namespace AWS.AgentCore;
+
+///
+/// A that persists conversation history to
+/// Amazon Bedrock AgentCore Memory. Loads history before each agent run via ListEvents
+/// and saves new messages after via CreateEvent.
+///
+/// Registered automatically by .
+/// Operates in pass-through mode (no-op) when MemoryId is not configured.
+///
+///
+public sealed class AgentCoreMemoryProvider : ChatHistoryProvider
+{
+ private readonly IAmazonBedrockAgentCore? _memoryClient;
+ private readonly AgentCoreOptions _options;
+ private readonly ILogger _logger;
+
+ public AgentCoreMemoryProvider(
+ AgentCoreOptions options,
+ ILogger logger,
+ IAmazonBedrockAgentCore? memoryClient = null)
+ : base(null, null)
+ {
+ _options = options;
+ _logger = logger;
+ _memoryClient = memoryClient;
+ }
+
+ public override string StateKey => "AgentCore.Memory";
+
+ ///
+ /// Resolves the effective MemoryId from options or environment variable.
+ /// Options takes precedence over environment variable.
+ ///
+ private string? GetEffectiveMemoryId()
+ {
+ if (!string.IsNullOrWhiteSpace(_options.MemoryId))
+ return _options.MemoryId;
+
+ var envValue = Environment.GetEnvironmentVariable("MEMORY_ID");
+ return string.IsNullOrWhiteSpace(envValue) ? null : envValue;
+ }
+
+ protected override async ValueTask> ProvideChatHistoryAsync(
+ InvokingContext context,
+ CancellationToken cancellationToken = default)
+ {
+ var memoryId = GetEffectiveMemoryId();
+ if (memoryId is null)
+ return [];
+
+ if (_memoryClient is null)
+ {
+ _logger.LogError("MemoryId is configured but IAmazonBedrockAgentCore is not registered in DI. Memory operations will be skipped.");
+ return [];
+ }
+
+ var sessionId = GetSessionId(context.Session);
+ if (sessionId is null)
+ {
+ _logger.LogWarning("SessionId not available in session StateBag. Skipping memory load.");
+ return [];
+ }
+
+ try
+ {
+ return await LoadHistoryAsync(memoryId, sessionId, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to load conversation history from AgentCore Memory. Proceeding without history.");
+ return [];
+ }
+ }
+
+ protected override async ValueTask StoreChatHistoryAsync(
+ InvokedContext context,
+ CancellationToken cancellationToken = default)
+ {
+ var memoryId = GetEffectiveMemoryId();
+ if (memoryId is null)
+ return;
+
+ if (_memoryClient is null)
+ return;
+
+ var sessionId = GetSessionId(context.Session);
+ if (sessionId is null)
+ return;
+
+ var messagesToSave = FilterMessagesForStorage(
+ context.RequestMessages, context.ResponseMessages);
+
+ foreach (var (role, text) in messagesToSave)
+ {
+ try
+ {
+ await SaveEventAsync(memoryId, sessionId, role, text, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to save message to AgentCore Memory. Continuing.");
+ }
+ }
+ }
+
+ // ... private helper methods (see detailed design below)
+}
+```
+
+#### Key Private Methods
+
+```csharp
+private string? GetSessionId(AgentSession session)
+{
+ // Retrieve AgentCoreRuntimeContext from the session StateBag
+ if (session.TryGetProperty(AgentCoreRuntimeContextProvider.ContextKey, out var contextObj)
+ && contextObj is AgentCoreRuntimeContext runtimeContext)
+ {
+ return runtimeContext.SessionId;
+ }
+ return null;
+}
+
+private async Task> LoadHistoryAsync(
+ string memoryId, string sessionId, CancellationToken cancellationToken)
+{
+ var messages = new List();
+ string? nextToken = null;
+
+ do
+ {
+ ListEventsResponse response;
+ try
+ {
+ response = await _memoryClient!.ListEventsAsync(new ListEventsRequest
+ {
+ MemoryId = memoryId,
+ ActorId = sessionId,
+ SessionId = sessionId,
+ IncludePayloads = true,
+ NextToken = nextToken
+ }, cancellationToken);
+ }
+ catch (Exception ex) when (nextToken is not null)
+ {
+ // Partial pagination failure — return what we have
+ _logger.LogWarning(ex, "Error fetching page during pagination. Returning {Count} messages loaded so far.", messages.Count);
+ break;
+ }
+
+ foreach (var evt in response.Events ?? [])
+ {
+ if (TryConvertEventToChatMessage(evt, out var chatMessage))
+ {
+ messages.Add(chatMessage);
+ }
+ }
+
+ nextToken = response.NextToken;
+ }
+ while (!string.IsNullOrEmpty(nextToken));
+
+ return messages;
+}
+
+private static bool TryConvertEventToChatMessage(Event evt, out ChatMessage message)
+{
+ message = default!;
+
+ if (evt.Payload is null || evt.Payload.Count == 0)
+ return false;
+
+ // Find the first conversational payload with text content
+ foreach (var payload in evt.Payload)
+ {
+ if (payload.Conversational is { } conversational
+ && conversational.Content?.Text is { Length: > 0 } text)
+ {
+ var role = conversational.Role switch
+ {
+ ConversationRole.USER => ChatRole.User,
+ ConversationRole.ASSISTANT => ChatRole.Assistant,
+ _ => (ChatRole?)null
+ };
+
+ if (role is not null)
+ {
+ message = new ChatMessage(role.Value, text);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+private async Task SaveEventAsync(
+ string memoryId, string sessionId, ConversationRole role, string text,
+ CancellationToken cancellationToken)
+{
+ await _memoryClient!.CreateEventAsync(new CreateEventRequest
+ {
+ MemoryId = memoryId,
+ SessionId = sessionId,
+ ActorId = sessionId,
+ EventTimestamp = DateTime.UtcNow,
+ Payload = new List
+ {
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = role,
+ Content = new Content { Text = text }
+ }
+ }
+ }
+ }, cancellationToken);
+}
+
+private static IEnumerable<(ConversationRole Role, string Text)> FilterMessagesForStorage(
+ IEnumerable? requestMessages,
+ IEnumerable? responseMessages)
+{
+ var allMessages = (requestMessages ?? []).Concat(responseMessages ?? []);
+
+ foreach (var message in allMessages)
+ {
+ // Skip messages with tool-call or tool-result content
+ if (HasToolContent(message))
+ continue;
+
+ // Extract text content
+ var text = message.Text;
+ if (string.IsNullOrWhiteSpace(text))
+ continue;
+
+ // Map role
+ var role = message.Role == ChatRole.User
+ ? ConversationRole.USER
+ : ConversationRole.ASSISTANT;
+
+ yield return (role, text);
+ }
+}
+
+private static bool HasToolContent(ChatMessage message)
+{
+ if (message.Contents is null)
+ return false;
+
+ foreach (var content in message.Contents)
+ {
+ if (content is FunctionCallContent or FunctionResultContent)
+ return true;
+ }
+
+ return false;
+}
+```
+
+### Modified Classes
+
+#### `AgentCoreOptions` (modified)
+
+```csharp
+public class AgentCoreOptions
+{
+ // ... existing properties ...
+
+ ///
+ /// The AgentCore Memory ID for persistent conversation history.
+ /// When set, the Memory provider actively loads and saves conversation history.
+ /// Falls back to the MEMORY_ID environment variable when not set.
+ ///
+ public string? MemoryId { get; set; }
+}
+```
+
+#### `AgentCoreBuilderExtensions.AddAgentCore()` (modified)
+
+Added registrations for the Memory provider and the AWS SDK client:
+
+```csharp
+public static WebApplicationBuilder AddAgentCore(this WebApplicationBuilder builder, Action? configure = null)
+{
+ // ... existing registrations ...
+
+ // Register IAmazonBedrockAgentCore for Memory operations (optional — TryAdd so it doesn't fail if not needed)
+ builder.Services.TryAddAWSService();
+
+ // Register AgentCoreMemoryProvider
+ builder.Services.AddSingleton();
+
+ // Wire the Memory provider into the agent's AIContextProviders list
+ // (done inside the AIAgent factory, after AgentCoreRuntimeContextProvider)
+
+ return builder;
+}
+```
+
+The `AIAgent` factory is updated to attach both context providers to the agent options:
+
+```csharp
+builder.Services.AddSingleton(sp =>
+{
+ // ... existing IChatClient resolution ...
+
+ var agentOptions = options.AgentOptions ?? new ChatClientAgentOptions();
+
+ // Attach context providers: RuntimeContext first, then Memory
+ var runtimeContextProvider = sp.GetRequiredService();
+ var memoryProvider = sp.GetRequiredService();
+
+ var providers = new List();
+ providers.Add(runtimeContextProvider);
+ providers.Add(memoryProvider);
+
+ // Preserve any user-registered providers
+ if (agentOptions.AIContextProviders is not null)
+ providers.AddRange(agentOptions.AIContextProviders);
+
+ agentOptions.AIContextProviders = providers;
+ // Also set ChatHistoryProvider specifically
+ agentOptions.ChatHistoryProvider = memoryProvider;
+
+ var agent = new ChatClientAgent(chatClient, agentOptions);
+
+ if (options.ConfigureAgent is not null)
+ return options.ConfigureAgent(agent);
+
+ return agent;
+});
+```
+
+### AWS SDK Package
+
+The .NET SDK package for AgentCore is **`AWSSDK.BedrockAgentCore`**. This follows the standard AWS SDK for .NET naming convention where the service name maps directly to the NuGet package name. The Java SDK uses `software.amazon.awssdk:bedrockagentcore`, and the .NET SDK follows the pattern `AWSSDK.{ServiceName}`.
+
+The service client interface is `IAmazonBedrockAgentCore` in the `Amazon.BedrockAgentCore` namespace, with model types in `Amazon.BedrockAgentCore.Model`.
+
+**Package reference to add to `AWS.AgentCore.csproj`:**
+
+```xml
+
+```
+
+### DI Registration Summary
+
+| Service | Lifetime | Condition |
+| --------------------------------- | --------- | -------------------------------------------------------------- |
+| `AgentCoreOptions` | Singleton | Always |
+| `IAmazonBedrockAgentCore` | Singleton | TryAdd (doesn't fail if already registered or not needed) |
+| `AgentCoreRuntimeContextProvider` | Singleton | Always |
+| `AgentCoreMemoryProvider` | Singleton | Always (operates in pass-through when MemoryId not configured) |
+| `AIAgent` / `ChatClientAgent` | Singleton | Always |
+
+### Session State Flow
+
+```
+HTTP Header: X-Amzn-Bedrock-AgentCore-Runtime-Session-Id
+ ↓
+AgentCoreRuntimeContext.SessionId (extracted in MapAgentCore pipeline)
+ ↓
+Handler stores context in session StateBag via AgentCoreSessionFactory
+ ↓
+AgentCoreMemoryProvider reads SessionId from StateBag
+ ↓
+Uses SessionId as both sessionId and actorId for Memory API calls
+```
+
+## Data Models
+
+### AgentCore Memory API Types (from AWS SDK)
+
+| Type | Description |
+| --------------------- | ----------------------------------------------------------------------------------------------- |
+| `ListEventsRequest` | Request: memoryId (URI), actorId (URI), sessionId (URI), includePayloads, maxResults, nextToken |
+| `ListEventsResponse` | Response: events[], nextToken |
+| `Event` | eventId, actorId, sessionId, memoryId, eventTimestamp, payload[], metadata |
+| `CreateEventRequest` | Request: memoryId (URI), actorId, sessionId, eventTimestamp, payload[] |
+| `CreateEventResponse` | Response: event |
+| `PayloadType` | Union: conversational OR blob |
+| `Conversational` | role (USER, ASSISTANT, TOOL, OTHER), content |
+| `Content` | Union: text (string, min 1, max 100000) |
+
+### Role Mapping
+
+| AgentCore Memory Role | MS AF ChatRole | Direction |
+| --------------------- | -------------------- | ------------------------------- |
+| `USER` | `ChatRole.User` | Load & Save |
+| `ASSISTANT` | `ChatRole.Assistant` | Load & Save |
+| `TOOL` | — | Skipped (not loaded, not saved) |
+| `OTHER` | — | Skipped (not loaded, not saved) |
+
+### Message Filtering Rules (Save)
+
+A message is persisted to Memory only if ALL of the following are true:
+
+1. The message role is User or Assistant
+2. The message does NOT contain `FunctionCallContent` or `FunctionResultContent`
+3. The message's text content (`message.Text`) is not null/empty/whitespace
+4. The text length is ≥ 1 character (Memory API constraint)
+
+## Correctness Properties
+
+_A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees._
+
+### Property 1: Event-to-ChatMessage Conversion Preserves Data
+
+_For any_ valid AgentCore Memory event with a conversational payload containing a USER or ASSISTANT role and non-empty text content, converting it to a ChatMessage and examining the result should yield a ChatMessage with the corresponding ChatRole and identical text content.
+
+**Validates: Requirements 1.2, 3.3**
+
+### Property 2: Message Filtering Excludes Invalid Messages
+
+_For any_ ChatMessage, the message should be persisted to Memory if and only if: (a) it has User or Assistant role, (b) it contains no FunctionCallContent or FunctionResultContent, and (c) its text content is non-null and non-whitespace with length ≥ 1.
+
+**Validates: Requirements 2.3, 3.1, 3.2, 3.4**
+
+### Property 3: Pagination Fetches All Pages in Order
+
+_For any_ sequence of N paginated ListEvents responses (where responses 1..N-1 have a NextToken and response N does not), the provider should make exactly N API calls and return all events concatenated in the order they were received (page 1 events, then page 2 events, etc.).
+
+**Validates: Requirements 1.3, 10.1, 10.2**
+
+### Property 4: Errors Never Propagate to Caller
+
+_For any_ exception thrown by the AgentCore Memory client during either ListEvents or CreateEvent, the provider should catch the exception, log it, and return gracefully (empty collection for load, no-throw for save) — never allowing the exception to propagate to the agent pipeline.
+
+**Validates: Requirements 1.5, 2.4**
+
+### Property 5: Concurrent Session Isolation
+
+_For any_ two concurrent invocations with different SessionIds, the Memory operations for each invocation should use only its own SessionId — ListEvents and CreateEvent calls for invocation A should never use invocation B's SessionId, and vice versa.
+
+**Validates: Requirements 6.2, 8.1, 8.2, 8.4**
+
+### Property 6: Partial Pagination Failure Returns Loaded Pages
+
+_For any_ pagination sequence where an error occurs on page K (where K > 1), the provider should return all events successfully loaded from pages 1 through K-1, log a warning, and not throw.
+
+**Validates: Requirements 10.3**
+
+## Error Handling
+
+### Error Strategy: Log and Continue
+
+The Memory provider follows a strict "log and continue" policy. Memory is an enhancement — it must never cause an agent invocation to fail.
+
+| Scenario | Behavior |
+| ----------------------------------------------------------- | --------------------------------------------------------------------- |
+| ListEvents throws any exception | Log error, return empty history, agent proceeds without context |
+| CreateEvent throws any exception | Log error, skip that message, continue saving remaining messages |
+| Pagination error on page K > 1 | Log warning, return pages 1..K-1, agent proceeds with partial history |
+| SessionId not in StateBag | Log warning, return empty / skip save |
+| IAmazonBedrockAgentCore not in DI (but MemoryId configured) | Log error once, operate in pass-through mode |
+| MemoryId not configured | No logging, no API calls, pure pass-through |
+
+### Error Logging Levels
+
+- `LogError` — Memory client threw an exception, or client not available when MemoryId is configured
+- `LogWarning` — SessionId missing from StateBag, partial pagination failure
+- No logging — MemoryId not configured (this is normal operation, not an error)
+
+### Retry Strategy
+
+The provider does NOT implement retries. The AWS SDK client handles retries internally via its configured retry policy. If the SDK exhausts retries and throws, the provider catches and logs.
+
+## Testing Strategy
+
+### Property-Based Testing
+
+This feature is suitable for property-based testing because:
+
+- The core logic involves data transformation (events ↔ ChatMessages) with a large input space
+- Message filtering has universal properties that should hold across all message types
+- Pagination logic has properties that hold regardless of page count
+- Error handling has a universal property (never propagate)
+
+**Library:** [FsCheck](https://fscheck.github.io/FsCheck/) via `FsCheck.Xunit` (standard .NET PBT library)
+
+**Configuration:** Minimum 100 iterations per property test.
+
+**Tag format:** `Feature: agentcore-memory, Property {number}: {property_text}`
+
+### Unit Tests
+
+| Test | What It Verifies |
+| ----------------------------------------------------------- | ----------------------------- |
+| `ProvideChatHistory_NoMemoryId_ReturnsEmpty` | Pass-through mode |
+| `ProvideChatHistory_NoSessionId_ReturnsEmptyAndLogsWarning` | Missing session handling |
+| `ProvideChatHistory_NoMemoryClient_LogsErrorReturnsEmpty` | Missing SDK client |
+| `StoreChatHistory_NoMemoryId_DoesNothing` | Pass-through mode for save |
+| `StoreChatHistory_SkipsToolCallMessages` | Tool-call filtering |
+| `StoreChatHistory_SkipsToolResultMessages` | Tool-result filtering |
+| `StoreChatHistory_SkipsEmptyTextMessages` | Empty text filtering |
+| `StoreChatHistory_SavesUserAndAssistantMessages` | Happy path save |
+| `StoreChatHistory_UsesSessionIdAsActorId` | ActorId mapping |
+| `GetEffectiveMemoryId_OptionsOverridesEnvVar` | Configuration precedence |
+| `GetEffectiveMemoryId_FallsBackToEnvVar` | Environment variable fallback |
+| `AddAgentCore_RegistersMemoryProvider` | DI registration |
+| `AddAgentCore_MemoryProviderAfterRuntimeContextProvider` | Provider ordering |
+| `AddAgentCore_PreservesUserContextProviders` | Non-interference |
+
+### Integration Tests
+
+| Test | What It Verifies |
+| ---------------------------------------------------- | ------------------------------------------- |
+| `MemoryProvider_LoadsAndSavesHistory_EndToEnd` | Full round-trip with real/mocked Memory API |
+| `MemoryProvider_ConcurrentRequests_Isolated` | Session isolation under concurrency |
+| `MemoryProvider_NativeAot_NoTrimmingWarnings` | AOT compatibility |
+| `MemoryProvider_SourceGenerator_RegisteredCorrectly` | Source generator DX path |
+
+### Property Tests (from Correctness Properties)
+
+| Test | Property |
+| ----------------------------------------------- | ---------- |
+| `EventToMessageConversion_PreservesRoleAndText` | Property 1 |
+| `MessageFiltering_OnlyPersistsValidMessages` | Property 2 |
+| `Pagination_FetchesAllPagesInOrder` | Property 3 |
+| `Errors_NeverPropagate` | Property 4 |
+| `ConcurrentSessions_UseCorrectSessionId` | Property 5 |
+| `PartialPaginationFailure_ReturnsLoadedPages` | Property 6 |
diff --git a/.kiro/specs/agentcore-memory/requirements.md b/.kiro/specs/agentcore-memory/requirements.md
new file mode 100644
index 0000000..04ce2cc
--- /dev/null
+++ b/.kiro/specs/agentcore-memory/requirements.md
@@ -0,0 +1,150 @@
+# Requirements Document
+
+## Introduction
+
+Integration of the Amazon Bedrock AgentCore Memory service into the AWS.AgentCore .NET library as an `AIContextProvider` within the Microsoft Agent Framework pipeline. This feature provides persistent conversation history that survives container restarts and scaling events, enabling stateful multi-turn conversations for agents deployed to AgentCore Runtime. The integration is opt-in (activated only when a Memory ID is configured), gracefully degrades when Memory is unavailable, and works with both the source generator and extension method developer experiences.
+
+## Glossary
+
+- **AgentCore_Memory**: The Amazon Bedrock AgentCore managed service for persistent conversation history, accessed via AWS SDK operations (ListEvents, CreateEvent).
+- **Memory_Provider**: The AIContextProvider implementation that bridges AgentCore Memory into the MS AF pipeline, loading history before agent runs and saving new messages after.
+- **MemoryId**: A unique identifier for a memory store, configured per runtime via the `MEMORY_ID` environment variable or AgentCoreOptions.
+- **SessionId**: The session identifier from the AgentCore Runtime HTTP header (`X-Amzn-Bedrock-AgentCore-Runtime-Session-Id`), used to scope memory operations.
+- **ActorId**: An identifier for the actor performing memory operations, derived from the session context.
+- **Event**: A single entry in AgentCore Memory, containing a Conversational payload with Role (USER or ASSISTANT) and Content (text).
+- **ListEvents**: The AgentCore Memory API operation that retrieves conversation history, supporting pagination via NextToken.
+- **CreateEvent**: The AgentCore Memory API operation that persists a new conversation event.
+- **AIContextProvider**: The MS AF abstraction for injecting context into the agent pipeline, with InvokingAsync (before) and InvokedAsync (after) hooks.
+- **ChatClientAgent**: The MS AF agent type registered in DI by AddAgentCore, which executes the agent pipeline including context providers.
+- **AgentCoreRuntimeContext**: The typed object populated from AgentCore HTTP headers, stored in the session StateBag.
+- **AddAgentCore**: The WebApplicationBuilder extension method that registers AgentCore services in DI.
+- **StateBag**: The MS AF session state dictionary where AgentCoreRuntimeContext is stored by AgentCoreSessionFactory.
+
+## Requirements
+
+### Requirement 1: Load Conversation History Before Agent Execution
+
+**User Story:** As a .NET developer, I want the agent to automatically load previous conversation history from AgentCore Memory before each run, so that the agent has full context of prior interactions without manual history management.
+
+#### Acceptance Criteria
+
+1. WHEN the agent pipeline executes InvokingAsync and a MemoryId is configured, THE Memory_Provider SHALL call ListEvents on the AgentCore Memory service using the current SessionId and MemoryId.
+2. WHEN ListEvents returns conversation events, THE Memory_Provider SHALL convert each event into a ChatMessage with the appropriate role (User or Assistant) and include them in the returned message collection.
+3. WHEN ListEvents returns paginated results with a NextToken, THE Memory_Provider SHALL continue fetching subsequent pages until all history is retrieved.
+4. WHEN the SessionId is not available in the session StateBag, THE Memory_Provider SHALL return an empty message collection and log a warning.
+5. WHEN the AgentCore Memory service returns an error during history loading, THE Memory_Provider SHALL log the error and return an empty message collection, allowing the agent to proceed without history.
+
+### Requirement 2: Save New Messages After Agent Execution
+
+**User Story:** As a .NET developer, I want the agent to automatically persist new conversation messages to AgentCore Memory after each run, so that future invocations have access to the complete conversation history.
+
+#### Acceptance Criteria
+
+1. WHEN the agent pipeline executes InvokedAsync and a MemoryId is configured, THE Memory_Provider SHALL call CreateEvent on the AgentCore Memory service for the user input message.
+2. WHEN the agent pipeline executes InvokedAsync and a MemoryId is configured, THE Memory_Provider SHALL call CreateEvent on the AgentCore Memory service for the assistant response message.
+3. WHEN a message has empty or whitespace-only text content, THE Memory_Provider SHALL skip that message and not call CreateEvent for it.
+4. WHEN the AgentCore Memory service returns an error during message saving, THE Memory_Provider SHALL log the error and allow the agent response to proceed without interruption.
+5. THE Memory_Provider SHALL use the same SessionId and MemoryId for CreateEvent as was used for ListEvents in the same invocation.
+
+### Requirement 3: Filter Tool-Call Messages
+
+**User Story:** As a .NET developer, I want tool-call and tool-result messages to be excluded from Memory persistence, so that only human-readable conversation content is stored and the Memory service does not reject messages with empty text.
+
+#### Acceptance Criteria
+
+1. WHEN saving messages to AgentCore Memory, THE Memory_Provider SHALL exclude messages that contain tool-call content (function invocations).
+2. WHEN saving messages to AgentCore Memory, THE Memory_Provider SHALL exclude messages that contain tool-result content (function responses).
+3. WHEN loading history from AgentCore Memory, THE Memory_Provider SHALL only produce ChatMessages with User or Assistant roles containing text content.
+4. THE Memory_Provider SHALL only persist messages where the text content has a length of at least 1 character.
+
+### Requirement 4: Graceful Degradation Without Memory Configuration
+
+**User Story:** As a .NET developer, I want my agent to work statelessly when AgentCore Memory is not configured, so that I can develop and test locally without a Memory service dependency.
+
+#### Acceptance Criteria
+
+1. WHEN MemoryId is not configured (neither in AgentCoreOptions nor the MEMORY_ID environment variable), THE Memory_Provider SHALL skip all Memory operations and return empty results from InvokingAsync.
+2. WHEN MemoryId is not configured, THE Memory_Provider SHALL not call any AgentCore Memory API operations.
+3. WHEN MemoryId is not configured, THE Memory_Provider SHALL not log errors or warnings about missing configuration.
+4. WHEN MemoryId becomes available after initial startup (configuration change), THE Memory_Provider SHALL use the configured MemoryId for subsequent requests.
+
+### Requirement 5: Automatic Registration via AddAgentCore
+
+**User Story:** As a .NET developer, I want the Memory provider to be automatically registered when I call AddAgentCore, so that I get persistent conversation history without additional setup code.
+
+#### Acceptance Criteria
+
+1. WHEN AddAgentCore is called, THE AddAgentCore method SHALL always register the Memory_Provider as an AIContextProvider in the DI container.
+2. WHEN a MemoryId is available (via AgentCoreOptions or MEMORY_ID environment variable), THE Memory_Provider SHALL actively load and save conversation history.
+3. WHEN no MemoryId is available, THE Memory_Provider SHALL operate in pass-through mode without calling any Memory APIs.
+4. THE Memory_Provider registration SHALL not interfere with other AIContextProviders registered by the user.
+5. THE Memory_Provider SHALL execute after the AgentCoreRuntimeContextProvider in the pipeline, ensuring the SessionId is available in the StateBag.
+6. WHEN using the source generator approach with [AgentCoreStartup] and ConfigureServices calling AddAgentCore, THE Memory_Provider SHALL be registered with the same behavior as the extension method approach.
+7. WHEN using the source generator approach with [AgentCoreHandler] only (no [AgentCoreStartup]), THE generated code SHALL call AddAgentCore with default options, which registers the Memory_Provider in pass-through mode (activating via MEMORY_ID environment variable at runtime).
+
+### Requirement 6: Use Session ID from AgentCoreRuntimeContext
+
+**User Story:** As a .NET developer, I want the Memory provider to automatically use the session ID from AgentCore HTTP headers, so that conversation history is correctly scoped per session without manual session management.
+
+#### Acceptance Criteria
+
+1. WHEN the Memory_Provider executes, THE Memory_Provider SHALL retrieve the SessionId from the AgentCoreRuntimeContext stored in the session StateBag.
+2. WHEN multiple concurrent requests arrive with different session IDs, THE Memory_Provider SHALL use the correct SessionId for each request's Memory operations.
+3. THE Memory_Provider SHALL use the SessionId as the ActorId for CreateEvent operations.
+4. WHEN the SessionId changes between invocations (new session), THE Memory_Provider SHALL load history for the new SessionId.
+
+### Requirement 7: MemoryId Configuration
+
+**User Story:** As a .NET developer, I want to configure the Memory ID via options or environment variable, so that I can use different memory stores for different environments.
+
+#### Acceptance Criteria
+
+1. WHEN the MEMORY_ID environment variable is set, THE Memory_Provider SHALL use that value as the MemoryId for all Memory operations.
+2. WHEN a MemoryId is set in AgentCoreOptions, THE AgentCoreOptions value SHALL take precedence over the MEMORY_ID environment variable.
+3. WHEN neither AgentCoreOptions.MemoryId nor the MEMORY_ID environment variable is set, THE Memory_Provider SHALL operate in pass-through mode without calling Memory APIs.
+4. THE MemoryId configuration SHALL be readable at request time, allowing runtime configuration changes.
+
+### Requirement 8: Concurrent Request Isolation
+
+**User Story:** As a .NET developer deploying to AgentCore Runtime, I want concurrent requests with different sessions to have isolated memory operations, so that conversation histories do not leak between users.
+
+#### Acceptance Criteria
+
+1. WHEN two concurrent requests arrive with different SessionIds, THE Memory_Provider SHALL load independent conversation histories for each request.
+2. WHEN two concurrent requests arrive with different SessionIds, THE Memory_Provider SHALL save messages to the correct session's history independently.
+3. THE Memory_Provider SHALL not use shared mutable state between concurrent requests.
+4. WHEN a Memory operation for one request fails, THE failure SHALL not affect Memory operations for other concurrent requests.
+
+### Requirement 9: Non-Interference with Existing Agents
+
+**User Story:** As an existing AWS.AgentCore user who does not use Memory, I want the Memory integration to have no impact on my agent's behavior or performance, so that I can upgrade without risk.
+
+#### Acceptance Criteria
+
+1. WHEN MemoryId is not configured, THE Memory_Provider SHALL add no measurable latency to agent invocations.
+2. WHEN MemoryId is not configured, THE Memory_Provider SHALL not make any network calls.
+3. THE Memory_Provider registration SHALL not require any new mandatory dependencies in the DI container.
+4. WHEN the AWS SDK client for AgentCore Memory is not available and MemoryId is configured, THE Memory_Provider SHALL log an error and operate in pass-through mode.
+
+### Requirement 10: Pagination Handling for Long Conversations
+
+**User Story:** As a .NET developer building conversational agents, I want the Memory provider to handle paginated history correctly, so that agents with long conversation histories load all prior context.
+
+#### Acceptance Criteria
+
+1. WHEN ListEvents returns a NextToken in the response, THE Memory_Provider SHALL issue subsequent ListEvents calls with the NextToken until no NextToken is returned.
+2. THE Memory_Provider SHALL assemble paginated results in chronological order.
+3. WHEN pagination encounters an error on a subsequent page, THE Memory_Provider SHALL return the successfully loaded pages and log a warning about incomplete history.
+4. THE Memory_Provider SHALL not impose an artificial limit on the number of pages fetched.
+
+### Requirement 11: NativeAOT Compatibility
+
+**User Story:** As a .NET developer targeting NativeAOT, I want the Memory provider to work without reflection or dynamic code generation, so that agents compiled ahead-of-time can use persistent conversation history with fast cold starts.
+
+#### Acceptance Criteria
+
+1. THE Memory_Provider SHALL not use reflection-based serialization or dynamic code generation for any Memory API operations.
+2. WHEN compiled with PublishAot=true, THE Memory_Provider SHALL produce no trimming warnings.
+3. THE Memory_Provider's DI registration SHALL not use reflection-based service resolution.
+4. WHEN the NativeAotAnnotations sample is configured with a MemoryId, THE application SHALL compile and run correctly with PublishAot=true.
+5. THE Memory_Provider SHALL use source-generated JSON serialization (JsonSerializerContext) for any custom types serialized to or deserialized from the Memory API.
diff --git a/.kiro/specs/agentcore-memory/tasks.md b/.kiro/specs/agentcore-memory/tasks.md
new file mode 100644
index 0000000..b22676c
--- /dev/null
+++ b/.kiro/specs/agentcore-memory/tasks.md
@@ -0,0 +1,169 @@
+# Implementation Plan: AgentCore Memory Integration
+
+## Overview
+
+This plan implements the AgentCore Memory integration as a `ChatHistoryProvider` within the Microsoft Agent Framework pipeline. The work adds the `AWSSDK.BedrockAgentCore` package, creates `AgentCoreMemoryProvider`, modifies `AddAgentCore()` to register the Memory client and provider, and adds comprehensive unit and property-based tests. All code is C#/.NET 10.
+
+## Tasks
+
+- [x] 1. Add MemoryId property to AgentCoreOptions
+ - [x] 1.1 Update AgentCoreOptions with MemoryId property
+ - Add `public string? MemoryId { get; set; }` property to `AgentCoreOptions`
+ - Add XML doc comment explaining it enables persistent conversation history and falls back to `MEMORY_ID` environment variable
+ - _Requirements: 7.1, 7.2, 7.3_
+
+- [x] 2. Add AWSSDK.BedrockAgentCore package reference
+ - [x] 2.1 Update AWS.AgentCore.csproj with new package reference
+ - Add `` to the ItemGroup
+ - Verify the project builds successfully with the new dependency
+ - _Requirements: 5.1, 9.3_
+
+- [x] 3. Implement AgentCoreMemoryProvider class
+ - [x] 3.1 Create AgentCoreMemoryProvider with constructor and configuration resolution
+ - Create new file `src/AWS.AgentCore/AgentCoreMemoryProvider.cs`
+ - Inherit from `ChatHistoryProvider` (from `Microsoft.Agents.AI`)
+ - Accept `AgentCoreOptions`, `ILogger`, and optional `IAmazonBedrockAgentCore?` via constructor
+ - Implement `GetEffectiveMemoryId()` method: options takes precedence over `MEMORY_ID` environment variable
+ - Implement `GetSessionId(AgentSession)` method: retrieve `AgentCoreRuntimeContext` from session StateBag using `AgentCoreRuntimeContextProvider.ContextKey`
+ - _Requirements: 7.1, 7.2, 7.3, 7.4, 6.1_
+
+ - [x] 3.2 Implement ProvideChatHistoryAsync (load history)
+ - Override `ProvideChatHistoryAsync` to load conversation history from AgentCore Memory
+ - Return empty collection when MemoryId is not configured (pass-through mode)
+ - Return empty collection and log error when `IAmazonBedrockAgentCore` is null but MemoryId is configured
+ - Return empty collection and log warning when SessionId is not available in StateBag
+ - Call `ListEventsAsync` with memoryId, sessionId as actorId, sessionId, and includePayloads=true
+ - Handle pagination: loop until no NextToken is returned
+ - On partial pagination failure (error on page K > 1): log warning, return pages loaded so far
+ - On complete failure: log error, return empty collection
+ - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 4.1, 4.2, 4.3, 10.1, 10.2, 10.3, 10.4_
+
+ - [x] 3.3 Implement event-to-ChatMessage conversion
+ - Implement `TryConvertEventToChatMessage` static method
+ - Map `ConversationRole.USER` to `ChatRole.User` and `ConversationRole.ASSISTANT` to `ChatRole.Assistant`
+ - Skip events with null/empty payload, non-conversational payloads, or empty text content
+ - Skip events with TOOL or OTHER roles
+ - _Requirements: 1.2, 3.3_
+
+ - [x] 3.4 Implement StoreChatHistoryAsync (save messages)
+ - Override `StoreChatHistoryAsync` to persist new messages to AgentCore Memory
+ - Return immediately (no-op) when MemoryId is not configured
+ - Return immediately when `IAmazonBedrockAgentCore` is null or SessionId is missing
+ - Filter messages: skip tool-call content (`FunctionCallContent`), tool-result content (`FunctionResultContent`), and empty/whitespace text
+ - Call `CreateEventAsync` for each valid message with memoryId, sessionId, actorId=sessionId, and conversational payload
+ - On failure for individual message: log error, continue with remaining messages
+ - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 3.1, 3.2, 3.4_
+
+ - [x] 3.5 Implement message filtering logic
+ - Implement `FilterMessagesForStorage` static method
+ - Implement `HasToolContent` static method to detect `FunctionCallContent` or `FunctionResultContent`
+ - Only persist messages where role is User or Assistant, no tool content, and text is non-null/non-whitespace with length ≥ 1
+ - _Requirements: 3.1, 3.2, 3.3, 3.4_
+
+- [x] 4. Modify AddAgentCore() to register Memory services
+ - [x] 4.1 Register IAmazonBedrockAgentCore and AgentCoreMemoryProvider in DI
+ - Add `builder.Services.TryAddAWSService()` to `AddAgentCore()`
+ - Add singleton registration for `AgentCoreMemoryProvider`
+ - Update the `AIAgent` factory to wire `AgentCoreMemoryProvider` as a `ChatHistoryProvider` on the agent options
+ - Ensure `AgentCoreMemoryProvider` executes after `AgentCoreRuntimeContextProvider` in the pipeline
+ - Preserve any user-registered `AIContextProviders`
+ - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 9.3, 9.4_
+
+- [x] 5. Checkpoint - Verify core library compiles
+ - Ensure all tests pass, ask the user if questions arise.
+
+- [x] 6. Write unit tests for AgentCoreMemoryProvider
+ - [x] 6.1 Create unit tests for pass-through and configuration behavior
+ - Create new test file `test/AWS.AgentCore.UnitTests/AgentCoreMemoryProviderTests.cs`
+ - Add `AWSSDK.BedrockAgentCore` package reference to the unit test project
+ - Test: `ProvideChatHistory_NoMemoryId_ReturnsEmpty` — verify pass-through when MemoryId not configured
+ - Test: `ProvideChatHistory_NoSessionId_ReturnsEmptyAndLogsWarning` — verify warning logged
+ - Test: `ProvideChatHistory_NoMemoryClient_LogsErrorReturnsEmpty` — verify error logged when client missing
+ - Test: `StoreChatHistory_NoMemoryId_DoesNothing` — verify no API calls in pass-through mode
+ - Test: `GetEffectiveMemoryId_OptionsOverridesEnvVar` — verify options takes precedence
+ - Test: `GetEffectiveMemoryId_FallsBackToEnvVar` — verify environment variable fallback
+ - _Requirements: 4.1, 4.2, 4.3, 7.1, 7.2, 7.3, 9.1, 9.2_
+
+ - [x] 6.2 Create unit tests for message filtering and saving
+ - Test: `StoreChatHistory_SkipsToolCallMessages` — verify FunctionCallContent messages excluded
+ - Test: `StoreChatHistory_SkipsToolResultMessages` — verify FunctionResultContent messages excluded
+ - Test: `StoreChatHistory_SkipsEmptyTextMessages` — verify empty/whitespace text excluded
+ - Test: `StoreChatHistory_SavesUserAndAssistantMessages` — verify happy path save with correct roles
+ - Test: `StoreChatHistory_UsesSessionIdAsActorId` — verify actorId matches sessionId
+ - Test: `StoreChatHistory_ContinuesOnIndividualFailure` — verify one failure doesn't stop others
+ - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 3.1, 3.2, 3.4, 6.3_
+
+ - [x] 6.3 Create unit tests for DI registration
+ - Test: `AddAgentCore_RegistersMemoryProvider` — verify AgentCoreMemoryProvider is in DI
+ - Test: `AddAgentCore_RegistersIAmazonBedrockAgentCore` — verify TryAddAWSService registers client
+ - Test: `AddAgentCore_MemoryProviderAfterRuntimeContextProvider` — verify provider ordering
+ - Test: `AddAgentCore_PreservesUserContextProviders` — verify user providers not overwritten
+ - _Requirements: 5.1, 5.4, 5.5, 9.3, 9.4_
+
+- [x] 7. Checkpoint - Ensure unit tests pass
+ - Ensure all tests pass, ask the user if questions arise.
+
+- [x] 8. Write property-based tests for correctness properties
+ - [x] 8.1 Write property test for event-to-ChatMessage conversion (Property 1)
+ - **Property 1: Event-to-ChatMessage Conversion Preserves Data**
+ - Generate arbitrary valid AgentCore Memory events with USER/ASSISTANT roles and non-empty text
+ - Verify conversion produces ChatMessage with corresponding ChatRole and identical text content
+ - Use FsCheck.Xunit with minimum 100 iterations
+ - **Validates: Requirements 1.2, 3.3**
+
+ - [x] 8.2 Write property test for message filtering (Property 2)
+ - **Property 2: Message Filtering Excludes Invalid Messages**
+ - Generate arbitrary ChatMessages with various roles, content types, and text values
+ - Verify a message is persisted if and only if: (a) User or Assistant role, (b) no FunctionCallContent/FunctionResultContent, (c) non-null non-whitespace text with length ≥ 1
+ - Use FsCheck.Xunit with minimum 100 iterations
+ - **Validates: Requirements 2.3, 3.1, 3.2, 3.4**
+
+ - [x] 8.3 Write property test for pagination (Property 3)
+ - **Property 3: Pagination Fetches All Pages in Order**
+ - Generate arbitrary sequences of N paginated ListEvents responses (1..N-1 have NextToken, N does not)
+ - Verify provider makes exactly N API calls and returns all events concatenated in page order
+ - Use FsCheck.Xunit with minimum 100 iterations
+ - **Validates: Requirements 1.3, 10.1, 10.2**
+
+ - [x] 8.4 Write property test for error handling (Property 4)
+ - **Property 4: Errors Never Propagate to Caller**
+ - Generate arbitrary exceptions thrown by the Memory client during ListEvents or CreateEvent
+ - Verify the provider catches the exception and returns gracefully (empty for load, no-throw for save)
+ - Use FsCheck.Xunit with minimum 100 iterations
+ - **Validates: Requirements 1.5, 2.4**
+
+ - [x] 8.5 Write property test for partial pagination failure (Property 6)
+ - **Property 6: Partial Pagination Failure Returns Loaded Pages**
+ - Generate pagination sequences where an error occurs on page K (K > 1)
+ - Verify provider returns all events from pages 1 through K-1 and does not throw
+ - Use FsCheck.Xunit with minimum 100 iterations
+ - **Validates: Requirements 10.3**
+
+- [x] 9. Verify NativeAOT compatibility
+ - [x] 9.1 Verify NativeAotAnnotations sample compiles with Memory provider
+ - Build the `NativeAotAnnotations` sample with `dotnet publish -c Release` (it has PublishAot=true)
+ - Verify no new trimming warnings related to `AgentCoreMemoryProvider` or `IAmazonBedrockAgentCore` registration
+ - Verify the Memory provider's DI registration does not use reflection-based service resolution
+ - If warnings appear, add appropriate attributes or use source-generated JSON serialization
+ - _Requirements: 11.1, 11.2, 11.3, 11.4_
+
+- [x] 10. Verify source generator compatibility
+ - [x] 10.1 Verify source generator works with Memory provider registration
+ - Build the `AnnotationsSample` project and verify generated code still compiles
+ - Verify that `AgentCoreMemoryProvider` is available in DI when using `[AgentCoreStartup]` approach
+ - Run existing source generator snapshot tests to confirm no regressions
+ - Verify that `[AgentCoreHandler]`-only approach (no `[AgentCoreStartup]`) registers Memory provider in pass-through mode
+ - _Requirements: 5.6, 5.7_
+
+- [x] 11. Final checkpoint - Ensure all tests pass
+ - Ensure all tests pass, ask the user if questions arise.
+
+## Notes
+
+- Tasks marked with `*` are optional and can be skipped for faster MVP
+- Each task references specific requirements for traceability
+- Property tests use FsCheck.Xunit and validate universal correctness properties from the design
+- The Memory provider uses `TryAddAWSService()` so it doesn't fail if the client is already registered or not resolvable
+- The provider accepts `IAmazonBedrockAgentCore?` as optional — gracefully degrades when null
+- Concurrent request isolation is achieved through per-request session state (SessionId from StateBag), not shared mutable state
+- The `ChatHistoryProvider` base class provides the pipeline integration points (`ProvideChatHistoryAsync` and `StoreChatHistoryAsync`)
diff --git a/src/AWS.AgentCore/AWS.AgentCore.csproj b/src/AWS.AgentCore/AWS.AgentCore.csproj
index ba6f925..e0b3a2b 100644
--- a/src/AWS.AgentCore/AWS.AgentCore.csproj
+++ b/src/AWS.AgentCore/AWS.AgentCore.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/AWS.AgentCore/AgentCoreMemoryProvider.cs b/src/AWS.AgentCore/AgentCoreMemoryProvider.cs
new file mode 100644
index 0000000..fabf01d
--- /dev/null
+++ b/src/AWS.AgentCore/AgentCoreMemoryProvider.cs
@@ -0,0 +1,262 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.BedrockAgentCore;
+using Amazon.BedrockAgentCore.Model;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+
+namespace AWS.AgentCore;
+
+///
+/// A that persists conversation history to
+/// Amazon Bedrock AgentCore Memory. Loads history before each agent run via ListEvents
+/// and saves new messages after via CreateEvent.
+///
+/// Registered automatically by .
+/// Operates in pass-through mode (no-op) when MemoryId is not configured.
+///
+///
+internal sealed class AgentCoreMemoryProvider(
+ AgentCoreOptions options,
+ ILogger logger,
+ IAmazonBedrockAgentCore? memoryClient = null)
+ : ChatHistoryProvider
+{
+ ///
+ public override IReadOnlyList StateKeys => ["AgentCore.Memory"];
+
+ ///
+ /// Resolves the effective MemoryId from options or environment variable.
+ /// Options take precedence over environment variable.
+ ///
+ internal string? GetEffectiveMemoryId()
+ {
+ if (!string.IsNullOrWhiteSpace(options.MemoryId))
+ return options.MemoryId;
+
+ var envValue = Environment.GetEnvironmentVariable(Constants.MemoryIdEnvironmentVariable);
+ return string.IsNullOrWhiteSpace(envValue) ? null : envValue;
+ }
+
+ ///
+ /// Retrieves the SessionId from the AgentCoreRuntimeContext stored in the session StateBag.
+ /// Falls back to the ambient AsyncLocal context set by the endpoint handlers.
+ ///
+ internal static string? GetSessionId(AgentSession? session)
+ {
+ // First try the session StateBag (explicit storage by user)
+ if (session is not null)
+ {
+ var context = session.StateBag.GetValue(AgentCoreRuntimeContextProvider.ContextKey);
+ if (context?.SessionId is not null)
+ return context.SessionId;
+ }
+
+ // Fall back to the ambient AsyncLocal context (set automatically by MapAgentCore endpoints)
+ return AgentCoreRuntimeContextProvider.CurrentContext?.SessionId;
+ }
+
+ ///
+ protected override async ValueTask> ProvideChatHistoryAsync(
+ InvokingContext context,
+ CancellationToken cancellationToken = default)
+ {
+ var memoryId = GetEffectiveMemoryId();
+ if (memoryId is null)
+ return [];
+
+ if (memoryClient is null)
+ {
+ logger.LogError("MemoryId is configured but IAmazonBedrockAgentCore is not registered in DI. Memory operations will be skipped.");
+ return [];
+ }
+
+ var sessionId = GetSessionId(context.Session);
+ if (sessionId is null)
+ {
+ logger.LogWarning("SessionId not available. Ensure the request is handled by a MapAgentCore endpoint. Skipping memory load.");
+ return [];
+ }
+
+ try
+ {
+ return await LoadHistoryAsync(memoryId, sessionId, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to load conversation history from AgentCore Memory. Proceeding without history.");
+ return [];
+ }
+ }
+
+ ///
+ protected override async ValueTask StoreChatHistoryAsync(
+ InvokedContext context,
+ CancellationToken cancellationToken = default)
+ {
+ var memoryId = GetEffectiveMemoryId();
+ if (memoryId is null)
+ return;
+
+ if (memoryClient is null)
+ return;
+
+ var sessionId = GetSessionId(context.Session);
+ if (sessionId is null)
+ return;
+
+ var messagesToSave = FilterMessagesForStorage(context.RequestMessages, context.ResponseMessages);
+
+ foreach (var (role, text) in messagesToSave)
+ {
+ try
+ {
+ await SaveEventAsync(memoryId, sessionId, role, text, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to save message to AgentCore Memory. Continuing.");
+ }
+ }
+ }
+
+ private async Task> LoadHistoryAsync(
+ string memoryId, string sessionId, CancellationToken cancellationToken)
+ {
+ if (memoryClient is null)
+ return [];
+
+ var messages = new List();
+
+ var request = new ListEventsRequest
+ {
+ MemoryId = memoryId,
+ // ActorId = sessionId: see SaveEventAsync comment for rationale
+ ActorId = sessionId,
+ SessionId = sessionId,
+ IncludePayloads = true
+ };
+
+ try
+ {
+ await foreach (var evt in memoryClient.Paginators.ListEvents(request).Events.WithCancellation(cancellationToken))
+ {
+ if (TryConvertEventToChatMessage(evt, out var chatMessage))
+ {
+ messages.Add(chatMessage);
+ }
+ }
+ }
+ catch (Exception ex) when (messages.Count > 0)
+ {
+ // Partial pagination failure — return what we have
+ logger.LogWarning(ex, "Error during pagination. Returning {Count} messages loaded so far.", messages.Count);
+ }
+
+ return messages;
+ }
+
+ internal static bool TryConvertEventToChatMessage(Event evt, out ChatMessage message)
+ {
+ message = default!;
+
+ if (evt.Payload is null || evt.Payload.Count == 0)
+ return false;
+
+ foreach (var payload in evt.Payload)
+ {
+ if (payload.Conversational is { } conversational
+ && conversational.Content?.Text is { Length: > 0 } text)
+ {
+ ChatRole? role = null;
+ if (conversational.Role == Role.USER)
+ role = ChatRole.User;
+ else if (conversational.Role == Role.ASSISTANT)
+ role = ChatRole.Assistant;
+
+ if (role is not null)
+ {
+ message = new ChatMessage(role.Value, text);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private async Task SaveEventAsync(
+ string memoryId, string sessionId, Role role, string text,
+ CancellationToken cancellationToken)
+ {
+ if (memoryClient is null)
+ return;
+
+ await memoryClient.CreateEventAsync(new CreateEventRequest
+ {
+ MemoryId = memoryId,
+ SessionId = sessionId,
+ // NOTE: ActorId is set to sessionId intentionally. In this session-scoped short-term
+ // memory implementation, the session IS the actor scope. A future long-term memory
+ // feature may introduce a separate ActorId/UserId concept.
+ ActorId = sessionId,
+ EventTimestamp = DateTime.UtcNow,
+ Payload = [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = role,
+ Content = new Content { Text = text }
+ }
+ }
+ ]
+ }, cancellationToken);
+ }
+
+ internal static IEnumerable<(Role Role, string Text)> FilterMessagesForStorage(
+ IEnumerable? requestMessages,
+ IEnumerable? responseMessages)
+ {
+ var allMessages = (requestMessages ?? []).Concat(responseMessages ?? []);
+
+ foreach (var message in allMessages)
+ {
+ // Skip messages with tool-call or tool-result content
+ if (HasToolContent(message))
+ continue;
+
+ // Only persist User and Assistant messages — skip System, Tool, and any other roles
+ Role role;
+ if (message.Role == ChatRole.User)
+ role = Role.USER;
+ else if (message.Role == ChatRole.Assistant)
+ role = Role.ASSISTANT;
+ else
+ continue;
+
+ // Extract text content
+ var text = message.Text;
+ if (string.IsNullOrWhiteSpace(text))
+ continue;
+
+ yield return (role, text);
+ }
+ }
+
+ internal static bool HasToolContent(ChatMessage message)
+ {
+ if (message.Contents is null)
+ return false;
+
+ foreach (var content in message.Contents)
+ {
+ if (content is FunctionCallContent or FunctionResultContent)
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/AWS.AgentCore/AgentCoreOptions.cs b/src/AWS.AgentCore/AgentCoreOptions.cs
index e0fb393..61702de 100644
--- a/src/AWS.AgentCore/AgentCoreOptions.cs
+++ b/src/AWS.AgentCore/AgentCoreOptions.cs
@@ -40,4 +40,13 @@ public class AgentCoreOptions
/// (which may be decorated with middleware).
///
public Func? ConfigureAgent { get; set; }
+
+ ///
+ /// The AgentCore Memory ID for persistent conversation history.
+ /// When set, the Memory provider actively loads and saves conversation history
+ /// across invocations and container restarts.
+ /// Falls back to the environment variable when not set.
+ /// When neither is configured, the agent operates statelessly.
+ ///
+ public string? MemoryId { get; set; }
}
diff --git a/src/AWS.AgentCore/AgentCoreRuntimeContextProvider.cs b/src/AWS.AgentCore/AgentCoreRuntimeContextProvider.cs
index 90167b7..88d9d82 100644
--- a/src/AWS.AgentCore/AgentCoreRuntimeContextProvider.cs
+++ b/src/AWS.AgentCore/AgentCoreRuntimeContextProvider.cs
@@ -11,7 +11,8 @@ namespace AWS.AgentCore;
///
/// Registered automatically by .
/// Downstream middleware and context providers can access the runtime context (session ID,
-/// request ID, access tokens, custom headers) via the session's state bag using .
+/// request ID, access tokens, custom headers) via the session's state bag using ,
+/// or via the ambient property which is set automatically by the endpoint handlers.
///
///
public class AgentCoreRuntimeContextProvider : AIContextProvider
@@ -20,4 +21,18 @@ public class AgentCoreRuntimeContextProvider : AIContextProvider
/// Key used to store/retrieve in the agent session state.
///
public const string ContextKey = "AgentCore.RuntimeContext";
+
+ private static readonly AsyncLocal _currentContext = new();
+
+ ///
+ /// Gets or sets the for the current async execution context.
+ /// This is set automatically by the AgentCore endpoint handlers (MapAgentCore) and
+ /// flows through async calls, making it available to the Memory provider without requiring
+ /// manual session StateBag population.
+ ///
+ public static AgentCoreRuntimeContext? CurrentContext
+ {
+ get => _currentContext.Value;
+ set => _currentContext.Value = value;
+ }
}
diff --git a/src/AWS.AgentCore/Constants.cs b/src/AWS.AgentCore/Constants.cs
new file mode 100644
index 0000000..bf0d82a
--- /dev/null
+++ b/src/AWS.AgentCore/Constants.cs
@@ -0,0 +1,17 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+namespace AWS.AgentCore;
+
+///
+/// Constants used throughout the AWS.AgentCore library.
+///
+internal static class Constants
+{
+ ///
+ /// Environment variable name for the AgentCore Memory ID.
+ /// When set, the Memory provider uses this value as the MemoryId for all Memory operations
+ /// (unless overridden by ).
+ ///
+ internal const string MemoryIdEnvironmentVariable = "AWS_AGENTCORE_MEMORY_ID";
+}
diff --git a/src/AWS.AgentCore/Extensions/AgentCoreBuilderExtensions.cs b/src/AWS.AgentCore/Extensions/AgentCoreBuilderExtensions.cs
index 236d52f..bfd62c4 100644
--- a/src/AWS.AgentCore/Extensions/AgentCoreBuilderExtensions.cs
+++ b/src/AWS.AgentCore/Extensions/AgentCoreBuilderExtensions.cs
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
+using Amazon.BedrockAgentCore;
using Amazon.BedrockRuntime;
using Microsoft.Agents.AI;
using Microsoft.AspNetCore.Builder;
@@ -114,6 +115,12 @@ public static WebApplicationBuilder AddAgentCore(this WebApplicationBuilder buil
// Register AgentCoreRuntimeContextProvider (AIContextProvider)
builder.Services.AddSingleton();
+ // Register IAmazonBedrockAgentCore for Memory operations
+ builder.Services.TryAddAWSService();
+
+ // Register AgentCoreMemoryProvider
+ builder.Services.AddSingleton();
+
// Register AIAgent (may be a ChatClientAgent or a middleware-decorated agent)
builder.Services.AddSingleton(sp =>
{
@@ -128,6 +135,12 @@ public static WebApplicationBuilder AddAgentCore(this WebApplicationBuilder buil
}
var agentOptions = options.AgentOptions ?? new ChatClientAgentOptions();
+
+ // Wire the Memory provider as the ChatHistoryProvider
+ var memoryProvider = sp.GetRequiredService();
+ // Only set the memory provider if the user hasn't configured their own ChatHistoryProvider
+ agentOptions.ChatHistoryProvider ??= memoryProvider;
+
var agent = new ChatClientAgent(chatClient, agentOptions);
if (options.ConfigureAgent is not null)
diff --git a/src/AWS.AgentCore/Extensions/AgentCoreEndpointExtensions.cs b/src/AWS.AgentCore/Extensions/AgentCoreEndpointExtensions.cs
index 8a30c8e..bfb1c01 100644
--- a/src/AWS.AgentCore/Extensions/AgentCoreEndpointExtensions.cs
+++ b/src/AWS.AgentCore/Extensions/AgentCoreEndpointExtensions.cs
@@ -209,6 +209,7 @@ await httpContext.Response.WriteAsJsonAsync(
}
var context = AgentCoreRuntimeContext.FromHttpContext(httpContext);
+ AgentCoreRuntimeContextProvider.CurrentContext = context;
var result = await handler(request, context, httpContext.RequestServices, httpContext.RequestAborted);
await httpContext.Response.WriteAsJsonAsync(
new JsonMessageResponse(result, DateTime.UtcNow),
@@ -269,6 +270,7 @@ await httpContext.Response.WriteAsJsonAsync(
}
var context = AgentCoreRuntimeContext.FromHttpContext(httpContext);
+ AgentCoreRuntimeContextProvider.CurrentContext = context;
var result = await handler(request, context, httpContext.RequestServices, httpContext.RequestAborted);
await httpContext.Response.WriteAsJsonAsync(
new JsonMessageResponse(result, DateTime.UtcNow),
@@ -312,6 +314,7 @@ await httpContext.Response.WriteAsJsonAsync(
}
var context = AgentCoreRuntimeContext.FromHttpContext(httpContext);
+ AgentCoreRuntimeContextProvider.CurrentContext = context;
var stream = handler(request, context, httpContext.RequestServices, httpContext.RequestAborted);
await StreamingResponseWriter.WriteStreamingResponseAsync(httpContext, stream);
});
@@ -370,6 +373,7 @@ await httpContext.Response.WriteAsJsonAsync(
}
var context = AgentCoreRuntimeContext.FromHttpContext(httpContext);
+ AgentCoreRuntimeContextProvider.CurrentContext = context;
var stream = handler(request, context, httpContext.RequestServices, httpContext.RequestAborted);
await StreamingResponseWriter.WriteStreamingResponseAsync(httpContext, stream);
});
diff --git a/src/AWS.AgentCore/Internal/ParameterBindingPlan.cs b/src/AWS.AgentCore/Internal/ParameterBindingPlan.cs
index 2fe10b9..2e491e2 100644
--- a/src/AWS.AgentCore/Internal/ParameterBindingPlan.cs
+++ b/src/AWS.AgentCore/Internal/ParameterBindingPlan.cs
@@ -108,6 +108,12 @@ internal static ParameterBindingPlan Create(Delegate handler)
};
}
+ // Set the ambient AsyncLocal so downstream code (e.g. AgentCoreMemoryProvider)
+ // can access the runtime context without manual StateBag population.
+ var runtimeContext = args.OfType().FirstOrDefault()
+ ?? AgentCoreRuntimeContext.FromHttpContext(httpContext);
+ AgentCoreRuntimeContextProvider.CurrentContext = runtimeContext;
+
return args;
}
diff --git a/test/AWS.AgentCore.IntegrationTests/Infrastructure/AgentCoreInvoker.cs b/test/AWS.AgentCore.IntegrationTests/Infrastructure/AgentCoreInvoker.cs
index 9f9787e..dd246bb 100644
--- a/test/AWS.AgentCore.IntegrationTests/Infrastructure/AgentCoreInvoker.cs
+++ b/test/AWS.AgentCore.IntegrationTests/Infrastructure/AgentCoreInvoker.cs
@@ -27,7 +27,7 @@ public AgentCoreInvoker(string region)
///
/// Invokes a non-streaming agent and returns the parsed message from the JSON response.
///
- public async Task InvokeAsync(string runtimeArn, string prompt, CancellationToken ct = default)
+ public async Task InvokeAsync(string runtimeArn, string prompt, CancellationToken ct = default, string? sessionId = null)
{
var payload = JsonSerializer.Serialize(new { prompt });
@@ -45,6 +45,11 @@ public async Task InvokeAsync(string runtimeArn, string prompt
Accept = "application/json",
};
+ if (!string.IsNullOrEmpty(sessionId))
+ {
+ request.RuntimeSessionId = sessionId;
+ }
+
var response = await _client.InvokeAgentRuntimeAsync(request, ct);
using var reader = new StreamReader(response.Response);
@@ -99,7 +104,7 @@ public async Task InvokeAsync(string runtimeArn, string prompt
/// Invokes a streaming agent and collects all SSE chunks into a result.
///
public async Task InvokeStreamingAsync(
- string runtimeArn, string prompt, CancellationToken ct = default)
+ string runtimeArn, string prompt, CancellationToken ct = default, string? sessionId = null)
{
var payload = JsonSerializer.Serialize(new { prompt });
@@ -117,6 +122,11 @@ public async Task InvokeStreamingAsync(
Accept = "text/event-stream",
};
+ if (!string.IsNullOrEmpty(sessionId))
+ {
+ request.RuntimeSessionId = sessionId;
+ }
+
var response = await _client.InvokeAgentRuntimeAsync(request, ct);
var chunks = new List();
diff --git a/test/AWS.AgentCore.IntegrationTests/Infrastructure/TestResourceManager.cs b/test/AWS.AgentCore.IntegrationTests/Infrastructure/TestResourceManager.cs
index a5ead1a..8cea6a9 100644
--- a/test/AWS.AgentCore.IntegrationTests/Infrastructure/TestResourceManager.cs
+++ b/test/AWS.AgentCore.IntegrationTests/Infrastructure/TestResourceManager.cs
@@ -33,6 +33,7 @@ public sealed class TestResourceManager : IAsyncDisposable
private string? _roleArn;
private string? _ecrRepositoryUri;
private string? _ecrRepositoryName;
+ private string? _memoryId;
/// Runtime ARNs keyed by sample app name (e.g. "MicrosoftAgentFrameworkSample").
private readonly Dictionary _runtimeArns = new();
@@ -81,6 +82,13 @@ public async Task GetEcrRepositoryUriAsync(CancellationToken ct = defaul
return _ecrRepositoryUri!;
}
+ /// Gets the AgentCore Memory ID.
+ public async Task GetMemoryIdAsync(CancellationToken ct = default)
+ {
+ await EnsureInitializedAsync(ct);
+ return _memoryId!;
+ }
+
public string Region => _region;
private async Task EnsureInitializedAsync(CancellationToken ct)
@@ -152,6 +160,7 @@ await _cfnClient.CreateStackAsync(new CreateStackRequest
_roleArn = outputs["RoleArn"];
_ecrRepositoryUri = outputs["EcrRepositoryUri"];
_ecrRepositoryName = outputs["EcrRepositoryName"];
+ _memoryId = outputs["MemoryId"];
Console.Error.WriteLine($"[Resources] Base stack ready. Role={_roleArn}, ECR={_ecrRepositoryUri}");
}
@@ -197,6 +206,7 @@ private async Task CreateRuntimesStackAsync(Dictionary imageUris
{
new() { ParameterKey = "TestRunId", ParameterValue = _testRunId },
new() { ParameterKey = "RoleArn", ParameterValue = _roleArn! },
+ new() { ParameterKey = "MemoryId", ParameterValue = _memoryId! },
};
foreach (var (appName, imageUri) in imageUris)
diff --git a/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-runtimes.template.json b/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-runtimes.template.json
index af80131..b471adb 100644
--- a/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-runtimes.template.json
+++ b/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-runtimes.template.json
@@ -25,6 +25,10 @@
},
"NativeAotAnnotationsImageUri": {
"Type": "String"
+ },
+ "MemoryId": {
+ "Type": "String",
+ "Description": "AgentCore Memory ID for session history."
}
},
"Resources": {
@@ -40,6 +44,9 @@
"ContainerUri": { "Ref": "MicrosoftAgentFrameworkSampleImageUri" }
}
},
+ "EnvironmentVariables": {
+ "AWS_AGENTCORE_MEMORY_ID": { "Ref": "MemoryId" }
+ },
"NetworkConfiguration": {
"NetworkMode": "PUBLIC"
},
@@ -59,6 +66,9 @@
"ContainerUri": { "Ref": "AnnotationsSampleImageUri" }
}
},
+ "EnvironmentVariables": {
+ "AWS_AGENTCORE_MEMORY_ID": { "Ref": "MemoryId" }
+ },
"NetworkConfiguration": {
"NetworkMode": "PUBLIC"
},
@@ -78,6 +88,9 @@
"ContainerUri": { "Ref": "StreamingAgentImageUri" }
}
},
+ "EnvironmentVariables": {
+ "AWS_AGENTCORE_MEMORY_ID": { "Ref": "MemoryId" }
+ },
"NetworkConfiguration": {
"NetworkMode": "PUBLIC"
},
@@ -99,6 +112,9 @@
"ContainerUri": { "Ref": "AnnotationsStreamingAgentImageUri" }
}
},
+ "EnvironmentVariables": {
+ "AWS_AGENTCORE_MEMORY_ID": { "Ref": "MemoryId" }
+ },
"NetworkConfiguration": {
"NetworkMode": "PUBLIC"
},
@@ -118,6 +134,9 @@
"ContainerUri": { "Ref": "NativeAotExtensionsImageUri" }
}
},
+ "EnvironmentVariables": {
+ "AWS_AGENTCORE_MEMORY_ID": { "Ref": "MemoryId" }
+ },
"NetworkConfiguration": {
"NetworkMode": "PUBLIC"
},
@@ -137,6 +156,9 @@
"ContainerUri": { "Ref": "NativeAotAnnotationsImageUri" }
}
},
+ "EnvironmentVariables": {
+ "AWS_AGENTCORE_MEMORY_ID": { "Ref": "MemoryId" }
+ },
"NetworkConfiguration": {
"NetworkMode": "PUBLIC"
},
diff --git a/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-stack.template.json b/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-stack.template.json
index 05f594c..0420e82 100644
--- a/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-stack.template.json
+++ b/test/AWS.AgentCore.IntegrationTests/Infrastructure/inttest-stack.template.json
@@ -1,6 +1,6 @@
{
"AWSTemplateFormatVersion": "2010-09-09",
- "Description": "Integration test resources for AWS.AgentCore — IAM role and ECR repository.",
+ "Description": "Integration test resources for AWS.AgentCore — IAM role, ECR repository, and Memory.",
"Parameters": {
"TestRunId": {
"Type": "String",
@@ -124,6 +124,18 @@
"Effect": "Allow",
"Action": ["s3:ListAllMyBuckets"],
"Resource": "*"
+ },
+ {
+ "Sid": "AgentCoreMemory",
+ "Effect": "Allow",
+ "Action": [
+ "bedrock-agentcore:CreateEvent",
+ "bedrock-agentcore:ListEvents",
+ "bedrock-agentcore:GetMemory"
+ ],
+ "Resource": {
+ "Fn::Sub": "arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:memory/*"
+ }
}
]
}
@@ -146,6 +158,17 @@
{ "Key": "TestRunId", "Value": { "Ref": "TestRunId" } }
]
}
+ },
+ "AgentCoreMemory": {
+ "Type": "AWS::BedrockAgentCore::Memory",
+ "Properties": {
+ "Name": { "Fn::Sub": "inttest_${TestRunId}" },
+ "EventExpiryDuration": 3,
+ "Tags": {
+ "CreatedBy": "AgentCoreIntegrationTests",
+ "TestRunId": { "Ref": "TestRunId" }
+ }
+ }
}
},
"Outputs": {
@@ -160,6 +183,10 @@
"EcrRepositoryName": {
"Description": "Name of the ECR repository.",
"Value": { "Ref": "EcrRepository" }
+ },
+ "MemoryId": {
+ "Description": "ID of the AgentCore Memory for integration tests.",
+ "Value": { "Fn::GetAtt": ["AgentCoreMemory", "MemoryId"] }
}
}
}
diff --git a/test/AWS.AgentCore.IntegrationTests/MemoryIntegrationTests.cs b/test/AWS.AgentCore.IntegrationTests/MemoryIntegrationTests.cs
new file mode 100644
index 0000000..4cb798a
--- /dev/null
+++ b/test/AWS.AgentCore.IntegrationTests/MemoryIntegrationTests.cs
@@ -0,0 +1,249 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using AWS.AgentCore.IntegrationTests.Infrastructure;
+
+namespace AWS.AgentCore.IntegrationTests;
+
+///
+/// Memory integration tests for non-streaming sample apps.
+/// Each test tells the agent a unique piece of information, then asks about it
+/// in a separate invocation using the same session ID to verify memory persistence.
+///
+public class MicrosoftAgentFrameworkMemoryTests : IClassFixture, IDisposable
+{
+ private readonly MicrosoftAgentFrameworkFixture _fixture;
+ private readonly AgentCoreInvoker _invoker;
+
+ public MicrosoftAgentFrameworkMemoryTests(MicrosoftAgentFrameworkFixture fixture)
+ {
+ _fixture = fixture;
+ _invoker = new AgentCoreInvoker(_fixture.Region);
+ }
+
+ [Fact]
+ public async Task Memory_RemembersInformationAcrossInvocations()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var sessionId = $"memory-test-msaf-{Guid.NewGuid():N}";
+ var secretCode = $"ALPHA-{Random.Shared.Next(1000, 9999)}";
+
+ // First invocation: tell the agent a unique piece of information
+ await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ $"Remember this secret code: {secretCode}. Just confirm you've noted it.",
+ ct,
+ sessionId: sessionId);
+
+ // Brief delay to allow memory persistence
+ await Task.Delay(TimeSpan.FromSeconds(2), ct);
+
+ // Second invocation: ask the agent to recall the information
+ var result = await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ "What was the secret code I told you earlier? Reply with ONLY the code, nothing else.",
+ ct,
+ sessionId: sessionId);
+
+ Assert.Equal(200, result.HttpStatusCode);
+ Assert.Contains(secretCode, result.Message);
+ }
+
+ public void Dispose() => _invoker.Dispose();
+}
+
+public class AnnotationsSampleMemoryTests : IClassFixture, IDisposable
+{
+ private readonly AnnotationsSampleFixture _fixture;
+ private readonly AgentCoreInvoker _invoker;
+
+ public AnnotationsSampleMemoryTests(AnnotationsSampleFixture fixture)
+ {
+ _fixture = fixture;
+ _invoker = new AgentCoreInvoker(_fixture.Region);
+ }
+
+ [Fact]
+ public async Task Memory_RemembersInformationAcrossInvocations()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var sessionId = $"memory-test-annotations-{Guid.NewGuid():N}";
+ var secretCode = $"BRAVO-{Random.Shared.Next(1000, 9999)}";
+
+ await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ $"Remember this secret code: {secretCode}. Just confirm you've noted it.",
+ ct,
+ sessionId: sessionId);
+
+ await Task.Delay(TimeSpan.FromSeconds(2), ct);
+
+ var result = await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ "What was the secret code I told you earlier? Reply with ONLY the code, nothing else.",
+ ct,
+ sessionId: sessionId);
+
+ Assert.Equal(200, result.HttpStatusCode);
+ Assert.Contains(secretCode, result.Message);
+ }
+
+ public void Dispose() => _invoker.Dispose();
+}
+
+public class NativeAotExtensionsMemoryTests : IClassFixture, IDisposable
+{
+ private readonly NativeAotExtensionsFixture _fixture;
+ private readonly AgentCoreInvoker _invoker;
+
+ public NativeAotExtensionsMemoryTests(NativeAotExtensionsFixture fixture)
+ {
+ _fixture = fixture;
+ _invoker = new AgentCoreInvoker(_fixture.Region);
+ }
+
+ [Fact]
+ public async Task Memory_RemembersInformationAcrossInvocations()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var sessionId = $"memory-test-aot-ext-{Guid.NewGuid():N}";
+ var secretCode = $"CHARLIE-{Random.Shared.Next(1000, 9999)}";
+
+ await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ $"Remember this secret code: {secretCode}. Just confirm you've noted it.",
+ ct,
+ sessionId: sessionId);
+
+ await Task.Delay(TimeSpan.FromSeconds(2), ct);
+
+ var result = await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ "What was the secret code I told you earlier? Reply with ONLY the code, nothing else.",
+ ct,
+ sessionId: sessionId);
+
+ Assert.Equal(200, result.HttpStatusCode);
+ Assert.Contains(secretCode, result.Message);
+ }
+
+ public void Dispose() => _invoker.Dispose();
+}
+
+public class NativeAotAnnotationsMemoryTests : IClassFixture, IDisposable
+{
+ private readonly NativeAotAnnotationsFixture _fixture;
+ private readonly AgentCoreInvoker _invoker;
+
+ public NativeAotAnnotationsMemoryTests(NativeAotAnnotationsFixture fixture)
+ {
+ _fixture = fixture;
+ _invoker = new AgentCoreInvoker(_fixture.Region);
+ }
+
+ [Fact]
+ public async Task Memory_RemembersInformationAcrossInvocations()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var sessionId = $"memory-test-aot-ann-{Guid.NewGuid():N}";
+ var secretCode = $"DELTA-{Random.Shared.Next(1000, 9999)}";
+
+ await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ $"Remember this secret code: {secretCode}. Just confirm you've noted it.",
+ ct,
+ sessionId: sessionId);
+
+ await Task.Delay(TimeSpan.FromSeconds(2), ct);
+
+ var result = await _invoker.InvokeAsync(
+ _fixture.RuntimeArn,
+ "What was the secret code I told you earlier? Reply with ONLY the code, nothing else.",
+ ct,
+ sessionId: sessionId);
+
+ Assert.Equal(200, result.HttpStatusCode);
+ Assert.Contains(secretCode, result.Message);
+ }
+
+ public void Dispose() => _invoker.Dispose();
+}
+
+public class StreamingAgentMemoryTests : IClassFixture, IDisposable
+{
+ private readonly StreamingAgentFixture _fixture;
+ private readonly AgentCoreInvoker _invoker;
+
+ public StreamingAgentMemoryTests(StreamingAgentFixture fixture)
+ {
+ _fixture = fixture;
+ _invoker = new AgentCoreInvoker(_fixture.Region);
+ }
+
+ [Fact]
+ public async Task Memory_RemembersInformationAcrossInvocations()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var sessionId = $"memory-test-streaming-{Guid.NewGuid():N}";
+ var secretCode = $"ECHO-{Random.Shared.Next(1000, 9999)}";
+
+ // Use streaming invocation for both calls
+ await _invoker.InvokeStreamingAsync(
+ _fixture.RuntimeArn,
+ $"Remember this secret code: {secretCode}. Just confirm you've noted it.",
+ ct,
+ sessionId: sessionId);
+
+ await Task.Delay(TimeSpan.FromSeconds(2), ct);
+
+ var result = await _invoker.InvokeStreamingAsync(
+ _fixture.RuntimeArn,
+ "What was the secret code I told you earlier? Reply with ONLY the code, nothing else.",
+ ct,
+ sessionId: sessionId);
+
+ Assert.Equal(200, result.HttpStatusCode);
+ Assert.Contains(secretCode, result.FinalMessage);
+ }
+
+ public void Dispose() => _invoker.Dispose();
+}
+
+public class AnnotationsStreamingAgentMemoryTests : IClassFixture, IDisposable
+{
+ private readonly AnnotationsStreamingAgentFixture _fixture;
+ private readonly AgentCoreInvoker _invoker;
+
+ public AnnotationsStreamingAgentMemoryTests(AnnotationsStreamingAgentFixture fixture)
+ {
+ _fixture = fixture;
+ _invoker = new AgentCoreInvoker(_fixture.Region);
+ }
+
+ [Fact]
+ public async Task Memory_RemembersInformationAcrossInvocations()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ var sessionId = $"memory-test-ann-stream-{Guid.NewGuid():N}";
+ var secretCode = $"FOXTROT-{Random.Shared.Next(1000, 9999)}";
+
+ await _invoker.InvokeStreamingAsync(
+ _fixture.RuntimeArn,
+ $"Remember this secret code: {secretCode}. Just confirm you've noted it.",
+ ct,
+ sessionId: sessionId);
+
+ await Task.Delay(TimeSpan.FromSeconds(2), ct);
+
+ var result = await _invoker.InvokeStreamingAsync(
+ _fixture.RuntimeArn,
+ "What was the secret code I told you earlier? Reply with ONLY the code, nothing else.",
+ ct,
+ sessionId: sessionId);
+
+ Assert.Equal(200, result.HttpStatusCode);
+ Assert.Contains(secretCode, result.FinalMessage);
+ }
+
+ public void Dispose() => _invoker.Dispose();
+}
diff --git a/test/AWS.AgentCore.UnitTests/AWS.AgentCore.UnitTests.csproj b/test/AWS.AgentCore.UnitTests/AWS.AgentCore.UnitTests.csproj
index e8257a3..465c468 100644
--- a/test/AWS.AgentCore.UnitTests/AWS.AgentCore.UnitTests.csproj
+++ b/test/AWS.AgentCore.UnitTests/AWS.AgentCore.UnitTests.csproj
@@ -9,6 +9,7 @@
+
diff --git a/test/AWS.AgentCore.UnitTests/AgentCoreMemoryProviderPropertyTests.cs b/test/AWS.AgentCore.UnitTests/AgentCoreMemoryProviderPropertyTests.cs
new file mode 100644
index 0000000..c0afafe
--- /dev/null
+++ b/test/AWS.AgentCore.UnitTests/AgentCoreMemoryProviderPropertyTests.cs
@@ -0,0 +1,449 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma warning disable MAAI001 // Experimental API usage required for testing
+
+using Amazon.BedrockAgentCore;
+using Amazon.BedrockAgentCore.Model;
+using AWS.AgentCore;
+using FsCheck;
+using FsCheck.Xunit;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+
+namespace AWS.AgentCore.UnitTests;
+
+///
+/// Property-based tests for AgentCoreMemoryProvider correctness properties.
+/// Uses FsCheck to generate arbitrary inputs and verify universal properties.
+/// Tag format: Feature: agentcore-memory, Property {number}: {property_text}
+///
+public class AgentCoreMemoryProviderPropertyTests
+{
+ // ──────────────────────────────────────────────────────────────────
+ // Property 1: Event-to-ChatMessage Conversion Preserves Data
+ // For any valid AgentCore Memory event with USER/ASSISTANT role and
+ // non-empty text, conversion produces a ChatMessage with the
+ // corresponding ChatRole and identical text content.
+ // Validates: Requirements 1.2, 3.3
+ // ──────────────────────────────────────────────────────────────────
+
+ [Property(MaxTest = 100)]
+ public bool EventToMessageConversion_PreservesRoleAndText(NonEmptyString textWrapper, bool isUser)
+ {
+ var text = textWrapper.Get;
+ // Skip whitespace-only strings since the implementation requires Length > 0 on Content.Text
+ if (string.IsNullOrWhiteSpace(text))
+ return true; // vacuously true for invalid inputs
+
+ var role = isUser ? Role.USER : Role.ASSISTANT;
+ var expectedChatRole = isUser ? ChatRole.User : ChatRole.Assistant;
+
+ var evt = new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = role,
+ Content = new Content { Text = text }
+ }
+ }
+ ]
+ };
+
+ var success = AgentCoreMemoryProvider.TryConvertEventToChatMessage(evt, out var message);
+
+ return success
+ && message.Role == expectedChatRole
+ && message.Text == text;
+ }
+
+ [Property(MaxTest = 100)]
+ public bool EventToMessageConversion_ToolAndOtherRoles_ReturnFalse(NonEmptyString textWrapper)
+ {
+ var text = textWrapper.Get;
+ if (string.IsNullOrWhiteSpace(text))
+ return true;
+
+ var toolRoles = new[] { Role.TOOL, Role.OTHER };
+
+ foreach (var role in toolRoles)
+ {
+ var evt = new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = role,
+ Content = new Content { Text = text }
+ }
+ }
+ ]
+ };
+
+ if (AgentCoreMemoryProvider.TryConvertEventToChatMessage(evt, out _))
+ return false;
+ }
+
+ return true;
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Property 2: Message Filtering Excludes Invalid Messages
+ // A message is persisted if and only if: (a) User or Assistant role,
+ // (b) no FunctionCallContent/FunctionResultContent, (c) non-null
+ // non-whitespace text with length >= 1.
+ // Validates: Requirements 2.3, 3.1, 3.2, 3.4
+ // ──────────────────────────────────────────────────────────────────
+
+ [Property(MaxTest = 100)]
+ public bool MessageFiltering_UserTextMessages_AreIncluded(NonEmptyString textWrapper, bool isUser)
+ {
+ var text = textWrapper.Get;
+ if (string.IsNullOrWhiteSpace(text))
+ return true; // vacuously true — whitespace messages should be excluded
+
+ var chatRole = isUser ? ChatRole.User : ChatRole.Assistant;
+ var message = new ChatMessage(chatRole, text);
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(
+ new[] { message }, null).ToList();
+
+ var expectedRole = isUser ? Role.USER : Role.ASSISTANT;
+
+ return result.Count == 1
+ && result[0].Role == expectedRole
+ && result[0].Text == text;
+ }
+
+ [Property(MaxTest = 100)]
+ public bool MessageFiltering_EmptyOrWhitespaceText_IsExcluded(byte whitespaceCount)
+ {
+ // Generate whitespace-only strings of various lengths
+ var text = new string(' ', whitespaceCount);
+ var message = new ChatMessage(ChatRole.User, text);
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(
+ new[] { message }, null).ToList();
+
+ return result.Count == 0;
+ }
+
+ [Property(MaxTest = 100)]
+ public bool MessageFiltering_ToolCallContent_IsExcluded(NonEmptyString textWrapper)
+ {
+ var text = textWrapper.Get;
+
+ // Message with FunctionCallContent should always be excluded
+ var message = new ChatMessage(ChatRole.Assistant,
+ [new FunctionCallContent("call-id", "FunctionName", new Dictionary { ["arg"] = text })]);
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(
+ new[] { message }, null).ToList();
+
+ return result.Count == 0;
+ }
+
+ [Property(MaxTest = 100)]
+ public bool MessageFiltering_ToolResultContent_IsExcluded(NonEmptyString textWrapper)
+ {
+ var text = textWrapper.Get;
+
+ // Message with FunctionResultContent should always be excluded
+ var message = new ChatMessage(ChatRole.Tool,
+ [new FunctionResultContent("call-id", text)]);
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(
+ new[] { message }, null).ToList();
+
+ return result.Count == 0;
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Property 3: Pagination Fetches All Pages in Order
+ // For any sequence of N paginated ListEvents responses (1..N-1 have
+ // NextToken, N does not), the provider makes exactly N API calls and
+ // returns all events concatenated in page order.
+ // Validates: Requirements 1.3, 10.1, 10.2
+ // ──────────────────────────────────────────────────────────────────
+
+ [Property(MaxTest = 100)]
+ public async Task Pagination_FetchesAllEventsInOrder(PositiveInt eventCountWrapper)
+ {
+ var eventCount = Math.Min(eventCountWrapper.Get, 20); // Cap for test performance
+
+ var events = Enumerable.Range(0, eventCount).Select(i => new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = Role.USER,
+ Content = new Content { Text = $"message-{i}" }
+ }
+ }
+ ]
+ }).ToList();
+
+ var mockPaginator = new Mock();
+ mockPaginator.Setup(p => p.Events).Returns(new TestPaginatedEnumerable(events));
+
+ var mockPaginatorFactory = new Mock();
+ mockPaginatorFactory.Setup(f => f.ListEvents(It.IsAny())).Returns(mockPaginator.Object);
+
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.Paginators).Returns(mockPaginatorFactory.Object);
+
+ var options = new AgentCoreOptions { MemoryId = "test-memory" };
+ var provider = new AgentCoreMemoryProvider(options, NullLogger.Instance, mockClient.Object);
+
+ var session = CreateSessionWithRuntimeContext("test-session");
+ var context = CreateInvokingContext(provider, session);
+
+ var result = await InvokeProvideChatHistoryAsync(provider, context);
+ var messages = result.ToList();
+
+ Assert.Equal(eventCount, messages.Count);
+
+ for (int i = 0; i < eventCount; i++)
+ {
+ Assert.Equal($"message-{i}", messages[i].Text);
+ }
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Property 4: Errors Never Propagate to Caller
+ // For any exception thrown by the Memory client during ListEvents or
+ // CreateEvent, the provider catches it and returns gracefully.
+ // Validates: Requirements 1.5, 2.4
+ // ──────────────────────────────────────────────────────────────────
+
+ [Property(MaxTest = 100)]
+ public async Task Errors_NeverPropagate_OnLoad(NonEmptyString exceptionMessage)
+ {
+ var mockPaginator = new Mock();
+ mockPaginator.Setup(p => p.Events).Returns(new ThrowingPaginatedEnumerable(new InvalidOperationException(exceptionMessage.Get)));
+
+ var mockPaginatorFactory = new Mock();
+ mockPaginatorFactory.Setup(f => f.ListEvents(It.IsAny())).Returns(mockPaginator.Object);
+
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.Paginators).Returns(mockPaginatorFactory.Object);
+
+ var options = new AgentCoreOptions { MemoryId = "test-memory" };
+ var provider = new AgentCoreMemoryProvider(options, NullLogger.Instance, mockClient.Object);
+
+ var session = CreateSessionWithRuntimeContext("test-session");
+ var context = CreateInvokingContext(provider, session);
+
+ // Should not throw — returns empty collection
+ var result = await InvokeProvideChatHistoryAsync(provider, context);
+ Assert.Empty(result);
+ }
+
+ [Property(MaxTest = 100)]
+ public async Task Errors_NeverPropagate_OnSave(NonEmptyString exceptionMessage, NonEmptyString messageText)
+ {
+ if (string.IsNullOrWhiteSpace(messageText.Get))
+ return;
+
+ var mockClient = new Mock();
+ mockClient
+ .Setup(c => c.CreateEventAsync(It.IsAny(), It.IsAny()))
+ .ThrowsAsync(new InvalidOperationException(exceptionMessage.Get));
+
+ var options = new AgentCoreOptions { MemoryId = "test-memory" };
+ var provider = new AgentCoreMemoryProvider(options, NullLogger.Instance, mockClient.Object);
+
+ var session = CreateSessionWithRuntimeContext("test-session");
+ var context = CreateInvokedContext(
+ provider,
+ session,
+ new[] { new ChatMessage(ChatRole.User, messageText.Get) },
+ new[] { new ChatMessage(ChatRole.Assistant, "response") });
+
+ // Should not throw
+ await InvokeStoreChatHistoryAsync(provider, context);
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Property 6: Partial Pagination Failure Returns Loaded Pages
+ // When an error occurs after some events have been loaded, the
+ // provider returns what was loaded and does not throw.
+ // Validates: Requirements 10.3
+ // ──────────────────────────────────────────────────────────────────
+
+ [Property(MaxTest = 100)]
+ public async Task PartialPaginationFailure_ReturnsLoadedEvents(PositiveInt successfulEventsWrapper)
+ {
+ var successfulEvents = Math.Min(successfulEventsWrapper.Get, 20); // Cap for performance
+
+ var mockPaginator = new Mock();
+ mockPaginator.Setup(p => p.Events).Returns(new EventsThenThrowPaginatedEnumerable(successfulEvents));
+
+ var mockPaginatorFactory = new Mock();
+ mockPaginatorFactory.Setup(f => f.ListEvents(It.IsAny())).Returns(mockPaginator.Object);
+
+ var mockClient = new Mock();
+ mockClient.Setup(c => c.Paginators).Returns(mockPaginatorFactory.Object);
+
+ var options = new AgentCoreOptions { MemoryId = "test-memory" };
+ var provider = new AgentCoreMemoryProvider(options, NullLogger.Instance, mockClient.Object);
+
+ var session = CreateSessionWithRuntimeContext("test-session");
+ var context = CreateInvokingContext(provider, session);
+
+ // Should not throw — returns partial results
+ var result = await InvokeProvideChatHistoryAsync(provider, context);
+ var messages = result.ToList();
+
+ // Should have exactly the messages from successful events
+ Assert.Equal(successfulEvents, messages.Count);
+ for (int i = 0; i < successfulEvents; i++)
+ {
+ Assert.Equal($"event-{i}", messages[i].Text);
+ }
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Helper methods
+ // ──────────────────────────────────────────────────────────────────
+
+ private static AgentSession CreateSessionWithRuntimeContext(string sessionId)
+ {
+ var session = new Mock() { CallBase = true }.Object;
+ var runtimeContext = new AgentCoreRuntimeContext
+ {
+ SessionId = sessionId,
+ RequestId = "test-request"
+ };
+ session.StateBag.SetValue(AgentCoreRuntimeContextProvider.ContextKey, runtimeContext);
+ return session;
+ }
+
+ private static ChatHistoryProvider.InvokingContext CreateInvokingContext(
+ AgentCoreMemoryProvider provider,
+ AgentSession session)
+ {
+ // The InvokingContext constructor requires (AIAgent, AgentSession, IEnumerable)
+ // We use a mock AIAgent since we only need the session for our tests
+ var mockAgent = new Mock() { CallBase = false };
+ return new ChatHistoryProvider.InvokingContext(
+ mockAgent.Object,
+ session,
+ new List());
+ }
+
+ private static ChatHistoryProvider.InvokedContext CreateInvokedContext(
+ AgentCoreMemoryProvider provider,
+ AgentSession session,
+ IEnumerable requestMessages,
+ IEnumerable responseMessages)
+ {
+ var mockAgent = new Mock() { CallBase = false };
+ return new ChatHistoryProvider.InvokedContext(
+ mockAgent.Object,
+ session,
+ requestMessages,
+ responseMessages);
+ }
+
+ private static async Task> InvokeProvideChatHistoryAsync(
+ AgentCoreMemoryProvider provider,
+ ChatHistoryProvider.InvokingContext context)
+ {
+ // ProvideChatHistoryAsync is protected, invoke via reflection
+ var method = typeof(AgentCoreMemoryProvider).GetMethod(
+ "ProvideChatHistoryAsync",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ var task = (ValueTask>)method!.Invoke(
+ provider, new object[] { context, CancellationToken.None })!;
+
+ return await task;
+ }
+
+ private static async Task InvokeStoreChatHistoryAsync(
+ AgentCoreMemoryProvider provider,
+ ChatHistoryProvider.InvokedContext context)
+ {
+ var method = typeof(AgentCoreMemoryProvider).GetMethod(
+ "StoreChatHistoryAsync",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ var task = (ValueTask)method!.Invoke(
+ provider, new object[] { context, CancellationToken.None })!;
+
+ await task;
+ }
+
+ /// Helper: wraps a list as IPaginatedEnumerable for mocking paginator.Events
+ private sealed class TestPaginatedEnumerable : Amazon.Runtime.IPaginatedEnumerable
+ {
+ private readonly IEnumerable _items;
+ public TestPaginatedEnumerable(IEnumerable items) => _items = items;
+ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken ct = default)
+ {
+ foreach (var item in _items)
+ {
+ await Task.CompletedTask;
+ yield return item;
+ }
+ }
+ }
+
+ /// Helper: IPaginatedEnumerable that throws immediately
+ private sealed class ThrowingPaginatedEnumerable : Amazon.Runtime.IPaginatedEnumerable
+ {
+ private readonly Exception _ex;
+ public ThrowingPaginatedEnumerable(Exception ex) => _ex = ex;
+#pragma warning disable CS0162
+ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken ct = default)
+ {
+ await Task.CompletedTask;
+ throw _ex;
+ yield break;
+ }
+#pragma warning restore CS0162
+ }
+
+ /// Helper: yields N events then throws
+ private sealed class EventsThenThrowPaginatedEnumerable : Amazon.Runtime.IPaginatedEnumerable
+ {
+ private readonly int _successfulCount;
+ public EventsThenThrowPaginatedEnumerable(int successfulCount) => _successfulCount = successfulCount;
+ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken ct = default)
+ {
+ for (int i = 0; i < _successfulCount; i++)
+ {
+ await Task.CompletedTask;
+ yield return new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = Role.USER,
+ Content = new Content { Text = $"event-{i}" }
+ }
+ }
+ ]
+ };
+ }
+
+ throw new AmazonBedrockAgentCoreException("Simulated pagination failure");
+ }
+ }
+}
diff --git a/test/AWS.AgentCore.UnitTests/AgentCoreMemoryProviderTests.cs b/test/AWS.AgentCore.UnitTests/AgentCoreMemoryProviderTests.cs
new file mode 100644
index 0000000..b249be2
--- /dev/null
+++ b/test/AWS.AgentCore.UnitTests/AgentCoreMemoryProviderTests.cs
@@ -0,0 +1,354 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+using Amazon.BedrockAgentCore;
+using Amazon.BedrockAgentCore.Model;
+using AWS.AgentCore.Extensions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+
+namespace AWS.AgentCore.UnitTests;
+
+[Collection("EnvironmentVariableTests")]
+public class AgentCoreMemoryProviderTests
+{
+ private static AgentCoreMemoryProvider CreateProvider(
+ AgentCoreOptions? options = null,
+ IAmazonBedrockAgentCore? memoryClient = null,
+ ILogger? logger = null)
+ {
+ return new AgentCoreMemoryProvider(
+ options ?? new AgentCoreOptions(),
+ logger ?? NullLogger.Instance,
+ memoryClient);
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Configuration tests
+ // ──────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void GetEffectiveMemoryId_WhenOptionsSet_ReturnsOptionsValue()
+ {
+ var provider = CreateProvider(new AgentCoreOptions { MemoryId = "mem-from-options" });
+
+ var result = provider.GetEffectiveMemoryId();
+
+ Assert.Equal("mem-from-options", result);
+ }
+
+ [Fact]
+ public void GetEffectiveMemoryId_WhenOptionsNotSet_FallsBackToEnvVar()
+ {
+ Environment.SetEnvironmentVariable(Constants.MemoryIdEnvironmentVariable, "mem-from-env");
+ try
+ {
+ var provider = CreateProvider(new AgentCoreOptions { MemoryId = null });
+
+ var result = provider.GetEffectiveMemoryId();
+
+ Assert.Equal("mem-from-env", result);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(Constants.MemoryIdEnvironmentVariable, null);
+ }
+ }
+
+ [Fact]
+ public void GetEffectiveMemoryId_WhenBothSet_OptionsWins()
+ {
+ Environment.SetEnvironmentVariable(Constants.MemoryIdEnvironmentVariable, "mem-from-env");
+ try
+ {
+ var provider = CreateProvider(new AgentCoreOptions { MemoryId = "mem-from-options" });
+
+ var result = provider.GetEffectiveMemoryId();
+
+ Assert.Equal("mem-from-options", result);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(Constants.MemoryIdEnvironmentVariable, null);
+ }
+ }
+
+ [Fact]
+ public void GetEffectiveMemoryId_WhenNeitherSet_ReturnsNull()
+ {
+ Environment.SetEnvironmentVariable(Constants.MemoryIdEnvironmentVariable, null);
+ var provider = CreateProvider(new AgentCoreOptions { MemoryId = null });
+
+ var result = provider.GetEffectiveMemoryId();
+
+ Assert.Null(result);
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Message filtering tests
+ // ──────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void FilterMessagesForStorage_SkipsToolCallMessages()
+ {
+ var messages = new List
+ {
+ new(ChatRole.Assistant, [new FunctionCallContent("call-1", "GetWeather", new Dictionary { ["location"] = "Seattle" })]),
+ };
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(messages, null).ToList();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void FilterMessagesForStorage_SkipsToolResultMessages()
+ {
+ var messages = new List
+ {
+ new(ChatRole.Tool, [new FunctionResultContent("call-1", "result data")]),
+ };
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(messages, null).ToList();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void FilterMessagesForStorage_SkipsEmptyTextMessages()
+ {
+ var messages = new List
+ {
+ new(ChatRole.User, ""),
+ new(ChatRole.User, " "),
+ };
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(messages, null).ToList();
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void FilterMessagesForStorage_SavesUserAndAssistantMessages()
+ {
+ var requestMessages = new List
+ {
+ new(ChatRole.User, "Hello"),
+ };
+ var responseMessages = new List
+ {
+ new(ChatRole.Assistant, "Hi there!"),
+ };
+
+ var result = AgentCoreMemoryProvider.FilterMessagesForStorage(requestMessages, responseMessages).ToList();
+
+ Assert.Equal(2, result.Count);
+ Assert.Equal(Amazon.BedrockAgentCore.Role.USER, result[0].Role);
+ Assert.Equal("Hello", result[0].Text);
+ Assert.Equal(Amazon.BedrockAgentCore.Role.ASSISTANT, result[1].Role);
+ Assert.Equal("Hi there!", result[1].Text);
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Event conversion tests
+ // ──────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void TryConvertEventToChatMessage_ValidUserEvent_ReturnsUserMessage()
+ {
+ var evt = new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = Amazon.BedrockAgentCore.Role.USER,
+ Content = new Content { Text = "Hello" }
+ }
+ }
+ ]
+ };
+
+ var success = AgentCoreMemoryProvider.TryConvertEventToChatMessage(evt, out var message);
+
+ Assert.True(success);
+ Assert.Equal(ChatRole.User, message.Role);
+ Assert.Equal("Hello", message.Text);
+ }
+
+ [Fact]
+ public void TryConvertEventToChatMessage_ValidAssistantEvent_ReturnsAssistantMessage()
+ {
+ var evt = new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = Amazon.BedrockAgentCore.Role.ASSISTANT,
+ Content = new Content { Text = "Hi there!" }
+ }
+ }
+ ]
+ };
+
+ var success = AgentCoreMemoryProvider.TryConvertEventToChatMessage(evt, out var message);
+
+ Assert.True(success);
+ Assert.Equal(ChatRole.Assistant, message.Role);
+ Assert.Equal("Hi there!", message.Text);
+ }
+
+ [Fact]
+ public void TryConvertEventToChatMessage_ToolRoleEvent_ReturnsFalse()
+ {
+ var evt = new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = Amazon.BedrockAgentCore.Role.TOOL,
+ Content = new Content { Text = "tool result" }
+ }
+ }
+ ]
+ };
+
+ var success = AgentCoreMemoryProvider.TryConvertEventToChatMessage(evt, out _);
+
+ Assert.False(success);
+ }
+
+ [Fact]
+ public void TryConvertEventToChatMessage_EmptyPayload_ReturnsFalse()
+ {
+ var evt = new Event { Payload = [] };
+
+ var success = AgentCoreMemoryProvider.TryConvertEventToChatMessage(evt, out _);
+
+ Assert.False(success);
+ }
+
+ [Fact]
+ public void TryConvertEventToChatMessage_EmptyText_ReturnsFalse()
+ {
+ #pragma warning disable BedrockAgentCore1000 // SDK validation warning for intentionally invalid test input
+ var evt = new Event
+ {
+ Payload =
+ [
+ new PayloadType
+ {
+ Conversational = new Conversational
+ {
+ Role = Amazon.BedrockAgentCore.Role.USER,
+ Content = new Content { Text = "" }
+ }
+ }
+ ]
+ };
+ #pragma warning restore BedrockAgentCore1000
+
+ var success = AgentCoreMemoryProvider.TryConvertEventToChatMessage(evt, out _);
+
+ Assert.False(success);
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // HasToolContent tests
+ // ──────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void HasToolContent_WithFunctionCallContent_ReturnsTrue()
+ {
+ var message = new ChatMessage(ChatRole.Assistant,
+ [new FunctionCallContent("call-1", "GetWeather", new Dictionary { ["loc"] = "NYC" })]);
+
+ Assert.True(AgentCoreMemoryProvider.HasToolContent(message));
+ }
+
+ [Fact]
+ public void HasToolContent_WithFunctionResultContent_ReturnsTrue()
+ {
+ var message = new ChatMessage(ChatRole.Tool,
+ [new FunctionResultContent("call-1", "sunny")]);
+
+ Assert.True(AgentCoreMemoryProvider.HasToolContent(message));
+ }
+
+ [Fact]
+ public void HasToolContent_WithTextOnly_ReturnsFalse()
+ {
+ var message = new ChatMessage(ChatRole.User, "Hello");
+
+ Assert.False(AgentCoreMemoryProvider.HasToolContent(message));
+ }
+}
+
+public class AgentCoreMemoryDIRegistrationTests
+{
+ [Fact]
+ public void AddAgentCore_RegistersMemoryProvider()
+ {
+ var builder = WebApplication.CreateBuilder();
+ var mockClient = new Mock();
+
+ builder.AddAgentCore(options =>
+ {
+ options.ChatClient = mockClient.Object;
+ });
+
+ var sp = builder.Build().Services;
+ var memoryProvider = sp.GetService();
+
+ Assert.NotNull(memoryProvider);
+ }
+
+ [Fact]
+ public void AddAgentCore_RegistersIAmazonBedrockAgentCore()
+ {
+ var builder = WebApplication.CreateBuilder();
+ var mockClient = new Mock();
+
+ builder.AddAgentCore(options =>
+ {
+ options.ChatClient = mockClient.Object;
+ });
+
+ var sp = builder.Build().Services;
+ // TryAddAWSService registers it — it should be resolvable (may fail at runtime without credentials, but the registration exists)
+ var descriptor = builder.Services.FirstOrDefault(d => d.ServiceType == typeof(IAmazonBedrockAgentCore));
+ Assert.NotNull(descriptor);
+ }
+
+ [Fact]
+ public void AddAgentCore_WiresMemoryProviderAsChatHistoryProvider()
+ {
+ var builder = WebApplication.CreateBuilder();
+ var mockClient = new Mock();
+
+ builder.AddAgentCore(options =>
+ {
+ options.ChatClient = mockClient.Object;
+ });
+
+ var sp = builder.Build().Services;
+ var agent = sp.GetRequiredService();
+
+ // The agent should be a ChatClientAgent with the memory provider wired
+ Assert.IsType(agent);
+ var chatAgent = (Microsoft.Agents.AI.ChatClientAgent)agent;
+ Assert.IsType(chatAgent.ChatHistoryProvider);
+ }
+}