UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,085 lines (1,084 loc) 41.6 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js"; import { y as truncateUtf16Safe } from "./utils-CCC-BEJH.js"; import "./agent-scope-MrLta7Pq.js"; import { r as resolveAgentConfig } from "./agent-scope-config-CgCYpZfK.js"; import { p as normalizeThinkLevel } from "./thinking-OG7pb4lf.js"; import { a as sanitizeToolCallIdsForCloudCodeAssist, n as extractToolResultId, t as extractToolCallsFromAssistant } from "./tool-call-id-DOLZ5Kkz.js"; import "./sanitize-user-facing-text-DDpUeAo_.js"; import { b as isReasoningConstraintErrorMessage } from "./errors-B5Da2wE-.js"; import { t as sanitizeContentBlocksImages } from "./tool-images-Du5Ml63i.js"; import { t as sanitizeGoogleAssistantFirstOrdering } from "./google-turn-ordering-CAKKJfPd.js"; import path from "node:path"; import fs from "node:fs/promises"; import { createHash } from "node:crypto"; //#region src/agents/embedded-agent-helpers/bootstrap.ts /** * Builds and sanitizes bootstrap context inserted into embedded-agent sessions. */ function isBase64Signature(value) { const trimmed = value.trim(); if (!trimmed) return false; const compact = trimmed.replace(/\s+/g, ""); if (!/^[A-Za-z0-9+/=_-]+$/.test(compact)) return false; const isUrl = compact.includes("-") || compact.includes("_"); try { const buf = Buffer.from(compact, isUrl ? "base64url" : "base64"); if (buf.length === 0) return false; const encoded = buf.toString(isUrl ? "base64url" : "base64"); const normalize = (input) => input.replace(/=+$/g, ""); return normalize(encoded) === normalize(compact); } catch { return false; } } /** * Strips Claude-style thought_signature fields from content blocks. * * Gemini expects thought signatures as base64-encoded bytes, but Claude stores message ids * like "msg_abc123...". We only strip "msg_*" to preserve any provider-valid signatures. */ function stripThoughtSignatures(content, options) { if (!Array.isArray(content)) return content; const allowBase64Only = options?.allowBase64Only ?? false; const includeCamelCase = options?.includeCamelCase ?? false; const shouldStripSignature = (value) => { if (!allowBase64Only) return typeof value === "string" && value.startsWith("msg_"); return typeof value !== "string" || !isBase64Signature(value); }; return content.map((block) => { if (!block || typeof block !== "object") return block; const rec = block; const stripSnake = shouldStripSignature(rec.thought_signature); const stripCamel = includeCamelCase ? shouldStripSignature(rec.thoughtSignature) : false; if (!stripSnake && !stripCamel) return block; const next = { ...rec }; if (stripSnake) delete next.thought_signature; if (stripCamel) delete next.thoughtSignature; return next; }); } const DEFAULT_BOOTSTRAP_MAX_CHARS = 2e4; const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 6e4; const DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE = "always"; const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64; const BOOTSTRAP_HEAD_RATIO = .75; const BOOTSTRAP_TAIL_RATIO = .25; const MIN_BOOTSTRAP_TRIMMED_CONTENT_CHARS = 16; const AGENTS_BOOTSTRAP_FILENAME = "AGENTS.md"; const AGENTS_POLICY_DIGEST_RATIO = .35; const AGENTS_POLICY_HEAD_RATIO = .45; const AGENTS_POLICY_TAIL_RATIO = .15; const AGENTS_POLICY_DIGEST_MAX_LINE_CHARS = 240; function resolveBootstrapMaxChars(cfg, agentId) { const raw = cfg && agentId ? resolveAgentConfig(cfg, agentId)?.bootstrapMaxChars ?? cfg.agents?.defaults?.bootstrapMaxChars : cfg?.agents?.defaults?.bootstrapMaxChars; if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.floor(raw); return DEFAULT_BOOTSTRAP_MAX_CHARS; } function resolveBootstrapTotalMaxChars(cfg, agentId) { const raw = cfg && agentId ? resolveAgentConfig(cfg, agentId)?.bootstrapTotalMaxChars ?? cfg.agents?.defaults?.bootstrapTotalMaxChars : cfg?.agents?.defaults?.bootstrapTotalMaxChars; if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.floor(raw); return DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS; } function resolveBootstrapPromptTruncationWarningMode(cfg) { const raw = cfg?.agents?.defaults?.bootstrapPromptTruncationWarning; if (raw === "off" || raw === "once" || raw === "always") return raw; return DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE; } function isAgentsBootstrapFile(fileName) { return fileName?.toLowerCase() === AGENTS_BOOTSTRAP_FILENAME.toLowerCase(); } function isPolicyDigestCandidate(line) { if (/^(?:#{1,6}|\s*[-*+]|\s*\d+[.)])\s+\S/u.test(line)) return true; return /\b(?:AGENTS\.md|scoped|required|must|never|do not|before subtree|read scoped|owner|security|secret|credential|test|validation|command|commit|push|github|pr)\b/iu.test(line); } function normalizePolicyDigestLine(line) { const normalized = line.trim().replace(/\s+/gu, " "); if (normalized.length <= AGENTS_POLICY_DIGEST_MAX_LINE_CHARS) return normalized; return `${truncateUtf16Safe(normalized, AGENTS_POLICY_DIGEST_MAX_LINE_CHARS - 1)}…`; } function buildAgentsPolicyDigest(content, budget) { if (budget <= 0) return { text: "", omittedLines: 0 }; const candidates = content.split(/\r?\n/u).map((line, index) => ({ index, line: normalizePolicyDigestLine(line) })).filter(({ line }) => line.length > 0 && isPolicyDigestCandidate(line)); const highPriorityPattern = /\b(?:AGENTS\.md|scoped|required|must|never|do not|before subtree|read scoped|security|secret|credential)\b/iu; const selected = /* @__PURE__ */ new Set(); let used = 0; const trySelect = (candidate) => { const separatorChars = selected.size > 0 ? 1 : 0; if (used + separatorChars + candidate.line.length > budget) return; selected.add(candidate.index); used += separatorChars + candidate.line.length; }; for (const candidate of candidates) if (highPriorityPattern.test(candidate.line)) trySelect(candidate); for (const candidate of candidates) if (!selected.has(candidate.index)) trySelect(candidate); const lines = candidates.filter((candidate) => selected.has(candidate.index)).toSorted((a, b) => a.index - b.index).map((candidate) => candidate.line); return { text: lines.join("\n"), omittedLines: Math.max(0, candidates.length - lines.length) }; } function trimAgentsBootstrapContent(content, maxChars) { const trimmed = content.trimEnd(); if (trimmed.length <= maxChars) return { content: trimmed, truncated: false, maxChars, originalLength: trimmed.length }; let headChars = Math.floor(maxChars * AGENTS_POLICY_HEAD_RATIO); let tailChars = Math.floor(maxChars * AGENTS_POLICY_TAIL_RATIO); let digestBudget = Math.floor(maxChars * AGENTS_POLICY_DIGEST_RATIO); let digest = buildAgentsPolicyDigest(trimmed, digestBudget); const render = () => [ trimmed.slice(0, headChars), `[...truncated, read ${AGENTS_BOOTSTRAP_FILENAME} for full content...]`, digest.text ? "[Policy digest from AGENTS.md]" : "", digest.text, digest.omittedLines > 0 ? `[...${digest.omittedLines} more policy lines omitted...]` : "", `…(truncated ${AGENTS_BOOTSTRAP_FILENAME}: kept ${headChars}+policy ${digest.text.length}+${tailChars} chars of ${trimmed.length})…`, tailChars > 0 ? trimmed.slice(-tailChars) : "" ].filter((part) => part.length > 0).join("\n"); let rendered = render(); while (rendered.length > maxChars && (tailChars > 0 || headChars > 1 || digestBudget > 0)) { const overflow = rendered.length - maxChars; if (tailChars > 0) tailChars = Math.max(0, tailChars - overflow); else if (headChars > 1) headChars = Math.max(1, headChars - overflow); else { digestBudget = Math.max(0, digestBudget - overflow); digest = buildAgentsPolicyDigest(trimmed, digestBudget); } rendered = render(); } return { content: rendered.length > maxChars ? truncateUtf16Safe(rendered, maxChars) : rendered, truncated: true, maxChars, originalLength: trimmed.length }; } function trimBootstrapContent(content, fileName, maxChars) { const trimmed = content.trimEnd(); if (trimmed.length <= maxChars) return { content: trimmed, truncated: false, maxChars, originalLength: trimmed.length }; if (isAgentsBootstrapFile(fileName)) return trimAgentsBootstrapContent(content, maxChars); const markerTemplate = (headChars, tailChars) => [ "", `[...truncated, read ${fileName} for full content...]`, `…(truncated ${fileName}: kept ${headChars}+${tailChars} chars of ${trimmed.length})…`, "" ].join("\n"); const compactMarkerTemplate = (headChars, tailChars) => `[…truncated ${headChars}+${tailChars}/${trimmed.length}]`; const separatorCharsFor = (headCount, tailCount, markerContent) => markerContent.includes("\n") ? Number(headCount > 0) + Number(tailCount > 0) : 0; const renderTruncatedContent = (head, markerContent, tail) => [ head, markerContent, tail ].filter((part) => part.length > 0).join(markerContent.includes("\n") ? "\n" : ""); const resolveMarkerTemplate = () => { const fullMarker = markerTemplate(0, 0); return maxChars - fullMarker.length - separatorCharsFor(1, 1, fullMarker) >= MIN_BOOTSTRAP_TRIMMED_CONTENT_CHARS ? markerTemplate : compactMarkerTemplate; }; const resolvedMarkerTemplate = resolveMarkerTemplate(); let headChars = 0; let tailChars = 0; let marker = resolvedMarkerTemplate(headChars, tailChars); for (let attempt = 0; attempt < 3; attempt += 1) { const contentBudget = Math.max(0, maxChars - marker.length - separatorCharsFor(headChars, tailChars, marker)); const nextHeadChars = Math.floor(contentBudget * BOOTSTRAP_HEAD_RATIO); const nextTailChars = Math.floor(contentBudget * BOOTSTRAP_TAIL_RATIO); const nextMarker = resolvedMarkerTemplate(nextHeadChars, nextTailChars); if (nextHeadChars === headChars && nextTailChars === tailChars && nextMarker.length === marker.length) break; headChars = nextHeadChars; tailChars = nextTailChars; marker = nextMarker; } let renderedLength = headChars + tailChars + marker.length + separatorCharsFor(headChars, tailChars, marker); while (renderedLength > maxChars && (tailChars > 0 || headChars > 0)) { const overflow = renderedLength - maxChars; if (tailChars > 0) tailChars = Math.max(0, tailChars - overflow); else headChars = Math.max(0, headChars - overflow); marker = resolvedMarkerTemplate(headChars, tailChars); renderedLength = headChars + tailChars + marker.length + separatorCharsFor(headChars, tailChars, marker); } if (headChars === 0 && tailChars === 0 && trimmed.length > 0) { const singleHeadMarker = resolvedMarkerTemplate(1, 0); if (1 + singleHeadMarker.length + separatorCharsFor(1, 0, singleHeadMarker) <= maxChars) { headChars = 1; marker = singleHeadMarker; } } const head = trimmed.slice(0, headChars); const tail = tailChars > 0 ? trimmed.slice(-tailChars) : ""; const contentWithMarker = renderTruncatedContent(head, marker, tail); return { content: contentWithMarker.length > maxChars ? truncateUtf16Safe(contentWithMarker, maxChars) : contentWithMarker, truncated: true, maxChars, originalLength: trimmed.length }; } function clampToBudget(content, budget) { if (budget <= 0) return ""; if (content.length <= budget) return content; if (budget <= 3) return truncateUtf16Safe(content, budget); return `${truncateUtf16Safe(content, budget - 1)}…`; } async function ensureSessionHeader(params) { const file = params.sessionFile; try { await fs.stat(file); return; } catch {} await fs.mkdir(path.dirname(file), { recursive: true, mode: 448 }); const entry = { type: "session", version: 2, id: params.sessionId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), cwd: params.cwd }; await fs.writeFile(file, `${JSON.stringify(entry)}\n`, { encoding: "utf-8", mode: 384 }); } function buildBootstrapContextFiles(files, opts) { const maxChars = opts?.maxChars ?? 2e4; let remainingTotalChars = Math.max(1, Math.floor(opts?.totalMaxChars ?? Math.max(maxChars, 6e4))); const result = []; for (const file of files) { if (remainingTotalChars <= 0) break; const pathValue = normalizeOptionalString(file.path) ?? ""; if (!pathValue) { opts?.warn?.(`skipping bootstrap file "${file.name}" — missing or invalid "path" field (hook may have used "filePath" instead)`); continue; } if (file.missing) { const cappedMissingText = clampToBudget(`[MISSING] Expected at: ${pathValue}`, remainingTotalChars); if (!cappedMissingText) break; remainingTotalChars = Math.max(0, remainingTotalChars - cappedMissingText.length); result.push({ path: pathValue, content: cappedMissingText }); continue; } if (remainingTotalChars < MIN_BOOTSTRAP_FILE_BUDGET_CHARS) { opts?.warn?.(`remaining bootstrap budget is ${remainingTotalChars} chars (<${MIN_BOOTSTRAP_FILE_BUDGET_CHARS}); skipping additional bootstrap files`); break; } const fileMaxChars = Math.max(1, Math.min(maxChars, remainingTotalChars)); const trimmed = trimBootstrapContent(file.content ?? "", file.name, fileMaxChars); const contentWithinBudget = clampToBudget(trimmed.content, remainingTotalChars); if (!contentWithinBudget) continue; if (trimmed.truncated || contentWithinBudget.length < trimmed.content.length) opts?.warn?.(`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`); remainingTotalChars = Math.max(0, remainingTotalChars - contentWithinBudget.length); result.push({ path: pathValue, content: contentWithinBudget }); } return result; } function sanitizeGoogleTurnOrdering(messages) { return sanitizeGoogleAssistantFirstOrdering(messages); } //#endregion //#region src/agents/embedded-agent-helpers/google.ts /** Detects Google-owned embedded runtime APIs. */ function isGoogleModelApi(api) { return api === "google-gemini-cli" || api === "google-generative-ai"; } //#endregion //#region src/agents/embedded-agent-helpers/openai.ts /** * Normalizes OpenAI Responses reasoning/tool-call history for safe replay. */ const OPENAI_RESPONSES_ID_MAX_LENGTH = 64; const OPENAI_RESPONSES_CALL_ID_RE = /^call_[A-Za-z0-9_-]{1,59}$/; const OPENAI_RESPONSES_FUNCTION_CALL_ITEM_ID_RE = /^fc_[A-Za-z0-9_-]{1,61}$/; function parseOpenAIReasoningSignature(value) { if (!value) return null; let candidate = null; if (typeof value === "string") { const trimmed = value.trim(); if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null; try { candidate = JSON.parse(trimmed); } catch { return null; } } else if (typeof value === "object") candidate = value; if (!candidate) return null; const id = typeof candidate.id === "string" ? candidate.id : ""; const type = typeof candidate.type === "string" ? candidate.type : ""; if (!id.startsWith("rs_")) return null; if (type === "reasoning" || type.startsWith("reasoning.")) return { id, type }; return null; } function hasFollowingNonThinkingBlock(content, index) { for (let i = index + 1; i < content.length; i++) { const block = content[i]; if (!block || typeof block !== "object") return true; if (block.type !== "thinking") return true; } return false; } function splitOpenAIFunctionCallPairing(id) { const separator = id.indexOf("|"); if (separator <= 0 || separator >= id.length - 1) return { callId: id }; return { callId: id.slice(0, separator), itemId: id.slice(separator + 1) }; } function isOpenAIToolCallType(type) { return type === "toolCall" || type === "toolUse" || type === "functionCall"; } function shortOpenAIResponsesIdHash(id) { return createHash("sha256").update(id).digest("hex").slice(0, 10); } function sanitizeOpenAIResponsesIdTail(value) { return value.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+|_+$/g, ""); } function normalizeOpenAIResponsesIdPart(params) { const trimmed = params.value.trim(); if (params.isValid(trimmed)) return trimmed; const rawTail = trimmed.startsWith(params.prefix) ? trimmed.slice(params.prefix.length) : trimmed; const hash = shortOpenAIResponsesIdHash(trimmed || params.prefix); const maxTailLength = OPENAI_RESPONSES_ID_MAX_LENGTH - params.prefix.length; const hashSuffix = `_${hash}`; const tail = `${sanitizeOpenAIResponsesIdTail(rawTail).slice(0, Math.max(1, maxTailLength - hashSuffix.length)) || "id"}${hashSuffix}`.slice(0, maxTailLength); return `${params.prefix}${tail}`; } function normalizeOpenAIResponsesFunctionCallId(id) { const { callId, itemId } = splitOpenAIFunctionCallPairing(id); const normalizedCallId = normalizeOpenAIResponsesIdPart({ value: callId, prefix: "call_", isValid: (value) => OPENAI_RESPONSES_CALL_ID_RE.test(value) }); if (!itemId) return normalizedCallId; return `${normalizedCallId}|${normalizeOpenAIResponsesIdPart({ value: itemId, prefix: "fc_", isValid: (value) => OPENAI_RESPONSES_FUNCTION_CALL_ITEM_ID_RE.test(value) })}`; } function shouldNormalizeOpenAIResponsesToolCallId(id) { const pairing = splitOpenAIFunctionCallPairing(id); if (!OPENAI_RESPONSES_CALL_ID_RE.test(pairing.callId)) return true; if (pairing.itemId === void 0) return false; return !OPENAI_RESPONSES_FUNCTION_CALL_ITEM_ID_RE.test(pairing.itemId); } function createOpenAIResponsesToolCallIdResolver() { const rewrittenByOriginalId = /* @__PURE__ */ new Map(); return { resolveAssistantId(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; }, resolveToolResultId(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; } }; } /** * 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) { let changed = false; const resolver = createOpenAIResponsesToolCallIdResolver(); const rewrittenMessages = []; for (const msg of messages) { if (!msg || typeof msg !== "object") { rewrittenMessages.push(msg); continue; } const role = msg.role; if (role === "assistant") { const assistantMsg = msg; if (!Array.isArray(assistantMsg.content)) { rewrittenMessages.push(msg); continue; } let assistantChanged = false; const nextContent = assistantMsg.content.map((block) => { if (!block || typeof block !== "object") return block; const toolCallBlock = block; if (!isOpenAIToolCallType(toolCallBlock.type) || typeof toolCallBlock.id !== "string") return block; const nextId = resolver.resolveAssistantId(toolCallBlock.id); if (nextId === toolCallBlock.id) return block; assistantChanged = true; return { ...block, id: nextId }; }); if (!assistantChanged) { rewrittenMessages.push(msg); continue; } changed = true; rewrittenMessages.push({ ...assistantMsg, content: nextContent }); continue; } if (role === "toolResult") { const toolResult = msg; let toolResultChanged = false; const updates = {}; if (typeof toolResult.toolCallId === "string") { const nextToolCallId = resolver.resolveToolResultId(toolResult.toolCallId); if (nextToolCallId !== toolResult.toolCallId) { updates.toolCallId = nextToolCallId; toolResultChanged = true; } } if (typeof toolResult.toolUseId === "string") { const nextToolUseId = resolver.resolveToolResultId(toolResult.toolUseId); if (nextToolUseId !== toolResult.toolUseId) { updates.toolUseId = nextToolUseId; toolResultChanged = true; } } if (!toolResultChanged) { rewrittenMessages.push(msg); continue; } changed = true; rewrittenMessages.push({ ...toolResult, ...updates }); continue; } rewrittenMessages.push(msg); } return changed ? rewrittenMessages : messages; } /** * 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 changed = false; const rewrittenMessages = []; let pendingRewrittenIds = null; for (const msg of messages) { if (!msg || typeof msg !== "object") { pendingRewrittenIds = null; rewrittenMessages.push(msg); continue; } const role = msg.role; if (role === "assistant") { const assistantMsg = msg; if (!Array.isArray(assistantMsg.content)) { pendingRewrittenIds = null; rewrittenMessages.push(msg); continue; } const localRewrittenIds = /* @__PURE__ */ new Map(); let seenReplayableReasoning = false; let assistantChanged = false; const nextContent = assistantMsg.content.map((block) => { if (!block || typeof block !== "object") return block; const thinkingBlock = block; if (thinkingBlock.type === "thinking" && parseOpenAIReasoningSignature(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; assistantChanged = true; localRewrittenIds.set(toolCallBlock.id, pairing.callId); return { ...block, id: pairing.callId }; }); pendingRewrittenIds = localRewrittenIds.size > 0 ? localRewrittenIds : null; if (!assistantChanged) { rewrittenMessages.push(msg); continue; } changed = true; rewrittenMessages.push({ ...assistantMsg, content: nextContent }); continue; } if (role === "toolResult" && pendingRewrittenIds && pendingRewrittenIds.size > 0) { const toolResult = msg; let toolResultChanged = false; const updates = {}; if (typeof toolResult.toolCallId === "string") { const nextToolCallId = pendingRewrittenIds.get(toolResult.toolCallId); if (nextToolCallId && nextToolCallId !== toolResult.toolCallId) { updates.toolCallId = nextToolCallId; toolResultChanged = true; } } if (typeof toolResult.toolUseId === "string") { const nextToolUseId = pendingRewrittenIds.get(toolResult.toolUseId); if (nextToolUseId && nextToolUseId !== toolResult.toolUseId) { updates.toolUseId = nextToolUseId; toolResultChanged = true; } } if (!toolResultChanged) { rewrittenMessages.push(msg); continue; } changed = true; rewrittenMessages.push({ ...toolResult, ...updates }); continue; } pendingRewrittenIds = null; rewrittenMessages.push(msg); } return changed ? rewrittenMessages : messages; } /** * Extracts the Responses `phase` (commentary/final_answer) from a v1 textSignature, if present. * 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 {} } /** * OpenAI Responses API can reject transcripts that contain a standalone `reasoning` item id * without the required following item, or stale encrypted reasoning after a model route switch. * * OpenClaw persists provider-specific reasoning metadata in `thinkingSignature`; if that metadata * is incomplete or no longer replay-safe, drop the block to keep history usable. */ function downgradeOpenAIReasoningBlocks(messages, options = {}) { let anyChanged = false; const out = []; for (const msg of messages) { if (!msg || typeof msg !== "object") { out.push(msg); continue; } if (msg.role !== "assistant") { out.push(msg); continue; } const assistantMsg = msg; if (!Array.isArray(assistantMsg.content)) { out.push(msg); continue; } let changed = false; let droppedReplayableReasoning = false; const nextContent = []; for (let i = 0; i < assistantMsg.content.length; i++) { const block = assistantMsg.content[i]; if (!block || typeof block !== "object") { nextContent.push(block); continue; } const record = block; if (record.type !== "thinking") { nextContent.push(block); continue; } if (!parseOpenAIReasoningSignature(record.thinkingSignature)) { nextContent.push(block); continue; } if (options.dropReplayableReasoning) { changed = true; droppedReplayableReasoning = true; continue; } if (hasFollowingNonThinkingBlock(assistantMsg.content, i)) { nextContent.push(block); continue; } changed = true; } if (!changed) { out.push(msg); continue; } anyChanged = true; if (nextContent.length === 0) continue; 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; out.push({ ...assistantMsg, content: finalContent }); } return anyChanged ? out : messages; } //#endregion //#region src/agents/embedded-agent-helpers/images.ts const EMPTY_CONTENT_PLACEHOLDER = "[empty content omitted]"; function dropEmptyTextBlocks(content) { return content.filter((block) => { if (!block || typeof block !== "object") return true; const rec = block; if (rec.type !== "text" || typeof rec.text !== "string") return true; return rec.text.trim().length > 0; }); } function ensureNonEmptyContent(content) { if (content.length > 0) return content; return [{ type: "text", text: EMPTY_CONTENT_PLACEHOLDER }]; } /** Resize/remove unsafe image payloads while keeping transcript turns valid. */ async function sanitizeSessionMessagesImages(messages, label, options) { const allowNonImageSanitization = (options?.sanitizeMode ?? "full") === "full"; const imageSanitization = { maxDimensionPx: options?.maxDimensionPx, maxBytes: options?.maxBytes }; const sanitizedIds = options?.sanitizeToolCallIds === true ? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode, { preserveNativeAnthropicToolUseIds: options?.preserveNativeAnthropicToolUseIds }) : messages; const out = []; for (const msg of sanitizedIds) { if (!msg || typeof msg !== "object") { out.push(msg); continue; } const role = msg.role; if (role === "toolResult") { const toolMsg = msg; const nextContent = await sanitizeContentBlocksImages(Array.isArray(toolMsg.content) ? toolMsg.content : [], label, imageSanitization); out.push({ ...toolMsg, content: ensureNonEmptyContent(dropEmptyTextBlocks(nextContent)) }); continue; } if (role === "user") { const userMsg = msg; const content = userMsg.content; if (Array.isArray(content)) { const nextContent = await sanitizeContentBlocksImages(content, label, imageSanitization); out.push({ ...userMsg, content: ensureNonEmptyContent(dropEmptyTextBlocks(nextContent)) }); continue; } } if (role === "assistant") { const assistantMsg = msg; if (assistantMsg.stopReason === "error") { const content = assistantMsg.content; if (Array.isArray(content)) { const finalContent = dropEmptyTextBlocks(await sanitizeContentBlocksImages(content, label, imageSanitization)); if (finalContent.length > 0) out.push({ ...assistantMsg, content: finalContent }); } else out.push(assistantMsg); continue; } const content = assistantMsg.content; if (Array.isArray(content)) { const strippedContent = options?.preserveSignatures ? content : stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); if (!allowNonImageSanitization) { const nextContent = await sanitizeContentBlocksImages(dropEmptyTextBlocks(strippedContent), label, imageSanitization); if (nextContent.length > 0) out.push({ ...assistantMsg, content: nextContent }); continue; } const finalContent = await sanitizeContentBlocksImages(dropEmptyTextBlocks(strippedContent), label, imageSanitization); if (finalContent.length === 0) continue; out.push({ ...assistantMsg, content: finalContent }); continue; } } out.push(msg); } return out; } //#endregion //#region src/agents/embedded-agent-helpers/messaging-dedupe.ts /** * Normalizes outbound message text to suppress duplicate send actions. */ const MIN_DUPLICATE_TEXT_LENGTH = 10; const MIN_REVERSE_SUBSTRING_DUPLICATE_RATIO = .5; /** * Normalize text for duplicate comparison. * - Trims whitespace * - Lowercases * - Strips emoji (Emoji_Presentation and Extended_Pictographic) * - Collapses multiple spaces to single space */ function normalizeTextForComparison(text) { return normalizeLowercaseStringOrEmpty(text).replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, "").replace(/\s+/g, " ").trim(); } /** Compare already-normalized message text against prior sends. */ function isMessagingToolDuplicateNormalized(normalized, normalizedSentTexts) { if (normalizedSentTexts.length === 0) return false; if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false; return normalizedSentTexts.some((normalizedSent) => { if (!normalizedSent || normalizedSent.length < MIN_DUPLICATE_TEXT_LENGTH) return false; if (normalized.includes(normalizedSent)) return true; return normalizedSent.includes(normalized) && normalized.length >= normalizedSent.length * MIN_REVERSE_SUBSTRING_DUPLICATE_RATIO; }); } /** Return true when raw message text duplicates a prior sent message. */ function isMessagingToolDuplicate(text, sentTexts) { if (sentTexts.length === 0) return false; const normalized = normalizeTextForComparison(text); if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false; return isMessagingToolDuplicateNormalized(normalized, sentTexts.map(normalizeTextForComparison)); } //#endregion //#region src/agents/embedded-agent-helpers/thinking.ts /** * Resolves fallback thinking levels for providers that require reasoning. */ function extractSupportedValues(raw) { const match = raw.match(/supported values are:\s*([^\n.]+)/i) ?? raw.match(/supported values:\s*([^\n.]+)/i); if (!match?.[1]) return []; const fragment = match[1]; const quoted = Array.from(fragment.matchAll(/['"]([^'"]+)['"]/g)).map((entry) => entry[1]?.trim()); if (quoted.length > 0) return normalizeStringEntries(quoted.filter((entry) => Boolean(entry))); return normalizeStringEntries(fragment.split(/,|\band\b/gi).map((entry) => entry.replace(/^[^a-zA-Z]+|[^a-zA-Z]+$/g, ""))); } /** Pick a configured or provider-safe reasoning level for fallback attempts. */ function pickFallbackThinkingLevel(params) { const raw = params.message?.trim(); if (!raw) return; if (isReasoningConstraintErrorMessage(raw) && !params.attempted.has("minimal")) return "minimal"; const supported = extractSupportedValues(raw); if (supported.length === 0) { if (/not supported/i.test(raw) && !params.attempted.has("off")) return "off"; return; } for (const entry of supported) { const normalized = normalizeThinkLevel(entry); if (!normalized) continue; if (params.attempted.has(normalized)) continue; return normalized; } } //#endregion //#region src/agents/embedded-agent-helpers/turns.ts /** * Normalizes embedded-agent conversation turn ordering for provider contracts. */ function isToolCallBlock(block) { return block.type === "toolUse" || block.type === "toolCall" || block.type === "functionCall"; } function isThinkingLikeBlock(block) { if (!block || typeof block !== "object") return false; const type = block.type; return type === "thinking" || type === "redacted_thinking"; } function isAbortedAssistantTurn(message) { const stopReason = message.stopReason; return stopReason === "aborted" || stopReason === "error"; } function extractToolResultMatchIds(record) { const ids = /* @__PURE__ */ new Set(); for (const value of [ record.toolUseId, record.toolCallId, record.tool_use_id, record.tool_call_id, record.callId, record.call_id ]) { const id = normalizeOptionalString(value); if (id) ids.add(id); } return ids; } function extractToolResultMatchName(record) { return normalizeOptionalString(record.toolName) ?? normalizeOptionalString(record.name) ?? null; } function collectAnyToolResultIds(message) { const ids = /* @__PURE__ */ new Set(); const role = message.role; if (role === "toolResult") { const toolResultId = extractToolResultId(message); if (toolResultId) ids.add(toolResultId); } else if (role === "tool") { const record = message; for (const id of extractToolResultMatchIds(record)) ids.add(id); } const content = message.content; if (!Array.isArray(content)) return ids; for (const block of content) { if (!block || typeof block !== "object") continue; const record = block; if (record.type !== "toolResult" && record.type !== "tool") continue; for (const id of extractToolResultMatchIds(record)) ids.add(id); } return ids; } function collectTrustedToolResultMatches(message) { const matches = /* @__PURE__ */ new Map(); const role = message.role; const addMatch = (ids, toolName) => { for (const id of ids) { const bucket = matches.get(id) ?? /* @__PURE__ */ new Set(); if (toolName) bucket.add(toolName); matches.set(id, bucket); } }; if (role === "toolResult") { const record = message; addMatch([...extractToolResultMatchIds(record), ...(() => { const canonicalId = extractToolResultId(message); return canonicalId ? [canonicalId] : []; })()], extractToolResultMatchName(record)); } else if (role === "tool") { const record = message; addMatch(extractToolResultMatchIds(record), extractToolResultMatchName(record)); } return matches; } function collectFutureToolResultMatches(messages, startIndex) { 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; if (candidate.role === "assistant") break; for (const [id, toolNames] of collectTrustedToolResultMatches(candidate)) { const bucket = matches.get(id) ?? /* @__PURE__ */ new Set(); for (const toolName of toolNames) bucket.add(toolName); matches.set(id, bucket); } } return matches; } function collectFutureToolResultIds(messages, startIndex) { const ids = /* @__PURE__ */ new Set(); for (let index = startIndex + 1; index < messages.length; index += 1) { const candidate = messages[index]; if (!candidate || typeof candidate !== "object") continue; if (candidate.role === "assistant") break; for (const id of collectAnyToolResultIds(candidate)) ids.add(id); } return ids; } /** * 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 (let i = 0; i < messages.length; i++) { const msg = messages[i]; if (!msg || typeof msg !== "object") { result.push(msg); continue; } if (msg.role !== "assistant") { result.push(msg); continue; } const assistantMsg = msg; const originalContent = Array.isArray(assistantMsg.content) ? assistantMsg.content : []; if (originalContent.length === 0) { result.push(msg); continue; } if (extractToolCallsFromAssistant(msg).length === 0) { result.push(msg); continue; } const hasThinking = originalContent.some((block) => isThinkingLikeBlock(block)); const validToolResultMatches = collectFutureToolResultMatches(messages, i); const validToolUseIds = collectFutureToolResultIds(messages, i); 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); })) result.push(msg); else result.push({ ...assistantMsg, content: isAbortedAssistantTurn(msg) ? [] : [{ type: "text", text: "[tool calls omitted]" }] }); continue; } 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) { result.push(msg); continue; } if (originalContent.length > 0 && filteredContent.length === 0) result.push({ ...assistantMsg, content: isAbortedAssistantTurn(msg) ? [] : [{ type: "text", text: "[tool calls omitted]" }] }); else result.push({ ...assistantMsg, content: filteredContent }); } 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) { if (!msg || typeof msg !== "object") { result.push(msg); continue; } const msgRole = msg.role; if (!msgRole) { result.push(msg); continue; } 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; } } result.push(msg); lastRole = msgRole; } return 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 } }; } /** * Validates and fixes conversation turn sequences for Gemini API. * Gemini requires strict alternating user→assistant→tool→user pattern. * Merges consecutive assistant messages together. */ function validateGeminiTurns(messages) { return validateTurnsWithConsecutiveMerge({ messages, role: "assistant", merge: mergeConsecutiveAssistantTurns }); } /** Merge adjacent user turns into a single provider-compatible user message. */ 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 []; } /** * Validates and fixes conversation turn sequences for Anthropic API. * Anthropic requires strict alternating user→assistant pattern. * Merges consecutive user messages together. * Also strips dangling tool_use blocks that lack corresponding tool_result blocks. */ function validateAnthropicTurns(messages) { return validateTurnsWithConsecutiveMerge({ messages: stripDanglingAnthropicToolUses(messages), role: "user", merge: mergeConsecutiveUserTurns }); } //#endregion export { stripThoughtSignatures as S, ensureSessionHeader as _, isMessagingToolDuplicate as a, resolveBootstrapTotalMaxChars as b, sanitizeSessionMessagesImages as c, normalizeOpenAIResponsesToolCallIds as d, isGoogleModelApi as f, buildBootstrapContextFiles as g, DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS as h, pickFallbackThinkingLevel as i, downgradeOpenAIFunctionCallReasoningPairs as l, DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE as m, validateAnthropicTurns as n, isMessagingToolDuplicateNormalized as o, DEFAULT_BOOTSTRAP_MAX_CHARS as p, validateGeminiTurns as r, normalizeTextForComparison as s, mergeConsecutiveUserTurns as t, downgradeOpenAIReasoningBlocks as u, resolveBootstrapMaxChars as v, sanitizeGoogleTurnOrdering as x, resolveBootstrapPromptTruncationWarningMode as y };