File size: 8,097 Bytes
76289e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | // Context-engine delegates bridge custom engines to built-in compaction and memory prompt paths.
import { normalizeStructuredPromptSection } from "@openclaw/ai/internal/shared";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
buildMemoryPromptSection,
getActivePreparedMemoryPromptSection,
prepareMemoryPromptSection,
type MemoryPromptSectionParams,
type PreparedMemoryPromptSection,
} from "../plugins/memory-state.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
import {
isRuntimeCompactionDelegate,
markRuntimeCompactionDelegate,
} from "./compaction-watchdog.js";
import type {
ContextEngine,
CompactResult,
ContextEngineRuntimeContext,
ContextEngineSessionTarget,
} from "./types.js";
const loadCompactRuntime = createLazyRuntimeModule(
() => import("../agents/embedded-agent-runner/compact.runtime.js"),
);
function assertCompactionSessionIdentity(params: {
agentId?: string;
sessionId: string;
sessionKey?: string;
sessionTarget?: ContextEngineSessionTarget;
}): void {
const targetAgentId = normalizeOptionalString(params.sessionTarget?.agentId);
const targetSessionId = normalizeOptionalString(params.sessionTarget?.sessionId);
const targetSessionKey = normalizeOptionalString(params.sessionTarget?.sessionKey);
const requestedAgentId = normalizeOptionalString(params.agentId);
const callerSessionId = normalizeOptionalString(params.sessionId);
const requestedSessionKey = normalizeOptionalString(params.sessionKey);
const requestedSessionKeyAgentId = parseAgentSessionKey(requestedSessionKey)?.agentId;
if (
(requestedAgentId && targetAgentId && requestedAgentId !== targetAgentId) ||
(callerSessionId && targetSessionId && callerSessionId !== targetSessionId) ||
(requestedSessionKey && targetSessionKey && requestedSessionKey !== targetSessionKey) ||
(requestedSessionKeyAgentId && targetAgentId && requestedSessionKeyAgentId !== targetAgentId)
) {
throw new Error("Context-engine successor target conflicts with the caller session identity");
}
const agentId = targetAgentId ?? requestedAgentId;
const sessionKeyAgentId = parseAgentSessionKey(targetSessionKey ?? requestedSessionKey)?.agentId;
if (sessionKeyAgentId && agentId && sessionKeyAgentId !== agentId) {
throw new Error("Context-engine successor session key conflicts with its agent identity");
}
}
/**
* Delegate a context-engine compaction request to OpenClaw's built-in runtime compaction path.
*
* This is the same bridge used by the legacy context engine. Third-party
* engines can call it from their own `compact()` implementations when they do
* not own the compaction algorithm but still need `/compact` and overflow
* recovery to use the stock runtime behavior.
*
* Note: `compactionTarget` is part of the public `compact()` contract, but the
* built-in runtime compaction path does not expose that knob. This helper
* ignores it to preserve legacy behavior; engines that need target-specific
* compaction should implement their own `compact()` algorithm.
*/
export async function delegateCompactionToRuntime(
params: Parameters<ContextEngine["compact"]>[0],
): Promise<CompactResult> {
type RuntimeCompactionParams = Parameters<
Awaited<ReturnType<typeof loadCompactRuntime>>["compactEmbeddedAgentSessionOnDemand"]
>[0];
// runtimeContext carries host-resolved runtime fields set by internal
// callers. Keep the public delegate keyed by session identity, not by the
// active transcript artifact that the runtime may resolve internally.
const runtimeContext = (params.runtimeContext ?? {}) as ContextEngineRuntimeContext &
Partial<RuntimeCompactionParams>;
const { sessionFile: _legacySessionFile, ...runtimeContextParams } = runtimeContext;
const sessionTarget = params.sessionTarget ?? runtimeContext.sessionTarget;
const agentId = params.agentId ?? runtimeContext.agentId;
const sessionKey = params.sessionKey ?? runtimeContext.sessionKey;
// Reject contradictory caller identity before loading or invoking the compactor:
// target precedence inside the runtime must not hide an invalid request.
assertCompactionSessionIdentity({
agentId,
sessionId: params.sessionId,
sessionKey,
sessionTarget,
});
const { compactEmbeddedAgentSessionOnDemand } = await loadCompactRuntime();
const currentTokenCount =
params.currentTokenCount ??
(typeof runtimeContext.currentTokenCount === "number" &&
Number.isFinite(runtimeContext.currentTokenCount) &&
runtimeContext.currentTokenCount > 0
? Math.floor(runtimeContext.currentTokenCount)
: undefined);
const result = await compactEmbeddedAgentSessionOnDemand({
...runtimeContextParams,
// Preserve identity for the private recovery-accounting bridge.
contextEngineRuntimeContext: runtimeContext,
agentId,
sessionId: params.sessionId,
sessionKey,
sessionTarget,
tokenBudget: params.tokenBudget,
...(currentTokenCount !== undefined ? { currentTokenCount } : {}),
force: params.force,
customInstructions: params.customInstructions,
abortSignal: params.abortSignal,
workspaceDir:
typeof runtimeContext.workspaceDir === "string" ? runtimeContext.workspaceDir : process.cwd(),
});
return {
ok: result.ok,
compacted: result.compacted,
reason: result.reason,
result: result.result
? {
summary: result.result.summary,
firstKeptEntryId: result.result.firstKeptEntryId,
tokensBefore: result.result.tokensBefore,
tokensAfter: result.result.tokensAfter,
details: result.result.details,
...(result.result.sessionId ? { sessionId: result.result.sessionId } : {}),
// Core reports successors only through the typed sessionTarget; the
// deprecated raw sessionFile field is reserved for shipped engines
// reporting rotation to core, and post-flip core has no file path.
...(result.result.sessionTarget ? { sessionTarget: result.result.sessionTarget } : {}),
}
: undefined,
};
}
markRuntimeCompactionDelegate(delegateCompactionToRuntime);
/** True only for the canonical bridge whose runtime owns the compaction watchdog. */
export { isRuntimeCompactionDelegate };
/**
* Build a context-engine-ready systemPromptAddition from the active memory
* plugin prompt path. This lets non-legacy engines explicitly opt into the
* same memory/wiki guidance that the legacy engine gets via system prompt
* assembly, without reimplementing memory prompt formatting.
*/
function renderMemorySystemPromptAddition(
params: MemoryPromptSectionParams,
prepared?: PreparedMemoryPromptSection,
): string | undefined {
const lines = buildMemoryPromptSection(params, prepared);
if (lines.length === 0) {
return undefined;
}
const normalized = normalizeStructuredPromptSection(lines.join("\n"));
return normalized || undefined;
}
export function buildMemorySystemPromptAddition(
params: MemoryPromptSectionParams,
): string | undefined {
const prepared = getActivePreparedMemoryPromptSection();
if (!prepared) {
return renderMemorySystemPromptAddition(params);
}
const contextParams: MemoryPromptSectionParams = {
availableTools: params.availableTools,
citationsMode: params.citationsMode ?? prepared.context.citationsMode,
agentId: params.agentId ?? prepared.context.agentId,
agentSessionKey: params.agentSessionKey ?? prepared.context.agentSessionKey,
sandboxed: params.sandboxed ?? prepared.context.sandboxed,
};
return renderMemorySystemPromptAddition(contextParams, prepared);
}
/** Prepare memory state asynchronously, then render it without prompt-path I/O. */
export async function prepareMemorySystemPromptAddition(
params: MemoryPromptSectionParams,
): Promise<string | undefined> {
const prepared = await prepareMemoryPromptSection(params);
return renderMemorySystemPromptAddition(params, prepared);
}
|