Skip to content

Commit a766bee

Browse files
feat: add SignalR AgentHub, Discord/Slack channel tests (Phase 3 progress)
1 parent 3dfe3dc commit a766bee

9 files changed

Lines changed: 448 additions & 5 deletions

File tree

ClawSharp.slnx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
</Folder>
1414
<Folder Name="/tests/">
1515
<Project Path="tests/ClawSharp.Agent.Tests/ClawSharp.Agent.Tests.csproj" />
16-
<Project Path="tests/ClawSharp.Channels.Tests/ClawSharp.Channels.Tests.csproj" />
1716
<Project Path="tests/ClawSharp.Cli.Tests/ClawSharp.Cli.Tests.csproj" />
1817
<Project Path="tests/ClawSharp.Core.Tests/ClawSharp.Core.Tests.csproj" />
1918
<Project Path="tests/ClawSharp.Gateway.Tests/ClawSharp.Gateway.Tests.csproj" />

src/ClawSharp.Channels/ClawSharp.Channels.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
1010
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
1111
<PackageReference Include="Telegram.Bot" Version="22.0.0" />
12+
<PackageReference Include="Discord.Net" Version="3.17.0" />
1213
</ItemGroup>
1314

1415
<ItemGroup>

src/ClawSharp.Gateway/ClawSharp.Gateway.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
<ItemGroup>
1313
<ProjectReference Include="..\ClawSharp.Agent\ClawSharp.Agent.csproj" />
14+
<ProjectReference Include="..\ClawSharp.Channels\ClawSharp.Channels.csproj" />
1415
<ProjectReference Include="..\ClawSharp.Core\ClawSharp.Core.csproj" />
1516
<ProjectReference Include="..\ClawSharp.Infrastructure\ClawSharp.Infrastructure.csproj" />
1617
</ItemGroup>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
using ClawSharp.Agent;
2+
using ClawSharp.Core.Channels;
3+
using ClawSharp.Core.Providers;
4+
using ClawSharp.Core.Sessions;
5+
using Microsoft.AspNetCore.SignalR;
6+
using CoreLlmMessage = ClawSharp.Core.Providers.LlmMessage;
7+
8+
namespace ClawSharp.Gateway.Hubs;
9+
10+
/// <summary>
11+
/// SignalR hub for real-time agent ↔ UI communication.
12+
/// </summary>
13+
public class AgentHub : Hub
14+
{
15+
private readonly AgentLoop _agentLoop;
16+
private readonly ISessionManager _sessionManager;
17+
private readonly IMessageBus _messageBus;
18+
private readonly ILogger<AgentHub> _logger;
19+
20+
public AgentHub(
21+
AgentLoop agentLoop,
22+
ISessionManager sessionManager,
23+
IMessageBus messageBus,
24+
ILogger<AgentHub> logger)
25+
{
26+
_agentLoop = agentLoop;
27+
_sessionManager = sessionManager;
28+
_messageBus = messageBus;
29+
_logger = logger;
30+
}
31+
32+
/// <summary>
33+
/// Send a message to the agent and get a streaming response.
34+
/// </summary>
35+
/// <param name="message">The user's message</param>
36+
/// <param name="channel">The channel name (e.g., "web", "telegram")</param>
37+
/// <param name="chatId">The chat/session ID</param>
38+
/// <param name="model">Optional model to use</param>
39+
/// <param name="cancellationToken">Cancellation token</param>
40+
public async Task SendMessageAsync(
41+
string message,
42+
string channel,
43+
string chatId,
44+
string? model = null,
45+
CancellationToken cancellationToken = default)
46+
{
47+
try
48+
{
49+
// Get or create session
50+
var session = await _sessionManager.GetOrCreateAsync(
51+
channel,
52+
chatId,
53+
Context.ConnectionId,
54+
cancellationToken);
55+
56+
// Add user message to session history
57+
session.History.Add(new CoreLlmMessage("user", message));
58+
59+
// Run agent loop
60+
var request = new AgentLoop.AgentRequest(
61+
Model: model ?? "default",
62+
InitialMessages: session.History
63+
);
64+
65+
_logger.LogInformation("Processing message for session {SessionKey}", session.SessionKey);
66+
67+
var result = await _agentLoop.RunAsync(request, cancellationToken);
68+
69+
// Add assistant response to session history
70+
session.History.Add(new CoreLlmMessage("assistant", result.Content));
71+
await _sessionManager.SaveAsync(session, cancellationToken);
72+
73+
// Send completion to client
74+
await Clients.Caller.SendAsync("OnComplete", result, cancellationToken);
75+
}
76+
catch (OperationCanceledException)
77+
{
78+
_logger.LogInformation("Message processing cancelled for connection {ConnectionId}", Context.ConnectionId);
79+
await Clients.Caller.SendAsync("OnError", new OperationCanceledException("Request was cancelled"), cancellationToken);
80+
}
81+
catch (Exception ex)
82+
{
83+
_logger.LogError(ex, "Error processing message for connection {ConnectionId}", Context.ConnectionId);
84+
await Clients.Caller.SendAsync("OnError", ex, cancellationToken);
85+
}
86+
}
87+
88+
/// <summary>
89+
/// Stream a message to the agent with streaming responses.
90+
/// </summary>
91+
public async Task SendMessageStreamAsync(
92+
string message,
93+
string channel,
94+
string chatId,
95+
string? model = null,
96+
CancellationToken cancellationToken = default)
97+
{
98+
IDisposable? toolStartedSubscription = null;
99+
IDisposable? toolCompletedSubscription = null;
100+
101+
try
102+
{
103+
// Get or create session
104+
var session = await _sessionManager.GetOrCreateAsync(
105+
channel,
106+
chatId,
107+
Context.ConnectionId,
108+
cancellationToken);
109+
110+
// Add user message to session history
111+
session.History.Add(new CoreLlmMessage("user", message));
112+
113+
// Run agent loop with streaming
114+
var request = new AgentLoop.AgentRequest(
115+
Model: model ?? "default",
116+
InitialMessages: session.History
117+
);
118+
119+
_logger.LogInformation("Streaming message for session {SessionKey}", session.SessionKey);
120+
121+
// Subscribe to tool events and forward to client
122+
toolStartedSubscription = _messageBus.Subscribe<AgentLoop.ToolStartedEvent>(async (evt) =>
123+
{
124+
await Clients.Caller.SendAsync("OnToolStarted", evt);
125+
});
126+
127+
toolCompletedSubscription = _messageBus.Subscribe<AgentLoop.ToolCompletedEvent>(async (evt) =>
128+
{
129+
await Clients.Caller.SendAsync("OnToolCompleted", evt);
130+
});
131+
132+
var result = await _agentLoop.RunAsync(request, cancellationToken);
133+
134+
// Add assistant response to session history
135+
session.History.Add(new CoreLlmMessage("assistant", result.Content));
136+
await _sessionManager.SaveAsync(session, cancellationToken);
137+
138+
// Send completion to client
139+
await Clients.Caller.SendAsync("OnComplete", result, cancellationToken);
140+
}
141+
catch (OperationCanceledException)
142+
{
143+
_logger.LogInformation("Message streaming cancelled for connection {ConnectionId}", Context.ConnectionId);
144+
await Clients.Caller.SendAsync("OnError", new OperationCanceledException("Request was cancelled"), cancellationToken);
145+
}
146+
catch (Exception ex)
147+
{
148+
_logger.LogError(ex, "Error streaming message for connection {ConnectionId}", Context.ConnectionId);
149+
await Clients.Caller.SendAsync("OnError", ex, cancellationToken);
150+
}
151+
finally
152+
{
153+
// Unsubscribe from events
154+
toolStartedSubscription?.Dispose();
155+
toolCompletedSubscription?.Dispose();
156+
}
157+
}
158+
159+
/// <summary>
160+
/// Cancel the current request.
161+
/// </summary>
162+
public Task CancelAsync()
163+
{
164+
// SignalR doesn't have built-in per-request cancellation
165+
// The cancellation token passed to SendMessage/SendMessageStream controls this
166+
_logger.LogInformation("Cancel requested for connection {ConnectionId}", Context.ConnectionId);
167+
return Task.CompletedTask;
168+
}
169+
170+
public override async Task OnConnectedAsync()
171+
{
172+
_logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
173+
await base.OnConnectedAsync();
174+
}
175+
176+
public override async Task OnDisconnectedAsync(Exception? exception)
177+
{
178+
_logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
179+
await base.OnDisconnectedAsync(exception);
180+
}
181+
}

