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
42 changes: 42 additions & 0 deletions src/SkillServer/Endpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,48 @@ private static void MapManifestEndpoints(this WebApplication app)
? Results.NotFound()
: Results.Json(skillVersion, SkillServerJsonContext.Default.NativeSkillVersionDetail);
});

manifest.MapGet("/subagents/index.json", async (
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var index = await manifestGenerator.GenerateSubAgentIndexAsync(ct);
return Results.Json(index, SkillServerJsonContext.Default.NativeSubAgentCollectionIndex);
});

manifest.MapGet("/subagents/pages/{page}.json", async (
string page,
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var subAgentPage = await manifestGenerator.GenerateSubAgentPageAsync(page, ct);
return subAgentPage is null
? Results.NotFound()
: Results.Json(subAgentPage, SkillServerJsonContext.Default.NativeSubAgentCollectionPage);
});

manifest.MapGet("/subagents/{subAgentName}/index.json", async (
string subAgentName,
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var subAgent = await manifestGenerator.GenerateSubAgentIdentityAsync(subAgentName, ct);
return subAgent is null
? Results.NotFound()
: Results.Json(subAgent, SkillServerJsonContext.Default.NativeSubAgentIdentityIndex);
});

manifest.MapGet("/subagents/{subAgentName}/versions/{version}.json", async (
string subAgentName,
string version,
NativeManifestGenerator manifestGenerator,
CancellationToken ct) =>
{
var subAgentVersion = await manifestGenerator.GenerateSubAgentVersionAsync(subAgentName, version, ct);
return subAgentVersion is null
? Results.NotFound()
: Results.Json(subAgentVersion, SkillServerJsonContext.Default.NativeSubAgentVersionDetail);
});
}

private static void MapSkillEndpoints(this WebApplication app)
Expand Down
105 changes: 105 additions & 0 deletions src/SkillServer/Models/NativeManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public sealed record NativeRootManifestLinks

[JsonPropertyName("skills")]
public required NativeManifestLink Skills { get; init; }

[JsonPropertyName("subagents")]
public required NativeManifestLink SubAgents { get; init; }
}

public sealed record NativeRootManifest
Expand Down Expand Up @@ -64,6 +67,18 @@ public sealed record NativeSkillCollectionIndex
public required IReadOnlyList<NativeManifestPageLink> Pages { get; init; }
}

public sealed record NativeSubAgentCollectionIndex
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "subagent-index";

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }

[JsonPropertyName("pages")]
public required IReadOnlyList<NativeManifestPageLink> Pages { get; init; }
}

public sealed record NativeVersionRange
{
[JsonPropertyName("min")]
Expand Down Expand Up @@ -91,6 +106,21 @@ public sealed record NativeSkillPageItem
public required string Href { get; init; }
}

public sealed record NativeSubAgentPageItem
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("latestVersion")]
public required string LatestVersion { get; init; }

[JsonPropertyName("versionRange")]
public required NativeVersionRange VersionRange { get; init; }

[JsonPropertyName("href")]
public required string Href { get; init; }
}

public sealed record NativeSkillCollectionPage
{
[JsonPropertyName("kind")]
Expand All @@ -106,6 +136,21 @@ public sealed record NativeSkillCollectionPage
public required NativeManifestSelfLinks Links { get; init; }
}

public sealed record NativeSubAgentCollectionPage
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "subagent-page";

[JsonPropertyName("range")]
public required string Range { get; init; }

[JsonPropertyName("items")]
public required IReadOnlyList<NativeSubAgentPageItem> Items { get; init; }

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }
}

public sealed record NativeSkillVersionLink
{
[JsonPropertyName("version")]
Expand All @@ -121,6 +166,21 @@ public sealed record NativeSkillVersionLink
public required string Href { get; init; }
}

public sealed record NativeSubAgentVersionLink
{
[JsonPropertyName("version")]
public required string Version { get; init; }

[JsonPropertyName("publishedAt")]
public required DateTimeOffset PublishedAt { get; init; }

[JsonPropertyName("digest")]
public required string Digest { get; init; }

[JsonPropertyName("href")]
public required string Href { get; init; }
}

public sealed record NativeSkillIdentityIndex
{
[JsonPropertyName("kind")]
Expand All @@ -139,6 +199,24 @@ public sealed record NativeSkillIdentityIndex
public required NativeManifestSelfLinks Links { get; init; }
}

public sealed record NativeSubAgentIdentityIndex
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "subagent";

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("latestVersion")]
public required string LatestVersion { get; init; }

