feat(CUB-55): add SignalR broadcast state method with AgentStatusHub and DTO
This commit is contained in:
75
backend/Dtos/AgentStatusUpdateDto.cs
Normal file
75
backend/Dtos/AgentStatusUpdateDto.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
namespace ControlCenter.Api.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data transfer object for broadcasting agent status updates
|
||||||
|
/// to all connected SignalR clients.
|
||||||
|
/// </summary>
|
||||||
|
public class AgentStatusUpdateDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Agent identifier, e.g. "otto", "dex", "rex".
|
||||||
|
/// Not null — every update must identify the agent it refers to.
|
||||||
|
/// </summary>
|
||||||
|
public string AgentId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Human-readable display name, e.g. "Otto", "Dex".
|
||||||
|
/// Not null — used by clients to render agent cards.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Role description, e.g. "Orchestrator Agent", "Backend Specialist".
|
||||||
|
/// Not null — provides context for the agent's function.
|
||||||
|
/// </summary>
|
||||||
|
public string Role { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current operational status of the agent.
|
||||||
|
/// Maps to <see cref="Entities.AgentStatus"/> values as lowercase strings:
|
||||||
|
/// "active", "idle", "thinking", "error".
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Description of the agent's current task, if any.
|
||||||
|
/// Null when the agent is idle with no active task.
|
||||||
|
/// </summary>
|
||||||
|
public string? CurrentTask { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Task progress percentage (0–100).
|
||||||
|
/// Null when progress is not trackable for the current task.
|
||||||
|
/// </summary>
|
||||||
|
public int? TaskProgress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Elapsed time string for the current task, e.g. "04m 12s".
|
||||||
|
/// Null when no task is active.
|
||||||
|
/// </summary>
|
||||||
|
public string? TaskElapsed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full session key, e.g. "agent:otto:telegram:direct:8787451565".
|
||||||
|
/// Not null — uniquely identifies the agent session.
|
||||||
|
/// </summary>
|
||||||
|
public string SessionKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Communication channel the agent is operating on, e.g. "telegram", "discord", "slack".
|
||||||
|
/// Not null — every agent session operates on exactly one channel.
|
||||||
|
/// </summary>
|
||||||
|
public string Channel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ISO 8601 timestamp of the agent's last activity.
|
||||||
|
/// Not null — used by clients to detect stale connections.
|
||||||
|
/// </summary>
|
||||||
|
public string LastActivity { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error message when the agent status is "error".
|
||||||
|
/// Null when the agent is not in an error state.
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
155
backend/Hubs/AgentStatusHub.cs
Normal file
155
backend/Hubs/AgentStatusHub.cs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
using ControlCenter.Api.Dtos;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace ControlCenter.Api.Hubs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SignalR hub for broadcasting agent status updates to connected clients.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Clients call <see cref="SendStatusUpdate"/> to broadcast a status change,
|
||||||
|
/// and the hub relays it to all connected clients via the
|
||||||
|
/// <see cref="IAgentStatusClient.AgentStatusChanged"/> callback.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Server-side code should use <see cref="AgentStatusHubExtensions.PushStatusUpdateAsync"/>
|
||||||
|
/// via <c>IHubContext<AgentStatusHub, IAgentStatusClient></c> for background-service broadcasts.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Architecture note: This hub bridges OpenClaw Gateway events to SignalR clients.
|
||||||
|
/// A background service subscribes to Gateway events and pushes them through
|
||||||
|
/// this hub's extension methods.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class AgentStatusHub : Hub<IAgentStatusClient>
|
||||||
|
{
|
||||||
|
private readonly ILogger<AgentStatusHub> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AgentStatusHub"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger for diagnostic output.</param>
|
||||||
|
public AgentStatusHub(ILogger<AgentStatusHub> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Broadcasts an agent status update to all connected clients.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Any connected client (or server-side caller) can invoke this method
|
||||||
|
/// to push a status update to every subscriber. The update is relayed
|
||||||
|
/// through the <see cref="IAgentStatusClient.AgentStatusChanged"/> callback.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update">The agent status update payload to broadcast.</param>
|
||||||
|
public async Task SendStatusUpdate(AgentStatusUpdateDto update)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Broadcasting status update for agent {AgentId}: {Status}",
|
||||||
|
update.AgentId, update.Status);
|
||||||
|
|
||||||
|
await Clients.All.AgentStatusChanged(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the calling connection to the fleet group.
|
||||||
|
/// Once joined, the client will receive all agent status updates.
|
||||||
|
/// </summary>
|
||||||
|
public async Task JoinFleet()
|
||||||
|
{
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, FleetGroupName);
|
||||||
|
_logger.LogDebug("Connection {ConnectionId} joined fleet group", Context.ConnectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the calling connection from the fleet group.
|
||||||
|
/// </summary>
|
||||||
|
public async Task LeaveFleet()
|
||||||
|
{
|
||||||
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, FleetGroupName);
|
||||||
|
_logger.LogDebug("Connection {ConnectionId} left fleet group", Context.ConnectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides <see cref="Hub{T}.OnDisconnectedAsync"/> to log disconnections.
|
||||||
|
/// SignalR automatically removes disconnected connections from all groups.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">Exception that caused the disconnection, if any.</param>
|
||||||
|
public override Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Connection {ConnectionId} disconnected", Context.ConnectionId);
|
||||||
|
return base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The SignalR group name for the entire fleet (all agents).
|
||||||
|
/// </summary>
|
||||||
|
internal const string FleetGroupName = "fleet";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strongly-typed client interface for the AgentStatus SignalR hub.
|
||||||
|
/// Defines the methods the server can invoke on connected clients
|
||||||
|
/// to push real-time agent status updates.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAgentStatusClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pushes an agent status change to all subscribed clients.
|
||||||
|
/// Fired whenever an agent's operational status changes
|
||||||
|
/// (e.g., idle → active, active → thinking, active → error).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="update">The full status update payload.</param>
|
||||||
|
/// <returns>A Task that completes when the client has processed the update.</returns>
|
||||||
|
Task AgentStatusChanged(AgentStatusUpdateDto update);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for pushing real-time agent updates through
|
||||||
|
/// the <see cref="IHubContext{T}"/> of <see cref="AgentStatusHub"/>.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// These methods are intended to be called from background services
|
||||||
|
/// or other server-side code that detects an agent state change,
|
||||||
|
/// using the injected <c>IHubContext<AgentStatusHub, IAgentStatusClient></c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static class AgentStatusHubExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pushes an agent status update to all connected clients.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Call this from any background service when an agent's
|
||||||
|
/// operational status changes (e.g., the Gateway reports a
|
||||||
|
/// session transition from "running" to "done").
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hubContext">The hub context injected via DI.</param>
|
||||||
|
/// <param name="update">The agent status update payload.</param>
|
||||||
|
/// <returns>A Task that completes when the message has been sent to all clients.</returns>
|
||||||
|
public static async Task PushStatusUpdateAsync(
|
||||||
|
this IHubContext<AgentStatusHub, IAgentStatusClient> hubContext,
|
||||||
|
AgentStatusUpdateDto update)
|
||||||
|
{
|
||||||
|
await hubContext.Clients.All.AgentStatusChanged(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pushes an agent status update to clients subscribed to the fleet group.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hubContext">The hub context injected via DI.</param>
|
||||||
|
/// <param name="update">The agent status update payload.</param>
|
||||||
|
/// <returns>A Task that completes when the message has been sent to the fleet group.</returns>
|
||||||
|
public static async Task PushStatusUpdateToFleetAsync(
|
||||||
|
this IHubContext<AgentStatusHub, IAgentStatusClient> hubContext,
|
||||||
|
AgentStatusUpdateDto update)
|
||||||
|
{
|
||||||
|
await hubContext.Clients.Group(AgentStatusHub.FleetGroupName)
|
||||||
|
.AgentStatusChanged(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using ControlCenter.Api.Data;
|
using ControlCenter.Api.Data;
|
||||||
|
using ControlCenter.Api.Hubs;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -6,6 +7,9 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
// Register SignalR for real-time agent status updates
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
// Register DbContext with PostgreSQL
|
// Register DbContext with PostgreSQL
|
||||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
{
|
{
|
||||||
@@ -28,4 +32,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
// Map SignalR hubs
|
||||||
|
app.MapHub<AgentStatusHub>("/hubs/agent-status");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
Reference in New Issue
Block a user