Skip to content

Commit be92b12

Browse files
authored
azure-ai-agents LRO poller util fix for custom end states (#48157)
* Poller fix * Polled operation fix changelog entries * PR copilot feedback * New recordings * Using TSP defined custom status for PollUtils * Removed comments * Using right value for header from enum * Added comment with warning * Applying poller customization through AgentsCustomization.java
1 parent 9232f31 commit be92b12

File tree

9 files changed

+278
-83
lines changed

9 files changed

+278
-83
lines changed

sdk/ai/azure-ai-agents/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232

3333
### Bugs Fixed
3434

35+
- Fixed Memory Stores long-running operations (e.g. `beginUpdateMemories`) failing because the required `Foundry-Features` header was not included in poll requests, and custom LRO terminal states (`"completed"`, `"superseded"`) were not mapped to standard `LongRunningOperationStatus` values, causing pollers to hang indefinitely.
3536
- Fixed request parameter name from `"agent"` to `"agent_reference"` in `ResponsesClient` and `ResponsesAsyncClient` methods `createWithAgent` and `createWithAgentConversation`
3637

3738
### Other Changes
3839

40+
- Enabled and stabilised `MemoryStoresTests` and `MemoryStoresAsyncTests` (previously `@Disabled`), with timeout guards to prevent hanging.
41+
3942
## 2.0.0-beta.1 (2026-02-25)
4043

4144
### Features Added

sdk/ai/azure-ai-agents/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "java",
44
"TagPrefix": "java/ai/azure-ai-agents",
5-
"Tag": "java/ai/azure-ai-agents_e4777fbd74"
5+
"Tag": "java/ai/azure-ai-agents_34d0d1c5d4"
66
}

sdk/ai/azure-ai-agents/customizations/src/main/java/AgentsCustomizations.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import com.azure.autorest.customization.Customization;
22
import com.azure.autorest.customization.LibraryCustomization;
3+
import com.github.javaparser.StaticJavaParser;
4+
import com.github.javaparser.ast.body.MethodDeclaration;
35
import org.slf4j.Logger;
46

57