[JsonPropertyName("versions")]
public required IReadOnlyList<NativeSubAgentVersionLink> Versions { get; init; }

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }
}

public sealed record NativeSkillArtifact
{
[JsonPropertyName("name")]
Expand Down Expand Up @@ -183,3 +261,30 @@ public sealed record NativeSkillVersionDetail
[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }
}

public sealed record NativeSubAgentVersionDetail
{
[JsonPropertyName("kind")]
public string Kind { get; init; } = "subagent-version";

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("version")]
public required string Version { get; init; }

[JsonPropertyName("type")]
public string Type { get; init; } = SubAgentTypes.AgentMd;

[JsonPropertyName("description")]
public required string Description { get; init; }

[JsonPropertyName("url")]
public required string Url { get; init; }

[JsonPropertyName("digest")]
public required string Digest { get; init; }

[JsonPropertyName("links")]
public required NativeManifestSelfLinks Links { get; init; }
}
4 changes: 4 additions & 0 deletions src/SkillServer/Models/SkillServerJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ namespace SkillServer.Models;
[JsonSerializable(typeof(NativeSkillCollectionPage))]
[JsonSerializable(typeof(NativeSkillIdentityIndex))]
[JsonSerializable(typeof(NativeSkillVersionDetail))]
[JsonSerializable(typeof(NativeSubAgentCollectionIndex))]
[JsonSerializable(typeof(NativeSubAgentCollectionPage))]
[JsonSerializable(typeof(NativeSubAgentIdentityIndex))]
[JsonSerializable(typeof(NativeSubAgentVersionDetail))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
Expand Down
127 changes: 117 additions & 10 deletions src/SkillServer/Services/NativeManifestGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ namespace SkillServer.Services;
public sealed class NativeManifestGenerator
{
private const string SkillsPageRange = "all";
private const string SubAgentsPageRange = "all";

private readonly SkillRepository _repository;
private readonly SkillRepository _skillRepository;
private readonly SubAgentRepository _subAgentRepository;
private readonly IConfiguration _configuration;

public NativeManifestGenerator(SkillRepository repository, IConfiguration configuration)
public NativeManifestGenerator(
SkillRepository skillRepository,
SubAgentRepository subAgentRepository,
IConfiguration configuration)
{
_repository = repository;
_skillRepository = skillRepository;
_subAgentRepository = subAgentRepository;
_configuration = configuration;
}

Expand All @@ -30,7 +36,8 @@ public Task<NativeRootManifest> GenerateRootAsync(CancellationToken ct = default
{
Self = Link("/manifest.json"),
RfcSkills = Link("/.well-known/agent-skills/index.json"),
Skills = Link("/manifest/skills/index.json")
Skills = Link("/manifest/skills/index.json"),
SubAgents = Link("/manifest/subagents/index.json")
}
};

Expand All @@ -57,12 +64,12 @@ public Task<NativeSkillCollectionIndex> GenerateSkillIndexAsync(CancellationToke
if (!page.Equals(SkillsPageRange, StringComparison.OrdinalIgnoreCase))
return null;

var latestVersions = await _repository.GetAllLatestVersionsWithMetadataAsync(ct: ct);
var latestVersions = await _skillRepository.GetAllLatestVersionsWithMetadataAsync(ct: ct);
var items = new List<NativeSkillPageItem>(latestVersions.Count);

foreach (var latest in latestVersions)
{
var versions = await _repository.GetAllVersionsAsync(latest.SkillId, ct);
var versions = await _skillRepository.GetAllVersionsAsync(latest.SkillId, ct);
if (versions.Count == 0)
continue;

Expand Down Expand Up @@ -91,11 +98,11 @@ public Task<NativeSkillCollectionIndex> GenerateSkillIndexAsync(CancellationToke

public async Task<NativeSkillIdentityIndex?> GenerateSkillIdentityAsync(string skillName, CancellationToken ct = default)
{
var skill = await _repository.GetSkillByNameAsync(skillName, ct);
var skill = await _skillRepository.GetSkillByNameAsync(skillName, ct);
if (skill is null)
return null;

var versions = await _repository.GetAllVersionsAsync(skill.Id, ct);
var versions = await _skillRepository.GetAllVersionsAsync(skill.Id, ct);
if (versions.Count == 0)
return null;

Expand All @@ -117,11 +124,11 @@ public Task<NativeSkillCollectionIndex> GenerateSkillIndexAsync(CancellationToke

public async Task<NativeSkillVersionDetail?> GenerateSkillVersionAsync(string skillName, string version, CancellationToken ct = default)
{
var skill = await _repository.GetSkillByNameAsync(skillName, ct);
var skill = await _skillRepository.GetSkillByNameAsync(skillName, ct);
if (skill is null)
return null;

var skillVersion = await _repository.GetVersionAsync(skill.Id, version, ct);
var skillVersion = await _skillRepository.GetVersionAsync(skill.Id, version, ct);
if (skillVersion is null)
return null;

Expand All @@ -139,6 +146,106 @@ public Task<NativeSkillCollectionIndex> GenerateSkillIndexAsync(CancellationToke
};
}

public Task<NativeSubAgentCollectionIndex> GenerateSubAgentIndexAsync(CancellationToken ct = default)
{
var index = new NativeSubAgentCollectionIndex
{
Links = SelfLinks("/manifest/subagents/index.json"),
Pages = [new NativeManifestPageLink
{
Range = SubAgentsPageRange,
Href = "/manifest/subagents/pages/all.json"
}]
};

return Task.FromResult(index);
}

public async Task<NativeSubAgentCollectionPage?> GenerateSubAgentPageAsync(string page, CancellationToken ct = default)
{
if (!page.Equals(SubAgentsPageRange, StringComparison.OrdinalIgnoreCase))
return null;

var latestVersions = await _subAgentRepository.GetAllLatestVersionsWithMetadataAsync(ct);
var items = new List<NativeSubAgentPageItem>(latestVersions.Count);

foreach (var latest in latestVersions)
{
var versions = await _subAgentRepository.GetAllVersionsAsync(latest.SubAgentId, ct);
if (versions.Count == 0)
continue;

var oldest = versions[^1];
items.Add(new NativeSubAgentPageItem
{
Name = latest.SubAgentName,
LatestVersion = latest.Version,
VersionRange = new NativeVersionRange
{
Min = oldest.Version,
Max = latest.Version,
Count = versions.Count
},
Href = $"/manifest/subagents/{latest.SubAgentName}/index.json"
});
}

return new NativeSubAgentCollectionPage
{
Range = SubAgentsPageRange,
Items = items,
Links = SelfLinks("/manifest/subagents/pages/all.json")
};
}

public async Task<NativeSubAgentIdentityIndex?> GenerateSubAgentIdentityAsync(string subAgentName, CancellationToken ct = default)
{
var subAgent = await _subAgentRepository.GetSubAgentByNameAsync(subAgentName, ct);
if (subAgent is null)
return null;

var versions = await _subAgentRepository.GetAllVersionsAsync(subAgent.Id, ct);
if (versions.Count == 0)
return null;

var latest = versions.FirstOrDefault(v => v.IsLatest) ?? versions[0];
return new NativeSubAgentIdentityIndex
{
Name = subAgent.Name,
LatestVersion = latest.Version,
Versions = versions.Select(v => new NativeSubAgentVersionLink
{
Version = v.Version,
PublishedAt = v.PublishedAt,
Digest = Sha256Digest.Create(v.Sha256).Value,
Href = $"/manifest/subagents/{subAgent.Name}/versions/{v.Version}.json"
}).ToList(),
Links = SelfLinks($"/manifest/subagents/{subAgent.Name}/index.json")
};
}

public async Task<NativeSubAgentVersionDetail?> GenerateSubAgentVersionAsync(string subAgentName, string version, CancellationToken ct = default)
{
var subAgent = await _subAgentRepository.GetSubAgentByNameAsync(subAgentName, ct);
if (subAgent is null)
return null;

var subAgentVersion = await _subAgentRepository.GetVersionAsync(subAgent.Id, version, ct);
if (subAgentVersion is null)
return null;

var baseUrl = GetBaseUrl();
return new NativeSubAgentVersionDetail
{
Name = subAgent.Name,
Version = subAgentVersion.Version,
Description = subAgentVersion.Description,
Url = $"{baseUrl}/subagents/{subAgent.Name}/{subAgentVersion.Version}/agent.md",
Digest = Sha256Digest.Create(subAgentVersion.Sha256).Value,
Links = SelfLinks($"/manifest/subagents/{subAgent.Name}/versions/{subAgentVersion.Version}.json")
};
}

private NativeSkillArtifact CreateArtifact(string skillName, SkillVersion version)
{
var baseUrl = GetBaseUrl();
Expand Down
Loading
Loading