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:
MelekiandClaude Opus 4.8 committed 2026-06-18 13:46:28 +02:00
1 parent f21ed9ff9f
commit b7ff928340
15 files changed
+239 -65

No files matched your search

+9 -1
View File
@@ -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;
+5
View File
@@ -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
View File
@@ -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 = {};
}