Skip to content

Eliminate Redundant API Calls in AgentCoreMemorySessionManager #222

@kevmyung

Description

@kevmyung

Problem Statement

AgentCoreMemorySessionManager saves each message with 2 separate API calls: one for the message, one for agent state. These capture the same point-in-time snapshot and could be batched into 1 call, but the current actorId design prevents this.

Simple conversation (5 API calls):

User sends "Hello"
    → MessageAddedEvent fires
    → append_message()  → saves message      ← API call #1
    → sync_agent()      → saves agent state  ← API call #2

Agent responds "Hi there!"
    → MessageAddedEvent fires
    → append_message()  → saves message      ← API call #3
    → sync_agent()      → saves agent state  ← API call #4

    → AfterInvocationEvent fires
    → sync_agent()      → saves agent state  ← API call #5

With tool usage (9 API calls):

User asks "What time is it?"
    → MessageAddedEvent fires
    → append_message()  → saves message      ← API call #1
    → sync_agent()      → saves agent state  ← API call #2

Agent calls get_time tool
    → MessageAddedEvent fires (tool_use)
    → append_message()  → saves message      ← API call #3
    → sync_agent()      → saves agent state  ← API call #4

Tool returns result
    → MessageAddedEvent fires (tool_result)
    → append_message()  → saves message      ← API call #5
    → sync_agent()      → saves agent state  ← API call #6

Agent responds "It's 3:00 PM"
    → MessageAddedEvent fires
    → append_message()  → saves message      ← API call #7
    → sync_agent()      → saves agent state  ← API call #8

    → AfterInvocationEvent fires
    → sync_agent()      → saves agent state  ← API call #9

Each additional tool call adds 4 more API calls (tool_use message + state, tool_result message + state).

Why this matters:

Each API call adds ~100-300ms of network latency:

Scenario API Calls Unnecessary Latency
Simple conversation 5 ~800ms
1 tool call 9 ~1.5s
2 tool calls 13 ~2s

Why batching isn't possible today:

The create_event() API can batch multiple payloads, but only if they share the same actorId. The current implementation uses different actorIds for different data types:

create_event(actorId="session_abc123", ...)   # Session metadata
create_event(actorId="agent_strands", ...)    # Agent state
create_event(actorId="user_123", ...)         # Messages

From my understanding, AgentCore Memory is designed around (actorId, sessionId) pairs, where actorId represents a user and sessionId represents a conversation. Storing different parts of the same conversation under different actorIds breaks this model.

Proposed Solution

1. Use a single actorId for all conversation data

Store everything under config.actor_id (the user ID) and distinguish data types using payload markers:

create_event(
    actorId=config.actor_id,      # Always the user ID
    sessionId=session_id,
    payload=[
        {"_type": "message", "role": "user", "content": [...]},
        {"_type": "agent_state", "_agent_id": "strands-agent", "state": {...}}
    ]
)

Key difference: save_message_with_state() batches message + agent state into a single API call, using unified actorId with _type markers to distinguish payload types.

Simple conversation (3 API calls):

User sends "Hello"                                                                                                                       
    → MessageAddedEvent fires                                                                                                            
    → save_message_with_state() → saves message + agent state  ← API call #1                                                             
                                                                                                                                         
Agent responds "Hi there!"                                                                                                               
    → MessageAddedEvent fires                                                                                                            
    → save_message_with_state() → saves message + agent state  ← API call #2                                                             
                                                                                                                                         
    → AfterInvocationEvent fires                                                                                                         
    → _sync_agent_state()       → saves agent state if changed            ← API call #3    

With tool usage (5 API calls):

User asks "What time is it?"                                                                                                             
    → MessageAddedEvent fires                                                                                                            
    → save_message_with_state() → saves message + agent state  ← API call #1                                                             
                                                                                                                                         
Agent calls get_time tool                                                                                                                
    → MessageAddedEvent fires (tool_use)                                                                                                 
    → save_message_with_state() → saves message + agent state  ← API call #2                                                             
                                                                                                                                         
Tool returns result                                                                                                                      
    → MessageAddedEvent fires (tool_result)                                                                                              
    → save_message_with_state() → saves message + agent state  ← API call #3                                                             
                                                                                                                                         
Agent responds "It's 3:00 PM"                                                                                                            
    → MessageAddedEvent fires                                                                                                            
    → save_message_with_state() → saves message + agent state  ← API call #4                                                             
                                                                                                                                         
    → AfterInvocationEvent fires                                                                                                         
    → _sync_agent_state()       → saves agent state if changed           ← API call #5      

2. Merge redundant callbacks

# Current: 2 callbacks = 2 API calls per message
registry.add_callback(MessageAddedEvent, lambda e: self.append_message(...))
registry.add_callback(MessageAddedEvent, lambda e: self.sync_agent(...))

# Proposed: 1 callback = 1 API call per message
registry.add_callback(MessageAddedEvent, lambda e: self.save_message_with_state(...))

3. Backward compatibility

AgentCoreMemorySessionManager(
    config=config,
    storage_version="v1",  # Default: current behavior
    # storage_version="v2",  # New: unified actorId, batched calls
)

(Optional) Support dual-read (try v2 first, fall back to v1) for gradual migration.

Additional context

Expected results:

Scenario Before After Reduction
Simple conversation 5 calls 2 calls 60%
1 tool call 9 calls 4 calls 56%

Compatibility:

  • Multi-agent support preserved via _agent_id field in payload
  • Long-term memory strategies (Summary, Semantic, etc.) remain compatible since messages already use config.actor_id
  • (Optional) Dual-read pattern ensures existing sessions continue to work

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions