-
Notifications
You must be signed in to change notification settings - Fork 46
Add WhenUpdatesCompleted to Foundry MemoryProvider to await async memory extraction #653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
442ca39
ad48af7
a98b2d6
2cb4693
2a3c877
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,8 +9,11 @@ import ( | |
| "log/slog" | ||
| "net/http" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/Azure/azure-sdk-for-go/sdk/azcore" | ||
| "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" | ||
| "github.com/microsoft/agent-framework-go/agent" | ||
| "github.com/microsoft/agent-framework-go/internal/azaiprojects" | ||
| "github.com/microsoft/agent-framework-go/message" | ||
|
|
@@ -55,6 +58,9 @@ type MemoryProvider struct { | |
| config MemoryProviderConfig | ||
| providerConfig agent.ContextProviderConfig | ||
| provider agent.ContextProvider | ||
|
|
||
| mu sync.Mutex | ||
| pendingUpdate *runtime.Poller[azaiprojects.MemoryStoresClientUpdateMemoriesResponse] | ||
| } | ||
|
|
||
| // NewMemoryProvider creates a Foundry memory provider backed by a Microsoft Foundry Projects memory store. | ||
|
|
@@ -223,14 +229,46 @@ func (p *MemoryProvider) store(ctx context.Context, invoked agent.InvokedContext | |
| Items: items, | ||
| UpdateDelay: &p.config.UpdateDelay, | ||
| } | ||
| if _, err := p.client.BeginUpdateMemories(ctx, p.memoryStoreName, scope, updateOptions); err != nil { | ||
| poller, err := p.client.BeginUpdateMemories(ctx, p.memoryStoreName, scope, updateOptions) | ||
| if err != nil { | ||
| p.log(ctx, slog.LevelError, "foundrymemory: failed to update memories", "memory_store", p.memoryStoreName, "count", len(items), "error", err) | ||
| return nil | ||
| } | ||
| p.mu.Lock() | ||
| p.pendingUpdate = poller | ||
| p.mu.Unlock() | ||
| p.log(ctx, slog.LevelInfo, "foundrymemory: submitted memory update", "memory_store", p.memoryStoreName, "count", len(items)) | ||
| return nil | ||
| } | ||
|
|
||
| // WhenUpdatesCompleted blocks until the memory update most recently submitted by | ||
| // the provider reaches a terminal state, polling the update status at | ||
| // pollingInterval. It returns nil when no update is pending or when the update | ||
| // completed successfully, and a non-nil error when the update failed. | ||
| // | ||
| // Foundry extracts memories asynchronously after a run, so [MemoryProvider.store] | ||
| // is fire-and-forget and returns before memories are persisted. Callers that must | ||
| // observe stored memories before the next search—typically tests—can await | ||
| // completion with this method. Normal runs need not call it and do not pay the | ||
| // polling cost. | ||
| func (p *MemoryProvider) WhenUpdatesCompleted(ctx context.Context, pollingInterval time.Duration) error { | ||
| p.mu.Lock() | ||
| poller := p.pendingUpdate | ||
| p.mu.Unlock() | ||
| if poller == nil { | ||
| return nil | ||
| } | ||
| if _, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: pollingInterval}); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parity gap — required vs optional In the .NET implementation, public async Task WhenUpdatesCompletedAsync(
TimeSpan? pollingInterval = null, // defaults to 5 seconds
CancellationToken cancellationToken = default)The Go signature requires callers to pass an explicit func (p *MemoryProvider) WhenUpdatesCompleted(ctx context.Context, pollingInterval time.Duration) errorThis is a minor ergonomics gap: callers must always choose a value rather than relying on a sensible default. Consider treating a zero |
||
| return err | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parity gap — The .NET The Go implementation delegates to The PR acknowledges this in the open design questions, but it is worth flagging explicitly since .NET has already shipped the fix: // .NET — FoundryMemoryProvider.cs
if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
return; // treat superseded as successSuggestion: before merging, consider either (a) using a custom |
||
| } | ||
| p.mu.Lock() | ||
| if p.pendingUpdate == poller { | ||
| p.pendingUpdate = nil | ||
| } | ||
| p.mu.Unlock() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parity gap — The .NET implementation explicitly treats if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
{
return;
}The Go implementation delegates polling to The PR description already flags this as an open question. For parity with .NET, |
||
| return nil | ||
| } | ||
|
|
||
| func (p *MemoryProvider) log(ctx context.Context, level slog.Level, msg string, args ...any) { | ||
| if p.config.Logger == nil || !p.config.Logger.Enabled(ctx, level) { | ||
| return | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parity gap —
supersededterminal state not handledThe .NET
WhenUpdatesCompletedAsync(FoundryMemoryProvider.cs) explicitly treatssupersededas a success terminal state alongsidecompleted:This Go implementation delegates to
runtime.Poller.PollUntilDone, which only knows about generic Azure LRO states (Succeeded,Failed,Canceled). The Foundry-specificsupersededstate is not a standard Azure LRO terminal state, so the poller will likely loop untilctxis cancelled if an update is superseded by a newer one.The PR description flags this as a known follow-up, but callers awaiting in a test or workflow may hang silently. Consider adding an explicit doc note to the godoc comment warning that a superseded update will block until the context deadline, and linking to a tracking issue.