Store file lists in BranchSummaryEntry.details for cumulative tracking

- BranchSummaryResult now returns readFiles and modifiedFiles separately
- BranchSummaryDetails type for details: { readFiles, modifiedFiles }
- branchWithSummary accepts optional details parameter
- Collect files from existing branch_summary.details when preparing entries
- Files accumulate across nested branch summaries
This commit is contained in:
Mario Zechner 2025-12-29 21:50:03 +01:00
parent 04f2fcf004
commit 4ef3325cec
4 changed files with 111 additions and 95 deletions

View file

@ -3835,13 +3835,13 @@ export const MODELS = {
reasoning: true, reasoning: true,
input: ["text"], input: ["text"],
cost: { cost: {
input: 0.39, input: 0.35,
output: 1.9, output: 1.5,
cacheRead: 0, cacheRead: 0,
cacheWrite: 0, cacheWrite: 0,
}, },
contextWindow: 204800, contextWindow: 202752,
maxTokens: 204800, maxTokens: 65536,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"z-ai/glm-4.6:exacto": { "z-ai/glm-4.6:exacto": {
id: "z-ai/glm-4.6:exacto", id: "z-ai/glm-4.6:exacto",
@ -6104,9 +6104,9 @@ export const MODELS = {
contextWindow: 32768, contextWindow: 32768,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"anthropic/claude-3.5-haiku": { "anthropic/claude-3.5-haiku-20241022": {
id: "anthropic/claude-3.5-haiku", id: "anthropic/claude-3.5-haiku-20241022",
name: "Anthropic: Claude 3.5 Haiku", name: "Anthropic: Claude 3.5 Haiku (2024-10-22)",
api: "openai-completions", api: "openai-completions",
provider: "openrouter", provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1", baseUrl: "https://openrouter.ai/api/v1",
@ -6121,9 +6121,9 @@ export const MODELS = {
contextWindow: 200000, contextWindow: 200000,
maxTokens: 8192, maxTokens: 8192,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"anthropic/claude-3.5-haiku-20241022": { "anthropic/claude-3.5-haiku": {
id: "anthropic/claude-3.5-haiku-20241022", id: "anthropic/claude-3.5-haiku",
name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", name: "Anthropic: Claude 3.5 Haiku",
api: "openai-completions", api: "openai-completions",
provider: "openrouter", provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1", baseUrl: "https://openrouter.ai/api/v1",
@ -6359,6 +6359,23 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 16384, maxTokens: 16384,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": { "meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct", id: "meta-llama/llama-3.1-405b-instruct",
name: "Meta: Llama 3.1 405B Instruct", name: "Meta: Llama 3.1 405B Instruct",
@ -6393,23 +6410,6 @@ export const MODELS = {
contextWindow: 131072, contextWindow: 131072,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mistral-nemo": { "mistralai/mistral-nemo": {
id: "mistralai/mistral-nemo", id: "mistralai/mistral-nemo",
name: "Mistral: Mistral Nemo", name: "Mistral: Mistral Nemo",
@ -6546,6 +6546,23 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o": { "openai/gpt-4o": {
id: "openai/gpt-4o", id: "openai/gpt-4o",
name: "OpenAI: GPT-4o", name: "OpenAI: GPT-4o",
@ -6580,23 +6597,6 @@ export const MODELS = {
contextWindow: 128000, contextWindow: 128000,
maxTokens: 64000, maxTokens: 64000,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-70b-instruct": { "meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct", id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct", name: "Meta: Llama 3 70B Instruct",
@ -6835,23 +6835,6 @@ export const MODELS = {
contextWindow: 8191, contextWindow: 8191,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo": {
id: "openai/gpt-3.5-turbo",
name: "OpenAI: GPT-3.5 Turbo",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4": { "openai/gpt-4": {
id: "openai/gpt-4", id: "openai/gpt-4",
name: "OpenAI: GPT-4", name: "OpenAI: GPT-4",
@ -6869,6 +6852,23 @@ export const MODELS = {
contextWindow: 8191, contextWindow: 8191,
maxTokens: 4096, maxTokens: 4096,
} satisfies Model<"openai-completions">, } satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo": {
id: "openai/gpt-3.5-turbo",
name: "OpenAI: GPT-3.5 Turbo",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openrouter/auto": { "openrouter/auto": {
id: "openrouter/auto", id: "openrouter/auto",
name: "OpenRouter: Auto Router", name: "OpenRouter: Auto Router",

View file

@ -1644,6 +1644,7 @@ export class AgentSession {
// Run default summarizer if needed // Run default summarizer if needed
let summaryText: string | undefined; let summaryText: string | undefined;
let summaryDetails: unknown;
if (options.summarize && entriesToSummarize.length > 0 && !hookSummary) { if (options.summarize && entriesToSummarize.length > 0 && !hookSummary) {
const model = this.model!; const model = this.model!;
const apiKey = await this._modelRegistry.getApiKey(model); const apiKey = await this._modelRegistry.getApiKey(model);
@ -1666,8 +1667,13 @@ export class AgentSession {
throw new Error(result.error); throw new Error(result.error);
} }
summaryText = result.summary; summaryText = result.summary;
summaryDetails = {
readFiles: result.readFiles || [],
modifiedFiles: result.modifiedFiles || [],
};
} else if (hookSummary) { } else if (hookSummary) {
summaryText = hookSummary.summary; summaryText = hookSummary.summary;
summaryDetails = hookSummary.details;
} }
// Determine the new leaf position based on target type // Determine the new leaf position based on target type
@ -1698,7 +1704,7 @@ export class AgentSession {
let summaryEntry: BranchSummaryEntry | undefined; let summaryEntry: BranchSummaryEntry | undefined;
if (summaryText) { if (summaryText) {
// Create summary at target position (can be null for root) // Create summary at target position (can be null for root)
const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText); const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails);
summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry; summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;
} else if (newLeafId === null) { } else if (newLeafId === null) {
// No summary, navigating to root - reset leaf // No summary, navigating to root - reset leaf

View file

@ -18,10 +18,18 @@ import { estimateTokens } from "./compaction.js";
export interface BranchSummaryResult { export interface BranchSummaryResult {
summary?: string; summary?: string;
readFiles?: string[];
modifiedFiles?: string[];
aborted?: boolean; aborted?: boolean;
error?: string; error?: string;
} }
/** Details stored in BranchSummaryEntry.details for file tracking */
export interface BranchSummaryDetails {
readFiles: string[];
modifiedFiles: string[];
}
export interface FileOperations { export interface FileOperations {
read: Set<string>; read: Set<string>;
written: Set<string>; written: Set<string>;
@ -183,6 +191,10 @@ function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperation
* Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget. * Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget.
* This ensures we keep the most recent context when the branch is too long. * This ensures we keep the most recent context when the branch is too long.
* *
* Also collects file operations from:
* - Tool calls in assistant messages
* - Existing branch_summary entries' details (for cumulative tracking)
*
* @param entries - Entries in chronological order * @param entries - Entries in chronological order
* @param tokenBudget - Maximum tokens to include (0 = no limit) * @param tokenBudget - Maximum tokens to include (0 = no limit)
*/ */
@ -195,13 +207,30 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
}; };
let totalTokens = 0; let totalTokens = 0;
// Walk from newest to oldest to prioritize recent context // First pass: collect file ops from ALL entries (even if they don't fit in token budget)
// This ensures we capture cumulative file tracking from nested branch summaries
for (const entry of entries) {
if (entry.type === "branch_summary" && entry.details) {
const details = entry.details as BranchSummaryDetails;
if (Array.isArray(details.readFiles)) {
for (const f of details.readFiles) fileOps.read.add(f);
}
if (Array.isArray(details.modifiedFiles)) {
// Modified files go into both edited and written for proper deduplication
for (const f of details.modifiedFiles) {
fileOps.edited.add(f);
}
}
}
}
// Second pass: walk from newest to oldest, adding messages until token budget
for (let i = entries.length - 1; i >= 0; i--) { for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i]; const entry = entries[i];
const message = getMessageFromEntry(entry); const message = getMessageFromEntry(entry);
if (!message) continue; if (!message) continue;
// Extract file ops from assistant messages // Extract file ops from assistant messages (tool calls)
extractFileOpsFromMessage(message, fileOps); extractFileOpsFromMessage(message, fileOps);
const tokens = estimateTokens(message); const tokens = estimateTokens(message);
@ -238,32 +267,6 @@ const BRANCH_SUMMARY_PROMPT = `Summarize this conversation branch concisely for
Be brief and focused on what matters for future reference.`; Be brief and focused on what matters for future reference.`;
/**
* Format file operations as a static section to append to summary.
*/
function formatFileOperations(fileOps: FileOperations): string {
// Combine edited and written into "modified"
const modified = new Set([...fileOps.edited, ...fileOps.written]);
// Read-only = read but not modified
const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();
const sections: string[] = [];
if (readOnly.length > 0) {
sections.push(`<read-files>\n${readOnly.join("\n")}\n</read-files>`);
}
if (modified.size > 0) {
const files = [...modified].sort();
sections.push(`<modified-files>\n${files.join("\n")}\n</modified-files>`);
}
if (sections.length === 0) return "";
return `\n\n${sections.join("\n\n")}`;
}
/** /**
* Convert messages to text for the summarization prompt. * Convert messages to text for the summarization prompt.
*/ */
@ -358,13 +361,19 @@ export async function generateBranchSummary(
return { error: response.errorMessage || "Summarization failed" }; return { error: response.errorMessage || "Summarization failed" };
} }
let summary = response.content const summary = response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text") .filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text) .map((c) => c.text)
.join("\n"); .join("\n");
// Append static file operations section // Compute file lists for details
summary += formatFileOperations(fileOps); const modified = new Set([...fileOps.edited, ...fileOps.written]);
const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();
const modifiedFiles = [...modified].sort();
return { summary: summary || "No summary generated" }; return {
summary: summary || "No summary generated",
readFiles: readOnly,
modifiedFiles,
};
} }

View file

@ -872,7 +872,7 @@ export class SessionManager {
* Same as branch(), but also appends a branch_summary entry that captures * Same as branch(), but also appends a branch_summary entry that captures
* context from the abandoned conversation path. * context from the abandoned conversation path.
*/ */
branchWithSummary(branchFromId: string | null, summary: string): string { branchWithSummary(branchFromId: string | null, summary: string, details?: unknown): string {
if (branchFromId !== null && !this.byId.has(branchFromId)) { if (branchFromId !== null && !this.byId.has(branchFromId)) {
throw new Error(`Entry ${branchFromId} not found`); throw new Error(`Entry ${branchFromId} not found`);
} }
@ -884,6 +884,7 @@ export class SessionManager {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
fromId: branchFromId ?? "root", fromId: branchFromId ?? "root",
summary, summary,
details,
}; };
this._appendEntry(entry); this._appendEntry(entry);
return entry.id; return entry.id;