UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

290 lines (289 loc) 15.9 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js"; import "./parse-finite-number-Z7n6tXLk.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { x as freezeDiagnosticTraceContext } from "./diagnostic-events-Chmz2PSy.js"; import { t as SAFETY_MARGIN } from "./compaction-planning-BOByCD2w.js"; import { t as estimateStringChars } from "./cjk-chars-BtjJDifS.js"; import { n as MIN_PROMPT_BUDGET_TOKENS, t as MIN_PROMPT_BUDGET_RATIO } from "./agent-compaction-constants-BHnSZLzH.js"; import { r as estimateToolResultReductionPotential } from "./tool-result-truncation-BiAgZIVd.js"; import "./tool-result-middleware-B3Hdp_cV.js"; //#region src/agents/embedded-agent-runner/run/preemptive-compaction.ts /** * Estimates prompt pressure and decides pre-prompt compaction routing. */ const PREEMPTIVE_OVERFLOW_ERROR_TEXT = "Context overflow: prompt too large for the model (precheck)."; const ESTIMATED_CHARS_PER_TOKEN = 4; const TOOL_RESULT_CHARS_PER_TOKEN = 2; const JSON_PAYLOAD_CHARS_PER_TOKEN = 3; const MESSAGE_BOUNDARY_OVERHEAD_TOKENS = 12; const CONTENT_BLOCK_OVERHEAD_TOKENS = 6; const IMAGE_BLOCK_TOKENS = 2e3; const TRUNCATION_ROUTE_BUFFER_TOKENS = 512; function estimateStringTokenPressure(text, charsPerToken = ESTIMATED_CHARS_PER_TOKEN) { return Math.ceil(estimateStringChars(text) / charsPerToken); } function estimateJsonPayloadTokenPressure(value, charsPerToken = JSON_PAYLOAD_CHARS_PER_TOKEN) { try { const serialized = JSON.stringify(value); return typeof serialized === "string" ? Math.ceil(estimateStringChars(serialized) / charsPerToken) : 1; } catch { return 256; } } function estimateIdentifierTokenPressure(value, charsPerToken = JSON_PAYLOAD_CHARS_PER_TOKEN) { if (value == null) return 0; if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return estimateStringTokenPressure(String(value), charsPerToken); return estimateJsonPayloadTokenPressure(value, charsPerToken); } function estimateContentBlockTokenPressure(block, charsPerToken = ESTIMATED_CHARS_PER_TOKEN) { if (typeof block === "string") return estimateStringTokenPressure(block, charsPerToken); if (!isRecord(block)) return estimateJsonPayloadTokenPressure(block, charsPerToken); const type = block.type; if (type === "text" && typeof block.text === "string") return CONTENT_BLOCK_OVERHEAD_TOKENS + estimateStringTokenPressure(block.text, charsPerToken); if (type === "thinking" && typeof block.thinking === "string") return CONTENT_BLOCK_OVERHEAD_TOKENS + estimateStringTokenPressure(block.thinking, charsPerToken); if (type === "image") return IMAGE_BLOCK_TOKENS; return CONTENT_BLOCK_OVERHEAD_TOKENS + estimateJsonPayloadTokenPressure(block, charsPerToken); } function estimateToolResultContentTokenPressure(content) { if (typeof content === "string") return estimateStringTokenPressure(content, TOOL_RESULT_CHARS_PER_TOKEN); if (Array.isArray(content)) return content.reduce((sum, block) => sum + estimateContentBlockTokenPressure(block, TOOL_RESULT_CHARS_PER_TOKEN), 0); if (content !== void 0) return estimateJsonPayloadTokenPressure(content, TOOL_RESULT_CHARS_PER_TOKEN); return 0; } function estimateAssistantToolCallTokenPressure(block) { const args = block.arguments ?? block.input ?? block.args ?? {}; return CONTENT_BLOCK_OVERHEAD_TOKENS + estimateIdentifierTokenPressure(block.name, JSON_PAYLOAD_CHARS_PER_TOKEN) + estimateJsonPayloadTokenPressure(args, JSON_PAYLOAD_CHARS_PER_TOKEN); } function estimateContentTokenPressure(content) { if (typeof content === "string") return estimateStringTokenPressure(content); if (Array.isArray(content)) return content.reduce((sum, block) => sum + estimateContentBlockTokenPressure(block), 0); if (content !== void 0) return estimateJsonPayloadTokenPressure(content); return 0; } function isToolResultMessage(message) { const record = message; return record.role === "toolResult" || record.role === "tool" || record.type === "toolResult"; } function estimateMessageTokenPressure(message) { const record = message; let tokens = MESSAGE_BOUNDARY_OVERHEAD_TOKENS; if (isToolResultMessage(message)) { tokens += estimateToolResultContentTokenPressure(record.content); tokens += estimateIdentifierTokenPressure(record.toolName ?? record.tool_name); return tokens; } if (record.role === "assistant") { const content = record.content; if (Array.isArray(content)) for (const block of content) if (isRecord(block) && (block.type === "toolCall" || block.type === "tool_use")) tokens += estimateAssistantToolCallTokenPressure(block); else tokens += estimateContentBlockTokenPressure(block); else tokens += estimateContentTokenPressure(content); const toolCalls = record.toolCalls ?? record.tool_calls; if (Array.isArray(toolCalls)) for (const toolCall of toolCalls) tokens += isRecord(toolCall) ? estimateAssistantToolCallTokenPressure(toolCall) : estimateJsonPayloadTokenPressure(toolCall); return tokens; } tokens += estimateContentTokenPressure(record.content); return tokens; } /** * Estimates the prompt pressure at the LLM boundary from transcript messages, * optional system prompt, and current prompt text. The result intentionally * includes a safety margin because this path runs before provider tokenization. */ function estimateLlmBoundaryTokenPressure(params) { const historyTokens = params.messages.reduce((sum, message) => sum + estimateMessageTokenPressure(message), 0); const systemTokens = typeof params.systemPrompt === "string" && params.systemPrompt.trim().length > 0 ? MESSAGE_BOUNDARY_OVERHEAD_TOKENS + estimateStringTokenPressure(params.systemPrompt) : 0; const promptTokens = MESSAGE_BOUNDARY_OVERHEAD_TOKENS + estimateStringTokenPressure(params.prompt); return Math.max(0, Math.ceil((historyTokens + systemTokens + promptTokens) * SAFETY_MARGIN)); } /** Estimates only the rendered prompt/system portion when history has already been accounted for. */ function estimateRenderedLlmBoundaryTokenPressure(params) { const systemTokens = typeof params.systemPrompt === "string" && params.systemPrompt.trim().length > 0 ? MESSAGE_BOUNDARY_OVERHEAD_TOKENS + estimateStringTokenPressure(params.systemPrompt) : 0; const promptTokens = MESSAGE_BOUNDARY_OVERHEAD_TOKENS + estimateStringTokenPressure(params.prompt); return Math.max(0, Math.ceil((systemTokens + promptTokens) * SAFETY_MARGIN)); } /** * Backward-compatible alias for callers that still name this a pre-prompt estimate. * * @deprecated Use estimateLlmBoundaryTokenPressure. */ function estimatePrePromptTokens(params) { return estimateLlmBoundaryTokenPressure(params); } function normalizeLlmBoundaryTokenPressure(pressure) { if (!pressure || !Number.isFinite(pressure.estimatedPromptTokens)) return; return { estimatedPromptTokens: Math.max(0, Math.ceil(pressure.estimatedPromptTokens)), source: pressure.source.trim() || "rendered_llm_boundary", ...typeof pressure.renderedChars === "number" && Number.isFinite(pressure.renderedChars) ? { renderedChars: Math.max(0, Math.ceil(pressure.renderedChars)) } : {} }; } /** * Decides whether a run should compact before submitting the prompt, and * whether reducible tool results can avoid or follow compaction. Rendered LLM * boundary pressure wins over local transcript estimates when supplied. */ function shouldPreemptivelyCompactBeforePrompt(params) { let messagesForPressure = params.messages; const llmBoundaryTokenPressure = normalizeLlmBoundaryTokenPressure(params.llmBoundaryTokenPressure); let estimatedPromptTokens = llmBoundaryTokenPressure?.estimatedPromptTokens ?? estimatePrePromptTokens({ messages: params.messages, systemPrompt: params.systemPrompt, prompt: params.prompt }); let pressureSource = llmBoundaryTokenPressure?.source ?? "transcript_estimate"; if (params.unwindowedMessages && params.unwindowedMessages !== params.messages) { const unwindowedEstimatedPromptTokens = estimatePrePromptTokens({ messages: params.unwindowedMessages, systemPrompt: params.systemPrompt, prompt: params.prompt }); if (unwindowedEstimatedPromptTokens > estimatedPromptTokens) { estimatedPromptTokens = unwindowedEstimatedPromptTokens; messagesForPressure = params.unwindowedMessages; pressureSource = "unwindowed_transcript_estimate"; } } const contextTokenBudget = Math.max(1, Math.floor(params.contextTokenBudget)); const requestedReserveTokens = Math.max(0, Math.floor(params.reserveTokens)); const minPromptBudget = Math.min(MIN_PROMPT_BUDGET_TOKENS, Math.max(1, Math.floor(contextTokenBudget * MIN_PROMPT_BUDGET_RATIO))); const effectiveReserveTokens = Math.min(requestedReserveTokens, Math.max(0, contextTokenBudget - minPromptBudget)); const promptBudgetBeforeReserve = Math.max(1, contextTokenBudget - effectiveReserveTokens); const overflowTokens = Math.max(0, estimatedPromptTokens - promptBudgetBeforeReserve); const toolResultPotential = estimateToolResultReductionPotential({ messages: messagesForPressure, contextWindowTokens: params.contextTokenBudget, maxCharsOverride: params.toolResultMaxChars }); const overflowChars = overflowTokens * ESTIMATED_CHARS_PER_TOKEN; const truncateOnlyThresholdChars = Math.max(overflowChars + TRUNCATION_ROUTE_BUFFER_TOKENS * ESTIMATED_CHARS_PER_TOKEN, Math.ceil(overflowChars * 1.5)); const toolResultReducibleChars = toolResultPotential.maxReducibleChars; let route = "fits"; if (overflowTokens > 0) if (toolResultReducibleChars <= 0) route = "compact_only"; else if (toolResultReducibleChars >= truncateOnlyThresholdChars) route = "truncate_tool_results_only"; else route = "compact_then_truncate"; return { route, shouldCompact: route === "compact_only" || route === "compact_then_truncate", estimatedPromptTokens, pressureSource, promptBudgetBeforeReserve, overflowTokens, toolResultReducibleChars, effectiveReserveTokens }; } /** Formats the compact operator log line for one pre-prompt budget check. */ function formatPrePromptPrecheckLog(params) { const { result } = params; return `[context-overflow-precheck] pre-prompt check sessionKey=${params.sessionKey ?? params.sessionId ?? "unknown"} provider=${params.provider}/${params.modelId} route=${result.route} estimatedPromptTokens=${result.estimatedPromptTokens} pressureSource=${result.pressureSource ?? "unknown"} promptBudgetBeforeReserve=${result.promptBudgetBeforeReserve} overflowTokens=${result.overflowTokens} toolResultReducibleChars=${result.toolResultReducibleChars} reserveTokens=${params.reserveTokens} effectiveReserveTokens=${result.effectiveReserveTokens} contextTokenBudget=${params.contextTokenBudget} messages=${params.messageCount} unwindowedMessages=${params.unwindowedMessageCount ?? params.messageCount} sessionFile=${params.sessionFile}`; } /** Converts the pre-prompt decision into the persisted session context-budget status record. */ function buildPrePromptContextBudgetStatus(params) { const { result } = params; const remainingPromptBudgetTokens = Math.max(0, result.promptBudgetBeforeReserve - result.estimatedPromptTokens); return { schemaVersion: 1, source: "pre-prompt-estimate", updatedAt: params.now ?? Date.now(), provider: params.provider, model: params.modelId, route: result.route, shouldCompact: result.shouldCompact, estimatedPromptTokens: result.estimatedPromptTokens, contextTokenBudget: Math.max(1, Math.floor(params.contextTokenBudget)), promptBudgetBeforeReserve: result.promptBudgetBeforeReserve, reserveTokens: Math.max(0, Math.floor(params.reserveTokens)), effectiveReserveTokens: result.effectiveReserveTokens, remainingPromptBudgetTokens, overflowTokens: result.overflowTokens, toolResultReducibleChars: result.toolResultReducibleChars, messageCount: Math.max(0, Math.floor(params.messageCount)), unwindowedMessageCount: Math.max(0, Math.floor(params.unwindowedMessageCount ?? params.messageCount)), ...params.sessionId ? { sessionId: params.sessionId } : {} }; } const TRAJECTORY_FLUSH_TIMEOUT_ENV = "OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS"; const CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX = "...[truncated]"; function normalizeExplicitTimeoutMs(value) { if (typeof value !== "number" || !Number.isFinite(value)) return; return Math.max(1, Math.floor(value)); } function parseTimeoutEnvValue(value) { const trimmed = value?.trim(); if (!trimmed) return; return parseStrictPositiveInteger(trimmed); } function resolveCleanupTimeoutDetails(getTimeoutDetails) { try { const timeoutDetails = getTimeoutDetails?.()?.trim(); return timeoutDetails ? ` details=${truncateCleanupTimeoutDetails(timeoutDetails)}` : ""; } catch (error) { return ` detailsError=${truncateCleanupTimeoutDetails(formatErrorMessage(error))}`; } } function truncateCleanupTimeoutDetails(value) { if (value.length <= 512) return value; const prefixLength = Math.max(0, 498); return `${value.slice(0, prefixLength)}${CLEANUP_TIMEOUT_DETAILS_TRUNCATED_SUFFIX}`; } /** Resolve the timeout for one agent cleanup step. */ function resolveAgentCleanupStepTimeoutMs(params) { const explicitTimeoutMs = normalizeExplicitTimeoutMs(params.timeoutMs); if (explicitTimeoutMs !== void 0) return explicitTimeoutMs; const env = params.env ?? process.env; if (params.step === "openclaw-trajectory-flush") { const trajectoryTimeoutMs = parseTimeoutEnvValue(env[TRAJECTORY_FLUSH_TIMEOUT_ENV]); if (trajectoryTimeoutMs !== void 0) return trajectoryTimeoutMs; } return parseTimeoutEnvValue(env["OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS"]) ?? 1e4; } /** Run one cleanup step with timeout logging and late-rejection handling. */ async function runAgentCleanupStep(params) { const timeoutMs = resolveAgentCleanupStepTimeoutMs({ step: params.step, timeoutMs: params.timeoutMs, env: params.env }); let timeoutHandle; let timedOut = false; const cleanupPromise = Promise.resolve().then(params.cleanup); const observedCleanupPromise = cleanupPromise.catch((error) => { if (!timedOut) params.log.warn(`agent cleanup failed: runId=${params.runId} sessionId=${params.sessionId} step=${params.step} error=${formatErrorMessage(error)}`); }); const timeoutPromise = new Promise((resolve) => { timeoutHandle = setTimeout(() => { timedOut = true; resolve("timeout"); }, timeoutMs); timeoutHandle.unref?.(); }); const result = await Promise.race([observedCleanupPromise.then(() => "done"), timeoutPromise]); if (timeoutHandle) clearTimeout(timeoutHandle); if (result === "timeout") { const details = resolveCleanupTimeoutDetails(params.getTimeoutDetails); params.log.warn(`agent cleanup timed out: runId=${params.runId} sessionId=${params.sessionId} step=${params.step} timeoutMs=${timeoutMs}${details}`); cleanupPromise.catch((error) => { params.log.warn(`agent cleanup rejected after timeout: runId=${params.runId} sessionId=${params.sessionId} step=${params.step} error=${formatErrorMessage(error)}`); }); } } //#endregion //#region src/agents/embedded-agent-runner/run/attempt.tool-run-context.ts /** * Builds tool run context passed to embedded-agent tool handlers. */ /** * Builds the stable tool-run context forwarded into an embedded-attempt execution. */ function buildEmbeddedAttemptToolRunContext(params) { return { trigger: params.trigger, jobId: params.jobId, memoryFlushWritePath: params.memoryFlushWritePath, ...params.toolsAllow ? { runtimeToolAllowlist: params.toolsAllow } : {}, ...params.trace ? { trace: freezeDiagnosticTraceContext(params.trace) } : {} }; } //#endregion export { estimateLlmBoundaryTokenPressure as a, shouldPreemptivelyCompactBeforePrompt as c, buildPrePromptContextBudgetStatus as i, runAgentCleanupStep as n, estimateRenderedLlmBoundaryTokenPressure as o, PREEMPTIVE_OVERFLOW_ERROR_TEXT as r, formatPrePromptPrecheckLog as s, buildEmbeddedAttemptToolRunContext as t };