using Nexus.Api.Data; namespace Nexus.Api.Services; 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); Task> GetAllowedAgentIdsAsync(CancellationToken cancellationToken); } /// /// Projects OpenClaw's live agent inventory into Nexus' application contract. /// OpenClaw remains the sole source of truth: no host config or workspace path /// is consulted by this service. /// public sealed class AgentService(IOpenClawControlService openClaw) : IAgentService { public async Task> GetAgentsAsync( CancellationToken cancellationToken) { var liveAgents = await openClaw.GetAgentsAsync(cancellationToken); var sessions = await openClaw.GetSessionsAsync(500, cancellationToken); var connection = openClaw.GetConnection(); var agents = new List(liveAgents.Items.Count); foreach (var live in liveAgents.Items .Where(item => !string.IsNullOrWhiteSpace(item.Id)) .DistinctBy(item => item.Id, StringComparer.OrdinalIgnoreCase)) { var session = FindLatestSession(sessions.Items, live.Id); var description = live.Description; if (string.IsNullOrWhiteSpace(description) && string.Equals(live.Id, "main", StringComparison.OrdinalIgnoreCase)) { description = "Primary conversational agent — routing and general-purpose chat"; } agents.Add(new AgentInfo( Id: live.Id, Name: string.IsNullOrWhiteSpace(live.Name) ? live.Id : live.Name, Role: DeriveRole(live.Id), Model: session?.Model ?? live.Model ?? "openclaw/default", Status: ResolveStatus(connection.Connected, live.Status), LastSeen: session?.UpdatedAt ?? connection.LastEventAt, Workspace: live.Workspace, Description: description)); } return agents.AsReadOnly(); } public async Task GetAgentAsync( string id, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(id)) return null; var liveAgents = await openClaw.GetAgentsAsync(cancellationToken); var live = liveAgents.Items.FirstOrDefault(item => string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase)); if (live is null) return null; var sessions = await openClaw.GetSessionsAsync(500, cancellationToken); var session = FindLatestSession(sessions.Items, live.Id); var connection = openClaw.GetConnection(); var description = live.Description; if (string.IsNullOrWhiteSpace(description) && string.Equals(live.Id, "main", StringComparison.OrdinalIgnoreCase)) { description = "Primary conversational agent — routing and general-purpose chat"; } return new AgentDetail( Id: live.Id, Name: string.IsNullOrWhiteSpace(live.Name) ? live.Id : live.Name, Role: DeriveRole(live.Id), Model: session?.Model ?? live.Model ?? "openclaw/default", Status: ResolveStatus(connection.Connected, live.Status), LastSeen: session?.UpdatedAt ?? connection.LastEventAt, Workspace: live.Workspace, AgentDir: null, Description: description, SubAgents: null, IdentityName: string.IsNullOrWhiteSpace(live.Name) ? live.Id : live.Name); } public async Task> GetAllowedAgentIdsAsync( CancellationToken cancellationToken) { var liveAgents = await openClaw.GetAgentsAsync(cancellationToken); return liveAgents.Items .Where(agent => !string.IsNullOrWhiteSpace(agent.Id)) .Select(agent => agent.Id.Trim().ToLowerInvariant()) .ToHashSet(StringComparer.OrdinalIgnoreCase); } private static Nexus.Api.Models.OpenClawSessionDto? FindLatestSession( IReadOnlyList sessions, string agentId) => sessions .Where(item => string.Equals(item.AgentId, agentId, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(item => item.UpdatedAt) .FirstOrDefault(); private static OperationalStatus ResolveStatus(bool connected, string? liveStatus) { if (!connected) return OperationalStatus.Offline; return liveStatus?.Trim().ToLowerInvariant() switch { "degraded" or "stale" or "warning" => OperationalStatus.Degraded, "offline" or "failed" or "error" => OperationalStatus.Offline, "unknown" or "unsupported" => OperationalStatus.Unknown, _ => OperationalStatus.Online }; } private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch { "iris" => "Orchestrator", "product-owner" => "Product Owner", "programmer" => "Developer", "programmer-fast" => "Developer", "reviewer" => "Reviewer", "architekt" => "Architect", "main" => "Assistant", _ => "Custom" }; }