Files
AzuTear f5552218bc
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s
feat: ship agent-first mission control v0.2.57
2026-07-31 22:39:47 +02:00

159 lines
5.8 KiB
C#

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<string>? SubAgents,
string? IdentityName
);
public interface IAgentService
{
Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(CancellationToken cancellationToken);
Task<AgentDetail?> GetAgentAsync(string id, CancellationToken cancellationToken);
Task<IReadOnlySet<string>> GetAllowedAgentIdsAsync(CancellationToken cancellationToken);
}
/// <summary>
/// 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.
/// </summary>
public sealed class AgentService(IOpenClawControlService openClaw) : IAgentService
{
public async Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(
CancellationToken cancellationToken)
{
var liveAgents = await openClaw.GetAgentsAsync(cancellationToken);
var sessions = await openClaw.GetSessionsAsync(500, cancellationToken);
var connection = openClaw.GetConnection();
var agents = new List<AgentInfo>(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<AgentDetail?> 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<IReadOnlySet<string>> 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<Nexus.Api.Models.OpenClawSessionDto> 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"
};
}