openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
620 lines (619 loc) • 22.9 kB
JavaScript
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { s as resolveBlockMessage, t as getGlobalHookRunner } from "./hook-runner-global-BnyQREr6.js";
import { t as SessionManager } from "./session-manager-_IGt1AS6.js";
import { n as SILENT_REPLY_TOKEN } from "./tokens-U6o7_k27.js";
import { h as isFailoverErrorMessage, n as classifyFailoverReason } from "./errors-B5Da2wE-.js";
import { o as isFailoverError, t as FailoverError, u as resolveFailoverStatus } from "./failover-error-DAwMDmz6.js";
import { t as buildAgentHookContextChannelFields } from "./hook-agent-context-52t6V0oE.js";
import "./embedded-agent-helpers-7sfpu1OT.js";
import { n as runAgentEndSideEffects, t as awaitAgentEndSideEffects } from "./agent-end-side-effects-Ba50U1Ua.js";
import { a as runAgentHarnessLlmInputHook, o as runAgentHarnessLlmOutputHook, s as buildAgentHookContext } from "./lifecycle-hook-helpers-BiRlZKs7.js";
import { a as finalizeHarnessContextEngineTurn, n as bootstrapHarnessContextEngine, s as runHarnessContextEngineMaintenance } from "./context-engine-lifecycle-CVy46YJP.js";
import { i as formatCliBackendOutputDigest, r as cliBackendLog } from "./log-RtgQ_MP8.js";
import { d as loadCliSessionHistoryMessages, m as buildAgentHookConversationMessages, n as claudeCliSessionTranscriptHasContent, u as loadCliSessionContextEngineMessages } from "./attempt-execution.helpers-CPoC_qYr.js";
//#region src/agents/cli-runner.ts
const log = createSubsystemLogger("agents/cli-runner");
const cliRunnerDeps = {
claudeCliSessionTranscriptHasContent,
delay: async (delayMs) => {
await new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
}
};
function isClaudeCliProvider(provider) {
return provider.trim().toLowerCase() === "claude-cli";
}
function shouldRetryFreshCliSessionAfterFailover(params) {
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;
}
}
/** Checks whether a Claude CLI session binding has reached its transcript file. */
async function isCliBindingFlushed(sessionId, provider, workspaceDir) {
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.rewriteFile?.();
}
function buildHandledReplyPayloads(reply) {
const normalized = reply ?? { text: "NO_REPLY" };
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) {
return {
role: "user",
content: prompt,
timestamp: Date.now()
};
}
function buildCliHookAssistantMessage(params) {
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) {
return Boolean(value && typeof value === "object" && "role" in value);
}
function buildCliContextEngineUserMessage(prompt) {
return {
role: "user",
content: prompt,
timestamp: Date.now()
};
}
function buildCliContextEngineAssistantMessage(params) {
return buildCliHookAssistantMessage(params);
}
function shouldAwaitCliAgentEndHook(params) {
return !params.messageChannel && !params.messageProvider;
}
async function runCliAgentEndHook(params, hookParams) {
if (shouldAwaitCliAgentEndHook(params)) {
await awaitAgentEndSideEffects(hookParams);
return;
}
runAgentEndSideEffects(hookParams);
}
async function persistApprovedCliUserTurnTranscript(params) {
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) Promise.resolve(notification).catch((error) => {
log.warn(`CLI user turn persistence notification failed: ${formatErrorMessage(error)}`);
});
} catch (error) {
log.warn(`CLI user turn persistence notification failed: ${formatErrorMessage(error)}`);
}
}
async function finalizeCliContextEngineTurn(params) {
const { context } = params;
if (!context.contextEngine) return;
const { params: runParams } = context;
const prePromptMessages = params.historyMessages.filter(isAgentMessage);
const turnMessages = [];
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;
if ((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,
runMaintenance: async (maintenanceParams) => await runHarnessContextEngineMaintenance({
...maintenanceParams,
onDeferredMaintenance: (promise) => {
deferredTurnMaintenance = promise;
}
}),
warn: (message) => log.warn(message)
})).postTurnFinalizationSucceeded && deferredTurnMaintenance) context.contextEngineDeferredTurnMaintenance = deferredTurnMaintenance;
}
/** Prepares and runs one CLI-backed agent turn. */
async function runCliAgent(params) {
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)
};
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 ?? "NO_REPLY";
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("./prepare.runtime.js");
const context = await prepareCliRunContext(params);
try {
return await runPreparedCliAgent(context);
} finally {
if (params.cleanupCliLiveSessionOnRunEnd === true) {
const { closeClaudeLiveSessionForContext } = await import("./claude-live-session-CGubx1by.js");
await closeClaudeLiveSessionForContext(context);
}
if (params.cleanupBundleMcpOnRunEnd === true) {
const { closeMcpLoopbackServer } = await import("./mcp-http-DXWAQNZ3.js");
await closeMcpLoopbackServer();
}
}
}
/** Runs an already-prepared CLI agent context through hooks and execution. */
async function runPreparedCliAgent(context) {
const { executePreparedCliRun } = await import("./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 historyMessages = hasLlmInputHooks || hasAgentEndHooks || hasBeforeAgentRunHooks ? 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
};
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)
};
const buildAgentEndMessages = (lastAssistant) => [...buildAgentHookConversationMessages({
historyMessages,
currentTurnMessages: [buildCliHookUserMessage(params.prompt), ...lastAssistant ? [lastAssistant] : []]
})];
const buildFailedAgentEndEvent = (error) => ({
messages: buildAgentEndMessages(),
success: false,
error,
durationMs: Date.now() - context.started
});
const buildBlockedAgentEndEvent = (message) => ({
messages: buildAgentHookConversationMessages({
historyMessages,
currentTurnMessages: [buildCliHookUserMessage(message)]
}),
success: false,
error: message,
durationMs: Date.now() - context.started
});
const buildBlockedBeforeAgentRunResult = (message) => ({
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
}
}
});
const persistBlockedBeforeAgentRun = async (block) => {
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
} }
});
flushSessionManagerFile(sessionManager);
} catch (err) {
log.warn(`before_agent_run block: failed to persist redacted CLI user message: ${formatErrorMessage(err)}`);
}
};
const toCliRunFailure = (error) => {
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, timeoutMs = params.timeoutMs) => {
const output = await executePreparedCliRun(timeoutMs === params.timeoutMs ? context : {
...context,
params: {
...context.params,
timeoutMs
}
}, cliSessionIdToUse);
const assistantText = output.text.trim();
if (!assistantText && params.allowEmptyAssistantReplyAsSilent !== true) throw new FailoverError("CLI backend returned an empty response.", {
reason: "empty_response",
provider: params.provider,
model: context.modelId,
sessionId: params.sessionId,
lane: params.lane
});
const assistantTexts = assistantText ? [assistantText] : [];
const lastAssistant = assistantText.length > 0 ? buildCliHookAssistantMessage({
text: assistantText,
provider: params.provider,
model: context.modelId,
usage: output.usage
}) : void 0;
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
};
};
const buildCliRunResult = (resultParams) => {
const text = resultParams.output.text?.trim();
const rawText = resultParams.output.rawText?.trim();
const payloads = text ? [{ text }] : params.allowEmptyAssistantReplyAsSilent === true ? [{ text: SILENT_REPLY_TOKEN }] : void 0;
const unflushedCliSessionId = resultParams.effectiveCliSessionId && resultParams.bindingFlushOk === false ? resultParams.effectiveCliSessionId : void 0;
const persistedCliSessionId = unflushedCliSessionId ? void 0 : resultParams.effectiveCliSessionId;
const agentSessionId = unflushedCliSessionId ? "" : resultParams.effectiveCliSessionId ?? params.sessionId ?? "";
return {
payloads,
meta: {
durationMs: Date.now() - context.started,
...resultParams.output.finalPromptText ? { finalPromptText: resultParams.output.finalPromptText } : {},
...text || rawText ? {
...text ? { finalAssistantVisibleText: text } : {},
...rawText ? { finalAssistantRawText: rawText } : {}
} : {},
systemPromptReport: context.systemPromptReport,
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: "stop",
stopReason: "completed",
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.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 } : {}
}
}
};
};
try {
await bootstrapHarnessContextEngine({
hadSessionFile: context.hadSessionFile,
contextEngine: context.contextEngine,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
config: context.contextEngineConfig,
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, fallbackCliSessionId) => {
const { output, lastAssistant } = result;
const assistantText = output.text.trim();
const effectiveCliSessionId = output.sessionId ?? fallbackCliSessionId;
await finalizeCliContextEngineTurn({
context,
historyMessages: context.contextEngine ? contextEngineHistoryMessages : historyMessages,
assistantText,
output
});
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
});
};
if (hasBeforeAgentRunHooks && hookRunner) {
let beforeRunResult;
try {
beforeRunResult = await hookRunner.runBeforeAgentRun({
prompt: params.prompt,
systemPrompt: context.systemPrompt,
messages: buildAgentHookConversationMessages({
historyMessages,
currentTurnMessages: []
}),
channelId: hookContext.channelId,
accountId: params.agentAccountId,
senderId: params.senderId ?? void 0,
senderIsOwner: params.senderIsOwner ?? void 0
}, 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) {
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) {
if (!await params.onBeforeFreshCliSessionRetry({
provider: params.provider,
reason: err.reason,
sessionId: retryableSessionId
})) throw err;
}
cliBackendLog.warn(`cli session recovery retry: provider=${params.provider} reason=${err.reason} sessionKey=${params.sessionKey}`);
return await finishCliAttempt(await executeCliAttempt(void 0, retryTimeoutMs));
} catch (retryErr) {
await runCliAgentEndHook(params, {
event: buildFailedAgentEndEvent(formatErrorMessage(retryErr)),
ctx: hookContext,
hookRunner
});
return toCliRunFailure(retryErr);
}
await runCliAgentEndHook(params, {
event: buildFailedAgentEndEvent(formatErrorMessage(err)),
ctx: hookContext,
hookRunner
});
throw err;
}
await runCliAgentEndHook(params, {
event: buildFailedAgentEndEvent(formatErrorMessage(err)),
ctx: hookContext,
hookRunner
});
return toCliRunFailure(err);
}
} finally {
await context.preparedBackend.cleanup?.();
}
}
//#endregion
export { runCliAgent as n, runPreparedCliAgent as r, isCliBindingFlushed as t };