mirror of https://github.com/openclaw/openclaw.git
fix: preserve persona and language continuity in compaction summaries (#10456)
Merged via squash.
Prepared head SHA: 4518fb20e1
Co-authored-by: keepitmello <71975659+keepitmello@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
parent
80e7da92ce
commit
72b6a11a83
|
|
@ -3,6 +3,7 @@
|
||||||
Docs: https://docs.openclaw.ai
|
Docs: https://docs.openclaw.ai
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.
|
- Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.
|
||||||
|
|
@ -28,6 +29,8 @@ Docs: https://docs.openclaw.ai
|
||||||
- Security/exec approvals: fail closed for Perl `-M` and `-I` approval flows so preload and load-path module resolution stays outside approval-backed runtime execution unless the operator uses a broader explicit trust path.
|
- Security/exec approvals: fail closed for Perl `-M` and `-I` approval flows so preload and load-path module resolution stays outside approval-backed runtime execution unless the operator uses a broader explicit trust path.
|
||||||
- Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.
|
- Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.
|
||||||
- macOS/onboarding: avoid self-restarting freshly bootstrapped launchd gateways and give new daemon installs longer to become healthy, so `openclaw onboard --install-daemon` no longer false-fails on slower Macs and fresh VM snapshots.
|
- macOS/onboarding: avoid self-restarting freshly bootstrapped launchd gateways and give new daemon installs longer to become healthy, so `openclaw onboard --install-daemon` no longer false-fails on slower Macs and fresh VM snapshots.
|
||||||
|
- Agents/compaction: preserve safeguard compaction summary language continuity via default and configurable custom instructions so persona drift is reduced after auto-compaction. (#10456) Thanks @keepitmello.
|
||||||
|
|
||||||
## 2026.3.12
|
## 2026.3.12
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export function buildEmbeddedExtensionFactories(params: {
|
||||||
contextWindowTokens: contextWindowInfo.tokens,
|
contextWindowTokens: contextWindowInfo.tokens,
|
||||||
identifierPolicy: compactionCfg?.identifierPolicy,
|
identifierPolicy: compactionCfg?.identifierPolicy,
|
||||||
identifierInstructions: compactionCfg?.identifierInstructions,
|
identifierInstructions: compactionCfg?.identifierInstructions,
|
||||||
|
customInstructions: compactionCfg?.customInstructions,
|
||||||
qualityGuardEnabled: qualityGuardCfg?.enabled ?? false,
|
qualityGuardEnabled: qualityGuardCfg?.enabled ?? false,
|
||||||
qualityGuardMaxRetries: qualityGuardCfg?.maxRetries,
|
qualityGuardMaxRetries: qualityGuardCfg?.maxRetries,
|
||||||
model: params.model,
|
model: params.model,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
DEFAULT_COMPACTION_INSTRUCTIONS,
|
||||||
|
resolveCompactionInstructions,
|
||||||
|
composeSplitTurnInstructions,
|
||||||
|
} from "./compaction-instructions.js";
|
||||||
|
|
||||||
|
describe("DEFAULT_COMPACTION_INSTRUCTIONS", () => {
|
||||||
|
it("is a non-empty string", () => {
|
||||||
|
expect(typeof DEFAULT_COMPACTION_INSTRUCTIONS).toBe("string");
|
||||||
|
expect(DEFAULT_COMPACTION_INSTRUCTIONS.trim().length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains language preservation directive", () => {
|
||||||
|
expect(DEFAULT_COMPACTION_INSTRUCTIONS).toContain("primary language");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains factual content directive", () => {
|
||||||
|
expect(DEFAULT_COMPACTION_INSTRUCTIONS).toContain("factual content");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not exceed MAX_INSTRUCTION_LENGTH (800 chars)", () => {
|
||||||
|
expect(DEFAULT_COMPACTION_INSTRUCTIONS.length).toBeLessThanOrEqual(800);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveCompactionInstructions", () => {
|
||||||
|
describe("null / undefined handling", () => {
|
||||||
|
it("returns DEFAULT when both args are undefined", () => {
|
||||||
|
expect(resolveCompactionInstructions(undefined, undefined)).toBe(
|
||||||
|
DEFAULT_COMPACTION_INSTRUCTIONS,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns DEFAULT when both args are explicitly null (untyped JS caller)", () => {
|
||||||
|
expect(
|
||||||
|
resolveCompactionInstructions(null as unknown as undefined, null as unknown as undefined),
|
||||||
|
).toBe(DEFAULT_COMPACTION_INSTRUCTIONS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("empty and whitespace normalization", () => {
|
||||||
|
it("treats empty-string event as absent -- runtime wins", () => {
|
||||||
|
const result = resolveCompactionInstructions("", "runtime value");
|
||||||
|
expect(result).toBe("runtime value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats whitespace-only event as absent -- runtime wins", () => {
|
||||||
|
const result = resolveCompactionInstructions(" ", "runtime value");
|
||||||
|
expect(result).toBe("runtime value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats tab/newline-only event as absent -- runtime wins", () => {
|
||||||
|
const result = resolveCompactionInstructions("\t\n\r", "runtime value");
|
||||||
|
expect(result).toBe("runtime value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats empty-string runtime as absent -- DEFAULT wins", () => {
|
||||||
|
const result = resolveCompactionInstructions(undefined, "");
|
||||||
|
expect(result).toBe(DEFAULT_COMPACTION_INSTRUCTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats whitespace-only runtime as absent -- DEFAULT wins", () => {
|
||||||
|
const result = resolveCompactionInstructions(undefined, " ");
|
||||||
|
expect(result).toBe(DEFAULT_COMPACTION_INSTRUCTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls through to DEFAULT when both are empty strings", () => {
|
||||||
|
expect(resolveCompactionInstructions("", "")).toBe(DEFAULT_COMPACTION_INSTRUCTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls through to DEFAULT when both are whitespace-only", () => {
|
||||||
|
expect(resolveCompactionInstructions(" ", "\t\n")).toBe(DEFAULT_COMPACTION_INSTRUCTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("non-breaking space (\\u00A0) IS trimmed by ES2015+ trim() -- falls through", () => {
|
||||||
|
const nbsp = "\u00A0";
|
||||||
|
const result = resolveCompactionInstructions(nbsp, "runtime");
|
||||||
|
expect(result).toBe("runtime");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("KNOWN_EDGE: zero-width space (\\u200B) survives normalization -- invisible string used as instructions", () => {
|
||||||
|
const zws = "\u200B";
|
||||||
|
const result = resolveCompactionInstructions(zws, "runtime");
|
||||||
|
expect(result).toBe(zws);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("precedence", () => {
|
||||||
|
it("event wins over runtime when both are non-empty", () => {
|
||||||
|
const result = resolveCompactionInstructions("event value", "runtime value");
|
||||||
|
expect(result).toBe("event value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runtime wins when event is undefined", () => {
|
||||||
|
const result = resolveCompactionInstructions(undefined, "runtime value");
|
||||||
|
expect(result).toBe("runtime value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("event is trimmed before use", () => {
|
||||||
|
const result = resolveCompactionInstructions(" event ", "runtime");
|
||||||
|
expect(result).toBe("event");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runtime is trimmed before use", () => {
|
||||||
|
const result = resolveCompactionInstructions(undefined, " runtime ");
|
||||||
|
expect(result).toBe("runtime");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("truncation at 800 chars", () => {
|
||||||
|
it("does NOT truncate string of exactly 800 chars", () => {
|
||||||
|
const exact800 = "A".repeat(800);
|
||||||
|
const result = resolveCompactionInstructions(exact800, undefined);
|
||||||
|
expect(result).toHaveLength(800);
|
||||||
|
expect(result).toBe(exact800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates string of 801 chars to 800", () => {
|
||||||
|
const over = "B".repeat(801);
|
||||||
|
const result = resolveCompactionInstructions(over, undefined);
|
||||||
|
expect(result).toHaveLength(800);
|
||||||
|
expect(result).toBe("B".repeat(800));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates very long string to exactly 800", () => {
|
||||||
|
const huge = "C".repeat(5000);
|
||||||
|
const result = resolveCompactionInstructions(huge, undefined);
|
||||||
|
expect(result).toHaveLength(800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncation applies AFTER trimming -- 810 raw chars with 10 leading spaces yields 800", () => {
|
||||||
|
const padded = " ".repeat(10) + "D".repeat(800);
|
||||||
|
const result = resolveCompactionInstructions(padded, undefined);
|
||||||
|
expect(result).toHaveLength(800);
|
||||||
|
expect(result).toBe("D".repeat(800));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncation applies to runtime fallback as well", () => {
|
||||||
|
const longRuntime = "R".repeat(1000);
|
||||||
|
const result = resolveCompactionInstructions(undefined, longRuntime);
|
||||||
|
expect(result).toHaveLength(800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates by code points, not code units (emoji safe)", () => {
|
||||||
|
const emojis801 = "\u{1F600}".repeat(801);
|
||||||
|
const result = resolveCompactionInstructions(emojis801, undefined);
|
||||||
|
expect(Array.from(result)).toHaveLength(800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not split surrogate pair when cut lands inside a pair", () => {
|
||||||
|
const input = "X" + "\u{1F600}".repeat(800);
|
||||||
|
const result = resolveCompactionInstructions(input, undefined);
|
||||||
|
const codePoints = Array.from(result);
|
||||||
|
expect(codePoints).toHaveLength(800);
|
||||||
|
expect(codePoints[0]).toBe("X");
|
||||||
|
// Every code point in the truncated result must be a complete character (no lone surrogates)
|
||||||
|
for (const cp of codePoints) {
|
||||||
|
const code = cp.codePointAt(0)!;
|
||||||
|
const isLoneSurrogate = code >= 0xd800 && code <= 0xdfff;
|
||||||
|
expect(isLoneSurrogate).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("return type", () => {
|
||||||
|
it("always returns a string, never undefined or null", () => {
|
||||||
|
const cases: [string | undefined, string | undefined][] = [
|
||||||
|
[undefined, undefined],
|
||||||
|
["", ""],
|
||||||
|
[" ", " "],
|
||||||
|
[null as unknown as undefined, null as unknown as undefined],
|
||||||
|
["valid", undefined],
|
||||||
|
[undefined, "valid"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [event, runtime] of cases) {
|
||||||
|
const result = resolveCompactionInstructions(event, runtime);
|
||||||
|
expect(typeof result).toBe("string");
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("composeSplitTurnInstructions", () => {
|
||||||
|
it("joins turn prefix, separator, and resolved instructions with double newlines", () => {
|
||||||
|
const result = composeSplitTurnInstructions("Turn prefix here", "Resolved instructions here");
|
||||||
|
expect(result).toBe(
|
||||||
|
"Turn prefix here\n\nAdditional requirements:\n\nResolved instructions here",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("output contains the turn prefix verbatim", () => {
|
||||||
|
const prefix = "Summarize the last 5 messages.";
|
||||||
|
const result = composeSplitTurnInstructions(prefix, "Keep it short.");
|
||||||
|
expect(result).toContain(prefix);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("output contains the resolved instructions verbatim", () => {
|
||||||
|
const instructions = "Write in Korean. Preserve persona.";
|
||||||
|
const result = composeSplitTurnInstructions("prefix", instructions);
|
||||||
|
expect(result).toContain(instructions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("output contains 'Additional requirements:' separator", () => {
|
||||||
|
const result = composeSplitTurnInstructions("a", "b");
|
||||||
|
expect(result).toContain("Additional requirements:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("KNOWN_EDGE: empty turnPrefix produces leading blank line", () => {
|
||||||
|
const result = composeSplitTurnInstructions("", "instructions");
|
||||||
|
expect(result).toBe("\n\nAdditional requirements:\n\ninstructions");
|
||||||
|
expect(result.startsWith("\n")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("KNOWN_EDGE: empty resolvedInstructions produces trailing blank area", () => {
|
||||||
|
const result = composeSplitTurnInstructions("prefix", "");
|
||||||
|
expect(result).toBe("prefix\n\nAdditional requirements:\n\n");
|
||||||
|
expect(result.endsWith("\n\n")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not deduplicate if instructions already contain 'Additional requirements:'", () => {
|
||||||
|
const instructions = "Additional requirements: keep it short.";
|
||||||
|
const result = composeSplitTurnInstructions("prefix", instructions);
|
||||||
|
const count = (result.match(/Additional requirements:/g) || []).length;
|
||||||
|
expect(count).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves multiline content in both inputs", () => {
|
||||||
|
const prefix = "Line 1\nLine 2";
|
||||||
|
const instructions = "Rule A\nRule B\nRule C";
|
||||||
|
const result = composeSplitTurnInstructions(prefix, instructions);
|
||||||
|
expect(result).toContain("Line 1\nLine 2");
|
||||||
|
expect(result).toContain("Rule A\nRule B\nRule C");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* Compaction instruction utilities.
|
||||||
|
*
|
||||||
|
* Provides default language-preservation instructions and a precedence-based
|
||||||
|
* resolver for customInstructions used during context compaction summaries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default instructions injected into every safeguard-mode compaction summary.
|
||||||
|
* Preserves conversation language and persona while keeping the SDK's required
|
||||||
|
* summary structure intact.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_COMPACTION_INSTRUCTIONS =
|
||||||
|
"Write the summary body in the primary language used in the conversation.\n" +
|
||||||
|
"Focus on factual content: what was discussed, decisions made, and current state.\n" +
|
||||||
|
"Keep the required summary structure and section headers unchanged.\n" +
|
||||||
|
"Do not translate or alter code, file paths, identifiers, or error messages.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upper bound on custom instruction length to prevent prompt bloat.
|
||||||
|
* ~800 chars ≈ ~200 tokens — keeps summarization quality stable.
|
||||||
|
*/
|
||||||
|
const MAX_INSTRUCTION_LENGTH = 800;
|
||||||
|
|
||||||
|
function truncateUnicodeSafe(s: string, maxCodePoints: number): string {
|
||||||
|
const chars = Array.from(s);
|
||||||
|
if (chars.length <= maxCodePoints) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return chars.slice(0, maxCodePoints).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(s: string | undefined): string | undefined {
|
||||||
|
if (s == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const trimmed = s.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve compaction instructions with precedence:
|
||||||
|
* event (SDK) → runtime (config) → DEFAULT constant.
|
||||||
|
*
|
||||||
|
* Each input is normalized first (trim + empty→undefined) so that blank
|
||||||
|
* strings don't short-circuit the fallback chain.
|
||||||
|
*/
|
||||||
|
export function resolveCompactionInstructions(
|
||||||
|
eventInstructions: string | undefined,
|
||||||
|
runtimeInstructions: string | undefined,
|
||||||
|
): string {
|
||||||
|
const resolved =
|
||||||
|
normalize(eventInstructions) ??
|
||||||
|
normalize(runtimeInstructions) ??
|
||||||
|
DEFAULT_COMPACTION_INSTRUCTIONS;
|
||||||
|
return truncateUnicodeSafe(resolved, MAX_INSTRUCTION_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose split-turn instructions by combining the SDK's turn-prefix
|
||||||
|
* instructions with the resolved compaction instructions.
|
||||||
|
*/
|
||||||
|
export function composeSplitTurnInstructions(
|
||||||
|
turnPrefixInstructions: string,
|
||||||
|
resolvedInstructions: string,
|
||||||
|
): string {
|
||||||
|
return [turnPrefixInstructions, "Additional requirements:", resolvedInstructions].join("\n\n");
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ export type CompactionSafeguardRuntimeValue = {
|
||||||
contextWindowTokens?: number;
|
contextWindowTokens?: number;
|
||||||
identifierPolicy?: AgentCompactionIdentifierPolicy;
|
identifierPolicy?: AgentCompactionIdentifierPolicy;
|
||||||
identifierInstructions?: string;
|
identifierInstructions?: string;
|
||||||
|
customInstructions?: string;
|
||||||
/**
|
/**
|
||||||
* Model to use for compaction summarization.
|
* Model to use for compaction summarization.
|
||||||
* Passed through runtime because `ctx.model` is undefined in the compact.ts workflow
|
* Passed through runtime because `ctx.model` is undefined in the compact.ts workflow
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ import { collectTextContentBlocks } from "../content-blocks.js";
|
||||||
import { wrapUntrustedPromptDataBlock } from "../sanitize-for-prompt.js";
|
import { wrapUntrustedPromptDataBlock } from "../sanitize-for-prompt.js";
|
||||||
import { repairToolUseResultPairing } from "../session-transcript-repair.js";
|
import { repairToolUseResultPairing } from "../session-transcript-repair.js";
|
||||||
import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js";
|
import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js";
|
||||||
|
import {
|
||||||
|
composeSplitTurnInstructions,
|
||||||
|
resolveCompactionInstructions,
|
||||||
|
} from "./compaction-instructions.js";
|
||||||
import { getCompactionSafeguardRuntime } from "./compaction-safeguard-runtime.js";
|
import { getCompactionSafeguardRuntime } from "./compaction-safeguard-runtime.js";
|
||||||
|
|
||||||
const log = createSubsystemLogger("compaction-safeguard");
|
const log = createSubsystemLogger("compaction-safeguard");
|
||||||
|
|
@ -697,7 +701,7 @@ async function readWorkspaceContextForSummary(): Promise<string> {
|
||||||
|
|
||||||
export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||||
api.on("session_before_compact", async (event, ctx) => {
|
api.on("session_before_compact", async (event, ctx) => {
|
||||||
const { preparation, customInstructions, signal } = event;
|
const { preparation, customInstructions: eventInstructions, signal } = event;
|
||||||
if (!preparation.messagesToSummarize.some(isRealConversationMessage)) {
|
if (!preparation.messagesToSummarize.some(isRealConversationMessage)) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Compaction safeguard: cancelling compaction with no real conversation messages to summarize.",
|
"Compaction safeguard: cancelling compaction with no real conversation messages to summarize.",
|
||||||
|
|
@ -715,6 +719,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||||
// Model resolution: ctx.model is undefined in compact.ts workflow (extensionRunner.initialize() is never called).
|
// Model resolution: ctx.model is undefined in compact.ts workflow (extensionRunner.initialize() is never called).
|
||||||
// Fall back to runtime.model which is explicitly passed when building extension paths.
|
// Fall back to runtime.model which is explicitly passed when building extension paths.
|
||||||
const runtime = getCompactionSafeguardRuntime(ctx.sessionManager);
|
const runtime = getCompactionSafeguardRuntime(ctx.sessionManager);
|
||||||
|
const customInstructions = resolveCompactionInstructions(
|
||||||
|
eventInstructions,
|
||||||
|
runtime?.customInstructions,
|
||||||
|
);
|
||||||
const summarizationInstructions = {
|
const summarizationInstructions = {
|
||||||
identifierPolicy: runtime?.identifierPolicy,
|
identifierPolicy: runtime?.identifierPolicy,
|
||||||
identifierInstructions: runtime?.identifierInstructions,
|
identifierInstructions: runtime?.identifierInstructions,
|
||||||
|
|
@ -892,7 +900,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||||
reserveTokens,
|
reserveTokens,
|
||||||
maxChunkTokens,
|
maxChunkTokens,
|
||||||
contextWindow: contextWindowTokens,
|
contextWindow: contextWindowTokens,
|
||||||
customInstructions: `${TURN_PREFIX_INSTRUCTIONS}\n\n${currentInstructions}`,
|
customInstructions: composeSplitTurnInstructions(
|
||||||
|
TURN_PREFIX_INSTRUCTIONS,
|
||||||
|
currentInstructions,
|
||||||
|
),
|
||||||
summarizationInstructions,
|
summarizationInstructions,
|
||||||
previousSummary: undefined,
|
previousSummary: undefined,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,8 @@ export type AgentCompactionConfig = {
|
||||||
reserveTokensFloor?: number;
|
reserveTokensFloor?: number;
|
||||||
/** Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5). */
|
/** Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5). */
|
||||||
maxHistoryShare?: number;
|
maxHistoryShare?: number;
|
||||||
|
/** Additional compaction-summary instructions that can preserve language or persona continuity. */
|
||||||
|
customInstructions?: string;
|
||||||
/** Preserve this many most-recent user/assistant turns verbatim in compaction summary context. */
|
/** Preserve this many most-recent user/assistant turns verbatim in compaction summary context. */
|
||||||
recentTurnsPreserve?: number;
|
recentTurnsPreserve?: number;
|
||||||
/** Identifier-preservation instruction policy for compaction summaries. */
|
/** Identifier-preservation instruction policy for compaction summaries. */
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ export const AgentDefaultsSchema = z
|
||||||
keepRecentTokens: z.number().int().positive().optional(),
|
keepRecentTokens: z.number().int().positive().optional(),
|
||||||
reserveTokensFloor: z.number().int().nonnegative().optional(),
|
reserveTokensFloor: z.number().int().nonnegative().optional(),
|
||||||
maxHistoryShare: z.number().min(0.1).max(0.9).optional(),
|
maxHistoryShare: z.number().min(0.1).max(0.9).optional(),
|
||||||
|
customInstructions: z.string().optional(),
|
||||||
identifierPolicy: z
|
identifierPolicy: z
|
||||||
.union([z.literal("strict"), z.literal("off"), z.literal("custom")])
|
.union([z.literal("strict"), z.literal("off"), z.literal("custom")])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue