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
5 changes: 2 additions & 3 deletions dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,13 @@ internal AgentFileSkill(
/// <inheritdoc/>
/// <remarks>
/// Returns the raw SKILL.md content. When the skill has scripts, a
/// <c>&lt;scripts&gt;&lt;script name="..."&gt;&lt;parameters_schema&gt;...&lt;/parameters_schema&gt;&lt;/script&gt;&lt;/scripts&gt;</c>
/// block is appended with a per-script entry describing the expected argument format.
/// <c>&lt;script_schemas&gt;</c> block is appended describing the arguments format.
/// The result is cached after the first access.
Comment thread
SergeyMenshykh marked this conversation as resolved.
/// </remarks>
public override ValueTask<string> GetContentAsync(CancellationToken cancellationToken = default)
{
var content = this._content ??= this._scripts is { Count: > 0 }
? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptsBlock(this._scripts)
? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptSchemasBlock(this._scripts)
: this._originalContent;
return new(content);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ protected AgentClassSkill()
this.Frontmatter.Name,
this.Frontmatter.Description,
this.Instructions,
this.Resources,
this.Scripts));
}

Expand Down Expand Up @@ -147,23 +146,35 @@ protected AgentClassSkill()
/// Gets the resources associated with this skill, or <see langword="null"/> if none.
/// </summary>
/// <remarks>
/// <para>
/// The default implementation returns resources discovered via reflection by scanning
/// <typeparamref name="TSelf"/> for members annotated with <see cref="AgentSkillResourceAttribute"/>.
/// This discovery is compatible with Native AOT because <typeparamref name="TSelf"/> is annotated with
/// <see cref="DynamicallyAccessedMembersAttribute"/>. The result is cached after the first access.
/// Override this property in derived classes to provide skill-specific resources.
/// </para>
/// <para>
/// Resources are not automatically included in the skill body.
/// To enable discovery, reference resources by name in the skill's instructions or in other resources.
/// </para>
/// </remarks>
public virtual IReadOnlyList<AgentSkillResource>? Resources => this._resources.Value;

/// <summary>
/// Gets the scripts associated with this skill, or <see langword="null"/> if none.
/// </summary>
/// <remarks>
/// <para>
/// The default implementation returns scripts discovered via reflection by scanning
/// <typeparamref name="TSelf"/> for methods annotated with <see cref="AgentSkillScriptAttribute"/>.
/// This discovery is compatible with Native AOT because <typeparamref name="TSelf"/> is annotated with
/// <see cref="DynamicallyAccessedMembersAttribute"/>. The result is cached after the first access.
/// Override this property in derived classes to provide skill-specific scripts.
/// </para>
/// <para>
/// Scripts are not automatically included in the skill body.
/// To enable discovery, reference scripts by name in the skill's instructions or in a resource.
/// </para>
Comment thread
SergeyMenshykh marked this conversation as resolved.
/// </remarks>
public virtual IReadOnlyList<AgentSkillScript>? Scripts => this._scripts.Value;

Expand All @@ -184,6 +195,10 @@ protected AgentClassSkill()
/// <summary>
/// Creates a skill resource backed by a static value.
/// </summary>
/// <remarks>
/// Resources are not automatically included in the skill body.
/// To enable discovery, reference the resource by name in the skill's instructions or in another resource.
/// </remarks>
/// <param name="name">The resource name.</param>
/// <param name="value">The static resource value.</param>
/// <param name="description">An optional description of the resource.</param>
Expand All @@ -194,6 +209,10 @@ protected AgentSkillResource CreateResource(string name, object value, string? d
/// <summary>
/// Creates a skill resource backed by a delegate that produces a dynamic value.
/// </summary>
/// <remarks>
/// Resources are not automatically included in the skill body.
/// To enable discovery, reference the resource by name in the skill's instructions or in another resource.
/// </remarks>
/// <param name="name">The resource name.</param>
/// <param name="method">A method that produces the resource value when requested.</param>
/// <param name="description">An optional description of the resource.</param>
Expand All @@ -208,6 +227,10 @@ protected AgentSkillResource CreateResource(string name, Delegate method, string
/// <summary>
/// Creates a skill script backed by a delegate.
/// </summary>
/// <remarks>
/// Scripts are not automatically included in the skill body.
/// To enable discovery, reference the script by name in the skill's instructions or in a resource.
/// </remarks>
Comment thread
SergeyMenshykh marked this conversation as resolved.
/// <param name="name">The script name.</param>
/// <param name="method">A method to execute when the script is invoked.</param>
/// <param name="description">An optional description of the script.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public AgentInlineSkill(
/// <inheritdoc/>
public override ValueTask<string> GetContentAsync(CancellationToken cancellationToken = default)
{
return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._resources, this._scripts));
return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._scripts));
}

