Skip to content
Open
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
3 changes: 3 additions & 0 deletions .aspire/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"appHostPath": "..\\src\\BotSharp.AppHost\\BotSharp.AppHost.csproj"
}
Comment on lines +1 to +3
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

PR description says the change only adds .aspire/settings.json, but this PR also introduces a large AgentSkills plugin implementation, new test projects/assets, and storage model changes. Update the PR description to accurately reflect the scope, or split into separate PRs to keep reviews manageable.

Copilot uses AI. Check for mistakes.
269 changes: 269 additions & 0 deletions BotSharp.sln

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
<PackageVersion Include="LLamaSharp" Version="0.25.0" />
<PackageVersion Include="FaissMask" Version="0.4.2" />
<PackageVersion Include="FastText.NetWrapper" Version="1.3.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.0" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.2" />
<PackageVersion Include="MongoDB.Driver" Version="3.5.0" />
<PackageVersion Include="Docnet.Core" Version="2.7.0-alpha.1" />
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="14.9.1" />
Expand All @@ -62,6 +62,7 @@
<PackageVersion Include="Qdrant.Client" Version="1.15.1" />
<PackageVersion Include="Selenium.WebDriver" Version="4.27.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Abstractions" Version="1.67.1" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.Memory" Version="1.16.0-alpha" />
<PackageVersion Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
Expand Down Expand Up @@ -94,6 +95,9 @@
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
<PackageVersion Include="CsCheck" Version="4.0.0" />
<PackageVersion Include="MSTest.TestAdapter" Version="4.0.2" />
<PackageVersion Include="MSTest.TestFramework" Version="4.0.2" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
Expand Down Expand Up @@ -123,10 +127,12 @@
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="2.3.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
Expand All @@ -148,10 +154,12 @@
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="2.3.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
Expand All @@ -173,10 +181,12 @@
<PackageVersion Include="Microsoft.AspNetCore.StaticFiles" Version="2.3.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.3" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public enum AgentField
McpTool,
KnowledgeBase,
Rule,
MaxMessageCount
MaxMessageCount,
Skills
}

public enum AgentTaskField
Expand Down
12 changes: 12 additions & 0 deletions src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ public class Agent
[JsonIgnore]
public List<string> SecondaryInstructions { get; set; } = [];

/// <summary>
/// Agent skills, which is a collection of instructions, functions, and other resources for specific domains or tasks.
/// </summary>
public List<AgentSkill> Skills { get; set; } = new();

public override string ToString()
=> $"{Name} {Id}";

Expand Down Expand Up @@ -193,6 +198,7 @@ public static Agent Clone(Agent agent)
Rules = agent.Rules,
LlmConfig = agent.LlmConfig,
KnowledgeBases = agent.KnowledgeBases,
Skills = agent.Skills,
CreatedDateTime = agent.CreatedDateTime,
UpdatedDateTime = agent.UpdatedDateTime,
};
Expand Down Expand Up @@ -346,4 +352,10 @@ public Agent SetMcpTools(List<McpTool>? mcps)
McpTools = mcps ?? [];
return this;
}

