Skip to content

Retrieve and inject Foundry static (user-profile) memories - #1125

Open
PratikDhanave (PratikDhanave) wants to merge 2 commits into
microsoft:mainfrom
PratikDhanaveFork:fix/foundry-static-memories
Open

PratikDhanave (PratikDhanave) wants to merge 2 commits into
microsoft:mainfrom
PratikDhanaveFork:fix/foundry-static-memories

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

The Foundry memory provider only ran a search with the current turn's items and returned early when a turn had no items. So a scope's static memories (e.g. user_profile), which the service returns from an item-less search, were never retrieved or injected.

The Python provider fetches static memories once per session with an item-less search_memories call, caches them in state, and prepends them to the contextual memories on every turn.

Change

  • Fetch static memories once per session via an item-less search (staticMemories), cache them on the session (same session-state pattern as hostedsession.go), and prepend them to the contextual memories each turn. Contextual search still runs only when the turn has items. Without a session there is nowhere to cache, so the item-less search is skipped rather than repeated every turn. Items is optional on the search options, so an item-less search is valid.

Test

  • TestMemoryProviderInjectsStaticMemories: over one session, the first turn issues an item-less static search + a contextual search and injects both; the second turn reuses the cached static memory (no second item-less search) and still prepends it. Fails before the change (static memory absent), passes after.

The memory provider only searched with the current turn's items, so a
scope's static (e.g. user-profile) memories - returned by an item-less
search - were never retrieved. It also returned early when a turn had no
search items, skipping memory injection entirely.

Fetch static memories once per session with an item-less search, cache them
on the session, and prepend them to the contextual memories on every turn,
matching the Python provider. Without a session (nowhere to cache) the
item-less search is skipped rather than repeated each turn.
Copilot AI lite review requested due to automatic review settings September 20, 2026 09:19
@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/foundry Changes files in the provider / foundry area size:large At most 300 changed lines across at most 10 files labels Sep 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The new cache is unsynchronized and does not isolate cached memories by provider or scope.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Extends the Foundry memory provider to retrieve and cache static memories per session, then prepend them to contextual memories.

Changes:

  • Added item-less static-memory retrieval and session caching.
  • Added two-turn regression coverage for caching and injection.
File Description
provider/​foundryprovider/​memory.go Implements static-memory retrieval and caching.
provider/​foundryprovider/​memory_test.go Tests static-memory injection and reuse.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +27 to +28
memoryStaticInitStateKey = "foundrymemory.staticInitialized"
memoryStaticMemoriesStateKey = "foundrymemory.staticMemories"
Comment thread provider/foundryprovider/memory_test.go Outdated
Comment on lines +190 to +192
if ctx := messagesString(messages); !strings.Contains(ctx, "static profile") || !strings.Contains(ctx, "contextual memory") {
t.Fatalf("first-turn context = %q, want static + contextual", ctx)
}
@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure labels Sep 20, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Generated by Go API Consistency Review Agent · copilot · auto · 88 AIC · ⌖ 10.5 AIC · ⊞ 9.2K

Options: &azaiprojects.MemorySearchResultOptions{MaxMemories: p.config.MaxMemories},
})
if err != nil {
// Leave the session uninitialized so the next turn retries.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This staticMemories helper leaves the session uninitialized on a failed item-less search so the next turn retries. The Python reference this PR states it's porting (python/packages/foundry/agent_framework_foundry/_memory_provider.py, before_run) always sets state["initialized"] = True in a finally block regardless of success, caching an empty static-memory list and never retrying the item-less search again for that session (test_static_memories_only_retrieved_once covers this).

So Go will keep re-issuing the item-less static search every subsequent turn after a transient failure, while Python treats the static fetch as one-shot best-effort per session. Since the PR explicitly aims to match the Python provider's caching semantics, consider setting the initialized flag (with an empty cached list) even on failure to match Python, or note this retry-on-failure behavior as an intentional Go-specific divergence.

Qualify the static-memory session-state keys with the memory store name and
resolved scope so a session shared across providers, or a scope callback whose
result changes between turns, cannot inject another scope's static memories.
Also assert prepend order in the test.
@github-actions github-actions Bot added the kind:dependencies Changes dependencies or manifests label Sep 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: user-visible behavior (Foundry memory retrieval semantics), tests

Changed Go contract: MemoryProvider.provide (unexported, but observably changes injected memory content/order and network calls) plus new unexported helpers staticMemories and staticStateKey in provider/foundryprovider/memory.go. No exported identifiers were added, removed, or changed.

Upstream evidence reviewed:

  • Python: python/packages/foundry/agent_framework_foundry/_memory_provider.py, FoundryMemoryProvider.before_run — item-less static search on first turn, cached in state["static_memories"], prepended to contextual memories every turn; try/except/finally always sets state["initialized"] = True regardless of success/failure.
  • .NET: dotnet/src/Microsoft.Agents.AI.Foundry/Memory/FoundryMemoryProvider.cs (ProvideAIContextAsync) — no static/item-less memory retrieval exists in .NET; only contextual (item-based) search is performed, so .NET does not need to be reconciled here.

Result: findings reported.

This PR correctly ports the Python provider's item-less "static memory" fetch-once/cache/prepend behavior, closing a real parity gap where Go previously never retrieved static (e.g. user_profile) memories. The overall approach (session-scoped cache keyed by store+scope, prepend before contextual memories, skip when no session) matches Python's before_run semantics.

One divergence was flagged inline: on a failed static search, Go leaves the session "uninitialized" so it retries the item-less search on every subsequent turn, whereas Python's finally block unconditionally marks state["initialized"] = True (caching an empty list) even on failure, so Python attempts the static search only once per session. This could cause unnecessary repeated calls against a failing/unavailable memory store in Go. See inline comment for detail.

Generated by Go API Consistency Review Agent · copilot · auto · 76.1 AIC · ⌖ 7.93 AIC · ⊞ 9.2K ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Generated by Go API Consistency Review Agent · copilot · auto · 76.1 AIC · ⌖ 7.93 AIC · ⊞ 9.2K

result, err := p.client.SearchMemories(ctx, p.memoryStoreName, scope, &azaiprojects.MemoryStoresClientSearchMemoriesOptions{
Options: &azaiprojects.MemorySearchResultOptions{MaxMemories: p.config.MaxMemories},
})
if err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The retry-on-failure behavior diverges from the upstream Python provider.

Here, when the item-less static search fails, initKey is left unset so every subsequent turn re-issues the static search until it succeeds (per the "Leave the session uninitialized so the next turn retries" comment).

Upstream python/packages/foundry/agent_framework_foundry/_memory_provider.py (FoundryMemoryProvider.before_run) does the opposite: the try/except/finally block sets state["static_memories"] = [] on failure and unconditionally sets state["initialized"] = True in the finally clause, so a failed static search is attempted only once per session and the provider silently proceeds with no static memories thereafter — it never retries on later turns.

This is a meaningful parity gap: a persistently failing/unavailable memory store causes the Go provider to retry the item-less search on every turn indefinitely (extra latency/cost per turn), while Python fails once and moves on. Consider caching an "attempted" flag (with empty static memories) on failure, matching the Python finally semantics, unless the indefinite-retry behavior is an intentional Go-specific choice.

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

Labels

area:provider/foundry Changes files in the provider / foundry area area:provider Changes files in the provider area kind:code Changes production behavior or code kind:dependencies Changes dependencies or manifests kind:tests Changes tests, fixtures, or test infrastructure size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants