feat: Vorbereiter-Agent (Preparer) für Sessions

- Neues optionales Session-Feld 'preparerAgentId' (DB + Model + API)
- PreparerContext im ChatContext für Agent-Konfiguration
- RunPreparer() in AnthropicService: führt Preparer vor dem Worker aus
  - [FORWARD] → aufbereiteter Prompt wird an Session-Agent weitergegeben
  - Ohne [FORWARD] → Rückfrage an User, Flow stoppt (preparer_query)
- Preparer-System-Prompt: erklärt Rolle, [FORWARD]-Mechanismus, Vorarbeit
- Preparer kann eigene Tools + Fähigkeiten aktiv nutzen
- ChatEndpoint: Preparer-Phase vor StreamChat integriert
- Frontend: SSE-Events (preparer_start/text/tool_use/done)
- Preparer-Bubble (Info-Farbe) mit Status Weitergeleitet/Rückfrage
- [Preparer: Name] → hübscher Header, [FORWARD] ausgeblendet
- [Vorbereitet von Name] → dezenter Header in User-Bubble
- Agent-Picker im Session-Form (vor Review-Agent)
This commit is contained in:
Meleki committed 2026-03-07 15:23:22 +01:00
1 parent f12fbfdc3e
commit 494f431963
12 files changed
+306 -8

No files matched your search

