Compare commits
2 Commits
82c12554d0
...
agent/hex/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
040d4cb54d | ||
|
|
47cbeed456 |
@@ -8,11 +8,12 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.14" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
namespace ControlCenter.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines which side of the Control Center dashboard a minion occupies.
|
|
||||||
/// </summary>
|
|
||||||
public enum MinionSide
|
|
||||||
{
|
|
||||||
/// <summary>Development side — Rex, Dex, Hex.</summary>
|
|
||||||
Dev,
|
|
||||||
|
|
||||||
/// <summary>Business side — Larry, Mel, Buzz.</summary>
|
|
||||||
Business
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Visual state of a minion sprite, derived from the agent's
|
|
||||||
/// <see cref="AgentStatus"/>. Maps Active/Idle/Thinking/Error
|
|
||||||
/// to frontend animation states.
|
|
||||||
/// </summary>
|
|
||||||
public enum MinionState
|
|
||||||
{
|
|
||||||
/// <summary>Agent is actively processing — minion shows working animation.</summary>
|
|
||||||
Active,
|
|
||||||
|
|
||||||
/// <summary>Agent is idle — minion shows idle/patrolling animation.</summary>
|
|
||||||
Idle,
|
|
||||||
|
|
||||||
/// <summary>Agent is thinking (LLM call in flight) — minion shows thinking animation.</summary>
|
|
||||||
Thinking,
|
|
||||||
|
|
||||||
/// <summary>Agent encountered an error — minion shows error/distress animation.</summary>
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Static mapping entry that associates an agent ID with a minion's
|
|
||||||
/// display side and position index within that side.
|
|
||||||
///
|
|
||||||
/// <para>Position indices are zero-based within each side. The dev side
|
|
||||||
/// has Rex at 0, Dex at 1, and Hex at 2. The business side has
|
|
||||||
/// Larry at 0, Mel at 1, and Buzz at 2.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AgentId">Agent identifier, e.g. "rex", "dex".</param>
|
|
||||||
/// <param name="Side">Which side of the dashboard the minion occupies.</param>
|
|
||||||
/// <param name="PositionIndex">Zero-based position index within the side.</param>
|
|
||||||
/// <param name="DisplayName">Human-readable name, e.g. "Rex".</param>
|
|
||||||
public record AgentMinionMapping(
|
|
||||||
string AgentId,
|
|
||||||
MinionSide Side,
|
|
||||||
int PositionIndex,
|
|
||||||
string DisplayName
|
|
||||||
);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Real-time minion state update pushed to SignalR clients
|
|
||||||
/// when an agent's status changes. Combines the static mapping
|
|
||||||
/// (who/where) with the dynamic state (what the minion is doing).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AgentId">Agent identifier, e.g. "rex".</param>
|
|
||||||
/// <param name="DisplayName">Human-readable minion name, e.g. "Rex".</param>
|
|
||||||
/// <param name="Side">Which side of the dashboard — Dev or Business.</param>
|
|
||||||
/// <param name="PositionIndex">Position within the side (0-based).</param>
|
|
||||||
/// <param name="State">Current minion animation state.</param>
|
|
||||||
/// <param name="Timestamp">ISO 8601 timestamp of the state change.</param>
|
|
||||||
public record MinionStateUpdate(
|
|
||||||
string AgentId,
|
|
||||||
string DisplayName,
|
|
||||||
MinionSide Side,
|
|
||||||
int PositionIndex,
|
|
||||||
MinionState State,
|
|
||||||
string Timestamp
|
|
||||||
);
|
|
||||||
@@ -52,11 +52,6 @@ builder.Services.AddSignalR();
|
|||||||
builder.Services.AddSingleton<GatewayEventBridgeService>();
|
builder.Services.AddSingleton<GatewayEventBridgeService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<GatewayEventBridgeService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<GatewayEventBridgeService>());
|
||||||
|
|
||||||
// ── Agent-Minion Mapper Service ────────────────────────────
|
|
||||||
// Maps agents to minion sprites/positions and publishes state
|
|
||||||
// updates through SignalR.
|
|
||||||
builder.Services.AddSingleton<AgentMinionMapperService>();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// ── Middleware ──────────────────────────────────────────────
|
// ── Middleware ──────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
using ControlCenter.Hubs;
|
|
||||||
using ControlCenter.Models;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
|
|
||||||
namespace ControlCenter.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Service that maps Linear agents to minion sprites and positions
|
|
||||||
/// in the Control Center dashboard.
|
|
||||||
///
|
|
||||||
/// <para>Static mappings define where each minion appears:</para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>Dev side: Rex (0), Dex (1), Hex (2)</item>
|
|
||||||
/// <item>Business side: Larry (0), Mel (1), Buzz (2)</item>
|
|
||||||
/// </list>
|
|
||||||
///
|
|
||||||
/// <para>Dynamic state is derived from the agent's <see cref="AgentStatus"/>:</para>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><c>Active</c> → <see cref="MinionState.Active"/></item>
|
|
||||||
/// <item><c>Idle</c> → <see cref="MinionState.Idle"/></item>
|
|
||||||
/// <item><c>Thinking</c> → <see cref="MinionState.Thinking"/></item>
|
|
||||||
/// <item><c>Error</c> → <see cref="MinionState.Error"/></item>
|
|
||||||
/// </list>
|
|
||||||
///
|
|
||||||
/// <para>State updates are published through the <see cref="AgentStatusHub"/>
|
|
||||||
/// SignalR hub so that connected clients can animate minion sprites
|
|
||||||
/// in real time.</para>
|
|
||||||
/// </summary>
|
|
||||||
public class AgentMinionMapperService
|
|
||||||
{
|
|
||||||
private readonly ILogger<AgentMinionMapperService> _logger;
|
|
||||||
private readonly IHubContext<AgentStatusHub, IAgentStatusClient> _hubContext;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Static agent-to-minion mapping table. Defines which side and position
|
|
||||||
/// each agent's minion occupies on the dashboard.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Dictionary<string, AgentMinionMapping> Mappings = new()
|
|
||||||
{
|
|
||||||
// ── Dev Side ──────────────────────────────────
|
|
||||||
["rex"] = new AgentMinionMapping("rex", MinionSide.Dev, 0, "Rex"),
|
|
||||||
["dex"] = new AgentMinionMapping("dex", MinionSide.Dev, 1, "Dex"),
|
|
||||||
["hex"] = new AgentMinionMapping("hex", MinionSide.Dev, 2, "Hex"),
|
|
||||||
|
|
||||||
// ── Business Side ─────────────────────────────
|
|
||||||
["larry"] = new AgentMinionMapping("larry", MinionSide.Business, 0, "Larry"),
|
|
||||||
["mel"] = new AgentMinionMapping("mel", MinionSide.Business, 1, "Mel"),
|
|
||||||
["buzz"] = new AgentMinionMapping("buzz", MinionSide.Business, 2, "Buzz"),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps <see cref="AgentStatus"/> string values to <see cref="MinionState"/>.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Dictionary<string, MinionState> StatusToMinionState = new()
|
|
||||||
{
|
|
||||||
["active"] = MinionState.Active,
|
|
||||||
["idle"] = MinionState.Idle,
|
|
||||||
["thinking"] = MinionState.Thinking,
|
|
||||||
["error"] = MinionState.Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
public AgentMinionMapperService(
|
|
||||||
ILogger<AgentMinionMapperService> logger,
|
|
||||||
IHubContext<AgentStatusHub, IAgentStatusClient> hubContext)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_hubContext = hubContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the minion mapping for a given agent ID.
|
|
||||||
/// Returns null if the agent is not mapped to a minion position.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="agentId">The agent identifier, e.g. "rex", "dex".</param>
|
|
||||||
/// <returns>The mapping record, or null if unmapped.</returns>
|
|
||||||
public AgentMinionMapping? GetMapping(string agentId)
|
|
||||||
{
|
|
||||||
return Mappings.GetValueOrDefault(agentId?.ToLowerInvariant() ?? string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all minion mappings, ordered by side then position index.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>All mappings, sorted for consistent display order.</returns>
|
|
||||||
public IReadOnlyList<AgentMinionMapping> GetAllMappings()
|
|
||||||
{
|
|
||||||
return Mappings.Values
|
|
||||||
.OrderBy(m => m.Side)
|
|
||||||
.ThenBy(m => m.PositionIndex)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts an agent status string to a <see cref="MinionState"/>.
|
|
||||||
/// Falls back to <see cref="MinionState.Idle"/> for unrecognized statuses.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="status">Agent status string: "active", "idle", "thinking", or "error".</param>
|
|
||||||
/// <returns>The corresponding minion state.</returns>
|
|
||||||
public MinionState StatusToState(string status)
|
|
||||||
{
|
|
||||||
return StatusToMinionState.GetValueOrDefault(
|
|
||||||
status?.ToLowerInvariant() ?? string.Empty,
|
|
||||||
MinionState.Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Publishes a minion state update through SignalR when an agent's
|
|
||||||
/// status changes. Only publishes for agents that have a minion mapping.
|
|
||||||
///
|
|
||||||
/// <para>This is the primary integration point: the
|
|
||||||
/// <see cref="GatewayEventBridgeService"/> calls this method
|
|
||||||
/// whenever it detects a status change from the OpenClaw Gateway.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="agentId">The agent whose status changed, e.g. "dex".</param>
|
|
||||||
/// <param name="status">The new status string: "active", "idle", "thinking", or "error".</param>
|
|
||||||
/// <returns>A task that completes when the SignalR message has been sent.</returns>
|
|
||||||
public async Task PublishMinionStateUpdateAsync(string agentId, string status)
|
|
||||||
{
|
|
||||||
var mapping = GetMapping(agentId);
|
|
||||||
if (mapping is null)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("No minion mapping for agent {AgentId}; skipping state update", agentId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minionState = StatusToState(status);
|
|
||||||
var update = new MinionStateUpdate(
|
|
||||||
AgentId: mapping.AgentId,
|
|
||||||
DisplayName: mapping.DisplayName,
|
|
||||||
Side: mapping.Side,
|
|
||||||
PositionIndex: mapping.PositionIndex,
|
|
||||||
State: minionState,
|
|
||||||
Timestamp: DateTime.UtcNow.ToString("o")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Broadcast to the fleet group (all subscribers)
|
|
||||||
await _hubContext.Clients.Group(AgentStatusHub.FleetGroupName)
|
|
||||||
.AgentStatusChanged(ToAgentStatusUpdate(agentId, status));
|
|
||||||
|
|
||||||
// Also push to the specific agent's group
|
|
||||||
var agentGroup = AgentStatusHub.AgentGroupName(agentId);
|
|
||||||
await _hubContext.Clients.Group(agentGroup)
|
|
||||||
.AgentStatusChanged(ToAgentStatusUpdate(agentId, status));
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Minion state update: {AgentId} → {State} (Side: {Side}, Position: {Index})",
|
|
||||||
agentId, minionState, mapping.Side, mapping.PositionIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current minion state for all mapped agents, suitable
|
|
||||||
/// for building an initial fleet snapshot.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>All minion mappings with their current (idle) state.</returns>
|
|
||||||
public IReadOnlyList<MinionStateUpdate> GetFullMinionState()
|
|
||||||
{
|
|
||||||
return Mappings.Values
|
|
||||||
.OrderBy(m => m.Side)
|
|
||||||
.ThenBy(m => m.PositionIndex)
|
|
||||||
.Select(m => new MinionStateUpdate(
|
|
||||||
AgentId: m.AgentId,
|
|
||||||
DisplayName: m.DisplayName,
|
|
||||||
Side: m.Side,
|
|
||||||
PositionIndex: m.PositionIndex,
|
|
||||||
State: MinionState.Idle,
|
|
||||||
Timestamp: DateTime.UtcNow.ToString("o")))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts a status string to an <see cref="AgentStatusUpdate"/>
|
|
||||||
/// for SignalR push. Uses the mapping table for display names and roles.
|
|
||||||
/// </summary>
|
|
||||||
private AgentStatusUpdate ToAgentStatusUpdate(string agentId, string status)
|
|
||||||
{
|
|
||||||
var mapping = GetMapping(agentId);
|
|
||||||
var displayName = mapping?.DisplayName ?? char.ToUpperInvariant(agentId[0]) + agentId[1..];
|
|
||||||
|
|
||||||
return new AgentStatusUpdate(
|
|
||||||
AgentId: agentId,
|
|
||||||
DisplayName: displayName,
|
|
||||||
Role: mapping is not null
|
|
||||||
? $"{mapping.Side} Agent"
|
|
||||||
: "Agent",
|
|
||||||
Status: status,
|
|
||||||
CurrentTask: null,
|
|
||||||
SessionKey: string.Empty,
|
|
||||||
Channel: string.Empty,
|
|
||||||
LastActivity: DateTime.UtcNow.ToString("o"),
|
|
||||||
ErrorMessage: status == "error" ? "Agent encountered an error" : null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user