Skip to content

feat(strands-memory): add event metadata support to AgentCoreMemorySessionManager#339

Open
tejaskash wants to merge 2 commits intomainfrom
worktree-pr1-metadata-support
Open

feat(strands-memory): add event metadata support to AgentCoreMemorySessionManager#339
tejaskash wants to merge 2 commits intomainfrom
worktree-pr1-metadata-support

Conversation

@tejaskash
Copy link
Contributor

@tejaskash tejaskash commented Mar 13, 2026

Summary

Adds user-supplied event metadata support to AgentCoreMemorySessionManager (Phase 1 of #149).

Static metadata:

  • New default_metadata field on AgentCoreMemoryConfig — attaches custom key-value metadata to every message event

Dynamic metadata (for traceId / Langfuse integration):

  • New metadata_provider field — a callable invoked at each event creation, so it can return per-invocation values (e.g. current traceId). This is needed because Strands controls the append_messagecreate_message call path, so users can't pass per-call kwargs through agent().
  • Merge precedence: default_metadata < metadata_provider() < per-call metadata kwarg < internal keys

Infrastructure:

  • _build_metadata() helper with validation: rejects reserved keys (stateType, agentId), enforces 15-key API limit
  • Refactors internal _message_buffer from raw tuple to BufferedMessage NamedTuple for clarity and extensibility
  • Metadata flows through both immediate-send and batched flush paths

Usage example (Langfuse traceId)

from langfuse.decorators import langfuse_context

config = AgentCoreMemoryConfig(
    memory_id=MEM_ID,
    session_id=SESSION_ID,
    actor_id=ACTOR_ID,
    metadata_provider=lambda: {
        "traceId": {"stringValue": langfuse_context.get_current_trace_id() or ""}
    },
)
sm = AgentCoreMemorySessionManager(agentcore_memory_config=config, region_name="us-east-1")
agent = Agent(session_manager=sm)
agent("Hello!")  # Event gets the current traceId automatically

Test plan

  • 11 new unit tests in TestMetadataSupport (default metadata, per-call, merge precedence, reserved keys, max keys, no-metadata, batched, blob, provider called per event, provider merge with defaults, provider reserved keys rejected)
  • 3 new integration tests with positive/negative filter assertions (metadata round-trip, session resume, dynamic traceId with disjoint event sets)
  • 123 existing unit tests pass unchanged (buffer tuple → BufferedMessage migration)
  • Full suite: 1098 tests pass, 0 failures

Related

…ntCoreMemorySessionManager

Allow users to attach custom key-value metadata to conversation events
via a new `default_metadata` config field and per-call `metadata` kwarg.
Metadata is merged (per-call > config defaults > internal) and validated
against reserved keys and the 15-key API limit.

Also refactors the internal message buffer from a raw tuple to a
`BufferedMessage` NamedTuple for clarity and extensibility.

Closes #149 (Phase 1: Metadata)
Default is "user_context".
filter_restored_tool_context: When True, strip historical toolUse/toolResult blocks from
restored messages before loading them into Strands runtime memory. Default is False.
default_metadata: Optional default metadata key-value pairs to attach to every message event.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this actually solve the customer's ask?
What if they need different metadata for each message event? Also, what exactly do they mean by "message_event" — are they
  referring to memory records, AgentCore Memory events, or individual conversation turns? Are they trying to attach a distinct metadata field to each conversation turn?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure this is the interface the customer is looking for? Could we ask them to send an example code block of the support they want?

…n metadata

Add `metadata_provider` config field — a callable invoked at each event
creation, enabling dynamic metadata like traceId that changes per
agent invocation. This solves the Langfuse/user-feedback use case where
a static `default_metadata` is insufficient because Strands controls
the append_message → create_message call path.

Merge precedence: default_metadata < metadata_provider() < per-call kwargs < internal keys.
session_id=SESSION_ID,
actor_id=ACTOR_ID,
default_metadata={
"project": {"stringValue": "atlas"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would we be able to build this map on behalf of the customer? It feels very verbose.

Ex:
{"project" : "atlas"} --> {"project" : { "stringValue": "atlas"}}

RESERVED_METADATA_KEYS = frozenset({STATE_TYPE_KEY, AGENT_ID_KEY})


class BufferedMessage(NamedTuple):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, I agree with the decision to add some structure here.


def test_metadata_reserved_keys_rejected(self, session_manager):
"""ValueError raised when user metadata contains reserved keys."""
from bedrock_agentcore.memory.integrations.strands.session_manager import RESERVED_METADATA_KEYS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: personally still new to python, but in most languages I'm used to seeing imports at the top unless we have a strong reason not to. lmk if python convention is different.

Default is "user_context".
filter_restored_tool_context: When True, strip historical toolUse/toolResult blocks from
restored messages before loading them into Strands runtime memory. Default is False.
default_metadata: Optional default metadata key-value pairs to attach to every message event.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure this is the interface the customer is looking for? Could we ask them to send an example code block of the support they want?

flush_interval_seconds: Optional[float] = Field(default=None, gt=0)
context_tag: str = Field(default="user_context", min_length=1)
filter_restored_tool_context: bool = Field(default=False)
default_metadata: Optional[Dict[str, Any]] = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we need Any here instead of the MetadataValue used internally?

with `default_metadata` and `metadata_provider` (per-call values override both for the same key):

```python
session_manager.create_message(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when we flush messages in a batch and the metadata is different on each message? Does all the metadata get merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants