Skip to content

Enable server-side metadata filtering on AgentCore Memory records and drop summary strategy - #4

Open
tverney wants to merge 1 commit into
mainfrom
feat/indexed-metadata-keys
Open

Enable server-side metadata filtering on AgentCore Memory records and drop summary strategy#4
tverney wants to merge 1 commit into
mainfrom
feat/indexed-metadata-keys

Conversation

@tverney

@tverney tverney commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

Garden sections were saved with custom metadata that couldn't filter retrieval, so "which beds have basil?" fell back to similarity search over prose. This makes those dimensions actually queryable.

  • Declare IndexedKeys (type, section, plants) on the memory resource. Only indexed keys can appear in a metadataFilters expression.
  • Push filters server-side in retrieve(). AgentCore applies them before the vector search, so they narrow the candidate set instead of trimming top-K results.
  • Declare a MetadataSchema per strategy so extracted records carry the same dimensions the app writes.
  • Attach metadata on CreateEvent for values known at event time (a captioned album's section), so extraction propagates.
  • Remove SummaryMemoryStrategy — It produced nothing in weeks of real use, not really necessary for this use case.

Worth noting

  1. Custom metadata keys are only filterable if declared as IndexedKeys; otherwise you get ValidationException: not a valid filter key. Indexed keys are additive-only, capped at 10, and can never be removed — hence only three.
  2. namespace matches the exact namespace, not a prefix (the API docs imply otherwise). Anything nested deeper is silently omitted, so retrieval uses namespacePath.
  3. Record indexing takes ~60–75s. An immediate post-write query returns nothing and looks like a bug.

Garden sections were stored with custom metadata that could not be used to filter
retrieval, so "which beds have basil?" fell back to similarity search over prose.
This makes those dimensions queryable and corrects several claims in the code and
docs that turned out to be wrong.

Template (AgentCoreMemory):
- Declare IndexedKeys: type (STRING), section (STRING), plants (STRINGLIST). Only
  indexed keys may appear in a metadataFilters expression; filtering an unindexed
  key raises ValidationException. Kept to three because indexed keys are
  additive-only, capped at 10, and cannot be removed once added. photo_count stays
  unindexed enrichment.
- Give each strategy a MemoryRecordSchema.MetadataSchema. Definition +
  LlmExtractionInstruction are the instructions handed to the extraction model, and
  Validation.AllowedValues/MaxItems constrain output so filter values stay
  consistent. This is the supported way to guide extraction, rather than an
  AppendToPrompt override.
- Remove SummaryMemoryStrategy. It produced nothing over weeks of real use (94
  records in the live namespace, all semantic/user-preference; its old episodic
  namespace held zero), it restated in looser prose what semantic extraction
  already stores densely, and it accumulated one record per session competing for
  the capped retrieval budget. The absence is documented, including the constraint
  that a re-added summarization namespace must end in {sessionId}.

Runtime (server.py):
- retrieve() pushes indexed keys down as metadataFilters, which the service applies
  BEFORE the vector search, so filters narrow the candidate set rather than trimming
  top-K results. build_metadata_filters splits indexed from unindexed keys and uses
  CONTAINS for the plants STRINGLIST; unindexed keys are filtered in-process.
- retrieve() queries namespacePath, not namespace. Verified against the service:
  namespace matches only the EXACT value given, so anything nested deeper is
  silently omitted (the API docs describing it as a prefix are misleading).
- persist() forwards CreateEvent metadata, and a captioned album attaches
  type/section at event time so extraction propagates the same dimension onto
  derived records instead of re-inferring it. Metadata is therefore no longer added
  only through the batch API. CreateEvent metadata is stringValue-only, which is why
  list-valued plants still travels the direct-write path.
- Add temporal scoping via build_created_after_filter / retrieve(created_after=...)
  on the reserved x-amz-agentcore-memory-createdAt field, which is filterable
  without consuming an indexed-key slot. Deliberately not applied to the chat or
  reminder paths: those need the oldest durable facts ("grows basil in containers",
  "zone 9b"), which a recency bound would hide.
- build_section_summary keeps the directly written record terse. One album turn
  yields several records describing the same bed (this one plus whatever extraction
  derives), and embedding the agent's full reply made ours the longest while adding
  no fact the others lacked.
- Document why memoryStrategyId is omitted on BatchCreateMemoryRecords: supplying it
  filters metadata to that strategy's schema and silently drops the rest, which
  would discard photo_count.
- Drop the dead derive_episodic_namespace helper and EPISODIC_SEGMENT.

Verified against the live service, not only in tests: section=north_bed -> 1,
plants CONTAINS basil -> 1, section=missing_bed -> 0, unindexed photo_count ->
ValidationException; temporal AFTER 2020 -> 1 and AFTER tomorrow -> 0; a deep-
namespace record is returned via namespacePath but not via namespace. Change sets
were checked for Replace=False on AgentCoreMemory before each apply, so stored
memories were never at risk, and get_memory confirms the final strategy set.

Note record indexing takes roughly 60-75 seconds, so an immediate post-write query
returns nothing and can look like a failure.

161 agent-container + 39 webhook tests pass; template validates; bandit 0 HIGH.
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.

1 participant