diff --git a/devlog/2026-07-01_principle-driven-prompts/REQ.md b/devlog/2026-07-01_principle-driven-prompts/REQ.md new file mode 100644 index 0000000..5d947b2 --- /dev/null +++ b/devlog/2026-07-01_principle-driven-prompts/REQ.md @@ -0,0 +1,16 @@ +# Principle-Driven Compression Prompts + +## Problem +System prompt had 72 lines of detailed compression rules (7-item priority list, 3 pressure levels, hardcoded thresholds). Models mechanically followed rules regardless of context level — compressing at 6% context, losing critical task details. + +## Requirements +- R1: Simplify system prompt to high-level principles (~15 lines) +- R2: Per-message shows only context number (no compression guidance) +- R3: Every 10 percentage points (from 15%): show Tips with tool names (not commands) +- R4: Below 15%: no compression prompts at all +- R5: At 65%+: stronger tone about overflow risk +- R6: Add "BE FRUGAL" section with examples of obvious waste +- R7: Config: minNudgeContextPercent=15, growthPercent=10pp + +## Design Philosophy +Minimal intervention. Give smart models principles, not rules. Let them decide when/what to compress. diff --git a/devlog/2026-07-01_principle-driven-prompts/WORKLOG.md b/devlog/2026-07-01_principle-driven-prompts/WORKLOG.md new file mode 100644 index 0000000..b5ec951 --- /dev/null +++ b/devlog/2026-07-01_principle-driven-prompts/WORKLOG.md @@ -0,0 +1,48 @@ +# Worklog: Principle-Driven Compression Prompts + +## Changes (commit 77098fe, 11 files, +158/-118) + +### system.ts (72→27 lines) +- Removed: CONTEXT PRESSURE LEVELS, WHAT TO COMPRESS FIRST (7-item list), DO NOT RE-COMPRESS, WHAT TO COMPRESS CAREFULLY, BEFORE/AFTER COMPRESSING +- Added: 2 failure modes principle (over-compress loses detail, under-compress causes overflow) +- Added: BE FRUGAL section with 5 examples (command output, sub-agent results, training logs, duplicate reads, failed explorations) +- Fixed: Empty backtick tags → `` and `` + +### utils.ts (buildContextUsageGuidance) +- Removed: All guidance text ("Be frugal", "Extract and keep what matters", pressure level descriptions) +- Now returns: Just "Context usage: XK / 1000K tokens (X%)" — no suggestions + +### inject.ts (shouldInjectPerMessageNudge) +- Added: minNudgeContextPercent check (default 15%) — below 15%, no nudge +- Changed: Growth from relative (%) to absolute (percentage points) +- Changed: Default growth threshold 3→10pp +- Tips: Only tool names, no compression commands + +### nudge.ts (buildCompressedBlockGuidance) +- Removed: Consolidation suggestion +- Added: Block token counts in list — `b50 (76t), b51 (88t), ...` + +### range.ts + message.ts (compress tools) +- Removed: Dual-tier soft/hard limit +- Added: Single 3000-char limit +- Added: `summaryMaxChars` optional parameter for override +- Error: "Add summaryMaxChars parameter to allow longer summaries." + +### decompress.ts +- Added: `toFile` optional parameter — writes content to file without inflating context +- Block stays compressed when toFile is used + +### config.ts +- Added: minNudgeContextPercent (default 15) + +### tests/nudge-text.test.ts +- Updated: All assertions for simplified output + +## Config Updates (not in commit) +- acp.jsonc: nudgeFrequency=6, perMessageNudgeGrowthPercent=10 +- dcp.jsonc: nudgeFrequency=6, perMessageNudgeGrowthPercent=10 + +## Verification +- typecheck: 0 errors +- tests: 494 pass, 0 fail +- build: 330KB diff --git a/lib/compress/decompress.ts b/lib/compress/decompress.ts index 5f396da..dcda25c 100644 --- a/lib/compress/decompress.ts +++ b/lib/compress/decompress.ts @@ -82,6 +82,10 @@ function buildSchema() { blockId: tool.schema .string() .describe('Block reference to decompress (e.g., "b0", "b2")'), + toFile: tool.schema + .string() + .optional() + .describe("If provided, writes restored content to this file path instead of inflating context. Block stays compressed. Use read tool to access specific parts. Example: '/tmp/block52.txt'"), } } @@ -121,6 +125,33 @@ export function createDecompressTool(ctx: ToolContext): ReturnType return `Error: Block ${target.displayId} is not active. It may have already been decompressed.` } + if (args.toFile) { + const block = activeBlocks[0] + const msgIds = new Set(block.effectiveMessageIds ?? []) + const blockMessages = rawMessages.filter((m) => { + const id = (m as { id?: string }).id ?? (m as { messageId?: string }).messageId ?? "" + return msgIds.has(id) + }) + const lines = blockMessages.map((m) => { + const msg = m as { role?: string; type?: string; content?: unknown; text?: string } + const role = msg.role || msg.type || "unknown" + const content = + typeof msg.content === "string" + ? msg.content + : typeof msg.text === "string" + ? msg.text + : JSON.stringify(msg.content || msg.text || "") + return `[${role}]\n${content}` + }) + const { writeFile } = await import("fs/promises") + const fileContent = + lines.length > 0 + ? lines.join("\n\n---\n\n") + : (block.summary ?? "(no content available)") + await writeFile(args.toFile as string, fileContent, "utf-8") + return `Block b${target.displayId} content (${blockMessages.length} messages, ${fileContent.length} chars) written to ${args.toFile}. Block stays compressed — context unchanged. Use read tool to access specific parts.` + } + const activeMessagesBefore = snapshotActiveMessages(messagesState) const activeBlockIdsBefore = new Set(messagesState.activeBlockIds) diff --git a/lib/compress/message.ts b/lib/compress/message.ts index 8d46791..6b97812 100644 --- a/lib/compress/message.ts +++ b/lib/compress/message.ts @@ -13,7 +13,7 @@ import { } from "./state" import type { CompressMessageToolArgs } from "./types" -function buildSchema(maxSummaryLength: number) { +function buildSchema() { return { topic: tool.schema .string() @@ -32,11 +32,15 @@ function buildSchema(maxSummaryLength: number) { summary: tool.schema .string() .describe( - `Complete technical summary replacing that one message. Aim for <=${maxSummaryLength} chars; exceed only when strictly necessary to preserve critical detail (file paths, decisions, signatures, exact values). Never pad.`, + "Complete technical summary replacing that one message. Keep only essential details (conclusions, file paths, decisions, exact values).", ), }), ) .describe("Batch of individual message summaries to create in one tool call"), + summaryMaxChars: tool.schema + .number() + .optional() + .describe("Override max summary length if default (3000) is too small."), } } @@ -46,16 +50,16 @@ export function createCompressMessageTool(ctx: ToolContext): ReturnType maxSummaryLengthHard) { + if (entry.summary.length > maxLen) { throw new Error( - `Summary too long (${entry.summary.length} chars; limit ${maxSummaryLengthHard}). Rewrite to under ${maxSummaryLengthHard} chars — keep only the most essential details (conclusions, file paths, decisions, exact values) and drop verbose narration or raw dumps.`, + `Summary too long (${entry.summary.length} chars, max ${maxLen}). Rewrite to keep only essential details (conclusions, file paths, decisions, exact values) and drop verbose narration. Or add summaryMaxChars parameter to allow longer summaries.`, ) } } diff --git a/lib/compress/range.ts b/lib/compress/range.ts index 76699ef..c4e79d1 100644 --- a/lib/compress/range.ts +++ b/lib/compress/range.ts @@ -26,7 +26,7 @@ import { } from "./state" import type { CompressRangeToolArgs } from "./types" -function buildSchema(maxSummaryLength: number) { +function buildSchema() { return { topic: tool.schema .string() @@ -45,13 +45,19 @@ function buildSchema(maxSummaryLength: number) { summary: tool.schema .string() .describe( - `Complete technical summary replacing all content in range. Aim for <=${maxSummaryLength} chars; exceed only when strictly necessary to preserve critical detail (file paths, decisions, signatures, exact values). Never pad.`, + "Complete technical summary replacing all content in range. Keep only essential details (conclusions, file paths, decisions, exact values).", ), }), ) .describe( "One or more ranges to compress, each with start/end boundaries and a summary", ), + summaryMaxChars: tool.schema + .number() + .optional() + .describe( + "Override max summary length if default (3000) is too small. Use only when the range contains critical detail requiring more space.", + ), } } @@ -61,16 +67,16 @@ export function createCompressRangeTool(ctx: ToolContext): ReturnType maxSummaryLengthHard) { + if (entry.summary.length > maxLen) { throw new Error( - `Summary too long (${entry.summary.length} chars; limit ${maxSummaryLengthHard}). Rewrite to under ${maxSummaryLengthHard} chars — keep only the most essential details (conclusions, file paths, decisions, exact values) and drop verbose narration or raw dumps.`, + `Summary too long (${entry.summary.length} chars, max ${maxLen}). Rewrite to keep only essential details (conclusions, file paths, decisions, exact values) and drop verbose narration. Or add summaryMaxChars parameter to allow longer summaries.`, ) } } diff --git a/lib/config.ts b/lib/config.ts index c926780..10edee0 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -25,6 +25,7 @@ export interface CompressConfig { modelMinLimits?: Record nudgeFrequency: number perMessageNudgeGrowthPercent: number + minNudgeContextPercent: number iterationNudgeThreshold: number nudgeForce: "strong" | "soft" protectedTools: string[] @@ -192,6 +193,7 @@ const defaultConfig: PluginConfig = { minContextLimit: "45%", nudgeFrequency: 5, perMessageNudgeGrowthPercent: 3, + minNudgeContextPercent: 15, iterationNudgeThreshold: 15, nudgeForce: "soft", protectedTools: [...COMPRESS_DEFAULT_PROTECTED_TOOLS], @@ -402,6 +404,7 @@ function mergeCompress( modelMinLimits: override.modelMinLimits ?? base.modelMinLimits, nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency, perMessageNudgeGrowthPercent: override.perMessageNudgeGrowthPercent ?? base.perMessageNudgeGrowthPercent, + minNudgeContextPercent: override.minNudgeContextPercent ?? base.minNudgeContextPercent, iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold, nudgeForce: override.nudgeForce ?? base.nudgeForce, protectedTools: [...new Set([...base.protectedTools, ...(override.protectedTools ?? [])])], diff --git a/lib/messages/inject/inject.ts b/lib/messages/inject/inject.ts index 7260d45..c2624ee 100644 --- a/lib/messages/inject/inject.ts +++ b/lib/messages/inject/inject.ts @@ -61,18 +61,19 @@ function shouldInjectPerMessageNudge( currentTokens?: number, modelContextLimit?: number, ): boolean { - const turn = state.currentTurn ?? 0 - const lastTurn = state.nudges.lastPerMessageNudgeTurn ?? 0 - const turnsSinceLast = turn - lastTurn - const tokens = currentTokens ?? 0 + const pct = modelContextLimit ? (tokens / modelContextLimit) * 100 : 0 + + // Below minimum threshold — no tips + const minPercent = config.compress.minNudgeContextPercent ?? 15 + if (pct < minPercent) return false + + // Fire every N percentage points (absolute, not relative) const lastTokens = state.nudges.lastPerMessageNudgeTokens ?? 0 - const tokenGrowth = tokens - lastTokens - const tokenGrowthPercent = modelContextLimit ? (tokenGrowth / modelContextLimit) * 100 : 0 + const lastPct = modelContextLimit ? (lastTokens / modelContextLimit) * 100 : 0 + const growthThreshold = config.compress.perMessageNudgeGrowthPercent ?? 10 - const frequency = config.compress.nudgeFrequency ?? 5 - const growthThreshold = config.compress.perMessageNudgeGrowthPercent ?? 3 - return turnsSinceLast >= frequency || tokenGrowthPercent >= growthThreshold + return (pct - lastPct) >= growthThreshold } export const injectCompressNudges = ( @@ -205,6 +206,13 @@ export const injectCompressNudges = ( } if (shouldNudge) { + const pct = modelContextLimit ? ((currentTokens ?? 0) / modelContextLimit) * 100 : 0 + const tips = pct >= 65 + ? "\n\n⚠️ Context is high. Keep context tidy to reduce overflow risk. Tools: compress, decompress, search_context." + : "\n\n💡 Tools: compress, decompress, search_context." + if (suffixMessage) { + appendToLastTextPart(suffixMessage, tips) + } state.nudges.lastPerMessageNudgeTurn = state.currentTurn ?? 0 state.nudges.lastPerMessageNudgeTokens = currentTokens ?? 0 } @@ -251,7 +259,7 @@ function injectVisibleIdRange(state: SessionState, messages: WithParts[], target visibleRefs.sort() const first = visibleRefs[0] const last = visibleRefs[visibleRefs.length - 1] - const rangeTag = `\n\n[Visible message IDs: ${first} to ${last} (${visibleRefs.length} messages). Only use IDs in this range for compress.]` + const rangeTag = `\n\n[Visible messages: ${first} to ${last} (${visibleRefs.length} messages)]` for (const part of target.parts) { if (part.type === "text") { diff --git a/lib/messages/inject/utils.ts b/lib/messages/inject/utils.ts index 7ce4c97..bc5f315 100644 --- a/lib/messages/inject/utils.ts +++ b/lib/messages/inject/utils.ts @@ -391,25 +391,7 @@ export function buildContextUsageGuidance( const percentage = pct.toFixed(1) const formatK = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n)) - const minPct = resolveThresholdPercent(config.compress.minContextLimit, modelContextLimit) ?? 45 - const maxPct = resolveThresholdPercent(config.compress.maxContextLimit, modelContextLimit) ?? 55 - - const base = `Context usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%).` - - if (minimal) { - return `\n\n${base}` - } - - let guidance: string - if (pct < minPct) { - guidance = " 💡 Be frugal with context. If any visible tool output exceeds 5000 characters and you've finished reading it, compress it into a summary now — don't keep large outputs 'just in case'. You can decompress later if needed." - } else if (pct < maxPct) { - guidance = " ⚠️ Context is growing — compress completed sections and high-token waste now." - } else { - guidance = " 🔥 Context is high — compress aggressively, preserve only what is essential." - } - - return `\n\n${base}${guidance}` + return `\n\nContext usage: ${formatK(currentTokens)} / ${formatK(modelContextLimit)} tokens (${percentage}%).` } export function applyAnchoredNudges( diff --git a/lib/prompts/extensions/nudge.ts b/lib/prompts/extensions/nudge.ts index ca472c3..88e78f3 100644 --- a/lib/prompts/extensions/nudge.ts +++ b/lib/prompts/extensions/nudge.ts @@ -22,7 +22,11 @@ export function buildCompressedBlockGuidance( .filter((id) => Number.isInteger(id) && id > 0) .sort((a, b) => a - b) - const refs = activeBlockIds.map((id) => `b${id}`) + const refs = activeBlockIds.map((id) => { + const block = state.prune.messages.blocksById.get(id) + const tokens = block?.summaryTokens ?? 0 + return `b${id}${tokens > 0 ? ` (${tokens}t)` : ""}` + }) const blockCount = refs.length let blockList: string if (blockCount <= 20) { @@ -35,13 +39,11 @@ export function buildCompressedBlockGuidance( const includeHint = context?.includeHint ?? true const lines = [ - "Compressed block context:", - `- Active compressed blocks: ${blockCount} (${blockList})`, - "- System auto-detects blocks in range — no need to manually list (bN) placeholders. Just write a short prose summary.", + `- Compressed blocks: ${blockCount} (${blockList})`, ] if (includeHint) { - lines.push("- 💡 When you've finished using tool outputs, compress them — you can decompress later if needed. Lean context improves accuracy.") + lines.push("- 💡 Tools: compress, decompress, search_context.") } if (blockCount > 50) { @@ -90,8 +92,6 @@ export function buildCompressedBlockGuidance( lines.push(...targets) lines.push(` System auto-detects blocks in range — no need to manually list (bN) placeholders. Just write a short prose summary.`) } - } else { - lines.push(`- 🔀 You have ${blockCount} blocks — use compress to consolidate adjacent same-topic blocks.`) } } diff --git a/lib/prompts/system.ts b/lib/prompts/system.ts index 34cb829..029bd5a 100644 --- a/lib/prompts/system.ts +++ b/lib/prompts/system.ts @@ -4,69 +4,15 @@ You operate in a context-constrained environment. Context management helps prese The tools you have for context management are \`compress\`, \`decompress\`, and \`search_context\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details. \`search_context\` searches compressed block summaries (and visible messages) to locate relevant content before you decompress. -\`\` and \`\` tags are environment-injected metadata. Do not output them. - COMPRESSION PHILOSOPHY -Compression replaces raw conversation content with dense summaries. When used correctly, it keeps your context sharp and focused. When used carelessly, it destroys information you need. - -The key principle: compress selectively to keep context lean — but never compress content you're actively using for an ongoing task. Compression is for COMPLETED work, not work in progress. Before compressing, ask: "Will I need this in the next few turns?" If yes or unsure, keep it. Large tool outputs (shell, diffs, logs) can be compressed into summaries after the task using them is done — you can decompress later if needed. Extract and keep what matters: user intent, key decisions, file paths, and important findings — even if buried in large messages. Compress everything else, including verbose parts of user messages, large code dumps, and long discussions. - -Target the largest UNCOMPRESSED content first. Savings scale with original size — compressing a 5000-token tool output frees far more than re-shrinking an already-summarized 300-token block. - -CONTEXT PRESSURE LEVELS - -- Normal: After completing a task or sub-task, compress its tool outputs (agent results, verbose commands, large tool outputs) into summaries. Do NOT compress content you're actively using for an ongoing task — wait until the task is complete. You can decompress later if needed. -- Elevated: Context is growing — compress completed sections and high-token waste now. -- Critical: Compress aggressively now — preserve only what is essential for the current task. - -WHAT TO COMPRESS FIRST (high value, low risk) - -- Agent/subagent review and consultation results: Prime compression targets when context pressure rises — the surrounding reasoning and tool-call chatter is typically the largest block of uncompressed content. Note: if the agent tool is in your protected list, its output is auto-preserved in the summary, so the savings come from the surrounding conversation, not the agent output itself. Compress once you have fully consumed the results (all recommended actions applied or recorded in files). Recover via \`decompress\` while the block is still active. Re-invoking the agent is a last resort — it is a fresh run, not a cache hit. -- Verbose command output (build/test runs, git diff/log/status, publish logs, directory listings): Once you have read the result, compress. Keep only the verdict — pass/fail status, commit hash, version number, or count. For failures, keep the specific error messages and file/line references needed to act on them. The full output is reproducible by re-running the command. -- Exploration that led nowhere (failed approaches, dead-end searches): Compress to a one-line note about what was tried and why it failed. -- Redundant tool results (reading the same file multiple times, repeated status checks, exhausted search results): Keep only the most recent result. -- Intermediate steps of completed multi-step tasks: Once the task is done, compress the process. Keep only the final outcome. -- Resolved discussion threads (clarification rounds, negotiated requirements, design debate that reached a decision): Once a conclusion is recorded, compress the back-and-forth. Keep the decision and its rationale. -- Large file contents that have already been used and are no longer needed: Compress to a summary of key functions, types, or patterns. - -DO NOT RE-COMPRESS (low value, diminishing returns) - -- Already-compressed block summaries: Re-compressing a summary into a shorter summary saves negligible tokens. If a block needs better detail, use \`decompress\` to restore it, then compress the original content properly. Exception: if a block-aging warning flags specific block IDs as facing GC truncation, re-summarize exactly those flagged blocks into a fresh range — this preserves detail that GC would otherwise destroy. -- Short messages (1-3 sentences): The compression overhead (block metadata, summary structure) may exceed the tokens saved. -- Content whose immediate use is complete — the task it supported is done and no open todo/plan references it. If still in active use, let it stay. -- User instructions and requirements: These must remain visible until the task is complete. -- Tool calls that are still pending or in-progress: Wait until the result is returned and consumed. - -WHAT TO COMPRESS CAREFULLY (high risk - verify before compressing) - -- Temporary secrets/keys/tokens needed later: Do NOT compress unless recorded elsewhere -- File paths and directory structures: Keep in summary - losing these wastes tokens rediscovering them -- Key function/method signatures and APIs: Summarize with exact names and signatures -- Critical error messages and stack traces: Keep the error type and key detail in summary -- User preferences and requirements: These must survive compression intact -- Architectural decisions and rationale: Summarize the decision, not just the conclusion - -BEFORE COMPRESSING IMPORTANT CONTENT - -Verify the information is persisted in one of: -- A file you have written or edited -- An issue, PR, or devlog entry -- The compression summary itself (include the critical bits explicitly) - -If it is not persisted anywhere, either persist it first or include it explicitly in your compression summary. - -AFTER COMPRESSING - -Generate recovery breadcrumbs in your summary so future-you can reconstruct the context: -- Reference specific files by path -- Include key variable names, function signatures, or configuration values -- Note what was decided and why, not just what was done -- Example: "Implemented auth check in src/middleware.ts using validateToken() from auth.ts - user table is users not user" +All compression serves the primary task, but be frugal. Two failure modes to avoid: +- Over-compression: Compressing too aggressively loses critical details, decisions, and state needed for your task. This directly harms task quality. +- Under-compression: Failing to compress verbose outputs causes context overflow, reducing accuracy and eventually blocking your work. -If you later realize you need the original details from a compressed block, use \`decompress\` to restore them. You can decompress, read the content, then re-compress if needed. +Balance is key. Compress selectively to keep context lean. But never compress content you're actively using for an ongoing task. Use \`search_context\` to find compressed content when needed, and \`decompress\` to restore details. -Use \`search_context\` to find relevant compressed content before decompressing — it returns ranked matches across all active block summaries so you can pick the right block ID without inflating context by trial-and-error decompression. +BE FRUGAL -Use \`compress\` and \`decompress\` deliberately with quality-first summaries. Prioritize stale content intelligently to maintain a high-signal context window. +Be frugal with context — compress obvious waste promptly. Examples include verbose command output (build/test logs, git diff/status, npm install), sub-agent results once consumed, experiment/training logs (keep final metrics only), duplicate file reads, and failed explorations. Any content that is finished serving the task and would not be needed in upcoming turns should be compressed — not just these examples. ` diff --git a/tests/nudge-text.test.ts b/tests/nudge-text.test.ts index abb7aee..fb3ad65 100644 --- a/tests/nudge-text.test.ts +++ b/tests/nudge-text.test.ts @@ -96,21 +96,42 @@ test("buildCompressedBlockGuidance summarizes older blocks when there are more t assert.match(guidance, /b25/) }) -test("buildContextUsageGuidance low tier says 'Be frugal' and leaks no threshold numbers", () => { - const guidance = buildContextUsageGuidance(buildConfig(), LOW_USAGE, MODEL_CONTEXT_LIMIT) - - assert.match(guidance, /Be frugal/) - assert.doesNotMatch(guidance, /threshold/i) +test("buildContextUsageGuidance returns context number without compression guidance", () => { + const low = buildContextUsageGuidance(buildConfig(), LOW_USAGE, MODEL_CONTEXT_LIMIT) + const mid = buildContextUsageGuidance(buildConfig(), MODERATE_USAGE, MODEL_CONTEXT_LIMIT) + const high = buildContextUsageGuidance(buildConfig(), HIGH_USAGE, MODEL_CONTEXT_LIMIT) + + assert.match(low, /Context usage:/) + assert.match(mid, /Context usage:/) + assert.match(high, /Context usage:/) + + assert.doesNotMatch(low, /Be frugal|compress/i) + assert.doesNotMatch(mid, /growing|compress/i) + assert.doesNotMatch(high, /aggressive|compress/i) }) -test("buildContextUsageGuidance moderate tier says 'Context is growing'", () => { - const guidance = buildContextUsageGuidance(buildConfig(), MODERATE_USAGE, MODEL_CONTEXT_LIMIT) +test("buildCompressedBlockGuidance shows token counts for blocks with summaryTokens", () => { + const state = createSessionState() + for (const id of [1, 2, 3]) { + state.prune.messages.activeBlockIds.add(id) + state.prune.messages.blocksById.set(id, { summaryTokens: id * 100 } as never) + } + + const guidance = buildCompressedBlockGuidance(state) - assert.match(guidance, /Context is growing/) + assert.match(guidance, /b1 \(100t\)/) + assert.match(guidance, /b2 \(200t\)/) + assert.match(guidance, /b3 \(300t\)/) }) -test("buildContextUsageGuidance high tier says 'Context is high'", () => { - const guidance = buildContextUsageGuidance(buildConfig(), HIGH_USAGE, MODEL_CONTEXT_LIMIT) +test("buildCompressedBlockGuidance omits token count when summaryTokens is 0 or missing", () => { + const state = createSessionState() + for (const id of [1, 2]) { + state.prune.messages.activeBlockIds.add(id) + } + + const guidance = buildCompressedBlockGuidance(state) - assert.match(guidance, /Context is high/) + assert.match(guidance, /b1/) + assert.doesNotMatch(guidance, /b1 \(0t\)/) })