feat: add Mixture of Agents (MoA) for arbiter verdicts
When MOA_ENABLED=true, arbiter queries multiple LLM models in parallel via OpenAI-compatible API, then an aggregator model synthesizes the final verdict from all opinions. Falls back to single-agent arbiter when MoA is disabled. Config: MOA_BASE_URL, MOA_API_KEY, MOA_REFERENCE_MODELS, MOA_AGGREGATOR_MODEL
This commit is contained in:
@@ -41,9 +41,14 @@ import {
|
||||
import {
|
||||
CODEX_REVIEW_SERVICE_ID,
|
||||
SERVICE_SESSION_SCOPE,
|
||||
TIMEZONE,
|
||||
isClaudeService,
|
||||
getRoleModelConfig,
|
||||
getMoaConfig,
|
||||
} from './config.js';
|
||||
import { buildArbiterContextPrompt } from './arbiter-context.js';
|
||||
import { runMoaArbiter } from './moa.js';
|
||||
import { readArbiterPrompt } from './platform-prompts.js';
|
||||
import {
|
||||
activateCodexFailover,
|
||||
getEffectiveChannelLease,
|
||||
@@ -323,6 +328,75 @@ export async function runAgentForGroup(
|
||||
return 'success';
|
||||
}
|
||||
|
||||
// ── MoA arbiter path ────────────────────────────────────────────
|
||||
// When MoA is enabled and we're in arbiter mode, query multiple
|
||||
// models in parallel instead of spawning a single agent process.
|
||||
const moaConfig = getMoaConfig();
|
||||
if (arbiterMode && moaConfig.enabled && pairedExecutionContext) {
|
||||
logger.info(
|
||||
{
|
||||
chatJid,
|
||||
group: group.name,
|
||||
runId,
|
||||
referenceModels: moaConfig.referenceModels.map((m) => m.model),
|
||||
aggregator: moaConfig.aggregator.model,
|
||||
},
|
||||
'Running MoA arbiter instead of single agent',
|
||||
);
|
||||
|
||||
const systemPrompt =
|
||||
readArbiterPrompt(process.cwd()) || 'You are an arbiter.';
|
||||
const contextPrompt = buildArbiterContextPrompt({
|
||||
chatJid,
|
||||
taskId: pairedExecutionContext.task.id,
|
||||
roundTripCount: pairedExecutionContext.task.round_trip_count,
|
||||
timezone: TIMEZONE,
|
||||
});
|
||||
|
||||
try {
|
||||
const moaResult = await runMoaArbiter({
|
||||
config: moaConfig,
|
||||
systemPrompt,
|
||||
contextPrompt,
|
||||
});
|
||||
|
||||
pairedExecutionSummary = moaResult.verdict.slice(0, 500);
|
||||
pairedExecutionStatus = 'succeeded';
|
||||
|
||||
// Build display text with reference model opinions
|
||||
const referenceSection = moaResult.referenceResponses
|
||||
.filter((r) => !r.error)
|
||||
.map((r) => `**${r.model}**: ${r.response.split('\n')[0]}`)
|
||||
.join('\n');
|
||||
const displayText = referenceSection
|
||||
? `${moaResult.verdict}\n\n---\n*MoA references: ${moaResult.referenceResponses.filter((r) => !r.error).length} models queried*\n${referenceSection}`
|
||||
: moaResult.verdict;
|
||||
|
||||
await onOutput?.({
|
||||
status: 'success',
|
||||
result: null,
|
||||
output: { visibility: 'public', text: displayText },
|
||||
phase: 'final',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ chatJid, group: group.name, runId, error },
|
||||
'MoA arbiter failed',
|
||||
);
|
||||
pairedExecutionSummary = 'ESCALATE\nMoA arbiter failed';
|
||||
pairedExecutionStatus = 'failed';
|
||||
}
|
||||
|
||||
completePairedExecutionContext({
|
||||
taskId: pairedExecutionContext.task.id,
|
||||
role: 'arbiter',
|
||||
status: pairedExecutionStatus,
|
||||
summary: pairedExecutionSummary,
|
||||
});
|
||||
pairedExecutionCompleted = true;
|
||||
return pairedExecutionStatus === 'succeeded' ? 'success' : 'error';
|
||||
}
|
||||
|
||||
const runAttempt = async (
|
||||
provider: string,
|
||||
): Promise<{
|
||||
|
||||
Reference in New Issue
Block a user