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.
This commit is contained in:
parent
efd6209bc8
commit
30262f9ffc
@ -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
|
||||
<!-- What were you working on? -->
|
||||
|
||||
## Key Decisions
|
||||
<!-- Important decisions made this session -->
|
||||
|
||||
## Next Steps
|
||||
<!-- What needs to happen next? -->
|
||||
`;
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user