From 4ef3325ceca09e2ef27234d33c42007e2a73ed38 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 29 Dec 2025 21:50:03 +0100 Subject: [PATCH] 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 --- packages/ai/src/models.generated.ts | 122 +++++++++--------- .../coding-agent/src/core/agent-session.ts | 8 +- .../core/compaction/branch-summarization.ts | 73 ++++++----- .../coding-agent/src/core/session-manager.ts | 3 +- 4 files changed, 111 insertions(+), 95 deletions(-) diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 6f32cbb3..795336b6 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -3835,13 +3835,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.39, - output: 1.9, + input: 0.35, + output: 1.5, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 204800, - maxTokens: 204800, + contextWindow: 202752, + maxTokens: 65536, } satisfies Model<"openai-completions">, "z-ai/glm-4.6:exacto": { id: "z-ai/glm-4.6:exacto", @@ -6104,9 +6104,9 @@ export const MODELS = { contextWindow: 32768, maxTokens: 4096, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Anthropic: Claude 3.5 Haiku", + "anthropic/claude-3.5-haiku-20241022": { + id: "anthropic/claude-3.5-haiku-20241022", + name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -6121,9 +6121,9 @@ export const MODELS = { contextWindow: 200000, maxTokens: 8192, } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku-20241022": { - id: "anthropic/claude-3.5-haiku-20241022", - name: "Anthropic: Claude 3.5 Haiku (2024-10-22)", + "anthropic/claude-3.5-haiku": { + id: "anthropic/claude-3.5-haiku", + name: "Anthropic: Claude 3.5 Haiku", api: "openai-completions", provider: "openrouter", baseUrl: "https://openrouter.ai/api/v1", @@ -6359,6 +6359,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 16384, } 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": { id: "meta-llama/llama-3.1-405b-instruct", name: "Meta: Llama 3.1 405B Instruct", @@ -6393,23 +6410,6 @@ export const MODELS = { contextWindow: 131072, maxTokens: 4096, } 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": { id: "mistralai/mistral-nemo", name: "Mistral: Mistral Nemo", @@ -6546,6 +6546,23 @@ export const MODELS = { contextWindow: 128000, maxTokens: 4096, } 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": { id: "openai/gpt-4o", name: "OpenAI: GPT-4o", @@ -6580,23 +6597,6 @@ export const MODELS = { contextWindow: 128000, maxTokens: 64000, } 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": { id: "meta-llama/llama-3-70b-instruct", name: "Meta: Llama 3 70B Instruct", @@ -6835,23 +6835,6 @@ export const MODELS = { contextWindow: 8191, maxTokens: 4096, } 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": { id: "openai/gpt-4", name: "OpenAI: GPT-4", @@ -6869,6 +6852,23 @@ export const MODELS = { contextWindow: 8191, maxTokens: 4096, } 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": { id: "openrouter/auto", name: "OpenRouter: Auto Router", diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 2aae1195..2db04ad4 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1644,6 +1644,7 @@ export class AgentSession { // Run default summarizer if needed let summaryText: string | undefined; + let summaryDetails: unknown; if (options.summarize && entriesToSummarize.length > 0 && !hookSummary) { const model = this.model!; const apiKey = await this._modelRegistry.getApiKey(model); @@ -1666,8 +1667,13 @@ export class AgentSession { throw new Error(result.error); } summaryText = result.summary; + summaryDetails = { + readFiles: result.readFiles || [], + modifiedFiles: result.modifiedFiles || [], + }; } else if (hookSummary) { summaryText = hookSummary.summary; + summaryDetails = hookSummary.details; } // Determine the new leaf position based on target type @@ -1698,7 +1704,7 @@ export class AgentSession { let summaryEntry: BranchSummaryEntry | undefined; if (summaryText) { // 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; } else if (newLeafId === null) { // No summary, navigating to root - reset leaf diff --git a/packages/coding-agent/src/core/compaction/branch-summarization.ts b/packages/coding-agent/src/core/compaction/branch-summarization.ts index fae49294..a0a750e6 100644 --- a/packages/coding-agent/src/core/compaction/branch-summarization.ts +++ b/packages/coding-agent/src/core/compaction/branch-summarization.ts @@ -18,10 +18,18 @@ import { estimateTokens } from "./compaction.js"; export interface BranchSummaryResult { summary?: string; + readFiles?: string[]; + modifiedFiles?: string[]; aborted?: boolean; error?: string; } +/** Details stored in BranchSummaryEntry.details for file tracking */ +export interface BranchSummaryDetails { + readFiles: string[]; + modifiedFiles: string[]; +} + export interface FileOperations { read: Set; written: Set; @@ -183,6 +191,10 @@ function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperation * 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. * + * 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 tokenBudget - Maximum tokens to include (0 = no limit) */ @@ -195,13 +207,30 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe }; 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--) { const entry = entries[i]; const message = getMessageFromEntry(entry); if (!message) continue; - // Extract file ops from assistant messages + // Extract file ops from assistant messages (tool calls) extractFileOpsFromMessage(message, fileOps); 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.`; -/** - * 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(`\n${readOnly.join("\n")}\n`); - } - - if (modified.size > 0) { - const files = [...modified].sort(); - sections.push(`\n${files.join("\n")}\n`); - } - - if (sections.length === 0) return ""; - - return `\n\n${sections.join("\n\n")}`; -} - /** * Convert messages to text for the summarization prompt. */ @@ -358,13 +361,19 @@ export async function generateBranchSummary( 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") .map((c) => c.text) .join("\n"); - // Append static file operations section - summary += formatFileOperations(fileOps); + // Compute file lists for details + 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, + }; } diff --git a/packages/coding-agent/src/core/session-manager.ts b/packages/coding-agent/src/core/session-manager.ts index d8c2b420..f1adda2a 100644 --- a/packages/coding-agent/src/core/session-manager.ts +++ b/packages/coding-agent/src/core/session-manager.ts @@ -872,7 +872,7 @@ export class SessionManager { * Same as branch(), but also appends a branch_summary entry that captures * 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)) { throw new Error(`Entry ${branchFromId} not found`); } @@ -884,6 +884,7 @@ export class SessionManager { timestamp: new Date().toISOString(), fromId: branchFromId ?? "root", summary, + details, }; this._appendEntry(entry); return entry.id;