Files
EJClaw/src/message-agent-executor.ts

267 lines
8.2 KiB
TypeScript

import { AgentOutput } from './agent-runner.js';
import { GroupQueue } from './group-queue.js';
import type { AgentTriggerReason } from './agent-error-detection.js';
import { getMoaConfig } from './config.js';
import { createPairedExecutionLifecycle } from './message-agent-executor-paired.js';
import type { PairedTurnIdentity } from './paired-turn-identity.js';
import { executeMessageAgentAttemptLifecycle } from './message-agent-executor-lifecycle.js';
import { runMessageAgentAttempt } from './message-agent-executor-attempt-runner.js';
import { handoffMessageAgentExecutionToCodex } from './message-agent-executor-handoff.js';
import { prepareMessageAgentExecutionTarget } from './message-agent-executor-target.js';
import { collectMoaReferences, formatMoaReferencesForPrompt } from './moa.js';
import { readArbiterPrompt } from './platform-prompts.js';
import { shouldRetryFreshSessionOnAgentFailure } from './session-recovery.js';
import type { AgentType, PairedRoomRole, RegisteredGroup } from './types.js';
// ── Main executor ─────────────────────────────────────────────────
export interface MessageAgentExecutorDeps {
assistantName: string;
queue: Pick<GroupQueue, 'registerProcess' | 'enqueueMessageCheck'> &
Partial<Pick<GroupQueue, 'getDirectTerminalDeliveryForRun'>>;
getRoomBindings: () => Record<string, RegisteredGroup>;
getSessions: () => Record<string, string>;
persistSession: (groupFolder: string, sessionId: string) => void;
clearSession: (groupFolder: string) => void;
}
export async function runAgentForGroup(
deps: MessageAgentExecutorDeps,
args: {
group: RegisteredGroup;
prompt: string;
chatJid: string;
runId: string;
startSeq?: number | null;
endSeq?: number | null;
hasHumanMessage?: boolean;
forcedRole?: PairedRoomRole;
forcedAgentType?: AgentType;
pairedTurnIdentity?: PairedTurnIdentity;
onOutput?: (output: AgentOutput) => Promise<void>;
},
): Promise<'success' | 'error'> {
const { group, prompt, chatJid, runId, startSeq, endSeq, onOutput } = args;
const isMain = group.isMain === true;
const preparedExecution = await prepareMessageAgentExecutionTarget(deps, {
group,
prompt,
chatJid,
runId,
startSeq,
endSeq,
hasHumanMessage: args.hasHumanMessage,
forcedRole: args.forcedRole,
forcedAgentType: args.forcedAgentType,
pairedTurnIdentity: args.pairedTurnIdentity,
});
const {
currentLease,
activeRole,
effectiveServiceId,
effectiveAgentType,
sessionFolder,
arbiterMode,
effectiveGroup,
isClaudeCodeAgent,
forceFreshClaudeReviewerSession,
shouldPersistSession,
provider,
memoryBriefing,
canRetryClaudeCredentials,
roomRoleContext,
pairedExecutionContext,
runtimePairedTurnIdentity,
log,
clearRoleSdkSessions,
} = preparedExecution;
let currentSessionId = preparedExecution.currentSessionId;
if (forceFreshClaudeReviewerSession) {
clearRoleSdkSessions();
}
// ── MoA prompt enrichment ─────────────────────────────────────
// When MoA is enabled and we're in arbiter mode, query external API
// models (Kimi, GLM, etc.) in parallel for their opinions, then inject
// those opinions into the arbiter's prompt. The SDK-based arbiter
// agent naturally aggregates all perspectives.
let moaEnrichedPrompt = prompt;
const moaConfig = getMoaConfig();
if (arbiterMode && moaConfig.enabled && pairedExecutionContext) {
log.info(
{
models: moaConfig.referenceModels.map((m) => m.name),
},
'MoA: collecting reference opinions before arbiter',
);
const systemPrompt =
readArbiterPrompt(process.cwd()) || 'You are an arbiter.';
try {
const references = await collectMoaReferences({
config: moaConfig,
systemPrompt,
contextPrompt: prompt,
});
const moaSection = formatMoaReferencesForPrompt(references);
if (moaSection) {
moaEnrichedPrompt = prompt + '\n' + moaSection;
log.info(
{
successCount: references.filter((r) => !r.error).length,
totalCount: references.length,
},
'MoA: injected reference opinions into arbiter prompt',
);
}
} catch (err) {
log.warn(
{
err,
models: moaConfig.referenceModels.map((m) => m.name),
},
'MoA: failed to collect reference opinions; continuing without enrichment',
);
}
}
const effectivePrompt = moaEnrichedPrompt;
const pairedExecutionLifecycle = createPairedExecutionLifecycle({
pairedExecutionContext,
pairedTurnIdentity: runtimePairedTurnIdentity,
completedRole: runtimePairedTurnIdentity?.role ?? activeRole,
chatJid,
runId,
enqueueMessageCheck: () => deps.queue.enqueueMessageCheck(chatJid),
getDirectTerminalDeliveryText: () =>
deps.queue.getDirectTerminalDeliveryForRun?.(
chatJid,
runId,
runtimePairedTurnIdentity?.role ?? activeRole,
) ?? null,
onOutput,
log,
});
const hasDirectTerminalDelivery = (): boolean =>
!!deps.queue.getDirectTerminalDeliveryForRun?.(
chatJid,
runId,
runtimePairedTurnIdentity?.role ?? activeRole,
);
const maybeHandoffToCodex = (
reason: AgentTriggerReason,
sawVisibleOutput: boolean,
): boolean =>
isClaudeCodeAgent &&
handoffMessageAgentExecutionToCodex({
activeRole,
effectiveAgentType,
hasReviewer: currentLease.reviewer_agent_type !== null,
reason,
sawVisibleOutput,
prompt,
startSeq,
endSeq,
chatJid,
group,
pairedTurnIdentity: runtimePairedTurnIdentity,
markDelegated: () => pairedExecutionLifecycle.markDelegated(),
log,
});
const agentInput = {
prompt: effectivePrompt,
sessionId: currentSessionId,
memoryBriefing,
groupFolder: group.folder,
chatJid,
runId,
isMain,
assistantName: deps.assistantName,
roomRoleContext,
};
if (pairedExecutionContext?.blockMessage) {
pairedExecutionLifecycle.updateSummary({
outputText: pairedExecutionContext.blockMessage,
});
log.warn(
{
roomRoleServiceId: roomRoleContext?.serviceId,
roomRole: roomRoleContext?.role,
},
'Blocked paired execution before runner start',
);
await onOutput?.({
status: 'success',
result: null,
output: {
visibility: 'public',
text: pairedExecutionContext.blockMessage,
},
phase: 'final',
});
pairedExecutionLifecycle.completeImmediately({ status: 'failed' });
return 'success';
}
const runAttempt = (currentProvider: 'claude' | 'codex') =>
runMessageAgentAttempt({
provider: currentProvider,
currentSessionId,
isClaudeCodeAgent,
canRetryClaudeCredentials,
shouldPersistSession,
effectiveGroup,
agentInput,
activeRole,
effectiveServiceId,
effectiveAgentType,
sessionFolder,
roomRoleContext,
pairedExecutionContext,
fallbackWorkspaceDir: group.workDir ?? null,
onPersistSession: (sessionId) => {
deps.persistSession(sessionFolder, sessionId);
currentSessionId = sessionId;
},
registerProcess: (proc, processName, ipcDir) =>
deps.queue.registerProcess(chatJid, proc, processName, ipcDir),
onOutput,
pairedExecutionLifecycle,
log,
});
try {
return await executeMessageAgentAttemptLifecycle({
provider,
runAttempt,
isClaudeCodeAgent,
canRetryClaudeCredentials,
clearStoredSession: () => {
deps.clearSession(sessionFolder);
currentSessionId = undefined;
},
clearRoleSdkSessions,
sessionFolder,
maybeHandoffToCodex,
hasDirectTerminalDelivery,
pairedExecutionLifecycle,
shouldRetryFreshSessionOnAgentFailure,
rotationLogContext: {
chatJid,
group: group.name,
groupFolder: group.folder,
runId,
},
log,
});
} finally {
await pairedExecutionLifecycle.asyncFinalize();
}
}