Add Mixture-of-Agents (MoA) mode on top of Fusion
Extend the panel engine with a layer loop: Fusion is one proposer round, MoA runs N rounds where each round after the first sees the previous round's responses via <previous_layer_responses> and refines its answer. Only the final round is persisted; intermediate rounds are live-only. Rounds are clamped to 1-5. - SessionRecord + migration: moa_layers column (default 2) - RunPanel: layer loop with per-member cloned message injection - CloneForMember / BuildPreviousLayerBlock helpers for member isolation - SSE: panel_layer_start / panel_layer_done events - Frontend: layer_index composite keying, round dividers, MoA option - Docs: ARCHITECTURE + ARCHITECTURE_QUICK updated Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f21ed9ff9f
commit
b7ff928340
15 files changed
+239
-65
No files matched your search
+2
-2
@@ -348,7 +348,7 @@ TelegramBot (1) ─── (*) Session (via telegram_bot_id)
|
||||
| `users` | Benutzer (username, password_hash, display_name, is_admin, persona, settings, default_path) |
|
||||
| `tokens` | Auth-Tokens (token → user_id) |
|
||||
| `agents` | AI-Agenten (model – z.B. `claude-sonnet-4-6` oder `nvidia:deepseek-ai/deepseek-r1`, custom_commands, auto_commit, use_soul/identity/user_info/memory/auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale/offset, avatar_small, is_public) |
|
||||
| `sessions` | Chat-Sessions (description, image, image_small, workspace, context, custom_commands, telegram_bot_id, telegram_chat_id, avatar_scale/offset, preparer_agent_id, review_agent_id, max_review_iterations, finalizer_agent_id, multi_model_mode, panel_agent_ids) |
|
||||
| `sessions` | Chat-Sessions (description, image, image_small, workspace, context, custom_commands, telegram_bot_id, telegram_chat_id, avatar_scale/offset, preparer_agent_id, review_agent_id, max_review_iterations, finalizer_agent_id, multi_model_mode, panel_agent_ids, moa_layers) |
|
||||
| `chains` | Request-Chains (model, tokens, iterations, timing, tool_choice, tool_names, stream_error_info, plan_mode, archive_id) |
|
||||
| `messages` | Einzelne Nachrichten (text, tool_use, tool_result, error, web_search_result, auto_context), Tag für Sub-Agent-Markierung oder Import-Herkunft (`openclaw-import`), SortKey für korrekte Reihenfolge, thought_signature (Gemini) |
|
||||
| `attachments` | Datei-Anhänge (base64, image/document) |
|
||||
@@ -1191,7 +1191,7 @@ Admins können für jeden User einen **Autologin-Link** generieren – z.B. für
|
||||
|
||||
Das Multi-Agent-System ermöglicht bis zu **vier Agents** pro Session: einen **Worker**, einen optionalen **Vorbereiter (Preparer)**, einen optionalen **Reviewer** und einen optionalen **Finalizer**.
|
||||
|
||||
> **Multi-Modell-Panel (Fusion):** Orthogonal dazu kann eine Session zusätzlich ein **Panel** aus mehreren Agenten haben (`multi_model_mode="fusion"`, `panel_agent_ids`=CSV). Bei jeder Nachricht antworten alle Panel-Agenten **parallel** (read-only) auf die Anfrage; ihre Antworten werden dem Worker als `<panel_responses>`-Block vorgelegt, der **eine** Gesamtantwort synthetisiert und normal streamt (OpenRouter-Fusion-/MoA-Muster). Backend: `RunPanel()` in `AnthropicService.Panel.cs` (im ChatEndpoint-try vor dem Worker-Dispatch), Member-Provider-Weiche (Anthropic read-only via `PlanMode`, CLI via `--permission-mode plan`, OpenAI tool-los via `CompleteText`), Proposals als `[Panel: Name]`-Messages mit `Tag="panel"`. SSE: `panel_start`/`panel_member_start`/`panel_member_done`/`panel_done`. Frontend: `.panel-bubble` pro Member. Mehrschichtiges MoA ist als Aufbau geplant. Siehe ARCHITECTURE_QUICK.md.
|
||||
> **Multi-Modell-Panel (Fusion / MoA):** Orthogonal dazu kann eine Session zusätzlich ein **Panel** aus mehreren Agenten haben (`multi_model_mode="fusion"`/`"moa"`, `panel_agent_ids`=CSV, `moa_layers`=Proposer-Runden bei MoA). Bei jeder Nachricht antworten alle Panel-Agenten **parallel** (read-only) und **streamen live** (CLI: `TextDelta`/`ToolStart`/`ToolResult`; Anthropic-API: pro Tool-Runde; OpenAI: one-shot); ihre Antworten werden dem Worker als `<panel_responses>`-Block vorgelegt, der **eine** Gesamtantwort synthetisiert und normal streamt (OpenRouter-Fusion-/MoA-Muster). **MoA** = mehrere Proposer-Runden: ab Runde 2 sieht jeder Member die Vorrunde (`<previous_layer_responses>`) und verfeinert; `RunPanel(layers)` ist EINE Engine (Fusion = `layers==1`), nur die finale Runde wird persistiert. Backend: `RunPanel()` in `AnthropicService.Panel.cs` (im ChatEndpoint-try vor dem Worker-Dispatch), Member-Provider-Weiche (Anthropic read-only via `PlanMode`, CLI via `--permission-mode plan`, OpenAI tool-los via `CompleteText`), Proposals als `[Panel: Name]`-Messages mit `Tag="panel"`, Write-Lock in `SseHelper` serialisiert parallele SSE-Flushes. SSE: `panel_start`/`panel_layer_start`/`panel_member_start`/`panel_member_text`/`panel_member_tool*`/`panel_member_done`/`panel_layer_done`/`panel_done`. Frontend: `.panel-bubble` pro Member (gekeyt `layer_index`), `.panel-layer-divider` bei MoA. Siehe ARCHITECTURE_QUICK.md.
|
||||
|
||||
### Architektur
|
||||
|
||||
|
||||
+10
-6
@@ -88,7 +88,7 @@ User 1──* TelegramBot 1──* Session (via telegram_bot_id)
|
||||
| `users` | Login, DisplayName, IsAdmin, Persona, Settings (JSON), DefaultPath |
|
||||
| `tokens` | Auth-Tokens → User |
|
||||
| `agents` | AI-Agenten (Model – z.B. `claude-sonnet-4-6` oder `nvidia:model-id`, CustomCommands, AutoCommit, UseSoul/Identity/UserInfo/Memory/AutoContext, UseGitHubSearch, UseSerperSearch, WebSearch, AllowUserChoice, TaskInstruction, Effort, AvatarScale/Offset, AvatarSmall, IsPublic) |
|
||||
| `sessions` | Chat-Sessions (Description, Image, ImageSmall, Workspace, Context, CustomCommands, TelegramBotId, TelegramChatId, AvatarScale/Offset, PreparerAgentId, ReviewAgentId, MaxReviewIterations, FinalizerAgentId, MultiModelMode, PanelAgentIds) |
|
||||
| `sessions` | Chat-Sessions (Description, Image, ImageSmall, Workspace, Context, CustomCommands, TelegramBotId, TelegramChatId, AvatarScale/Offset, PreparerAgentId, ReviewAgentId, MaxReviewIterations, FinalizerAgentId, MultiModelMode, PanelAgentIds, MoaLayers) |
|
||||
| `chains` | Ein Request-Response-Zyklus (Tokens, Timing, Iterations, PlanMode, ToolChoice, ToolNames, StreamErrorInfo, ArchiveId) |
|
||||
| `messages` | Einzelne Nachrichten (text / tool_use / tool_result / error / web_search_result / auto_context), Tag für Sub-Agent-Markierung oder Import-Herkunft (`openclaw-import`), thought_signature (Gemini) |
|
||||
| `attachments` | Base64-Anhänge (Bilder, Dokumente) |
|
||||
@@ -347,17 +347,21 @@ Eine Session kann bis zu **vier Agents** haben: Worker (Pflicht), Preparer (opti
|
||||
- History: `renderMarkdown` parst `[Preparer: AgentName]`, `[Vorbereitet von AgentName]`, `[Review: AgentName]`, `[Finalizer: AgentName]`, filtert `[APPROVED]`, `[FORWARD]`, `[QUESTION]`
|
||||
- Bubble-Klassen: `.preparer-bubble` (Info-Farbe), `.review-bubble` (sekundäre Farbe), `.finalizer-bubble` (grüne Farbe)
|
||||
|
||||
## Multi-Modell-Panel (Fusion)
|
||||
## Multi-Modell-Panel (Fusion / MoA)
|
||||
|
||||
Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten haben (`multi_model_mode = "fusion"`, `panel_agent_ids` = CSV der Agent-IDs). Bei jeder Nachricht antworten alle Panel-Agenten **parallel** auf die Anfrage; ihre Antworten werden dem **Worker** als `<panel_responses>`-Block in die letzte User-Message gemergt, der daraus **eine** Gesamtantwort synthetisiert und normal streamt (Fusion = OpenRouter-Fusion-/Mixture-of-Agents-Muster). Mehrschichtiges MoA ist als Aufbau geplant.
|
||||
Zusätzlich zum Worker kann eine Session ein **Panel** aus mehreren Agenten haben (`multi_model_mode = "fusion"`/`"moa"`, `panel_agent_ids` = CSV der Agent-IDs, `moa_layers` = Proposer-Runden bei MoA, Default 2). Bei jeder Nachricht antworten alle Panel-Agenten **parallel** auf die Anfrage; ihre Antworten werden dem **Worker** als `<panel_responses>`-Block in die letzte User-Message gemergt, der daraus **eine** Gesamtantwort synthetisiert und normal streamt (OpenRouter-Fusion-/Mixture-of-Agents-Muster).
|
||||
|
||||
**Fusion vs. MoA:** Fusion = 1 Proposer-Runde → Synthese. **MoA** = `moa_layers` Runden (1–5, Backstop): ab Runde 2 sieht jeder Member die Antworten der Vorrunde (`<previous_layer_responses>`, isoliert pro Member via `CloneForMember` gemergt) und verfeinert. `RunPanel(layers)` ist EINE Engine (Fusion = `layers==1`). Nur die **finale** Runde wird persistiert; Zwischenrunden sind **live-only** (überleben kein F5).
|
||||
|
||||
**Member laufen read-only** (Anthropic: `FilterReadOnlyTools` via `PlanMode`, CLI: `--permission-mode plan`, OpenAI: tool-los via `OpenAiService.CompleteText`) → keine parallelen Schreibkonflikte. Concurrency-Cap (3) begrenzt parallele CLI-Children.
|
||||
|
||||
**Backend:** `ChatService.PrepareContext` baut `ChatContext.PanelAgents` (`List<SubAgentContext>`) aus `panel_agent_ids`. `RunPanel()` (AnthropicService.Panel.cs) läuft im ChatEndpoint-try VOR dem Worker-Dispatch: Member parallel (`Task.WhenAll` + `SemaphoreSlim`), Provider-Weiche pro Member, jede Proposal als `[Panel: Name]`-Message mit `Tag="panel"` persistiert (aus Worker-History gefiltert, überlebt F5). `NonStreamingLoopCallbacks.Persist=false` verhindert Doppel-Persistenz der Member-Arbeit. Der Synthese-Block wird in den **bestehenden** Text-Block der letzten User-Message gemergt (damit auch der CLI-Worker via `ExtractLastUserText` ihn sieht). Panel-Fehler sind graceful (Worker läuft ohne Synthese weiter).
|
||||
**Member streamen LIVE** (wie der Session-Agent): CLI forwardet `TextDelta`/`ToolStart`/`ToolResult`, Anthropic-API streamt pro Tool-Runde über `NonStreamingLoopCallbacks`, OpenAI = one-shot (Text am Ende). Ein `SemaphoreSlim`-Write-Lock in `SseHelper.TryWriteSse` serialisiert die SSE-Byte-Flushes paralleler Member (sonst vermischen sich die Frames).
|
||||
|
||||
**SSE-Events:** `panel_start`, `panel_member_start`, `panel_member_done` (voller Text), `panel_done`.
|
||||
**Backend:** `ChatService.PrepareContext` baut `ChatContext.PanelAgents` (`List<SubAgentContext>`) + `MoaLayers`. `RunPanel(layers)` (AnthropicService.Panel.cs) läuft im ChatEndpoint-try VOR dem Worker-Dispatch: pro Runde Member parallel (`Task.WhenAll` + `SemaphoreSlim`), Provider-Weiche pro Member, finale Proposals als `[Panel: Name]`-Message mit `Tag="panel"` persistiert (aus Worker-History gefiltert). `NonStreamingLoopCallbacks.Persist=false` verhindert Doppel-Persistenz. Der Synthese-Block wird in den **bestehenden** Text-Block der letzten User-Message gemergt (damit auch der CLI-Worker via `ExtractLastUserText` ihn sieht). Panel-Fehler sind graceful.
|
||||
|
||||
**Frontend:** `stream-ui.js` rendert pro Member eine `.panel-bubble` (keyed by index; non-streaming → Bubble wird bei `_done` gefüllt). History: `history.js _renderPanelMemberBubble` rendert `Tag="panel"`-Messages als Panel-Bubbles. Picker: `#multiModelMode`-Dropdown + Mehrfach-Agenten-Picker (`_openMultiPickerModal`) in der Session-Bearbeitung.
|
||||
**SSE-Events:** `panel_start` (mode/layers), `panel_layer_start`/`panel_layer_done` (MoA-Runden), `panel_member_start` (mit `layer`), `panel_member_text`/`panel_member_tool_use`/`panel_member_tool_end`/`panel_member_tool` (live), `panel_member_done`, `panel_done`.
|
||||
|
||||
**Frontend:** `stream-ui.js` rendert pro Member eine `.panel-bubble`, gekeyt per `layer_index` (MoA: gleiche Member über mehrere Runden; aktive Runde pro Index wird bei `panel_member_start` gemerkt, da Runden sequenziell laufen). `panel_layer_start` zeigt bei >1 Runde einen `.panel-layer-divider`. History: `history.js _renderPanelMemberBubble` rendert `Tag="panel"`-Messages. Picker: `#multiModelMode`-Dropdown (Aus/Fusion/MoA) + `#moaLayers` + Mehrfach-Agenten-Picker (`_openMultiPickerModal`).
|
||||
|
||||
**Web-Search Sources:**
|
||||
- `web_search_result`-Messages in DB gespeichert (Sources als JSON-Array, leere werden übersprungen)
|
||||
|
||||
@@ -236,11 +236,13 @@ public static class ChatEndpoint
|
||||
// danach als Synthesizer über den unveränderten Dispatch unten. Heartbeat läuft bereits,
|
||||
// Cancellation (workCt/sseCt) ist verfügbar. Nur bei aktivem Fusion-Modus + nicht-leerem
|
||||
// Panel + keiner Choice-Antwort.
|
||||
if (!isChoiceResponse && ctx.MultiModelMode == "fusion" && ctx.PanelAgents != null && ctx.PanelAgents.Count > 0)
|
||||
if (!isChoiceResponse && (ctx.MultiModelMode == "fusion" || ctx.MultiModelMode == "moa") && ctx.PanelAgents != null && ctx.PanelAgents.Count > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
await anthropic.RunPanel(res, ctx.PanelAgents, messages, ctx.ChainId, sessionId, userApiKey?.ApiKey, workCt, sseCt);
|
||||
// Fusion = 1 Runde, MoA = MoaLayers Proposer-Runden vor der Synthese.
|
||||
var panelLayers = ctx.MultiModelMode == "moa" ? System.Math.Max(1, ctx.MoaLayers) : 1;
|
||||
await anthropic.RunPanel(res, ctx.PanelAgents, messages, ctx.ChainId, sessionId, userApiKey?.ApiKey, panelLayers, workCt, sseCt);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -148,8 +148,9 @@ public static class SessionEndpoint
|
||||
var preparerAgentId = body.TryGetProperty("preparerAgentId", out var pra) ? pra.GetString() : null;
|
||||
var multiModelMode = body.TryGetProperty("multiModelMode", out var mmm) ? mmm.GetString() : null;
|
||||
var panelAgentIds = body.TryGetProperty("panelAgentIds", out var pai) && pai.ValueKind != JsonValueKind.Null ? pai.GetString() : null;
|
||||
var moaLayers = body.TryGetProperty("moaLayers", out var mol) && mol.ValueKind == JsonValueKind.Number ? mol.GetInt32() : 2;
|
||||
|
||||
var session = await db.CreateSession(userId, name, description, image, imageSmall, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId, multiModelMode, panelAgentIds);
|
||||
var session = await db.CreateSession(userId, name, description, image, imageSmall, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId, multiModelMode, panelAgentIds, moaLayers);
|
||||
await ctx.Response.WriteAsJsonAsync(session);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ChatContext
|
||||
// der Worker synthetisiert. "off" / "fusion" (später "moa").
|
||||
public string MultiModelMode { get; set; }
|
||||
public List<SubAgentContext> PanelAgents { get; set; }
|
||||
public int MoaLayers { get; set; } = 2; // MoA: Anzahl Proposer-Runden (nur bei mode="moa")
|
||||
}
|
||||
|
||||
public class SubAgentContext
|
||||
|
||||
@@ -23,8 +23,9 @@ public class SessionRecord
|
||||
public int MaxReviewIterations { get; set; } = 3;
|
||||
public string FinalizerAgentId { get; set; }
|
||||
public string PreparerAgentId { get; set; }
|
||||
public string MultiModelMode { get; set; } // "off" / "fusion" (später "moa")
|
||||
public string MultiModelMode { get; set; } // "off" / "fusion" / "moa"
|
||||
public string PanelAgentIds { get; set; } // CSV der Panel-Agenten (Proposer)
|
||||
public int MoaLayers { get; set; } = 2; // MoA: Anzahl Proposer-Runden vor der Synthese (Fusion ignoriert)
|
||||
public string UnityMcpUrl { get; set; }
|
||||
public string UnityMcpToken { get; set; }
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
@@ -12,13 +12,17 @@ namespace Gott.Services;
|
||||
|
||||
public partial class AnthropicService
|
||||
{
|
||||
// ── Multi-Modell-Panel (Fusion) ──
|
||||
// ── Multi-Modell-Panel (Fusion / MoA) ──
|
||||
//
|
||||
// Mehrere Panel-Agenten ("Proposer") beantworten dieselbe Anfrage PARALLEL und
|
||||
// non-streaming. Anschließend werden ihre Antworten dem Worker-Agent als
|
||||
// streamen live. Anschließend werden ihre Antworten dem Worker-Agent als
|
||||
// <panel_responses>-Block vorgelegt, der daraus EINE Gesamtantwort synthetisiert
|
||||
// und über die unveränderte Streaming-Pipeline ausgibt.
|
||||
//
|
||||
// MoA (Mixture-of-Agents): layers>1 → mehrere Proposer-Runden; jede Runde k>1 sieht
|
||||
// die Antworten der Vorrunde (<previous_layer_responses>) und verfeinert. Fusion = layers==1.
|
||||
// Nur die FINALE Runde wird persistiert (Zwischen-Runden sind live-only).
|
||||
//
|
||||
// Member laufen READ-ONLY (Anthropic: PlanMode-Filter, CLI: --permission-mode plan)
|
||||
// bzw. tool-los (OpenAI), damit parallele Member keine Dateien gleichzeitig schreiben.
|
||||
// Member-Arbeit wird NICHT über den Loop persistiert (Persist=false) – stattdessen
|
||||
@@ -34,10 +38,11 @@ public partial class AnthropicService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Führt das Panel aus (Fusion): alle Member parallel, sammelt ihre Antworten,
|
||||
/// persistiert sie (Tag="panel") und merged den Synthese-Block in die letzte
|
||||
/// User-Message von <paramref name="workerMessages"/> ein. Danach streamt der Worker
|
||||
/// als Synthesizer ganz normal weiter. Gibt die Anzahl beigetragener Member zurück
|
||||
/// Führt das Panel aus (Fusion = 1 Runde, MoA = <paramref name="layers"/> Runden): pro Runde
|
||||
/// alle Member parallel; ab Runde 2 sieht jeder Member die Antworten der Vorrunde. Die FINALE
|
||||
/// Runde wird persistiert (Tag="panel"); ihr Synthese-Block wird in die letzte User-Message von
|
||||
/// <paramref name="workerMessages"/> gemergt. Danach streamt der Worker als Synthesizer ganz
|
||||
/// normal weiter. Gibt die Anzahl beigetragener Member der finalen Runde zurück
|
||||
/// (0 = nichts injiziert → Worker antwortet regulär).
|
||||
/// </summary>
|
||||
public async Task<int> RunPanel(
|
||||
@@ -47,47 +52,65 @@ public partial class AnthropicService
|
||||
string chainId,
|
||||
string sessionId,
|
||||
string apiToken,
|
||||
int layers,
|
||||
CancellationToken workCt,
|
||||
CancellationToken sseCt)
|
||||
{
|
||||
if (panel == null || panel.Count == 0) return 0;
|
||||
layers = Math.Clamp(layers, 1, 5); // Kosten-Backstop: max. 5 Proposer-Runden
|
||||
|
||||
// Write-Lock: alle Member streamen LIVE parallel auf DIESELBE HttpResponse →
|
||||
// ohne Serialisierung würden sich ihre SSE-Frames auf Byte-Ebene vermischen.
|
||||
// Serialisiert nur die µs-Byte-Flushes, nicht die parallele Modell-Arbeit.
|
||||
using var sseGate = new SemaphoreSlim(1, 1);
|
||||
|
||||
await SseHelper.TryWriteSse(response, "panel_start", new { members = panel.Count, mode = "fusion" }, sseCt, sseGate);
|
||||
var mode = layers > 1 ? "moa" : "fusion";
|
||||
await SseHelper.TryWriteSse(response, "panel_start", new { members = panel.Count, mode, layers }, sseCt, sseGate);
|
||||
|
||||
// Concurrency begrenzen: bei CLI-Membern sind das parallele claude-Children.
|
||||
using var sem = new SemaphoreSlim(Math.Min(3, panel.Count));
|
||||
var tasks = new List<Task<PanelMemberResult>>();
|
||||
for (int i = 0; i < panel.Count; i++)
|
||||
// MoA: mehrere Proposer-Runden. Runde k>1 bekommt <previous_layer_responses> und verfeinert.
|
||||
// Fusion = layers==1 (keine Vorrunde). prev füttert die nächste Runde bzw. die finale Synthese.
|
||||
List<PanelMemberResult> prev = null;
|
||||
for (int layer = 1; layer <= layers; layer++)
|
||||
{
|
||||
var member = panel[i];
|
||||
var index = i;
|
||||
tasks.Add(Task.Run(async () =>
|
||||
bool isFinalLayer = layer == layers;
|
||||
await SseHelper.TryWriteSse(response, "panel_layer_start", new { layer, total = layers }, sseCt, sseGate);
|
||||
|
||||
string prevBlock = prev == null ? null : BuildPreviousLayerBlock(prev, layer, layers);
|
||||
|
||||
// Concurrency begrenzen: bei CLI-Membern sind das parallele claude-Children.
|
||||
using var sem = new SemaphoreSlim(Math.Min(3, panel.Count));
|
||||
var tasks = new List<Task<PanelMemberResult>>();
|
||||
for (int i = 0; i < panel.Count; i++)
|
||||
{
|
||||
await sem.WaitAsync(workCt);
|
||||
try { return await RunPanelMember(response, member, workerMessages, index, chainId, sessionId, apiToken, sseGate, workCt, sseCt); }
|
||||
finally { sem.Release(); }
|
||||
}, workCt));
|
||||
var member = panel[i];
|
||||
var index = i;
|
||||
var lyr = layer;
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
await sem.WaitAsync(workCt);
|
||||
try { return await RunPanelMember(response, member, workerMessages, index, lyr, layers, prevBlock, isFinalLayer, chainId, sessionId, apiToken, sseGate, workCt, sseCt); }
|
||||
finally { sem.Release(); }
|
||||
}, workCt));
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
var ordered = results
|
||||
.Where(r => r != null && !string.IsNullOrWhiteSpace(r.Text))
|
||||
.OrderBy(r => r.Index)
|
||||
.ToList();
|
||||
|
||||
await SseHelper.TryWriteSse(response, "panel_layer_done", new { layer, total = layers, members = ordered.Count }, sseCt, sseGate);
|
||||
|
||||
if (ordered.Count == 0)
|
||||
{
|
||||
// Keine verwertbaren Antworten in dieser Runde → ohne Synthese weiter.
|
||||
await SseHelper.TryWriteSse(response, "panel_done", new { synthesizing = false, members = 0 }, sseCt, sseGate);
|
||||
return 0;
|
||||
}
|
||||
prev = ordered;
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
var ordered = results
|
||||
.Where(r => r != null && !string.IsNullOrWhiteSpace(r.Text))
|
||||
.OrderBy(r => r.Index)
|
||||
.ToList();
|
||||
|
||||
if (ordered.Count == 0)
|
||||
{
|
||||
await SseHelper.TryWriteSse(response, "panel_done", new { synthesizing = false, members = 0 }, sseCt, sseGate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Synthese-Block bauen und in die letzte User-Message einmergen.
|
||||
// Synthese-Block aus der LETZTEN Runde bauen und in die letzte User-Message einmergen.
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("---");
|
||||
@@ -98,7 +121,7 @@ public partial class AnthropicService
|
||||
+ "dem User (du musst das Panel nicht erwähnen). Du kannst deine eigenen Tools nutzen, um zu prüfen oder die Aufgabe abzuschließen.");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("<panel_responses>");
|
||||
foreach (var r in ordered)
|
||||
foreach (var r in prev)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"### Antwort von {r.Name} ({r.Model}):");
|
||||
@@ -108,8 +131,8 @@ public partial class AnthropicService
|
||||
|
||||
MergePanelIntoLastUserText(workerMessages, sb.ToString());
|
||||
|
||||
await SseHelper.TryWriteSse(response, "panel_done", new { synthesizing = true, members = ordered.Count }, sseCt, sseGate);
|
||||
return ordered.Count;
|
||||
await SseHelper.TryWriteSse(response, "panel_done", new { synthesizing = true, members = prev.Count }, sseCt, sseGate);
|
||||
return prev.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -122,6 +145,10 @@ public partial class AnthropicService
|
||||
SubAgentContext member,
|
||||
List<object> workerMessages,
|
||||
int index,
|
||||
int layer,
|
||||
int totalLayers,
|
||||
string previousLayerBlock,
|
||||
bool persist,
|
||||
string chainId,
|
||||
string sessionId,
|
||||
string apiToken,
|
||||
@@ -130,9 +157,12 @@ public partial class AnthropicService
|
||||
CancellationToken sseCt)
|
||||
{
|
||||
var name = member.Agent?.Name ?? $"Panel-Agent {index + 1}";
|
||||
await SseHelper.TryWriteSse(response, "panel_member_start", new { index, name, model = member.ModelId }, sseCt, sseGate);
|
||||
await SseHelper.TryWriteSse(response, "panel_member_start", new { index, name, model = member.ModelId, layer, totalLayers }, sseCt, sseGate);
|
||||
|
||||
var memberMessages = new List<object>(workerMessages); // eigene Kopie pro Member
|
||||
// Eigene, ISOLIERTE Kopie pro Member. Bei MoA (Runde > 1) wird der Vorrunden-Block in den
|
||||
// bestehenden Text-Block der letzten User-Message gemergt – isoliert, damit er nicht in
|
||||
// workerMessages oder parallele Member leckt.
|
||||
var memberMessages = CloneForMember(workerMessages, previousLayerBlock);
|
||||
string text;
|
||||
try
|
||||
{
|
||||
@@ -180,7 +210,8 @@ public partial class AnthropicService
|
||||
|
||||
// Proposal als EINE Message persistieren: Tag="panel" → aus Worker-History gefiltert,
|
||||
// [Panel: Name]-Prefix → Label beim History-Rendering. Iteration=index+1 → stabile Reihenfolge.
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
// Nur die FINALE Runde persistieren (MoA-Zwischenrunden sind live-only, überleben kein F5).
|
||||
if (persist && !string.IsNullOrEmpty(text))
|
||||
{
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
@@ -302,4 +333,68 @@ public partial class AnthropicService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liefert eine pro-Member isolierte Kopie der Worker-Messages. Ist <paramref name="block"/>
|
||||
/// gesetzt (MoA, Runde > 1), wird er in den bestehenden Text-Block der letzten User-Message
|
||||
/// gemergt – auf einer GEKLONTEN Message + Content-Liste, damit die Mutation NICHT in
|
||||
/// workerMessages oder parallele Member leckt. block==null → einfache Shallow-Kopie (wie Fusion).
|
||||
/// </summary>
|
||||
private static List<object> CloneForMember(List<object> messages, string block)
|
||||
{
|
||||
var copy = new List<object>(messages);
|
||||
if (string.IsNullOrEmpty(block)) return copy;
|
||||
for (int i = copy.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (copy[i] is Dictionary<string, object> d &&
|
||||
d.TryGetValue("role", out var r) && (r as string) == "user" &&
|
||||
d.TryGetValue("content", out var c) && c is List<object> contentList)
|
||||
{
|
||||
var newContent = new List<object>(contentList);
|
||||
var newD = new Dictionary<string, object>(d) { ["content"] = newContent };
|
||||
bool merged = false;
|
||||
for (int j = 0; j < newContent.Count; j++)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(newContent[j]);
|
||||
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||||
if (el.TryGetProperty("type", out var t) && t.GetString() == "text")
|
||||
{
|
||||
var orig = el.TryGetProperty("text", out var txt) ? txt.GetString() ?? "" : "";
|
||||
newContent[j] = new { type = "text", text = orig + "\n\n" + block };
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!merged) newContent.Add(new { type = "text", text = block });
|
||||
copy[i] = newD;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut den <previous_layer_responses>-Block für MoA: die Antworten der Vorrunde als
|
||||
/// Referenz, mit der Anweisung, die eigene Antwort zu VERBESSERN (nicht blind zu kopieren).
|
||||
/// </summary>
|
||||
private static string BuildPreviousLayerBlock(List<PanelMemberResult> prev, int layer, int total)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("---");
|
||||
sb.AppendLine($"[Multi-Modell-Panel · Runde {layer} von {total}] Andere KI-Agenten haben in der vorherigen Runde "
|
||||
+ "auf dieselbe Anfrage geantwortet. Nutze ihre Antworten als zusätzliche Referenz: übernimm korrekte Fakten "
|
||||
+ "und gute Ideen, korrigiere erkennbare Fehler und VERBESSERE deine eigene Antwort. Kopiere nicht blind – "
|
||||
+ "denke kritisch und liefere die bestmögliche eigenständige Antwort auf die ursprüngliche Anfrage.");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("<previous_layer_responses>");
|
||||
foreach (var r in prev)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"### Antwort von {r.Name} ({r.Model}):");
|
||||
sb.AppendLine(r.Text.Trim());
|
||||
}
|
||||
sb.AppendLine("</previous_layer_responses>");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -222,6 +222,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||
Finalizer = finalizerContext,
|
||||
MultiModelMode = session.MultiModelMode,
|
||||
PanelAgents = panelContexts,
|
||||
MoaLayers = session.MoaLayers,
|
||||
PlanMode = planMode
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public partial class DatabaseService
|
||||
{
|
||||
// ── Sessions ──
|
||||
|
||||
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string imageSmall = 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, string multiModelMode = null, string panelAgentIds = null)
|
||||
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string imageSmall = 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, string multiModelMode = null, string panelAgentIds = null, int moaLayers = 2)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
@@ -37,12 +37,13 @@ public partial class DatabaseService
|
||||
PreparerAgentId = preparerAgentId,
|
||||
MultiModelMode = multiModelMode,
|
||||
PanelAgentIds = panelAgentIds,
|
||||
MoaLayers = moaLayers,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO sessions (session_id, user_id, name, description, image, image_small, 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, multi_model_mode, panel_agent_ids, created_at, updated_at)
|
||||
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @ImageSmall, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @PreparerAgentId, @MultiModelMode, @PanelAgentIds, @CreatedAt, @UpdatedAt)
|
||||
INSERT INTO sessions (session_id, user_id, name, description, image, image_small, 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, multi_model_mode, panel_agent_ids, moa_layers, created_at, updated_at)
|
||||
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @ImageSmall, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @PreparerAgentId, @MultiModelMode, @PanelAgentIds, @MoaLayers, @CreatedAt, @UpdatedAt)
|
||||
", session);
|
||||
return session;
|
||||
}
|
||||
@@ -159,6 +160,7 @@ public partial class DatabaseService
|
||||
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()); }
|
||||
if (body.TryGetProperty("multiModelMode", out var mmm)) { sets.Add("multi_model_mode = @multiModelMode"); p.Add("multiModelMode", mmm.ValueKind == System.Text.Json.JsonValueKind.Null ? null : mmm.GetString()); }
|
||||
if (body.TryGetProperty("panelAgentIds", out var pai)) { sets.Add("panel_agent_ids = @panelAgentIds"); p.Add("panelAgentIds", pai.ValueKind == System.Text.Json.JsonValueKind.Null ? null : pai.GetString()); }
|
||||
if (body.TryGetProperty("moaLayers", out var mol) && mol.ValueKind == System.Text.Json.JsonValueKind.Number) { sets.Add("moa_layers = @moaLayers"); p.Add("moaLayers", mol.GetInt32()); }
|
||||
if (body.TryGetProperty("unityMcpUrl", out var umu)) { sets.Add("unity_mcp_url = @unityMcpUrl"); p.Add("unityMcpUrl", umu.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umu.GetString()); }
|
||||
if (body.TryGetProperty("unityMcpToken", out var umt)) { sets.Add("unity_mcp_token = @unityMcpToken"); p.Add("unityMcpToken", umt.ValueKind == System.Text.Json.JsonValueKind.Null ? null : umt.GetString()); }
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@ public partial class DatabaseService
|
||||
TryAddColumn(conn, "chains", "effort", "TEXT NOT NULL DEFAULT ''");
|
||||
TryAddColumn(conn, "sessions", "multi_model_mode", "TEXT");
|
||||
TryAddColumn(conn, "sessions", "panel_agent_ids", "TEXT");
|
||||
TryAddColumn(conn, "sessions", "moa_layers", "INTEGER NOT NULL DEFAULT 2");
|
||||
|
||||
// Migration: 1M-Suffix entfernen (Mai 2026). Seit März 2026 ist 1M-Context Default
|
||||
// bei Opus 4.6/4.7/4.8 + Sonnet 4.6 – kein "[1m]"-Suffix mehr nötig.
|
||||
|
||||
@@ -1227,6 +1227,34 @@
|
||||
border-color: var(--accent, #9b6dff);
|
||||
}
|
||||
|
||||
/* MoA: Trenner zwischen Proposer-Runden */
|
||||
.panel-layer-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .6em;
|
||||
margin: 1.1em 0 .5em;
|
||||
color: var(--accent, #9b6dff);
|
||||
font-size: .82em;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
opacity: .85;
|
||||
}
|
||||
.panel-layer-divider::before,
|
||||
.panel-layer-divider::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--accent, #9b6dff);
|
||||
opacity: .35;
|
||||
}
|
||||
.panel-layer-divider span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .4em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-inline-feedback {
|
||||
color: var(--warning);
|
||||
margin-bottom: .4em;
|
||||
|
||||
+7
-1
@@ -156,10 +156,11 @@
|
||||
<div class="agent-select-empty"><i class="fa fa-flag-checkered"></i> Kein Finalizer-Agent</div>
|
||||
</div>
|
||||
<input type="hidden" id="finalizerAgentSelect" value="">
|
||||
<label>Multi-Modell-Panel <small>(Fusion)</small></label>
|
||||
<label>Multi-Modell-Panel <small>(Fusion / MoA)</small></label>
|
||||
<select id="multiModelMode">
|
||||
<option value="off">Aus</option>
|
||||
<option value="fusion">Fusion – Panel antwortet parallel, Session-Agent synthetisiert</option>
|
||||
<option value="moa">MoA – mehrere Proposer-Runden, dann Synthese</option>
|
||||
</select>
|
||||
<div id="panelAgentsRow" hidden>
|
||||
<label>Panel-Agenten <small>(antworten parallel zur Anfrage)</small></label>
|
||||
@@ -169,6 +170,11 @@
|
||||
<input type="hidden" id="panelAgentIds" value="">
|
||||
<small class="muted">Jeder Panel-Agent kostet zusätzlich Tokens/Zeit – alle laufen pro Nachricht. Member arbeiten read-only.</small>
|
||||
</div>
|
||||
<div id="moaLayersRow" hidden>
|
||||
<label>MoA-Runden <small>(Proposer-Runden vor der Synthese)</small></label>
|
||||
<input type="number" id="moaLayers" min="1" max="5" step="1" value="2">
|
||||
<small class="muted">Mehr Runden = mehr Verfeinerung, aber Kosten/Zeit ≈ Runden × Agenten. Empfohlen: 2–3.</small>
|
||||
</div>
|
||||
<label>Benutzerdefinierte Befehle</label>
|
||||
<div class="custom-commands" id="customCommandsList"></div>
|
||||
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
||||
|
||||
+9
-1
@@ -128,11 +128,19 @@ function handleStreamEvent(eventType, data) {
|
||||
finalizeReviewBubble('done', data, 'finalizer');
|
||||
break;
|
||||
|
||||
// Multi-Modell-Panel (Fusion): mehrere Member streamen parallel LIVE
|
||||
// Multi-Modell-Panel (Fusion/MoA): mehrere Member streamen parallel LIVE
|
||||
case 'panel_start':
|
||||
startPanel(data);
|
||||
break;
|
||||
|
||||
// MoA: Runden-Trenner (nur bei mehreren Runden sichtbar)
|
||||
case 'panel_layer_start':
|
||||
startPanelLayer(data);
|
||||
break;
|
||||
|
||||
case 'panel_layer_done':
|
||||
break;
|
||||
|
||||
case 'panel_member_start':
|
||||
addPanelMemberBubble(data);
|
||||
break;
|
||||
|
||||
@@ -33,6 +33,8 @@ registerPage('session', {
|
||||
const panelAgentsRow = document.getElementById('panelAgentsRow');
|
||||
const panelAgentsCard = document.getElementById('panelAgentsCard');
|
||||
const panelAgentIdsEl = document.getElementById('panelAgentIds');
|
||||
const moaLayersRow = document.getElementById('moaLayersRow');
|
||||
const moaLayersEl = document.getElementById('moaLayers');
|
||||
const imagePreview = document.getElementById('sessionImagePreview');
|
||||
const saveBtn = document.getElementById('sessionSaveBtn');
|
||||
const cancelBtn = document.getElementById('sessionCancelBtn');
|
||||
@@ -148,9 +150,11 @@ registerPage('session', {
|
||||
}
|
||||
function updatePanelVisibility() {
|
||||
panelAgentsRow.hidden = (panelModeEl.value === 'off');
|
||||
if (moaLayersRow) moaLayersRow.hidden = (panelModeEl.value !== 'moa');
|
||||
}
|
||||
panelModeEl.value = (session && session.multiModelMode) || 'off';
|
||||
panelAgentIdsEl.value = (session && session.panelAgentIds) || '';
|
||||
if (moaLayersEl) moaLayersEl.value = (session && session.moaLayers) || 2;
|
||||
renderPanelInCard();
|
||||
updatePanelVisibility();
|
||||
panelModeEl.addEventListener('change', updatePanelVisibility);
|
||||
@@ -215,6 +219,7 @@ registerPage('session', {
|
||||
preparerAgentId: preparerAgentSelect.value || null,
|
||||
multiModelMode: panelModeEl.value || 'off',
|
||||
panelAgentIds: panelAgentIdsEl.value || null,
|
||||
moaLayers: moaLayersEl ? (parseInt(moaLayersEl.value, 10) || 2) : 2,
|
||||
context: _sessionContextMde.value().trim() || null,
|
||||
workspace: workspaceEl.value.trim() || null,
|
||||
customCommands: commands,
|
||||
|
||||
+29
-10
@@ -206,19 +206,36 @@ function finalizeReviewBubble(type, data, agentType) {
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
// ── Panel Bubbles (Fusion: mehrere Member streamen parallel LIVE) ──
|
||||
// Pro Member eine eigene Bubble (keyed by index). Innerhalb der Bubble werden Text
|
||||
// und Tool-Boxen interleaved gerendert (wie beim Session-Agent), sodass parallele
|
||||
// Member sich NICHT miteinander vermischen – das Demuxing macht der index der Events.
|
||||
// ── Panel Bubbles (Fusion/MoA: mehrere Member streamen parallel LIVE) ──
|
||||
// Pro Member eine eigene Bubble. Bei MoA wiederholen sich die Member über mehrere Runden
|
||||
// (gleicher index, andere layer) → Bubbles werden per "layer_index" gekeyt. Da die Runden
|
||||
// SEQUENZIELL laufen, genügt es, die aktive Runde pro index zu merken (gesetzt bei
|
||||
// panel_member_start); die Live-Text/Tool-Events tragen nur den index und finden so ihre Bubble.
|
||||
|
||||
let _panelBubbles = {};
|
||||
let _panelActiveLayer = {}; // index -> aktuell laufende Runde (MoA)
|
||||
|
||||
function _panelKey(index, layer) { return (layer || 1) + '_' + index; }
|
||||
function _panelGetBubble(index) { return _panelBubbles[_panelKey(index, _panelActiveLayer[index])]; }
|
||||
|
||||
function startPanel(data) {
|
||||
endStreamBubble();
|
||||
_panelBubbles = {};
|
||||
_panelActiveLayer = {};
|
||||
}
|
||||
|
||||
// MoA: Runden-Trenner (nur bei mehr als einer Runde sichtbar).
|
||||
function startPanelLayer(data) {
|
||||
if (!data || !data.total || data.total <= 1) return;
|
||||
var div = document.createElement('div');
|
||||
div.className = 'panel-layer-divider';
|
||||
div.innerHTML = '<span><i class="fa fa-clone"></i> Runde ' + data.layer + ' / ' + data.total + '</span>';
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function addPanelMemberBubble(data) {
|
||||
var layer = data.layer || 1;
|
||||
var bubble = document.createElement('div');
|
||||
bubble.className = 'msg review-bubble panel-bubble streamed';
|
||||
var model = (typeof prettyModelName === 'function') ? prettyModelName(data.model) : data.model;
|
||||
@@ -232,7 +249,8 @@ function addPanelMemberBubble(data) {
|
||||
bubble._segTextStr = ''; // dessen akkumulierter Text
|
||||
bubble._tool = null; // aktuell offene Tool-Box (CLI: Spinner→Füllen)
|
||||
bubble._fullText = ''; // gesamter gestreamter Text (für Finalize-Fallback)
|
||||
_panelBubbles[data.index] = bubble;
|
||||
_panelActiveLayer[data.index] = layer; // ab jetzt zeigen Events für diesen index auf diese Runde
|
||||
_panelBubbles[_panelKey(data.index, layer)] = bubble;
|
||||
chatEl().appendChild(bubble);
|
||||
scrollDown();
|
||||
}
|
||||
@@ -242,7 +260,7 @@ function _panelClearSpinner(bubble) {
|
||||
}
|
||||
|
||||
function appendPanelMemberText(data) {
|
||||
var bubble = _panelBubbles[data.index];
|
||||
var bubble = _panelGetBubble(data.index);
|
||||
if (!bubble || !data.text) return;
|
||||
_panelClearSpinner(bubble);
|
||||
// Neues Text-Segment beginnen, wenn keins offen ist (z.B. direkt nach einer Tool-Box)
|
||||
@@ -259,7 +277,7 @@ function appendPanelMemberText(data) {
|
||||
}
|
||||
|
||||
function addPanelMemberToolUse(data) {
|
||||
var bubble = _panelBubbles[data.index];
|
||||
var bubble = _panelGetBubble(data.index);
|
||||
if (!bubble) return;
|
||||
_panelClearSpinner(bubble);
|
||||
bubble._segText = null; // Text-Segment schließen → Folge-Text kommt UNTER die Tool-Box
|
||||
@@ -275,7 +293,7 @@ function addPanelMemberToolUse(data) {
|
||||
}
|
||||
|
||||
function finishPanelMemberTool(data) {
|
||||
var bubble = _panelBubbles[data.index];
|
||||
var bubble = _panelGetBubble(data.index);
|
||||
if (!bubble) return;
|
||||
_panelClearSpinner(bubble);
|
||||
bubble._segText = null; // nach Tool-Result neuen Text-Block beginnen
|
||||
@@ -295,7 +313,7 @@ function finishPanelMemberTool(data) {
|
||||
}
|
||||
|
||||
function finalizePanelMember(data) {
|
||||
var bubble = _panelBubbles[data.index];
|
||||
var bubble = _panelGetBubble(data.index);
|
||||
if (!bubble) return;
|
||||
_panelClearSpinner(bubble);
|
||||
// Wenn LIVE nichts gestreamt wurde (z.B. OpenAI-Member = one-shot): finalen Text rendern.
|
||||
@@ -308,10 +326,11 @@ function finalizePanelMember(data) {
|
||||
}
|
||||
bubble._segText.innerHTML = renderMarkdown(text);
|
||||
}
|
||||
delete _panelBubbles[data.index];
|
||||
delete _panelBubbles[_panelKey(data.index, _panelActiveLayer[data.index])];
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function endPanel(data) {
|
||||
_panelBubbles = {};
|
||||
_panelActiveLayer = {};
|
||||
}
|
||||
Reference in new issue
Block a user