From 30262f9ffc1c74ab6bdfece0968f15a40f533116 Mon Sep 17 00:00:00 2001 From: sasheenmusic Date: Thu, 29 Jan 2026 10:50:02 -0800 Subject: [PATCH] feat(session_compact): add threshold check and auto-save compaction file Improvements: 1. threshold parameter - skip compaction if context below threshold % 2. Auto-saves compaction file to memory/compactions/ with timestamp 3. Returns file path in response so agent knows where to read This enables fully autonomous context management without manual steps. --- src/agents/tools/session-compact-tool.ts | 143 ++++++++++++++++++++--- 1 file changed, 130 insertions(+), 13 deletions(-) diff --git a/src/agents/tools/session-compact-tool.ts b/src/agents/tools/session-compact-tool.ts index 36b2a90a3..029622afa 100644 --- a/src/agents/tools/session-compact-tool.ts +++ b/src/agents/tools/session-compact-tool.ts @@ -1,4 +1,6 @@ import { Type } from "@sinclair/typebox"; +import * as fs from "node:fs"; +import * as path from "node:path"; import { abortEmbeddedPiRun, compactEmbeddedPiSession, @@ -11,18 +13,12 @@ import { loadSessionStore, resolveSessionFilePath, resolveStorePath, - updateSessionStore, } from "../../config/sessions.js"; import { resolveAgentIdFromSessionKey, DEFAULT_AGENT_ID } from "../../routing/session-key.js"; import { formatContextUsageShort, formatTokenCount } from "../../auto-reply/status.js"; import { incrementCompactionCount } from "../../auto-reply/reply/session-updates.js"; import type { AnyAgentTool } from "./common.js"; -import { readStringParam } from "./common.js"; -import { - resolveInternalSessionKey, - resolveMainSessionAlias, - createAgentToAgentPolicy, -} from "./sessions-helpers.js"; +import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js"; import { resolveDefaultModelForAgent } from "../../agents/model-selection.js"; const SessionCompactToolSchema = Type.Object({ @@ -32,6 +28,14 @@ const SessionCompactToolSchema = Type.Object({ "Optional instructions for what to focus on during compaction (e.g., 'Focus on decisions and open tasks')", }), ), + threshold: Type.Optional( + Type.Number({ + description: + "Only compact if context usage exceeds this percentage (default: 0, meaning always compact). Set to 60 to skip compaction when context is below 60%.", + minimum: 0, + maximum: 100, + }), + ), }); interface SessionCompactToolOpts { @@ -41,15 +45,77 @@ interface SessionCompactToolOpts { thinkLevel?: string; } +function formatTimestamp(): string { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + const seconds = String(now.getSeconds()).padStart(2, "0"); + return `${year}-${month}-${day}-${hours}${minutes}${seconds}`; +} + +function writeCompactionFile(params: { + workspaceDir: string; + tokensBefore?: number; + tokensAfter?: number; + contextBefore?: number; + contextAfter?: number; + instructions?: string; +}): string | null { + try { + const compactionsDir = path.join(params.workspaceDir, "memory", "compactions"); + if (!fs.existsSync(compactionsDir)) { + fs.mkdirSync(compactionsDir, { recursive: true }); + } + + const timestamp = formatTimestamp(); + const filename = `${timestamp}.md`; + const filepath = path.join(compactionsDir, filename); + + const content = `# Context Compaction - ${new Date().toLocaleString("en-US", { + timeZone: "America/Los_Angeles", + dateStyle: "full", + timeStyle: "short", + })} + +## Compaction Summary +- **Tokens before:** ${params.tokensBefore ? formatTokenCount(params.tokensBefore) : "unknown"} +- **Tokens after:** ${params.tokensAfter ? formatTokenCount(params.tokensAfter) : "unknown"} +- **Context before:** ${params.contextBefore ? `${params.contextBefore}%` : "unknown"} +- **Context after:** ${params.contextAfter ? `${params.contextAfter}%` : "unknown"} +${params.instructions ? `- **Focus:** ${params.instructions}` : ""} + +## Instructions +Read this file after compaction to restore context. Add your working state below. + +## Active Task + + +## Key Decisions + + +## Next Steps + +`; + + fs.writeFileSync(filepath, content, "utf-8"); + return filepath; + } catch { + return null; + } +} + export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgentTool { return { label: "Session Compact", name: "session_compact", description: - "Compact the current session's context to free up token space. Use when context is above 60% to proactively manage memory. The compaction summarizes older conversation history while preserving recent messages. After compaction, read your latest compaction file from memory/compactions/ to restore state.", + "Compact the current session's context to free up token space. Use when context is above 60% to proactively manage memory. The compaction summarizes older conversation history while preserving recent messages. Automatically saves a compaction file to memory/compactions/ and returns the path.", parameters: SessionCompactToolSchema, execute: async (_toolCallId, args) => { - const params = args as { instructions?: string }; + const params = args as { instructions?: string; threshold?: number }; const cfg = opts?.config ?? loadConfig(); const { mainKey, alias } = resolveMainSessionAlias(cfg); @@ -77,6 +143,31 @@ export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgen }; } + // Check threshold - skip if context is below threshold + const threshold = params.threshold ?? 0; + const contextTokens = entry.contextTokens ?? 200_000; // Default to 200k if unknown + const totalTokens = entry.totalTokens ?? (entry.inputTokens ?? 0) + (entry.outputTokens ?? 0); + const currentContextPercent = + contextTokens > 0 ? Math.round((totalTokens / contextTokens) * 100) : 0; + + if (threshold > 0 && currentContextPercent < threshold) { + return { + content: [ + { + type: "text", + text: `⏭️ Compaction skipped: context at ${currentContextPercent}% is below ${threshold}% threshold.`, + }, + ], + details: { + ok: true, + compacted: false, + skipped: true, + reason: `context ${currentContextPercent}% < threshold ${threshold}%`, + currentContextPercent, + }, + }; + } + const sessionId = entry.sessionId; // Abort any active run before compacting @@ -131,26 +222,49 @@ export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgen }); } + // Calculate context percentages for the compaction file const tokensAfterCompaction = result.result?.tokensAfter; - const totalTokens = + const contextAfterPercent = + contextTokens > 0 && tokensAfterCompaction + ? Math.round((tokensAfterCompaction / contextTokens) * 100) + : undefined; + + // Auto-save compaction file + let compactionFilePath: string | null = null; + if (result.ok && result.compacted && workspaceDir) { + compactionFilePath = writeCompactionFile({ + workspaceDir, + tokensBefore: result.result?.tokensBefore, + tokensAfter: result.result?.tokensAfter, + contextBefore: currentContextPercent, + contextAfter: contextAfterPercent, + instructions: params.instructions, + }); + } + + const newTotalTokens = tokensAfterCompaction ?? entry.totalTokens ?? (entry.inputTokens ?? 0) + (entry.outputTokens ?? 0); const contextSummary = formatContextUsageShort( - totalTokens > 0 ? totalTokens : null, + newTotalTokens > 0 ? newTotalTokens : null, entry.contextTokens ?? null, ); const reason = result.reason?.trim(); - const line = reason + const statusLine = reason ? `${compactLabel}: ${reason} • ${contextSummary}` : `${compactLabel} • ${contextSummary}`; + const fileNote = compactionFilePath + ? `\n\n📁 Compaction file saved: \`${compactionFilePath}\`\nRead this file to restore your working context.` + : "\n\nNext: Read your latest file from memory/compactions/ to restore context state."; + return { content: [ { type: "text", - text: `🧹 ${line}\n\nNext: Read your latest file from memory/compactions/ to restore context state.`, + text: `🧹 ${statusLine}${fileNote}`, }, ], details: { @@ -158,6 +272,9 @@ export function createSessionCompactTool(opts?: SessionCompactToolOpts): AnyAgen compacted: result.compacted, tokensBefore: result.result?.tokensBefore, tokensAfter: result.result?.tokensAfter, + contextBefore: currentContextPercent, + contextAfter: contextAfterPercent, + compactionFile: compactionFilePath, reason: result.reason, }, };