From 3c468eb37794cf0ff0669645a9007023b94c7807 Mon Sep 17 00:00:00 2001 From: ModerRAS Date: Fri, 17 Apr 2026 14:39:19 +0800 Subject: [PATCH] Fix LLM agent stability: conditional execution, error handling, tool registration - BackgroundServices (TelegramTaskConsumer, ChunkPollingService, AgentRegistryService) now early-return when EnableLLMAgentProcess is disabled, preventing unnecessary Redis calls and potential crashes from RedisTimeoutException - Agent process BRPOP timeout reduced from 5s to 2s to avoid race with SE.Redis async timeout (same fix as PR #267 for the main process) - AgentLoopService main loop now catches RedisException with retry delay, preventing a single transient Redis failure from crashing the agent process - Heartbeat loop catches OperationCanceledException and RedisException, preventing unobserved exceptions during shutdown or transient failures - Agent process now registers LLM project tools (FileToolService, BashToolService) via two-assembly McpToolHelper.EnsureInitialized, giving the agent access to file operations and shell execution instead of only echo/calculator/send_message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- TelegramSearchBot.LLMAgent/LLMAgentProgram.cs | 9 ++++++- .../Service/AgentLoopService.cs | 25 ++++++++++++++++--- .../Service/AI/LLM/AgentRegistryService.cs | 5 ++++ .../Service/AI/LLM/ChunkPollingService.cs | 19 ++++++++++++-- .../Service/AI/LLM/TelegramTaskConsumer.cs | 5 ++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/TelegramSearchBot.LLMAgent/LLMAgentProgram.cs b/TelegramSearchBot.LLMAgent/LLMAgentProgram.cs index 3b9e7c8d..dfd95c7e 100644 --- a/TelegramSearchBot.LLMAgent/LLMAgentProgram.cs +++ b/TelegramSearchBot.LLMAgent/LLMAgentProgram.cs @@ -6,8 +6,10 @@ using TelegramSearchBot.Common; using TelegramSearchBot.Interface; using TelegramSearchBot.Interface.AI.LLM; +using TelegramSearchBot.Interface.Tools; using TelegramSearchBot.Model; using TelegramSearchBot.Service.AI.LLM; +using TelegramSearchBot.Service.Tools; namespace TelegramSearchBot.LLMAgent { public static class LLMAgentProgram { @@ -23,7 +25,10 @@ public static async Task RunAsync(string[] args) { using var services = BuildServices(port); var logger = services.GetRequiredService().CreateLogger("LLMAgent"); - McpToolHelper.EnsureInitialized(typeof(Service.AgentToolService).Assembly, services, logger); + McpToolHelper.EnsureInitialized( + typeof(Service.AgentToolService).Assembly, + typeof(FileToolService).Assembly, + services, logger); var loop = services.GetRequiredService(); using var shutdownCts = new CancellationTokenSource(); @@ -63,6 +68,8 @@ private static ServiceProvider BuildServices(int port) { services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); diff --git a/TelegramSearchBot.LLMAgent/Service/AgentLoopService.cs b/TelegramSearchBot.LLMAgent/Service/AgentLoopService.cs index 591c8dc8..fbabbd38 100644 --- a/TelegramSearchBot.LLMAgent/Service/AgentLoopService.cs +++ b/TelegramSearchBot.LLMAgent/Service/AgentLoopService.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using StackExchange.Redis; using TelegramSearchBot.Common; using TelegramSearchBot.Model.AI; @@ -42,7 +43,15 @@ public async Task RunAsync(long chatId, int port, CancellationToken cancellation break; } - var payload = await _garnetClient.BRPopAsync(LlmAgentRedisKeys.AgentTaskQueue, TimeSpan.FromSeconds(5)); + string? payload; + try { + payload = await _garnetClient.BRPopAsync(LlmAgentRedisKeys.AgentTaskQueue, TimeSpan.FromSeconds(2)); + } catch (RedisException ex) { + _logger.LogWarning(ex, "Redis error during BRPOP, retrying in 1s"); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + continue; + } + if (string.IsNullOrWhiteSpace(payload)) { continue; } @@ -186,9 +195,17 @@ await _garnetClient.PublishChunkAsync(new AgentStreamChunk { private async Task RunHeartbeatAsync(AgentSessionInfo session, CancellationToken cancellationToken) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(Math.Max(1, Env.AgentHeartbeatIntervalSeconds))); - while (!cancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync(cancellationToken)) { - session.LastHeartbeatUtc = DateTime.UtcNow; - await _rpcClient.SaveSessionAsync(session); + try { + while (!cancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync(cancellationToken)) { + try { + session.LastHeartbeatUtc = DateTime.UtcNow; + await _rpcClient.SaveSessionAsync(session); + } catch (RedisException ex) { + _logger.LogWarning(ex, "Redis error during heartbeat, will retry next tick"); + } + } + } catch (OperationCanceledException) { + // Normal shutdown – heartbeat stops } } diff --git a/TelegramSearchBot/Service/AI/LLM/AgentRegistryService.cs b/TelegramSearchBot/Service/AI/LLM/AgentRegistryService.cs index 3964485b..a62fdb93 100644 --- a/TelegramSearchBot/Service/AI/LLM/AgentRegistryService.cs +++ b/TelegramSearchBot/Service/AI/LLM/AgentRegistryService.cs @@ -183,6 +183,11 @@ public async Task RunMaintenanceOnceAsync(CancellationToken cancellationToken = } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + if (!Env.EnableLLMAgentProcess) { + _logger.LogDebug("LLM agent process mode disabled – AgentRegistryService will not start"); + return; + } + while (!stoppingToken.IsCancellationRequested) { try { await RunMaintenanceOnceAsync(stoppingToken); diff --git a/TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs b/TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs index ed437738..7759e6cc 100644 --- a/TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs +++ b/TelegramSearchBot/Service/AI/LLM/ChunkPollingService.cs @@ -52,9 +52,24 @@ public async Task RunPollCycleAsync(CancellationToken cancellationToken = defaul } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + if (!Env.EnableLLMAgentProcess) { + return; + } + while (!stoppingToken.IsCancellationRequested) { - await RunPollCycleAsync(stoppingToken); - await Task.Delay(Math.Max(50, Env.AgentChunkPollingIntervalMilliseconds), stoppingToken); + try { + await RunPollCycleAsync(stoppingToken); + } catch (OperationCanceledException) { + break; + } catch (RedisException) { + // Transient Redis failure – wait before retrying + } + + try { + await Task.Delay(Math.Max(50, Env.AgentChunkPollingIntervalMilliseconds), stoppingToken); + } catch (OperationCanceledException) { + break; + } } } diff --git a/TelegramSearchBot/Service/AI/LLM/TelegramTaskConsumer.cs b/TelegramSearchBot/Service/AI/LLM/TelegramTaskConsumer.cs index e4a07c92..afcbd03c 100644 --- a/TelegramSearchBot/Service/AI/LLM/TelegramTaskConsumer.cs +++ b/TelegramSearchBot/Service/AI/LLM/TelegramTaskConsumer.cs @@ -29,6 +29,11 @@ public TelegramTaskConsumer( } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + if (!Env.EnableLLMAgentProcess) { + _logger.LogDebug("LLM agent process mode disabled – TelegramTaskConsumer will not start"); + return; + } + while (!stoppingToken.IsCancellationRequested) { try { // Use a 2-second block time so BRPOP returns well within SE.Redis's 5 s async timeout.