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);
}