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
This comment has been minimized.
This comment has been minimized.
API Consistency Review — PR #653Verdict: Parity mostly preserved, one actionable gap flagged inline. What changed
Upstream equivalents found
Parity issues1. (Inline comment) .NET's 2. .NET defaults to 5 seconds ( Items that match upstream
LabelsNo label changes needed —
|
There was a problem hiding this comment.
Generated by Go API Consistency Review Agent · sonnet46 · 31.6 AIC · ⌖ 5.27 AIC · ⊞ 6K
| if poller == nil { | ||
| return nil | ||
| } | ||
| if _, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: pollingInterval}); err != nil { |
There was a problem hiding this comment.
Parity gap — superseded terminal state not handled
The .NET WhenUpdatesCompletedAsync (FoundryMemoryProvider.cs) explicitly treats superseded as a success terminal state alongside completed:
if (status == MemoryStoreUpdateStatus.Completed || status == MemoryStoreUpdateStatus.Superseded)
{
return;
}This Go implementation delegates to runtime.Poller.PollUntilDone, which only knows about generic Azure LRO states (Succeeded, Failed, Canceled). The Foundry-specific superseded state is not a standard Azure LRO terminal state, so the poller will likely loop until ctx is 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.
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.