diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 6ae6db9..6c9255a 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -85,7 +85,7 @@ public static class ChatEndpoint try { - await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: ctx.ToolChoice, review: ctx.Review, cancellationToken: linkedCts.Token); + await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: ctx.ToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, cancellationToken: linkedCts.Token); } finally { diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index a4cb1bd..a10f55e 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -81,8 +81,9 @@ public static class SessionEndpoint var avatarOffsetY = body.TryGetProperty("avatarOffsetY", out var aoy) ? aoy.GetDouble() : 0; var reviewAgentId = body.TryGetProperty("reviewAgentId", out var ra) ? ra.GetString() : null; var maxReviewIterations = body.TryGetProperty("maxReviewIterations", out var mri) ? mri.GetInt32() : 3; + var finalizerAgentId = body.TryGetProperty("finalizerAgentId", out var fa) ? fa.GetString() : null; - var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations); + var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId); await ctx.Response.WriteAsJsonAsync(session); } diff --git a/Models/ChatContext.cs b/Models/ChatContext.cs index dc87865..2372a33 100644 --- a/Models/ChatContext.cs +++ b/Models/ChatContext.cs @@ -20,6 +20,21 @@ public class ChatContext // Review-Agent public ReviewContext Review { get; set; } + + // Finalizer-Agent + public FinalizerContext Finalizer { get; set; } +} + +public class FinalizerContext +{ + public string ModelId { get; set; } + public List SystemPrompt { get; set; } + public string AgentId { get; set; } + public string Workspace { get; set; } + public bool UseMemory { get; set; } + public string WebSearchVersion { get; set; } + public string ToolChoice { get; set; } + public AgentRecord Agent { get; set; } } public class ReviewContext diff --git a/Models/SessionRecord.cs b/Models/SessionRecord.cs index 7ec208b..81dc277 100644 --- a/Models/SessionRecord.cs +++ b/Models/SessionRecord.cs @@ -22,6 +22,7 @@ public class SessionRecord public double AvatarOffsetY { get; set; } public string ReviewAgentId { get; set; } public int MaxReviewIterations { get; set; } = 3; + public string FinalizerAgentId { get; set; } public string CreatedAt { get; set; } public string UpdatedAt { get; set; } } diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 92364df..11ab319 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -48,6 +48,7 @@ public class AnthropicService int maxIterations = 100, string toolChoice = "any", ReviewContext review = null, + FinalizerContext finalizer = null, CancellationToken cancellationToken = default) { var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -202,6 +203,13 @@ public class AnthropicService } } + // ── Finalizer-Agent Phase ── + if (finalizer != null && stopReason == "end_turn") + { + var finalizerIterations = await RunFinalizer(response, finalizer, messages, chainId, workspace, sessionId, usage, iterations, cancellationToken); + iterations += finalizerIterations; + } + var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason; await FinalizeChain(chain, usage, iterations, finalStopReason, sw, streamErrorInfo); await TryWriteSse(response, "done", new @@ -311,9 +319,12 @@ public class AnthropicService var reviewMessages = BuildReviewMessages(workerMessages); // Review-Agent iteriert mit eigenen Tools - var (reviewText, reviewIterations) = await RunReviewAgentChat( - response, review, reviewMessages, chainId, workspace, sessionId, - usage, baseIterations, cancellationToken); + var (reviewText, reviewIterations) = await RunAgentChat( + response, review.ModelId, review.SystemPrompt, review.AgentId, + workspace, review.UseMemory, review.WebSearchVersion, + review.ToolChoice, reviewMessages, chainId, sessionId, usage, + baseIterations, "review", review.Agent?.Name ?? "Review-Agent", + cancellationToken); var result = new ReviewResult { ReviewIterationCount = reviewIterations }; @@ -344,6 +355,47 @@ public class AnthropicService return result; } + // ── Finalizer-Agent Logik ── + + /// + /// Führt den Finalizer-Agent aus. Wird einmal nach der Review-Phase (oder direkt nach dem Worker) aufgerufen. + /// Der Finalizer kann Tools nutzen, gibt aber kein Feedback an den Worker zurück. + /// + private async Task RunFinalizer( + HttpResponse response, + FinalizerContext finalizer, + List workerMessages, + string chainId, + string workspace, + string sessionId, + TokenUsage usage, + int baseIterations, + CancellationToken cancellationToken) + { + await TryWriteSse(response, "finalizer_start", new + { + agentId = finalizer.AgentId, + agentName = finalizer.Agent?.Name ?? "Finalizer-Agent" + }, cancellationToken); + + // Finalizer bekommt die Konversation als Kontext (gleiche Methode wie Review) + var finalizerMessages = BuildReviewMessages(workerMessages); + + var (text, iterations) = await RunAgentChat( + response, finalizer.ModelId, finalizer.SystemPrompt, finalizer.AgentId, + finalizer.Workspace, finalizer.UseMemory, finalizer.WebSearchVersion, + finalizer.ToolChoice, finalizerMessages, chainId, sessionId, usage, + baseIterations, "finalizer", finalizer.Agent?.Name ?? "Finalizer-Agent", + cancellationToken); + + await TryWriteSse(response, "finalizer_done", new + { + agentName = finalizer.Agent?.Name ?? "Finalizer-Agent" + }, cancellationToken); + + return iterations; + } + /// /// Baut die Messages für den Review-Agent auf. /// Der Review-Agent bekommt die komplette Worker-Konversation als eine User-Message. @@ -411,23 +463,32 @@ public class AnthropicService } /// - /// Führt den Review-Agent Chat aus (non-streaming für den Agent, aber SSE-Events für den Client). - /// Gibt den finalen Text und die Anzahl der Iterationen zurück. + /// Generischer Agent-Chat (non-streaming für den Agent, aber SSE-Events für den Client). + /// Wird für Review-Agent und Finalizer-Agent verwendet. + /// agentType steuert SSE-Event-Prefixe und DB-Message-Tags ("review" oder "finalizer"). /// - private async Task<(string text, int iterations)> RunReviewAgentChat( + private async Task<(string text, int iterations)> RunAgentChat( HttpResponse response, - ReviewContext review, + string modelId, + List systemPrompt, + string agentId, + string agentWorkspace, + bool useMemory, + string webSearchVersion, + string agentToolChoice, List messages, string chainId, - string workspace, string sessionId, TokenUsage usage, int baseIterations, + string agentType, + string agentName, CancellationToken cancellationToken) { int iterations = 0; var finalText = new StringBuilder(); - int maxToolIterations = 20; // Review-Agent bekommt max 20 Tool-Iterationen + int maxToolIterations = 20; + var tag = agentType == "finalizer" ? "Finalizer" : "Review"; while (iterations < maxToolIterations) { @@ -436,19 +497,19 @@ public class AnthropicService var payload = new Dictionary { - ["model"] = review.ModelId, + ["model"] = modelId, ["max_tokens"] = 8192, ["messages"] = messages, ["stream"] = false }; - if (review.SystemPrompt != null && review.SystemPrompt.Count > 0) - payload["system"] = review.SystemPrompt; + if (systemPrompt != null && systemPrompt.Count > 0) + payload["system"] = systemPrompt; - var hasWorkspace = !string.IsNullOrEmpty(workspace); - var includeMemory = review.UseMemory && _memoryService != null && !string.IsNullOrEmpty(review.AgentId); + var hasWorkspace = !string.IsNullOrEmpty(agentWorkspace); + var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId); var hasTools = hasWorkspace || includeMemory; - var hasWebSearch = !string.IsNullOrEmpty(review.WebSearchVersion); + var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion); if (hasTools || hasWebSearch) { @@ -456,9 +517,9 @@ public class AnthropicService if (hasTools) tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory)); if (hasWebSearch) - tools.Add(new { type = review.WebSearchVersion, name = "web_search", max_uses = 5 }); + tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 }); payload["tools"] = tools; - var effectiveChoice = (review.ToolChoice == "any" && iterations == 1) ? "any" : "auto"; + var effectiveChoice = (agentToolChoice == "any" && iterations == 1) ? "any" : "auto"; payload["tool_choice"] = new { type = effectiveChoice }; } @@ -475,12 +536,11 @@ public class AnthropicService if (!httpResp.IsSuccessStatusCode) { - return ($"Review-Agent API Fehler: {(int)httpResp.StatusCode}: {respBody}", iterations); + return ($"{tag}-Agent API Fehler: {(int)httpResp.StatusCode}: {respBody}", iterations); } var resp = JsonSerializer.Deserialize(respBody); - // Usage tracken (auf den gemeinsamen Zähler) if (resp.TryGetProperty("usage", out var u)) { if (u.TryGetProperty("input_tokens", out var it)) usage.InputTokens += it.GetInt64(); @@ -505,22 +565,20 @@ public class AnthropicService assistantContent.Add(new { type = "text", text }); finalText.Append(text); - // Review-Text per SSE an den Client streamen - await TryWriteSse(response, "review_text", new + await TryWriteSse(response, $"{agentType}_text", new { text, - agentName = review.Agent?.Name ?? "Review-Agent", - agentId = review.AgentId + agentName, + agentId }, cancellationToken); - // In DB speichern (als Review-Message markiert) await _db.SaveMessage(new MessageRecord { ChainId = chainId, Iteration = baseIterations + iterations, Role = "assistant", Type = "text", - Content = $"[Review: {review.Agent?.Name ?? "Review-Agent"}] {text}" + Content = $"[{tag}: {agentName}] {text}" }); } else if (blockType == "tool_use") @@ -531,13 +589,12 @@ public class AnthropicService assistantContent.Add(new { type = "tool_use", id = toolId, name = toolName, input = toolInput }); - // Tool-Use per SSE - await TryWriteSse(response, "review_tool_use", new + await TryWriteSse(response, $"{agentType}_tool_use", new { toolName, toolId, input = toolInput.ToString(), - agentName = review.Agent?.Name ?? "Review-Agent" + agentName }, cancellationToken); await _db.SaveMessage(new MessageRecord @@ -553,7 +610,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, workspace, _memoryService, review.AgentId, cancellationToken); } + try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, cancellationToken); } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } const int MaxToolResultChars = 20_000; diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 3c1428d..c20f3e8 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -142,6 +142,41 @@ public class ChatService } } + // Finalizer-Agent vorbereiten (falls konfiguriert) + FinalizerContext finalizerContext = null; + if (!string.IsNullOrEmpty(session.FinalizerAgentId)) + { + var finalizerAgent = await _db.GetAgent(session.FinalizerAgentId); + if (finalizerAgent != null) + { + var finalizerModelId = ResolveModelId(finalizerAgent.Model); + var finalizerMarked = await _db.GetMarkedMessages(sessionId); + var finalizerMemDir = _memoryService.GetAgentMemoryDir(finalizerAgent.AgentId); + var finalizerSkills = await _db.GetAgentSkills(finalizerAgent.AgentId); + var finalizerWorkspace = workspace; + var finalizerSystemText = SystemPromptBuilder.Build( + finalizerAgent, session, finalizerMarked, _memoryService, finalizerMemDir, + finalizerSkills, _skillService, userPersona: user?.GetEffectivePersona(), + resolvedWorkspace: finalizerWorkspace); + + var finalizerPrompt = new List(); + if (!string.IsNullOrWhiteSpace(finalizerSystemText)) + finalizerPrompt.Add(new { type = "text", text = finalizerSystemText }); + + finalizerContext = new FinalizerContext + { + ModelId = finalizerModelId, + SystemPrompt = finalizerPrompt, + AgentId = finalizerAgent.AgentId, + Workspace = finalizerWorkspace, + UseMemory = finalizerAgent.UseMemory, + WebSearchVersion = finalizerAgent.WebSearchVersion, + ToolChoice = finalizerAgent.ToolChoice ?? "any", + Agent = finalizerAgent + }; + } + } + return new ChatContext { ChainId = chainId, @@ -157,7 +192,8 @@ public class ChatService MaxIterations = maxIterations, Agent = agent, Session = session, - Review = reviewContext + Review = reviewContext, + Finalizer = finalizerContext }; } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index ace0136..8290ebc 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -177,6 +177,7 @@ public class DatabaseService TryAddColumn(conn, "sessions", "avatar_offset_y", "REAL NOT NULL DEFAULT 0"); TryAddColumn(conn, "agents", "avatar_offset_x", "REAL NOT NULL DEFAULT 0"); TryAddColumn(conn, "agents", "avatar_offset_y", "REAL NOT NULL DEFAULT 0"); + TryAddColumn(conn, "sessions", "finalizer_agent_id", "TEXT"); conn.Execute(@" CREATE TABLE IF NOT EXISTS agent_skills ( @@ -373,7 +374,7 @@ public class DatabaseService // ── Sessions ── - public async Task CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3) + public async Task CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3, string finalizerAgentId = null) { using var conn = CreateConnection(); var now = DateTime.UtcNow.ToString("o"); @@ -393,12 +394,13 @@ public class DatabaseService AvatarOffsetY = avatarOffsetY, ReviewAgentId = reviewAgentId, MaxReviewIterations = maxReviewIterations, + FinalizerAgentId = finalizerAgentId, CreatedAt = now, UpdatedAt = now }; await conn.ExecuteAsync(@" - INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, created_at, updated_at) - VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @CreatedAt, @UpdatedAt) + INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, finalizer_agent_id, created_at, updated_at) + VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @CreatedAt, @UpdatedAt) ", session); return session; } @@ -446,6 +448,7 @@ public class DatabaseService s.AvatarOffsetY, s.ReviewAgentId, s.MaxReviewIterations, + s.FinalizerAgentId, s.IsDeleted, s.CreatedAt, s.UpdatedAt, @@ -500,6 +503,7 @@ public class DatabaseService if (body.TryGetProperty("avatarOffsetY", out var aoy)) { sets.Add("avatar_offset_y = @avatarOffsetY"); p.Add("avatarOffsetY", aoy.GetDouble()); } if (body.TryGetProperty("reviewAgentId", out var ra)) { sets.Add("review_agent_id = @reviewAgentId"); p.Add("reviewAgentId", ra.ValueKind == System.Text.Json.JsonValueKind.Null ? null : ra.GetString()); } if (body.TryGetProperty("maxReviewIterations", out var mri)) { sets.Add("max_review_iterations = @maxReviewIterations"); p.Add("maxReviewIterations", mri.GetInt32()); } + if (body.TryGetProperty("finalizerAgentId", out var fa)) { sets.Add("finalizer_agent_id = @finalizerAgentId"); p.Add("finalizerAgentId", fa.ValueKind == System.Text.Json.JsonValueKind.Null ? null : fa.GetString()); } await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p); } diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 39bf6af..93d6534 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -992,6 +992,18 @@ margin-right: .3em; } +.finalizer-inline-header { + color: var(--primary); +} + +.finalizer-bubble .review-header { + color: var(--primary); +} + +.finalizer-bubble { + border-color: var(--primary); +} + .review-inline-feedback { color: var(--warning); margin-bottom: .4em; diff --git a/wwwroot/index.html b/wwwroot/index.html index b1fd379..e3d137a 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -130,6 +130,11 @@ + +
+
Kein Finalizer-Agent
+
+
diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index 2b146f3..19c8767 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -228,6 +228,23 @@ async function sendMessage() { finalizeReviewBubble('approved', data); break; + case 'finalizer_start': + endStreamBubble(); + addReviewBubble('start', data, 'finalizer'); + break; + + case 'finalizer_text': + appendToReviewText(data.text); + break; + + case 'finalizer_tool_use': + addReviewToolUse(data); + break; + + case 'finalizer_done': + finalizeReviewBubble('done', data, 'finalizer'); + break; + case 'error': endStreamBubble(); addErrorBubble(data.error, true); diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index 3ff1690..f518b04 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -157,8 +157,8 @@ function _mergeConsecutiveAssistantTexts(msgs) { for (const msg of msgs) { if (msg.type === 'text' && msg.role === 'assistant') { const prev = result[result.length - 1]; - const isReview = msg.content && msg.content.startsWith('[Review:'); - const prevIsReview = prev && prev.content && prev.content.startsWith('[Review:'); + const isReview = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:')); + const prevIsReview = prev && prev.content && (prev.content.startsWith('[Review:') || prev.content.startsWith('[Finalizer:')); if (prev && prev.type === 'text' && prev.role === 'assistant' && prev.iteration === msg.iteration && !isReview && !prevIsReview) { // Gleiche Iteration: nahtlos zusammenfügen (Web-Search-Fragmente etc.) @@ -181,7 +181,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) { const div = document.createElement('div'); switch (msg.type) { case 'text': { - const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback')); + const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback') || msg.content.startsWith('[Finalizer:')); if (msg.role === 'user' && !isReviewMsg) { div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : ''); if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden'); @@ -290,7 +290,7 @@ function renderMessages(msgs, addToConversation) { } } } else if (msg.role === 'assistant') { - if (msg.content && msg.content.startsWith('[Review:')) { + if (msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:'))) { addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); } else { addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn); diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index 87ef22d..f6d2495 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -25,6 +25,8 @@ registerPage('session', { const reviewAgentSelectCard = document.getElementById('reviewAgentSelectCard'); const reviewIterationsRow = document.getElementById('reviewIterationsRow'); const maxReviewIterationsEl = document.getElementById('maxReviewIterations'); + const finalizerAgentSelect = document.getElementById('finalizerAgentSelect'); + const finalizerAgentSelectCard = document.getElementById('finalizerAgentSelectCard'); const imagePreview = document.getElementById('sessionImagePreview'); const saveBtn = document.getElementById('sessionSaveBtn'); const cancelBtn = document.getElementById('sessionCancelBtn'); @@ -123,6 +125,14 @@ registerPage('session', { } updateReviewVisibility(); + // Initiale Anzeige – Finalizer-Agent + if (session && session.finalizerAgentId) { + finalizerAgentSelect.value = session.finalizerAgentId; + renderAgentInCard(finalizerAgentSelectCard, agentMap[session.finalizerAgentId] || null, 'fa-flag-checkered', 'Kein Finalizer-Agent'); + } else { + renderAgentInCard(finalizerAgentSelectCard, null, 'fa-flag-checkered', 'Kein Finalizer-Agent'); + } + // Klick auf Agent-Card → Modal öffnen agentSelectCard.addEventListener('click', () => { openAgentPickerModal(agents, agentSkillsMap, agentSelect.value, (selectedId) => { @@ -140,6 +150,14 @@ registerPage('session', { }); }); + // Klick auf Finalizer-Agent-Card → Modal öffnen + finalizerAgentSelectCard.addEventListener('click', () => { + openAgentPickerModal(agents, agentSkillsMap, finalizerAgentSelect.value, (selectedId) => { + finalizerAgentSelect.value = selectedId || ''; + renderAgentInCard(finalizerAgentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-flag-checkered', 'Kein Finalizer-Agent'); + }); + }); + more.addEventListener('click', () => { const content = more.querySelector('[hidden]'); while (content.firstChild) @@ -189,6 +207,7 @@ registerPage('session', { agentId: agentSelect.value || null, reviewAgentId: reviewAgentSelect.value || null, maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3, + finalizerAgentId: finalizerAgentSelect.value || null, context: _sessionContextMde.value().trim() || null, workspace: workspaceEl.value.trim() || null, customCommands: commands, diff --git a/wwwroot/js/pages/shared-page.js b/wwwroot/js/pages/shared-page.js index 3704dcb..9e2fc15 100644 --- a/wwwroot/js/pages/shared-page.js +++ b/wwwroot/js/pages/shared-page.js @@ -26,10 +26,10 @@ registerPage('shared', { for (const msg of messages) { const c = msg.content || ''; - const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback'); + const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback') || c.startsWith('[Finalizer:'); const div = document.createElement('div'); if (isReview) { - div.className = 'msg review-bubble markdown'; + div.className = 'msg review-bubble markdown' + (c.startsWith('[Finalizer:') ? ' finalizer-bubble' : ''); } else { div.className = 'msg ' + (msg.role === 'user' ? 'user' : 'assistant') + ' markdown'; } @@ -63,11 +63,12 @@ function _mergeConsecutiveTexts(msgs) { const result = []; for (const msg of msgs) { const c = msg.content || ''; - const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback'); + const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback') || c.startsWith('[Finalizer:'); if (msg.role === 'assistant' && !isReview) { const prev = result[result.length - 1]; if (prev && prev.role === 'assistant' && !(prev.content || '').startsWith('[Review:') + && !(prev.content || '').startsWith('[Finalizer:') && prev.iteration === msg.iteration) { prev.content = (prev.content || '') + ' ' + c; if (msg.createdAt) prev.createdAt = msg.createdAt; diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index eefe341..158af23 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -17,6 +17,7 @@ function buildSessionCard(session, agent) { const badges = []; if (agent) badges.push(`${esc(agent.name)}`); if (session.reviewAgentId) badges.push(``); + if (session.finalizerAgentId) badges.push(``); if (session.context) badges.push(``); if (session.telegramConfigured) badges.push(``); @@ -178,6 +179,11 @@ function renderMarkdown(text) { return ' ' + esc(agentName.trim()) + ' '; }); + // 4a2) [Finalizer: AgentName] prefix → Finalizer-Header + html = html.replace(/\[Finalizer:\s*([^\]]+)\]\s*/g, function(match, agentName) { + return ' ' + esc(agentName.trim()) + ' '; + }); + // 4b) [Review-Feedback von AgentName (Runde X/Y)]: → Review-Feedback-Header html = html.replace(/\[Review-Feedback von\s+([^\(]+)\(Runde\s+(\d+)\/(\d+)\)\]:\s*/g, function(match, agentName, round, maxRounds) { return '
Feedback von ' + esc(agentName.trim()) + ' Runde ' + esc(round) + '/' + esc(maxRounds) + '
'; @@ -682,12 +688,17 @@ let _reviewBubble = null; let _reviewTextEl = null; let _reviewText = ''; -function addReviewBubble(type, data) { +function addReviewBubble(type, data, agentType) { _reviewBubble = document.createElement('div'); _reviewBubble.className = 'msg review-bubble streamed'; + if (agentType === 'finalizer') _reviewBubble.classList.add('finalizer-bubble'); const header = document.createElement('div'); header.className = 'review-header'; - header.innerHTML = ' ' + esc(data.agentName || 'Review-Agent') + ' Runde ' + data.round + '/' + data.maxRounds + ''; + if (agentType === 'finalizer') { + header.innerHTML = ' ' + esc(data.agentName || 'Finalizer-Agent') + ''; + } else { + header.innerHTML = ' ' + esc(data.agentName || 'Review-Agent') + ' Runde ' + data.round + '/' + data.maxRounds + ''; + } _reviewBubble.appendChild(header); _reviewTextEl = document.createElement('div'); _reviewTextEl.className = 'review-content markdown'; @@ -713,11 +724,14 @@ function addReviewToolUse(data) { scrollDown(); } -function finalizeReviewBubble(type, data) { +function finalizeReviewBubble(type, data, agentType) { if (!_reviewBubble) return; const status = document.createElement('div'); status.className = 'review-status'; - if (type === 'approved') { + if (agentType === 'finalizer' || type === 'done') { + status.className += ' review-approved'; + status.innerHTML = ' Abgeschlossen'; + } else if (type === 'approved') { status.className += ' review-approved'; status.innerHTML = ' Approved'; } else {