diff --git a/src/RockBot.Agent/ClearContextHandler.cs b/src/RockBot.Agent/ClearContextHandler.cs new file mode 100644 index 0000000..ed93c76 --- /dev/null +++ b/src/RockBot.Agent/ClearContextHandler.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Logging; +using RockBot.Host; +using RockBot.Messaging; +using RockBot.UserProxy; + +namespace RockBot.Agent; + +/// +/// Handles by clearing conversation memory +/// for the session. Long-term memory and conversation logs are preserved. +/// +internal sealed class ClearContextHandler( + IConversationMemory conversationMemory, + ISessionTracker sessionTracker, + IMessagePublisher publisher, + AgentIdentity agent, + ILogger logger) : IMessageHandler +{ + public async Task HandleAsync(ClearContextRequest message, MessageHandlerContext context) + { + var ct = context.CancellationToken; + + // Cancel any in-flight work for this session + var handle = sessionTracker.BeginSession(message.SessionId, ct); + sessionTracker.EndSession(message.SessionId, handle.Generation); + + // Clear conversation memory (ephemeral turns only — logs are preserved) + await conversationMemory.ClearAsync(message.SessionId, ct); + + logger.LogInformation("Cleared conversation context for session {SessionId}", message.SessionId); + + var reply = new AgentReply + { + Content = "Context cleared — starting fresh.", + SessionId = message.SessionId, + AgentName = agent.Name, + IsFinal = true + }; + var envelope = reply.ToEnvelope(source: agent.Name); + await publisher.PublishAsync(UserProxyTopics.UserResponse, envelope, ct); + } +} diff --git a/src/RockBot.Agent/Program.cs b/src/RockBot.Agent/Program.cs index d925722..2200462 100644 --- a/src/RockBot.Agent/Program.cs +++ b/src/RockBot.Agent/Program.cs @@ -227,6 +227,7 @@ IChatClient BuildClient(LlmTierConfig config) agent.HandleMessage(); agent.HandleMessage(); agent.HandleMessage(); + agent.HandleMessage(); agent.HandleMessage(); agent.HandleMessage(); agent.WithSavedResponses(); @@ -237,6 +238,7 @@ IChatClient BuildClient(LlmTierConfig config) agent.SubscribeTo(UserProxyTopics.UserMessage); agent.SubscribeTo(UserProxyTopics.UserFeedback); agent.SubscribeTo(UserProxyTopics.CancelSession); + agent.SubscribeTo(UserProxyTopics.ClearContext); agent.SubscribeTo(UserProxyTopics.ConversationHistoryRequest); agent.SubscribeTo(UserProxyTopics.AgentInfoRequest); agent.SubscribeTo(UserProxyTopics.SaveResponseRequest); diff --git a/src/RockBot.UserProxy.Abstractions/ClearContextRequest.cs b/src/RockBot.UserProxy.Abstractions/ClearContextRequest.cs new file mode 100644 index 0000000..f5c7b31 --- /dev/null +++ b/src/RockBot.UserProxy.Abstractions/ClearContextRequest.cs @@ -0,0 +1,10 @@ +namespace RockBot.UserProxy; + +/// +/// Message sent from the user proxy to clear conversation context for a session. +/// Long-term memory and conversation logs are preserved. +/// +public sealed record ClearContextRequest +{ + public required string SessionId { get; init; } +} diff --git a/src/RockBot.UserProxy.Abstractions/UserProxyTopics.cs b/src/RockBot.UserProxy.Abstractions/UserProxyTopics.cs index 12fae4e..4a92fc9 100644 --- a/src/RockBot.UserProxy.Abstractions/UserProxyTopics.cs +++ b/src/RockBot.UserProxy.Abstractions/UserProxyTopics.cs @@ -8,6 +8,7 @@ public static class UserProxyTopics public const string UserMessage = "user.message"; public const string UserResponse = "user.response"; public const string CancelSession = "user.cancel"; + public const string ClearContext = "user.context.clear"; public const string ConversationHistoryRequest = "user.history.request"; public const string ConversationHistoryResponse = "user.history.response"; public const string UserFeedback = "user.feedback"; diff --git a/src/RockBot.UserProxy.Blazor/Pages/Chat.razor b/src/RockBot.UserProxy.Blazor/Pages/Chat.razor index 7db3a48..af9bab4 100644 --- a/src/RockBot.UserProxy.Blazor/Pages/Chat.razor +++ b/src/RockBot.UserProxy.Blazor/Pages/Chat.razor @@ -420,6 +420,21 @@ currentMessage = string.Empty; await JSRuntime.InvokeVoidAsync("chatHelpers.addToHistory", messageInput, messageContent); + // Handle /clear command — pure plumbing, no LLM involved + if (messageContent.Equals("/clear", StringComparison.OrdinalIgnoreCase)) + { + ChatState.ClearMessages(); + try + { + await ProxyService.ClearContextAsync(SessionId); + } + catch (Exception ex) + { + ChatState.AddError($"Could not clear agent context: {ex.Message}"); + } + return; + } + ChatState.AddUserMessage(messageContent, UserId, SessionId); ChatState.SetProcessing(true); diff --git a/src/RockBot.UserProxy.Blazor/Services/ChatStateService.cs b/src/RockBot.UserProxy.Blazor/Services/ChatStateService.cs index bb6cb1d..f459689 100644 --- a/src/RockBot.UserProxy.Blazor/Services/ChatStateService.cs +++ b/src/RockBot.UserProxy.Blazor/Services/ChatStateService.cs @@ -273,6 +273,20 @@ public void AddError(string message) NotifyStateChanged(); } + // ── Clear context ─────────────────────────────────────────────────────── + + public void ClearMessages() + { + lock (_lock) + { + _messages.Clear(); + _activeActivityLogs.Clear(); + } + _currentThinkingMessage = null; + _isProcessing = false; + NotifyStateChanged(); + } + // ── Saved references ──────────────────────────────────────────────────── public void AddSavedReference(string id, string label, string content, string agentName) diff --git a/src/RockBot.UserProxy/UserProxyService.cs b/src/RockBot.UserProxy/UserProxyService.cs index 429e4b6..7d67e2b 100644 --- a/src/RockBot.UserProxy/UserProxyService.cs +++ b/src/RockBot.UserProxy/UserProxyService.cs @@ -333,6 +333,18 @@ public async Task CancelSessionAsync(string sessionId, CancellationToken cancell logger.LogInformation("Published cancel request for session {SessionId}", sessionId); } + /// + /// Publishes a clear context request to reset conversation memory for the given session. + /// Long-term memory and conversation logs are preserved. + /// + public async Task ClearContextAsync(string sessionId, CancellationToken cancellationToken = default) + { + var request = new ClearContextRequest { SessionId = sessionId }; + var envelope = request.ToEnvelope(source: options.ProxyId); + await publisher.PublishAsync(UserProxyTopics.ClearContext, envelope, cancellationToken); + logger.LogInformation("Published clear context request for session {SessionId}", sessionId); + } + internal Task HandleResponseAsync(MessageEnvelope envelope, CancellationToken ct) { AgentReply? reply;