using System.Collections.Concurrent; using ClawdDotNet.App.Models; namespace ClawdDotNet.App.Services; /// /// „Jetzt ausführen" — von Hand angestoßene Läufe, gleich von welcher Oberfläche. /// /// Stand bis zum 2026-09-06 zweimal in den Ansichtsmodellen: einmal für den /// Agenten (AgentsPageViewModel.RunAgentNowAsync), einmal für den geplanten Lauf /// (TasksPageViewModel.RunJobNowAsync). Beide Fassungen taten fast dasselbe und /// unterschieden sich in dem, was sie in den Verlauf schrieben. /// /// Die Unterscheidung, die diese Klasse trägt: ein Tool-Job geht über den /// Scanner, ein Agentenlauf über die Engine. Der Scanner ist der einzige periodische /// Treiber (A1) und kennt die Sperre auf der Aufgabe — ein Tool-Job an ihm vorbei liefe /// womöglich neben dem geplanten Lauf desselben Jobs. /// public sealed class RunCoordinator { private readonly AppHost _host; /// /// Wann ein Lauf zuletzt von Hand angestoßen wurde, je Job-Kennung. Nur im /// Arbeitsspeicher und bewusst nicht in der Instanzdatei — es ist eine Notiz für die /// Anzeige, keine Konfiguration. Beide Oberflächen teilen sie sich, damit ein im /// Browser angestoßener Lauf auch im Fenster als „zuletzt" auftaucht. /// private readonly ConcurrentDictionary _lastRuns = new(); public RunCoordinator(AppHost host) { _host = host; History = new JobHistoryService(host.InstancePath); } /// Der Verlauf von Hand angestoßener Läufe. Fasst sich selbst threadsicher an. public JobHistoryService History { get; } public DateTime? LastRun(string jobId) => _lastRuns.TryGetValue(jobId, out var when) ? when : null; /// /// Weckt einen Agenten von Hand. /// /// Ohne eigene Nachricht wird die des geplanten Laufs genommen — der Handlauf /// soll zeigen, was der geplante täte. /// public async Task RunAgentAsync( string agentId, string? message, CancellationToken ct) { if (_host.Instance.Agents.FirstOrDefault( a => a.AgentId.Equals(agentId, StringComparison.OrdinalIgnoreCase)) is not { } agent) return AdminResult.Failure($"Kein Agent mit der ID '{agentId}'."); if (_host.Engine is null) return AdminResult.Failure("Agent-Engine ist nicht aktiv (kein API-Key)."); var taskMessage = string.IsNullOrWhiteSpace(message) ? agent.Scheduler?.TaskMessage ?? "Führe deine zugewiesenen Aufgaben aus." : message; try { var result = await _host.Engine.RunAsync( agent, taskMessage, _host.Instance.InstanceId, ct); _lastRuns[JobAdministration.WakeupId(agent.AgentId)] = DateTime.Now; History.Add(new JobHistoryEntry { JobName = "Agent Wakeup", Agent = agent.DisplayName, Time = DateTime.Now, JobDescription = taskMessage, Info = $"{result.Status} ({result.StepCount} Steps, {result.TokensUsed:N0} Tokens)", Status = "Manual" }); return AdminResult.Success( $"'{agent.DisplayName}' abgeschlossen: {result.Status} " + $"({result.StepCount} Schritte, {result.TokensUsed:N0} Tokens)."); } catch (OperationCanceledException) { return AdminResult.Failure($"Lauf von '{agent.DisplayName}' abgebrochen."); } catch (Exception ex) { return AdminResult.Failure($"Lauf fehlgeschlagen: {ex.Message}"); } } /// /// Stößt einen Tool-Job von Hand an — über den Scanner, nicht an ihm vorbei. /// /// „Nicht bereit" ist keine Störung: Der Scanner gibt false zurück, wenn /// die Aufgabe gerade von jemand anderem gehalten wird oder gar nicht existiert. /// Genau dafür ist die Sperre da. /// public async Task RunToolJobAsync( string agentId, string jobId, CancellationToken ct) { if (_host.Instance.Agents.FirstOrDefault( a => a.AgentId.Equals(agentId, StringComparison.OrdinalIgnoreCase)) is not { } agent) return AdminResult.Failure($"Kein Agent mit der ID '{agentId}'."); if (agent.ToolJobs.FirstOrDefault(j => j.JobId == jobId) is not { } job) return AdminResult.Failure($"Kein Tool-Job mit der Kennung '{jobId}'."); if (_host.Scanner is null) return AdminResult.Failure("Scanner ist nicht aktiv."); try { var ran = await _host.Scanner.RunTaskNowAsync($"tj-{agent.AgentId}-{job.JobId}", ct); var info = ran ? "Ausgeführt" : "Nicht bereit"; _lastRuns[job.JobId] = DateTime.Now; History.Add(new JobHistoryEntry { JobName = job.JobTypeId, Agent = agent.DisplayName, Time = DateTime.Now, JobDescription = $"Tool Job: {job.ToolName}", Info = info, Status = "Manual" }); return AdminResult.Success($"Tool-Job '{job.JobTypeId}' abgeschlossen ({info})."); } catch (OperationCanceledException) { return AdminResult.Failure($"Tool-Job '{job.JobTypeId}' abgebrochen."); } catch (Exception ex) { return AdminResult.Failure($"Fehler beim Ausführen: {ex.Message}"); } } /// Stößt an, was hinter der Zeile steckt — Wakeup oder Tool-Job. public Task RunAsync(JobSummary job, CancellationToken ct) => job.Kind == JobKind.ToolJob ? RunToolJobAsync(job.AgentId, job.JobId, ct) : RunAgentAsync(job.AgentId, job.Message, ct); }