UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

805 lines (776 loc) • 39.9 kB
import { n as estimateStringChars } from "./cjk-chars-6ld30jSx.mjs"; import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs"; import { n as sliceUtf16Safe, r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs"; import { n as ok, t as err } from "./result-BQGgYouL.mjs"; import { f as selectResetKeptEntries } from "./tool-result-pairing-Cxh1CJVG.mjs"; import { g as resolveClaudeOpus5ModelIdentity, u as resolveClaudeFable5ModelIdentity, v as resolveClaudeSonnet5ModelIdentity } from "./anthropic-OLTjbWiq.mjs"; import "./src-CB2-8F04.mjs"; import { d as isRuntimeContextCarrier, i as projectSessionEntryMessage, t as buildSessionContext, u as convertToLlm } from "./session-BA9qunLx.mjs"; //#region packages/agent-core/src/harness/types.ts var CompactionError = class extends Error { constructor(code, message, cause) { super(message, cause === void 0 ? void 0 : { cause }); this.name = "CompactionError"; this.code = code; } }; /** Internal typed signal for a completed summary response with no usable text. */ var InvalidSummaryOutputError = class extends CompactionError { constructor(message) { super("summarization_failed", message); } }; /** A length stop with no visible summary is deterministic for an unchanged request. */ var SummaryOutputBudgetError = class extends CompactionError { constructor(message) { super("summarization_failed", message); } }; var BranchSummaryError = class extends Error { constructor(code, message, cause) { super(message, cause === void 0 ? void 0 : { cause }); this.name = "BranchSummaryError"; this.code = code; } }; //#endregion //#region packages/agent-core/src/reasoning.ts function resolveAgentReasoningOption(model, thinkingLevel) { if (thinkingLevel !== "off") return thinkingLevel; const offFallback = model.thinkingLevelMap?.off ?? ((model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") && resolveClaudeFable5ModelIdentity(model) ? "low" : void 0); switch (offFallback) { case "minimal": case "low": case "medium": case "high": case "xhigh": case "max": return offFallback; default: return model.thinkingLevelMap?.off !== null || model.api === "anthropic-messages" && (resolveClaudeSonnet5ModelIdentity(model) || resolveClaudeOpus5ModelIdentity(model)) ? "off" : void 0; } } //#endregion //#region packages/agent-core/src/runtime-deps.ts function missingRuntimeDep(name) { return /* @__PURE__ */ new Error(`@openclaw/agent-core runtime dependency "${name}" is not configured. Pass an AgentCoreRuntimeDeps instance or a streamFn explicitly.`); } /** Resolve the stream function, preferring an explicit override over injected runtime deps. */ function resolveAgentCoreStreamFn(runtime, streamFn) { if (streamFn) return streamFn; if (runtime?.streamSimple) return runtime.streamSimple; throw missingRuntimeDep("streamSimple"); } /** Standalone runtimes consume directly; hosts may retain their exact stream owner. */ function runAgentCoreStream(stream, consume, runtime) { return runtime?.runStream ? runtime.runStream(stream, consume) : consume(); } /** Drain a host-decorated stream before reading its final assistant message. */ async function consumeAgentCoreStream(stream, runtime) { return await runAgentCoreStream(stream, async () => { const response = await stream; for await (const _ of response); return response.result(); }, runtime); } /** Resolve the completion function used by non-streaming helper flows. */ function resolveAgentCoreCompleteFn(runtime) { if (runtime?.completeSimple) return runtime.completeSimple; throw missingRuntimeDep("completeSimple"); } /** Shared role instruction used by ordinary and branch compaction requests. */ const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified. When a conversation line includes sender={...}, that JSON identifies the author of that user turn. The id is authoritative; name and username are readable labels only. Preserve attribution for material facts, preferences, instructions, decisions, and disagreements; never transfer them to another sender or an anonymous user. A user line without sender={...} is unattributed: preserve its facts as unattributed and do not assign them to a known sender. Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`; //#endregion //#region packages/agent-core/src/harness/compaction/utils.ts function normalizeFileToolName(value) { const name = typeof value === "string" ? value.toLowerCase() : ""; const separator = name.indexOf("__", name.startsWith("mcp__") ? 5 : 0); return separator < 0 ? name : name.slice(separator + 2); } function addFilePaths(target, value) { for (const path of Array.isArray(value) ? value : []) if (typeof path === "string") target.add(path); } /** Create an empty file-operation accumulator. */ function createFileOps() { return { read: /* @__PURE__ */ new Set(), written: /* @__PURE__ */ new Set(), edited: /* @__PURE__ */ new Set() }; } /** Restore file metadata recorded by an earlier compaction or branch summary. */ function mergeSummaryFileOperations(fileOps, details) { addFilePaths(fileOps.read, details.readFiles); addFilePaths(fileOps.edited, details.modifiedFiles); } /** Add file operations from tool calls and results to an accumulator. */ function extractFileOpsFromMessage(message, fileOps) { if (message.role === "toolResult") { if (normalizeFileToolName(message.toolName) !== "apply_patch") return; for (const result of [message, ...Array.isArray(message.content) ? message.content : []]) { const details = asOptionalRecord(asOptionalRecord(result)?.details); const summary = asOptionalRecord(details?.summary); addFilePaths(fileOps.written, summary?.added); addFilePaths(fileOps.edited, summary?.modified); } return; } if (message.role !== "assistant" || !Array.isArray(message.content)) return; for (const block of message.content) { const toolCall = asOptionalRecord(block); if (toolCall?.type !== "toolCall") continue; const args = asOptionalRecord(toolCall.arguments); const path = [ args?.path, args?.file_path, args?.filePath ].find((value) => typeof value === "string"); if (!path) continue; switch (normalizeFileToolName(toolCall.name)) { case "read": fileOps.read.add(path); break; case "write": fileOps.written.add(path); break; case "edit": fileOps.edited.add(path); } } } /** Compute sorted read-only and modified file lists from accumulated operations. */ function computeFileLists(fileOps) { const modified = /* @__PURE__ */ new Set([...fileOps.edited, ...fileOps.written]); return { readFiles: [...fileOps.read].filter((f) => !modified.has(f)).toSorted(), modifiedFiles: [...modified].toSorted() }; } const MAX_FILE_OPS_SECTION_CHARS = 2e3; function formatBoundedFileList(tag, files, maxChars) { if (files.length === 0 || maxChars <= 0) return ""; const openTag = `<${tag}>\n`; const closeTag = `\n</${tag}>`; const lines = []; let usedChars = openTag.length + closeTag.length; for (let i = 0; i < files.length; i++) { const line = `${files[i]}\n`; const remaining = files.length - i - 1; const overflowLine = remaining > 0 ? `...and ${remaining} more\n` : ""; if (usedChars + line.length + overflowLine.length > maxChars) { const overflow = `...and ${files.length - i} more\n`; if (usedChars + overflow.length <= maxChars) lines.push(overflow); break; } lines.push(line); usedChars += line.length; } return lines.length > 0 ? `${openTag}${lines.join("").trimEnd()}${closeTag}` : ""; } /** Format file lists as bounded summary metadata tags. */ function formatFileOperations(readFiles, modifiedFiles) { const sections = [formatBoundedFileList("read-files", readFiles, 900), formatBoundedFileList("modified-files", modifiedFiles, 900)].filter(Boolean); if (sections.length === 0) return ""; const joined = `\n\n${sections.join("\n\n")}`; return joined.length > 2e3 ? joined.slice(0, MAX_FILE_OPS_SECTION_CHARS) : joined; } /** Extract visible summary text without normalizing valid model output. */ function extractSummaryText(response) { const summary = response.content.filter((block) => block.type === "text").map((block) => block.text).join("\n"); return summary.trim() ? summary : void 0; } const TOOL_RESULT_MAX_CHARS = 2e3; const IMPORTANT_TOOL_RESULT_TAIL = /(error|exception|failed|fatal|traceback|panic|stack trace|errno|exit code)/i; function stringifyCompactionValue(value) { try { return JSON.stringify(value) ?? "undefined"; } catch { return "[unserializable]"; } } function truncateForSummary(text, maxChars) { if (text.length <= maxChars) return text; const tailChars = Math.min(Math.floor(maxChars * .3), 600); const diagnosticSearch = sliceUtf16Safe(text, -maxChars); const diagnosticMatches = [...diagnosticSearch.matchAll(new RegExp(IMPORTANT_TOOL_RESULT_TAIL.source, "gi"))]; const diagnosticMatch = diagnosticMatches.findLast((match) => /^(error|exception|fatal|panic|errno)$/i.test(match[0])) ?? diagnosticMatches.at(-1); if (diagnosticMatch) { const head = truncateUtf16Safe(text, maxChars - tailChars); const displacedHead = sliceUtf16Safe(text, Math.max(0, head.length - 32), maxChars); if (!IMPORTANT_TOOL_RESULT_TAIL.test(displacedHead)) { const diagnosticOffset = text.length - diagnosticSearch.length + (diagnosticMatch.index ?? 0); const tailStart = Math.min(diagnosticOffset, text.length - tailChars); if (tailStart >= head.length) { const tail = sliceUtf16Safe(text, tailStart, tailStart + tailChars); return `${head}\n\n[... ${text.length - head.length - tail.length} ${tailStart + tail.length < text.length ? "middle/trailing" : "more"} characters truncated]\n\n${tail}`; } } } const sliced = truncateUtf16Safe(text, maxChars); return `${sliced}\n\n[... ${text.length - sliced.length} more characters truncated]`; } /** Extract text that compaction both estimates and includes in summary prompts. */ function getCompactionContentBlockText(block) { if ((block.type === "text" || block.type === "toolResult" || block.type === "tool_result") && block.text) return block.text; return (block.type === "toolResult" || block.type === "tool_result") && typeof block.content === "string" ? block.content : ""; } /** Project summary content once so rendering and token accounting share omission facts. */ function getCompactionContent(content) { const omissions = /* @__PURE__ */ new Set(); return { text: typeof content === "string" ? content : content.map((block) => { const blockText = getCompactionContentBlockText(block); if (block.type !== "text" && !blockText) omissions.add(block.type === "image" ? "[image data omitted from summary input]" : "[non-text data omitted from summary input]"); return blockText; }).filter(Boolean).join("\n"), omissionText: [...omissions].join("\n") }; } const MAX_OMISSION_MESSAGES = 8; const OMISSION_OVERFLOW = "[More image/non-text data omitted from summary input]"; function readPersistedSender(message) { if (message.role !== "user") return; const metadata = asOptionalRecord(Reflect.get(message, "__openclaw")); if (!metadata) return; const normalize = (value) => { if (typeof value !== "string") return; return value.replaceAll("\0", "").trim() || void 0; }; const sender = { id: normalize(metadata.senderId), name: normalize(metadata.senderName), username: normalize(metadata.senderUsername) }; return sender.id ? sender : void 0; } /** * Return exactly the persisted-sender text which is projected into a user * conversation label. Keep this shared with token accounting: adding a label * to the prompt without charging it can make bounded compaction overflow. */ function formatPersistedSenderSuffix(message) { const sender = readPersistedSender(message); return sender ? ` sender=${JSON.stringify(sender)}` : ""; } function formatConversationSpeaker(message) { if (message.role !== "user") return message.role === "toolResult" ? "Tool result" : "User"; return `User${formatPersistedSenderSuffix(message)}`; } /** Serialize LLM messages to plain text for summarization prompts. */ function serializeConversation(messages) { const parts = []; let omissionMessages = 0; for (const msg of messages) { if (msg.role === "user" && msg.runtimeContextCarrier === true) continue; if (msg.role === "user" || msg.role === "toolResult") { const { text, omissionText } = getCompactionContent(msg.content); if (omissionText && omissionMessages++ === MAX_OMISSION_MESSAGES) parts.push(OMISSION_OVERFLOW); const content = [omissionMessages <= MAX_OMISSION_MESSAGES ? omissionText : "", msg.role === "toolResult" ? truncateForSummary(text, TOOL_RESULT_MAX_CHARS) : text].filter(Boolean).join("\n"); if (content) parts.push(`[${formatConversationSpeaker(msg)}]: ${content}`); } else if (msg.role === "assistant") { const textParts = []; const toolCalls = []; for (const block of msg.content) if (block.type === "text") textParts.push(block.text); else if (block.type === "toolCall") { const argsStr = Object.entries(block.arguments).map(([k, v]) => `${k}=${stringifyCompactionValue(v)}`).join(", "); toolCalls.push(`${block.name}(${argsStr})`); } if (textParts.length > 0) parts.push(`[Assistant]: ${textParts.join("\n")}`); if (toolCalls.length > 0) parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`); } } return parts.join("\n\n"); } //#endregion //#region packages/agent-core/src/harness/compaction/summarization-completion.ts function createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel) { const options = { maxTokens, signal, apiKey, headers }; const fableReasoning = (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") && resolveClaudeFable5ModelIdentity(model) !== void 0; if ((model.reasoning || fableReasoning) && thinkingLevel) options.reasoning = resolveAgentReasoningOption(model, thinkingLevel); return options; } /** Runs one summarization completion and maps abort/error stops to CompactionError. */ async function runSummarizationCompletion(params) { let promptText = `<conversation>\n${serializeConversation(convertToLlm(params.messages))}\n</conversation>\n\n`; if (params.previousSummary) promptText += `<previous-summary>\n${params.previousSummary}\n</previous-summary>\n\n`; promptText += params.prompt; if (params.customInstructions) promptText += `\n\nAdditional focus: ${params.customInstructions}`; const context = { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }] }; const options = createSummarizationOptions(params.model, params.maxTokens, params.apiKey, params.headers, params.signal, params.thinkingLevel); const response = params.streamFn ? await consumeAgentCoreStream(params.streamFn(params.model, context, options), params.runtime) : await resolveAgentCoreCompleteFn(params.runtime)(params.model, context, options); params.runtime?.internalUsageSink?.(response.usage); if (response.stopReason === "aborted") return err(new CompactionError("aborted", response.errorMessage || `${params.errorLabel} aborted`)); if (response.stopReason === "error") return err(new CompactionError("summarization_failed", `${params.errorLabel} failed: ${response.errorMessage || "Unknown error"}`)); const summary = extractSummaryText(response); if (summary === void 0) { if (response.stopReason === "length") return err(new SummaryOutputBudgetError(`${params.errorLabel} failed: summary output budget (${params.maxTokens} tokens) was exhausted without visible text; reduce thinking or increase the selected model's maxTokens before retrying`)); return err(new InvalidSummaryOutputError(`${params.errorLabel} failed: model returned no summary text`)); } return ok(summary); } //#endregion //#region packages/agent-core/src/harness/compaction/compaction.ts function parseCompactionDetails(value) { const details = asOptionalRecord(value); if (!details || !Array.isArray(details.readFiles) || !details.readFiles.every((file) => typeof file === "string") || !Array.isArray(details.modifiedFiles) || !details.modifiedFiles.every((file) => typeof file === "string")) return; const request = details.latestUnresolvedUserRequest; const latestUnresolvedUserRequest = typeof request === "string" && request.length <= MAX_LATEST_USER_REQUEST_CHARS ? request : void 0; return { readFiles: details.readFiles, modifiedFiles: details.modifiedFiles, ...latestUnresolvedUserRequest ? { latestUnresolvedUserRequest } : {} }; } function extractFileOperations(messages, entries, prevBoundaryIndex) { const fileOps = createFileOps(); if (prevBoundaryIndex >= 0) { const prevCompaction = entries[prevBoundaryIndex]; if (prevCompaction?.type === "compaction" && !prevCompaction.fromHook) { const details = parseCompactionDetails(prevCompaction.details); if (details) mergeSummaryFileOperations(fileOps, details); } } for (const msg of messages) extractFileOpsFromMessage(msg, fileOps); return fileOps; } function getMessageFromEntryForCompaction(entry) { if (entry.type === "compaction") return; return projectSessionEntryMessage(entry); } const MAX_COMPACTION_SUMMARY_CHARS = 16e3; const SUMMARY_TRUNCATED_MARKER = "\n\n[Compaction summary truncated to fit budget]"; const TURN_CONTEXT_PREFIX = "\n\n---\n\n**Turn Context (split turn):**\n\n"; const MAX_LATEST_USER_REQUEST_CHARS = 800; const LATEST_USER_REQUEST_TRUNCATED_MARKER = "\n[... latest user request truncated ...]\n"; function extractLatestUserRequest(messages) { let source = ""; for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (message?.role === "user") { source = getCompactionContent(message.content).text.trim(); if (source) break; } } if (!source || source.length <= MAX_LATEST_USER_REQUEST_CHARS) return source || void 0; return `${truncateUtf16Safe(source, Math.floor(759 / 2))}${LATEST_USER_REQUEST_TRUNCATED_MARKER}${sliceUtf16Safe(source, -380)}`; } function capCompactionSummary(summary, maxChars = MAX_COMPACTION_SUMMARY_CHARS, preservedSuffix = "") { if (maxChars <= 0 || summary.length <= maxChars) return summary; const suffix = preservedSuffix && summary.endsWith(preservedSuffix) ? preservedSuffix : ""; if (maxChars < 46 + suffix.length) return truncateUtf16Safe(summary, maxChars); const budget = maxChars - 46 - suffix.length; const prefix = suffix ? summary.slice(0, -suffix.length) : summary; return `${truncateUtf16Safe(prefix, budget)}${SUMMARY_TRUNCATED_MARKER}${suffix}`; } /** Let each summary owner preserve its structure before checking the foreground token budget. */ function fitCompactionSummary(tokenBudget, render) { const full = render(MAX_COMPACTION_SUMMARY_CHARS); if (full && (tokenBudget === void 0 || estimateStringChars(full.summary) / 4 <= tokenBudget)) return ok(full); let low = 1; let high = 15999; let fitted; while (low <= high) { const mid = Math.floor((low + high) / 2); const candidate = render(mid); if (!candidate) low = mid + 1; else if (tokenBudget === void 0 || estimateStringChars(candidate.summary) / 4 <= tokenBudget) { fitted = candidate; low = mid + 1; } else high = mid - 1; } return fitted ? ok(fitted) : err(new CompactionError("summarization_failed", "The compaction summary cannot fit beside the foreground prompt and retained history.")); } /** Default compaction settings used by the harness. */ const DEFAULT_COMPACTION_SETTINGS = { enabled: true, reserveTokens: 16384, keepRecentTokens: 2e4 }; /** Calculate total context tokens from provider usage. */ function calculateContextTokens(usage) { if (usage.contextUsage?.state === "available") return usage.contextUsage.totalTokens; return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite; } function getAssistantUsage(msg) { return msg.role === "assistant" && "usage" in msg && msg.stopReason !== "aborted" && msg.stopReason !== "error" && msg.usage && calculateContextTokens(msg.usage) > 0 ? msg.usage : void 0; } function isUnavailableContextBarrier(message) { if (message.role !== "assistant") return false; const usage = "usage" in message ? message.usage : void 0; if (!usage) return false; if (message.api === "cli" && usage.contextUsage === void 0) return true; if (usage.contextUsage?.state !== "unavailable") return false; return calculateContextTokens(usage) === 0; } /** Return usage from the last valid assistant message in session entries. */ function getLastAssistantUsage(entries) { for (const entry of entries.toReversed()) if (entry.type === "message") { if (isUnavailableContextBarrier(entry.message)) return; const usage = getAssistantUsage(entry.message); if (usage) return usage; } } function getLastAssistantUsageInfo(messages) { for (let i = messages.length - 1; i >= 0; i--) { const message = messages.at(i); if (!message) continue; if (isUnavailableContextBarrier(message)) return; const usage = getAssistantUsage(message); if (usage && usage.contextUsage?.state !== "unavailable") return { usage, index: i }; } } /** Estimate context tokens for messages using provider usage when available. */ function estimateContextTokens(messages) { const usageInfo = getLastAssistantUsageInfo(messages); const usageTokens = usageInfo ? calculateContextTokens(usageInfo.usage) : 0; let trailingTokens = 0; for (const message of usageInfo ? messages.slice(usageInfo.index + 1) : messages) trailingTokens += estimateTokens(message); return { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo?.index ?? null }; } /** Return whether context usage exceeds the configured compaction threshold. */ function shouldCompact(contextTokens, contextWindow, settings) { if (!settings.enabled || !Number.isFinite(contextWindow) || contextWindow <= 0) return false; return contextTokens > contextWindow - settings.reserveTokens; } const IMAGE_BLOCK_TOKENS = 2e3; const IMAGE_BLOCK_CHARS = IMAGE_BLOCK_TOKENS * 4; function countContentChars(content) { const { text, omissionText } = getCompactionContent(content); const images = typeof content === "string" ? 0 : content.filter((block) => block.type === "image").length; const omissionChars = omissionText ? omissionText.length + 17 : 0; return estimateStringChars(text) + images * IMAGE_BLOCK_CHARS + omissionChars; } /** Estimate token count for one message using a conservative character heuristic. */ function estimateTokens(message) { if ("excludeFromContext" in message && message.excludeFromContext === true) return 0; let chars = 0; switch (message.role) { case "assistant": for (const block of message.content) if (block.type === "text") chars += estimateStringChars(block.text); else if (block.type === "thinking") chars += estimateStringChars(block.thinking); else if (block.type === "toolCall") chars += estimateStringChars(block.name) + estimateStringChars(stringifyCompactionValue(block.arguments)); break; case "user": chars = countContentChars(message.content); chars += estimateStringChars(formatPersistedSenderSuffix(message)); break; case "custom": case "toolResult": chars = countContentChars(message.content); break; case "bashExecution": chars = estimateStringChars(message.command) + estimateStringChars(message.output); break; case "branchSummary": case "compactionSummary": chars = estimateStringChars(message.summary); } return Math.ceil(chars / 4); } function isCutPointMessage(message) { return message.role === "assistant" || isTurnStartMessage(message); } function isTurnStartMessage(message) { switch (message.role) { case "custom": return !isRuntimeContextCarrier(message); case "user": case "bashExecution": case "branchSummary": case "compactionSummary": return true; case "assistant": case "toolResult": return false; } return false; } function isTurnStartEntry(entry) { const message = getMessageFromEntryForCompaction(entry); return message ? isTurnStartMessage(message) : false; } /** Find the user-visible message that starts the turn containing an entry. */ function findTurnStartIndex(entries, entryIndex, startIndex) { for (let i = entryIndex; i >= startIndex; i--) { const entry = entries[i]; if (!entry) continue; if (isTurnStartEntry(entry)) return i; } return -1; } /** Find the compaction cut point that keeps approximately the requested recent-token budget. */ function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, constraints) { const retention = constraints?.budget; let cutIndex; let lastAllowedCut = endIndex - 1; for (let i = startIndex; i < endIndex; i++) { const entry = entries[i]; const message = entry ? getMessageFromEntryForCompaction(entry) : void 0; if (entry && entry.id === constraints?.preserveFromEntryId) lastAllowedCut = i; if (message && isCutPointMessage(message)) cutIndex = i; } if (cutIndex === void 0) return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false }; let accumulatedTokens = 0; for (let i = endIndex - 1; i >= startIndex; i--) { const entry = entries[i]; if (!entry) continue; const message = getMessageFromEntryForCompaction(entry); if (!message) continue; if (isCutPointMessage(message)) cutIndex = i; accumulatedTokens += retention?.estimateTokens(message) ?? estimateTokens(message); if (accumulatedTokens >= keepRecentTokens) break; } cutIndex = Math.min(cutIndex, lastAllowedCut); if (retention) { let retainedTokens = 0; let fittingCut = endIndex; let tailLimit = retention.maxTokens; for (let i = endIndex - 1; i >= cutIndex; i--) { const entry = entries[i]; const message = entry ? getMessageFromEntryForCompaction(entry) : void 0; retainedTokens += message ? retention.estimateTokens(message) : 0; if (retainedTokens > tailLimit) break; if (i <= lastAllowedCut && message && isCutPointMessage(message)) { if (fittingCut === endIndex) tailLimit = Math.max(retainedTokens, retention.maxTokens - retention.reserveTokens); fittingCut = i; } } if (fittingCut === endIndex) return { firstKeptEntryIndex: endIndex, turnStartIndex: -1, isSplitTurn: false }; cutIndex = fittingCut; } while (cutIndex > startIndex) { const prevEntry = entries[cutIndex - 1]; if (!prevEntry) break; if (prevEntry.type === "compaction" || prevEntry.type === "reset") break; if (prevEntry.type === "message" || getMessageFromEntryForCompaction(prevEntry)) break; cutIndex--; } const cutEntry = entries[cutIndex]; if (!cutEntry) throw new Error("compaction cut point does not reference a session entry"); const startsTurn = isTurnStartEntry(cutEntry); const turnStartIndex = startsTurn ? -1 : findTurnStartIndex(entries, cutIndex, startIndex); return { firstKeptEntryIndex: cutIndex, turnStartIndex, isSplitTurn: !startsTurn && turnStartIndex !== -1 }; } const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work. Use this EXACT format: ## Goal [What is the user trying to accomplish? Can be multiple items if the session covers different tasks.] ## Constraints & Preferences - [Any constraints, preferences, or requirements mentioned by user] - [Or "(none)" if none were mentioned] ## Progress ### Done - [x] [Completed tasks/changes] ### In Progress - [ ] [Current work] ### Blocked - [Issues preventing progress, if any] ## Key Decisions - **[Decision]**: [Brief rationale] ## Next Steps 1. [Ordered list of what should happen next] ## Critical Context - [Any data, examples, or references needed to continue] - [Or "(none)" if not applicable] Keep each section concise. Preserve exact file paths, function names, and error messages.`; const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags. Update the existing structured summary with new information. RULES: - PRESERVE all existing information from the previous summary - ADD new progress, decisions, and context from the new messages - UPDATE the Progress section: move items from "In Progress" to "Done" when completed - UPDATE "Next Steps" based on what was accomplished - PRESERVE exact file paths, function names, and error messages - If something is no longer relevant, you may remove it Use this EXACT format: ## Goal [Preserve existing goals, add new ones if the task expanded] ## Constraints & Preferences - [Preserve existing, add new ones discovered] ## Progress ### Done - [x] [Include previously done items AND newly completed items] ### In Progress - [ ] [Current work - update based on progress] ### Blocked - [Current blockers - remove if resolved] ## Key Decisions - **[Decision]**: [Brief rationale] (preserve all previous, add new) ## Next Steps 1. [Update based on current state] ## Critical Context - [Preserve important context, add new if needed] Keep each section concise. Preserve exact file paths, function names, and error messages.`; /** Generate or update a conversation summary for compaction. */ async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, summaryPrompt) { const maxTokens = Math.min(Math.floor((summaryPrompt?.kind === "turn-prefix" ? .5 : .8) * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); const selectedPrompt = summaryPrompt?.kind === "turn-prefix" ? TURN_PREFIX_SUMMARIZATION_PROMPT : summaryPrompt?.instructions; return await runSummarizationCompletion({ messages: currentMessages, prompt: summaryPrompt ? [previousSummary && "Update the previous summary with the new conversation. Preserve relevant facts, decisions, and unresolved asks; remove stale or duplicate detail. Use the format below.", selectedPrompt].filter(Boolean).join("\n\n") : previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT, customInstructions, previousSummary, model, maxTokens, apiKey, headers, signal, thinkingLevel, streamFn, runtime, errorLabel: summaryPrompt?.kind === "turn-prefix" ? "Turn prefix summarization" : "Summarization" }); } /** Prepare session entries for compaction, or return undefined when compaction is not applicable. */ function prepareCompaction(pathEntries, settings, requestState, constraints) { const lastEntry = pathEntries.at(-1); if (!lastEntry || lastEntry.type === "reset" || lastEntry.type === "compaction" && lastEntry.fromHook) return ok(void 0); let prevBoundaryIndex = pathEntries.findLastIndex((entry) => entry?.type === "compaction" || entry?.type === "reset"); let previousSummary; let previousSummaryDetails; let previousLatestUnresolvedUserRequest; let effectiveEntries = pathEntries; let resetPreludeMessages = []; let boundaryStart = 0; if (prevBoundaryIndex >= 0) { const prevBoundary = pathEntries[prevBoundaryIndex]; previousSummary = prevBoundary?.type === "compaction" ? prevBoundary.summary : void 0; if (prevBoundary?.type === "compaction") { const details = parseCompactionDetails(prevBoundary.details); previousLatestUnresolvedUserRequest = details?.latestUnresolvedUserRequest; if (!prevBoundary.fromHook) previousSummaryDetails = details; } const firstKeptEntryId = prevBoundary?.type === "compaction" || prevBoundary?.type === "reset" ? prevBoundary.firstKeptEntryId : void 0; const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === firstKeptEntryId); if (prevBoundary?.type === "reset") { resetPreludeMessages = (firstKeptEntryIndex >= 0 ? selectResetKeptEntries(pathEntries.slice(firstKeptEntryIndex, prevBoundaryIndex)) : []).flatMap((entry) => { const message = getMessageFromEntryForCompaction(entry); return message ? [message] : []; }); effectiveEntries = pathEntries.slice(prevBoundaryIndex + 1); prevBoundaryIndex = -1; } else boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevBoundaryIndex + 1; } const boundaryEnd = effectiveEntries.length; const contextMessages = buildSessionContext(pathEntries).messages; const latestUnresolvedUserRequest = requestState ? extractLatestUserRequest(contextMessages) ?? previousLatestUnresolvedUserRequest : void 0; const contextUsage = estimateContextTokens(contextMessages); const tokensBefore = contextUsage.tokens; const totalEstimatedTokens = contextMessages.reduce((total, message) => total + estimateTokens(message), 0); const triggerUnitScale = !constraints?.budget && totalEstimatedTokens > 0 && Number.isFinite(totalEstimatedTokens) && Number.isFinite(contextUsage.usageTokens) ? Math.min(Math.max(1, settings.keepRecentTokens), Math.max(1, contextUsage.usageTokens / totalEstimatedTokens)) : 1; const resetPreludeTokens = resetPreludeMessages.reduce((total, message) => total + estimateTokens(message), 0); const keepRecentTokens = Math.min(Number.MAX_SAFE_INTEGER, settings.keepRecentTokens / triggerUnitScale + resetPreludeTokens); const cutPoint = findCutPoint(effectiveEntries, boundaryStart, boundaryEnd, keepRecentTokens, constraints); if (cutPoint.firstKeptEntryIndex === boundaryEnd) return err(new CompactionError("summarization_failed", "No complete recent message fits beside the foreground prompt, tools, and summary. Reduce the request or select a larger context window.")); const firstKeptEntry = effectiveEntries[cutPoint.firstKeptEntryIndex]; if (!firstKeptEntry?.id) return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration")); const firstKeptEntryId = firstKeptEntry.id; const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex; const messagesToSummarize = [...resetPreludeMessages]; for (let i = boundaryStart; i < historyEnd; i++) { const entry = effectiveEntries.at(i); const msg = entry ? getMessageFromEntryForCompaction(entry) : void 0; if (msg) messagesToSummarize.push(msg); } const turnPrefixMessages = []; if (cutPoint.isSplitTurn) for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) { const entry = effectiveEntries.at(i); const msg = entry ? getMessageFromEntryForCompaction(entry) : void 0; if (msg) turnPrefixMessages.push(msg); } if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) return ok(void 0); const fileOps = extractFileOperations(messagesToSummarize, effectiveEntries, prevBoundaryIndex); if (cutPoint.isSplitTurn) for (const msg of turnPrefixMessages) extractFileOpsFromMessage(msg, fileOps); return ok({ firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn: cutPoint.isSplitTurn, ...latestUnresolvedUserRequest ? { latestUnresolvedUserRequest } : {}, tokensBefore, previousSummary, previousSummaryDetails, fileOps, settings }); } const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained. Summarize the prefix to provide context for the retained suffix: ## Original Request [What did the user ask for in this turn?] ## Early Progress - [Key decisions and work done in the prefix] ## Context for Suffix - [Information needed to understand the retained recent work] Be concise. Focus on what's needed to understand the kept suffix.`; /** Generate compaction summary data from prepared session history. */ async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime) { const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, previousSummaryDetails, fileOps, settings } = preparation; if (!firstKeptEntryId) return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration")); const summarizeTurnPrefix = isSplitTurn && turnPrefixMessages.length > 0; const previousFileOperations = previousSummaryDetails ? formatFileOperations(previousSummaryDetails.readFiles, previousSummaryDetails.modifiedFiles) : ""; const preservedPreviousSummary = previousFileOperations && previousSummary?.endsWith(previousFileOperations) ? previousSummary.slice(0, -previousFileOperations.length) : previousSummary; const historyResult = messagesToSummarize.length > 0 || !summarizeTurnPrefix ? await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime) : ok(preservedPreviousSummary ?? "No prior history."); if (!historyResult.ok) return err(historyResult.error); let latestContext = ""; if (summarizeTurnPrefix) { const turnPrefixResult = await generateSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, void 0, thinkingLevel, streamFn, runtime, { kind: "turn-prefix" }); if (!turnPrefixResult.ok) return err(turnPrefixResult.error); latestContext = `${TURN_CONTEXT_PREFIX}${turnPrefixResult.value}`; } const { readFiles, modifiedFiles } = computeFileLists(fileOps); const fileOperations = formatFileOperations(readFiles, modifiedFiles); const unresolvedRequestContext = preparation.latestUnresolvedUserRequest ? `## Latest unresolved user request\n${JSON.stringify(preparation.latestUnresolvedUserRequest)}\n\n` : ""; const fitted = fitCompactionSummary(preparation.summaryTokenBudget, (maxChars) => { const requiredChars = fileOperations.length + unresolvedRequestContext.length; if (maxChars <= requiredChars + 46) return; const preservedHistoryChars = Math.min(historyResult.value.length, Math.floor((preparation.summaryTokenBudget === void 0 ? maxChars : maxChars - requiredChars) / 2)); const latestContextBudget = maxChars - requiredChars - 46 - preservedHistoryChars; if (preparation.summaryTokenBudget !== void 0 && latestContext && latestContextBudget < 86) return; const suffix = `${latestContextBudget > 0 ? capCompactionSummary(latestContext, latestContextBudget) : ""}${fileOperations}`; return { summary: `${unresolvedRequestContext}${capCompactionSummary(`${historyResult.value}${suffix}`, maxChars - unresolvedRequestContext.length, suffix)}` }; }); if (!fitted.ok) return fitted; const { summary } = fitted.value; return ok({ summary, firstKeptEntryId, tokensBefore, details: { readFiles, modifiedFiles, ...preparation.latestUnresolvedUserRequest ? { latestUnresolvedUserRequest: preparation.latestUnresolvedUserRequest } : {} } }); } //#endregion export { resolveAgentReasoningOption as A, mergeSummaryFileOperations as C, resolveAgentCoreCompleteFn as D, consumeAgentCoreStream as E, CompactionError as M, InvalidSummaryOutputError as N, resolveAgentCoreStreamFn as O, SummaryOutputBudgetError as P, formatFileOperations as S, SUMMARIZATION_SYSTEM_PROMPT as T, MAX_FILE_OPS_SECTION_CHARS as _, calculateContextTokens as a, extractFileOpsFromMessage as b, estimateContextTokens as c, findTurnStartIndex as d, fitCompactionSummary as f, shouldCompact as g, prepareCompaction as h, SUMMARY_TRUNCATED_MARKER as i, BranchSummaryError as j, runAgentCoreStream as k, estimateTokens as l, getLastAssistantUsage as m, IMAGE_BLOCK_TOKENS as n, capCompactionSummary as o, generateSummary as p, MAX_COMPACTION_SUMMARY_CHARS as r, compact as s, DEFAULT_COMPACTION_SETTINGS as t, findCutPoint as u, computeFileLists as v, serializeConversation as w, extractSummaryText as x, createFileOps as y };