Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,36 @@ namespace Microsoft.Agents.AI;
internal sealed class McpListedSkillsCache : IDisposable
{
private readonly TimeProvider _timeProvider;
private readonly Func<CancellationToken, Task<McpListedSkillsSnapshot>> _discoverSkills;
private readonly SemaphoreSlim _discoveryGate = new(1, 1);
private volatile McpListedSkillsDiscoveryResult? _cachedSkills;
private volatile McpListedSkillsSnapshot? _cachedSnapshot;
private bool _disposed;

/// <summary>
/// Initializes a cache using the clock that records discovery page receipt timestamps.
/// </summary>
/// <param name="timeProvider">The clock for checking page freshness.</param>
public McpListedSkillsCache(TimeProvider timeProvider)
/// <param name="discoverSkills">Discovers and validates all pages, including their receipt timestamps and TTLs.</param>
public McpListedSkillsCache(TimeProvider timeProvider, Func<CancellationToken, Task<McpListedSkillsSnapshot>> discoverSkills)
{
this._timeProvider = Throw.IfNull(timeProvider);
this._discoverSkills = Throw.IfNull(discoverSkills);
}

/// <summary>
/// Gets fresh cached skills or invokes discovery to replace the catalog.
/// </summary>
/// <param name="discoverSkills">Discovers and validates all pages, including their receipt timestamps and TTLs.</param>
/// <param name="cancellationToken">The token used to cancel waiting or discovery.</param>
/// <returns>The cached or newly discovered skills.</returns>
public async Task<IList<AgentSkill>> GetOrCreateAsync(Func<CancellationToken, Task<McpListedSkillsDiscoveryResult>> discoverSkills, CancellationToken cancellationToken)
public async Task<IList<AgentSkill>> GetOrCreateAsync(CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(this._disposed, this);
cancellationToken.ThrowIfCancellationRequested();

// Reuse the catalog only while every page is fresh.
if (this.GetCachedSkills() is { } cached)
if (this.GetCachedSnapshot() is { } cached)
{
return cached;
return cached.Skills;
}

// Allow only one caller to discover or refresh the catalog at a time.
Expand All @@ -52,20 +54,20 @@ public async Task<IList<AgentSkill>> GetOrCreateAsync(Func<CancellationToken, Ta
cancellationToken.ThrowIfCancellationRequested();

// Another caller may have refreshed the cache while this caller waited.
cached = this.GetCachedSkills();
cached = this.GetCachedSnapshot();
if (cached is not null)
{
return cached;
return cached.Skills;
}

// Refresh all pages from the beginning.
var result = await discoverSkills(cancellationToken).ConfigureAwait(false);
var snapshot = await this._discoverSkills(cancellationToken).ConfigureAwait(false);

cancellationToken.ThrowIfCancellationRequested();

this.CacheSkills(result);
this.CacheSnapshot(snapshot);

return result.Skills;
return snapshot.Skills;
}
finally
{
Expand All @@ -74,11 +76,11 @@ public async Task<IList<AgentSkill>> GetOrCreateAsync(Func<CancellationToken, Ta
}

/// <summary>
/// Gets the cached skills, or <see langword="null"/> if the cache is absent or expired.
/// Gets the cached snapshot, or <see langword="null"/> if it is absent or any listing page has expired.
/// </summary>
private List<AgentSkill>? GetCachedSkills()
private McpListedSkillsSnapshot? GetCachedSnapshot()
{
if (this._cachedSkills is not { } cached)
if (this._cachedSnapshot is not { } cached)
{
return null;
}
Expand All @@ -94,17 +96,17 @@ public async Task<IList<AgentSkill>> GetOrCreateAsync(Func<CancellationToken, Ta
}
}

return cached.Skills;
return cached;
}

/// <summary>
/// Caches the discovered skills together with their page lifetimes.
/// Stores the snapshot.
/// </summary>
/// <param name="result">The complete discovery result.</param>
private void CacheSkills(McpListedSkillsDiscoveryResult result)
/// <param name="snapshot">The skill list and its listing-page lifetimes.</param>
private void CacheSnapshot(McpListedSkillsSnapshot snapshot)
{
// Publish the complete catalog and page lifetimes together only after discovery and validation succeed.
this._cachedSkills = result;
this._cachedSnapshot = snapshot;
}

/// <inheritdoc/>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public McpListedSkillsLoader(McpClient client, ILogger? logger = null, AgentMcpS
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(this._maxResourceSizeBytes, nameof(options.ListedResourceMaxSizeBytes));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(this._maxPageCount, nameof(options.ListedMaxPageCount));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(this._maxSkillCount, nameof(options.ListedMaxSkillCount));
this._cache = new McpListedSkillsCache(this._timeProvider);
this._cache = new McpListedSkillsCache(this._timeProvider, this.DiscoverCoreAsync);
}

/// <summary>
Expand All @@ -76,10 +76,10 @@ public McpListedSkillsLoader(McpClient client, ILogger? logger = null, AgentMcpS
/// <exception cref="ObjectDisposedException">The loader has been disposed.</exception>
public Task<IList<AgentSkill>> DiscoverAsync(CancellationToken cancellationToken)
{
return this._cache.GetOrCreateAsync(this.DiscoverCoreAsync, cancellationToken);
return this._cache.GetOrCreateAsync(cancellationToken);
}

private async Task<McpListedSkillsDiscoveryResult> DiscoverCoreAsync(CancellationToken cancellationToken)
private async Task<McpListedSkillsSnapshot> DiscoverCoreAsync(CancellationToken cancellationToken)
{
var (skillEntries, pageLifetimes) = await this.ListSkillEntriesAsync(cancellationToken).ConfigureAwait(false);

Expand All @@ -95,7 +95,7 @@ private async Task<McpListedSkillsDiscoveryResult> DiscoverCoreAsync(Cancellatio
skills.Add(new AgentMcpListedSkill(frontmatter, entry.Uri, resources, this._client, this._logger, this._maxResourceSizeBytes));
}

return new McpListedSkillsDiscoveryResult(skills, pageLifetimes);
return new McpListedSkillsSnapshot(skills, pageLifetimes);
}

private async Task<(List<McpListedSkillEntry> Entries, List<McpListedSkillsPageLifetime> PageLifetimes)> ListSkillEntriesAsync(CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;

namespace Microsoft.Agents.AI;

/// <summary>
/// A skill list and its listing-page lifetimes at a point in time.
/// </summary>
internal sealed class McpListedSkillsSnapshot
{
/// <summary>
/// Initializes a snapshot with its skills and listing-page lifetimes.
/// </summary>
/// <param name="skills">The complete skill list for this snapshot.</param>
/// <param name="pageLifetimes">The receipt timestamp and TTL of each listing page.</param>
public McpListedSkillsSnapshot(List<AgentSkill> skills, List<McpListedSkillsPageLifetime> pageLifetimes)
{
this.Skills = skills;
this.PageLifetimes = pageLifetimes;
}

/// <summary>
/// Gets the complete skill list for this snapshot.
/// </summary>
public List<AgentSkill> Skills { get; }

/// <summary>
/// Gets the receipt timestamp and TTL of each listing page.
/// </summary>
public List<McpListedSkillsPageLifetime> PageLifetimes { get; }
}
Loading
Loading