Skip to content
Draft
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
7 changes: 7 additions & 0 deletions Agentstration.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@
<Project Path="src/Agentstration.Management.Abstractions/Agentstration.Management.Abstractions.csproj" />
<Project Path="src/Agentstration.Management.Core/Agentstration.Management.Core.csproj" />
<Project Path="src/Agentstration.Management.Storage.Sqlite/Agentstration.Management.Storage.Sqlite.csproj" />
<Project Path="src/Agentstration.Memory/Agentstration.Memory.csproj" />
<Project Path="src/Agentstration.Memory.Application/Agentstration.Memory.Application.csproj" />
<Project Path="src/Agentstration.Memory.Storage.Abstractions/Agentstration.Memory.Storage.Abstractions.csproj" />
<Project Path="src/Agentstration.Memory.Storage.Sqlite/Agentstration.Memory.Storage.Sqlite.csproj" />
<Project Path="src/Agentstration.Memory.Testing/Agentstration.Memory.Testing.csproj" />
<Project Path="src/Agentstration.ModelProviders/Agentstration.ModelProviders.csproj" />
<Project Path="src/Agentstration.Extensions.Ollama/Agentstration.Extensions.Ollama.csproj" />
<Project Path="src/Agentstration.Extensions.LlamaCpp/Agentstration.Extensions.LlamaCpp.csproj" />
<Project Path="src/Agentstration.Extensions.Memory.Sqlite/Agentstration.Extensions.Memory.Sqlite.csproj" />
<Project Path="src/Agentstration.Extensions.Utilities/Agentstration.Extensions.Utilities.csproj" />
<Project Path="src/Agentstration.Runtime.Abstractions/Agentstration.Runtime.Abstractions.csproj" />
<Project Path="src/Agentstration.Runtime.AgentFramework/Agentstration.Runtime.AgentFramework.csproj" />
Expand Down Expand Up @@ -52,6 +58,7 @@
<Project Path="tests/Agentstration.ArchitectureTests/Agentstration.ArchitectureTests.csproj" />
<Project Path="tests/Agentstration.Evaluation.Tests/Agentstration.Evaluation.Tests.csproj" />
<Project Path="tests/Agentstration.Management.Tests/Agentstration.Management.Tests.csproj" />
<Project Path="tests/Agentstration.Memory.Conformance.Tests/Agentstration.Memory.Conformance.Tests.csproj" />
<Project Path="tests/Agentstration.ModelProviders.Tests/Agentstration.ModelProviders.Tests.csproj" />
<Project Path="tests/Agentstration.Runtime.Tests/Agentstration.Runtime.Tests.csproj" />
<Project Path="tests/Agentstration.Web.Tests/Agentstration.Web.Tests.csproj" />
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ The product is a modular monolith organized around a Management Plane, Runtime P

The autonomous Agentstration Extension Protocol SDK, conformance validator, CLI, samples, and standalone Inspector are staged in [`aep/`](aep/README.md). That directory has its own solution and build configuration so it can be moved into a dedicated repository without carrying Agentstration application projects.

Governed Memory is explicit and Workspace-isolated. Direct startup uses the builtin SQLite store; the Aspire AppHost additionally starts the autonomous `Agentstration.Extensions.Memory.Sqlite` AEP reference provider. Storage-provider selection does not move retrieval policy or context assembly outside Agentstration. See [Memory and execution context](docs/memory-context.md).

## Quick start

Requirements: the .NET SDK version selected by [`global.json`](global.json) (currently .NET 10.0.300 or a compatible feature band).
Expand Down
44 changes: 43 additions & 1 deletion aep/src/Agentstration.Aep.Abstractions/AepProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static class AepProtocol
public const string LegacyDiscoveryPath = "/.well-known/agentstration";
public const string HealthPath = "/aep/health";
public const string ModelProvidersPath = "/aep/model-providers";
public const string MemoryProvidersPath = "/aep/memory-providers";

public static JsonSerializerOptions JsonOptions { get; } = CreateJsonOptions();

Expand All @@ -25,6 +26,7 @@ public static class AepCapabilityNames
{
public const string Health = "aep.health";
public const string ModelProvider = "aep.model-provider";
public const string MemoryProvider = "aep.memory-provider";
public const string Tools = "aep.tools";
public const string Configuration = "aep.configuration";
}
Expand All @@ -47,7 +49,8 @@ public sealed record AepHealth(string Status, string? Details = null);

public sealed record AepContributions(
IReadOnlyList<AepModelProviderDescriptor> ModelProviders,
IReadOnlyList<AepToolContribution>? Tools = null);
IReadOnlyList<AepToolContribution>? Tools = null,
IReadOnlyList<AepMemoryProviderDescriptor>? MemoryProviders = null);

