mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Fix PR review issues: retry off-by-one, URI encoding, docs, tests, build
- Fix off-by-one in FoundryToolboxBearerTokenHandler retry loop (4 attempts → 3) - URI-encode version parameter in HostedMcpToolboxAITool.BuildAddress - Add XML doc clarifying version pinning is reserved for future use - Add comment clarifying AddHostedService deduplication safety - Fix DevTemporaryTokenCredential expiry to use DateTimeOffset.MaxValue - Fix AgentCard ambiguity in A2AServer sample with using alias - Add 18 new unit tests for retry handler and ReadMcpToolboxMarkers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -34,7 +34,11 @@ public sealed class HostedMcpToolboxAITool : HostedMcpServerTool
|
||||
/// Initializes a new instance of the <see cref="HostedMcpToolboxAITool"/> class.
|
||||
/// </summary>
|
||||
/// <param name="toolboxName">The Foundry toolbox name.</param>
|
||||
/// <param name="version">Optional pinned toolbox version. When <see langword="null"/>, the project's default version is used.</param>
|
||||
/// <param name="version">
|
||||
/// Optional pinned toolbox version. When <see langword="null"/>, the project's default version is used.
|
||||
/// Currently reserved for forward compatibility — version-specific routing is handled server-side by
|
||||
/// the Foundry proxy.
|
||||
/// </param>
|
||||
public HostedMcpToolboxAITool(string toolboxName, string? version = null)
|
||||
: base(
|
||||
serverName: NotNullOrWhitespace(toolboxName, nameof(toolboxName)),
|
||||
@@ -63,7 +67,7 @@ public sealed class HostedMcpToolboxAITool : HostedMcpServerTool
|
||||
|
||||
return string.IsNullOrEmpty(version)
|
||||
? $"{UriScheme}://{toolboxName}"
|
||||
: $"{UriScheme}://{toolboxName}?version={version}";
|
||||
: $"{UriScheme}://{toolboxName}?version={Uri.EscapeDataString(version)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -48,6 +48,7 @@ internal sealed class FoundryToolboxBearerTokenHandler : DelegatingHandler
|
||||
request.Headers.TryAddWithoutValidation("Foundry-Features", this._featuresHeaderValue);
|
||||
}
|
||||
|
||||
// MaxRetries is the total number of attempts (not additional retries after the first).
|
||||
for (int attempt = 0; attempt < MaxRetries; attempt++)
|
||||
{
|
||||
// Clone the request for retries (the original request cannot be sent twice)
|
||||
@@ -65,19 +66,20 @@ internal sealed class FoundryToolboxBearerTokenHandler : DelegatingHandler
|
||||
return response;
|
||||
}
|
||||
|
||||
// Last attempt exhausted — return the error response as-is.
|
||||
if (attempt == MaxRetries - 1)
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
response.Dispose();
|
||||
|
||||
if (attempt < MaxRetries - 1)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Final attempt after backoff exhausted — return last response (already disposed above, so resend)
|
||||
return await base.SendAsync(
|
||||
await CloneRequestAsync(request, cancellationToken).ConfigureAwait(false),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
// Unreachable when MaxRetries > 0, but satisfies the compiler.
|
||||
throw new InvalidOperationException("Retry loop completed without returning a response.");
|
||||
}
|
||||
|
||||
private static async Task<HttpRequestMessage> CloneRequestAsync(
|
||||
|
||||
@@ -127,7 +127,11 @@ public sealed class FoundryToolboxService : IHostedService, IAsyncDisposable
|
||||
/// <see cref="FoundryToolboxOptions.StrictMode"/> to either reject or lazily open it.
|
||||
/// </summary>
|
||||
/// <param name="toolboxName">The Foundry toolbox name from the marker.</param>
|
||||
/// <param name="version">Optional pinned version; ignored when matching a pre-registered entry.</param>
|
||||
/// <param name="version">
|
||||
/// Optional pinned version. Currently reserved for future use — version-specific routing is
|
||||
/// handled server-side by the Foundry proxy. This parameter is accepted for forward compatibility
|
||||
/// but does not affect the proxy URL used to connect to the toolbox.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">The request cancellation token.</param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the toolbox is not pre-registered and <see cref="FoundryToolboxOptions.StrictMode"/>
|
||||
|
||||
@@ -161,7 +161,8 @@ public static class FoundryHostingExtensions
|
||||
// Register FoundryToolboxService as a singleton so it can be injected into the handler
|
||||
services.TryAddSingleton<FoundryToolboxService>();
|
||||
|
||||
// Add it as a hosted service so StartAsync is called before the app starts serving requests
|
||||
// AddHostedService uses TryAddEnumerable internally, so calling AddFoundryToolboxes
|
||||
// multiple times will not invoke StartAsync twice on the same singleton.
|
||||
services.AddHostedService(sp => sp.GetRequiredService<FoundryToolboxService>());
|
||||
|
||||
return services;
|
||||
|
||||
Reference in New Issue
Block a user