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
+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