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>
171 lines
5.8 KiB
C#
171 lines
5.8 KiB
C#
using System.Collections.Generic;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
|
||
namespace Gott.Models;
|
||
|
||
public class ChatMessage
|
||
{
|
||
[JsonPropertyName("role")]
|
||
public string Role { get; set; } = "";
|
||
|
||
[JsonPropertyName("content")]
|
||
public JsonElement Content { get; set; }
|
||
|
||
/// <summary>
|
||
/// Extrahiert den reinen Text aus Content (egal ob string oder content-block array).
|
||
/// </summary>
|
||
public string GetTextContent()
|
||
{
|
||
if (Content.ValueKind == JsonValueKind.String)
|
||
return Content.GetString() ?? "";
|
||
|
||
if (Content.ValueKind == JsonValueKind.Array)
|
||
{
|
||
var parts = new List<string>();
|
||
foreach (var block in Content.EnumerateArray())
|
||
{
|
||
if (block.TryGetProperty("type", out var t) && t.GetString() == "text"
|
||
&& block.TryGetProperty("text", out var txt))
|
||
parts.Add(txt.GetString() ?? "");
|
||
}
|
||
return string.Join("\n", parts);
|
||
}
|
||
|
||
return Content.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Prüft ob Datei-Attachments (image/document) enthalten sind.
|
||
/// </summary>
|
||
public bool HasAttachments()
|
||
{
|
||
if (Content.ValueKind != JsonValueKind.Array) return false;
|
||
foreach (var block in Content.EnumerateArray())
|
||
{
|
||
if (block.TryGetProperty("type", out var t))
|
||
{
|
||
var type = t.GetString();
|
||
if (type == "image" || type == "document") return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gibt Attachment-Infos zurück (type + media_type).
|
||
/// </summary>
|
||
public List<AttachmentInfo> GetAttachments()
|
||
{
|
||
var result = new List<AttachmentInfo>();
|
||
if (Content.ValueKind != JsonValueKind.Array) return result;
|
||
foreach (var block in Content.EnumerateArray())
|
||
{
|
||
if (block.TryGetProperty("type", out var t))
|
||
{
|
||
var type = t.GetString();
|
||
if (type == "image" || type == "document")
|
||
{
|
||
var mediaType = "";
|
||
if (block.TryGetProperty("source", out var src) && src.TryGetProperty("media_type", out var mt))
|
||
mediaType = mt.GetString() ?? "";
|
||
result.Add(new AttachmentInfo { Type = type, MediaType = mediaType });
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gibt vollständige Attachment-Blöcke zurück (inkl. Base64-Daten).
|
||
/// </summary>
|
||
public List<AttachmentBlock> GetAttachmentBlocks()
|
||
{
|
||
var result = new List<AttachmentBlock>();
|
||
if (Content.ValueKind != JsonValueKind.Array) return result;
|
||
foreach (var block in Content.EnumerateArray())
|
||
{
|
||
if (block.TryGetProperty("type", out var t))
|
||
{
|
||
var type = t.GetString();
|
||
if (type == "image" || type == "document")
|
||
{
|
||
var mediaType = "";
|
||
var data = "";
|
||
var fileName = "";
|
||
if (block.TryGetProperty("source", out var src))
|
||
{
|
||
if (src.TryGetProperty("media_type", out var mt))
|
||
mediaType = mt.GetString() ?? "";
|
||
if (src.TryGetProperty("data", out var d))
|
||
data = d.GetString() ?? "";
|
||
}
|
||
if (block.TryGetProperty("_file_name", out var fn))
|
||
fileName = fn.GetString() ?? "";
|
||
result.Add(new AttachmentBlock { Type = type, MediaType = mediaType, Data = data, FileName = fileName });
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
|
||
public class AttachmentBlock
|
||
{
|
||
public string Type { get; set; } = "";
|
||
public string MediaType { get; set; } = "";
|
||
public string Data { get; set; } = "";
|
||
public string FileName { get; set; } = "";
|
||
}
|
||
|
||
public class AttachmentInfo
|
||
{
|
||
public string Type { get; set; } = "";
|
||
public string MediaType { get; set; } = "";
|
||
}
|
||
|
||
public class ChatRequest
|
||
{
|
||
[JsonPropertyName("content")]
|
||
public JsonElement Content { get; set; }
|
||
|
||
[JsonPropertyName("sessionId")]
|
||
public string SessionId { get; set; } = "";
|
||
|
||
/// <summary>
|
||
/// Wenn gesetzt: Dies ist eine Antwort auf ein ask_user_choice Tool.
|
||
/// Enthält die tool_use_id des offenen ask_user_choice-Calls.
|
||
/// </summary>
|
||
[JsonPropertyName("choiceToolCallId")]
|
||
public string ChoiceToolCallId { get; set; }
|
||
|
||
/// <summary>
|
||
/// Der gewählte Text aus den Auswahloptionen.
|
||
/// </summary>
|
||
[JsonPropertyName("choiceResult")]
|
||
public string ChoiceResult { get; set; }
|
||
|
||
/// <summary>
|
||
/// Wenn true: Plan-Modus aktiv. Nur Read-only Tools, keine Datei-Änderungen.
|
||
/// </summary>
|
||
[JsonPropertyName("planMode")]
|
||
public bool PlanMode { get; set; }
|
||
|
||
/// <summary>
|
||
/// Wenn true: Ultracode aktiv. Die Anfrage wird als nativer dynamischer Workflow
|
||
/// (mehrere Subagenten) gefahren – das Workflow-Tool wird freigeschaltet, Effort = xhigh.
|
||
/// Nur für den Claude-CLI-Pfad relevant (z.ai/GLM und der API-Pfad haben kein Workflow-Runtime).
|
||
/// </summary>
|
||
[JsonPropertyName("ultracode")]
|
||
public bool Ultracode { get; set; }
|
||
|
||
/// <summary>
|
||
/// On-demand Panel-Modus für genau diese Anfrage (transient, NICHT persistent):
|
||
/// "" / "off" / "fusion" / "moa". Wie Plan-/Ultracode-Modus pro Request gewählt.
|
||
/// Die Panel-Mitglieder kommen weiterhin aus der persistierten Session (PanelAgentIds),
|
||
/// nur der Modus (ob/wie das Panel läuft) wird hier pro Anfrage gesetzt.
|
||
/// </summary>
|
||
[JsonPropertyName("multiModelMode")]
|
||
public string MultiModelMode { get; set; } = "";
|
||
}
|