Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/cli/src/ui/utils/historyExportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import type { Content } from '@google/genai';
/**
* Serializes chat history to a Markdown string.
*/
export function serializeHistoryToMarkdown(history: Content[]): string {
export function serializeHistoryToMarkdown(
history: readonly Content[],
): string {
return history
.map((item) => {
const text =
Expand Down Expand Up @@ -49,7 +51,7 @@ export function serializeHistoryToMarkdown(history: Content[]): string {
* Options for exporting chat history.
*/
export interface ExportHistoryOptions {
history: Content[];
history: readonly Content[];
filePath: string;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface MessageActionReturn {
export interface LoadHistoryActionReturn<HistoryType = unknown> {
type: 'load_history';
history: HistoryType;
clientHistory: Content[]; // The history for the generative client
clientHistory: readonly Content[]; // The history for the generative client
}

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,15 @@ export class GeminiClient {
return this.chat !== undefined;
}

getHistory(): Content[] {
getHistory(): readonly Content[] {
return this.getChat().getHistory();
}

stripThoughtsFromHistory() {
this.getChat().stripThoughtsFromHistory();
}

setHistory(history: Content[]) {
setHistory(history: readonly Content[]) {
this.getChat().setHistory(history);
this.updateTelemetryTokenCount();
this.forceFullIdeContext = true;
Expand Down Expand Up @@ -1171,7 +1171,7 @@ export class GeminiClient {
/**
* Masks bulky tool outputs to save context window space.
*/
private async tryMaskToolOutputs(history: Content[]): Promise<void> {
private async tryMaskToolOutputs(history: readonly Content[]): Promise<void> {
if (!this.config.getToolOutputMaskingEnabled()) {
return;
}
Expand Down
26 changes: 11 additions & 15 deletions packages/core/src/core/geminiChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export class GeminiChat {

private async makeApiCallAndProcessStream(
modelConfigKey: ModelConfigKey,
requestContents: Content[],
requestContents: readonly Content[],
prompt_id: string,
abortSignal: AbortSignal,
role: LlmRole,
Expand All @@ -489,7 +489,7 @@ export class GeminiChat {
let currentGenerateContentConfig: GenerateContentConfig =
newAvailabilityConfig;
let lastConfig: GenerateContentConfig = currentGenerateContentConfig;
let lastContentsToUse: Content[] = requestContents;
let lastContentsToUse: Content[] = [...requestContents];

const getAvailabilityContext = createAvailabilityContextProvider(
this.config,
Expand Down Expand Up @@ -528,9 +528,9 @@ export class GeminiChat {
abortSignal,
};

let contentsToUse = supportsModernFeatures(modelToUse)
? contentsForPreviewModel
: requestContents;
let contentsToUse: Content[] = supportsModernFeatures(modelToUse)
? [...contentsForPreviewModel]
: [...requestContents];

const hookSystem = this.config.getHookSystem();
if (hookSystem) {
Expand Down Expand Up @@ -687,16 +687,10 @@ export class GeminiChat {
* @return History contents alternating between user and model for the entire
* chat session.
*/
getHistory(curated: boolean = false): Content[] {
getHistory(curated: boolean = false): readonly Content[] {
const history = curated
? extractCuratedHistory(this.history)
: this.history;
// Return a shallow copy of the array to prevent callers from mutating
// the internal history array (push/pop/splice). Content objects are
// shared references — callers MUST NOT mutate them in place.
// This replaces a prior structuredClone() which deep-copied the entire
// conversation on every call, causing O(n) memory pressure per turn
// that compounded into OOM crashes in long-running sessions.
return [...history];
}

Expand All @@ -714,8 +708,8 @@ export class GeminiChat {
this.history.push(content);
}

setHistory(history: Content[]): void {
this.history = history;
setHistory(history: readonly Content[]): void {
this.history = [...history];
this.lastPromptTokenCount = estimateTokenCountSync(
this.history.flatMap((c) => c.parts || []),
);
Expand All @@ -742,7 +736,9 @@ export class GeminiChat {
// To ensure our requests validate, the first function call in every model
// turn within the active loop must have a `thoughtSignature` property.
// If we do not do this, we will get back 400 errors from the API.
ensureActiveLoopHasThoughtSignatures(requestContents: Content[]): Content[] {
ensureActiveLoopHasThoughtSignatures(
requestContents: readonly Content[],
): readonly Content[] {
// First, find the start of the active loop by finding the last user turn
// with a text message, i.e. that is not a function response.
let activeLoopStartIndex = -1;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface LogEntry {
}

export interface Checkpoint {
history: Content[];
history: readonly Content[];
authType?: AuthType;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/routing/routingStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface RoutingDecision {
*/
export interface RoutingContext {
/** The full history of the conversation. */
history: Content[];
history: readonly Content[];
/** The immediate request parts to be processed. */
request: PartListUnion;
/** An abort signal to cancel an LLM call during routing. */
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/safety/context-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export class ContextBuilder {
}

// Helper to convert Google GenAI Content[] to Safety Protocol ConversationTurn[]
private convertHistoryToTurns(history: Content[]): ConversationTurn[] {
private convertHistoryToTurns(
history: readonly Content[],
): ConversationTurn[] {
const turns: ConversationTurn[] = [];
let currentUserRequest: { text: string } | undefined;

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/services/chatCompressionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function modelStringToModelConfigAlias(model: string): string {
* contain massive tool outputs (like large grep results or logs).
*/
async function truncateHistoryToBudget(
history: Content[],
history: readonly Content[],
config: Config,
): Promise<Content[]> {
let functionResponseTokenCounter = 0;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/services/chatRecordingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ export class ChatRecordingService {
* Updates the conversation history based on the provided API Content array.
* This is used to persist changes made to the history (like masking) back to disk.
*/
updateMessagesFromHistory(history: Content[]): void {
updateMessagesFromHistory(history: readonly Content[]): void {
if (!this.conversationFile) return;

try {
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/services/toolOutputMaskingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const EXEMPT_TOOLS = new Set([
]);

export interface MaskingResult {
newHistory: Content[];
newHistory: readonly Content[];
maskedCount: number;
tokensSaved: number;
}
Expand All @@ -67,7 +67,10 @@ export interface MaskingResult {
* are preserved until they collectively reach the threshold.
*/
export class ToolOutputMaskingService {
async mask(history: Content[], config: Config): Promise<MaskingResult> {
async mask(
history: readonly Content[],
config: Config,
): Promise<MaskingResult> {
const maskingConfig = await config.getToolOutputMaskingConfig();
if (!maskingConfig.enabled || history.length === 0) {
return { newHistory: history, maskedCount: 0, tokensSaved: 0 };
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/checkpointUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { ToolCallRequestInfo } from '../scheduler/types.js';

export interface ToolCallData<HistoryType = unknown, ArgsType = unknown> {
history?: HistoryType;
clientHistory?: Content[];
clientHistory?: readonly Content[];
commitHash?: string;
toolCall: {
name: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export class GeminiCliSession {
break;
}

const transcript: Content[] = client.getHistory();
const transcript: readonly Content[] = client.getHistory();
const context: SessionContext = {
sessionId,
transcript,
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export interface AgentShell {

export interface SessionContext {
sessionId: string;
transcript: Content[];
transcript: readonly Content[];
cwd: string;
timestamp: string;
fs: AgentFilesystem;
Expand Down
Loading