Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ beforeEach(() => {

function summary() {
return {
childBlockRefs: [],
sections: {
"Current Objective": "Continue the current task",
"User Constraints": "Preserve explicit user constraints",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ beforeEach(() => {

function summary(childBlockRefs: string[] = []) {
return {
childBlockRefs,
sections: {
"Current Objective": childBlockRefs.length === 0 ? "Continue the current task" : `Continue after (${childBlockRefs[0]})`,
"User Constraints": "Preserve explicit user constraints",
Expand Down Expand Up @@ -176,6 +175,26 @@ describe("hybrid compression hooks", () => {
expect(JSON.stringify(strong.messages)).toContain("strong nudge");
});

test("lists active block refs and explains materialization in compression nudges", async () => {
const store = makeStore(8);
const child = prepareDynamicRangeCompression(
store.getState(),
{ startId: "m0002", endId: "m0003", summary: summary() },
1000,
);
expect(child.ok).toBe(true);
if (!child.ok) throw new Error("expected child compression success");
store.setState({ compression: child.state });
const hook = createHybridCompressionHook(silentLogger);
const call = callCtx(store, 550);

await hook.beforeModelCall(call);

const rendered = JSON.stringify(call.messages);
expect(rendered).toContain("Active compressed blocks: b1");
expect(rendered).toContain("complete stored summary should be inserted");
});

test("runs forced hard compact at exactly 85% when a safe range exists", async () => {
const store = makeStore();
const hook = createHybridCompressionHook(silentLogger);
Expand Down
17 changes: 14 additions & 3 deletions packages/agent-core/src/agents/query/hooks/hybrid-compression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ export function createHybridCompressionHook(
const pressure = getCompressionTokenPressure(ctx.store, ctx.binding.modelInfo.limit.context);
if (pressure === null || pressure.ratio < SOFT_NUDGE_RATIO || pressure.ratio >= HARD_COMPACT_RATIO) return;
const strength = pressure.ratio >= STRONG_NUDGE_RATIO ? "strong" : "soft";
ctx.messages.push(compressionNudgeMessage(strength, pressure.ratio));
ctx.messages.push(compressionNudgeMessage(
strength,
pressure.ratio,
ctx.store.getState().compression.activeBlockRefs,
));
};

return { beforeModelBuild, beforeModelCall, circuitBreaker, scheduleToolOutputRecoveryNotice };
Expand All @@ -91,13 +95,20 @@ function toolOutputRecoveryNotice(count: number): ModelCallMessage {
};
}

function compressionNudgeMessage(strength: "soft" | "strong", ratio: number): ModelCallMessage {
function compressionNudgeMessage(
strength: "soft" | "strong",
ratio: number,
activeBlockRefs: readonly string[],
): ModelCallMessage {
const percent = Math.floor(ratio * 100);
const guidance = strength === "strong"
? "Context pressure is high. Dynamic compression is an in-conversation tool action: use the compress tool on a safe older range only if it helps before the hard safety threshold. Do not compress the latest two rounds or protected content."
: "Context pressure is rising. Keep responses concise and consider whether an older safe range should be dynamically compressed later.";
const blockGuidance = activeBlockRefs.length === 0
? "There are no active compressed blocks."
: `Active compressed blocks: ${activeBlockRefs.join(", ")}. If a selected range contains one, place each required (bN) placeholder exactly once where its complete stored summary should be inserted.`;
return {
role: "user",
content: [{ type: "text", text: `<system-reminder>\nDynamic compression ${strength} nudge at ${percent}% context pressure. ${guidance}\n</system-reminder>` }],
content: [{ type: "text", text: `<system-reminder>\nDynamic compression ${strength} nudge at ${percent}% context pressure. ${guidance} ${blockGuidance}\n</system-reminder>` }],
};
}
2 changes: 1 addition & 1 deletion packages/agent-core/src/compression/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const DCP_PARITY_ITEMS = [
"stable_session_local_block_refs",
"range_compression_by_start_end_ref",
"model_callable_compress_contract",
"nested_blocks_with_placeholder_validation",
"nested_blocks_with_materialized_child_summaries",
"active_inactive_superseded_lifecycle",
"protected_content_contracts",
"user_messages_preserve_canonical_originals",
Expand Down
10 changes: 5 additions & 5 deletions packages/agent-core/src/compression/dcp-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ const EXECUTABLE_DCP_PARITY_COVERAGE = {
anchors: ["startId: \"m0001\"", "endId: \"m0004\"", "summary: summary()"],
},
],
nested_blocks_with_placeholder_validation: [
nested_blocks_with_materialized_child_summaries: [
{
file: "packages/agent-core/src/compression/summary.test.ts",
testName: "summary requires child placeholders exactly once",
anchors: ["validateCompressionSummary(summary, [\"b1\"]).ok", "Resume Instructions"],
testName: "replaces a child placeholder with the complete stored summary",
anchors: ["CHILD_SENTINEL", "not.toContain(\"(b1)\")"],
},
{
file: "packages/agent-core/src/compression/dynamic-range.test.ts",
testName: "nested parent requires child placeholder exactly once and supersedes the child",
anchors: ["summary([\"b1\"])", "supersededBy"],
testName: "three-level nesting stores a self-contained top summary",
anchors: ["LEVEL_THREE", "LEVEL_ONE_SENTINEL", "activeBlockRefs).toEqual([\"b3\"])"],
},
],
active_inactive_superseded_lifecycle: [
Expand Down
132 changes: 125 additions & 7 deletions packages/agent-core/src/compression/dynamic-range.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { describe, expect, test } from "bun:test";
import { createEmptyCompressionState, prepareDynamicRangeCompression, purgeRepeatedOldErrors } from "./index";
import type { CompressionSummary } from "./types";
import {
createEmptyCompressionState,
prepareDynamicRangeCompression,
purgeRepeatedOldErrors,
renderCompressionSummary,
} from "./index";
import type { CompressionSummaryTemplate } from "./types";
import type { SessionStoreState, StoredMessage } from "../store/types";
import {
createEmptySessionStats,
type AssistantSessionPart,
type UserSessionPart,
} from "@archcode/protocol";

function summary(childBlockRefs: string[] = []): CompressionSummary {
function summary(childBlockRefs: string[] = [], objective?: string): CompressionSummaryTemplate {
return {
childBlockRefs: childBlockRefs as CompressionSummary["childBlockRefs"],
sections: {
"Current Objective": childBlockRefs.length > 0 ? "Continue after nested child block" : "Continue task",
"Current Objective": objective ?? (childBlockRefs.length > 0 ? "Continue after nested child block" : "Continue task"),
"User Constraints": "Preserve user constraints",
"Decisions Made": "Dynamic range compression is model-authored",
"Open Tasks": "Continue implementation",
Expand Down Expand Up @@ -199,9 +203,13 @@ describe("dynamic range compression", () => {
expect(result.event.type).toBe("compression.block_failed");
});

test("nested parent requires child placeholder exactly once and supersedes the child", () => {
test("nested parent materializes the child summary and supersedes its lineage block", () => {
const state = baseState(fourMessages());
const child = prepareDynamicRangeCompression(state, { startId: "m0002", endId: "m0003", summary: summary() }, 1000);
const child = prepareDynamicRangeCompression(state, {
startId: "m0002",
endId: "m0003",
summary: summary([], "CHILD_SUMMARY_SENTINEL"),
}, 1000);
expect(child.ok).toBe(true);
if (!child.ok) throw new Error("expected child success");

Expand All @@ -214,6 +222,116 @@ describe("dynamic range compression", () => {
expect(parent.state.blocksByRef.b2?.status).toBe("active");
expect(parent.state.blocksByRef.b1?.supersededBy).toBe("b2");
expect(parent.state.activeBlockRefs).toEqual(["b2"]);
expect(parent.block.summary.sections["Child Block Refs"]).toContain("CHILD_SUMMARY_SENTINEL");
expect(JSON.stringify(parent.block.summary)).not.toContain("(b1)");
});

test("nested parent rejects a summary that omits its runtime-derived child placeholder", () => {
const state = baseState(fourMessages());
const child = prepareDynamicRangeCompression(state, {
startId: "m0002",
endId: "m0003",
summary: summary([], "OMITTED_CHILD_SENTINEL"),
}, 1000);
expect(child.ok).toBe(true);
if (!child.ok) throw new Error("expected child success");

const parent = prepareDynamicRangeCompression(
{ ...state, compression: child.state },
{ startId: "m0001", endId: "m0004", summary: summary() },
2000,
);

expect(parent.ok).toBe(false);
if (parent.ok) throw new Error("expected parent rejection");
expect(parent.code).toBe("summary_rejected");
expect(parent.issues[0]?.message).toContain("Required child placeholder (b1) must appear exactly once; found 0");
});

test("three-level nesting stores a self-contained top summary", () => {
const state = baseState(fourMessages());
const child = prepareDynamicRangeCompression(state, {
startId: "m0002",
endId: "m0003",
summary: summary([], "LEVEL_ONE_SENTINEL"),
}, 1000);
expect(child.ok).toBe(true);
if (!child.ok) throw new Error("expected child success");
const parent = prepareDynamicRangeCompression(
{ ...state, compression: child.state },
{ startId: "m0001", endId: "m0004", summary: summary(["b1"], "LEVEL_TWO") },
2000,
);
expect(parent.ok).toBe(true);
if (!parent.ok) throw new Error("expected parent success");
const grandparent = prepareDynamicRangeCompression(
{ ...state, compression: parent.state },
{ startId: "b2", endId: "b2", summary: summary(["b2"], "LEVEL_THREE") },
3000,
);

expect(grandparent.ok).toBe(true);
if (!grandparent.ok) throw new Error("expected grandparent success");
const rendered = JSON.stringify(grandparent.block.summary);
expect(rendered).toContain("LEVEL_THREE");
expect(rendered).toContain("LEVEL_TWO");
expect(rendered).toContain("LEVEL_ONE_SENTINEL");
expect(rendered).not.toMatch(/\(b[12]\)/);
expect(grandparent.state.activeBlockRefs).toEqual(["b3"]);
expect(grandparent.state.blocksByRef.b2?.supersededBy).toBe("b3");
});

test("nested token estimate counts sibling child summaries once plus only uncovered messages", () => {
const messages = fourMessages();
messages.push(
message("msg-7", "user", [text("t7", "seven")]),
message("msg-8", "assistant", [output("t8", "eight")]),
);
messages[1] = message("msg-2", "assistant", [output("t2-large", "x".repeat(8_000))]);
messages[2] = message("msg-3", "user", [text("t3-large", "y".repeat(8_000))]);
messages[3] = message("msg-4", "assistant", [output("t4-large", "z".repeat(8_000))]);
messages[4] = message("msg-5", "user", [text("t5-large", "w".repeat(8_000))]);
const state = baseState(messages);
const firstChild = prepareDynamicRangeCompression(state, {
startId: "m0002",
endId: "m0003",
summary: summary([], "FIRST_CONDENSED_CHILD"),
}, 1000);
expect(firstChild.ok).toBe(true);
if (!firstChild.ok) throw new Error("expected first child success");
const secondChild = prepareDynamicRangeCompression(
{ ...state, compression: firstChild.state },
{
startId: "m0004",
endId: "m0005",
summary: summary([], "SECOND_CONDENSED_CHILD"),
},
1500,
);
expect(secondChild.ok).toBe(true);
if (!secondChild.ok) throw new Error("expected second child success");
const parent = prepareDynamicRangeCompression(
{ ...state, compression: secondChild.state },
{ startId: "m0001", endId: "m0006", summary: summary(["b1", "b2"]) },
2000,
);
expect(parent.ok).toBe(true);
if (!parent.ok) throw new Error("expected parent success");

const expectedOriginalChars = renderCompressionSummary(firstChild.block.summary).length
+ renderCompressionSummary(secondChild.block.summary).length
+ JSON.stringify(messages[0]!.parts).length
+ JSON.stringify(messages[5]!.parts).length;
expect(parent.block.tokenEstimate).toEqual({
originalTokens: Math.ceil(expectedOriginalChars / 4),
summaryTokens: Math.ceil(renderCompressionSummary(parent.block.summary).length / 4),
savedTokens: Math.max(
0,
Math.ceil(expectedOriginalChars / 4)
- Math.ceil(renderCompressionSummary(parent.block.summary).length / 4),
),
estimatedAt: 2000,
});
});

test("rejects partial active overlap", () => {
Expand Down
51 changes: 42 additions & 9 deletions packages/agent-core/src/compression/dynamic-range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
ProtectedRef,
} from "./types";
import { commitCompressionBlock, createEmptyCompressionState, recordCompressionFailure, CompressionStateError } from "./state";
import { renderCompressionSummary } from "./summary";
import { materializeCompressionSummaryTemplate, renderCompressionSummary } from "./summary";
import { collectProtectedRefsForRange } from "./protection";
import { deduplicateCompletedToolOutputs, type DeduplicatedToolOutputGroup } from "./deduplication";
import { purgeRepeatedOldErrors, type PurgedRepeatedErrorGroup } from "./purge-errors";
Expand Down Expand Up @@ -75,16 +75,34 @@ export function prepareDynamicRangeCompression(
);
}

let materializedSummary: CompressionSummary;
try {
materializedSummary = materializeCompressionSummaryTemplate(
summary.summary,
resolved.value.requiredChildRefs,
stateWithRefs.blocksByRef,
);
} catch (error) {
const issue = stateIssue(error);
return reject(stateWithRefs, issue.code, issue.message, [issue], [], input, now);
}

const draft: CompressionBlockDraft = {
id: crypto.randomUUID(),
canonicalBlockId: crypto.randomUUID(),
strategy: "dynamic-range",
trigger: "model_tool_call",
range: resolved.value.range,
summary: summary.summary,
summary: materializedSummary,
protectedRefs: [],
childBlockRefs: summary.summary.childBlockRefs,
tokenEstimate: estimateCompressionTokens(storeState, resolved.value.range, summary.summary, now),
childBlockRefs: resolved.value.requiredChildRefs,
tokenEstimate: estimateCompressionTokens(
storeState,
resolved.value.range,
resolved.value.requiredChildRefs,
materializedSummary,
now,
),
createdAt: now,
};

Expand Down Expand Up @@ -135,7 +153,7 @@ export function compressionBlockSnapshot(block: CompressionBlock): CompressionBl
trigger: block.trigger,
range: block.range,
summary: { sections: { ...block.summary.sections } },
childBlockRefs: block.childBlockRefs,
childBlockRefs: [...block.childBlockRefs],
protectedRefs: block.protectedRefs.map((ref) => ref.ref),
...(block.tokenEstimate === undefined ? {} : { tokenEstimate: block.tokenEstimate }),
createdAt: block.createdAt,
Expand Down Expand Up @@ -192,13 +210,28 @@ function compressionFailureSnapshot(failure: CompressionFailure): CompressionFai
function estimateCompressionTokens(
storeState: SessionStoreState,
range: CompressionBlockDraft["range"],
childBlockRefs: readonly CompressionBlock["ref"][],
summary: CompressionSummary,
now: number,
): CompressionBlockDraft["tokenEstimate"] {
const originalChars = storeState.messages
.slice(range.startIndex, range.endIndex + 1)
.map((message) => JSON.stringify(message.parts))
.join("\n").length;
const childBlocks = childBlockRefs
.map((ref) => storeState.compression?.blocksByRef[ref])
.filter((block): block is CompressionBlock => block !== undefined);
const coveredIndexes = new Set<number>();
for (const child of childBlocks) {
for (let index = child.range.startIndex; index <= child.range.endIndex; index += 1) {
coveredIndexes.add(index);
}
}

let originalChars = childBlocks
.map((child) => renderCompressionSummary(child.summary).length)
.reduce((total, length) => total + length, 0);
for (let index = range.startIndex; index <= range.endIndex; index += 1) {
if (coveredIndexes.has(index)) continue;
const message = storeState.messages[index];
if (message !== undefined) originalChars += JSON.stringify(message.parts).length;
}
const summaryChars = renderCompressionSummary(summary).length;
const originalTokens = Math.ceil(originalChars / 4);
const summaryTokens = Math.ceil(summaryChars / 4);
Expand Down
5 changes: 2 additions & 3 deletions packages/agent-core/src/compression/original-range.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "@archcode/protocol";
import { createEmptyCompressionState, prepareDynamicRangeCompression } from "./index";
import { resolveCompressionOriginalRange } from "./original-range";
import type { CompressionSummary } from "./types";
import type { BlockRef, CompressionSummaryTemplate } from "./types";
import type { SessionFile } from "../store/helpers";
import type { SessionStoreState, StoredMessage } from "../store/types";

Expand All @@ -29,9 +29,8 @@ function finalizedResult(
};
}

function summary(childBlockRefs: CompressionSummary["childBlockRefs"] = []): CompressionSummary {
function summary(childBlockRefs: BlockRef[] = []): CompressionSummaryTemplate {
return {
childBlockRefs,
sections: {
"Current Objective": childBlockRefs.length > 0 ? "Continue task after child blocks" : "Continue task",
"User Constraints": "Preserve constraints",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/compression/original-range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function resolveCompressionOriginalRange(
status: block.status,
strategy: block.strategy,
trigger: block.trigger,
childBlockRefs: block.childBlockRefs,
childBlockRefs: [...block.childBlockRefs],
range: block.range,
coveredRefs: coveredEntries.map((entry) => entry.ref),
coveredMessageIds: coveredEntries.map((entry) => entry.message.id),
Expand Down
Loading