using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Engine; using Microsoft.Extensions.Logging; namespace ClawdDotNet.Core.Scheduling; public sealed class AgentScheduler : IAsyncDisposable { private readonly AgentEngine _engine; private readonly string _instanceId; private readonly ILogger _logger; private readonly CancellationTokenSource _cts = new(); private readonly List _schedulerTasks = new(); private readonly Dictionary _lastResults = new(); private readonly Lock _resultsLock = new(); public event Action? OnRunCompleted; public AgentScheduler(AgentEngine engine, string instanceId, ILoggerFactory loggerFactory) { _engine = engine; _instanceId = instanceId; _logger = loggerFactory.CreateLogger("ClawdDotNet.Core.Scheduling"); } public void RegisterAgent(AgentConfig agentConfig) { if (agentConfig.Scheduler is null) return; _logger.LogInformation("Registering scheduled agent: {AgentId}, cron='{Cron}', runOnStart={RunOnStart}", agentConfig.AgentId, agentConfig.Scheduler.Cron, agentConfig.Scheduler.RunOnStart); var task = RunScheduledAgentAsync(agentConfig, _cts.Token); _schedulerTasks.Add(task); } public void RegisterAll(IEnumerable agents) { foreach (var agent in agents) RegisterAgent(agent); } public async Task RunNowAsync(AgentConfig agentConfig, string userMessage, CancellationToken ct) { _logger.LogInformation("Manual run triggered: {AgentId}", agentConfig.AgentId); var result = await _engine.RunAsync(agentConfig, userMessage, _instanceId, ct); StoreResult(agentConfig.AgentId, result); OnRunCompleted?.Invoke(agentConfig.AgentId, result); return result; } public AgentRunResult? GetLastResult(string agentId) { lock (_resultsLock) return _lastResults.GetValueOrDefault(agentId); } private async Task RunScheduledAgentAsync(AgentConfig agentConfig, CancellationToken ct) { var scheduler = agentConfig.Scheduler!; if (scheduler.RunOnStart) { await ExecuteScheduledRunAsync(agentConfig, ct); } if (string.IsNullOrWhiteSpace(scheduler.Cron)) return; var cron = CronExpression.Parse(scheduler.Cron); while (!ct.IsCancellationRequested) { var now = DateTime.Now; var next = cron.GetNextOccurrence(now); if (next is null) { _logger.LogWarning("No next occurrence found for agent {AgentId}", agentConfig.AgentId); return; } var delay = next.Value - now; _logger.LogDebug("Agent {AgentId} next run at {NextRun}", agentConfig.AgentId, next.Value); try { await Task.Delay(delay, ct); } catch (OperationCanceledException) { break; } await ExecuteScheduledRunAsync(agentConfig, ct); } } private async Task ExecuteScheduledRunAsync(AgentConfig agentConfig, CancellationToken ct) { try { var result = await _engine.RunAsync( agentConfig, agentConfig.Scheduler?.TaskMessage ?? "Führe deine zugewiesenen Aufgaben aus.", _instanceId, ct); StoreResult(agentConfig.AgentId, result); OnRunCompleted?.Invoke(agentConfig.AgentId, result); _logger.LogInformation( "Scheduled run completed: {AgentId}, status={Status}, tokens={Tokens}", agentConfig.AgentId, result.Status, result.TokensUsed); } catch (Exception ex) when (ex is not OperationCanceledException) { _logger.LogError(ex, "Scheduled run failed for {AgentId}", agentConfig.AgentId); } } private void StoreResult(string agentId, AgentRunResult result) { lock (_resultsLock) _lastResults[agentId] = result; } public async ValueTask DisposeAsync() { await _cts.CancelAsync(); try { await Task.WhenAll(_schedulerTasks); } catch (OperationCanceledException) { } _cts.Dispose(); } }