UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

715 lines (714 loc) • 38.5 kB
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-1VuiLaaR.mjs"; import { v as parseDateFirstTimestampMs } from "./number-coercion-CkhMiRw8.mjs"; import { d as normalizeStringEntries } from "./string-normalization-D6DKCpwE.mjs"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { A as formatRawAssistantErrorForUi, D as extractLeadingHttpStatus, E as extractErrorHttpStatus, F as parseApiErrorInfo, L as extractHttpResponseBody, N as isGenericProviderInternalError, P as isKnownTransportErrorCode, j as formatTransportErrorCopy, k as formatProviderRefusalText } from "./redact-BV3wTtTv.mjs"; import { t as classifyGatewayStorageFailure } from "./sqlite-error-diagnostics-CWRyv4lz.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import { r as normalizeProviderId } from "./provider-id-D1VImdKk.mjs"; import { o as extractToolCallsFromAssistant, s as extractToolResultId } from "./tool-result-pairing--BK1RaBE.mjs"; import { c as normalizeThinkLevel } from "./thinking.shared-BI1319dV.mjs"; import { o as classifyOAuthRefreshFailure } from "./oauth-refresh-failure-DW27Dugk.mjs"; import { _ as matchesFormatErrorPattern, g as isUnsupportedReasoningEffortParameterError, h as isTimeoutErrorMessage, u as isProviderCompletedErrorFinishReasonMessage, v as resolveExecutionApprovalFailureMessage } from "./message-patterns-DU8N-ZQt.mjs"; import "./thinking-7OesKvXK.mjs"; import { t as sanitizeContentBlocksImages } from "./tool-images-Bl1ujVGo.mjs"; import { D as formatExecDeniedUserMessage, S as renderRateLimitOrOverloadedCopy, c as formatDiskSpaceErrorCopy, d as isLikelyHttpErrorText, f as isRawApiErrorPayload, p as isStreamingJsonParseError, r as AUTH_INVALID_TOKEN_USER_TEXT, s as formatBillingErrorMessage, u as isInvalidStreamingEventOrderError } from "./user-facing-text-BCPDY-39.mjs"; import { a as isContextOverflowErrorFromTables, o as isReasoningConstraintErrorMessage } from "./classify-core-Oyk-1wHl.mjs"; import { a as failoverReasonFromClassification, c as isExactUnknownNoDetailsError, o as inferSignalStatus, u as isReplayInvalidErrorMessage } from "./classification-rules-R8lQYbtq.mjs"; import { a as renderAssistantRequestFailureCopy, i as renderAssistantFormatFailureCopy, o as renderFormatErrorCopy } from "./assistant-request-failure-copy-bk0US-bY.mjs"; import { n as REPEATED_TOOL_ERROR_MESSAGE } from "./errors-C0684lhv.mjs"; import { i as isLikelyContextOverflowError, n as classifyFailoverSignal, t as classifyFailoverReason } from "./classify-DiG40ST4.mjs"; import { o as stripThoughtSignatures } from "./bootstrap-CB2bEBTF.mjs"; import { t as buildAssistantFailoverSignal } from "./assistant-message-failures-OdyZvSi1.mjs"; import { t as formatSandboxToolPolicyBlockedMessage } from "./runtime-status-BBzhhE6k.mjs"; import { t as isAnthropicApi } from "./anthropic-api-B2n5h2_B.mjs"; import { i as isThinkingLikeBlock, n as rewriteToolResultIds, r as sanitizeToolCallIdsForCloudCodeAssist } from "./tool-call-id-CBojAlOj.mjs"; import { normalizeOpenAIResponsesFunctionCallId, replaceCompactionReplayOwnerContent, shouldNormalizeOpenAIResponsesToolCallId, splitOpenAIFunctionCallPairing } from "@openclaw/ai/transports"; //#region src/agents/embedded-agent-helpers/context-overflow-observation.ts function isCompactionFailureError(errorMessage) { if (!errorMessage) return false; const lower = normalizeLowercaseStringOrEmpty(errorMessage); return (lower.includes("summarization failed") || lower.includes("compaction")) && (isLikelyContextOverflowError(errorMessage) || lower.includes("context overflow")); } const OBSERVED_OVERFLOW_TOKEN_PATTERNS = [ /input length(?:\s+and\s+max_tokens)?\s+exceed\s+context(?:\s+limit|\s+window)?\s*\(i\.e\s*([\d,]+)\s*\+\s*([\d,]+)\s*>\s*[\d,]+\)/i, /input length\s+and\s+`max_tokens`\s+exceed\s+context\s+limit:\s*([\d,]+)\s*\+\s*([\d,]+)\s*>\s*[\d,]+/i, /prompt is too long:\s*([\d,]+)\s+tokens\s*>\s*[\d,]+\s+maximum/i, /prompt is too long:\s*([\d,]+)\s*,\s*model maximum context length\s*:\s*[\d,]+/i, /requested\s+([\d,]+)\s+tokens/i, /token limit\s*:\s*[\d,]+\s*\(requested\s*:\s*([\d,]+)\)/i, /resulted in\s+([\d,]+)\s+tokens/i ]; function extractObservedOverflowTokenCount(errorMessage) { if (!errorMessage) return; for (const pattern of OBSERVED_OVERFLOW_TOKEN_PATTERNS) { const counts = errorMessage.match(pattern)?.slice(1).map((capture) => { const raw = capture.replaceAll(",", ""); return raw ? Number(raw) : NaN; }); if (counts?.every((count, index) => Number.isFinite(count) && (index === 0 ? count > 0 : count >= 0))) return Math.floor(counts.reduce((sum, count) => sum + count, 0)); } } //#endregion //#region src/agents/embedded-agent-helpers/provider-runtime-failure.ts const AUTH_SCOPE_HINT_RE = /\b(?:missing|required|requires|insufficient)\s+(?:the\s+following\s+)?scopes?\b|\bmissing\s+scope\b/i; const AUTH_SCOPE_NAME_RE = /\b(?:api\.responses\.write|model\.request)\b/i; const AUTH_INVALID_TOKEN_HINT_RE = /\bunauthorized\b|\b(?:invalid|incorrect|expired|stale)[_\s-]?api[_\s-]?key\b|\b(?:invalid|incorrect|expired|stale)\s+(?:token|jwt|credential|api[_\s-]?key)\b|\b(?:token|jwt|credential|api[_\s-]?key)\s+(?:is\s+)?(?:invalid|incorrect|expired|stale)\b/i; const HTML_BODY_RE = /^\s*(?:<!doctype\s+html\b|<html\b)/i; const HTML_CLOSE_RE = /<\/html>/i; const CLOUDFLARE_CHALLENGE_RE = /Enable\s+JavaScript\s+and\s+cookies\s+to\s+continue|cf-browser-verification|__cf_challenge|cdn-cgi\/challenge-platform|challenge-error-text/i; const PROXY_ERROR_RE = /\bproxyconnect\b|\bhttps?_proxy\b|\b407\b|\bproxy authentication required\b|\btunnel connection failed\b|\bconnect tunnel\b|\bsocks proxy\b|\bproxy error\b/i; const DNS_ERROR_RE = /\benotfound\b|\beai_again\b|\bgetaddrinfo\b|\bno such host\b|\bdns\b/i; const INTERRUPTED_NETWORK_ERROR_RE = /\beconnrefused\b|\beconnreset\b|\beconnaborted\b|\benetreset\b|\behostunreach\b|\behostdown\b|\benetunreach\b|\bepipe\b|\bsocket hang up\b|\bconnection refused\b|\bconnection reset\b|\bconnection aborted\b|\bnetwork is unreachable\b|\bhost is unreachable\b|\bfetch failed\b|\bconnection error\b|\bnetwork request failed\b/i; const SANDBOX_BLOCKED_RE = /\bapproval is required\b|\bapproval timed out\b|\bapproval was denied\b|\bblocked by sandbox\b|\bsandbox\b.*\b(?:blocked|denied|forbidden|disabled|not allowed)\b|\bexec denied\s*\(/i; const OAUTH_FAILURE_PATTERNS = [ [/\boauth refresh call\b.*\bexceeded hard timeout\b/i, "refresh_timeout"], [/\bcallback_timeout\b/i, "callback_timeout"], [/\bcallback_validation_failed\b/i, "callback_validation"] ]; function isHtmlErrorResponse(raw, status) { if (status === void 0 || status < 400) return false; const candidate = extractLeadingHttpStatus(raw) ? raw : raw.replace(/^error:\s*/i, "").trim(); const rest = extractHttpResponseBody(extractLeadingHttpStatus(candidate))?.body ?? candidate; return HTML_BODY_RE.test(rest) && HTML_CLOSE_RE.test(rest); } function isOpenAICodexScopeContext(raw, provider) { return normalizeLowercaseStringOrEmpty(provider) === "openai" || /\bopenai\s+codex\b/i.test(raw) || /\bcodex\b.*\bscopes?\b/i.test(raw); } function isAuthScopeErrorMessage(raw, status, provider) { if (!isOpenAICodexScopeContext(raw, provider)) return false; const hasScopeHint = AUTH_SCOPE_HINT_RE.test(raw); const hasKnownScopeName = AUTH_SCOPE_NAME_RE.test(raw); if (!hasScopeHint && !hasKnownScopeName) return false; return status === void 0 ? hasScopeHint : status === 401 || status === 403; } function isOAuthRefreshContentionMessage(raw) { return /\brefresh_contention\b/i.test(raw) || /\bfile lock timeout\b/i.test(raw) && /(?:\/|\\|^)(?:oauth-refresh|openclaw-oauth-refresh)[^/\n\\]*?(?:\.lock)?\b/i.test(raw); } function classifyProviderRuntimeFailureKind(signal, opts) { const normalizedSignal = typeof signal === "string" ? { message: signal } : signal; if (classifyGatewayStorageFailure(normalizedSignal)) return "gateway_storage"; const message = normalizedSignal.message?.trim() ?? ""; const status = inferSignalStatus(normalizedSignal); const hasStructuredErrorSignal = Boolean(normalizedSignal.code || normalizedSignal.errorType); if (!message && typeof status !== "number" && !hasStructuredErrorSignal) return "empty_response"; if (normalizedSignal.code === "refresh_contention" || message && isOAuthRefreshContentionMessage(message)) return "refresh_contention"; for (const [pattern, kind] of OAUTH_FAILURE_PATTERNS) if (message && pattern.test(message)) return kind; if (message && classifyOAuthRefreshFailure(message)) return "auth_refresh"; if (message && isAuthScopeErrorMessage(message, status, normalizedSignal.provider)) return "auth_scope"; if (message && (status === 407 || PROXY_ERROR_RE.test(message))) return "proxy"; if (message && isHtmlErrorResponse(message, status)) { if (status === 403 && CLOUDFLARE_CHALLENGE_RE.test(message)) return "upstream_html"; return status === 401 || status === 403 ? "auth_html" : "upstream_html"; } const failoverClassification = classifyFailoverSignal({ ...normalizedSignal, status, message: message || void 0 }, opts); const failoverReason = failoverClassification?.kind === "reason" ? failoverClassification.reason : void 0; switch (failoverReason) { case "tls_certificate": case "rate_limit": case "model_not_found": return failoverReason; } if (message && DNS_ERROR_RE.test(message)) return "dns"; if (message && (formatExecDeniedUserMessage(message) || SANDBOX_BLOCKED_RE.test(message))) return "sandbox_blocked"; if (message && isReplayInvalidErrorMessage(message)) return "replay_invalid"; const schemaOptions = message ? { ...opts, provider: normalizedSignal.provider } : void 0; if (message && !isContextOverflowErrorFromTables(message) && (classifyFailoverReason(message, schemaOptions) === "format" || matchesFormatErrorPattern(message))) return "schema"; const messageMentions401 = /\b401\b/.test(message); const messageMentions403 = /\b403\b/.test(message); const has401Evidence = status === 401 || status === void 0 && messageMentions401 && !messageMentions403; const hasPermissionScopeSignal = AUTH_SCOPE_HINT_RE.test(message) || AUTH_SCOPE_NAME_RE.test(message); if (failoverReason === "auth" && has401Evidence && AUTH_INVALID_TOKEN_HINT_RE.test(message) && !hasPermissionScopeSignal) return "auth_invalid_token"; if (failoverReason === "timeout" || failoverReason === "overloaded") return "timeout"; if (message && (isTimeoutErrorMessage(message) || INTERRUPTED_NETWORK_ERROR_RE.test(message) || status !== void 0 && [ 408, 499, 500, 502, 503, 504, 521, 522, 523, 524, 529 ].includes(status))) return "timeout"; if (message && isExactUnknownNoDetailsError(message)) return "no_error_details"; return "unclassified"; } //#endregion //#region src/agents/embedded-agent-helpers/error-text.ts const log = createSubsystemLogger("errors"); const sandboxToolPolicyAuditMessages = /* @__PURE__ */ new WeakSet(); const GENERIC_ASSISTANT_ERROR_TEXT = "LLM request failed."; const SYNTHESIZED_TIMEOUT_ERROR_TEXT = "LLM request timed out."; const RUNTIME_FAILURE_COPY = { auth_refresh: "Couldn't sign in to the AI service. Sign in again under Models in the Control UI or run `openclaw configure`.", refresh_contention: "Another sign-in is still in progress. Wait a moment, then try again.", refresh_timeout: "Signing in took too long. Try again in a moment. If it keeps happening, sign in again under Models in the Control UI.", callback_timeout: "Sign-in wasn't completed. Try signing in again. If asked for a link, paste the full link from your browser.", callback_validation: "The sign-in link wasn't accepted. Try signing in again. If asked for a link, paste the full link from your browser.", auth_scope: "This login doesn't have the access OpenClaw needs. Sign in again under Models in the Control UI.", auth_html: "Couldn't sign in to the AI service. Sign in again under Models in the Control UI or run `openclaw configure`.", auth_invalid_token: AUTH_INVALID_TOKEN_USER_TEXT, upstream_html: "Couldn't reach the AI service. Try again in a moment. If it continues, open Settings → Logs in the Control UI or run `openclaw logs --follow`.", proxy: "Couldn't connect to the AI service. For details, open Settings → Logs in the Control UI or run `openclaw logs --follow`.", tls_certificate: "Couldn't connect securely to the AI service. For details, open Settings → Logs in the Control UI or run `openclaw logs --follow`.", model_not_found: "This model was not found. Choose another model in the Control UI." }; const TOOL_CALL_INPUT_MISSING_RE = /tool_(?:use|call)\.(?:input|arguments).*?(?:field required|required)/i; const TOOL_CALL_INPUT_PATH_RE = /messages\.\d+\.content\.\d+\.tool_(?:use|call)\.(?:input|arguments)/i; function classifyAssistantErrorFacts(msg, opts) { const signal = buildAssistantFailoverSignal(msg, { provider: opts?.providerOwner?.id ?? opts?.provider }); const providerPlugin = opts?.providerOwner ?? null; const classification = classifyFailoverSignal(signal, { providerPlugin }); return { provider: opts?.provider ?? msg.provider ?? opts?.providerOwner?.id, model: opts?.model ?? msg.model, reason: failoverReasonFromClassification(classification), status: signal.status ?? extractErrorHttpStatus(signal.message ?? "")?.code, providerRuntimeFailureKind: classifyProviderRuntimeFailureKind(signal, { providerPlugin }), storageFailure: classifyGatewayStorageFailure(msg), code: signal.code }; } function formatAssistantErrorText(msg, opts, facts) { const raw = (msg.errorMessage ?? "").trim(); if (msg.stopReason !== "error" && !raw) return; const providerRefusalText = formatProviderRefusalText(msg); if (providerRefusalText) return providerRefusalText; const formatCopy = renderFormatErrorCopy(raw); const classifiedFacts = facts ?? classifyAssistantErrorFacts(msg, opts); if (classifiedFacts.storageFailure) return renderAssistantRequestFailureCopy(classifiedFacts); const { reason: failoverReason, status: formatStatus, providerRuntimeFailureKind } = classifiedFacts; const unknownTool = raw.match(/unknown tool[:\s]+["']?([a-z0-9_-]+)["']?/i) ?? raw.match(/tool\s+["']?([a-z0-9_-]+)["']?\s+(?:not found|is not available)/i); if (unknownTool?.[1]) { const audit = !sandboxToolPolicyAuditMessages.has(msg); const rewritten = formatSandboxToolPolicyBlockedMessage({ cfg: opts?.cfg, sessionKey: opts?.sessionKey, agentId: opts?.agentId, toolName: unknownTool[1], audit }); if (rewritten) { if (audit) sandboxToolPolicyAuditMessages.add(msg); return rewritten; } } const diskSpaceCopy = formatDiskSpaceErrorCopy(raw); if (diskSpaceCopy) return diskSpaceCopy; const runtimeCopy = RUNTIME_FAILURE_COPY[providerRuntimeFailureKind]; if (runtimeCopy) return runtimeCopy; if (failoverReason === "billing") return formatBillingErrorMessage(opts?.provider, opts?.model ?? msg.model, opts?.authMode); const transientCopy = failoverReason === "rate_limit" || failoverReason === "overloaded" ? renderRateLimitOrOverloadedCopy({ reason: failoverReason, raw }) : void 0; if (transientCopy) return transientCopy; if ((formatStatus === 400 || formatStatus === 422) && formatCopy !== "The AI service couldn't accept this request. Try a new conversation with /new, or choose another model in the Control UI." && !formatCopy.startsWith("LLM request rejected:")) return formatCopy; if (failoverReason === "context_overflow") return "Context overflow: prompt too large for the model. Try /reset (or /new) to start a fresh session, or use a larger-context model."; if (isReasoningConstraintErrorMessage(raw)) return "Reasoning is required for this model endpoint. Use /think minimal (or any non-off level) and try again."; if (isInvalidStreamingEventOrderError(raw)) return "LLM request failed: provider returned an invalid streaming response. Please try again."; if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(raw)) return "Message ordering conflict - please try again. If this persists, use /new to start a fresh session."; if (raw && (TOOL_CALL_INPUT_MISSING_RE.test(raw) || TOOL_CALL_INPUT_PATH_RE.test(raw))) return "Session history looks corrupted (tool call input missing). Use /new to start a fresh session. If this keeps happening, reset the session or delete the corrupted session transcript."; if (providerRuntimeFailureKind === "replay_invalid") return "Session history or replay state is invalid. Use /new to start a fresh session and try again."; const apiError = parseApiErrorInfo(raw); if (providerRuntimeFailureKind === "schema" && apiError?.type?.toLowerCase().includes("invalid_request") && apiError.message?.trim()) return `LLM request rejected: ${apiError.message.trim()}`; if (isGenericProviderInternalError(raw)) return formatRawAssistantErrorForUi(raw); const transportCopy = formatTransportErrorCopy(msg.errorCode && isKnownTransportErrorCode(msg.errorCode) ? `${raw} ${msg.errorCode}` : raw); if (transportCopy) return transportCopy; if (isProviderCompletedErrorFinishReasonMessage(raw)) return formatRawAssistantErrorForUi(raw); if (isTimeoutErrorMessage(raw) && !(facts?.status !== void 0 && facts.status >= 500)) return SYNTHESIZED_TIMEOUT_ERROR_TEXT; if (providerRuntimeFailureKind === "schema" || failoverReason === "format") return formatCopy; if (!raw) return failoverReason ? renderAssistantRequestFailureCopy(classifiedFacts) : "LLM request failed with an unknown error."; if (isRawApiErrorPayload(raw) || isLikelyHttpErrorText(raw)) return formatRawAssistantErrorForUi(raw); if (isStreamingJsonParseError(raw)) return "LLM streaming response contained a malformed fragment. Please try again."; if (raw.length > 600) log.warn(`Long error truncated: ${truncateUtf16Safe(raw, 200)}`); return raw.length > 600 ? `${truncateUtf16Safe(raw, 600)}…` : raw; } function isRawAssistantErrorPassthrough(params) { const friendlyError = params.friendlyError?.trim(); const rawError = params.rawError?.trim(); if (!friendlyError || !rawError) return false; const parsedMessage = parseApiErrorInfo(rawError)?.message?.trim(); const leadingStatusRest = extractLeadingHttpStatus(rawError)?.rest?.trim(); const hasRawDerivedProviderPrefix = friendlyError.startsWith("LLM request rejected:") || friendlyError.startsWith("LLM error") || friendlyError.startsWith("HTTP "); return friendlyError === rawError && friendlyError !== "LLM request timed out." || rawError.length > 600 && friendlyError === `${truncateUtf16Safe(rawError, 600)}…` || Boolean(parsedMessage && hasRawDerivedProviderPrefix) || Boolean(leadingStatusRest && friendlyError.startsWith("HTTP ")); } function formatUserFacingAssistantErrorText(msg, opts) { if (msg.errorCode === "openclaw_repeated_tool_error") return REPEATED_TOOL_ERROR_MESSAGE; const rawError = msg.errorMessage?.trim(); const approvalMessage = resolveExecutionApprovalFailureMessage(rawError); if (approvalMessage) return `⚠️ ${approvalMessage}`; const facts = classifyAssistantErrorFacts(msg, opts); const friendlyError = formatAssistantErrorText(msg, opts, facts); const rawPassthrough = isRawAssistantErrorPassthrough({ friendlyError, rawError }); const schemaFriendlyError = friendlyError === "The AI service couldn't accept this request. Try a new conversation with /new, or choose another model in the Control UI." || friendlyError?.startsWith("LLM request rejected:"); const safeFriendlyError = (schemaFriendlyError ? renderAssistantFormatFailureCopy(msg, facts.reason) : void 0) ?? (rawPassthrough ? schemaFriendlyError ? "The AI service couldn't accept this request. Try a new conversation with /new, or choose another model in the Control UI." : void 0 : friendlyError); if (safeFriendlyError) return safeFriendlyError.trim(); return renderAssistantRequestFailureCopy(facts) ?? "LLM request failed."; } //#endregion //#region src/agents/embedded-agent-helpers/openai.ts function hasOpenAIReasoningSignature(value) { if (!value) return false; let candidate = null; if (typeof value === "string") { const trimmed = value.trim(); if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false; try { candidate = JSON.parse(trimmed); } catch { return false; } } else if (typeof value === "object") candidate = value; if (!candidate) return false; const id = typeof candidate.id === "string" ? candidate.id : ""; const type = typeof candidate.type === "string" ? candidate.type : ""; return id.startsWith("rs_") && (type === "reasoning" || type.startsWith("reasoning.")); } function isOpenAIToolCallType(type) { return type === "toolCall" || type === "toolUse" || type === "functionCall"; } const DROP_REPLAY_MESSAGE = Symbol("dropReplayMessage"); function rewriteReplayMessages(messages, rewrite) { let changed = false; const result = []; for (const message of messages) { const next = rewrite(message); changed ||= !Object.is(next, message); if (next !== DROP_REPLAY_MESSAGE) result.push(next); } return changed ? result : messages; } function rewriteAssistantContent(message, rewrite) { if (!Array.isArray(message.content)) return message; let changed = false; const content = message.content.map((block) => { const next = block && typeof block === "object" ? rewrite(block) : block; changed ||= !Object.is(next, block); return next; }); return changed ? replaceCompactionReplayOwnerContent(message, content) : message; } /** * OpenAI Responses rejects replayed `function_call.call_id`, * `function_call.id`, and matching `function_call_output.call_id` values * that exceed its 64-char `call_*` / `fc_*` shape. pi-ai skips its own * normalizer for same-model replay, then splits persisted `call_id|fc_id` * pairs directly into the provider payload, so OpenClaw must normalize here. */ function normalizeOpenAIResponsesToolCallIds(messages) { const rewrittenByOriginalId = /* @__PURE__ */ new Map(); const resolveId = (id) => { const rewritten = rewrittenByOriginalId.get(id); if (rewritten) return rewritten; if (!shouldNormalizeOpenAIResponsesToolCallId(id)) return id; const normalized = normalizeOpenAIResponsesFunctionCallId(id); rewrittenByOriginalId.set(id, normalized); return normalized; }; return rewriteReplayMessages(messages, (msg) => { if (!msg || typeof msg !== "object") return msg; const role = msg.role; if (role === "assistant") return rewriteAssistantContent(msg, (block) => { const toolCallBlock = block; if (!isOpenAIToolCallType(toolCallBlock.type) || typeof toolCallBlock.id !== "string") return block; const nextId = resolveId(toolCallBlock.id); if (nextId === toolCallBlock.id) return block; return { ...block, id: nextId }; }); if (role === "toolResult") return rewriteToolResultIds({ message: msg, resolveId }); return msg; }); } /** * OpenAI can reject replayed `function_call` items with an `fc_*` id if the * matching `reasoning` item is absent in the same assistant turn. * * When that pairing is missing, strip the `|fc_*` suffix from tool call ids so * shared model runtime omits `function_call.id` on replay. */ function downgradeOpenAIFunctionCallReasoningPairs(messages) { let pendingRewrittenIds = null; return rewriteReplayMessages(messages, (msg) => { if (!msg || typeof msg !== "object") { pendingRewrittenIds = null; return msg; } const role = msg.role; if (role === "assistant") { const assistantMsg = msg; const localRewrittenIds = /* @__PURE__ */ new Map(); let seenReplayableReasoning = false; const next = rewriteAssistantContent(assistantMsg, (block) => { const thinkingBlock = block; if (thinkingBlock.type === "thinking" && hasOpenAIReasoningSignature(thinkingBlock.thinkingSignature)) { seenReplayableReasoning = true; return block; } const toolCallBlock = block; if (!isOpenAIToolCallType(toolCallBlock.type) || typeof toolCallBlock.id !== "string") return block; const pairing = splitOpenAIFunctionCallPairing(toolCallBlock.id); if (seenReplayableReasoning || !pairing.itemId || !pairing.itemId.startsWith("fc_")) return block; localRewrittenIds.set(toolCallBlock.id, pairing.callId); return { ...block, id: pairing.callId }; }); pendingRewrittenIds = localRewrittenIds.size > 0 ? localRewrittenIds : null; return next; } if (role === "toolResult" && pendingRewrittenIds && pendingRewrittenIds.size > 0) { const toolResult = msg; const updates = {}; for (const field of ["toolCallId", "toolUseId"]) { const id = toolResult[field]; const nextId = typeof id === "string" ? pendingRewrittenIds.get(id) : void 0; if (nextId && nextId !== id) updates[field] = nextId; } return Object.keys(updates).length > 0 ? { ...toolResult, ...updates } : msg; } pendingRewrittenIds = null; return msg; }); } /** * Used when dropping the paired msg_* id so phase metadata can be preserved independently. */ function extractTextSignaturePhase(signature) { if (!signature.startsWith("{")) return; try { const parsed = JSON.parse(signature); if (parsed.v === 1 && (parsed.phase === "commentary" || parsed.phase === "final_answer")) return parsed.phase; } catch {} } /** * Drops reasoning from before a model route switch and clears paired message ids. * The transport owns orphan detection after preparing the actual replay payload. */ function dropStaleOpenAIReasoning(messages, dropBefore) { if (dropBefore === void 0) return messages; return rewriteReplayMessages(messages, (msg) => { if (!msg || typeof msg !== "object") return msg; if (msg.role !== "assistant") return msg; const assistantMsg = msg; if (!Array.isArray(assistantMsg.content)) return msg; const messageTimestamp = parseDateFirstTimestampMs(assistantMsg.timestamp); if (messageTimestamp !== void 0 && messageTimestamp > dropBefore) return msg; let droppedReplayableReasoning = false; const nextContent = assistantMsg.content.filter((block) => { if (!block) return false; const record = block; if (typeof block !== "object" || record.type !== "thinking" || !hasOpenAIReasoningSignature(record.thinkingSignature)) return true; droppedReplayableReasoning = true; return false; }); if (nextContent.length === assistantMsg.content.length) return msg; if (nextContent.length === 0) return DROP_REPLAY_MESSAGE; const finalContent = droppedReplayableReasoning ? nextContent.map((contentBlock) => { if (!contentBlock || typeof contentBlock !== "object") return contentBlock; if (contentBlock.type !== "text" || contentBlock.textSignature === void 0) return contentBlock; const phase = extractTextSignaturePhase(contentBlock.textSignature); const { textSignature: _droppedTextSignature, ...rest } = contentBlock; return phase !== void 0 ? { ...rest, textSignature: JSON.stringify({ v: 1, phase }) } : rest; }) : nextContent; return replaceCompactionReplayOwnerContent(assistantMsg, finalContent); }); } //#endregion //#region src/agents/embedded-agent-helpers/images.ts const EMPTY_CONTENT_PLACEHOLDER = "[empty content omitted]"; function dropEmptyTextBlocks(content) { return content.filter((block) => { const rec = block; return !block || typeof block !== "object" || rec.type !== "text" || typeof rec.text !== "string" || rec.text.trim().length > 0; }); } async function sanitizeSessionMessagesImages(messages, label, options) { const imageSanitization = { maxDimensionPx: options?.maxDimensionPx, maxBytes: options?.maxBytes, verifyDecodability: true }; const sanitizeImages = (content) => sanitizeContentBlocksImages(content, label, imageSanitization); const sanitizedIds = options?.sanitizeToolCallIds === true ? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode, { preserveNativeAnthropicToolUseIds: options?.preserveNativeAnthropicToolUseIds, duplicateToolCallIdStyle: options?.duplicateToolCallIdStyle }) : messages; const out = []; for (const msg of sanitizedIds) { if (!msg || typeof msg !== "object") { out.push(msg); continue; } const role = msg.role; if (role === "toolResult" || role === "user") { const contentMsg = msg; const content = contentMsg.content; if (Array.isArray(content) || role === "toolResult") { const nextContent = dropEmptyTextBlocks(await sanitizeImages(Array.isArray(content) ? content : [])); out.push({ ...contentMsg, content: nextContent.length ? nextContent : [{ type: "text", text: EMPTY_CONTENT_PLACEHOLDER }] }); continue; } } if (role === "assistant") { const assistantMsg = msg; const content = assistantMsg.content; if (Array.isArray(content)) { const finalContent = await sanitizeImages(dropEmptyTextBlocks(assistantMsg.stopReason === "error" || options?.preserveSignatures ? content : stripThoughtSignatures(content, options?.sanitizeThoughtSignatures))); if (finalContent.length > 0 || assistantMsg.providerReplay) out.push(replaceCompactionReplayOwnerContent(assistantMsg, finalContent)); continue; } } out.push(msg); } return out; } //#endregion //#region src/agents/embedded-agent-helpers/thinking.ts function pickFallbackThinkingLevel(params) { const raw = params.message?.trim() ?? ""; if (isUnsupportedReasoningEffortParameterError(raw)) return; const requiresReasoning = isReasoningConstraintErrorMessage(raw); if (!requiresReasoning && !/(?<![\w./-])(?:think(?:ing)?(?:[._]|\s+)(?:value|level|budget)|reasoning(?:[._]|\s+)(?:effort|level|budget))(?![\w/-]|\.[\w/-])/i.test(raw)) return; if (requiresReasoning && !params.attempted.has("minimal")) return "minimal"; const fragment = raw.match(/supported values(?: are)?:\s*([^\n.]+)/i)?.[1] ?? ""; const quoted = Array.from(fragment.matchAll(/['"]([^'"]+)['"]/g), ([, value]) => value); const supported = normalizeStringEntries(quoted.length > 0 ? quoted : fragment.split(/,|\band\b/gi).map((entry) => entry.replace(/^[^a-zA-Z]+|[^a-zA-Z]+$/g, ""))); if (supported.length === 0) return /not supported/i.test(raw) && !params.attempted.has("off") ? "off" : void 0; for (const entry of supported) { const normalized = normalizeThinkLevel(entry); if (normalized && !params.attempted.has(normalized)) return normalized; } } //#endregion //#region src/agents/embedded-agent-helpers/turns.ts const SIGNED_THINKING_PROVIDERS = /* @__PURE__ */ new Set([ "anthropic", "amazon-bedrock", "anthropic-vertex" ]); function providerRequiresSignedThinking(provider) { return SIGNED_THINKING_PROVIDERS.has(normalizeProviderId(provider ?? "")); } function shouldAllowProviderOwnedThinkingReplay(params) { const hasProviderOwnedSignedThinking = params.policy.preserveSignatures || providerRequiresSignedThinking(params.provider); return isAnthropicApi(params.modelApi) && params.policy.validateAnthropicTurns && hasProviderOwnedSignedThinking && !params.policy.dropThinkingBlocks; } /** * Bedrock Converse still requires strict role alternation, so only the direct * Messages API keeps consecutive user turns separate under append-only replay. */ function shouldMergeConsecutiveUserTurns(policy, modelApi) { return !(policy.appendOnlyRuntimeContext && modelApi === "anthropic-messages"); } function isToolCallBlock(block) { return block.type === "toolUse" || block.type === "toolCall" || block.type === "functionCall"; } function extractToolResultMatchIds(record) { const ids = /* @__PURE__ */ new Set(); for (const value of [ Reflect.get(record, "toolUseId"), Reflect.get(record, "toolCallId"), Reflect.get(record, "tool_use_id"), Reflect.get(record, "tool_call_id"), Reflect.get(record, "callId"), Reflect.get(record, "call_id") ]) { const id = normalizeOptionalString(value); if (id) ids.add(id); } return ids; } function collectFutureToolResults(messages, startIndex) { const ids = /* @__PURE__ */ new Set(); const matches = /* @__PURE__ */ new Map(); for (let index = startIndex + 1; index < messages.length; index += 1) { const candidate = messages[index]; if (!candidate || typeof candidate !== "object") continue; const role = candidate.role; if (role === "assistant") break; const content = candidate.content; for (const block of Array.isArray(content) ? content : []) if (block && typeof block === "object" && (block.type === "toolResult" || block.type === "tool")) for (const id of extractToolResultMatchIds(block)) ids.add(id); if (role !== "toolResult" && role !== "tool") continue; const matchIds = extractToolResultMatchIds(candidate); if (role === "toolResult") { const canonicalId = extractToolResultId(candidate); if (canonicalId) ids.add(canonicalId); } const toolName = normalizeOptionalString(Reflect.get(candidate, "toolName")) ?? normalizeOptionalString(Reflect.get(candidate, "name")); for (const id of matchIds) { if (role === "tool") ids.add(id); const bucket = matches.get(id) ?? /* @__PURE__ */ new Set(); if (toolName) bucket.add(toolName); matches.set(id, bucket); } } return { ids, matches }; } /** * Strips dangling tool-call blocks from assistant messages when no later * tool-result span before the next assistant turn resolves them. * This fixes the "tool_use ids found without tool_result blocks" error from Anthropic. */ function stripDanglingAnthropicToolUses(messages) { const result = []; for (const [i, msg] of messages.entries()) { if (!msg) continue; if (typeof msg !== "object" || msg.role !== "assistant") { result.push(msg); continue; } const assistantMsg = msg; const originalContent = Array.isArray(assistantMsg.content) ? assistantMsg.content : []; if (originalContent.length === 0 || extractToolCallsFromAssistant(msg).length === 0) { result.push(msg); continue; } const hasThinking = originalContent.some((block) => isThinkingLikeBlock(block)); const { matches: validToolResultMatches, ids: validToolUseIds } = collectFutureToolResults(messages, i); const stopReason = msg.stopReason; const omittedContent = stopReason === "aborted" || stopReason === "error" ? [] : [{ type: "text", text: "[tool calls omitted]" }]; let nextContent = originalContent; if (hasThinking) { if (!originalContent.every((block) => { if (!block || !isToolCallBlock(block)) return true; const blockId = normalizeOptionalString(block.id); const blockName = normalizeOptionalString(block.name); if (!blockId || !blockName) return false; const matchingToolNames = validToolResultMatches.get(blockId); if (!matchingToolNames) return false; return matchingToolNames.size === 0 || matchingToolNames.has(blockName); })) nextContent = omittedContent; } else { const filteredContent = originalContent.filter((block) => { if (!block) return false; if (!isToolCallBlock(block)) return true; const blockId = normalizeOptionalString(block.id); return blockId ? validToolUseIds.has(blockId) : false; }); if (filteredContent.length !== originalContent.length) nextContent = filteredContent.length === 0 ? omittedContent : filteredContent; } result.push(nextContent === originalContent ? msg : { ...assistantMsg, content: nextContent }); } return result; } function validateTurnsWithConsecutiveMerge(params) { const { messages, role, merge } = params; if (!Array.isArray(messages) || messages.length === 0) return messages; const result = []; let lastRole; for (const msg of messages) { const msgRole = msg && typeof msg === "object" ? msg.role : void 0; if (msgRole) { if (msgRole === lastRole && lastRole === role) { const lastMsg = result[result.length - 1]; const currentMsg = msg; if (lastMsg && typeof lastMsg === "object") { const lastTyped = lastMsg; result[result.length - 1] = merge(lastTyped, currentMsg); continue; } } lastRole = msgRole; } result.push(msg); } return result.length === messages.length ? messages : result; } function mergeConsecutiveAssistantTurns(previous, current) { const mergedContent = [...Array.isArray(previous.content) ? previous.content : [], ...Array.isArray(current.content) ? current.content : []]; return { ...previous, content: mergedContent, ...current.usage && { usage: current.usage }, ...current.stopReason && { stopReason: current.stopReason }, ...current.errorMessage && { errorMessage: current.errorMessage } }; } /** Merge consecutive assistant turns for Gemini's provider turn-order contract. */ function validateGeminiTurns(messages) { return validateTurnsWithConsecutiveMerge({ messages, role: "assistant", merge: mergeConsecutiveAssistantTurns }); } function mergeConsecutiveUserTurns(previous, current) { const mergedContent = [...normalizeUserContentForMerge(previous.content), ...normalizeUserContentForMerge(current.content)]; return { ...current, content: mergedContent, timestamp: current.timestamp ?? previous.timestamp }; } function normalizeUserContentForMerge(content) { if (Array.isArray(content)) return content; if (typeof content === "string") return [{ type: "text", text: content }]; return []; } const mergeConsecutiveUserMessages = (messages) => validateTurnsWithConsecutiveMerge({ messages, role: "user", merge: mergeConsecutiveUserTurns }); /** * Repair Anthropic tool-use/result pairing; user-turn merging stays optional * because prefix-bound signed replay must preserve the original turn bytes. */ function validateAnthropicTurns(messages, options = {}) { const stripped = stripDanglingAnthropicToolUses(validateTurnsWithConsecutiveMerge({ messages, role: "assistant", merge: mergeConsecutiveAssistantTurns })); if (options.mergeConsecutiveUserTurns === false) return stripped; return mergeConsecutiveUserMessages(stripped); } //#endregion export { extractObservedOverflowTokenCount as _, validateAnthropicTurns as a, sanitizeSessionMessagesImages as c, normalizeOpenAIResponsesToolCallIds as d, GENERIC_ASSISTANT_ERROR_TEXT as f, classifyProviderRuntimeFailureKind as g, formatUserFacingAssistantErrorText as h, shouldMergeConsecutiveUserTurns as i, downgradeOpenAIFunctionCallReasoningPairs as l, formatAssistantErrorText as m, providerRequiresSignedThinking as n, validateGeminiTurns as o, SYNTHESIZED_TIMEOUT_ERROR_TEXT as p, shouldAllowProviderOwnedThinkingReplay as r, pickFallbackThinkingLevel as s, mergeConsecutiveUserMessages as t, dropStaleOpenAIReasoning as u, isCompactionFailureError as v };