@@ -12,6 +14,7 @@ public class AgentsCustomizations extends Customization {
1214
@Override
1315
public void customize(LibraryCustomization libraryCustomization, Logger logger) {
1416
renameImageGenToolSize(libraryCustomization, logger);
17+
modifyPollingStrategies(libraryCustomization, logger);
1518
}
1619

1720
private void renameImageGenToolSize(LibraryCustomization customization, Logger logger) {
@@ -30,4 +33,24 @@ private void renameImageGenToolSize(LibraryCustomization customization, Logger l
3033
.filter(entry -> "ONE_FIVE_THREE_SIXX_ONE_ZERO_TWO_FOUR".equals(entry.getName().getIdentifier()))
3134
.forEach(entry -> entry.setName("RESOLUTION_1536_X_1024"))));
3235
}
36+
37+
private void modifyPollingStrategies(LibraryCustomization customization, Logger logger) {
38+
customization.getClass("com.azure.ai.agents.implementation", "OperationLocationPollingStrategy")
39+
.customizeAst(ast -> ast.getClassByName("OperationLocationPollingStrategy")
40+
.ifPresent(clazz -> {
41+
clazz.getConstructors().get(1).getBody().getStatements()
42+
.set(0, StaticJavaParser.parseStatement("super(PollingUtils.OPERATION_LOCATION_HEADER, AgentsServicePollUtils.withFoundryFeatures(pollingStrategyOptions));"));
43+
44+
clazz.addMember(StaticJavaParser.parseMethodDeclaration("@Override public Mono<PollResponse<T>> poll(PollingContext<T> pollingContext, TypeReference<T> pollResponseType) { return super.poll(pollingContext, pollResponseType).map(AgentsServicePollUtils::remapStatus); }"));
45+
}));
46+
47+
customization.getClass("com.azure.ai.agents.implementation", "SyncOperationLocationPollingStrategy")
48+
.customizeAst(ast -> ast.getClassByName("SyncOperationLocationPollingStrategy")
49+
.ifPresent(clazz -> {
50+
clazz.getConstructors().get(1).getBody().getStatements()
51+
.set(0, StaticJavaParser.parseStatement("super(PollingUtils.OPERATION_LOCATION_HEADER, AgentsServicePollUtils.withFoundryFeatures(pollingStrategyOptions));"));
52+
53+
clazz.addMember(StaticJavaParser.parseMethodDeclaration("@Override public PollResponse<T> poll(PollingContext<T> pollingContext, TypeReference<T> pollResponseType) { return AgentsServicePollUtils.remapStatus(super.poll(pollingContext, pollResponseType)); }"));
54+
}));
55+
}
3356
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.ai.agents.implementation;
5+
6+
import com.azure.ai.agents.models.FoundryFeaturesOptInKeys;
7+
import com.azure.ai.agents.models.MemoryStoreUpdateStatus;
8+
import com.azure.core.http.HttpHeaderName;
9+
import com.azure.core.http.HttpHeaders;
10+
import com.azure.core.http.policy.AddHeadersFromContextPolicy;
11+
import com.azure.core.util.Context;
12+
import com.azure.core.util.polling.LongRunningOperationStatus;
13+
import com.azure.core.util.polling.PollResponse;
14+
import com.azure.core.util.polling.PollingStrategyOptions;
15+
16+
/**
17+
* Shared polling helpers for the Agents SDK.
18+
*
19+
* <p>The generated {@code OperationLocationPollingStrategy} / {@code SyncOperationLocationPollingStrategy}
20+
* delegate here so that the two strategies stay in sync and only minimal edits are needed in the
21+
* generated files.</p>
22+
*
23+
* <p>This class is package-private; it is <b>not</b> part of the public API.</p>
24+
*/
25+
final class AgentsServicePollUtils {
26+
27+
/** Required preview-feature header for Memory Stores operations. */
28+
private static final HttpHeaderName FOUNDRY_FEATURES = HttpHeaderName.fromString("Foundry-Features");
29+
private static final String FOUNDRY_FEATURES_VALUE = FoundryFeaturesOptInKeys.MEMORY_STORES_V1_PREVIEW.toString();
30+
31+
private AgentsServicePollUtils() {
32+
}
33+
34+
/**
35+
* Adds the {@code Foundry-Features} header to the given {@link PollingStrategyOptions}'s
36+
* {@link Context}. If the context already carries {@link HttpHeaders} under the
37+
* {@link AddHeadersFromContextPolicy} key they are preserved; the {@code Foundry-Features}
38+
* entry is merged in. Because the pipeline already contains
39+
* {@link AddHeadersFromContextPolicy}, the header is automatically added to every HTTP
40+
* request the parent strategy makes (initial, poll, and final-result GETs).
41+
*
42+
* <p><strong>Note:</strong> this method mutates and returns the same
43+
* {@code PollingStrategyOptions} instance.</p>
44+
*/
45+
static PollingStrategyOptions withFoundryFeatures(PollingStrategyOptions options) {
46+
Context context = options.getContext() != null ? options.getContext() : Context.NONE;
47+
Object existing = context.getData(AddHeadersFromContextPolicy.AZURE_REQUEST_HTTP_HEADERS_KEY).orElse(null);
48+
HttpHeaders headers
49+
= (existing instanceof HttpHeaders) ? new HttpHeaders((HttpHeaders) existing) : new HttpHeaders();
50+
headers.set(FOUNDRY_FEATURES, FOUNDRY_FEATURES_VALUE);
51+
return options.setContext(context.addData(AddHeadersFromContextPolicy.AZURE_REQUEST_HTTP_HEADERS_KEY, headers));
52+
}
53+
54+
/**
55+
* Remaps a {@link PollResponse} whose status may contain a custom service terminal state
56+
* ({@code "completed"}, {@code "superseded"}) that the base {@code OperationResourcePollingStrategy}
57+
* cannot recognize. If no remapping is needed the original response is returned as-is.
58+
*
59+
* <p>The Memory Stores Azure core defines:</p>
60+
* <ul>
61+
* <li>{@code "completed"} {@link LongRunningOperationStatus#SUCCESSFULLY_COMPLETED}</li>
62+
* <li>{@code "superseded"} {@link LongRunningOperationStatus#USER_CANCELLED}</li>
63+
* </ul>
64+
*/
65+
static <T> PollResponse<T> remapStatus(PollResponse<T> response) {
66+
LongRunningOperationStatus status = response.getStatus();
67+
LongRunningOperationStatus mapped = mapCustomStatus(status);
68+
if (mapped == status) {
69+
return response;
70+
}
71+
return new PollResponse<>(mapped, response.getValue(), response.getRetryAfter());
72+
}
73+
74+
private static LongRunningOperationStatus mapCustomStatus(LongRunningOperationStatus status) {
75+
// Standard statuses (Succeeded, Failed, Canceled, InProgress, NotStarted) are already
76+
// mapped correctly by the parent's PollResult; only remap the custom ones.
77+
String name = status.toString();
78+
if (MemoryStoreUpdateStatus.COMPLETED.toString().equalsIgnoreCase(name)) {
79+
return LongRunningOperationStatus.SUCCESSFULLY_COMPLETED;
80+
} else if (MemoryStoreUpdateStatus.SUPERSEDED.toString().equalsIgnoreCase(name)) {
81+
return LongRunningOperationStatus.USER_CANCELLED;
82+
}
83+
return status;
84+
}
85+
}

sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/OperationLocationPollingStrategy.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
// Code generated by Microsoft (R) TypeSpec Code Generator.
4-
54
package com.azure.ai.agents.implementation;
65

76
import com.azure.core.exception.AzureException;
@@ -22,7 +21,6 @@
2221
import reactor.core.publisher.Mono;
2322

2423
// DO NOT modify this helper class
25-
2624
/**
2725
* Implements an operation location polling strategy, from Operation-Location.
2826
*
@@ -35,7 +33,9 @@ public final class OperationLocationPollingStrategy<T, U> extends OperationResou
3533
private static final ClientLogger LOGGER = new ClientLogger(OperationLocationPollingStrategy.class);
3634

3735
private final ObjectSerializer serializer;
36+
3837
private final String endpoint;
38+
3939
private final String propertyName;
4040

4141
/**
@@ -56,7 +56,8 @@ public OperationLocationPollingStrategy(PollingStrategyOptions pollingStrategyOp
5656
* @throws NullPointerException if {@code pollingStrategyOptions} is null.
5757
*/
5858
public OperationLocationPollingStrategy(PollingStrategyOptions pollingStrategyOptions, String propertyName) {
59-
super(PollingUtils.OPERATION_LOCATION_HEADER, pollingStrategyOptions);
59+
super(PollingUtils.OPERATION_LOCATION_HEADER,
60+
AgentsServicePollUtils.withFoundryFeatures(pollingStrategyOptions));
6061
this.propertyName = propertyName;
6162
this.endpoint = pollingStrategyOptions.getEndpoint();
6263
this.serializer = pollingStrategyOptions.getSerializer() != null
@@ -71,7 +72,6 @@ public OperationLocationPollingStrategy(PollingStrategyOptions pollingStrategyOp
7172
public Mono<PollResponse<T>> onInitialResponse(Response<?> response, PollingContext<T> pollingContext,
7273
TypeReference<T> pollResponseType) {
7374
// Response<?> is Response<BinaryData>
74-
7575
HttpHeader operationLocationHeader = response.getHeaders().get(PollingUtils.OPERATION_LOCATION_HEADER);
7676
if (operationLocationHeader != null) {
7777
pollingContext.setData(PollingUtils.OPERATION_LOCATION_HEADER.getCaseSensitiveName(),
@@ -80,7 +80,6 @@ public Mono<PollResponse<T>> onInitialResponse(Response<?> response, PollingCont
8080
final String httpMethod = response.getRequest().getHttpMethod().name();
8181
pollingContext.setData(PollingUtils.HTTP_METHOD, httpMethod);
8282
pollingContext.setData(PollingUtils.REQUEST_URL, response.getRequest().getUrl().toString());
83-
8483
if (response.getStatusCode() == 200
8584
|| response.getStatusCode() == 201
8685
|| response.getStatusCode() == 202
@@ -137,4 +136,9 @@ public Mono<U> getResult(PollingContext<T> pollingContext, TypeReference<U> resu
137136
return super.getResult(pollingContext, resultType);
138137
}
139138
}
139+
140+
@Override
141+
public Mono<PollResponse<T>> poll(PollingContext<T> pollingContext, TypeReference<T> pollResponseType) {
142+
return super.poll(pollingContext, pollResponseType).map(AgentsServicePollUtils::remapStatus);
143+
}
140144
}

sdk/ai/azure-ai-agents/src/main/java/com/azure/ai/agents/implementation/SyncOperationLocationPollingStrategy.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33
// Code generated by Microsoft (R) TypeSpec Code Generator.
4-
54
package com.azure.ai.agents.implementation;
65

76
import com.azure.core.exception.AzureException;
@@ -23,7 +22,6 @@
2322
import java.util.Map;
2423

2524
// DO NOT modify this helper class
26-
2725
/**
2826
* Implements a synchronous operation location polling strategy, from Operation-Location.
2927
*
@@ -36,7 +34,9 @@ public final class SyncOperationLocationPollingStrategy<T, U> extends SyncOperat
3634
private static final ClientLogger LOGGER = new ClientLogger(SyncOperationLocationPollingStrategy.class);
3735

3836
private final ObjectSerializer serializer;
37+
3938
private final String endpoint;
39+
4040
private final String propertyName;
4141

4242
/**
@@ -57,7 +57,8 @@ public SyncOperationLocationPollingStrategy(PollingStrategyOptions pollingStrate
5757
* @throws NullPointerException if {@code pollingStrategyOptions} is null.
5858
*/
5959
public SyncOperationLocationPollingStrategy(PollingStrategyOptions pollingStrategyOptions, String propertyName) {
60-
super(PollingUtils.OPERATION_LOCATION_HEADER, pollingStrategyOptions);
60+
super(PollingUtils.OPERATION_LOCATION_HEADER,
61+
AgentsServicePollUtils.withFoundryFeatures(pollingStrategyOptions));
6162
this.propertyName = propertyName;
6263
this.endpoint = pollingStrategyOptions.getEndpoint();
6364
this.serializer = pollingStrategyOptions.getSerializer() != null
@@ -72,7 +73,6 @@ public SyncOperationLocationPollingStrategy(PollingStrategyOptions pollingStrate
7273
public PollResponse<T> onInitialResponse(Response<?> response, PollingContext<T> pollingContext,
7374
TypeReference<T> pollResponseType) {
7475
// Response<?> is Response<BinaryData>
75-
7676
HttpHeader operationLocationHeader = response.getHeaders().get(PollingUtils.OPERATION_LOCATION_HEADER);
7777
if (operationLocationHeader != null) {
7878
pollingContext.setData(PollingUtils.OPERATION_LOCATION_HEADER.getCaseSensitiveName(),
@@ -81,7 +81,6 @@ public PollResponse<T> onInitialResponse(Response<?> response, PollingContext<T>
8181
final String httpMethod = response.getRequest().getHttpMethod().name();
8282
pollingContext.setData(PollingUtils.HTTP_METHOD, httpMethod);
8383
pollingContext.setData(PollingUtils.REQUEST_URL, response.getRequest().getUrl().toString());
84-
8584
if (response.getStatusCode() == 200
8685
|| response.getStatusCode() == 201
8786
|| response.getStatusCode() == 202
@@ -97,7 +96,6 @@ public PollResponse<T> onInitialResponse(Response<?> response, PollingContext<T>
9796
}
9897
return new PollResponse<>(LongRunningOperationStatus.IN_PROGRESS, initialResponseType, retryAfter);
9998
}
100-
10199
throw LOGGER.logExceptionAsError(new AzureException(
102100
String.format("Operation failed or cancelled with status code %d, '%s' header: %s, and response body: %s",
103101
response.getStatusCode(), PollingUtils.OPERATION_LOCATION_HEADER, operationLocationHeader,
@@ -130,4 +128,9 @@ public U getResult(PollingContext<T> pollingContext, TypeReference<U> resultType
130128
return super.getResult(pollingContext, resultType);
131129
}
132130
}
131+
132+
@Override
133+
public PollResponse<T> poll(PollingContext<T> pollingContext, TypeReference<T> pollResponseType) {
134+
return AgentsServicePollUtils.remapStatus(super.poll(pollingContext, pollResponseType));
135+
}
133136
}

sdk/ai/azure-ai-agents/src/test/java/com/azure/ai/agents/MemoryStoresAsyncTests.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@
1212
import com.azure.ai.agents.models.MemoryStoreDetails;
1313
import com.azure.ai.agents.models.MemoryStoreUpdateCompletedResult;
1414
import com.azure.ai.agents.models.MemoryStoreUpdateResponse;
15-
import com.azure.ai.agents.models.MemoryStoreUpdateStatus;
1615
import com.azure.ai.agents.models.PageOrder;
1716
import com.azure.core.exception.ResourceNotFoundException;
1817
import com.azure.core.http.HttpClient;
1918
import com.azure.core.util.polling.AsyncPollResponse;
20-
import com.azure.core.util.polling.LongRunningOperationStatus;
2119
import com.azure.core.util.polling.PollerFlux;
2220
import com.openai.models.responses.EasyInputMessage;
2321
import com.openai.models.responses.ResponseInputItem;
24-
import org.junit.jupiter.api.Disabled;
22+
import org.junit.jupiter.api.Timeout;
2523
import org.junit.jupiter.params.ParameterizedTest;
2624
import org.junit.jupiter.params.provider.MethodSource;
2725
import reactor.core.publisher.Mono;
2826
import reactor.test.StepVerifier;
2927

28+
import java.time.Duration;
3029
import java.util.Arrays;
3130
import java.util.Objects;
3231

@@ -35,12 +34,9 @@
3534
import static org.junit.jupiter.api.Assertions.assertNotNull;
3635
import static org.junit.jupiter.api.Assertions.assertTrue;
3736

38-
@Disabled("Awaiting service versioning consolidation.")
37+
@Timeout(30)
3938
public class MemoryStoresAsyncTests extends ClientTestBase {
4039

41-
private static final LongRunningOperationStatus COMPLETED_OPERATION_STATUS
42-
= LongRunningOperationStatus.fromString(MemoryStoreUpdateStatus.COMPLETED.toString(), true);
43-
4440
@ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS)
4541
@MethodSource("com.azure.ai.agents.TestUtils#getTestParameters")
4642
public void basicMemoryStoresCrud(HttpClient httpClient, AgentsServiceVersion serviceVersion) {
@@ -282,15 +278,9 @@ private static Mono<Void> cleanupBeforeTest(MemoryStoresAsyncClient memoryStoreC
282278
private static Mono<MemoryStoreUpdateCompletedResult>
283279
waitForUpdateCompletion(PollerFlux<MemoryStoreUpdateResponse, MemoryStoreUpdateCompletedResult> pollerFlux) {
284280
Objects.requireNonNull(pollerFlux, "pollerFlux cannot be null");
285-
return pollerFlux.takeUntil(response -> COMPLETED_OPERATION_STATUS.equals(response.getStatus()))
281+
return pollerFlux.takeUntil(response -> response.getStatus().isComplete())
282+
.timeout(Duration.ofSeconds(30))
286283
.last()
287-
.map(AsyncPollResponse::getValue)
288-
.map(response -> {
289-
MemoryStoreUpdateCompletedResult result = response == null ? null : response.getResult();
290-
if (result == null) {
291-
throw new IllegalStateException("Memory store update did not complete successfully.");
292-
}
293-
return result;
294-
});
284+
.flatMap(AsyncPollResponse::getFinalResult);
295285
}
296286
}

0 commit comments

Comments
 (0)