feat: Finalizer-Agent – dritter Agent der nach Worker + Review einschreitet

Backend:
- SessionRecord + DB-Migration: finalizer_agent_id auf sessions
- ChatContext.FinalizerContext (Model, SystemPrompt, Tools, Workspace)
- ChatService: FinalizerContext aufbauen (analog zu ReviewContext)
- AnthropicService: RunFinalizer() nach Review-Phase (oder direkt nach Worker)
- RunReviewAgentChat() refactored zu generischem RunAgentChat()
  mit agentType-Parameter ('review' | 'finalizer') für SSE-Events und DB-Tags
- SSE-Events: finalizer_start, finalizer_text, finalizer_tool_use, finalizer_done

Frontend:
- Session-Page: Finalizer-Agent-Picker (im 'Weitere Einstellungen' Bereich)
- Chat: SSE-Events für Finalizer-Phase verarbeiten
- UI: addReviewBubble/finalizeReviewBubble unterstützen agentType 'finalizer'
  mit eigener Farbe (success-green) und Flag-Checkered-Icon
- renderMarkdown: [Finalizer: AgentName] Tags parsen
- History: Finalizer-Messages als .finalizer-bubble rendern
- Shared-Page: Finalizer-Messages korrekt erkennen und stylen
- Dashboard: Finalizer-Badge (flag-checkered) auf Session-Cards
This commit is contained in:
Meleki committed 2026-03-06 23:35:30 +01:00
1 parent a3f2aa38c1
commit 4bec740765
14 files changed
+228 -46

No files matched your search

+1 -1
View File
@@ -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
{
+2 -1
View File
@@ -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);
}
+15
View File
@@ -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<object> 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
+1
View File
@@ -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; }
}
+86 -29
View File
@@ -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 ──
/// <summary>
/// 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.
/// </summary>
private async Task<int> RunFinalizer(
HttpResponse response,
FinalizerContext finalizer,
List<object> 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;
}
/// <summary>
/// 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
}
/// <summary>
/// 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").
/// </summary>
private async Task<(string text, int iterations)> RunReviewAgentChat(
private async Task<(string text, int iterations)> RunAgentChat(
HttpResponse response,
ReviewContext review,
string modelId,
List<object> systemPrompt,
string agentId,
string agentWorkspace,
bool useMemory,
string webSearchVersion,
string agentToolChoice,
List<object> 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<string, object>
{
["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<JsonElement>(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;
+37 -1
View File
@@ -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<object>();
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
};
}
+7 -3
View File
@@ -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<SessionRecord> 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<SessionRecord> 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);
}
+12
View File
@@ -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;
+5
View File
@@ -130,6 +130,11 @@
<label>Max. Review-Runden</label>
<input type="number" id="maxReviewIterations" min="1" max="10" value="3">
</div>
<label>Finalizer-Agent <small>(optional)</small></label>
<div id="finalizerAgentSelectCard" class="agent-select-card">
<div class="agent-select-empty"><i class="fa fa-flag-checkered"></i> Kein Finalizer-Agent</div>
</div>
<input type="hidden" id="finalizerAgentSelect" value="">
<label>Benutzerdefinierte Befehle</label>
<div class="custom-commands" id="customCommandsList"></div>
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
+17
View File
@@ -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);
+4 -4
View File
@@ -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);
+19
View File
@@ -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,
+4 -3
View File
@@ -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;
+18 -4
View File
@@ -17,6 +17,7 @@ function buildSessionCard(session, agent) {
const badges = [];
if (agent) badges.push(`<span class="badge">${esc(agent.name)}</span>`);
if (session.reviewAgentId) badges.push(`<span class="badge badge-feature" title="Review-Agent"><i class="fa fa-check-circle-o"></i></span>`);
if (session.finalizerAgentId) badges.push(`<span class="badge badge-feature" title="Finalizer-Agent"><i class="fa fa-flag-checkered"></i></span>`);
if (session.context) badges.push(`<span class="badge badge-feature" title="${esc(session.context)}"><i class="fa fa-file-text-o"></i></span>`);
if (session.telegramConfigured) badges.push(`<span class="badge ${session.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${session.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
@@ -178,6 +179,11 @@ function renderMarkdown(text) {
return '<span class="review-inline-header"><i class="fa fa-check-circle-o"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
});
// 4a2) [Finalizer: AgentName] prefix → Finalizer-Header
html = html.replace(/\[Finalizer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
return '<span class="review-inline-header finalizer-inline-header"><i class="fa fa-flag-checkered"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
});
// 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 '<div class="review-inline-feedback"><i class="fa fa-repeat"></i> <strong>Feedback von ' + esc(agentName.trim()) + '</strong> <span class="review-round">Runde ' + esc(round) + '/' + esc(maxRounds) + '</span></div>';
@@ -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 = '<i class="fa fa-check-circle-o"></i> <strong>' + esc(data.agentName || 'Review-Agent') + '</strong> <span class="review-round">Runde ' + data.round + '/' + data.maxRounds + '</span>';
if (agentType === 'finalizer') {
header.innerHTML = '<i class="fa fa-flag-checkered"></i> <strong>' + esc(data.agentName || 'Finalizer-Agent') + '</strong>';
} else {
header.innerHTML = '<i class="fa fa-check-circle-o"></i> <strong>' + esc(data.agentName || 'Review-Agent') + '</strong> <span class="review-round">Runde ' + data.round + '/' + data.maxRounds + '</span>';
}
_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 = '<i class="fa fa-flag-checkered"></i> Abgeschlossen';
} else if (type === 'approved') {
status.className += ' review-approved';
status.innerHTML = '<i class="fa fa-check"></i> Approved';
} else {