openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
201 lines (198 loc) • 8.4 kB
JavaScript
import { y as parseDateStringTimestampMs } from "./number-coercion-CLj0HTDM.js";
import { u as selectResetKeptEntries } from "./tool-result-pairing-Bccl6DhI.js";
import { stripCompactionReplayCheckpoint } from "@openclaw/ai/transports";
//#region packages/agent-core/src/harness/messages.ts
function asAgentMessage(message) {
return message;
}
function requireSessionTimestampMs(value, label) {
const parsed = parseDateStringTimestampMs(value);
if (parsed === void 0) throw new Error(`${label} must be a valid timestamp`);
return parsed;
}
function normalizeCompactionSummaryTimestamp(timestamp) {
if (typeof timestamp === "number") return timestamp;
return parseDateStringTimestampMs(timestamp) ?? 0;
}
const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
<summary>
`;
const COMPACTION_SUMMARY_SUFFIX = `
</summary>`;
const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:
<summary>
`;
const BRANCH_SUMMARY_SUFFIX = `</summary>`;
/** Render a shell execution record as user-visible context text for the model. */
function bashExecutionToText(msg) {
let text = `Ran \`${msg.command}\`\n`;
if (msg.output) text += `\`\`\`\n${msg.output}\n\`\`\``;
else text += "(no output)";
if (msg.cancelled) text += "\n\n(command cancelled)";
else if (msg.exitCode !== null && msg.exitCode !== void 0 && msg.exitCode !== 0) text += `\n\nCommand exited with code ${msg.exitCode}`;
if (msg.truncated && msg.fullOutputPath) text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`;
return text;
}
/** Build a persisted branch summary message from the repository timestamp string. */
function createBranchSummaryMessage(summary, fromId, timestamp) {
return {
role: "branchSummary",
summary,
fromId,
timestamp: requireSessionTimestampMs(timestamp, "branch summary timestamp")
};
}
/** Build a persisted compaction summary message from the repository timestamp string. */
function createCompactionSummaryMessage(summary, tokensBefore, timestamp) {
return {
role: "compactionSummary",
summary,
tokensBefore,
timestamp: requireSessionTimestampMs(timestamp, "compaction summary timestamp")
};
}
/** Build a custom transcript message that can be shown and replayed into context. */
function createCustomMessage(customType, content, display, details, timestamp) {
return {
role: "custom",
customType,
content,
display,
details,
timestamp: requireSessionTimestampMs(timestamp, "custom message timestamp")
};
}
/** Convert harness transcript messages into the LLM-facing message sequence. */
function convertToLlm(messages) {
return messages.map((m) => {
const message = m;
switch (message.role) {
case "bashExecution":
if (message.excludeFromContext) return;
return {
role: "user",
content: [{
type: "text",
text: bashExecutionToText(message)
}],
timestamp: message.timestamp
};
case "custom": {
if (message.excludeFromContext) return;
const content = typeof message.content === "string" ? [{
type: "text",
text: message.content
}] : message.content;
const runtimeContextCarrier = message.details?.runtimeContextCarrier === true;
return {
role: "user",
content,
timestamp: message.timestamp,
...runtimeContextCarrier ? { runtimeContextCarrier: true } : {}
};
}
case "branchSummary": return {
role: "user",
content: [{
type: "text",
text: BRANCH_SUMMARY_PREFIX + message.summary + BRANCH_SUMMARY_SUFFIX
}],
timestamp: message.timestamp
};
case "compactionSummary": return {
role: "user",
content: [{
type: "text",
text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX
}],
timestamp: normalizeCompactionSummaryTimestamp(message.timestamp)
};
case "user":
case "assistant":
case "toolResult": return message;
default: return;
}
}).filter((m) => m !== void 0);
}
//#endregion
//#region packages/agent-core/src/harness/session/session.ts
const SESSION_HISTORY_PRELUDE = Symbol.for("openclaw.sessionHistoryPrelude");
/** The same semantic cut is used before payload acquisition and when building messages. */
function resolveSessionContextWindow(entries) {
const boundaryIndex = entries.findLastIndex((entry) => entry.type === "reset" || entry.type === "compaction");
const firstKeptIndex = entries.findIndex((entry) => entry.id === entries[boundaryIndex]?.firstKeptEntryId);
return {
boundaryIndex,
firstKeptIndex: firstKeptIndex >= 0 && firstKeptIndex < boundaryIndex ? firstKeptIndex : boundaryIndex
};
}
/** Project persisted session entries into the message shared by replay and summarization. */
function projectSessionEntryMessage(entry) {
switch (entry.type) {
case "message": return "excludeFromContext" in entry.message && entry.message.excludeFromContext === true ? void 0 : entry.message;
case "custom_message": return asAgentMessage(createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp));
case "branch_summary": return asAgentMessage(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
case "compaction": return asAgentMessage(createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp));
default: return;
}
}
/** Select the canonical window using only navigation and tool-pairing facts. */
function* iterateSessionContextEntries(pathEntries) {
const { boundaryIndex, firstKeptIndex } = resolveSessionContextWindow(pathEntries);
const boundary = pathEntries[boundaryIndex];
const resetKept = boundary?.type === "reset" ? new Set(selectResetKeptEntries(pathEntries.slice(firstKeptIndex, boundaryIndex))) : void 0;
if (boundary) yield {
entry: boundary,
context: "current"
};
for (const [index, entry] of pathEntries.entries()) {
const retained = index < boundaryIndex;
if (index === boundaryIndex || retained && (index < firstKeptIndex || resetKept && !resetKept.has(entry))) continue;
if (!(entry.type === "message" || entry.type === "custom_message" || entry.type === "branch_summary") || !resetKept?.has(entry) && entry.type === "message" && "excludeFromContext" in entry.message && entry.message.excludeFromContext === true) continue;
yield {
entry,
context: retained ? resetKept ? "reset-retained" : "retained" : "current"
};
}
}
/** Hydrate selected messages lazily so bounded consumers can stop before later payloads. */
function* iterateSessionContextMessages(pathEntries, readEntry = (entry) => entry) {
for (const { entry, context } of iterateSessionContextEntries(pathEntries)) {
if (entry.type === "reset") continue;
const hydrated = readEntry(entry);
if (hydrated.type === "branch_summary" && !hydrated.summary) continue;
let message = context === "reset-retained" && hydrated.type === "message" ? hydrated.message : projectSessionEntryMessage(hydrated);
if (!message) continue;
if (context !== "current" && message.role === "assistant") message = stripCompactionReplayCheckpoint(message);
if (context === "reset-retained" && (message.role === "user" || message.role === "assistant")) {
message = { ...message };
Object.defineProperty(message, SESSION_HISTORY_PRELUDE, {
configurable: true,
enumerable: false,
value: true
});
}
yield message;
}
}
/** Build model context from an ordered session branch and its latest state markers. */
function buildSessionContext(pathEntries) {
let thinkingLevel = "off";
let model = null;
for (const entry of pathEntries) if (entry.type === "thinking_level_change") thinkingLevel = entry.thinkingLevel;
else if (entry.type === "model_change") model = {
provider: entry.provider,
modelId: entry.modelId
};
else if (entry.type === "message" && entry.message.role === "assistant") model = {
provider: entry.message.provider,
modelId: entry.message.model
};
return {
messages: Array.from(iterateSessionContextMessages(pathEntries)),
thinkingLevel,
model
};
}
//#endregion
export { BRANCH_SUMMARY_PREFIX as a, COMPACTION_SUMMARY_SUFFIX as c, projectSessionEntryMessage as i, bashExecutionToText as l, iterateSessionContextEntries as n, BRANCH_SUMMARY_SUFFIX as o, iterateSessionContextMessages as r, COMPACTION_SUMMARY_PREFIX as s, buildSessionContext as t, convertToLlm as u };