Skip to content

ADR to support a multi-source architecture for agent skills#4787

Open
SergeyMenshykh wants to merge 6 commits intomicrosoft:mainfrom
SergeyMenshykh:agent-skills-adr
Open

ADR to support a multi-source architecture for agent skills#4787
SergeyMenshykh wants to merge 6 commits intomicrosoft:mainfrom
SergeyMenshykh:agent-skills-adr

Conversation

@SergeyMenshykh
Copy link
Member

This PR adds an ADR proposing a new design to support a multi-source architecture for agent skills.

Copilot AI review requested due to automatic review settings March 19, 2026 12:46
@markwallace-microsoft markwallace-microsoft added documentation Improvements or additions to documentation .NET labels Mar 19, 2026
@github-actions github-actions bot changed the title Add ADR suggesting a new design to support a multi-source architecture for agent skills .NET: Add ADR suggesting a new design to support a multi-source architecture for agent skills Mar 19, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Architectural Decision Record proposing a multi-source “agent skills” architecture for the .NET Agent Framework, covering file-based, code-defined, and class-based skills along with composition/filtering/caching strategies.

Changes:

  • Introduces a new ADR describing abstractions for skills, resources, scripts, and skill sources.
  • Documents alternative approaches for composition, filtering, caching, and deduplication.
  • Proposes a builder pattern for configuring an AgentSkillsProvider from multiple sources.

@SergeyMenshykh SergeyMenshykh self-assigned this Mar 19, 2026
@SergeyMenshykh SergeyMenshykh moved this to In Review in Agent Framework Mar 19, 2026
@SergeyMenshykh SergeyMenshykh changed the title .NET: Add ADR suggesting a new design to support a multi-source architecture for agent skills ADR to support a multi-source architecture for agent skills Mar 19, 2026
Comment on lines +57 to +62
public abstract class AgentSkillScript
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> ExecuteAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would make sense, making it an AIFunction ? It seems to address this purpose, given the AIFunctionArguments so you just add AgentSkillScriptAIFunction to the AgentSkillScript.

And use the InvokeAsync as is? Including all the IServiceProvider capabilities, etc?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried it both ways. I prefer the expressiveness of the ExecuteAsync signature - as an implementer of a custom script, you clearly see the parameters the method accepts (e.g., the owning AgentSkill). With AIFunction as the base, users would override InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) and rely on documentation or reverse engineering to discover which arguments it's called with and how to access them.

Comment on lines +122 to +125
public override Task<object?> ReadAsync(AIFunctionArguments arguments, CancellationToken cancellationToken = default)
{
return File.ReadAllTextAsync(path, cancellationToken);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is AIFunctionArguments intended here?

Suggested change
public override Task<object?> ReadAsync(AIFunctionArguments arguments, CancellationToken cancellationToken = default)
{
return File.ReadAllTextAsync(path, cancellationToken);
}
public override Task<object?> ReadAsync(CancellationToken cancellationToken = default)
{
return File.ReadAllTextAsync(path, cancellationToken);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's intentional for a few reasons:

  1. DI support: AIFunctionArguments contains an IServiceProvider, enabling resource functions to access services/dependencies from the service collection. We could use IServiceProvider directly, but that would break symmetry (see point 2) and wouldn't allow us to support LLM-provided arguments in the future without a breaking change.
  2. Symmetry: It keeps the signature consistent with AgentSkillScript.ExecuteAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default).

The base class defines the contract broadly; concrete implementations use what they need. The arguments are used by scripts for code-defined skills. For file-based skills, they are not relevant and not used.

AgentSkill skill, AgentFileSkillScript script,
AIFunctionArguments arguments, CancellationToken cancellationToken);

public sealed class AgentFileSkillScript : AgentSkillScript
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we don't need the abstract AgentSkillScript at all if we can use the AIFunction directly and implement one specific.

Suggested change
public sealed class AgentFileSkillScript : AgentSkillScript
public sealed class AgentFileSkillScriptAIFunction : AIFunction

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the reasoning above - with AIFunction, the owning AgentSkill has to be smuggled through AIFunctionArguments via a magic string key:

// With AgentSkillScript - contract is explicit
public override Task<object?> ExecuteAsync(
    AgentSkill skill,              // ← owning skill is right here
    AIFunctionArguments arguments,
    CancellationToken ct)
{
    var scriptPath = Path.Combine(skill.SourcePath, this.Path);
    return this._executor(skill, this, arguments, ct);
}

// With AIFunction - skill is hidden behind a convention
protected override ValueTask<object?> InvokeCoreAsync(
    AIFunctionArguments arguments,
    CancellationToken ct)
{
    // Caller must remember to do: arguments["__agentSkill"] = skill
    var skill = arguments["__agentSkill"] as AgentSkill
        ?? throw new InvalidOperationException("...");
    var scriptPath = Path.Combine(skill.SourcePath, this.Path);
    return this._executor(skill, this, arguments, ct);
}

ExecuteAsync makes the skill-script relationship a compile-time contract. With AIFunction, it becomes a runtime convention.

Comment on lines +555 to +559
var resources = doc.Resources?.Select(
r => new AgentCodeSkillResource(r.Content, r.Name, r.Description)).ToList();

skills.Add(new AgentCodeSkill(frontmatter, doc.Instructions)
.AddResources(resources));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any special reason in this example for retrieving a CodeSkillResource from the CosmosDB ?

Maybe a real Delegate sample would fit best for this type.

Comment on lines +512 to +514
.AddFileSkills("./skills") // file-based source
.AddCodeSkills(codeSkill) // code-defined source
.AddClassSkills(new ClassSkill()) // class-based source
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our other builders have the Use naming, we might consider UseFileSkills(...).

UseFileScriptExecutor()
UseScriptApproval()
UsePromptTemplate().

Comment on lines +576 to +584
var compositeSource = new CompositeAgentSkillsSource([
new CachingSkillsSource(new CosmosDbSkillsSource(cosmosClient)),
new AgentFileSkillsSource(["./skills"]),
]);
var provider = new AgentSkillsProvider(compositeSource);

// Or via the builder (requires registering the source type with the builder)
var provider = new AgentSkillsProviderBuilder()
.AddSource(new CosmosDbSkillsSource(cosmosClient))
Copy link
Member

@rogerbarreto rogerbarreto Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would drop the CompositeAgentSkillSource and suggest we keep via builder similar how we do for AIAgents, Middlewares, etc.

i.e

Suggested change
var compositeSource = new CompositeAgentSkillsSource([
new CachingSkillsSource(new CosmosDbSkillsSource(cosmosClient)),
new AgentFileSkillsSource(["./skills"]),
]);
var provider = new AgentSkillsProvider(compositeSource);
// Or via the builder (requires registering the source type with the builder)
var provider = new AgentSkillsProviderBuilder()
.AddSource(new CosmosDbSkillsSource(cosmosClient))
var provider = new AgentSkillsProvider(compositeSource);
// Or via the builder (requires registering the source type with the builder)
var provider = new AgentSkillsProviderBuilder()
.UseSource(new CachingSkillsSource(new CosmosDbSkillsSource(cosmosClient))
.UseSource(new AgentFileSkillsSource(["./skills"])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation .NET

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

4 participants