feat(strands-memory): add event metadata support to AgentCoreMemorySessionManager#339
feat(strands-memory): add event metadata support to AgentCoreMemorySessionManager#339
Conversation
…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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"}, |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
what happens when we flush messages in a batch and the metadata is different on each message? Does all the metadata get merged?
Summary
Adds user-supplied event metadata support to
AgentCoreMemorySessionManager(Phase 1 of #149).Static metadata:
default_metadatafield onAgentCoreMemoryConfig— attaches custom key-value metadata to every message eventDynamic metadata (for traceId / Langfuse integration):
metadata_providerfield — a callable invoked at each event creation, so it can return per-invocation values (e.g. current traceId). This is needed because Strands controls theappend_message→create_messagecall path, so users can't pass per-call kwargs throughagent().default_metadata<metadata_provider()< per-callmetadatakwarg < internal keysInfrastructure:
_build_metadata()helper with validation: rejects reserved keys (stateType,agentId), enforces 15-key API limit_message_bufferfrom raw tuple toBufferedMessageNamedTuple for clarity and extensibilityUsage example (Langfuse traceId)
Test plan
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)BufferedMessagemigration)Related