feat(coding-agent): support compaction routing sessions

This commit is contained in:
Vegard Stikbakke
2026-08-17 07:34:52 +02:00
parent 47bf47f11f
commit 58302d34e7
3 changed files with 41 additions and 5 deletions
@@ -1873,6 +1873,7 @@ export class AgentSession {
env,
this.settingsManager.getRetrySettings(),
this._summarizationRetryCallbacks({ source: "compaction", reason: "manual" }),
undefined, // sessionId
);
summary = result.summary;
firstKeptEntryId = result.firstKeptEntryId;
@@ -2161,6 +2162,7 @@ export class AgentSession {
env,
this.settingsManager.getRetrySettings(),
this._summarizationRetryCallbacks({ source: "compaction", reason }),
undefined, // sessionId
);
summary = compactResult.summary;
firstKeptEntryId = compactResult.firstKeptEntryId;
@@ -544,8 +544,9 @@ function createSummarizationOptions(
env: Record<string, string> | undefined,
signal: AbortSignal | undefined,
thinkingLevel: ThinkingLevel | undefined,
sessionId: string | undefined,
): SimpleStreamOptions {
const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers, env };
const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers, env, sessionId };
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
options.reasoning = thinkingLevel;
}
@@ -567,11 +568,12 @@ export async function completeSummarization(
retry?: RetryPolicy,
callbacks?: RetryCallbacks,
): Promise<AssistantMessage> {
// Summaries are standalone requests, so isolate routing and avoid cache writes that cannot be reused.
// Avoid cache writes for one-off summaries. Reuse caller-supplied routing when available;
// callers without a session ID, including branch summaries, receive a fresh routing ID.
const requestOptions: SimpleStreamOptions = {
...options,
cacheRetention: "none",
sessionId: uuidv7(),
sessionId: options.sessionId ?? uuidv7(),
};
const produce = async (): Promise<AssistantMessage> =>
streamFn
@@ -598,6 +600,7 @@ export async function generateSummary(
env?: Record<string, string>,
retry?: RetryPolicy,
callbacks?: RetryCallbacks,
sessionId?: string,
): Promise<string> {
return (
await generateSummaryWithUsage(
@@ -614,6 +617,7 @@ export async function generateSummary(
env,
retry,
callbacks,
sessionId,
)
).text;
}
@@ -633,6 +637,7 @@ export async function generateSummaryWithUsage(
env?: Record<string, string>,
retry?: RetryPolicy,
callbacks?: RetryCallbacks,
sessionId?: string,
): Promise<{ text: string; usage: Usage }> {
const maxTokens = Math.min(
Math.floor(0.8 * reserveTokens),
@@ -665,7 +670,16 @@ export async function generateSummaryWithUsage(
},
];
const completionOptions = createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel);
const completionOptions = createSummarizationOptions(
model,
maxTokens,
apiKey,
headers,
env,
signal,
thinkingLevel,
sessionId,
);
const response = await completeSummarization(
model,
@@ -813,6 +827,7 @@ Be concise. Focus on what's needed to understand the kept suffix.`;
*
* @param preparation - Pre-calculated preparation from prepareCompaction()
* @param customInstructions - Optional custom focus for the summary
* @param sessionId - Optional routing session ID forwarded without enabling prompt caching
*/
export async function compact(
preparation: CompactionPreparation,
@@ -826,6 +841,7 @@ export async function compact(
env?: Record<string, string>,
retry?: RetryPolicy,
callbacks?: RetryCallbacks,
sessionId?: string,
): Promise<CompactionResult> {
const {
firstKeptEntryId,
@@ -860,6 +876,7 @@ export async function compact(
env,
retry,
callbacks,
sessionId,
);
historyText = historyResult.text;
historyUsage = historyResult.usage;
@@ -876,6 +893,7 @@ export async function compact(
streamFn,
retry,
callbacks,
sessionId,
);
// Merge into single summary
summary = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.text}`;
@@ -896,6 +914,7 @@ export async function compact(
env,
retry,
callbacks,
sessionId,
);
summary = result.text;
summaryUsage = result.usage;
@@ -933,6 +952,7 @@ async function generateTurnPrefixSummary(
streamFn?: StreamFn,
retry?: RetryPolicy,
callbacks?: RetryCallbacks,
sessionId?: string,
): Promise<{ text: string; usage: Usage }> {
const maxTokens = Math.min(
Math.floor(0.5 * reserveTokens),
@@ -952,7 +972,7 @@ async function generateTurnPrefixSummary(
const response = await completeSummarization(
model,
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel),
createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel, sessionId),
streamFn,
retry,
callbacks,
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import {
type CompactionPreparation,
compact,
completeSummarization,
generateSummary,
generateSummaryWithUsage,
} from "../src/core/compaction/index.ts";
@@ -102,6 +103,19 @@ describe("generateSummary reasoning options", () => {
expect(sessionIds[0]).not.toBe(sessionIds[1]);
});
it("honors a caller-supplied routing session without prompt caching", async () => {
await completeSummarization(
createModel(false),
{ systemPrompt: "Summarize", messages: [] },
{ sessionId: "current-routing-session", cacheRetention: "long" },
);
expect(completeSimpleMock.mock.calls[0][2]).toMatchObject({
sessionId: "current-routing-session",
cacheRetention: "none",
});
});
it("does not set reasoning when thinking is off", async () => {
await generateSummary(
messages,