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

312 lines
9.2 KiB
TypeScript

import type { Logger } from 'pino';
import { createEvaluatedOutputHandler } from './agent-attempt.js';
import type { AttemptStreamedTrigger } from './agent-attempt-retry.js';
import { runAgentProcess, type AgentOutput } from './agent-runner.js';
import { getCodexAccountCount } from './codex-token-rotation.js';
import type { PreparedPairedExecutionContext } from './paired-execution-context.js';
import {
shouldResetCodexSessionOnAgentFailure,
shouldResetSessionOnAgentFailure,
} from './session-recovery.js';
import type { AgentType, RegisteredGroup, RoomRoleContext } from './types.js';
export interface MessageAgentAttempt {
output?: AgentOutput;
error?: unknown;
sawOutput: boolean;
sawVisibleOutput: boolean;
sawSuccessNullResultWithoutOutput: boolean;
retryableSessionFailureDetected: boolean;
resetSessionRequested: boolean;
streamedTriggerReason?: AttemptStreamedTrigger;
}
interface AgentInput {
prompt: string;
sessionId?: string;
memoryBriefing?: string;
groupFolder: string;
chatJid: string;
runId: string;
isMain: boolean;
assistantName: string;
roomRoleContext?: RoomRoleContext;
}
export async function runMessageAgentAttempt(args: {
provider: 'claude' | 'codex';
currentSessionId: string | undefined;
isClaudeCodeAgent: boolean;
canRetryClaudeCredentials: boolean;
shouldPersistSession: boolean;
effectiveGroup: RegisteredGroup;
agentInput: AgentInput;
activeRole: string;
effectiveServiceId: string;
effectiveAgentType: AgentType;
sessionFolder: string;
roomRoleContext?: RoomRoleContext;
pairedExecutionContext?: PreparedPairedExecutionContext;
fallbackWorkspaceDir?: string | null;
onPersistSession: (sessionId: string) => void;
registerProcess: (
proc: Parameters<typeof runAgentProcess>[2] extends (
proc: infer TProc,
processName: infer TProcessName,
ipcDir: infer TIpcDir,
) => unknown
? TProc
: never,
processName: string,
ipcDir?: string,
) => void;
onOutput?: (output: AgentOutput) => Promise<void>;
pairedExecutionLifecycle: {
updateSummary(args: {
outputText?: string | null;
errorText?: string | null;
}): void;
recordFinalOutputBeforeDelivery(outputText: string): boolean;
};
log: Logger;
}): Promise<MessageAgentAttempt> {
const {
provider,
currentSessionId,
isClaudeCodeAgent,
canRetryClaudeCredentials,
shouldPersistSession,
effectiveGroup,
agentInput,
activeRole,
effectiveServiceId,
effectiveAgentType,
sessionFolder,
roomRoleContext,
pairedExecutionContext,
fallbackWorkspaceDir,
onPersistSession,
registerProcess,
onOutput,
pairedExecutionLifecycle,
log,
} = args;
const attemptSessionId = currentSessionId;
let resetSessionRequested = false;
const streamedOutputHandler = createEvaluatedOutputHandler({
agentType: isClaudeCodeAgent ? 'claude-code' : 'codex',
provider,
evaluationOptions: {
suppressClaudeAuthErrorOutput: provider === 'claude',
trackSuccessNullResult: true,
shortCircuitTriggeredErrors:
provider === 'claude'
? canRetryClaudeCredentials
: getCodexAccountCount() > 1,
},
onEvaluatedOutput: async ({
output,
outputText,
structuredOutput,
evaluation,
}) => {
const outputPhase = output.phase ?? 'final';
if (outputPhase !== 'final') {
log.info(
{
provider,
outputPhase,
outputStatus: output.status,
visibility: structuredOutput?.visibility ?? null,
preview:
outputText && outputText.length > 0
? outputText.slice(0, 160)
: null,
errorPreview:
typeof output.error === 'string' && output.error.length > 0
? output.error.slice(0, 160)
: null,
activeRole,
effectiveServiceId,
effectiveAgentType,
sessionFolder,
resumedSession: attemptSessionId ?? null,
streamedSessionId: output.newSessionId ?? null,
roomRoleServiceId: roomRoleContext?.serviceId ?? null,
roomRole: roomRoleContext?.role ?? null,
pairedTaskId: pairedExecutionContext?.task.id ?? null,
workspaceDir:
pairedExecutionContext?.workspace?.workspace_dir ??
fallbackWorkspaceDir ??
null,
},
'Observed streamed agent activity',
);
}
if (
isClaudeCodeAgent &&
provider === 'claude' &&
shouldResetSessionOnAgentFailure(output)
) {
resetSessionRequested = true;
}
if (
!isClaudeCodeAgent &&
provider === 'codex' &&
shouldResetCodexSessionOnAgentFailure(output)
) {
resetSessionRequested = true;
}
if (
output.newSessionId &&
!resetSessionRequested &&
shouldPersistSession
) {
onPersistSession(output.newSessionId);
}
pairedExecutionLifecycle.updateSummary({
outputText,
errorText: typeof output.error === 'string' ? output.error : null,
});
if (evaluation.newTrigger && outputText && output.status === 'success') {
log.warn(
{
reason: evaluation.newTrigger.reason,
resultPreview: outputText.slice(0, 120),
},
'Detected Claude rotation trigger in successful output',
);
} else if (evaluation.newTrigger && typeof output.error === 'string') {
log.warn(
{
reason: evaluation.newTrigger.reason,
errorPreview: output.error.slice(0, 120),
},
provider === 'claude'
? 'Detected Claude rotation trigger in streamed error output'
: 'Detected Codex rotation trigger in streamed error output',
);
}
if (evaluation.suppressedAuthError) {
log.warn(
{
resultPreview: outputText ? outputText.slice(0, 120) : undefined,
},
'Suppressed Claude 401 auth error from chat output',
);
return;
}
if (evaluation.suppressedRetryableSessionFailure) {
log.warn(
{
resultPreview: outputText
? outputText.slice(0, 160)
: output.error?.slice(0, 160),
},
provider === 'claude'
? 'Suppressed retryable Claude session failure from chat output'
: 'Suppressed retryable Codex session failure from chat output',
);
return;
}
if (!evaluation.shouldForwardOutput) {
return;
}
if (outputText && outputText.length > 0) {
streamedOutputHandler.markVisibleOutput();
}
if (
outputPhase === 'final' &&
output.status === 'success' &&
outputText &&
outputText.length > 0
) {
let finalOutputAccepted = true;
try {
finalOutputAccepted =
pairedExecutionLifecycle.recordFinalOutputBeforeDelivery(
outputText,
);
} catch (err) {
log.warn(
{ pairedTaskId: pairedExecutionContext?.task.id ?? null, err },
'Failed to persist paired turn output and status before delivery',
);
}
if (!finalOutputAccepted) {
return;
}
}
if (onOutput) {
await onOutput(output);
}
},
});
const wrappedOnOutput = async (output: AgentOutput) => {
await streamedOutputHandler.handleOutput(output);
};
const providerLog = log.child({
provider,
agentType: effectiveAgentType,
});
providerLog.info('Using provider');
try {
const output = await runAgentProcess(
effectiveGroup,
{
...agentInput,
sessionId: attemptSessionId,
},
registerProcess,
wrappedOnOutput,
pairedExecutionContext?.envOverrides,
);
if (output.newSessionId && shouldPersistSession) {
onPersistSession(output.newSessionId);
}
providerLog.info(
{
status: output.status,
sawOutput: streamedOutputHandler.getState().sawOutput,
},
`Provider response completed (provider: ${provider})`,
);
const streamedState = streamedOutputHandler.getState();
return {
output,
sawOutput: streamedState.sawOutput,
sawVisibleOutput: streamedState.sawVisibleOutput,
sawSuccessNullResultWithoutOutput:
streamedState.sawSuccessNullResultWithoutOutput,
retryableSessionFailureDetected:
streamedState.retryableSessionFailureDetected === true,
resetSessionRequested,
streamedTriggerReason: streamedState.streamedTriggerReason,
};
} catch (error) {
const streamedState = streamedOutputHandler.getState();
return {
error,
sawOutput: streamedState.sawOutput,
sawVisibleOutput: streamedState.sawVisibleOutput,
sawSuccessNullResultWithoutOutput:
streamedState.sawSuccessNullResultWithoutOutput,
retryableSessionFailureDetected:
streamedState.retryableSessionFailureDetected === true,
resetSessionRequested,
streamedTriggerReason: streamedState.streamedTriggerReason,
};
}
}