public sealed record AepMcpDescriptor(IReadOnlyList<AepMcpServerDescriptor> Servers);

Expand Down Expand Up @@ -85,6 +88,13 @@ public static IReadOnlyList<string> Validate(AepManifest descriptor)
if (string.IsNullOrWhiteSpace(tool.Mcp.Server) || !servers.Contains(tool.Mcp.Server))
errors.Add($"Tool contribution '{tool.Id}' references unknown MCP server '{tool.Mcp.Server}'.");
}
var memoryProviders = new HashSet<string>(StringComparer.Ordinal);
foreach (var provider in descriptor.Contributions.MemoryProviders ?? [])
{
if (string.IsNullOrWhiteSpace(provider.Id)) errors.Add("Memory provider id is required.");
else if (!memoryProviders.Add(provider.Id)) errors.Add($"Memory provider '{provider.Id}' is duplicated.");
if (string.IsNullOrWhiteSpace(provider.DisplayName)) errors.Add($"Memory provider '{provider.Id}' displayName is required.");
}
return errors;
}

Expand Down Expand Up @@ -134,6 +144,38 @@ public sealed record AepModelDescriptor(

public sealed record AepProviderHealth(string Status, string? Details = null);

public sealed record AepMemoryProviderDescriptor(
string Id,
string DisplayName,
AepMemoryProviderCapabilities Capabilities,
IReadOnlyDictionary<string, JsonElement>? Metadata = null);

public sealed record AepMemoryProviderCapabilities(
bool ExactScope = true,
bool Expiry = true,
bool Delete = true,
bool ClearScope = true,
bool PurgeExpired = true);

public sealed record AepMemoryScope(string Kind, string Key);
public sealed record AepMemoryProvenance(string SourceKind, string? SourceId, string Reason, Guid CreatedByPrincipalId);
public sealed record AepMemoryRecord(
Guid Id,
Guid WorkspaceId,
AepMemoryScope Scope,
string Content,
IReadOnlyList<string> Tags,
AepMemoryProvenance Provenance,
DateTimeOffset CreatedAt,
DateTimeOffset? ExpiresAt = null);
public sealed record AepMemoryRecordRequest(Guid WorkspaceId, Guid RecordId);
public sealed record AepMemoryGetResponse(AepMemoryRecord? Value);
public sealed record AepMemoryListRequest(Guid WorkspaceId, AepMemoryScope? Scope, DateTimeOffset Now, int Skip, int Take);
public sealed record AepMemoryListResponse(IReadOnlyList<AepMemoryRecord> Value);
public sealed record AepMemoryScopeRequest(Guid WorkspaceId, AepMemoryScope Scope);
public sealed record AepMemoryPurgeRequest(Guid WorkspaceId, DateTimeOffset Now, int Take);
public sealed record AepMemoryMutationResponse(int Affected);

public enum AepRole { System, User, Assistant, Tool }
public enum AepContentKind { Text, Image, File, Structured, ToolCall, ToolResult }
public enum AepFinishReason { Stop, Length, ToolCalls, ContentFilter, Error, Other }
Expand Down
92 changes: 86 additions & 6 deletions aep/src/Agentstration.Aep.AspNetCore/AepServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ Task<AepProviderHealth> GetHealthAsync(CancellationToken cancellationToken = def
Task.FromResult(new AepProviderHealth("available"));
}

public interface IAepMemoryProvider
{
AepMemoryProviderDescriptor Descriptor { get; }
Task<AepProviderHealth> GetHealthAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(new AepProviderHealth("available"));
Task WriteAsync(AepMemoryRecord record, CancellationToken cancellationToken);
Task<AepMemoryRecord?> GetAsync(AepMemoryRecordRequest request, CancellationToken cancellationToken);
Task<IReadOnlyList<AepMemoryRecord>> ListAsync(AepMemoryListRequest request, CancellationToken cancellationToken);
Task<bool> DeleteAsync(AepMemoryRecordRequest request, CancellationToken cancellationToken);
Task<int> ClearScopeAsync(AepMemoryScopeRequest request, CancellationToken cancellationToken);
Task<int> PurgeExpiredAsync(AepMemoryPurgeRequest request, CancellationToken cancellationToken);
}

public sealed class AepExtensionOptions
{
public AepExtensionIdentity Extension { get; set; } = new("agentstration.extension", "Agentstration extension", "1.0.0");
Expand All @@ -46,39 +59,53 @@ public static IServiceCollection AddAgentstrationAep(this IServiceCollection ser
public static IServiceCollection AddModelProvider<TProvider>(this IServiceCollection services)
where TProvider : class, IAepModelProvider => services.AddSingleton<IAepModelProvider, TProvider>();

public static IServiceCollection AddMemoryProvider<TProvider>(this IServiceCollection services)
where TProvider : class, IAepMemoryProvider => services.AddSingleton<IAepMemoryProvider, TProvider>();

public static IEndpointRouteBuilder MapAgentstrationAep(this IEndpointRouteBuilder endpoints)
{
endpoints.MapGet(AepProtocol.DiscoveryPath, (IOptions<AepExtensionOptions> options, IEnumerable<IAepModelProvider> providers) =>
Results.Json(CreateManifest(options.Value, providers), AepProtocol.JsonOptions));
endpoints.MapGet(AepProtocol.LegacyDiscoveryPath, (IOptions<AepExtensionOptions> options, IEnumerable<IAepModelProvider> providers) =>
Results.Json(CreateManifest(options.Value, providers), AepProtocol.JsonOptions));
endpoints.MapGet(AepProtocol.DiscoveryPath, (IOptions<AepExtensionOptions> options, IEnumerable<IAepModelProvider> providers, IEnumerable<IAepMemoryProvider> memories) =>
Results.Json(CreateManifest(options.Value, providers, memories), AepProtocol.JsonOptions));
endpoints.MapGet(AepProtocol.LegacyDiscoveryPath, (IOptions<AepExtensionOptions> options, IEnumerable<IAepModelProvider> providers, IEnumerable<IAepMemoryProvider> memories) =>
Results.Json(CreateManifest(options.Value, providers, memories), AepProtocol.JsonOptions));
endpoints.MapGet(AepProtocol.HealthPath, () => Results.Json(new AepHealth("available"), AepProtocol.JsonOptions));
endpoints.MapGet(AepProtocol.ModelProvidersPath, (IEnumerable<IAepModelProvider> providers) =>
Results.Json(providers.Select(value => value.Descriptor).ToArray(), AepProtocol.JsonOptions));
endpoints.MapPost($"{AepProtocol.ModelProvidersPath}/{{providerId}}/chat", ChatAsync);
endpoints.MapPost($"{AepProtocol.ModelProvidersPath}/{{providerId}}/chat/stream", StreamAsync);
endpoints.MapGet($"{AepProtocol.ModelProvidersPath}/{{providerId}}/models", ListModelsAsync);
endpoints.MapGet($"{AepProtocol.ModelProvidersPath}/{{providerId}}/health", ProviderHealthAsync);
endpoints.MapGet(AepProtocol.MemoryProvidersPath, (IEnumerable<IAepMemoryProvider> providers) =>
Results.Json(providers.Select(value => value.Descriptor).ToArray(), AepProtocol.JsonOptions));
endpoints.MapGet($"{AepProtocol.MemoryProvidersPath}/{{providerId}}/health", MemoryHealthAsync);
endpoints.MapPost($"{AepProtocol.MemoryProvidersPath}/{{providerId}}/records", WriteMemoryAsync);
endpoints.MapPost($"{AepProtocol.MemoryProvidersPath}/{{providerId}}/records/get", GetMemoryAsync);
endpoints.MapPost($"{AepProtocol.MemoryProvidersPath}/{{providerId}}/records/query", ListMemoryAsync);
endpoints.MapPost($"{AepProtocol.MemoryProvidersPath}/{{providerId}}/records/delete", DeleteMemoryAsync);
endpoints.MapPost($"{AepProtocol.MemoryProvidersPath}/{{providerId}}/records/clear", ClearMemoryAsync);
endpoints.MapPost($"{AepProtocol.MemoryProvidersPath}/{{providerId}}/records/purge", PurgeMemoryAsync);
endpoints.MapHealthChecks("/health");
return endpoints;
}

public static IEndpointRouteBuilder MapAep(this IEndpointRouteBuilder endpoints) => endpoints.MapAgentstrationAep();

private static AepManifest CreateManifest(AepExtensionOptions options, IEnumerable<IAepModelProvider> providers)
private static AepManifest CreateManifest(AepExtensionOptions options, IEnumerable<IAepModelProvider> providers, IEnumerable<IAepMemoryProvider> memories)
{
var modelProviders = providers.Select(value => value.Descriptor).ToArray();
var memoryProviders = memories.Select(value => value.Descriptor).ToArray();
var capabilities = new Dictionary<string, AepCapabilityDescriptor>(options.Capabilities, StringComparer.Ordinal)
{
[AepCapabilityNames.Health] = new("1.0", AepProtocol.HealthPath)
};
if (modelProviders.Length > 0) capabilities[AepCapabilityNames.ModelProvider] = new("1.0", AepProtocol.ModelProvidersPath);
if (memoryProviders.Length > 0) capabilities[AepCapabilityNames.MemoryProvider] = new("1.0", AepProtocol.MemoryProvidersPath);
if (options.Tools.Count > 0) capabilities[AepCapabilityNames.Tools] = new("1.0");
var descriptor = new AepManifest(
AepProtocol.Version,
options.Extension,
capabilities,
new AepContributions(modelProviders, options.Tools.ToArray()),
new AepContributions(modelProviders, options.Tools.ToArray(), memoryProviders),
options.McpServers.Count == 0 ? null : new AepMcpDescriptor(options.McpServers.ToArray()));
var errors = AepDescriptorValidator.Validate(descriptor);
if (errors.Count > 0) throw new InvalidOperationException($"The AEP extension descriptor is invalid: {string.Join(" ", errors)}");
Expand Down Expand Up @@ -137,6 +164,59 @@ private static async Task StreamAsync(string providerId, AepChatRequest request,
private static IAepModelProvider? Find(IEnumerable<IAepModelProvider> providers, string id) =>
providers.FirstOrDefault(value => string.Equals(value.Descriptor.Id, id, StringComparison.OrdinalIgnoreCase));

private static IAepMemoryProvider? FindMemory(IEnumerable<IAepMemoryProvider> providers, string id) =>
providers.FirstOrDefault(value => string.Equals(value.Descriptor.Id, id, StringComparison.OrdinalIgnoreCase));

private static async Task<IResult> MemoryHealthAsync(string providerId, IEnumerable<IAepMemoryProvider> providers, CancellationToken token)
{
var provider = FindMemory(providers, providerId);
return provider is null ? Error(404, "provider_unavailable", $"Memory provider '{providerId}' is not registered.")
: Results.Json(await provider.GetHealthAsync(token), AepProtocol.JsonOptions);
}

private static async Task<IResult> WriteMemoryAsync(string providerId, AepMemoryRecord record, IEnumerable<IAepMemoryProvider> providers, CancellationToken token)
{
var provider = FindMemory(providers, providerId);
if (provider is null) return Error(404, "provider_unavailable", $"Memory provider '{providerId}' is not registered.");
await provider.WriteAsync(record, token);
return Results.NoContent();
}

private static async Task<IResult> GetMemoryAsync(string providerId, AepMemoryRecordRequest request, IEnumerable<IAepMemoryProvider> providers, CancellationToken token)
{
var provider = FindMemory(providers, providerId);
if (provider is null) return Error(404, "provider_unavailable", $"Memory provider '{providerId}' is not registered.");
return Results.Json(new AepMemoryGetResponse(await provider.GetAsync(request, token)), AepProtocol.JsonOptions);
}

private static async Task<IResult> ListMemoryAsync(string providerId, AepMemoryListRequest request, IEnumerable<IAepMemoryProvider> providers, CancellationToken token)
{
var provider = FindMemory(providers, providerId);
if (provider is null) return Error(404, "provider_unavailable", $"Memory provider '{providerId}' is not registered.");
return Results.Json(new AepMemoryListResponse(await provider.ListAsync(request, token)), AepProtocol.JsonOptions);
}

private static async Task<IResult> DeleteMemoryAsync(string providerId, AepMemoryRecordRequest request, IEnumerable<IAepMemoryProvider> providers, CancellationToken token)
{
var provider = FindMemory(providers, providerId);
if (provider is null) return Error(404, "provider_unavailable", $"Memory provider '{providerId}' is not registered.");
return Results.Json(new AepMemoryMutationResponse(await provider.DeleteAsync(request, token) ? 1 : 0), AepProtocol.JsonOptions);
}

private static async Task<IResult> ClearMemoryAsync(string providerId, AepMemoryScopeRequest request, IEnumerable<IAepMemoryProvider> providers, CancellationToken token)
{
var provider = FindMemory(providers, providerId);
if (provider is null) return Error(404, "provider_unavailable", $"Memory provider '{providerId}' is not registered.");
return Results.Json(new AepMemoryMutationResponse(await provider.ClearScopeAsync(request, token)), AepProtocol.JsonOptions);
}

private static async Task<IResult> PurgeMemoryAsync(string providerId, AepMemoryPurgeRequest request, IEnumerable<IAepMemoryProvider> providers, CancellationToken token)
{
var provider = FindMemory(providers, providerId);
if (provider is null) return Error(404, "provider_unavailable", $"Memory provider '{providerId}' is not registered.");
return Results.Json(new AepMemoryMutationResponse(await provider.PurgeExpiredAsync(request, token)), AepProtocol.JsonOptions);
}

private static IResult Error(int status, string code, string message) =>
Results.Json(new AepErrorResponse(new AepError(code, message)), AepProtocol.JsonOptions, statusCode: status);
}
Expand Down
Loading
Loading