/// <inheritdoc/>
Expand All @@ -115,6 +115,10 @@ public override ValueTask<string> GetContentAsync(CancellationToken cancellation
/// <summary>
/// Registers a static resource with this skill.
/// </summary>
/// <remarks>
/// Resources are not automatically included in the skill body.
/// To enable discovery, reference the resource by name in the skill's instructions or in another resource.
/// </remarks>
/// <param name="name">The resource name.</param>
/// <param name="value">The static resource value.</param>
/// <param name="description">An optional description of the resource.</param>
Expand All @@ -129,6 +133,10 @@ public AgentInlineSkill AddResource(string name, object value, string? descripti
/// Registers a dynamic resource with this skill, backed by a C# delegate.
/// The delegate's parameters and return type are automatically marshaled via <c>AIFunctionFactory</c>.
/// </summary>
/// <remarks>
/// Resources are not automatically included in the skill body.
/// To enable discovery, reference the resource by name in the skill's instructions or in another resource.
/// </remarks>
/// <param name="name">The resource name.</param>
/// <param name="method">A method that produces the resource value when requested.</param>
/// <param name="description">An optional description of the resource.</param>
Expand All @@ -147,6 +155,10 @@ public AgentInlineSkill AddResource(string name, Delegate method, string? descri
/// Registers a script with this skill, backed by a C# delegate.
/// The delegate's parameters and return type are automatically marshaled via <c>AIFunctionFactory</c>.
/// </summary>
/// <remarks>
/// Scripts are not automatically included in the skill body.
/// To enable discovery, reference the script by name in the skill's instructions or in a resource.
/// </remarks>
Comment thread
SergeyMenshykh marked this conversation as resolved.
/// <param name="name">The script name.</param>
/// <param name="method">A method to execute when the script is invoked.</param>
/// <param name="description">An optional description of the script.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@ namespace Microsoft.Agents.AI;
internal static class AgentInlineSkillContentBuilder
{
/// <summary>
/// Builds the complete skill content containing name, description, instructions, resources, and scripts.
/// Builds the complete skill content containing name, description, instructions, and script parameter schemas.
/// </summary>
/// <param name="name">The skill name.</param>
/// <param name="description">The skill description.</param>
/// <param name="instructions">The raw instructions text.</param>
/// <param name="resources">Optional resources associated with the skill.</param>
/// <param name="scripts">Optional scripts associated with the skill.</param>
/// <returns>An XML-structured content string.</returns>
public static string Build(
string name,
string description,
string instructions,
IReadOnlyList<AgentSkillResource>? resources,
Comment thread
SergeyMenshykh marked this conversation as resolved.
IReadOnlyList<AgentSkillScript>? scripts)
{
_ = Throw.IfNullOrWhitespace(name);
Expand All @@ -39,41 +37,24 @@ public static string Build(
.Append(EscapeXmlString(instructions))
.Append("\n</instructions>");

if (resources is { Count: > 0 })
{
sb.Append("\n\n<resources>\n");
foreach (var resource in resources)
{
if (resource.Description is not null)
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\" description=\"{EscapeXmlString(resource.Description)}\"/>\n");
}
else
{
sb.Append($" <resource name=\"{EscapeXmlString(resource.Name)}\"/>\n");
}
}

sb.Append("</resources>");
}

if (scripts is { Count: > 0 })
{
sb.Append('\n');
sb.Append(BuildScriptsBlock(scripts));
sb.Append(BuildScriptSchemasBlock(scripts));
}

return sb.ToString();
}

/// <summary>
/// Builds a <c>&lt;scripts&gt;...&lt;/scripts&gt;</c> XML block for the given scripts.
/// Each script is emitted as a <c>&lt;script name="..."&gt;</c> element with optional
/// <c>description</c> attribute and <c>&lt;parameters_schema&gt;</c> child element.
/// Builds a <c>&lt;script_schemas&gt;...&lt;/script_schemas&gt;</c> XML block for the given scripts.
/// Each script is emitted as a <c>&lt;schema script="..."&gt;</c> element containing only
/// the parameter schema. This block serves as a reference for the model to know how to
/// format arguments when calling scripts, not as a discovery mechanism.
/// </summary>
/// <param name="scripts">The scripts to include in the block.</param>
/// <returns>An XML string starting with <c>\n&lt;scripts&gt;</c>, or an empty string if the list is empty.</returns>
public static string BuildScriptsBlock(IReadOnlyList<AgentSkillScript> scripts)
/// <returns>An XML string starting with <c>\n&lt;script_schemas&gt;</c>, or an empty string if the list is empty.</returns>
public static string BuildScriptSchemasBlock(IReadOnlyList<AgentSkillScript> scripts)
{
_ = Throw.IfNull(scripts);

Expand All @@ -83,32 +64,23 @@ public static string BuildScriptsBlock(IReadOnlyList<AgentSkillScript> scripts)
}

var sb = new StringBuilder();
sb.Append("\n<scripts>\n");
sb.Append("\n<script_schemas>\n");

foreach (var script in scripts)
{
var parametersSchema = script.ParametersSchema;

if (script.Description is null && parametersSchema is null)
if (parametersSchema is null)
{
sb.Append($" <script name=\"{EscapeXmlString(script.Name)}\"/>\n");
sb.Append($" <schema script=\"{EscapeXmlString(script.Name)}\"/>\n");
}
else
{
sb.Append(script.Description is not null
? $" <script name=\"{EscapeXmlString(script.Name)}\" description=\"{EscapeXmlString(script.Description)}\">\n"
: $" <script name=\"{EscapeXmlString(script.Name)}\">\n");

if (parametersSchema is not null)
{
sb.Append($" <parameters_schema>{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</parameters_schema>\n");
}

sb.Append(" </script>\n");
sb.Append($" <schema script=\"{EscapeXmlString(script.Name)}\">{EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}</schema>\n");
}
}

sb.Append("</scripts>");
sb.Append("</script_schemas>");

return sb.ToString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ public async Task FullClassSkill_ReturnsOverriddenLists_AndCachesContentAsync()
// Act & Assert — Content is cached
Assert.Same(await skill.GetContentAsync(), await skill.GetContentAsync());

// Act & Assert — Content includes parameter schema from typed script
Assert.Contains("parameters_schema", await skill.GetContentAsync());
Assert.Contains("value", await skill.GetContentAsync());
// Act & Assert — Content includes parameter schema from typed script (with preserved quotes)
Assert.Contains("\"value\"", await skill.GetContentAsync());
}

[Fact]
Expand Down Expand Up @@ -383,10 +382,9 @@ public async Task AttributedFullSkill_IncludesContentWithSchema_AndCachesMembers
// Arrange
var skill = new AttributedFullSkill();

// Act & Assert — Content includes reflected resources and scripts
Assert.Contains("<resources>", await skill.GetContentAsync());
Assert.Contains("conversion-table", await skill.GetContentAsync());
Assert.Contains("<scripts>", await skill.GetContentAsync());
// Act & Assert — Content no longer includes resources in body; scripts are in script_schemas
Assert.DoesNotContain("<resources>", await skill.GetContentAsync());
Assert.Contains("<script_schemas>", await skill.GetContentAsync());
Assert.Contains("convert", await skill.GetContentAsync());

// Act & Assert — discovered members are cached
Expand Down Expand Up @@ -512,8 +510,8 @@ public async Task Content_IncludesDescription_ForReflectedResourcesAsync()
// Act
var content = await skill.GetContentAsync();

// Assert — descriptions from [Description] attribute appear in synthesized content
Assert.Contains("Some important data.", content);
// Assert — resources are no longer rendered in body content
Assert.DoesNotContain("<resources>", content);
Comment thread
SergeyMenshykh marked this conversation as resolved.
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,10 @@ public async Task Content_WithScripts_AppendsPerScriptEntriesAsync()

// Assert — content starts with original and appends per-script entries
Assert.StartsWith("Original content", content);
Assert.Contains("<scripts>", content);
Assert.Contains("<script name=\"build\">", content);
Assert.Contains("<script name=\"deploy\">", content);
Assert.Contains("<parameters_schema>", content);
Assert.Contains("</scripts>", content);
Assert.Contains("<script_schemas>", content);
Assert.Contains("<schema script=\"build\">", content);
Assert.Contains("<schema script=\"deploy\">", content);
Assert.Contains("</script_schemas>", content);
}

[Fact]
Expand Down
Loading
Loading