openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
769 lines (741 loc) • 35.5 kB
JavaScript
import { n as estimateStringChars } from "./cjk-chars-CGxY6W63.js";
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js";
import { n as sliceUtf16Safe, r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { n as ok, t as err } from "./result-BQGgYouL.js";
import { d as resolveClaudeOpus5ModelIdentity, f as resolveClaudeSonnet5ModelIdentity, s as resolveClaudeFable5ModelIdentity } from "./anthropic-DfKaEfcc.js";
import "./src-g7CpceXJ.js";
import { i as projectSessionEntryMessage, t as buildSessionContext, u as convertToLlm } from "./session-CcAchsL_.js";
import { u as selectResetKeptEntries } from "./tool-result-pairing-Bccl6DhI.js";
//#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");
}
/** Drain a host-decorated stream before reading its final assistant message. */
async function consumeAgentCoreStream(stream) {
const response = await stream;
for await (const _ of response);
return response.result();
}
/** Resolve the completion function used by non-streaming helper flows. */
function resolveAgentCoreCompleteFn(runtime) {
if (runtime?.completeSimple) return runtime.completeSimple;
throw missingRuntimeDep("completeSimple");
}
//#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/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);
}
};
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/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;
}).join(""),
omissionText: [...omissions].join("\n")
};
}
const MAX_OMISSION_MESSAGES = 8;
const OMISSION_OVERFLOW = "[More image/non-text data omitted from summary input]";
/** 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(`[${msg.role === "user" ? "User" : "Tool result"}]: ${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/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 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}`;
}
/** 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) {
if (msg.role === "assistant" && "usage" in msg) {
const assistantMsg = msg;
if (assistantMsg.stopReason !== "aborted" && assistantMsg.stopReason !== "error" && assistantMsg.usage && calculateContextTokens(assistantMsg.usage) > 0) return assistantMsg.usage;
}
}
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);
if (!usageInfo) {
let estimated = 0;
for (const message of messages) estimated += estimateTokens(message);
return {
tokens: estimated,
usageTokens: 0,
trailingTokens: estimated,
lastUsageIndex: null
};
}
const usageTokens = calculateContextTokens(usageInfo.usage);
let trailingTokens = 0;
for (const message of messages.slice(usageInfo.index + 1)) trailingTokens += estimateTokens(message);
return {
tokens: usageTokens + trailingTokens,
usageTokens,
trailingTokens,
lastUsageIndex: usageInfo.index
};
}
/** 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;
const harnessMessage = message;
switch (harnessMessage.role) {
case "assistant": {
const assistant = harnessMessage;
for (const block of assistant.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));
return Math.ceil(chars / 4);
}
case "user":
case "custom":
case "toolResult":
chars = countContentChars(harnessMessage.content);
return Math.ceil(chars / 4);
case "bashExecution":
chars = estimateStringChars(harnessMessage.command) + estimateStringChars(harnessMessage.output);
return Math.ceil(chars / 4);
case "branchSummary":
case "compactionSummary":
chars = estimateStringChars(harnessMessage.summary);
return Math.ceil(chars / 4);
}
return 0;
}
function isCutPointMessage(message) {
switch (message.role) {
case "user":
case "assistant":
case "bashExecution":
case "custom":
case "branchSummary":
case "compactionSummary": return true;
case "toolResult": return false;
}
return false;
}
function isTurnStartMessage(message) {
switch (message.role) {
case "user":
case "bashExecution":
case "custom":
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;
}
function findValidCutPoints(entries, startIndex, endIndex) {
const cutPoints = [];
for (let i = startIndex; i < endIndex; i++) {
const entry = entries[i];
if (!entry) continue;
const message = getMessageFromEntryForCompaction(entry);
if (message && isCutPointMessage(message)) cutPoints.push(i);
}
return cutPoints;
}
/** 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) {
const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
if (cutPoints.length === 0) return {
firstKeptEntryIndex: startIndex,
turnStartIndex: -1,
isSplitTurn: false
};
let accumulatedTokens = 0;
const firstCutIndex = cutPoints.at(0);
if (firstCutIndex === void 0) return {
firstKeptEntryIndex: startIndex,
turnStartIndex: -1,
isSplitTurn: false
};
let cutIndex = firstCutIndex;
for (let i = endIndex - 1; i >= startIndex; i--) {
const entry = entries[i];
if (!entry) continue;
const message = getMessageFromEntryForCompaction(entry);
if (!message) continue;
const messageTokens = estimateTokens(message);
accumulatedTokens += messageTokens;
if (accumulatedTokens >= keepRecentTokens) {
const lastCutIndex = cutPoints.at(-1);
if (lastCutIndex === void 0) throw new Error("compaction cut-point list became empty during selection");
cutIndex = lastCutIndex;
for (const cutPoint of cutPoints) if (cutPoint >= i) {
cutIndex = cutPoint;
break;
}
break;
}
}
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_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.
Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
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.`;
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)) : 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) return err(new InvalidSummaryOutputError(`${params.errorLabel} failed: model returned no summary text`));
return ok(summary);
}
/** Generate or update a conversation summary for compaction. */
async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime) {
const maxTokens = Math.min(Math.floor(.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
return await runSummarizationCompletion({
messages: currentMessages,
prompt: previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT,
customInstructions,
previousSummary,
model,
maxTokens,
apiKey,
headers,
signal,
thinkingLevel,
streamFn,
runtime,
errorLabel: "Summarization"
});
}
/** Prepare session entries for compaction, or return undefined when compaction is not applicable. */
function prepareCompaction(pathEntries, settings, requestState) {
const lastEntry = pathEntries.at(-1);
if (!lastEntry || lastEntry.type === "reset" || lastEntry.type === "compaction" && lastEntry.fromHook) return ok(void 0);
let prevBoundaryIndex = -1;
for (let i = pathEntries.length - 1; i >= 0; i--) {
const type = pathEntries.at(i)?.type;
if (type === "compaction" || type === "reset") {
prevBoundaryIndex = i;
break;
}
}
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 = 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);
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 maxTokens = Math.min(Math.floor(.5 * settings.reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
const turnPrefixResult = await runSummarizationCompletion({
messages: turnPrefixMessages,
prompt: TURN_PREFIX_SUMMARIZATION_PROMPT,
customInstructions,
model,
maxTokens,
apiKey,
headers,
signal,
thinkingLevel,
streamFn,
runtime,
errorLabel: "Turn prefix summarization"
});
if (!turnPrefixResult.ok) return err(turnPrefixResult.error);
latestContext = `\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value}`;
}
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
const fileOperations = formatFileOperations(readFiles, modifiedFiles);
const preservedHistoryChars = Math.min(historyResult.value.length, Math.floor(MAX_COMPACTION_SUMMARY_CHARS / 2));
const latestContextBudget = 15954 - fileOperations.length - preservedHistoryChars;
latestContext = `${capCompactionSummary(latestContext, latestContextBudget)}${fileOperations}`;
const summary = capCompactionSummary(`${preparation.latestUnresolvedUserRequest ? `## Latest unresolved user request\n${JSON.stringify(preparation.latestUnresolvedUserRequest)}\n\n` : ""}${historyResult.value}${latestContext}`, MAX_COMPACTION_SUMMARY_CHARS, latestContext);
return ok({
summary,
firstKeptEntryId,
tokensBefore,
details: {
readFiles,
modifiedFiles,
...preparation.latestUnresolvedUserRequest ? { latestUnresolvedUserRequest: preparation.latestUnresolvedUserRequest } : {}
}
});
}
//#endregion
export { consumeAgentCoreStream as A, formatFileOperations as C, CompactionError as D, BranchSummaryError as E, resolveAgentCoreStreamFn as M, InvalidSummaryOutputError as O, extractSummaryText as S, serializeConversation as T, shouldCompact as _, SUMMARY_TRUNCATED_MARKER as a, createFileOps as b, capCompactionSummary as c, estimateTokens as d, findCutPoint as f, prepareCompaction as g, getLastAssistantUsage as h, SUMMARIZATION_SYSTEM_PROMPT as i, resolveAgentCoreCompleteFn as j, resolveAgentReasoningOption as k, compact as l, generateSummary as m, IMAGE_BLOCK_TOKENS as n, TURN_PREFIX_SUMMARIZATION_PROMPT as o, findTurnStartIndex as p, MAX_COMPACTION_SUMMARY_CHARS as r, calculateContextTokens as s, DEFAULT_COMPACTION_SETTINGS as t, estimateContextTokens as u, MAX_FILE_OPS_SECTION_CHARS as v, mergeSummaryFileOperations as w, extractFileOpsFromMessage as x, computeFileLists as y };