Add multi-model panel with Fusion mode

Panel agents answer the user prompt in parallel (read-only, non-streaming);
their responses are merged into the worker's last user message as a
<panel_responses> block, and the session agent synthesizes one final
streamed answer. The RunPanel engine is built layer-ready for future MoA.

- New session fields: multi_model_mode (off/fusion), panel_agent_ids (CSV)
  with idempotent column migration
- New partial Services/AnthropicService.Panel.cs holding the RunPanel engine
- Provider-agnostic members: Anthropic RunNonStreamingToolLoop / OpenAI
  CompleteText / CLI plan-mode; all forced read-only, persisted with Tag=panel
- SSE events: panel_start, panel_member_start, panel_member_done, panel_done
- Frontend: multi-agent picker, live panel bubbles, history rendering
- Graceful degradation: panel errors fall back to worker-only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-18 12:55:06 +02:00
1 parent b23eba32b4
commit 268f11fa6d
20 files changed
+647 -35

No files matched your search

+20
View File
@@ -230,6 +230,26 @@ public static class ChatEndpoint
try
{
// ── Multi-Modell-Panel (Fusion) ──
// Vor dem Worker: Panel-Agenten parallel antworten lassen und ihre Antworten dem
// Worker als Synthese-Block in die letzte User-Message einmergen. Der Worker streamt
// 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)
{
try
{
await anthropic.RunPanel(res, ctx.PanelAgents, messages, ctx.ChainId, sessionId, userApiKey?.ApiKey, workCt, sseCt);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
// Panel-Fehler darf den Worker nicht killen → ohne Synthese normal weiter.
Console.WriteLine($"[Panel] Fehler, fahre ohne Synthese fort: {ex.Message}");
}
}
if (!string.IsNullOrEmpty(ctx.Provider))
{
// OpenAI-kompatibler Provider – Key des Users für diesen Agent verwenden
+3 -1
View File
@@ -146,8 +146,10 @@ public static class SessionEndpoint
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 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 session = await db.CreateSession(userId, name, description, image, imageSmall, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId);
var session = await db.CreateSession(userId, name, description, image, imageSmall, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId, multiModelMode, panelAgentIds);
await ctx.Response.WriteAsJsonAsync(session);
}