Workflow-status: a new WorkflowProgressTracker singleton taps the
suppressed dynamic-workflow subagent events (those carrying
parent_tool_use_id) into per-session telemetry without altering the
existing guard; the snapshot is serialized into the new chains.workflow_info
column on FinalizeChain (ChainId-matched so a panel member of the same
session cannot clear it) and exposed via GET /api/sessions/{id}/workflow-status
(live -> persisted -> none). The header menu gains a 'Workflow-Status' entry
with a modal that live-polls every 2s while running and self-stops on close.
Panel on-demand: Fusion/MoA is now a transient per-request multiModelMode
flag (like plan/ultracode) instead of a persisted session column. Session
settings keep only the panel members + moa_layers; the mode <select> was
removed and the composer dropdown drives the mode per request.
Docs: ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md updated for both
features plus the already-shipped z.ai/GLM provider, GLM 5.2+ effort
passthrough, web-search-prime MCP and the ultracode toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
5.8 KiB
C#
132 lines
5.8 KiB
C#
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Generic;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
|
||
namespace Gott.Services;
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public class WorkflowProgressTracker
|
||
{
|
||
private readonly ConcurrentDictionary<string, WorkflowProgress> _progress
|
||
= new ConcurrentDictionary<string, WorkflowProgress>();
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public void RecordSubagent(string sessionId, string chainId, string parentToolUseId,
|
||
IReadOnlyList<string> 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>Threadsicherer Live-Snapshot – null, wenn für die Session kein Workflow läuft.</summary>
|
||
public WorkflowProgress GetSnapshot(string sessionId)
|
||
=> _progress.TryGetValue(sessionId, out var wp) ? wp.Snapshot() : null;
|
||
|
||
/// <summary>Aktuellen Stand als JSON serialisieren (threadsicher) – null wenn kein Eintrag.</summary>
|
||
public string GetJson(string sessionId)
|
||
=> _progress.TryGetValue(sessionId, out var wp) ? wp.ToJson() : null;
|
||
|
||
/// <summary>ChainId des laufenden Workflow-Eintrags (für die Zuordnung beim Finalize).</summary>
|
||
public string ChainIdFor(string sessionId)
|
||
=> _progress.TryGetValue(sessionId, out var wp) ? wp.ChainId : null;
|
||
|
||
/// <summary>Entfernt den Eintrag (beim Chain-Finalize aufzurufen).</summary>
|
||
public void End(string sessionId) => _progress.TryRemove(sessionId, out _);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public class WorkflowProgress
|
||
{
|
||
[JsonIgnore] public object Sync { get; } = new object();
|
||
// Dedup-Hilfsstruktur (nicht persistiert; SubagentCount trägt den Wert).
|
||
[JsonIgnore] public HashSet<string> SubagentIds { get; set; } = new HashSet<string>();
|
||
|
||
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<string, int> ToolBreakdown { get; set; } = new Dictionary<string, int>();
|
||
public List<string> RecentTools { get; set; } = new List<string>();
|
||
|
||
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<string, int>(ToolBreakdown),
|
||
RecentTools = new List<string>(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());
|
||
}
|