diff --git a/src/core/condense/__tests__/condense.spec.ts b/src/core/condense/__tests__/condense.spec.ts index 5eb97b3e8a..1c094829c7 100644 --- a/src/core/condense/__tests__/condense.spec.ts +++ b/src/core/condense/__tests__/condense.spec.ts @@ -72,7 +72,17 @@ describe("Condense", () => { { role: "user", content: "Ninth message" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation( + messages, + mockApiHandler, + "System prompt", + taskId, + 5000, + false, + undefined, + undefined, + true, + ) // powerSteeringEnabled = true to preserve first message // Verify the first message is preserved expect(result.messages[0]).toEqual(messages[0]) @@ -106,7 +116,17 @@ describe("Condense", () => { { role: "user", content: "Thanks!" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation( + messages, + mockApiHandler, + "System prompt", + taskId, + 5000, + false, + undefined, + undefined, + true, + ) // powerSteeringEnabled = true to preserve first message // The first message with slash command should be intact expect(result.messages[0].content).toBe(slashCommandContent) @@ -131,7 +151,17 @@ describe("Condense", () => { { role: "user", content: "Perfect!" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation( + messages, + mockApiHandler, + "System prompt", + taskId, + 5000, + false, + undefined, + undefined, + true, + ) // powerSteeringEnabled = true to preserve first message // The first message with complex content should be preserved expect(result.messages[0].content).toEqual(complexContent) @@ -146,7 +176,17 @@ describe("Condense", () => { { role: "assistant", content: "Fourth message" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation( + messages, + mockApiHandler, + "System prompt", + taskId, + 5000, + false, + undefined, + undefined, + false, + ) // Should return an error since we have only 4 messages (first + 3 to keep) expect(result.error).toBeDefined() @@ -165,7 +205,17 @@ describe("Condense", () => { { role: "user", content: "Final message" }, ] - const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation( + messages, + mockApiHandler, + "System prompt", + taskId, + 5000, + false, + undefined, + undefined, + false, + ) // Should return an error due to recent summary in last N messages expect(result.error).toBeDefined() @@ -198,7 +248,17 @@ describe("Condense", () => { { role: "user", content: "Seventh" }, ] - const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, 5000, false) + const result = await summarizeConversation( + messages, + emptyHandler, + "System prompt", + taskId, + 5000, + false, + undefined, + undefined, + false, + ) expect(result.error).toBeDefined() expect(result.messages).toEqual(messages) diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts index d86b500f90..498a97ff34 100644 --- a/src/core/condense/__tests__/index.spec.ts +++ b/src/core/condense/__tests__/index.spec.ts @@ -129,6 +129,10 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) expect(result.messages).toEqual(messages) expect(result.cost).toBe(0) @@ -155,6 +159,10 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) expect(result.messages).toEqual(messages) expect(result.cost).toBe(0) @@ -181,6 +189,10 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) // Check that the API was called correctly @@ -188,14 +200,11 @@ describe("summarizeConversation", () => { expect(maybeRemoveImageBlocks).toHaveBeenCalled() // Verify the structure of the result - // The result should be: first message + summary + last N messages - expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N - - // Check that the first message is preserved - expect(result.messages[0]).toEqual(messages[0]) + // When powerSteeringEnabled is false, the result should be: summary + last N messages + expect(result.messages.length).toBe(1 + N_MESSAGES_TO_KEEP) // summary + last N // Check that the summary message was inserted correctly - const summaryMessage = result.messages[1] + const summaryMessage = result.messages[0] expect(summaryMessage.role).toBe("assistant") expect(summaryMessage.content).toBe("This is a summary") expect(summaryMessage.isSummary).toBe(true) @@ -244,6 +253,10 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) // Should return original messages when summary is empty @@ -265,7 +278,17 @@ describe("summarizeConversation", () => { { role: "user", content: "Tell me more", ts: 7 }, ] - await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS) + await summarizeConversation( + messages, + mockApiHandler, + defaultSystemPrompt, + taskId, + DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, + ) // Verify the final request message const expectedFinalMessage = { @@ -312,6 +335,10 @@ describe("summarizeConversation", () => { systemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) // Verify that countTokens was called with the correct messages including system prompt @@ -355,6 +382,10 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, prevContextTokens, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) // Should return original messages when context would grow @@ -395,11 +426,15 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, prevContextTokens, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) // Should successfully summarize - // Result should be: first message + summary + last N messages - expect(result.messages.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N + // When powerSteeringEnabled is false, the result should be: summary + last N messages + expect(result.messages.length).toBe(1 + N_MESSAGES_TO_KEEP) // summary + last N expect(result.cost).toBe(0.03) expect(result.summary).toBe("Concise summary") expect(result.error).toBeUndefined() @@ -416,6 +451,10 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) // Should return original messages when not enough to summarize @@ -444,6 +483,10 @@ describe("summarizeConversation", () => { defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS, + false, + undefined, + undefined, + false, // powerSteeringEnabled ) // Should return original messages when recent summary exists @@ -493,6 +536,7 @@ describe("summarizeConversation", () => { false, undefined, invalidCondensingHandler, + false, // powerSteeringEnabled ) // Should return original messages when both handlers are invalid @@ -601,6 +645,8 @@ describe("summarizeConversation with custom settings", () => { DEFAULT_PREV_CONTEXT_TOKENS, false, customPrompt, + undefined, + false, // powerSteeringEnabled ) // Verify the custom prompt was used @@ -622,6 +668,8 @@ describe("summarizeConversation with custom settings", () => { DEFAULT_PREV_CONTEXT_TOKENS, false, " ", // Empty custom prompt + undefined, + false, // powerSteeringEnabled ) // Verify the default prompt was used @@ -639,6 +687,8 @@ describe("summarizeConversation with custom settings", () => { DEFAULT_PREV_CONTEXT_TOKENS, false, undefined, // No custom prompt + undefined, + false, // powerSteeringEnabled ) // Verify the default prompt was used again @@ -660,6 +710,7 @@ describe("summarizeConversation with custom settings", () => { false, undefined, mockCondensingApiHandler, + false, // powerSteeringEnabled ) // Verify the condensing handler was used @@ -680,6 +731,7 @@ describe("summarizeConversation with custom settings", () => { false, undefined, undefined, + false, // powerSteeringEnabled ) // Verify the main handler was used @@ -711,6 +763,7 @@ describe("summarizeConversation with custom settings", () => { false, undefined, invalidHandler, + false, // powerSteeringEnabled ) // Verify the main handler was used as fallback @@ -737,6 +790,8 @@ describe("summarizeConversation with custom settings", () => { DEFAULT_PREV_CONTEXT_TOKENS, false, "Custom prompt", + undefined, + false, // powerSteeringEnabled ) // Verify telemetry was called with custom prompt flag @@ -761,6 +816,7 @@ describe("summarizeConversation with custom settings", () => { false, undefined, mockCondensingApiHandler, + false, // powerSteeringEnabled ) // Verify telemetry was called with custom API handler flag @@ -785,6 +841,7 @@ describe("summarizeConversation with custom settings", () => { true, // isAutomaticTrigger "Custom prompt", mockCondensingApiHandler, + false, // powerSteeringEnabled ) // Verify telemetry was called with both flags diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 166a8ba4ca..9af2bc4fee 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -80,6 +80,7 @@ export type SummarizeResponse = { * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically * @param {string} customCondensingPrompt - Optional custom prompt to use for condensing * @param {ApiHandler} condensingApiHandler - Optional specific API handler to use for condensing + * @param {boolean} powerSteeringEnabled - Whether power steering is enabled (controls initial prompt preservation) * @returns {SummarizeResponse} - The result of the summarization operation (see above) */ export async function summarizeConversation( @@ -91,6 +92,7 @@ export async function summarizeConversation( isAutomaticTrigger?: boolean, customCondensingPrompt?: string, condensingApiHandler?: ApiHandler, + powerSteeringEnabled?: boolean, ): Promise { TelemetryService.instance.captureContextCondensed( taskId, @@ -101,10 +103,14 @@ export async function summarizeConversation( const response: SummarizeResponse = { messages, cost: 0, summary: "" } - // Always preserve the first message (which may contain slash command content) - const firstMessage = messages[0] - // Get messages to summarize, excluding the first message and last N messages - const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(1, -N_MESSAGES_TO_KEEP)) + // Conditionally preserve the first message based on power steering setting + // When power steering is enabled, preserve the initial prompt to maintain task context + // When disabled, don't preserve it to prevent task restarts after condensing + const firstMessage = powerSteeringEnabled ? messages[0] : undefined + const messagesToKeepFrom = powerSteeringEnabled ? 1 : 0 + + // Get messages to summarize, excluding the conditionally preserved first message and last N messages + const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(messagesToKeepFrom, -N_MESSAGES_TO_KEEP)) if (messagesToSummarize.length <= 1) { const error = @@ -188,8 +194,10 @@ export async function summarizeConversation( isSummary: true, } - // Reconstruct messages: [first message, summary, last N messages] - const newMessages = [firstMessage, summaryMessage, ...keepMessages] + // Reconstruct messages: [first message (if power steering enabled), summary, last N messages] + const newMessages = firstMessage + ? [firstMessage, summaryMessage, ...keepMessages] + : [summaryMessage, ...keepMessages] // Count the tokens in the context for the next API request // We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens diff --git a/src/core/sliding-window/__tests__/sliding-window.spec.ts b/src/core/sliding-window/__tests__/sliding-window.spec.ts index 0f2c70c81b..c58c78797e 100644 --- a/src/core/sliding-window/__tests__/sliding-window.spec.ts +++ b/src/core/sliding-window/__tests__/sliding-window.spec.ts @@ -594,6 +594,7 @@ describe("Sliding Window", () => { true, undefined, // customCondensingPrompt undefined, // condensingApiHandler + undefined, // powerSteeringEnabled ) // Verify the result contains the summary information @@ -765,6 +766,7 @@ describe("Sliding Window", () => { true, undefined, // customCondensingPrompt undefined, // condensingApiHandler + undefined, // powerSteeringEnabled ) // Verify the result contains the summary information diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index 1e518c9a56..702dfead8f 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -6,6 +6,7 @@ import { ApiHandler } from "../../api" import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense" import { ApiMessage } from "../task-persistence/apiMessages" import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types" +import { experiments, EXPERIMENT_IDS } from "../../shared/experiments" /** * Default percentage of the context window to use as a buffer when deciding when to truncate @@ -77,6 +78,7 @@ type TruncateOptions = { condensingApiHandler?: ApiHandler profileThresholds: Record currentProfileId: string + powerSteeringEnabled?: boolean } type TruncateResponse = SummarizeResponse & { prevContextTokens: number } @@ -102,6 +104,7 @@ export async function truncateConversationIfNeeded({ condensingApiHandler, profileThresholds, currentProfileId, + powerSteeringEnabled, }: TruncateOptions): Promise { let error: string | undefined let cost = 0 @@ -155,6 +158,7 @@ export async function truncateConversationIfNeeded({ true, // automatic trigger customCondensingPrompt, condensingApiHandler, + powerSteeringEnabled, ) if (result.error) { error = result.error diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index cf16df8dcc..396bb9af83 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1006,6 +1006,9 @@ export class Task extends EventEmitter implements TaskLike { const { contextTokens: prevContextTokens } = this.getTokenUsage() + // Check if power steering is enabled + const powerSteeringEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING) + const { messages, summary, @@ -1021,6 +1024,7 @@ export class Task extends EventEmitter implements TaskLike { false, // manual trigger customCondensingPrompt, // User's custom prompt condensingApiHandler, // Specific handler for condensing + powerSteeringEnabled, // Pass power steering state ) if (error) { this.say( @@ -2461,6 +2465,9 @@ export class Task extends EventEmitter implements TaskLike { `Forcing truncation to ${FORCED_CONTEXT_REDUCTION_PERCENT}% of current context.`, ) + // Check if power steering is enabled + const powerSteeringEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING) + // Force aggressive truncation by keeping only 75% of the conversation history const truncateResult = await truncateConversationIfNeeded({ messages: this.apiConversationHistory, @@ -2474,6 +2481,7 @@ export class Task extends EventEmitter implements TaskLike { taskId: this.taskId, profileThresholds, currentProfileId, + powerSteeringEnabled, }) if (truncateResult.messages !== this.apiConversationHistory) { @@ -2577,6 +2585,9 @@ export class Task extends EventEmitter implements TaskLike { // Get the current profile ID using the helper method const currentProfileId = this.getCurrentProfileId(state) + // Check if power steering is enabled + const powerSteeringEnabled = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING) + const truncateResult = await truncateConversationIfNeeded({ messages: this.apiConversationHistory, totalTokens: contextTokens, @@ -2591,6 +2602,7 @@ export class Task extends EventEmitter implements TaskLike { condensingApiHandler, profileThresholds, currentProfileId, + powerSteeringEnabled, }) if (truncateResult.messages !== this.apiConversationHistory) { await this.overwriteApiConversationHistory(truncateResult.messages)