public Agent SetSkills(List<AgentSkill>? skills)
{
Skills = skills ?? [];
return this;
Comment on lines +356 to +359
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

2. setskills assigns shared list 📘 Rule violation ⛯ Reliability

Agent.SetSkills assigns the caller-provided list directly to Skills, allowing the caller to
mutate agent state after assignment. This violates the defensive-copy requirement for
caller-provided collections.
Agent Prompt
## Issue description
`Agent.SetSkills` stores the input `skills` list by reference instead of copying it.

## Issue Context
Callers can retain the reference and mutate it later, causing unexpected agent state changes.

## Fix Focus Areas
- src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs[356-359]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace BotSharp.Abstraction.Agents.Models;

public class AgentSkill
{
/// <summary>
/// Name of the Skill
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; set; }

/// <summary>
/// Description of the Skill
/// </summary>
[JsonPropertyName("description")]
public required string Description { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public async Task UpdateAgent(Agent agent, AgentField updateField)
record.Samples = agent.Samples ?? [];
record.Utilities = agent.Utilities ?? [];
record.McpTools = agent.McpTools ?? [];
record.Skills = agent.Skills ?? [];
record.KnowledgeBases = agent.KnowledgeBases ?? [];
record.Rules = agent.Rules ?? [];
if (agent.LlmConfig != null && !agent.LlmConfig.IsInherit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ public async Task UpdateAgent(Agent agent, AgentField field)
case AgentField.MaxMessageCount:
await UpdateAgentMaxMessageCount(agent.Id, agent.MaxMessageCount);
break;
case AgentField.Skills:
await UpdateAgentSkills(agent.Id, agent.Skills);
break;
case AgentField.All:
await UpdateAgentAllFields(agent);
break;
Expand Down Expand Up @@ -505,6 +508,25 @@ private async Task UpdateAgentMaxMessageCount(string agentId, int? maxMessageCou
await File.WriteAllTextAsync(agentFile, json);
}

private async Task UpdateAgentSkills(string agentId, List<AgentSkill> skills)
{
if (skills == null)
{
return;
}

var (agent, agentFile) = GetAgentFromFile(agentId);
if (agent == null)
{
return;
}

agent.Skills = skills;
agent.UpdatedDateTime = DateTime.UtcNow;
var json = JsonSerializer.Serialize(agent, _options);
await File.WriteAllTextAsync(agentFile, json);
}

private async Task UpdateAgentAllFields(Agent inputAgent)
{
var (agent, agentFile) = GetAgentFromFile(inputAgent.Id);
Expand All @@ -531,6 +553,7 @@ private async Task UpdateAgentAllFields(Agent inputAgent)
agent.LlmConfig = inputAgent.LlmConfig;
agent.MaxMessageCount = inputAgent.MaxMessageCount;
agent.UpdatedDateTime = DateTime.UtcNow;
agent.Skills = inputAgent.Skills;
var json = JsonSerializer.Serialize(agent, _options);
await File.WriteAllTextAsync(agentFile, json);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class AgentCreationModel
public List<AgentKnowledgeBase> KnowledgeBases { get; set; } = new();
public List<AgentRule> Rules { get; set; } = new();
public AgentLlmConfig? LlmConfig { get; set; }
public List<AgentSkill> Skills { get; set; }

Comment on lines 73 to 76
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

Skills is declared as non-nullable but is never initialized. When the request omits the field, model binding can leave it null, and you already handle null in ToAgent(). Make the property nullable (List<AgentSkill>?) or initialize it to an empty list to align with nullable reference type expectations.

Copilot uses AI. Check for mistakes.
public Agent ToAgent()
{
Expand Down Expand Up @@ -100,6 +101,7 @@ public Agent ToAgent()
KnowledgeBases = KnowledgeBases,
Rules = Rules,
RoutingRules = RoutingRules?.Select(x => RoutingRuleUpdateModel.ToDomainElement(x))?.ToList() ?? [],
Skills = Skills ?? new List<AgentSkill>(),
};
}
}
57 changes: 57 additions & 0 deletions src/Plugins/BotSharp.Plugin.AgentSkills/AgentSkillsPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Settings;
using BotSharp.Plugin.AgentSkills.Functions;
using BotSharp.Plugin.AgentSkills.Services;
using BotSharp.Plugin.AgentSkills.Skills;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace BotSharp.Plugin.AgentSkills;

/// <summary>
/// Agent Skills plugin for BotSharp.
/// Enables AI agents to leverage reusable skills following the Agent Skills specification (https://agentskills.io).
/// Implements requirements: FR-1.1, FR-3.1, FR-4.1
/// </summary>
public class AgentSkillsPlugin : IBotSharpPlugin
{
public string Id => "a5b3e8c1-7d2f-4a9e-b6c4-8f5d1e2a3b4c";
public string Name => "Agent Skills";
public string Description => "Enables AI agents to leverage reusable skills following the Agent Skills specification (https://agentskills.io).";
public string IconUrl => "https://raw.githubusercontent.com/SciSharp/BotSharp/master/docs/static/logos/BotSharp.png";
public string[] AgentIds => [];

/// <summary>
/// Register dependency injection services.
/// Implements requirements: FR-1.1, FR-3.1, FR-4.1, FR-6.1, NFR-4.1
/// </summary>
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
// FR-6.1: Register AgentSkillsSettings configuration
// Use ISettingService to bind configuration from appsettings.json
services.AddScoped(provider =>
{
var settingService = provider.GetRequiredService<ISettingService>();
return settingService.Bind<AgentSkillsSettings>("AgentSkills");
});

// FR-1.1: Register AgentSkillsFactory as singleton
// Singleton pattern avoids creating multiple factory instances
services.AddSingleton<AgentSkillsFactory>();

// FR-1.1, NFR-4.1: Register ISkillService and SkillService as scoped
services.AddScoped<ISkillService, SkillService>();

services.AddScoped<IFunctionCallback, GetInstructionsFn>();
services.AddScoped<IFunctionCallback, GetSkillBynameFn>();
services.AddScoped<IFunctionCallback, GetSkillFileContentFn>();

// FR-2.1: Register AgentSkillsInstructionHook for instruction injection
services.AddScoped<IAgentHook, AgentSkillsInstructionHook>();

// FR-3.1: Register AgentSkillsFunctionHook for function registration
services.AddScoped<IAgentHook, AgentSkillsFunctionHook>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GenerateDocumentationFile)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>

<ItemGroup>
<Content Include="data\agents\471ca181-375f-b16f-7134-5f868ecd31c6\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="data\agents\471ca181-375f-b16f-7134-5f868ecd31c6\instructions\instruction.liquid">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="YamlDotNet" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>

</Project>
Loading
Loading