+44
View File
@@ -135,6 +135,50 @@ public static class ChatEndpoint
res.Headers["Cache-Control"] = "no-cache";
res.Headers["Connection"] = "keep-alive";
// ── Preparer-Agent Phase (vor dem Worker) ──
if (ctx.Preparer != null && !isChoiceResponse)
{
var preparerResult = await anthropic.RunPreparer(
res, ctx.Preparer, messages, ctx.ChainId, ctx.Workspace, sessionId, cancellationToken);
if (!preparerResult.ShouldForward)
{
// Preparer hat eine Rückfrage → Antwort als Text streamen, Flow beenden
// Preparer-Antwort wird schon in RunAgentChat in DB gespeichert
await AnthropicService.WriteSseEvent(res, "done", new
{
chainId = ctx.ChainId,
stopReason = "preparer_query",
planMode = ctx.PlanMode
}, cancellationToken);
return;
}
// Preparer ist zufrieden → aufbereiteten Prompt als zusätzlichen Kontext an die History anhängen
if (!string.IsNullOrWhiteSpace(preparerResult.ForwardedPrompt))
{
// Den aufbereiteten Prompt als System-Context-Message einfügen
messages.Add(new Dictionary<string, object>
{
["role"] = "user",
["content"] = new List<object>
{
new { type = "text", text = $"[Vorbereitet von {ctx.Preparer.Agent?.Name ?? "Vorbereiter-Agent"}]:\n{preparerResult.ForwardedPrompt}" }
}
});
// Auch in DB speichern
await db.SaveMessage(new MessageRecord
{
ChainId = ctx.ChainId,
Iteration = 0,
Role = "user",
Type = "text",
Content = $"[Vorbereitet von {ctx.Preparer.Agent?.Name ?? "Vorbereiter-Agent"}]:\n{preparerResult.ForwardedPrompt}"
});
}
}
// Bei Choice-Response: toolChoice auf "auto" setzen, damit Claude frei antworten kann
var effectiveToolChoice = isChoiceResponse ? "auto" : ctx.ToolChoice;
+2 -1
View File
@@ -82,8 +82,9 @@ public static class SessionEndpoint
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 preparerAgentId = body.TryGetProperty("preparerAgentId", out var pra) ? pra.GetString() : null;
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId);
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId);
await ctx.Response.WriteAsJsonAsync(session);
}
+16
View File
@@ -19,6 +19,9 @@ public class ChatContext
public SessionRecord Session { get; set; }
public bool PlanMode { get; set; }
// Preparer-Agent (vor dem Worker)
public PreparerContext Preparer { get; set; }
// Review-Agent
public ReviewContext Review { get; set; }
@@ -26,6 +29,19 @@ public class ChatContext
public FinalizerContext Finalizer { get; set; }
}
public class PreparerContext
{
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 bool AllowUserChoice { get; set; }
public AgentRecord Agent { get; set; }
}
public class FinalizerContext
{
public string ModelId { get; set; }
+1
View File
@@ -23,6 +23,7 @@ public class SessionRecord
public string ReviewAgentId { get; set; }
public int MaxReviewIterations { get; set; } = 3;
public string FinalizerAgentId { get; set; }
public string PreparerAgentId { get; set; }
public string CreatedAt { get; set; }
public string UpdatedAt { get; set; }
}
+77 -1
View File
@@ -314,6 +314,82 @@ public class AnthropicService
}
}
// ── Preparer-Agent Logik ──
public class PreparerResult
{
public bool ShouldForward { get; set; }
public string ForwardedPrompt { get; set; }
public string ResponseText { get; set; }
public int IterationCount { get; set; }
}
/// <summary>
/// Führt den Preparer-Agent aus. Der Preparer bekommt die User-Nachricht und die bisherige Konversation.
/// Antwortet er mit [FORWARD] → der Text nach [FORWARD] wird an den Session-Agent weitergegeben.
/// Sonst → seine Antwort wird als Rückfrage an den User gestreamt.
/// </summary>
public async Task<PreparerResult> RunPreparer(
HttpResponse response,
PreparerContext preparer,
List<object> historyMessages,
string chainId,
string workspace,
string sessionId,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await TryWriteSse(response, "preparer_start", new
{
agentId = preparer.AgentId,
agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent"
}, cancellationToken);
// Preparer bekommt die History als Kontext
var preparerMessages = new List<object>(historyMessages);
var usage = new TokenUsage();
var (text, iterations) = await RunAgentChat(
response, preparer.ModelId, preparer.SystemPrompt, preparer.AgentId,
preparer.Workspace, preparer.UseMemory, preparer.WebSearchVersion,
preparer.ToolChoice, preparerMessages, chainId, sessionId, usage,
0, "preparer", preparer.Agent?.Name ?? "Vorbereiter-Agent",
cancellationToken);
var result = new PreparerResult { IterationCount = iterations, ResponseText = text };
// Prüfe ob [FORWARD] enthalten ist
if (!string.IsNullOrEmpty(text) && text.Contains("[FORWARD]"))
{
var idx = text.IndexOf("[FORWARD]");
var forwardedText = text.Substring(idx + "[FORWARD]".Length).Trim();
result.ShouldForward = true;
result.ForwardedPrompt = forwardedText;
await TryWriteSse(response, "preparer_done", new
{
agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent",
forwarded = true
}, cancellationToken);
}
else
{
// Rückfrage an User – Text streamen
result.ShouldForward = false;
await TryWriteSse(response, "preparer_done", new
{
agentName = preparer.Agent?.Name ?? "Vorbereiter-Agent",
forwarded = false,
responseText = text
}, cancellationToken);
}
return result;
}
// ── Review-Agent Logik ──
private class ReviewResult
@@ -525,7 +601,7 @@ public class AnthropicService
int iterations = 0;
var finalText = new StringBuilder();
int maxToolIterations = 20;
var tag = agentType == "finalizer" ? "Finalizer" : "Review";
var tag = agentType == "preparer" ? "Preparer" : agentType == "finalizer" ? "Finalizer" : "Review";
while (iterations < maxToolIterations)
{
+76
View File
@@ -107,6 +107,81 @@ public class ChatService
if (!string.IsNullOrWhiteSpace(systemPromptText))
systemPrompt.Add(new { type = "text", text = systemPromptText });
// Preparer-Agent vorbereiten (falls konfiguriert)
PreparerContext preparerContext = null;
if (!string.IsNullOrEmpty(session.PreparerAgentId))
{
var preparerAgent = await _db.GetAgent(session.PreparerAgentId);
if (preparerAgent != null)
{
var preparerModelId = ResolveModelId(preparerAgent.Model);
var preparerMarked = await _db.GetMarkedMessages(sessionId);
var preparerMemDir = _memoryService.GetAgentMemoryDir(preparerAgent.AgentId);
var preparerSkills = await _db.GetAgentSkills(preparerAgent.AgentId);
var preparerWorkspace = workspace;
var preparerSystemText = SystemPromptBuilder.Build(
preparerAgent, session, preparerMarked, _memoryService, preparerMemDir,
preparerSkills, _skillService, userPersona: user?.GetEffectivePersona(),
resolvedWorkspace: preparerWorkspace);
var preparerPrompt = new List<object>();
if (!string.IsNullOrWhiteSpace(preparerSystemText))
preparerPrompt.Add(new { type = "text", text = preparerSystemText });
// Preparer-Instruktion: erklärt [FORWARD]-Mechanismus
preparerPrompt.Add(new { type = "text", text = @"## Deine Rolle als Vorbereiter-Agent
Du bist ein Vorbereiter-Agent. Deine Aufgabe ist es, die Anfrage des Users zu analysieren, vorzubereiten und – wenn möglich – Teile der Aufgabe bereits selbst zu erledigen, BEVOR das Ergebnis an den eigentlichen Session-Agent weitergegeben wird.
### Nutze deine Fähigkeiten aktiv!
Du hast eigene Fähigkeiten und Tools. Nutze sie! Wenn du Teile der Aufgabe selbst erledigen kannst (z.B. Dateien lesen, Daten aufbereiten, Strukturen erstellen, Recherchen durchführen), dann tu das BEVOR du weiterleitest. Der Session-Agent soll von dir ein möglichst gut vorbereitetes Paket bekommen.
### Dein Ablauf:
1. **Analysiere** die User-Anfrage
2. **Stelle Rückfragen**, wenn die Anfrage unklar, unvollständig oder mehrdeutig ist (antworte einfach mit deiner Frage als normaler Text)
3. **Erledige** was du kannst – nutze deine Fähigkeiten und Tools, um Vorarbeit zu leisten
4. **Leite weiter** mit dem Ergebnis deiner Arbeit
### Weiterleitung:
Wenn du fertig bist, schreibe `[FORWARD]` gefolgt von dem aufbereiteten Ergebnis/Prompt, der an den Session-Agent weitergegeben werden soll. Alles nach `[FORWARD]` wird als Prompt an den Session-Agent übergeben.
Beispiel – Anfrage nur verfeinert:
```
[FORWARD]
Der User möchte eine REST-API mit folgenden Endpunkten erstellen:
- GET /users – Liste aller User
- POST /users – Neuen User anlegen
Technologie: .NET 10, SQLite
```
Beispiel – Vorarbeit geleistet:
```
[FORWARD]
Der User möchte Produktdaten als JSON. Ich habe das JSON-Objekt bereits erstellt:
{""products"": [{""id"": 1, ""name"": ""Widget""}, {""id"": 2, ""name"": ""Gadget""}]}
Bitte speichere es in die Datei products.json im Workspace.
```
### Wichtige Regeln:
- Antworte OHNE `[FORWARD]` wenn du Rückfragen hast → der User sieht deine Antwort und kann darauf reagieren
- Antworte MIT `[FORWARD]` wenn du fertig bist → der Text nach [FORWARD] wird an den Session-Agent weitergegeben
- Nutze deine Tools und Fähigkeiten aktiv, um dem Session-Agent möglichst viel Arbeit abzunehmen
- Sei effizient: Wenn die Anfrage offensichtlich klar ist und du nichts vorbereiten kannst, leite sofort weiter" });
preparerContext = new PreparerContext
{
ModelId = preparerModelId,
SystemPrompt = preparerPrompt,
AgentId = preparerAgent.AgentId,
Workspace = preparerWorkspace,
UseMemory = preparerAgent.UseMemory,
WebSearchVersion = preparerAgent.WebSearchVersion,
ToolChoice = preparerAgent.ToolChoice ?? "any",
AllowUserChoice = preparerAgent.AllowUserChoice,
Agent = preparerAgent
};
}
}
// Review-Agent vorbereiten (falls konfiguriert)
ReviewContext reviewContext = null;
if (!string.IsNullOrEmpty(session.ReviewAgentId))
@@ -193,6 +268,7 @@ public class ChatService
MaxIterations = maxIterations,
Agent = agent,
Session = session,
Preparer = preparerContext,
Review = reviewContext,
Finalizer = finalizerContext,
PlanMode = planMode
+7 -3
View File
@@ -180,6 +180,7 @@ public class DatabaseService
TryAddColumn(conn, "sessions", "finalizer_agent_id", "TEXT");
TryAddColumn(conn, "agents", "allow_user_choice", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "chains", "plan_mode", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "preparer_agent_id", "TEXT");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS agent_skills (
@@ -377,7 +378,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, string finalizerAgentId = null)
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, string preparerAgentId = null)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
@@ -398,12 +399,13 @@ public class DatabaseService
ReviewAgentId = reviewAgentId,
MaxReviewIterations = maxReviewIterations,
FinalizerAgentId = finalizerAgentId,
PreparerAgentId = preparerAgentId,
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, 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)
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, preparer_agent_id, created_at, updated_at)
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @PreparerAgentId, @CreatedAt, @UpdatedAt)
", session);
return session;
}
@@ -452,6 +454,7 @@ public class DatabaseService
s.ReviewAgentId,
s.MaxReviewIterations,
s.FinalizerAgentId,
s.PreparerAgentId,
s.IsDeleted,
s.CreatedAt,
s.UpdatedAt,
@@ -507,6 +510,7 @@ public class DatabaseService
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()); }
if (body.TryGetProperty("preparerAgentId", out var pa)) { sets.Add("preparer_agent_id = @preparerAgentId"); p.Add("preparerAgentId", pa.ValueKind == System.Text.Json.JsonValueKind.Null ? null : pa.GetString()); }
await conn.ExecuteAsync($"UPDATE sessions SET {string.Join(", ", sets)} WHERE session_id = @sessionId", p);
}
+19
View File
@@ -1073,6 +1073,17 @@
color: var(--primary);
}
.preparer-inline-header {
color: var(--info);
}
.preparer-forwarded-header {
color: var(--info);
font-size: .85em;
margin-bottom: .3em;
opacity: .8;
}
.finalizer-bubble .review-header {
color: var(--primary);
}
@@ -1081,6 +1092,14 @@
border-color: var(--primary);
}
.preparer-bubble .review-header {
color: var(--info, #17a2b8);
}
.preparer-bubble {
border-color: var(--info, #17a2b8);
}
.review-inline-feedback {
color: var(--warning);
margin-bottom: .4em;
+5
View File
@@ -121,6 +121,11 @@
</div>
<label>Kontext</label>
<textarea id="sessionContext" rows="4" placeholder="Zusätzlicher Kontext für den System-Prompt (optional)"></textarea>
<label>Vorbereiter-Agent <small>(optional)</small></label>
<div id="preparerAgentSelectCard" class="agent-select-card">
<div class="agent-select-empty"><i class="fa fa-filter"></i> Kein Vorbereiter-Agent</div>
</div>
<input type="hidden" id="preparerAgentSelect" value="">
<label>Review-Agent <small>(optional)</small></label>
<div id="reviewAgentSelectCard" class="agent-select-card">
<div class="agent-select-empty"><i class="fa fa-check-circle-o"></i> Kein Review-Agent</div>
+17
View File
@@ -77,6 +77,23 @@ function handleStreamEvent(eventType, data) {
addChoiceBubble(data.toolCallId, data.question, data.choices);
break;
case 'preparer_start':
endStreamBubble();
addReviewBubble('start', data, 'preparer');
break;
case 'preparer_text':
appendToReviewText(data.text);
break;
case 'preparer_tool_use':
addReviewToolUse(data);
break;
case 'preparer_done':
finalizeReviewBubble(data.forwarded ? 'forwarded' : 'query', data, 'preparer');
break;
case 'review_start':
endStreamBubble();
addReviewBubble('start', data);
+19
View File
@@ -21,6 +21,8 @@ registerPage('session', {
const workspaceEl = document.getElementById('sessionWorkspace');
const agentSelect = document.getElementById('agentSelect');
const agentSelectCard = document.getElementById('agentSelectCard');
const preparerAgentSelect = document.getElementById('preparerAgentSelect');
const preparerAgentSelectCard = document.getElementById('preparerAgentSelectCard');
const reviewAgentSelect = document.getElementById('reviewAgentSelect');
const reviewAgentSelectCard = document.getElementById('reviewAgentSelectCard');
const reviewIterationsRow = document.getElementById('reviewIterationsRow');
@@ -115,6 +117,14 @@ registerPage('session', {
renderAgentInCard(agentSelectCard, null, 'fa-user-circle-o', 'Kein Agent ausgewählt');
}
// Initiale Anzeige – Preparer-Agent
if (session && session.preparerAgentId) {
preparerAgentSelect.value = session.preparerAgentId;
renderAgentInCard(preparerAgentSelectCard, agentMap[session.preparerAgentId] || null, 'fa-filter', 'Kein Vorbereiter-Agent');
} else {
renderAgentInCard(preparerAgentSelectCard, null, 'fa-filter', 'Kein Vorbereiter-Agent');
}
// Initiale Anzeige – Review-Agent
if (session && session.reviewAgentId) {
reviewAgentSelect.value = session.reviewAgentId;
@@ -141,6 +151,14 @@ registerPage('session', {
});
});
// Klick auf Preparer-Agent-Card → Modal öffnen
preparerAgentSelectCard.addEventListener('click', () => {
openAgentPickerModal(agents, agentSkillsMap, preparerAgentSelect.value, (selectedId) => {
preparerAgentSelect.value = selectedId || '';
renderAgentInCard(preparerAgentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-filter', 'Kein Vorbereiter-Agent');
});
});
// Klick auf Review-Agent-Card → Modal öffnen
reviewAgentSelectCard.addEventListener('click', () => {
openAgentPickerModal(agents, agentSkillsMap, reviewAgentSelect.value, (selectedId) => {
@@ -208,6 +226,7 @@ registerPage('session', {
reviewAgentId: reviewAgentSelect.value || null,
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
finalizerAgentId: finalizerAgentSelect.value || null,
preparerAgentId: preparerAgentSelect.value || null,
context: _sessionContextMde.value().trim() || null,
workspace: workspaceEl.value.trim() || null,
customCommands: commands,
+23 -3
View File
@@ -185,13 +185,24 @@ function renderMarkdown(text) {
return '<span class="review-inline-header finalizer-inline-header"><i class="fa fa-flag-checkered"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
});
// 4a3) [Preparer: AgentName] prefix → Preparer-Header
html = html.replace(/\[Preparer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
return '<span class="review-inline-header preparer-inline-header"><i class="fa fa-filter"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
});
// 4a4) [Vorbereitet von AgentName]: → Preparer-Forwarded-Header
html = html.replace(/\[Vorbereitet von\s+([^\]]+)\]:\s*/g, function(match, agentName) {
return '<div class="preparer-forwarded-header"><i class="fa fa-forward"></i> <strong>Vorbereitet von ' + esc(agentName.trim()) + '</strong></div>';
});
// 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>';
});
// 4c) [APPROVED] ausblenden
// 4c) [APPROVED] und [FORWARD] ausblenden
html = html.replace(/\[APPROVED\]/g, '');
html = html.replace(/\[FORWARD\]\s*/g, '');
// 4d) [Krea: dateiname.png] → Inline-Thumbnail mit Klick für Vollbild
html = html.replace(/\[Krea:\s*([^\]]+)\]/g, function(match, filename) {
@@ -767,9 +778,12 @@ function addReviewBubble(type, data, agentType) {
_reviewBubble = document.createElement('div');
_reviewBubble.className = 'msg review-bubble streamed';
if (agentType === 'finalizer') _reviewBubble.classList.add('finalizer-bubble');
if (agentType === 'preparer') _reviewBubble.classList.add('preparer-bubble');
const header = document.createElement('div');
header.className = 'review-header';
if (agentType === 'finalizer') {
if (agentType === 'preparer') {
header.innerHTML = '<i class="fa fa-filter"></i> <strong>' + esc(data.agentName || 'Vorbereiter-Agent') + '</strong>';
} else 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>';
@@ -803,7 +817,13 @@ function finalizeReviewBubble(type, data, agentType) {
if (!_reviewBubble) return;
const status = document.createElement('div');
status.className = 'review-status';
if (agentType === 'finalizer' || type === 'done') {
if (agentType === 'preparer' && type === 'forwarded') {
status.className += ' review-approved';
status.innerHTML = '<i class="fa fa-arrow-right"></i> Weitergeleitet an Session-Agent';
} else if (agentType === 'preparer' && type === 'query') {
status.className += ' review-rejected';
status.innerHTML = '<i class="fa fa-question-circle"></i> Rückfrage an User';
} else if (agentType === 'finalizer' || type === 'done') {
status.className += ' review-approved';
status.innerHTML = '<i class="fa fa-flag-checkered"></i> Abgeschlossen';
} else if (type === 'approved') {