159 lines
5.8 KiB
C#
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"
|
|
};
|
|
}
|