From 5f8a492717015ee7f2383899ab21083e19c2d4af Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sun, 20 Sep 2026 14:49:34 +0530 Subject: [PATCH 1/2] Retrieve and inject Foundry static (user-profile) memories 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. --- provider/foundryprovider/memory.go | 69 ++++++++++++++++++++----- provider/foundryprovider/memory_test.go | 64 +++++++++++++++++++++++ 2 files changed, 120 insertions(+), 13 deletions(-) diff --git a/provider/foundryprovider/memory.go b/provider/foundryprovider/memory.go index 5bc36243..7457c45e 100644 --- a/provider/foundryprovider/memory.go +++ b/provider/foundryprovider/memory.go @@ -21,6 +21,11 @@ const ( defaultMemoryContextPrompt = "## Memories\nConsider the following memories when answering user questions:" defaultMaxMemories = 5 defaultSourceID = "foundrymemory" + + // Session-state keys for the static (item-less) memory cache, fetched once + // per session and reused on later turns. + memoryStaticInitStateKey = "foundrymemory.staticInitialized" + memoryStaticMemoriesStateKey = "foundrymemory.staticMemories" ) // MemoryProviderConfig configures a Foundry memory provider. @@ -195,22 +200,32 @@ func (p *MemoryProvider) EnsureStoredMemoriesDeleted(ctx context.Context, sessio } func (p *MemoryProvider) provide(ctx context.Context, invoking agent.InvokingContext) ([]*message.Message, []agent.Option, error) { - items := searchMemoryItems(invoking.Messages) - if len(items) == 0 { - return nil, nil, nil - } session, _ := agent.GetOption(invoking.Options, agent.WithSession) scope := p.scope(session) - searchOptions := &azaiprojects.MemoryStoresClientSearchMemoriesOptions{ - Items: items, - Options: &azaiprojects.MemorySearchResultOptions{MaxMemories: p.config.MaxMemories}, - } - result, err := p.client.SearchMemories(ctx, p.memoryStoreName, scope, searchOptions) - if err != nil { - p.log(ctx, slog.LevelError, "foundrymemory: failed to search memories", "memory_store", p.memoryStoreName, "error", err) - return nil, nil, nil + + // Static (e.g. user-profile) memories are fetched once per session with an + // item-less search and prepended to the contextual memories on every turn, + // matching the Python provider. + staticMemories := p.staticMemories(ctx, session, scope) + + var contextual []string + if items := searchMemoryItems(invoking.Messages); len(items) > 0 { + searchOptions := &azaiprojects.MemoryStoresClientSearchMemoriesOptions{ + Items: items, + Options: &azaiprojects.MemorySearchResultOptions{MaxMemories: p.config.MaxMemories}, + } + result, err := p.client.SearchMemories(ctx, p.memoryStoreName, scope, searchOptions) + if err != nil { + p.log(ctx, slog.LevelError, "foundrymemory: failed to search memories", "memory_store", p.memoryStoreName, "error", err) + } else { + contextual = memoryContents(result.Memories) + } } - memories := memoryContents(result.Memories) + + // Copy into a fresh slice so appending never mutates the cached static list. + memories := make([]string, 0, len(staticMemories)+len(contextual)) + memories = append(memories, staticMemories...) + memories = append(memories, contextual...) p.log(ctx, slog.LevelInfo, "foundrymemory: retrieved memories", "memory_store", p.memoryStoreName, "count", len(memories)) if len(memories) == 0 { return nil, nil, nil @@ -219,6 +234,34 @@ func (p *MemoryProvider) provide(ctx context.Context, invoking agent.InvokingCon return []*message.Message{contextMessage}, nil, nil } +// staticMemories returns the scope's static (item-less) memories, fetching them +// once per session and caching them on the session for reuse on later turns. +// Without a session there is nowhere to cache, so it returns nothing rather +// than issuing an item-less search on every turn. +func (p *MemoryProvider) staticMemories(ctx context.Context, session *agent.Session, scope string) []string { + if session == nil { + return nil + } + var initialized bool + if ok, _ := session.Get(memoryStaticInitStateKey, &initialized); ok && initialized { + var cached []string + session.Get(memoryStaticMemoriesStateKey, &cached) + return cached + } + result, err := p.client.SearchMemories(ctx, p.memoryStoreName, scope, &azaiprojects.MemoryStoresClientSearchMemoriesOptions{ + Options: &azaiprojects.MemorySearchResultOptions{MaxMemories: p.config.MaxMemories}, + }) + if err != nil { + // Leave the session uninitialized so the next turn retries. + p.log(ctx, slog.LevelError, "foundrymemory: failed to search static memories", "memory_store", p.memoryStoreName, "error", err) + return nil + } + static := memoryContents(result.Memories) + session.Set(memoryStaticMemoriesStateKey, static) + session.Set(memoryStaticInitStateKey, true) + return static +} + func (p *MemoryProvider) store(ctx context.Context, invoked agent.InvokedContext) error { items := updateMemoryItems(invoked.RequestMessages) items = append(items, updateMemoryItems(invoked.ResponseMessages)...) diff --git a/provider/foundryprovider/memory_test.go b/provider/foundryprovider/memory_test.go index 5956ffd6..85afb9f9 100644 --- a/provider/foundryprovider/memory_test.go +++ b/provider/foundryprovider/memory_test.go @@ -160,6 +160,70 @@ func TestNewMemoryProviderStoreFiltersDefaultToExternalOnlyRequestAndPassThrough } } +// Static (user-profile) memories are fetched once per session with an item-less +// search and prepended to the contextual memories on every turn, matching the +// Python provider. +func TestMemoryProviderInjectsStaticMemories(t *testing.T) { + transport := &recordingTransport{} + transport.handle = func(req *http.Request, body string) (*http.Response, error) { + // A populated items array marks the contextual search; the item-less + // static search sends no items. + if strings.Contains(body, `"items":[{`) { + return jsonResponse(req, http.StatusOK, `{"memories":[{"memory_item":{"content":"contextual memory","kind":"procedural","memory_id":"mem_c","scope":"user-456","updated_at":1}}]}`), nil + } + return jsonResponse(req, http.StatusOK, `{"memories":[{"memory_item":{"content":"static profile","kind":"user_profile","memory_id":"mem_s","scope":"user-456","updated_at":1}}]}`), nil + } + provider := foundryprovider.NewMemoryProvider(validEndpoint, validCredential, "memory", validScope, foundryprovider.MemoryProviderConfig{ + ClientOptions: azcore.ClientOptions{Transport: transport}, + ContextPrompt: new("Memories:"), + }) + session := &agent.Session{} + + // First turn: item-less static search + contextual search; both surfaced. + messages, _, err := provider.Invoking(t.Context(), agent.InvokingContext{ + Messages: []*message.Message{message.NewText("what do you remember?")}, + Options: []agent.Option{agent.WithSession(session)}, + }) + if err != nil { + t.Fatalf("Invoking error = %v", err) + } + if ctx := messagesString(messages); !strings.Contains(ctx, "static profile") || !strings.Contains(ctx, "contextual memory") { + t.Fatalf("first-turn context = %q, want static + contextual", ctx) + } + + // Second turn: static is cached, so no second item-less search is issued, + // but the static memory is still prepended. + messages, _, err = provider.Invoking(t.Context(), agent.InvokingContext{ + Messages: []*message.Message{message.NewText("again")}, + Options: []agent.Option{agent.WithSession(session)}, + }) + if err != nil { + t.Fatalf("Invoking error = %v", err) + } + if ctx := messagesString(messages); !strings.Contains(ctx, "static profile") { + t.Fatalf("second-turn context = %q, want static memory prepended", ctx) + } + + // One item-less static search total (turn 1) + one contextual search per turn. + itemless := 0 + for _, r := range transport.Requests() { + if !strings.Contains(r.Body, `"items":[{`) { + itemless++ + } + } + if itemless != 1 { + t.Fatalf("item-less static searches = %d, want 1 (cached after first turn)", itemless) + } +} + +func messagesString(messages []*message.Message) string { + var b strings.Builder + for _, m := range messages { + b.WriteString(m.String()) + } + return b.String() +} + func TestMemoryProviderPanicsWhenScopeIsEmptyOnUse(t *testing.T) { provider := foundryprovider.NewMemoryProvider(validEndpoint, validCredential, "memory", func(*agent.Session) string { return " " }, foundryprovider.MemoryProviderConfig{}) assertPanics(t, func() { From 5f2c14f4e7da3acebe03922f39d377d26bb00ef7 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Mon, 21 Sep 2026 16:51:23 +0530 Subject: [PATCH 2/2] Address review: key static-memory cache by store and scope 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. --- go.mod | 21 ++++++------ go.sum | 44 ++++++++++++++----------- provider/foundryprovider/memory.go | 19 ++++++++--- provider/foundryprovider/memory_test.go | 6 ++-- 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index d18de353..0a9307b3 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 github.com/a2aproject/a2a-go/v2 v2.5.0 github.com/ag-ui-protocol/ag-ui/sdks/community/go v0.0.0-20260312103001-8e7ab1df34c8 - github.com/anthropics/anthropic-sdk-go v1.72.0 - github.com/github/copilot-sdk/go v1.0.13 + github.com/anthropics/anthropic-sdk-go v1.73.0 + github.com/github/copilot-sdk/go v1.0.14 github.com/gofrs/flock v0.13.1 github.com/google/jsonschema-go v0.4.3 github.com/google/uuid v1.6.0 @@ -22,12 +22,12 @@ require ( go.opentelemetry.io/otel/trace v1.46.0 golang.org/x/sync v0.23.0 google.golang.org/genai v1.71.0 - google.golang.org/grpc v1.83.2 + google.golang.org/grpc v1.84.0 ) require ( cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect @@ -36,14 +36,14 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coder/websocket v1.8.15 // indirect github.com/ebitengine/purego v0.10.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect - github.com/googleapis/gax-go/v2 v2.17.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect + github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/invopop/jsonschema v0.14.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -59,7 +59,7 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect golang.org/x/crypto v0.55.0 // indirect golang.org/x/mod v0.38.0 // indirect @@ -68,7 +68,8 @@ require ( golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/api v0.278.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index 9ff26b78..2cc359b8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo= @@ -20,8 +20,8 @@ github.com/a2aproject/a2a-go/v2 v2.5.0 h1:ZdcFoxv+nZTUV0i2ue5hES76YCANFPG9vjqd7v github.com/a2aproject/a2a-go/v2 v2.5.0/go.mod h1:NcRp/ZHxgMzDj12/BteIC2gOjljuEBKaGRfEdJ2lNSI= github.com/ag-ui-protocol/ag-ui/sdks/community/go v0.0.0-20260312103001-8e7ab1df34c8 h1:gcgM2iVSJ+5v1Li2O2xBm8jdY9bIo0coZdOlXOd0uLU= github.com/ag-ui-protocol/ag-ui/sdks/community/go v0.0.0-20260312103001-8e7ab1df34c8/go.mod h1:ERAMOexUee4AIuoxksuuGoEcHl3aqLwaazjGwlR9ZCI= -github.com/anthropics/anthropic-sdk-go v1.72.0 h1:T0qQWWfygiL24lqEQaffRXCvEeno3yiv/mFzUdY1E88= -github.com/anthropics/anthropic-sdk-go v1.72.0/go.mod h1:x+lPk/cCl48uRegeP0hlYYBN1b7bEBTveInIMgLicnY= +github.com/anthropics/anthropic-sdk-go v1.73.0 h1:brLt0oVV6LH0YcG6O6G3pBnIVDJQ/QPoXul+OJHXEFc= +github.com/anthropics/anthropic-sdk-go v1.73.0/go.mod h1:x+lPk/cCl48uRegeP0hlYYBN1b7bEBTveInIMgLicnY= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= @@ -36,10 +36,10 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/github/copilot-sdk/go v1.0.13 h1:sc4kLSJksYb6lsUwFV48P4jDJ06PQR+4OzpqOhvxho0= -github.com/github/copilot-sdk/go v1.0.13/go.mod h1:U0STg9Jdv9LDwtkUtcUJCzPQ4KTnKakRfP+1LhEx248= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/github/copilot-sdk/go v1.0.14 h1:DeHPF2CbxRCe4EeAjYIo7v3CovGEEMHsrURUNaogf5A= +github.com/github/copilot-sdk/go v1.0.14/go.mod h1:U0STg9Jdv9LDwtkUtcUJCzPQ4KTnKakRfP+1LhEx248= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -59,10 +59,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= -github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= -github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= -github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= +github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= +github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= @@ -107,8 +107,8 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.46.0 h1:KdRxPiAoMptR3vfWzvjjvutTsSiwbC2uG0496rzZNfo= @@ -151,14 +151,18 @@ golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= +google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/genai v1.71.0 h1:Wfo9n0uSzMhZH7d+rP7QxxSWELEDSD4z6O8W/C9s3oM= google.golang.org/genai v1.71.0/go.mod h1:mDdPDFXo1Ats7f1WXVyZgWb/CkMzFWTWJruIMy7hGIU= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= -google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4= +google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/provider/foundryprovider/memory.go b/provider/foundryprovider/memory.go index 7457c45e..1a47f6f4 100644 --- a/provider/foundryprovider/memory.go +++ b/provider/foundryprovider/memory.go @@ -242,10 +242,15 @@ func (p *MemoryProvider) staticMemories(ctx context.Context, session *agent.Sess if session == nil { return nil } + // Key the cache by store and scope so a session shared across providers, or + // a scope callback whose result changes between turns, does not reuse the + // wrong scope's static memories. + initKey := p.staticStateKey(memoryStaticInitStateKey, scope) + memKey := p.staticStateKey(memoryStaticMemoriesStateKey, scope) var initialized bool - if ok, _ := session.Get(memoryStaticInitStateKey, &initialized); ok && initialized { + if ok, _ := session.Get(initKey, &initialized); ok && initialized { var cached []string - session.Get(memoryStaticMemoriesStateKey, &cached) + session.Get(memKey, &cached) return cached } result, err := p.client.SearchMemories(ctx, p.memoryStoreName, scope, &azaiprojects.MemoryStoresClientSearchMemoriesOptions{ @@ -257,11 +262,17 @@ func (p *MemoryProvider) staticMemories(ctx context.Context, session *agent.Sess return nil } static := memoryContents(result.Memories) - session.Set(memoryStaticMemoriesStateKey, static) - session.Set(memoryStaticInitStateKey, true) + session.Set(memKey, static) + session.Set(initKey, true) return static } +// staticStateKey qualifies a static-memory session-state key with the store and +// scope so cached state is not shared across providers or scopes on one session. +func (p *MemoryProvider) staticStateKey(prefix, scope string) string { + return prefix + ":" + p.memoryStoreName + ":" + scope +} + func (p *MemoryProvider) store(ctx context.Context, invoked agent.InvokedContext) error { items := updateMemoryItems(invoked.RequestMessages) items = append(items, updateMemoryItems(invoked.ResponseMessages)...) diff --git a/provider/foundryprovider/memory_test.go b/provider/foundryprovider/memory_test.go index 85afb9f9..7539df66 100644 --- a/provider/foundryprovider/memory_test.go +++ b/provider/foundryprovider/memory_test.go @@ -187,8 +187,10 @@ func TestMemoryProviderInjectsStaticMemories(t *testing.T) { if err != nil { t.Fatalf("Invoking error = %v", err) } - if ctx := messagesString(messages); !strings.Contains(ctx, "static profile") || !strings.Contains(ctx, "contextual memory") { - t.Fatalf("first-turn context = %q, want static + contextual", ctx) + // Static memories are prepended before the contextual ones, so assert order, + // not just presence. + if ctx := messagesString(messages); !strings.Contains(ctx, "static profile\ncontextual memory") { + t.Fatalf("first-turn context = %q, want static memory prepended before contextual", ctx) } // Second turn: static is cached, so no second item-less search is issued,