![[Update] Tried Using Strictly Consistent Metadata with AgentCore Memory Long-Term Memory](https://images.ctfassets.net/ct0aopd36mqt/7M0d5bjsd0K4Et30cVFvB6/5b2095750cc8bf73f04f63ed0d4b3546/AgentCore2.png?w=3840&fm=webp)
[Update] Tried Using Strictly Consistent Metadata with AgentCore Memory Long-Term Memory
This page has been translated by machine translation. View original
Introduction
Hello, I'm Jinno from the Consulting Department, a big fan of supermarkets.
Previously, I wrote an article about using custom metadata filters with AgentCore Memory's long-term memory.
In the previous article, I introduced how to automatically extract metadata from event conversation content using LLM extraction (LLM_INFERRED). However, LLM extraction is inherently non-deterministic and was considered unsuitable for assigning strict values.
That's why today's update added Strictly Consistent metadata to AgentCore Memory.
This is a feature that allows the application side to directly specify metadata values in short-term memory, and those exact values are reflected in long-term memory records without going through an LLM. The What's New announcement lists department-scoped search, compliance boundaries, and multi-tenant memory implementations as use cases.
This time, while touching on the differences from the previous article, I'll actually try out STRICTLY_CONSISTENT metadata!
Prerequisites
| Item | Version/Value |
|---|---|
| Python | 3.13 |
| boto3 | 1.43.29 |
| botocore | 1.43.29 |
| AWS Region | us-east-1 |
| Dependency management | uv |
Create a project with uv and install boto3.
uv init
uv add boto3
All subsequent code is executed with uv run.
STRICTLY_CONSISTENT Metadata
Differences from LLM_INFERRED
The LLM_INFERRED approach introduced in the previous article was a mechanism where an LLM analyzes the event's conversation content to extract metadata values. For example, it would infer destination = kyoto from a conversation like "I want to travel to Kyoto."
With the newly added STRICTLY_CONSISTENT, the application directly specifies values via the metadata parameter when creating short-term memory events, and those values are reflected as-is in long-term memory records even after the extraction and consolidation process. Since no LLM inference is involved, it is guaranteed that the values set in short-term memory will be delivered as-is to long-term memory.
| Item | LLM_INFERRED | STRICTLY_CONSISTENT |
|---|---|---|
| Value determination | LLM infers from conversation content | Application specifies directly |
| Determinism | Non-deterministic (depends on LLM interpretation) | Deterministic (specified value is reflected as-is) |
| Setting limit | Within metadata schema limit (max 20 entries) | Maximum 3 keys |
| Supported types | STRING / STRINGLIST / NUMBER | STRING only |
| Consolidation behavior | Semantically similar records may be consolidated | Records with different values are separated from consolidation targets |
| Use cases | Content classification / topic extraction | Department scoping / compliance boundaries / multi-tenant memory |
Let me add a brief note about "Consolidation behavior" in the table. AgentCore Memory's long-term memory has a consolidation process that merges multiple records into one. For example, if a user had conversations like "The invoice amount is wrong" and "I want to change the billing payment method" in different sessions, these might be consolidated into one record since they are semantically close.
When STRICTLY_CONSISTENT is set, this consolidation is separated by value groups. Events with the same department=sales are extracted and consolidated together, but records with department=sales and records with department=engineering are treated as separate groups even if their content is similar, and their consolidation targets are separated.
The What's New announcement lists multi-tenant memory and compliance boundaries as use cases, but the official documentation's best practices warn against "relying solely on metadata for tenant isolation." It seems best to use namespaces for tenant isolation and combine metadata for filtering within them. I'd like to dig deeper into this.
Supported Strategies
STRICTLY_CONSISTENT is available with the following strategies.
- Semantic Memory Strategy
- User Preference Memory Strategy
- Episodic Memory Strategy
Note that Summarization Strategy is not supported.
Let's Try It
Creating a Memory
This time, I'll set department (department) as STRICTLY_CONSISTENT and topic (topic category) as LLM_INFERRED, configuring both extraction types in a single Memory. Assuming a customer support scenario where the same customer contacts multiple departments, I'll verify that filtering by department works within the same namespace.
import boto3
control = boto3.client("bedrock-agentcore-control", region_name="us-east-1")
response = control.create_memory(
name="support_memory_sc",
description="Multi-department support memory with strictly consistent metadata",
eventExpiryDuration=30,
memoryStrategies=[
{
"semanticMemoryStrategy": {
"name": "semantic_strategy",
"namespaceTemplates": ["support/{actorId}/facts"],
"memoryRecordSchema": {
"metadataSchema": [
{
"key": "department",
"type": "STRING",
"extractionType": "STRICTLY_CONSISTENT",
},
{
"key": "topic",
"type": "STRING",
"extractionType": "LLM_INFERRED",
"extractionConfig": {
"llmExtractionConfig": {
"definition": "The support topic category discussed in the conversation",
"llmExtractionInstruction": "LATEST_VALUE",
"validation": {
"stringValidation": {
"allowedValues": [
"billing",
"technical",
"account",
"general",
]
}
},
}
},
},
]
},
}
}
],
indexedKeys=[
{"key": "department", "type": "STRING"},
{"key": "topic", "type": "STRING"},
],
)
memory_id = response["memory"]["id"]
print(f"Memory ID: {memory_id}")
For the department key, STRICTLY_CONSISTENT is specified in extractionType. extractionConfig is not needed, and the type is limited to STRING. On the other hand, the topic key uses LLM_INFERRED as before, with definition and allowedValues configured.
In the previous article, even values that should be determined deterministically, like department, were extracted by the LLM, so this is a significant difference. You can clearly separate values that should be controlled by the application from values you want the LLM to infer.
Submitting Events
Let's submit events with different metadata values per department in a scenario where the same customer (actorId=customer-001) contacts different departments.
import boto3
from datetime import datetime, timezone
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
def create_support_event(session_id, department, messages):
payload = []
for role, text in messages:
payload.append(
{"conversational": {"role": role, "content": {"text": text}}}
)
response = client.create_event(
memoryId=memory_id,
actorId="customer-001",
sessionId=session_id,
eventTimestamp=datetime.now(timezone.utc),
payload=payload,
metadata={
"department": {"stringValue": department},
},
)
return response
# Inquiry to sales department (billing-related)
create_support_event(
session_id="session-001",
department="sales",
messages=[
("USER", "It seems the amount on last month's invoice is incorrect. Could you check it?"),
("ASSISTANT", "Certainly. Could you please provide the invoice number?"),
("USER", "It's INV-2026-0542. It should be 500,000 yen but it shows 550,000 yen."),
("ASSISTANT", "I've confirmed. The extra 50,000 yen is from an unpaid balance from the previous month that was added."),
],
)
# Inquiry to engineering department (technical-related)
create_support_event(
session_id="session-002",
department="engineering",
messages=[
("USER", "The API response in the production environment has suddenly become slow."),
("ASSISTANT", "Which endpoint is experiencing the delay?"),
("USER", "The /api/v2/reports endpoint. It normally takes 200ms but is now taking over 5 seconds."),
("ASSISTANT", "The database connection pool may be exhausted. Please check the connection count."),
],
)
# Another inquiry to sales department (account-related)
create_support_event(
session_id="session-003",
department="sales",
messages=[
("USER", "I'd like to create a new account. Could you tell me the procedure?"),
("ASSISTANT", "Please select 'Create New Account' from the admin screen and fill in the required information. You'll receive an email after approval."),
],
)
print("Events created successfully!")
The value is passed directly with stringValue for the department key in the metadata parameter. This value is reflected as-is in the long-term memory record as STRICTLY_CONSISTENT, without going through LLM inference.
On the other hand, topic is automatically inferred by the LLM from the conversation content. If it's about billing, it becomes billing; if it's about API latency, technical; if it's about account creation, account.
Checking Long-Term Memory Records
After submitting events, wait a while for the extraction job to run and long-term memory records to be generated. Let's check all records within the same namespace.
import boto3
import json
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
response = client.list_memory_records(
memoryId=memory_id,
namespace="support/customer-001/facts",
)
for record in response.get("memoryRecordSummaries", []):
metadata = record.get("metadata", {})
dept = metadata.get("department", {}).get("stringValue", "N/A")
topic = metadata.get("topic", {}).get("stringValue", "N/A")
print(f"Content: {record['content']}")
print(f"department={dept}, topic={topic}")
print("---")
Content: {'text': 'The API response of the /api/v2/reports endpoint in the production environment has suddenly become slow, taking over 5 seconds instead of the usual 200ms.'}
department=engineering, topic=technical
---
Content: {'text': 'The invoice (INV-2026-0542) for May 2026 showed 550,000 yen instead of the expected 500,000 yen, and the extra 50,000 yen was explained as an unpaid balance from the previous month.'}
department=sales, topic=billing
---
Content: {'text': 'The user recognized that the original amount of invoice INV-2026-0542 was 500,000 yen.'}
department=sales, topic=billing
---
Content: {'text': 'The user wants to create a new account.'}
department=sales, topic=account
---
Records with department=sales and department=engineering coexist within the same namespace! The department reflects the values passed by the application as-is, and topic is correctly inferred by the LLM from the conversation content.
In the previous article, values that should be determined deterministically (such as department names or tenant attributes) were also extracted by the LLM, which carried the risk of values fluctuating or becoming unintended values. With STRICTLY_CONSISTENT, the values specified by the application are entered directly, so you can rest assured.
Searching with Metadata Filters
STRICTLY_CONSISTENT metadata can also be used as a filter. Let's try narrowing down the search by department within the same namespace.
import boto3
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
namespace = "support/customer-001/facts"
# Search only sales department records
response = client.retrieve_memory_records(
memoryId=memory_id,
namespace=namespace,
searchCriteria={
"searchQuery": "inquiry content",
"topK": 10,
"metadataFilters": [
{
"left": {"metadataKey": "department"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "sales"}},
}
],
},
)
print("=== Filter by department=sales ===")
for record in response.get("memoryRecordSummaries", []):
metadata = record.get("metadata", {})
dept = metadata.get("department", {}).get("stringValue", "N/A")
topic = metadata.get("topic", {}).get("stringValue", "N/A")
print(f"Score: {record['score']}")
print(f"Content: {record['content']}")
print(f"department={dept}, topic={topic}")
print("---")
# Search only engineering department records (specifying the same namespace)
response = client.retrieve_memory_records(
memoryId=memory_id,
namespace=namespace,
searchCriteria={
"searchQuery": "inquiry content",
"topK": 10,
"metadataFilters": [
{
"left": {"metadataKey": "department"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "engineering"}},
}
],
},
)
print("=== Filter by department=engineering ===")
for record in response.get("memoryRecordSummaries", []):
metadata = record.get("metadata", {})
dept = metadata.get("department", {}).get("stringValue", "N/A")
topic = metadata.get("topic", {}).get("stringValue", "N/A")
print(f"Score: {record['score']}")
print(f"Content: {record['content']}")
print(f"department={dept}, topic={topic}")
print("---")
metadataFilters is specified inside searchCriteria. Note that while it is at the top level in ListMemoryRecords, it is placed inside searchCriteria in RetrieveMemoryRecords.
Even though the same namespace is specified, records are correctly narrowed down by department value. The actual execution results are as follows.
=== Filter by department=sales ===
Score: 0.3785945
Content: {'text': 'The user recognized that the original amount of invoice INV-2026-0542 was 500,000 yen.'}
department=sales, topic=billing
Score: 0.3695521
Content: {'text': 'The invoice (INV-2026-0542) for May 2026 showed 550,000 yen instead of the expected 500,000 yen, and the extra 50,000 yen was explained as an unpaid balance from the previous month.'}
department=sales, topic=billing
Score: 0.36409447
Content: {'text': 'The user wants to create a new account.'}
department=sales, topic=account
=== Filter by department=engineering ===
Score: 0.35761523
Content: {'text': 'The API response of the /api/v2/reports endpoint in the production environment has suddenly become slow, taking over 5 seconds instead of the usual 200ms.'}
department=engineering, topic=technical
The 4 records within the same namespace are correctly separated and returned by department value! Filtering by sales returns 3 records for billing and account, and filtering by engineering returns only 1 record for technical.
Filtering with Both Extraction Types Combined
You can also filter by combining STRICTLY_CONSISTENT and LLM_INFERRED metadata.
import boto3
import json
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
# Search for records in sales department AND billing topic
response = client.retrieve_memory_records(
memoryId=memory_id,
namespace="support/customer-001/facts",
searchCriteria={
"searchQuery": "inquiry content",
"topK": 10,
"metadataFilters": [
{
"left": {"metadataKey": "department"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "sales"}},
},
{
"left": {"metadataKey": "topic"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "billing"}},
},
],
},
)
print("=== Sales department × billing topic ===")
for record in response.get("memoryRecordSummaries", []):
print(f"Content: {record['content']}")
print(f"Metadata: {json.dumps(record.get('metadata', {}), ensure_ascii=False, default=str)}")
print("---")
department can be reliably filtered with the value specified by the application, while topic can be filtered by the classification the LLM determined from the conversation content. It's great to be able to combine axes you want to control deterministically with axes you want to leave to the LLM.
Comparison with the Previous Article
Let me compare the LLM_INFERRED-only approach introduced in the previous article with the approach using STRICTLY_CONSISTENT this time.
Issues from the Previous Article and Solutions This Time
In the previous article, LLM extraction had the following issues.
- If allowedValues are too restrictive, the LLM forcibly selects from the allowed values even for unrelated conversations
- Without validation, spelling variations occur (tokyo / Tokyo / 東京, etc.)
- Values that should be determined deterministically, like department codes, also had to be left to the LLM
STRICTLY_CONSISTENT resolves these issues, particularly the third one. Since the application directly controls the values, they are not subject to the LLM's whims.
Guidelines for Usage
| Nature of value | Recommended extractionType |
|---|---|
| Department codes, tenant attributes, compliance levels | STRICTLY_CONSISTENT |
| Region, environment names (prod/stg/dev) | STRICTLY_CONSISTENT |
| Conversation topics, categories | LLM_INFERRED |
| User sentiment, satisfaction | LLM_INFERRED |
The rule is simple: use STRICTLY_CONSISTENT for values that are determined on the application side, and LLM_INFERRED for values that need to be inferred from conversation content.
Configuration Comparison at Memory Creation
In the previous article, values equivalent to department were all set with LLM extraction.
{
"key": "department",
"type": "STRING",
- "extractionConfig": {
- "llmExtractionConfig": {
- "definition": "The department this conversation belongs to",
- "llmExtractionInstruction": "LATEST_VALUE",
- "validation": {
- "stringValidation": {
- "allowedValues": ["sales", "engineering", "hr", "unknown"]
- }
- },
- }
- },
+ "extractionType": "STRICTLY_CONSISTENT",
},
Switching to STRICTLY_CONSISTENT eliminates the need for extractionConfig, making the configuration simpler as well. There's no need to ask the LLM to "infer the department"; instead, you pass the value directly via the metadata parameter when submitting events.
Constraints
STRICTLY_CONSISTENT has several constraints.
| Constraint | Details |
|---|---|
| Maximum keys per strategy | 3 |
| Supported types | STRING only |
| Declaration in indexedKeys | Required |
| extractionConfig | Cannot be specified (value is obtained from the event) |
| Supported strategies | Semantic / User Preference / Episodic (including custom overrides). Summary is not supported |
| When value is not specified | That key is omitted from the record |
Since there is a limit of 3 keys, you need to limit usage to keys where truly deterministic control is required. For other classifications and categorizations, it seems best to leave them to LLM_INFERRED.
The official documentation contains details on constraints and quotas.
Conclusion
Since having values that should be determined deterministically, like department codes and compliance levels, depend on LLM inference results carries some risk, it's great that we can now set values directly from the application side into short-term memory and have them carried over to long-term memory as-is. As mentioned in the What's New announcement with multi-tenant memory and compliance boundaries as use cases, combining namespace-based subject isolation with STRICTLY_CONSISTENT metadata scoping enables more practical memory design.
I hope this article is helpful in some way. Thank you for reading to the end!