Add WhenUpdatesCompleted to Foundry MemoryProvider to await async memory extraction - #653
Conversation
e4cc68d to
b43f47a
Compare
This comment has been minimized.
This comment has been minimized.
b43f47a to
eab5828
Compare
This comment has been minimized.
This comment has been minimized.
Foundry extracts memories asynchronously after a run, so store() is fire-and-forget and returns before memories are persisted. Retain the long-running poller returned by BeginUpdateMemories (guarded by a mutex) and expose WhenUpdatesCompleted to await the most recent update. This lets callers and tests observe stored memories before the next search without making the store path block.
eab5828 to
442ca39
Compare
# Conflicts: # provider/foundryprovider/memory_test.go
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 31.2 AIC · ⌖ 5.89 AIC · ⊞ 5.7K
| return nil | ||
| } | ||
| if _, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: pollingInterval}); err != nil { | ||
| return err |
There was a problem hiding this comment.
Parity gap — superseded terminal state
The .NET WhenUpdatesCompletedAsync explicitly treats MemoryStoreUpdateStatus.Superseded as a successful terminal state (returns without error), mirroring Foundry service semantics where a superseded update is implicitly replaced by a newer one.
The Go implementation delegates to runtime.Poller.PollUntilDone, which only knows standard Azure LRO states and does not treat superseded as terminal. As a result, WhenUpdatesCompleted will keep polling past a superseded status until the service returns a different state or the context times out — diverging from the .NET behaviour.
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 PollingHandler / response classifier in the Azure Go SDK so the poller treats superseded as done, or (b) replacing PollUntilDone with a manual polling loop that mirrors .NET's WaitForUpdateAsync and explicitly handles superseded → success.
There was a problem hiding this comment.
Pull request overview
Adds an opt-in mechanism to await completion of Foundry’s asynchronous memory extraction, enabling deterministic sequencing of “store → search” without making the normal store() path blocking.
Changes:
- Retains the most recent Foundry LRO poller returned by
BeginUpdateMemoriesand exposesWhenUpdatesCompleted(ctx, pollingInterval)to drive it to completion. - Keeps
store()non-blocking while allowing callers (notably tests) to wait for extraction to finish before searching. - Adds unit tests covering success (including “clears pending update”), failure, and “no pending update” behavior using the existing recording transport harness.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| provider/foundryprovider/memory.go | Stores the latest update poller and adds WhenUpdatesCompleted to await LRO completion on demand. |
| provider/foundryprovider/memory_test.go | Adds tests validating await-success, await-failure, and no-op behavior when nothing is pending. |
# Conflicts: # provider/foundryprovider/memory_test.go
Cross-Repo Parity ReviewThis PR adds FindingsOne minor parity delta: The Go API requires a This is a small divergence in ergonomics. Callers who want the default behaviour must choose an explicit interval in Go but not in .NET. Consider making All other aspects — semantics (fire-and-forget store, explicit opt-in await, clear-on-success, no-op when idle), error behavior (non-nil on failure), and docstring intent — are well-aligned with the .NET implementation. Label:
|
What
Adds
WhenUpdatesCompleted(ctx, pollingInterval)to the FoundryMemoryProviderso callers can await asynchronous memory extraction before searching.store()previously discarded the poller returned byBeginUpdateMemories, making memory updates fire-and-forget with no way to know when they land. This retains the most recent poller on the provider (guarded by async.Mutex) and drives it to completion on demand viaruntime.Poller.PollUntilDone.store()stays non-blocking, so only opt-in callers pay the polling cost.nilwhen no update is pending or the update completed successfully.Why (parity)
Memory extraction on Foundry is a long-running operation. The .NET/Python agent-framework Foundry memory providers submit the update and expose a way to await the extraction result (the LRO poller / update-result endpoint) so a subsequent
SearchMemoriessees the freshly written memories. The Go port kept the fire-and-forget submit but never surfaced the completion handle, so tests and callers had no deterministic way to sequence store -> search. This closes that gap while keeping the hot path unchanged.Not a duplicate of #627/#619/#617.
How tested
go build ./...,go vet ./provider/foundryprovider/...,go test ./provider/foundryprovider/...all pass. New tests inmemory_test.gouse the existingrecordingTransportto:BeginUpdateMemories(202 +Operation-Location) then a status sequencequeued -> completed, assertingWhenUpdatesCompletedreturns nil and clears the pending update (a second call issues no further requests);failedstatus, asserting a non-nil error;Open design questions
pollingIntervaland relies on the caller'sctxfor the overall deadline. Should it instead take a fullruntime.PollUntilDoneOptions, or default the interval?PollUntilDonetreatscompletedas success andfailedas error. Foundry also has asupersededterminal state (an update replaced by a newer one); the generic poller does not treat it as terminal. If awaiting a superseded update matters, a follow-up could pollGetUpdateResultdirectly and treatsupersededas success. Flagging as a follow-up rather than expanding scope here.