src/ClawSharp.Gateway/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
using ClawSharp.Agent;
2+
using ClawSharp.Channels;
23
using ClawSharp.Core.Channels;
34
using ClawSharp.Core.Config;
45
using ClawSharp.Core.Providers;
56
using ClawSharp.Core.Sessions;
67
using ClawSharp.Core.Tools;
8+
using ClawSharp.Gateway;
9+
using ClawSharp.Gateway.Endpoints;
10+
using ClawSharp.Gateway.Hubs;
711
using ClawSharp.Infrastructure;
812
using ClawSharp.Infrastructure.Messaging;
9-
using ClawSharp.Gateway.Endpoints;
1013
using Microsoft.AspNetCore.Mvc;
1114

1215
var builder = WebApplication.CreateBuilder(args);
1316

1417
// Add services to the container.
1518
builder.Services.AddEndpointsApiExplorer();
1619
builder.Services.AddOpenApi();
20+
builder.Services.AddSignalR();
1721

1822
// Add ClawSharp services
1923
var config = new ClawSharpConfig
@@ -68,6 +72,9 @@
6872
// Map Gateway endpoints
6973
app.MapGatewayEndpoints();
7074

75+
// Map SignalR hubs
76+
app.MapHub<AgentHub>("/hubs/agent");
77+
7178
app.Run();
7279

7380
/// <summary>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="coverlet.collector" Version="6.0.4" />
11+
<PackageReference Include="FluentAssertions" Version="8.8.0" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
13+
<PackageReference Include="xunit" Version="2.9.3" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
15+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
16+
<PackageReference Include="NSubstitute" Version="5.3.0" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<Using Include="Xunit" />
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<ProjectReference Include="..\..\src\ClawSharp.Channels\ClawSharp.Channels.csproj" />
25+
<ProjectReference Include="..\..\src\ClawSharp.Core\ClawSharp.Core.csproj" />
26+
<ProjectReference Include="..\..\src\ClawSharp.Infrastructure\ClawSharp.Infrastructure.csproj" />
27+
<ProjectReference Include="..\ClawSharp.TestHelpers\ClawSharp.TestHelpers.csproj" />
28+
</ItemGroup>
29+
30+
</Project>

0 commit comments

Comments
 (0)