UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

620 lines (619 loc) 22.9 kB
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 };