Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -78,7 +78,7 @@ public AgentMcpSkillsSource(McpClient client, AgentMcpSkillsSourceOptions? optio
}

/// <inheritdoc/>
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
public override async Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default)
{
if (this.TryGetCachedSkills() is { } cached)
{
Expand All @@ -99,7 +99,7 @@ public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken c
{
// The refresh owner uses CancellationToken.None so that a single caller's cancellation
// does not abort the shared refresh for all concurrent waiters.
var skills = await this.GetCoreSkillsAsync(CancellationToken.None).ConfigureAwait(false);
var skills = await this.GetCoreSkillsAsync(context, CancellationToken.None).ConfigureAwait(false);

this.UpdateCache(skills);

Expand Down Expand Up @@ -155,7 +155,7 @@ private void UpdateCache(IList<AgentSkill> skills)
/// Reads the skill index from the MCP server, dispatches entries to registered loaders, and
/// returns the aggregated skill list.
/// </summary>
private async Task<IList<AgentSkill>> GetCoreSkillsAsync(CancellationToken cancellationToken)
private async Task<IList<AgentSkill>> GetCoreSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken)
{
McpSkillIndex? index = await this.TryReadIndexAsync(cancellationToken).ConfigureAwait(false);

Expand Down Expand Up @@ -185,7 +185,7 @@ private async Task<IList<AgentSkill>> GetCoreSkillsAsync(CancellationToken cance
foreach (var loader in this._loaders.Values)
{
var entries = entriesByType.TryGetValue(loader.EntryType, out List<McpSkillIndexEntry>? matched) ? matched : [];
skills.AddRange(await loader.LoadAsync(entries, cancellationToken).ConfigureAwait(false));
skills.AddRange(await loader.LoadAsync(entries, context, cancellationToken).ConfigureAwait(false));
}

LogSkillsLoadedTotal(this._logger, skills.Count);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public ArchiveEntryLoader(McpClient client, AgentMcpSkillsSourceOptions? options
public string EntryType => "archive";

/// <inheritdoc/>
public async Task<IList<AgentSkill>> LoadAsync(IReadOnlyList<McpSkillIndexEntry> entries, CancellationToken cancellationToken)
public async Task<IList<AgentSkill>> LoadAsync(IReadOnlyList<McpSkillIndexEntry> entries, AgentSkillsSourceContext context, CancellationToken cancellationToken)
{
// Filter out entries that are missing required fields or have invalid names.
var archiveEntries = this.FilterValidEntries(entries);
Expand Down Expand Up @@ -81,7 +81,7 @@ public async Task<IList<AgentSkill>> LoadAsync(IReadOnlyList<McpSkillIndexEntry>
// Delegate discovery of extracted content to a file-based skills source.
AgentFileSkillsSource fileSource = this.CreateFileSkillsSource(skillDirectories);

return await fileSource.GetSkillsAsync(cancellationToken).ConfigureAwait(false);
return await fileSource.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal interface IMcpSkillEntryLoader
/// loaded successfully.
/// </summary>
/// <param name="entries">The index entries of this loader's <see cref="EntryType"/>. May be empty.</param>
/// <param name="context">Contextual information about the agent and session requesting skills.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task<IList<AgentSkill>> LoadAsync(IReadOnlyList<McpSkillIndexEntry> entries, CancellationToken cancellationToken);
Task<IList<AgentSkill>> LoadAsync(IReadOnlyList<McpSkillIndexEntry> entries, AgentSkillsSourceContext context, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public SkillMdEntryLoader(McpClient client, ILoggerFactory loggerFactory)
public string EntryType => "skill-md";

/// <inheritdoc/>
public Task<IList<AgentSkill>> LoadAsync(IReadOnlyList<McpSkillIndexEntry> entries, CancellationToken cancellationToken)
public Task<IList<AgentSkill>> LoadAsync(IReadOnlyList<McpSkillIndexEntry> entries, AgentSkillsSourceContext context, CancellationToken cancellationToken)
{
var skills = new List<AgentSkill>();

Expand Down
1 change: 1 addition & 0 deletions dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />

<InternalsVisibleTo Include="Microsoft.Agents.AI.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.AI.Mcp.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.AI.Declarative.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.AI.Hosting.UnitTests" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public AgentInMemorySkillsSource(IEnumerable<AgentSkill> skills)
}

/// <inheritdoc/>
public override Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
public override Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default)
{
return Task.FromResult<IList<AgentSkill>>(this._skills);
}
Expand Down
35 changes: 35 additions & 0 deletions dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillFilterContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// Provides contextual information to a skill filter predicate.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentSkillFilterContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentSkillFilterContext"/> class.
/// </summary>
/// <param name="skill">The skill being evaluated by the filter.</param>
/// <param name="skillsSourceContext">Contextual information about the agent and session requesting skills.</param>
internal AgentSkillFilterContext(AgentSkill skill, AgentSkillsSourceContext skillsSourceContext)
{
this.Skill = Throw.IfNull(skill);
this.SkillsSourceContext = Throw.IfNull(skillsSourceContext);
}

/// <summary>
/// Gets the skill being evaluated by the filter.
/// </summary>
public AgentSkill Skill { get; }

/// <summary>
/// Gets contextual information about the agent and session requesting skills.
/// </summary>
public AgentSkillsSourceContext SkillsSourceContext { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public AgentSkillsProvider(AgentSkillsSource source, AgentSkillsProviderOptions?
/// <inheritdoc />
protected override async ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var skills = await this._source.GetSkillsAsync(cancellationToken).ConfigureAwait(false);
var skills = await this._source.GetSkillsAsync(new AgentSkillsSourceContext(context.Agent, context.Session), cancellationToken).ConfigureAwait(false);
if (skills is not { Count: > 0 })
{
return await base.ProvideAIContextAsync(context, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ public sealed class AgentSkillsProviderBuilder
private AgentSkillsProviderOptions? _options;
private ILoggerFactory? _loggerFactory;
private AgentFileSkillScriptRunner? _scriptRunner;
private Func<AgentSkill, bool>? _filter;
private Func<AgentSkillFilterContext, bool>? _filter;
private bool _disableCaching;
private CachingAgentSkillsSourceOptions? _cachingOptions;

/// <summary>
/// Adds a file-based skill source that discovers skills from a filesystem directory.
Expand Down Expand Up @@ -189,7 +190,7 @@ public AgentSkillsProviderBuilder UseLoggerFactory(ILoggerFactory loggerFactory)
/// </remarks>
/// <param name="predicate">A predicate that determines which skills to include.</param>
/// <returns>This builder instance for chaining.</returns>
public AgentSkillsProviderBuilder UseFilter(Func<AgentSkill, bool> predicate)
public AgentSkillsProviderBuilder UseFilter(Func<AgentSkillFilterContext, bool> predicate)
{
_ = Throw.IfNull(predicate);
this._filter = predicate;
Expand Down Expand Up @@ -219,6 +220,19 @@ public AgentSkillsProviderBuilder DisableCaching()
return this;
}

/// <summary>
/// Configures skill caching behavior.
/// </summary>
/// <param name="configure">A delegate to configure caching options.</param>
/// <returns>This builder instance for chaining.</returns>
public AgentSkillsProviderBuilder UseCachingOptions(Action<CachingAgentSkillsSourceOptions> configure)
{
_ = Throw.IfNull(configure);
this._cachingOptions ??= new CachingAgentSkillsSourceOptions();
configure(this._cachingOptions);
return this;
}

/// <summary>
/// Builds the <see cref="AgentSkillsProvider"/>.
/// </summary>
Expand All @@ -243,7 +257,7 @@ public AgentSkillsProvider Build()

if (!this._disableCaching)
{
source = new CachingAgentSkillsSource(source);
source = new CachingAgentSkillsSource(source, this._cachingOptions);
}

// Apply user-specified filter, then dedup.
Expand Down
3 changes: 2 additions & 1 deletion dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public abstract class AgentSkillsSource
/// <summary>
/// Gets the skills provided by this source.
/// </summary>
/// <param name="context">Contextual information about the agent and session requesting skills.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A collection of skills from this source.</returns>
public abstract Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default);
public abstract Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default);
}
36 changes: 36 additions & 0 deletions dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsSourceContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// Provides contextual information about the agent and session to an <see cref="AgentSkillsSource"/>
/// when retrieving skills.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentSkillsSourceContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentSkillsSourceContext"/> class.
/// </summary>
/// <param name="agent">The agent requesting skills.</param>
/// <param name="session">The session associated with the agent invocation, if any.</param>
internal AgentSkillsSourceContext(AIAgent agent, AgentSession? session)
{
this.Agent = Throw.IfNull(agent);
Comment thread
SergeyMenshykh marked this conversation as resolved.
this.Session = session;
}

/// <summary>
/// Gets the agent requesting skills.
/// </summary>
public AIAgent Agent { get; }

/// <summary>
/// Gets the session associated with the agent invocation, if any.
/// </summary>
public AgentSession? Session { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ public AggregatingAgentSkillsSource(IEnumerable<AgentSkillsSource> sources)
}

/// <inheritdoc/>
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
public override async Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default)
{
var allSkills = new List<AgentSkill>();
foreach (var source in this._sources)
{
var skills = await source.GetSkillsAsync(cancellationToken).ConfigureAwait(false);
var skills = await source.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false);
allSkills.AddRange(skills);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -18,42 +19,52 @@ namespace Microsoft.Agents.AI;
/// </remarks>
internal sealed class CachingAgentSkillsSource : DelegatingAgentSkillsSource
{
private Task<IList<AgentSkill>>? _cachedTask;
private const string SharedCacheKey = "";
Comment thread
SergeyMenshykh marked this conversation as resolved.
Outdated

private readonly ConcurrentDictionary<string, Task<IList<AgentSkill>>> _cachedTasks = new(StringComparer.Ordinal);
private readonly CachingAgentSkillsSourceOptions? _options;

/// <summary>
/// Initializes a new instance of the <see cref="CachingAgentSkillsSource"/> class.
/// </summary>
/// <param name="innerSource">The inner source whose results will be cached.</param>
internal CachingAgentSkillsSource(AgentSkillsSource innerSource)
/// <param name="options">Optional cache configuration.</param>
internal CachingAgentSkillsSource(AgentSkillsSource innerSource, CachingAgentSkillsSourceOptions? options = null)
: base(innerSource)
{
this._options = options;
}

/// <inheritdoc/>
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
public override async Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default)
{
var cacheKey = this._options?.CacheIsolationKeySelector?.Invoke(context) ?? SharedCacheKey;

var tcs = new TaskCompletionSource<IList<AgentSkill>>(TaskCreationOptions.RunContinuationsAsynchronously);

if (Interlocked.CompareExchange(ref this._cachedTask, tcs.Task, null) is { } existing)
while (!this._cachedTasks.TryAdd(cacheKey, tcs.Task))
{
return await existing.ConfigureAwait(false);
if (this._cachedTasks.TryGetValue(cacheKey, out var existing))
{
return await existing.ConfigureAwait(false);
}
}

try
{
var result = await this.InnerSource.GetSkillsAsync(cancellationToken).ConfigureAwait(false);
var result = await this.InnerSource.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false);
tcs.SetResult(result);
return result;
}
catch (OperationCanceledException)
{
Volatile.Write(ref this._cachedTask, null);
this._cachedTasks.TryRemove(cacheKey, out _);
tcs.TrySetCanceled(cancellationToken);
throw;
}
catch (Exception ex)
{
Volatile.Write(ref this._cachedTask, null);
this._cachedTasks.TryRemove(cacheKey, out _);
tcs.TrySetException(ex);
throw;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Agents.AI;

/// <summary>
/// Options for configuring <see cref="CachingAgentSkillsSource"/>.
/// </summary>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class CachingAgentSkillsSourceOptions
{
/// <summary>
/// Gets or sets a delegate that returns the cache isolation key for a skills source invocation.
/// </summary>
/// <remarks>
/// When this delegate is <see langword="null"/>, or when it returns <see langword="null"/>,
/// the skills are stored in the shared cache bucket. When it returns a non-null string,
/// the skills are cached under that key.
/// </remarks>
Comment thread
SergeyMenshykh marked this conversation as resolved.
public Func<AgentSkillsSourceContext, string?>? CacheIsolationKeySelector { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public DeduplicatingAgentSkillsSource(AgentSkillsSource innerSource, ILoggerFact
}

/// <inheritdoc/>
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
public override async Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default)
{
var allSkills = await this.InnerSource.GetSkillsAsync(cancellationToken).ConfigureAwait(false);
var allSkills = await this.InnerSource.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false);

var deduplicated = new List<AgentSkill>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ protected DelegatingAgentSkillsSource(AgentSkillsSource innerSource)
protected AgentSkillsSource InnerSource { get; }

/// <inheritdoc/>
public override Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
=> this.InnerSource.GetSkillsAsync(cancellationToken);
public override Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default)
=> this.InnerSource.GetSkillsAsync(context, cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI;
/// </remarks>
internal sealed partial class FilteringAgentSkillsSource : DelegatingAgentSkillsSource
{
private readonly Func<AgentSkill, bool> _predicate;
private readonly Func<AgentSkillFilterContext, bool> _predicate;
private readonly ILogger<FilteringAgentSkillsSource> _logger;

/// <summary>
Expand All @@ -33,7 +33,7 @@ internal sealed partial class FilteringAgentSkillsSource : DelegatingAgentSkills
/// <param name="loggerFactory">Optional logger factory.</param>
public FilteringAgentSkillsSource(
AgentSkillsSource innerSource,
Func<AgentSkill, bool> predicate,
Func<AgentSkillFilterContext, bool> predicate,
ILoggerFactory? loggerFactory = null)
: base(innerSource)
{
Expand All @@ -42,14 +42,14 @@ public FilteringAgentSkillsSource(
}

/// <inheritdoc/>
public override async Task<IList<AgentSkill>> GetSkillsAsync(CancellationToken cancellationToken = default)
public override async Task<IList<AgentSkill>> GetSkillsAsync(AgentSkillsSourceContext context, CancellationToken cancellationToken = default)
{
var allSkills = await this.InnerSource.GetSkillsAsync(cancellationToken).ConfigureAwait(false);
var allSkills = await this.InnerSource.GetSkillsAsync(context, cancellationToken).ConfigureAwait(false);

var filtered = new List<AgentSkill>();
foreach (var skill in allSkills)
{
if (this._predicate(skill))
if (this._predicate(new AgentSkillFilterContext(skill, context)))
Comment thread
SergeyMenshykh marked this conversation as resolved.
Outdated
{
filtered.Add(skill);
}
Expand Down
Loading
Loading