using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; namespace Gott.Services; /// /// Sammelt Live-Telemetrie laufender dynamischer Workflows ("ultracode"): die Aktivität der /// Subagenten, die die Claude CLI über das Workflow-/Task-Tool spawnt. Diese Subagent-Events /// werden im Haupt-Stream bewusst unterdrückt (parent_tool_use_id-Guard in ClaudeCliService) – /// hier werden sie abgegriffen, damit der Workflow-Fortschritt LIVE (während des Laufs) und /// danach (persistiert in chains.workflow_info) abrufbar ist. In-Memory pro Session, analog /// StreamingTracker. Telemetrie darf den Stream nie blockieren → alles defensiv + lock-leicht. /// public class WorkflowProgressTracker { private readonly ConcurrentDictionary _progress = new ConcurrentDictionary(); /// /// Verbucht ein Subagent-Event (Tool-Aufrufe + Token-Nutzung eines Subagenten). /// Lazy: legt den Eintrag bei der ERSTEN Subagent-Aktivität einer Session an, d.h. nur /// echte Workflow-Läufe (mit gespawnten Subagenten) erzeugen überhaupt einen Eintrag. /// public void RecordSubagent(string sessionId, string chainId, string parentToolUseId, IReadOnlyList toolNames, long inputTokens, long outputTokens) { if (string.IsNullOrEmpty(sessionId)) return; var wp = _progress.GetOrAdd(sessionId, _ => new WorkflowProgress { ChainId = chainId, StartedAt = DateTime.UtcNow }); lock (wp.Sync) { // Stale-Eintrag eines früheren, nicht sauber finalisierten Laufs (gleiche Session, // andere Chain) → atomar für die neue Chain zurücksetzen, statt fälschlich zu mischen. if (wp.ChainId != chainId) { wp.ChainId = chainId; wp.StartedAt = DateTime.UtcNow; wp.SubagentIds.Clear(); wp.SubagentCount = 0; wp.SubagentToolCalls = 0; wp.SubagentInputTokens = 0; wp.SubagentOutputTokens = 0; wp.ToolBreakdown.Clear(); wp.RecentTools.Clear(); } if (!string.IsNullOrEmpty(parentToolUseId) && wp.SubagentIds.Add(parentToolUseId)) wp.SubagentCount = wp.SubagentIds.Count; if (toolNames != null) { foreach (var tn in toolNames) { if (string.IsNullOrEmpty(tn)) continue; wp.SubagentToolCalls++; wp.ToolBreakdown[tn] = wp.ToolBreakdown.TryGetValue(tn, out var c) ? c + 1 : 1; wp.RecentTools.Add(tn); if (wp.RecentTools.Count > 12) wp.RecentTools.RemoveAt(0); } } wp.SubagentInputTokens += inputTokens; wp.SubagentOutputTokens += outputTokens; wp.LastActivityAt = DateTime.UtcNow; } } /// Threadsicherer Live-Snapshot – null, wenn für die Session kein Workflow läuft. public WorkflowProgress GetSnapshot(string sessionId) => _progress.TryGetValue(sessionId, out var wp) ? wp.Snapshot() : null; /// Aktuellen Stand als JSON serialisieren (threadsicher) – null wenn kein Eintrag. public string GetJson(string sessionId) => _progress.TryGetValue(sessionId, out var wp) ? wp.ToJson() : null; /// ChainId des laufenden Workflow-Eintrags (für die Zuordnung beim Finalize). public string ChainIdFor(string sessionId) => _progress.TryGetValue(sessionId, out var wp) ? wp.ChainId : null; /// Entfernt den Eintrag (beim Chain-Finalize aufzurufen). public void End(string sessionId) => _progress.TryRemove(sessionId, out _); } /// /// Telemetrie eines dynamischen Workflows: Subagent-Anzahl, Tool-Aufrufe (+ Aufschlüsselung) /// und aggregierte Subagent-Tokens. Wird live geführt und beim Chain-Finalize als JSON in /// chains.workflow_info persistiert. /// public class WorkflowProgress { [JsonIgnore] public object Sync { get; } = new object(); // Dedup-Hilfsstruktur (nicht persistiert; SubagentCount trägt den Wert). [JsonIgnore] public HashSet SubagentIds { get; set; } = new HashSet(); public string ChainId { get; set; } public DateTime StartedAt { get; set; } public DateTime LastActivityAt { get; set; } public int SubagentCount { get; set; } public int SubagentToolCalls { get; set; } public long SubagentInputTokens { get; set; } public long SubagentOutputTokens { get; set; } public Dictionary ToolBreakdown { get; set; } = new Dictionary(); public List RecentTools { get; set; } = new List(); public WorkflowProgress Snapshot() { lock (Sync) { return new WorkflowProgress { ChainId = ChainId, StartedAt = StartedAt, LastActivityAt = LastActivityAt, SubagentCount = SubagentCount, SubagentToolCalls = SubagentToolCalls, SubagentInputTokens = SubagentInputTokens, SubagentOutputTokens = SubagentOutputTokens, ToolBreakdown = new Dictionary(ToolBreakdown), RecentTools = new List(RecentTools) }; } } // Serialisiert einen entkoppelten Snapshot (Snapshot() kopiert unter Lock) – so läuft die // Serialisierung außerhalb jeder kritischen Sektion und kann nie auf eine live mutierte // Collection treffen. public string ToJson() => JsonSerializer.Serialize(Snapshot()); }