mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-06 02:43:33 +00:00
1243 lines
42 KiB
TypeScript
1243 lines
42 KiB
TypeScript
/**
|
|
* Top-level CLI-backed agent runner orchestration.
|
|
*/
|
|
import { setReplyPayloadMetadata, type ReplyPayload } from "../auto-reply/reply-payload.js";
|
|
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|
import { appendExactAssistantMessageToSessionTranscript } from "../config/sessions/transcript.js";
|
|
import { buildGenericCliContextEngineHostSupport } from "../context-engine/host-compat.js";
|
|
import {
|
|
assertAgentRunLifecycleGenerationCurrent,
|
|
captureAgentRunLifecycleGeneration,
|
|
withAgentRunLifecycleGeneration,
|
|
} from "../infra/agent-events.js";
|
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import { buildAgentHookContextChannelFields } from "../plugins/hook-agent-context.js";
|
|
import { resolveBlockMessage } from "../plugins/hook-decision-types.js";
|
|
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
|
import type { CliOutput } from "./cli-output.js";
|
|
import {
|
|
attachCliMessagingDeliveryEvidence,
|
|
getCliMessagingDeliveryEvidence,
|
|
} from "./cli-runner/delivery-evidence.js";
|
|
import { cliBackendLog, formatCliBackendOutputDigest } from "./cli-runner/log.js";
|
|
import {
|
|
loadCliSessionContextEngineMessages,
|
|
loadCliSessionHistoryMessages,
|
|
} from "./cli-runner/session-history.js";
|
|
import type { PreparedCliRunContext, RunCliAgentParams } from "./cli-runner/types.js";
|
|
import { claudeCliSessionTranscriptHasContent as claudeCliSessionTranscriptHasContentImpl } from "./command/attempt-execution.helpers.js";
|
|
import { classifyFailoverReason, isFailoverErrorMessage } from "./embedded-agent-helpers.js";
|
|
import type { EmbeddedAgentRunResult } from "./embedded-agent-runner.js";
|
|
import { waitForDeferredTurnMaintenanceForSession } from "./embedded-agent-runner/context-engine-maintenance.js";
|
|
import { buildEmbeddedRunPayloads } from "./embedded-agent-runner/run/payloads.js";
|
|
import { FailoverError, isFailoverError, resolveFailoverStatus } from "./failover-error.js";
|
|
import {
|
|
awaitAgentEndSideEffects,
|
|
runAgentEndSideEffects,
|
|
} from "./harness/agent-end-side-effects.js";
|
|
import {
|
|
bootstrapHarnessContextEngine,
|
|
finalizeHarnessContextEngineTurn,
|
|
runHarnessContextEngineMaintenance,
|
|
} from "./harness/context-engine-lifecycle.js";
|
|
import { buildAgentHookContext } from "./harness/hook-context.js";
|
|
import { runAgentHarnessBeforeMessageWriteHook } from "./harness/hook-helpers.js";
|
|
import { buildAgentHookConversationMessages } from "./harness/hook-history.js";
|
|
import {
|
|
runAgentHarnessLlmInputHook,
|
|
runAgentHarnessLlmOutputHook,
|
|
} from "./harness/lifecycle-hook-helpers.js";
|
|
import type { AgentMessage } from "./runtime/index.js";
|
|
import { SessionManager } from "./sessions/session-manager.js";
|
|
import { buildAssistantMessage, buildUsageWithNoCost } from "./stream-message-shared.js";
|
|
|
|
const log = createSubsystemLogger("agents/cli-runner");
|
|
|
|
const cliRunnerDeps = {
|
|
claudeCliSessionTranscriptHasContent: claudeCliSessionTranscriptHasContentImpl,
|
|
delay: async (delayMs: number) => {
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, delayMs);
|
|
});
|
|
},
|
|
};
|
|
|
|
/** Overrides top-level CLI runner dependencies for tests. */
|
|
export function setCliRunnerTestDeps(overrides: Partial<typeof cliRunnerDeps>): void {
|
|
Object.assign(cliRunnerDeps, overrides);
|
|
}
|
|
|
|
/** Restores default top-level CLI runner dependencies after tests. */
|
|
export function restoreCliRunnerTestDeps(): void {
|
|
cliRunnerDeps.claudeCliSessionTranscriptHasContent = claudeCliSessionTranscriptHasContentImpl;
|
|
cliRunnerDeps.delay = async (delayMs: number) => {
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, delayMs);
|
|
});
|
|
};
|
|
}
|
|
|
|
function isClaudeCliProvider(provider: string): boolean {
|
|
return provider.trim().toLowerCase() === "claude-cli";
|
|
}
|
|
|
|
function shouldRetryFreshCliSessionAfterFailover(params: {
|
|
error: FailoverError;
|
|
hasHistoryPrompt: boolean;
|
|
}): boolean {
|
|
if (!params.hasHistoryPrompt) {
|
|
return false;
|
|
}
|
|
switch (params.error.reason) {
|
|
case "session_expired":
|
|
return true;
|
|
case "unknown":
|
|
return params.error.code === "cli_unknown_empty_failure";
|
|
case "timeout":
|
|
return params.error.code === "cli_no_output_timeout";
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function formatCliEmptyOutputDiagnostics(output: CliOutput): string | undefined {
|
|
const process = output.diagnostics?.process;
|
|
if (!process) {
|
|
return undefined;
|
|
}
|
|
return [
|
|
`backend=${process.backendId}`,
|
|
`reason=${process.processReason}`,
|
|
`exitCode=${process.exitCode ?? "null"}`,
|
|
`exitSignal=${process.exitSignal ?? "null"}`,
|
|
`durationMs=${process.durationMs}`,
|
|
`stdoutBytes=${process.stdoutBytes}`,
|
|
`stdoutHash=${process.stdoutHash}`,
|
|
`stderrBytes=${process.stderrBytes}`,
|
|
`stderrHash=${process.stderrHash}`,
|
|
`useResume=${process.useResume ? "true" : "false"}`,
|
|
].join(" ");
|
|
}
|
|
|
|
/** Checks whether a Claude CLI session binding has reached its transcript file. */
|
|
export async function isCliBindingFlushed(
|
|
sessionId: string | undefined,
|
|
provider: string | undefined,
|
|
workspaceDir?: string,
|
|
): Promise<boolean> {
|
|
if (!provider || !isClaudeCliProvider(provider)) {
|
|
return true;
|
|
}
|
|
if (!sessionId) {
|
|
return false;
|
|
}
|
|
for (const delayMs of [0, 50, 150]) {
|
|
if (delayMs > 0) {
|
|
await cliRunnerDeps.delay(delayMs);
|
|
}
|
|
if (await cliRunnerDeps.claudeCliSessionTranscriptHasContent({ sessionId, workspaceDir })) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function flushSessionManagerFile(sessionManager: SessionManager): void {
|
|
(sessionManager as unknown as { rewriteFile?: () => void }).rewriteFile?.();
|
|
}
|
|
|
|
function buildHandledReplyPayloads(reply?: ReplyPayload) {
|
|
const normalized = reply ?? { text: SILENT_REPLY_TOKEN };
|
|
return [
|
|
{
|
|
text: normalized.text,
|
|
mediaUrl: normalized.mediaUrl,
|
|
mediaUrls: normalized.mediaUrls,
|
|
replyToId: normalized.replyToId,
|
|
audioAsVoice: normalized.audioAsVoice,
|
|
isError: normalized.isError,
|
|
isReasoning: normalized.isReasoning,
|
|
},
|
|
];
|
|
}
|
|
|
|
function buildCliHookUserMessage(prompt: string): unknown {
|
|
return {
|
|
role: "user",
|
|
content: prompt,
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
function buildCliHookAssistantMessage(params: {
|
|
text: string;
|
|
provider: string;
|
|
model: string;
|
|
usage?: {
|
|
input?: number;
|
|
output?: number;
|
|
cacheRead?: number;
|
|
cacheWrite?: number;
|
|
total?: number;
|
|
};
|
|
}): unknown {
|
|
return {
|
|
role: "assistant",
|
|
content: [{ type: "text", text: params.text }],
|
|
api: "responses",
|
|
provider: params.provider,
|
|
model: params.model,
|
|
...(params.usage ? { usage: params.usage } : {}),
|
|
stopReason: "stop",
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
function isAgentMessage(value: unknown): value is AgentMessage {
|
|
return Boolean(value && typeof value === "object" && "role" in value);
|
|
}
|
|
|
|
function buildCliContextEngineUserMessage(prompt: string): AgentMessage {
|
|
return {
|
|
role: "user",
|
|
content: prompt,
|
|
timestamp: Date.now(),
|
|
} as AgentMessage;
|
|
}
|
|
|
|
function buildCliContextEngineAssistantMessage(params: {
|
|
text: string;
|
|
provider: string;
|
|
model: string;
|
|
usage?: {
|
|
input?: number;
|
|
output?: number;
|
|
cacheRead?: number;
|
|
cacheWrite?: number;
|
|
total?: number;
|
|
};
|
|
}): AgentMessage {
|
|
return buildCliHookAssistantMessage(params) as AgentMessage;
|
|
}
|
|
|
|
type CliAgentEndHookParams = Parameters<typeof runAgentEndSideEffects>[0];
|
|
|
|
function shouldAwaitCliAgentEndHook(params: RunCliAgentParams): boolean {
|
|
return !params.messageChannel && !params.messageProvider;
|
|
}
|
|
|
|
async function runCliAgentEndHook(
|
|
params: RunCliAgentParams,
|
|
hookParams: CliAgentEndHookParams,
|
|
): Promise<void> {
|
|
if (shouldAwaitCliAgentEndHook(params)) {
|
|
await awaitAgentEndSideEffects(hookParams);
|
|
return;
|
|
}
|
|
runAgentEndSideEffects(hookParams);
|
|
}
|
|
|
|
async function persistApprovedCliUserTurnTranscript(params: RunCliAgentParams): Promise<void> {
|
|
if (params.suppressNextUserMessagePersistence === true || !params.userTurnTranscriptRecorder) {
|
|
return;
|
|
}
|
|
|
|
const target = {
|
|
transcriptPath: params.sessionFile,
|
|
sessionId: params.sessionId,
|
|
agentId: params.agentId,
|
|
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
cwd: params.cwd ?? params.workspaceDir,
|
|
...(params.config ? { config: params.config } : {}),
|
|
};
|
|
const persisted = await params.userTurnTranscriptRecorder.persistApproved({ target });
|
|
if (persisted) {
|
|
try {
|
|
const notification = params.onUserMessagePersisted?.(persisted.message);
|
|
if (notification) {
|
|
void Promise.resolve(notification).catch((error: unknown) => {
|
|
log.warn(`CLI user turn persistence notification failed: ${formatErrorMessage(error)}`);
|
|
});
|
|
}
|
|
} catch (error) {
|
|
log.warn(`CLI user turn persistence notification failed: ${formatErrorMessage(error)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function persistCliAssistantTranscript(params: {
|
|
runParams: RunCliAgentParams;
|
|
text: string;
|
|
modelId: string;
|
|
usage?: {
|
|
input?: number;
|
|
output?: number;
|
|
cacheRead?: number;
|
|
cacheWrite?: number;
|
|
total?: number;
|
|
};
|
|
}): Promise<boolean> {
|
|
const { runParams } = params;
|
|
if (!runParams.persistAssistantTranscript || !runParams.sessionKey || !params.text) {
|
|
return false;
|
|
}
|
|
if (runParams.currentInboundEventKind === "room_event") {
|
|
return true;
|
|
}
|
|
try {
|
|
const result = await appendExactAssistantMessageToSessionTranscript({
|
|
sessionKey: runParams.sessionKey,
|
|
agentId: runParams.agentId,
|
|
expectedSessionId: runParams.sessionId,
|
|
storePath: runParams.storePath,
|
|
idempotencyKey: `cli-assistant:${runParams.runId}`,
|
|
config: runParams.config,
|
|
beforeMessageWrite: runAgentHarnessBeforeMessageWriteHook,
|
|
message: buildAssistantMessage({
|
|
model: {
|
|
api: "cli",
|
|
provider: runParams.provider,
|
|
id: params.modelId,
|
|
},
|
|
content: [{ type: "text", text: params.text }],
|
|
stopReason: "stop",
|
|
usage: buildUsageWithNoCost({
|
|
input: params.usage?.input,
|
|
output: params.usage?.output,
|
|
cacheRead: params.usage?.cacheRead,
|
|
cacheWrite: params.usage?.cacheWrite,
|
|
totalTokens: params.usage?.total,
|
|
}),
|
|
}),
|
|
});
|
|
if (!result.ok) {
|
|
log.warn(`CLI assistant transcript persistence skipped: ${result.reason}`);
|
|
return result.code === "blocked" || result.code === "session-rebound";
|
|
}
|
|
return true;
|
|
} catch (error) {
|
|
log.warn(`CLI assistant transcript persistence failed: ${formatErrorMessage(error)}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function finalizeCliContextEngineTurn(params: {
|
|
context: PreparedCliRunContext;
|
|
historyMessages: unknown[];
|
|
assistantText: string;
|
|
output: Awaited<
|
|
ReturnType<typeof import("./cli-runner/execute.runtime.js").executePreparedCliRun>
|
|
>;
|
|
}): Promise<void> {
|
|
const { context } = params;
|
|
if (!context.contextEngine) {
|
|
return;
|
|
}
|
|
|
|
const { params: runParams } = context;
|
|
const prePromptMessages = params.historyMessages.filter(isAgentMessage);
|
|
const turnMessages: AgentMessage[] = [];
|
|
if (context.contextEngineTurnPrompt) {
|
|
turnMessages.push(buildCliContextEngineUserMessage(context.contextEngineTurnPrompt));
|
|
}
|
|
if (params.assistantText) {
|
|
turnMessages.push(
|
|
buildCliContextEngineAssistantMessage({
|
|
text: params.assistantText,
|
|
provider: runParams.provider,
|
|
model: context.modelId,
|
|
usage: params.output.usage,
|
|
}),
|
|
);
|
|
}
|
|
|
|
let deferredTurnMaintenance: Promise<void> | undefined;
|
|
const contextEngineHostSupport = buildGenericCliContextEngineHostSupport({
|
|
backendId: context.backendResolved.id,
|
|
});
|
|
const result = await finalizeHarnessContextEngineTurn({
|
|
contextEngine: context.contextEngine,
|
|
promptError: false,
|
|
aborted: runParams.abortSignal?.aborted === true,
|
|
yieldAborted: false,
|
|
sessionIdUsed: runParams.sessionId,
|
|
sessionKey: runParams.sessionKey,
|
|
sessionFile: runParams.sessionFile,
|
|
isHeartbeat: runParams.bootstrapContextRunKind === "heartbeat",
|
|
messagesSnapshot: [...prePromptMessages, ...turnMessages],
|
|
prePromptMessageCount: prePromptMessages.length,
|
|
config: context.contextEngineConfig,
|
|
contextEngineHostSupport,
|
|
providerId: runParams.provider,
|
|
modelId: context.modelId,
|
|
runMaintenance: async (maintenanceParams) =>
|
|
await runHarnessContextEngineMaintenance({
|
|
...maintenanceParams,
|
|
onDeferredMaintenance: (promise) => {
|
|
deferredTurnMaintenance = promise;
|
|
},
|
|
}),
|
|
warn: (message) => log.warn(message),
|
|
});
|
|
if (result.postTurnFinalizationSucceeded && deferredTurnMaintenance) {
|
|
context.contextEngineDeferredTurnMaintenance = deferredTurnMaintenance;
|
|
}
|
|
}
|
|
|
|
/** Prepares and runs one CLI-backed agent turn. */
|
|
export function runCliAgent(paramsInput: RunCliAgentParams): Promise<EmbeddedAgentRunResult> {
|
|
const lifecycleGeneration =
|
|
paramsInput.lifecycleGeneration ?? captureAgentRunLifecycleGeneration(paramsInput.runId);
|
|
return withAgentRunLifecycleGeneration(lifecycleGeneration, () =>
|
|
runCliAgentInternal({
|
|
...paramsInput,
|
|
lifecycleGeneration,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async function runCliAgentInternal(params: RunCliAgentParams): Promise<EmbeddedAgentRunResult> {
|
|
assertAgentRunLifecycleGenerationCurrent(params.lifecycleGeneration!);
|
|
// Cron gate must fire before prepareCliRunContext — that call allocates
|
|
// backend resources released only by runPreparedCliAgent's try…finally.
|
|
params.onExecutionStarted?.();
|
|
if (params.trigger === "cron") {
|
|
const startedAt = Date.now();
|
|
const hookRunner = getGlobalHookRunner();
|
|
if (hookRunner?.hasHooks("before_agent_reply")) {
|
|
const hookContext = {
|
|
runId: params.runId,
|
|
jobId: params.jobId,
|
|
agentId: params.agentId,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionId,
|
|
workspaceDir: params.workspaceDir,
|
|
trigger: params.trigger,
|
|
...buildAgentHookContextChannelFields(params),
|
|
} as const;
|
|
params.onExecutionPhase?.({
|
|
phase: "before_agent_reply",
|
|
provider: params.provider,
|
|
model: params.model ?? "",
|
|
});
|
|
const hookResult = await hookRunner.runBeforeAgentReply(
|
|
{ cleanedBody: params.prompt },
|
|
hookContext,
|
|
);
|
|
if (hookResult?.handled) {
|
|
const finalText = hookResult.reply?.text ?? SILENT_REPLY_TOKEN;
|
|
cliBackendLog.info(
|
|
`cli synthetic turn: provider=${params.provider} model=<synthetic> requestedModel=${params.model ?? ""} durationMs=${Date.now() - startedAt} ${formatCliBackendOutputDigest(finalText)}`,
|
|
);
|
|
return {
|
|
payloads: buildHandledReplyPayloads(hookResult.reply),
|
|
meta: {
|
|
durationMs: Date.now() - startedAt,
|
|
agentMeta: {
|
|
sessionId: params.sessionId,
|
|
provider: params.provider,
|
|
model: params.model ?? "",
|
|
},
|
|
finalAssistantVisibleText: finalText,
|
|
finalAssistantRawText: finalText,
|
|
},
|
|
};
|
|
}
|
|
params.onExecutionPhase?.({
|
|
phase: "runtime_plugins",
|
|
provider: params.provider,
|
|
model: params.model ?? "",
|
|
});
|
|
}
|
|
}
|
|
const { prepareCliRunContext } = await import("./cli-runner/prepare.runtime.js");
|
|
const context = await prepareCliRunContext(params);
|
|
let result: EmbeddedAgentRunResult | undefined;
|
|
let runError: unknown;
|
|
try {
|
|
result = await runPreparedCliAgent(context);
|
|
} catch (error) {
|
|
runError = error;
|
|
}
|
|
let cleanupError: unknown;
|
|
const recordCleanupError = (error: unknown) => {
|
|
cleanupError ??= error;
|
|
};
|
|
if (params.cleanupCliLiveSessionOnRunEnd === true) {
|
|
try {
|
|
const { closeClaudeLiveSessionForContext } =
|
|
await import("./cli-runner/claude-live-session.js");
|
|
await closeClaudeLiveSessionForContext(context);
|
|
} catch (error) {
|
|
recordCleanupError(error);
|
|
}
|
|
}
|
|
if (params.cleanupBundleMcpOnRunEnd === true) {
|
|
try {
|
|
const { closeMcpLoopbackServer } = await import("../gateway/mcp-http.js");
|
|
await closeMcpLoopbackServer();
|
|
} catch (error) {
|
|
recordCleanupError(error);
|
|
}
|
|
}
|
|
if (cleanupError) {
|
|
if (runError || result?.didSendViaMessagingTool === true) {
|
|
log.warn(`cli run cleanup failed after completion: ${formatErrorMessage(cleanupError)}`);
|
|
} else {
|
|
runError =
|
|
cleanupError instanceof Error ? cleanupError : new Error(formatErrorMessage(cleanupError));
|
|
}
|
|
}
|
|
if (runError) {
|
|
throw runError instanceof Error ? runError : new Error(formatErrorMessage(runError));
|
|
}
|
|
return result as EmbeddedAgentRunResult;
|
|
}
|
|
|
|
/** Runs an already-prepared CLI agent context through hooks and execution. */
|
|
export async function runPreparedCliAgent(
|
|
context: PreparedCliRunContext,
|
|
): Promise<EmbeddedAgentRunResult> {
|
|
const { executePreparedCliRun } = await import("./cli-runner/execute.runtime.js");
|
|
const { params } = context;
|
|
const hookRunner = getGlobalHookRunner();
|
|
const hasLlmInputHooks = hookRunner?.hasHooks("llm_input") === true;
|
|
const hasLlmOutputHooks = hookRunner?.hasHooks("llm_output") === true;
|
|
const hasAgentEndHooks = hookRunner?.hasHooks("agent_end") === true;
|
|
const hasBeforeAgentRunHooks = hookRunner?.hasHooks("before_agent_run") === true;
|
|
const needsHookHistory = hasLlmInputHooks || hasAgentEndHooks || hasBeforeAgentRunHooks;
|
|
// Prior turn maintenance can rewrite transcript entries after finalization.
|
|
// Reads for the next same-session inference must observe that rewrite.
|
|
await waitForDeferredTurnMaintenanceForSession(params.sessionKey ?? params.sessionId);
|
|
const historyMessages = needsHookHistory
|
|
? await loadCliSessionHistoryMessages({
|
|
sessionId: params.sessionId,
|
|
sessionFile: params.sessionFile,
|
|
sessionKey: params.sessionKey,
|
|
agentId: params.agentId,
|
|
config: params.config,
|
|
})
|
|
: [];
|
|
const llmInputEvent = {
|
|
runId: params.runId,
|
|
sessionId: params.sessionId,
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
systemPrompt: context.systemPrompt,
|
|
prompt: params.prompt,
|
|
historyMessages,
|
|
imagesCount: params.images?.length ?? 0,
|
|
} as const;
|
|
const hookContext = {
|
|
runId: params.runId,
|
|
jobId: params.jobId,
|
|
agentId: params.agentId,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionId,
|
|
workspaceDir: params.workspaceDir,
|
|
trigger: params.trigger,
|
|
...(params.config ? { config: params.config } : {}),
|
|
...(context.contextWindowInfo?.tokens
|
|
? { contextTokenBudget: context.contextWindowInfo.tokens }
|
|
: {}),
|
|
...(context.contextWindowInfo?.source
|
|
? { contextWindowSource: context.contextWindowInfo.source }
|
|
: {}),
|
|
...(context.contextWindowInfo?.referenceTokens
|
|
? { contextWindowReferenceTokens: context.contextWindowInfo.referenceTokens }
|
|
: {}),
|
|
...buildAgentHookContextChannelFields(params),
|
|
} as const;
|
|
|
|
const buildAgentEndMessages = (lastAssistant?: unknown): unknown[] => [
|
|
...buildAgentHookConversationMessages({
|
|
historyMessages,
|
|
currentTurnMessages: [
|
|
buildCliHookUserMessage(params.prompt),
|
|
...(lastAssistant ? [lastAssistant] : []),
|
|
],
|
|
}),
|
|
];
|
|
|
|
const buildFailedAgentEndEvent = (error: string) => ({
|
|
messages: buildAgentEndMessages(),
|
|
success: false,
|
|
error,
|
|
durationMs: Date.now() - context.started,
|
|
});
|
|
|
|
const buildBlockedAgentEndEvent = (message: string) => ({
|
|
messages: buildAgentHookConversationMessages({
|
|
historyMessages,
|
|
currentTurnMessages: [buildCliHookUserMessage(message)],
|
|
}),
|
|
success: false,
|
|
error: message,
|
|
durationMs: Date.now() - context.started,
|
|
});
|
|
|
|
const buildBlockedBeforeAgentRunResult = (message: string): EmbeddedAgentRunResult => ({
|
|
payloads: [{ text: message, isError: true }],
|
|
meta: {
|
|
durationMs: Date.now() - context.started,
|
|
finalAssistantVisibleText: message,
|
|
finalAssistantRawText: message,
|
|
livenessState: "blocked",
|
|
error: {
|
|
kind: "hook_block",
|
|
message,
|
|
},
|
|
systemPromptReport: context.systemPromptReport,
|
|
executionTrace: {
|
|
winnerProvider: params.provider,
|
|
winnerModel: context.modelId,
|
|
attempts: [
|
|
{
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
result: "error",
|
|
reason: "before_agent_run blocked the run",
|
|
},
|
|
],
|
|
fallbackUsed: false,
|
|
runner: "cli",
|
|
},
|
|
requestShaping: {
|
|
...(params.thinkLevel ? { thinking: params.thinkLevel } : {}),
|
|
...(context.effectiveAuthProfileId ? { authMode: "auth-profile" } : {}),
|
|
},
|
|
completion: {
|
|
finishReason: "blocked",
|
|
stopReason: "blocked",
|
|
refusal: true,
|
|
},
|
|
agentMeta: {
|
|
sessionId: params.sessionId ?? "",
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
},
|
|
},
|
|
});
|
|
|
|
let deliveredMessagingSideEffect = false;
|
|
const buildCliSourceReplyMirrorPayloads = (
|
|
evidence: Pick<
|
|
CliOutput,
|
|
| "didSendViaMessagingTool"
|
|
| "didDeliverSourceReplyViaMessageTool"
|
|
| "messagingToolSourceReplyPayloads"
|
|
>,
|
|
): ReplyPayload[] => {
|
|
return buildEmbeddedRunPayloads({
|
|
assistantTexts: [],
|
|
toolMetas: [],
|
|
lastAssistant: undefined,
|
|
inlineToolResultsAllowed: false,
|
|
sessionKey: params.sessionKey ?? "",
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
didSendViaMessagingTool: evidence.didSendViaMessagingTool,
|
|
didDeliverSourceReplyViaMessageTool: evidence.didDeliverSourceReplyViaMessageTool,
|
|
messagingToolSourceReplyPayloads: evidence.messagingToolSourceReplyPayloads,
|
|
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
|
agentId: params.agentId,
|
|
runId: params.runId,
|
|
});
|
|
};
|
|
|
|
const resolveCliSourceReplyMirror = (
|
|
evidence: Pick<
|
|
CliOutput,
|
|
| "didSendViaMessagingTool"
|
|
| "didDeliverSourceReplyViaMessageTool"
|
|
| "messagingToolSourceReplyPayloads"
|
|
>,
|
|
) => {
|
|
const payloads = buildCliSourceReplyMirrorPayloads(evidence);
|
|
const delivered =
|
|
payloads.length > 0 ||
|
|
(params.sourceReplyDeliveryMode === "message_tool_only" &&
|
|
evidence.didDeliverSourceReplyViaMessageTool === true);
|
|
const visibleText =
|
|
payloads
|
|
.map((payload) => payload.text?.trim() ?? "")
|
|
.filter(Boolean)
|
|
.join("\n\n") || undefined;
|
|
return { payloads, delivered, visibleText };
|
|
};
|
|
|
|
const buildDeliveredFailureResult = (
|
|
error: unknown,
|
|
evidence: NonNullable<ReturnType<typeof getCliMessagingDeliveryEvidence>>,
|
|
): EmbeddedAgentRunResult => {
|
|
const message = formatErrorMessage(error);
|
|
const { payloads } = resolveCliSourceReplyMirror(evidence);
|
|
deliveredMessagingSideEffect = true;
|
|
return {
|
|
...(payloads.length > 0 ? { payloads } : {}),
|
|
meta: {
|
|
durationMs: Date.now() - context.started,
|
|
systemPromptReport: context.systemPromptReport,
|
|
stopReason: "error",
|
|
executionTrace: {
|
|
winnerProvider: params.provider,
|
|
winnerModel: context.modelId,
|
|
attempts: [
|
|
{
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
result: "error",
|
|
reason: message,
|
|
},
|
|
],
|
|
fallbackUsed: false,
|
|
runner: "cli",
|
|
},
|
|
requestShaping: {
|
|
...(params.thinkLevel ? { thinking: params.thinkLevel } : {}),
|
|
...(context.effectiveAuthProfileId ? { authMode: "auth-profile" } : {}),
|
|
},
|
|
completion: {
|
|
finishReason: "error",
|
|
stopReason: "error",
|
|
refusal: false,
|
|
},
|
|
agentMeta: {
|
|
sessionId: "",
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
...(context.reusableCliSession.sessionId ? { clearCliSessionBinding: true } : {}),
|
|
},
|
|
},
|
|
didSendViaMessagingTool: true,
|
|
...(evidence.didDeliverSourceReplyViaMessageTool
|
|
? { didDeliverSourceReplyViaMessageTool: true }
|
|
: {}),
|
|
...(evidence.messagingToolSentTexts?.length
|
|
? { messagingToolSentTexts: evidence.messagingToolSentTexts }
|
|
: {}),
|
|
...(evidence.messagingToolSentMediaUrls?.length
|
|
? { messagingToolSentMediaUrls: evidence.messagingToolSentMediaUrls }
|
|
: {}),
|
|
...(evidence.messagingToolSentTargets?.length
|
|
? { messagingToolSentTargets: evidence.messagingToolSentTargets }
|
|
: {}),
|
|
...(evidence.messagingToolSourceReplyPayloads?.length
|
|
? { messagingToolSourceReplyPayloads: evidence.messagingToolSourceReplyPayloads }
|
|
: {}),
|
|
};
|
|
};
|
|
|
|
const persistBlockedBeforeAgentRun = async (block: {
|
|
message: string;
|
|
pluginId: string;
|
|
}): Promise<void> => {
|
|
try {
|
|
const nowMs = Date.now();
|
|
const sessionManager = SessionManager.open(params.sessionFile);
|
|
sessionManager.appendMessage({
|
|
role: "user",
|
|
content: [{ type: "text", text: block.message }],
|
|
timestamp: nowMs,
|
|
idempotencyKey: `hook-block:before_agent_run:user:${params.runId}`,
|
|
__openclaw: {
|
|
beforeAgentRunBlocked: {
|
|
blockedBy: block.pluginId,
|
|
blockedAt: nowMs,
|
|
},
|
|
},
|
|
} as Parameters<typeof sessionManager.appendMessage>[0]);
|
|
flushSessionManagerFile(sessionManager);
|
|
} catch (err) {
|
|
log.warn(
|
|
`before_agent_run block: failed to persist redacted CLI user message: ${formatErrorMessage(
|
|
err,
|
|
)}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
const toCliRunFailure = (error: unknown): never => {
|
|
if (isFailoverError(error)) {
|
|
throw error;
|
|
}
|
|
const message = formatErrorMessage(error);
|
|
if (isFailoverErrorMessage(message, { provider: params.provider })) {
|
|
const reason = classifyFailoverReason(message, { provider: params.provider }) ?? "unknown";
|
|
const status = resolveFailoverStatus(reason);
|
|
throw new FailoverError(message, {
|
|
reason,
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
sessionId: params.sessionId,
|
|
lane: params.lane,
|
|
status,
|
|
});
|
|
}
|
|
throw error;
|
|
};
|
|
|
|
const executeCliAttempt = async (cliSessionIdToUse?: string, timeoutMs = params.timeoutMs) => {
|
|
const attemptContext =
|
|
timeoutMs === params.timeoutMs
|
|
? context
|
|
: {
|
|
...context,
|
|
params: {
|
|
...context.params,
|
|
timeoutMs,
|
|
},
|
|
};
|
|
const output = await executePreparedCliRun(attemptContext, cliSessionIdToUse);
|
|
const sourceReplyMirror = resolveCliSourceReplyMirror(output);
|
|
const assistantText = sourceReplyMirror.delivered
|
|
? (sourceReplyMirror.visibleText ?? "")
|
|
: output.text.trim();
|
|
if (
|
|
!assistantText &&
|
|
!output.didSendViaMessagingTool &&
|
|
params.allowEmptyAssistantReplyAsSilent !== true
|
|
) {
|
|
const emptyOutputDiagnostics = formatCliEmptyOutputDiagnostics(output);
|
|
if (emptyOutputDiagnostics) {
|
|
cliBackendLog.warn(`cli empty response diagnostics: ${emptyOutputDiagnostics}`);
|
|
}
|
|
throw attachCliMessagingDeliveryEvidence(
|
|
new FailoverError("CLI backend returned an empty response.", {
|
|
reason: "empty_response",
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
sessionId: params.sessionId,
|
|
lane: params.lane,
|
|
}),
|
|
output,
|
|
);
|
|
}
|
|
const assistantTexts = assistantText ? [assistantText] : [];
|
|
const lastAssistant =
|
|
assistantText.length > 0
|
|
? buildCliHookAssistantMessage({
|
|
text: assistantText,
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
usage: output.usage,
|
|
})
|
|
: undefined;
|
|
if (assistantText.length > 0 && hasLlmOutputHooks) {
|
|
runAgentHarnessLlmOutputHook({
|
|
event: {
|
|
runId: params.runId,
|
|
sessionId: params.sessionId,
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
...(context.contextWindowInfo?.tokens
|
|
? { contextTokenBudget: context.contextWindowInfo.tokens }
|
|
: {}),
|
|
...(context.contextWindowInfo?.source
|
|
? { contextWindowSource: context.contextWindowInfo.source }
|
|
: {}),
|
|
...(context.contextWindowInfo?.referenceTokens
|
|
? { contextWindowReferenceTokens: context.contextWindowInfo.referenceTokens }
|
|
: {}),
|
|
resolvedRef: `${params.provider}/${context.modelId}`,
|
|
assistantTexts,
|
|
...(lastAssistant ? { lastAssistant } : {}),
|
|
...(output.usage ? { usage: output.usage } : {}),
|
|
},
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
}
|
|
return {
|
|
output,
|
|
assistantText,
|
|
lastAssistant,
|
|
sourceReplyWasDelivered: sourceReplyMirror.delivered,
|
|
};
|
|
};
|
|
|
|
const buildCliRunResult = (resultParams: {
|
|
output: Awaited<ReturnType<typeof executePreparedCliRun>>;
|
|
effectiveCliSessionId?: string;
|
|
bindingFlushOk?: boolean;
|
|
assistantTranscriptOwned?: boolean;
|
|
}): EmbeddedAgentRunResult => {
|
|
const text = resultParams.output.text?.trim();
|
|
const rawText = resultParams.output.rawText?.trim();
|
|
const sourceReplyMirror = resolveCliSourceReplyMirror(resultParams.output);
|
|
const finalAssistantVisibleText = sourceReplyMirror.delivered
|
|
? sourceReplyMirror.visibleText
|
|
: text;
|
|
const payloads =
|
|
sourceReplyMirror.payloads.length > 0
|
|
? sourceReplyMirror.payloads
|
|
: sourceReplyMirror.delivered
|
|
? undefined
|
|
: text
|
|
? [
|
|
resultParams.assistantTranscriptOwned
|
|
? setReplyPayloadMetadata({ text }, { assistantTranscriptOwned: true })
|
|
: { text },
|
|
]
|
|
: params.allowEmptyAssistantReplyAsSilent === true
|
|
? [{ text: SILENT_REPLY_TOKEN }]
|
|
: undefined;
|
|
if (resultParams.output.didSendViaMessagingTool) {
|
|
deliveredMessagingSideEffect = true;
|
|
}
|
|
const unflushedCliSessionId =
|
|
resultParams.effectiveCliSessionId && resultParams.bindingFlushOk === false
|
|
? resultParams.effectiveCliSessionId
|
|
: undefined;
|
|
const persistedCliSessionId = unflushedCliSessionId
|
|
? undefined
|
|
: resultParams.effectiveCliSessionId;
|
|
const agentSessionId = unflushedCliSessionId
|
|
? ""
|
|
: (resultParams.effectiveCliSessionId ?? params.sessionId ?? "");
|
|
const yielded = resultParams.output.yielded === true;
|
|
const stopReason = yielded ? "end_turn" : "completed";
|
|
|
|
return {
|
|
payloads,
|
|
meta: {
|
|
durationMs: Date.now() - context.started,
|
|
...(resultParams.output.finalPromptText
|
|
? { finalPromptText: resultParams.output.finalPromptText }
|
|
: {}),
|
|
...(finalAssistantVisibleText || rawText
|
|
? {
|
|
...(finalAssistantVisibleText ? { finalAssistantVisibleText } : {}),
|
|
...(rawText ? { finalAssistantRawText: rawText } : {}),
|
|
}
|
|
: {}),
|
|
systemPromptReport: context.systemPromptReport,
|
|
...(yielded ? { yielded: true, livenessState: "paused" as const, stopReason } : {}),
|
|
executionTrace: {
|
|
winnerProvider: params.provider,
|
|
winnerModel: context.modelId,
|
|
attempts: [
|
|
{
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
result: "success",
|
|
},
|
|
],
|
|
fallbackUsed: false,
|
|
runner: "cli",
|
|
},
|
|
requestShaping: {
|
|
...(params.thinkLevel ? { thinking: params.thinkLevel } : {}),
|
|
...(context.effectiveAuthProfileId ? { authMode: "auth-profile" } : {}),
|
|
},
|
|
completion: {
|
|
finishReason: yielded ? "end_turn" : "stop",
|
|
stopReason,
|
|
refusal: false,
|
|
},
|
|
agentMeta: {
|
|
sessionId: agentSessionId,
|
|
provider: params.provider,
|
|
model: context.modelId,
|
|
usage: resultParams.output.usage,
|
|
...(resultParams.output.usage ? { lastCallUsage: resultParams.output.usage } : {}),
|
|
...(persistedCliSessionId
|
|
? {
|
|
cliSessionBinding: {
|
|
sessionId: persistedCliSessionId,
|
|
...(context.effectiveAuthProfileId
|
|
? { authProfileId: context.effectiveAuthProfileId }
|
|
: {}),
|
|
...(context.authEpoch ? { authEpoch: context.authEpoch } : {}),
|
|
authEpochVersion: context.authEpochVersion,
|
|
...(context.extraSystemPromptHash
|
|
? { extraSystemPromptHash: context.extraSystemPromptHash }
|
|
: {}),
|
|
...(context.messageToolPolicyHash
|
|
? { messageToolPolicyHash: context.messageToolPolicyHash }
|
|
: {}),
|
|
...(context.promptToolNamesHash
|
|
? { promptToolNamesHash: context.promptToolNamesHash }
|
|
: {}),
|
|
...(context.cwdHash ? { cwdHash: context.cwdHash } : {}),
|
|
...(context.preparedBackend.mcpConfigHash
|
|
? { mcpConfigHash: context.preparedBackend.mcpConfigHash }
|
|
: {}),
|
|
...(context.preparedBackend.mcpResumeHash
|
|
? { mcpResumeHash: context.preparedBackend.mcpResumeHash }
|
|
: {}),
|
|
},
|
|
}
|
|
: {}),
|
|
...(unflushedCliSessionId ? { clearCliSessionBinding: true } : {}),
|
|
},
|
|
},
|
|
...(resultParams.output.didSendViaMessagingTool ? { didSendViaMessagingTool: true } : {}),
|
|
...(resultParams.output.didDeliverSourceReplyViaMessageTool
|
|
? { didDeliverSourceReplyViaMessageTool: true }
|
|
: {}),
|
|
...(resultParams.output.messagingToolSentTexts?.length
|
|
? { messagingToolSentTexts: resultParams.output.messagingToolSentTexts }
|
|
: {}),
|
|
...(resultParams.output.messagingToolSentMediaUrls?.length
|
|
? { messagingToolSentMediaUrls: resultParams.output.messagingToolSentMediaUrls }
|
|
: {}),
|
|
...(resultParams.output.messagingToolSentTargets?.length
|
|
? { messagingToolSentTargets: resultParams.output.messagingToolSentTargets }
|
|
: {}),
|
|
...(resultParams.output.messagingToolSourceReplyPayloads?.length
|
|
? { messagingToolSourceReplyPayloads: resultParams.output.messagingToolSourceReplyPayloads }
|
|
: {}),
|
|
};
|
|
};
|
|
|
|
const executeRun = async (): Promise<EmbeddedAgentRunResult> => {
|
|
await bootstrapHarnessContextEngine({
|
|
hadSessionFile: context.hadSessionFile,
|
|
contextEngine: context.contextEngine,
|
|
sessionId: params.sessionId,
|
|
sessionKey: params.sessionKey,
|
|
sessionFile: params.sessionFile,
|
|
config: context.contextEngineConfig,
|
|
contextEngineHostSupport: buildGenericCliContextEngineHostSupport({
|
|
backendId: context.backendResolved.id,
|
|
}),
|
|
providerId: params.provider,
|
|
modelId: context.modelId,
|
|
warn: (message) => log.warn(message),
|
|
});
|
|
const contextEngineHistoryMessages = context.contextEngine
|
|
? await loadCliSessionContextEngineMessages({
|
|
sessionId: params.sessionId,
|
|
sessionFile: params.sessionFile,
|
|
sessionKey: params.sessionKey,
|
|
agentId: params.agentId,
|
|
config: params.config,
|
|
})
|
|
: [];
|
|
const finishCliAttempt = async (
|
|
result: Awaited<ReturnType<typeof executeCliAttempt>>,
|
|
fallbackCliSessionId?: string,
|
|
) => {
|
|
const { output, assistantText, lastAssistant, sourceReplyWasDelivered } = result;
|
|
try {
|
|
const effectiveCliSessionId = output.sessionId ?? fallbackCliSessionId;
|
|
await finalizeCliContextEngineTurn({
|
|
context,
|
|
historyMessages: context.contextEngine ? contextEngineHistoryMessages : historyMessages,
|
|
assistantText,
|
|
output,
|
|
});
|
|
const assistantTranscriptOwned = await persistCliAssistantTranscript({
|
|
runParams: params,
|
|
// Dispatch owns source-reply transcript mirrors and their idempotency keys.
|
|
// Persisting them here would duplicate the same visible assistant reply.
|
|
text: sourceReplyWasDelivered ? "" : assistantText,
|
|
modelId: context.modelId,
|
|
usage: output.usage,
|
|
});
|
|
const bindingFlushOk = await isCliBindingFlushed(
|
|
effectiveCliSessionId,
|
|
params.provider,
|
|
context.cwd ?? context.workspaceDir,
|
|
);
|
|
await runCliAgentEndHook(params, {
|
|
event: {
|
|
messages: buildAgentEndMessages(lastAssistant),
|
|
success: true,
|
|
durationMs: Date.now() - context.started,
|
|
},
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
return buildCliRunResult({
|
|
output,
|
|
effectiveCliSessionId,
|
|
bindingFlushOk,
|
|
assistantTranscriptOwned,
|
|
});
|
|
} catch (error) {
|
|
throw attachCliMessagingDeliveryEvidence(error, output);
|
|
}
|
|
};
|
|
|
|
const finishDeliveredFailure = async (
|
|
error: unknown,
|
|
): Promise<EmbeddedAgentRunResult | undefined> => {
|
|
const evidence = getCliMessagingDeliveryEvidence(error);
|
|
if (!evidence) {
|
|
return undefined;
|
|
}
|
|
await runCliAgentEndHook(params, {
|
|
event: buildFailedAgentEndEvent(formatErrorMessage(error)),
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
return buildDeliveredFailureResult(error, evidence);
|
|
};
|
|
|
|
if (hasBeforeAgentRunHooks && hookRunner) {
|
|
let beforeRunResult:
|
|
| Awaited<ReturnType<NonNullable<typeof hookRunner>["runBeforeAgentRun"]>>
|
|
| undefined;
|
|
try {
|
|
beforeRunResult = await hookRunner.runBeforeAgentRun(
|
|
{
|
|
prompt: params.prompt,
|
|
systemPrompt: context.systemPrompt,
|
|
messages: buildAgentHookConversationMessages({
|
|
historyMessages,
|
|
currentTurnMessages: [],
|
|
}),
|
|
channelId: hookContext.channelId,
|
|
accountId: params.agentAccountId,
|
|
senderId: params.senderId ?? undefined,
|
|
senderIsOwner: params.senderIsOwner ?? undefined,
|
|
},
|
|
buildAgentHookContext(hookContext),
|
|
);
|
|
} catch {
|
|
const blockMessage = resolveBlockMessage(
|
|
{ outcome: "block", reason: "before_agent_run hook failed" },
|
|
{ blockedBy: "before_agent_run" },
|
|
);
|
|
await persistBlockedBeforeAgentRun({
|
|
message: blockMessage,
|
|
pluginId: "before_agent_run",
|
|
});
|
|
await runCliAgentEndHook(params, {
|
|
event: buildBlockedAgentEndEvent(blockMessage),
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
return buildBlockedBeforeAgentRunResult(blockMessage);
|
|
}
|
|
|
|
const beforeRunDecision = beforeRunResult?.decision;
|
|
if (beforeRunDecision?.outcome === "block") {
|
|
const blockMessage = resolveBlockMessage(beforeRunDecision, {
|
|
blockedBy: beforeRunResult?.pluginId ?? "unknown",
|
|
});
|
|
await persistBlockedBeforeAgentRun({
|
|
message: blockMessage,
|
|
pluginId: beforeRunResult?.pluginId ?? "unknown",
|
|
});
|
|
await runCliAgentEndHook(params, {
|
|
event: buildBlockedAgentEndEvent(blockMessage),
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
return buildBlockedBeforeAgentRunResult(blockMessage);
|
|
}
|
|
}
|
|
|
|
await persistApprovedCliUserTurnTranscript(params);
|
|
runAgentHarnessLlmInputHook({
|
|
event: llmInputEvent,
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
try {
|
|
return await finishCliAttempt(
|
|
await executeCliAttempt(context.reusableCliSession.sessionId),
|
|
context.reusableCliSession.sessionId,
|
|
);
|
|
} catch (err) {
|
|
const deliveredFailure = await finishDeliveredFailure(err);
|
|
if (deliveredFailure) {
|
|
return deliveredFailure;
|
|
}
|
|
if (isFailoverError(err)) {
|
|
const retryableSessionId = context.reusableCliSession.sessionId;
|
|
if (
|
|
shouldRetryFreshCliSessionAfterFailover({
|
|
error: err,
|
|
hasHistoryPrompt: Boolean(context.openClawHistoryPrompt),
|
|
}) &&
|
|
retryableSessionId &&
|
|
params.sessionKey
|
|
) {
|
|
try {
|
|
const retryTimeoutMs = params.timeoutMs - (Date.now() - context.started);
|
|
if (retryTimeoutMs <= 0) {
|
|
throw err;
|
|
}
|
|
if (params.onBeforeFreshCliSessionRetry) {
|
|
const clearedStaleBinding = await params.onBeforeFreshCliSessionRetry({
|
|
provider: params.provider,
|
|
reason: err.reason,
|
|
sessionId: retryableSessionId,
|
|
});
|
|
if (!clearedStaleBinding) {
|
|
throw err;
|
|
}
|
|
}
|
|
cliBackendLog.warn(
|
|
`cli session recovery retry: provider=${params.provider} reason=${err.reason} sessionKey=${params.sessionKey}`,
|
|
);
|
|
return await finishCliAttempt(await executeCliAttempt(undefined, retryTimeoutMs));
|
|
} catch (retryErr) {
|
|
const deliveredRetryFailure = await finishDeliveredFailure(retryErr);
|
|
if (deliveredRetryFailure) {
|
|
return deliveredRetryFailure;
|
|
}
|
|
const retryMessage = formatErrorMessage(retryErr);
|
|
await runCliAgentEndHook(params, {
|
|
event: buildFailedAgentEndEvent(retryMessage),
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
return toCliRunFailure(retryErr);
|
|
}
|
|
}
|
|
await runCliAgentEndHook(params, {
|
|
event: buildFailedAgentEndEvent(formatErrorMessage(err)),
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
throw err;
|
|
}
|
|
const message = formatErrorMessage(err);
|
|
await runCliAgentEndHook(params, {
|
|
event: buildFailedAgentEndEvent(message),
|
|
ctx: hookContext,
|
|
hookRunner,
|
|
});
|
|
return toCliRunFailure(err);
|
|
}
|
|
};
|
|
|
|
let runResult: EmbeddedAgentRunResult | undefined;
|
|
let runError: unknown;
|
|
let runFailed = false;
|
|
try {
|
|
runResult = await executeRun();
|
|
} catch (error) {
|
|
runFailed = true;
|
|
runError = error;
|
|
}
|
|
try {
|
|
await context.preparedBackend.cleanup?.();
|
|
} catch (cleanupError) {
|
|
if (!deliveredMessagingSideEffect) {
|
|
if (runFailed) {
|
|
cliBackendLog.warn(
|
|
`CLI run also failed before backend cleanup: ${formatErrorMessage(runError)}`,
|
|
);
|
|
}
|
|
throw cleanupError;
|
|
}
|
|
cliBackendLog.warn(
|
|
`CLI backend cleanup failed after confirmed message delivery: ${formatErrorMessage(cleanupError)}`,
|
|
);
|
|
}
|
|
if (runFailed) {
|
|
throw runError;
|
|
}
|
|
if (!runResult) {
|
|
throw new Error("CLI run completed without a result");
|
|
}
|
|
return runResult;
|
|
}
|