using System.Text.Json; using System.Text.Json.Serialization; using Nexus.Api.Data; using Nexus.Api.Integrations; namespace Nexus.Api.Services; public sealed record AgentConfig { [JsonPropertyName("id")] public string Id { get; init; } = string.Empty; [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("workspace")] public string? Workspace { get; init; } [JsonPropertyName("agentDir")] public string? AgentDir { get; init; } [JsonPropertyName("model")] public string? Model { get; init; } [JsonPropertyName("identity")] public AgentIdentityConfig? Identity { get; init; } [JsonPropertyName("subagents")] public SubAgentConfig? Subagents { get; init; } } public sealed record SubAgentConfig { [JsonPropertyName("allowAgents")] public IReadOnlyList? AllowAgents { get; init; } } public sealed record AgentIdentityConfig { [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("theme")] public string Theme { get; init; } = string.Empty; } public sealed record AgentInfo( string Id, string Name, string Role, string Model, OperationalStatus Status, DateTimeOffset? LastSeen, string? Workspace, string? Description ); public sealed record AgentDetail( string Id, string Name, string Role, string Model, OperationalStatus Status, DateTimeOffset? LastSeen, string? Workspace, string? AgentDir, string? Description, IReadOnlyList? SubAgents, string? IdentityName ); public interface IAgentService { Task> GetAgentsAsync(CancellationToken cancellationToken); Task GetAgentAsync(string id, CancellationToken cancellationToken); } public sealed class AgentService(IConfiguration configuration, IAgentRuntime runtime) : IAgentService { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public async Task> GetAgentsAsync(CancellationToken cancellationToken) { var configs = await LoadAgentConfigsAsync(cancellationToken); var runtimeStatus = await runtime.GetStatusAsync(cancellationToken); var overallOperational = runtimeStatus.Status; var now = DateTimeOffset.UtcNow; var agents = new List(configs.Count); foreach (var config in configs) { var model = config.Model ?? "deepseek/deepseek-v4-flash"; var role = DeriveRole(config.Id); var description = config.Identity?.Theme ?? string.Empty; if (string.IsNullOrEmpty(description)) { description = config.Id switch { "main" => "Primary conversational agent — routing and general-purpose chat", _ => description }; } agents.Add(new AgentInfo( Id: config.Id, Name: config.Identity?.Name ?? config.Name ?? config.Id, Role: role, Model: model, Status: overallOperational, LastSeen: now, Workspace: config.Workspace, Description: description )); } return agents.AsReadOnly(); } public async Task GetAgentAsync(string id, CancellationToken cancellationToken) { var configs = await LoadAgentConfigsAsync(cancellationToken); var config = configs.FirstOrDefault(a => a.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); if (config is null) return null; var runtimeStatus = await runtime.GetStatusAsync(cancellationToken); var now = DateTimeOffset.UtcNow; var role = DeriveRole(config.Id); var description = config.Identity?.Theme ?? string.Empty; if (string.IsNullOrEmpty(description) && config.Id == "main") description = "Primary conversational agent — routing and general-purpose chat"; return new AgentDetail( Id: config.Id, Name: config.Identity?.Name ?? config.Name ?? config.Id, Role: role, Model: config.Model ?? "deepseek/deepseek-v4-flash", Status: runtimeStatus.Status, LastSeen: now, Workspace: config.Workspace, AgentDir: config.AgentDir, Description: description, SubAgents: config.Subagents?.AllowAgents, IdentityName: config.Identity?.Name ); } private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch { "iris" => "Orchestrator", "programmer" => "Developer", "reviewer" => "Reviewer", "architekt" => "Architect", "main" => "Assistant", _ => "Custom" }; private async Task> LoadAgentConfigsAsync(CancellationToken cancellationToken) { var path = configuration.GetValue("AgentConfigPath") ?? "/home/node/.openclaw/openclaw.json"; if (!File.Exists(path)) return Array.Empty(); var json = await File.ReadAllTextAsync(path, cancellationToken); using var document = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = true }); var root = document.RootElement; if (!root.TryGetProperty("agents", out var agentsElement)) return Array.Empty(); if (!agentsElement.TryGetProperty("list", out var listElement)) return Array.Empty(); var defaults = agentsElement.TryGetProperty("defaults", out var defaultsElement) ? JsonSerializer.Deserialize(defaultsElement.GetRawText(), JsonOptions) : null; var configs = new List(); foreach (var agentElement in listElement.EnumerateArray()) { var config = JsonSerializer.Deserialize(agentElement.GetRawText(), JsonOptions); if (config is null || string.IsNullOrWhiteSpace(config.Id)) continue; // Inherit defaults for missing fields if (string.IsNullOrWhiteSpace(config.Name)) config = config with { Name = config.Id }; if (string.IsNullOrWhiteSpace(config.Model) && defaults?.Model?.Primary is not null) config = config with { Model = defaults.Model.Primary }; if (string.IsNullOrWhiteSpace(config.Workspace) && defaults?.Workspace is not null) config = config with { Workspace = defaults.Workspace }; configs.Add(config); } return configs.AsReadOnly(); } private sealed record AgentDefaults { [JsonPropertyName("workspace")] public string? Workspace { get; init; } [JsonPropertyName("model")] public AgentDefaultModel? Model { get; init; } } private sealed record AgentDefaultModel { [JsonPropertyName("primary")] public string? Primary { get; init; } } }