mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 22:02:59 -07:00
managed history imporvements
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
import type { Part } from '@google/genai';
|
||||
import { type ConcreteNode, NodeType } from './types.js';
|
||||
import { randomUUID, createHash } from 'node:crypto';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { debugLogger } from '../../utils/debugLogger.js';
|
||||
import type { NodeIdService } from './nodeIdService.js';
|
||||
import type { HistoryTurn } from '../../core/agentChatHistory.js';
|
||||
@@ -157,7 +157,7 @@ export function getStableId(
|
||||
if (turnSalt && partIdx === -1) {
|
||||
id = `turn_${turnSalt}`;
|
||||
} else {
|
||||
id = randomUUID();
|
||||
id = `${turnSalt}_f_${partIdx}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { deriveStableId } from '../../utils/cryptoUtils.js';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import type { ProcessArgs, ContextProcessor } from '../pipeline.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
@@ -62,7 +62,8 @@ export function createBlobDegradationProcessor(
|
||||
if (payload.inlineData?.data && payload.inlineData?.mimeType) {
|
||||
await ensureDir();
|
||||
const ext = payload.inlineData.mimeType.split('/')[1] || 'bin';
|
||||
const fileName = `blob_${Date.now()}_${randomUUID()}.${ext}`;
|
||||
// Use a stable filename based on the node ID
|
||||
const fileName = `blob_${deriveStableId([node.id])}.${ext}`;
|
||||
const filePath = path.join(blobOutputsDir, fileName);
|
||||
|
||||
const buffer = Buffer.from(payload.inlineData.data, 'base64');
|
||||
@@ -92,7 +93,7 @@ export function createBlobDegradationProcessor(
|
||||
if (newText && tokensSaved > 0) {
|
||||
returnedNodes.push({
|
||||
...node,
|
||||
id: randomUUID(),
|
||||
id: deriveStableId([node.id, 'degraded']),
|
||||
payload: { text: newText },
|
||||
replacesId: node.id,
|
||||
turnId: node.turnId,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { deriveStableId } from '../../utils/cryptoUtils.js';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import type { ContextProcessor, ProcessArgs } from '../pipeline.js';
|
||||
import { type ConcreteNode, NodeType } from '../graph/types.js';
|
||||
@@ -99,9 +99,10 @@ export function createNodeDistillationProcessor(
|
||||
if (newTokens < oldTokens) {
|
||||
const distilledPayload = updatePart(payload, { text: summary });
|
||||
|
||||
const newId = deriveStableId([node.id, 'distilled']);
|
||||
returnedNodes.push({
|
||||
...node,
|
||||
id: randomUUID(),
|
||||
id: newId,
|
||||
payload: distilledPayload,
|
||||
replacesId: node.id,
|
||||
timestamp: node.timestamp,
|
||||
@@ -158,9 +159,10 @@ export function createNodeDistillationProcessor(
|
||||
functionResponse: newFR,
|
||||
});
|
||||
|
||||
const newId = deriveStableId([node.id, 'distilled']);
|
||||
returnedNodes.push({
|
||||
...node,
|
||||
id: randomUUID(),
|
||||
id: newId,
|
||||
payload: distilledPayload,
|
||||
replacesId: node.id,
|
||||
timestamp: node.timestamp,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { deriveStableId } from '../../utils/cryptoUtils.js';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import type { ContextProcessor, ProcessArgs } from '../pipeline.js';
|
||||
import type { ContextEnvironment } from '../pipeline/environment.js';
|
||||
@@ -79,9 +79,10 @@ export function createNodeTruncationProcessor(
|
||||
if (text) {
|
||||
const squashResult = tryApplySquash(text, limitChars);
|
||||
if (squashResult) {
|
||||
const newId = deriveStableId([node.id, 'truncated']);
|
||||
returnedNodes.push({
|
||||
...node,
|
||||
id: randomUUID(),
|
||||
id: newId,
|
||||
payload: { ...payload, text: squashResult.text },
|
||||
replacesId: node.id,
|
||||
turnId: node.turnId,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { deriveStableId } from '../../utils/cryptoUtils.js';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import type {
|
||||
ContextProcessor,
|
||||
@@ -112,7 +112,8 @@ export function createRollingSummaryProcessor(
|
||||
try {
|
||||
// Synthesize the rolling summary synchronously
|
||||
const snapshotText = await generateRollingSummary(nodesToSummarize);
|
||||
const newId = randomUUID();
|
||||
const consumedIds = nodesToSummarize.map((n) => n.id);
|
||||
const newId = deriveStableId(consumedIds);
|
||||
|
||||
const summaryNode: RollingSummary = {
|
||||
id: newId,
|
||||
@@ -121,10 +122,9 @@ export function createRollingSummaryProcessor(
|
||||
timestamp: nodesToSummarize[nodesToSummarize.length - 1].timestamp,
|
||||
role: 'user',
|
||||
payload: { text: snapshotText },
|
||||
abstractsIds: nodesToSummarize.map((n) => n.id),
|
||||
abstractsIds: consumedIds,
|
||||
};
|
||||
|
||||
const consumedIds = nodesToSummarize.map((n) => n.id);
|
||||
const returnedNodes = targets.filter(
|
||||
(t) => !consumedIds.includes(t.id),
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
import { deriveStableId } from '../../utils/cryptoUtils.js';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import type {
|
||||
ContextProcessor,
|
||||
@@ -50,13 +50,6 @@ export function createStateSnapshotProcessor(
|
||||
): ContextProcessor {
|
||||
const generator = new SnapshotGenerator(env);
|
||||
|
||||
const generateStableId = (consumedIds: string[]) => {
|
||||
return createHash('sha256')
|
||||
.update(consumedIds.sort().join(','))
|
||||
.digest('hex')
|
||||
.slice(0, 32);
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
name: 'StateSnapshotProcessor',
|
||||
@@ -101,7 +94,7 @@ export function createStateSnapshotProcessor(
|
||||
`[StateSnapshotProcessor] Successfully spliced PROPOSED_SNAPSHOT from Inbox into Graph. Consumed ${consumedIds.length} nodes.`,
|
||||
);
|
||||
// If valid, apply it!
|
||||
const newId = generateStableId(consumedIds);
|
||||
const newId = deriveStableId(consumedIds);
|
||||
|
||||
const snapshotNode: Snapshot = {
|
||||
id: newId,
|
||||
@@ -197,7 +190,7 @@ export function createStateSnapshotProcessor(
|
||||
if (baselineIdToConsume && !consumedIds.includes(baselineIdToConsume)) {
|
||||
consumedIds.push(baselineIdToConsume);
|
||||
}
|
||||
const newId = generateStableId(consumedIds);
|
||||
const newId = deriveStableId(consumedIds);
|
||||
|
||||
const snapshotNode: Snapshot = {
|
||||
id: newId,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { deriveStableId } from '../../utils/cryptoUtils.js';
|
||||
import type { JSONSchemaType } from 'ajv';
|
||||
import type { ContextProcessor, ProcessArgs } from '../pipeline.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
@@ -120,7 +120,7 @@ export function createToolMaskingProcessor(
|
||||
directoryCreated = true;
|
||||
}
|
||||
|
||||
const fileName = `${sanitizeFilenamePart(toolName).toLowerCase()}_${sanitizeFilenamePart(callId).toLowerCase()}_${nodeType}_${randomUUID()}.txt`;
|
||||
const fileName = `${sanitizeFilenamePart(toolName).toLowerCase()}_${sanitizeFilenamePart(callId).toLowerCase()}_${nodeType}_${deriveStableId([content])}.txt`;
|
||||
const filePath = path.join(toolOutputsDir, fileName);
|
||||
|
||||
await fs.writeFile(filePath, content);
|
||||
@@ -214,9 +214,10 @@ export function createToolMaskingProcessor(
|
||||
functionCall: newFC,
|
||||
});
|
||||
|
||||
const newId = deriveStableId([node.id, 'masked']);
|
||||
returnedNodes.push({
|
||||
...node,
|
||||
id: randomUUID(),
|
||||
id: newId,
|
||||
payload: maskedPart,
|
||||
replacesId: node.id,
|
||||
turnId: node.turnId,
|
||||
@@ -242,9 +243,10 @@ export function createToolMaskingProcessor(
|
||||
functionResponse: newFR,
|
||||
});
|
||||
|
||||
const newId = deriveStableId([node.id, 'masked']);
|
||||
returnedNodes.push({
|
||||
...node,
|
||||
id: randomUUID(),
|
||||
id: newId,
|
||||
payload: maskedPart,
|
||||
replacesId: node.id,
|
||||
turnId: node.turnId,
|
||||
|
||||
@@ -296,13 +296,7 @@ export class GeminiClient {
|
||||
}
|
||||
|
||||
setHistory(history: readonly (Content | HistoryTurn)[]) {
|
||||
const turns = history.map((item) => {
|
||||
if ('id' in item && 'content' in item) {
|
||||
return item as HistoryTurn;
|
||||
}
|
||||
return { id: randomUUID(), content: item as Content };
|
||||
});
|
||||
this.getChat().setHistory(turns);
|
||||
this.getChat().setHistory(history);
|
||||
this.updateTelemetryTokenCount();
|
||||
this.forceFullIdeContext = true;
|
||||
}
|
||||
@@ -651,7 +645,11 @@ export class GeminiClient {
|
||||
if (this.contextManager) {
|
||||
const rawPendingRequest = createUserContent(request);
|
||||
const pendingRequest = {
|
||||
id: randomUUID(),
|
||||
id:
|
||||
this.getChatRecordingService()?.recordSyntheticMessage(
|
||||
'user',
|
||||
rawPendingRequest.parts || [],
|
||||
) || randomUUID(),
|
||||
content: rawPendingRequest,
|
||||
};
|
||||
const {
|
||||
@@ -678,11 +676,7 @@ export class GeminiClient {
|
||||
signal,
|
||||
);
|
||||
if (newHistory.length !== this.getHistory().length) {
|
||||
const turns = newHistory.map((c) => ({
|
||||
id: randomUUID(),
|
||||
content: c,
|
||||
}));
|
||||
this.getChat().setHistory(turns);
|
||||
this.getChat().setHistory(newHistory);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1242,11 +1236,7 @@ export class GeminiClient {
|
||||
if (newHistory) {
|
||||
// We truncated content to save space, but summarization is still "failed".
|
||||
// We update the chat context directly without resetting the failure flag.
|
||||
const turns = newHistory.map((c) => ({
|
||||
id: randomUUID(),
|
||||
content: c,
|
||||
}));
|
||||
this.getChat().setHistory(turns);
|
||||
this.getChat().setHistory(newHistory);
|
||||
this.updateTelemetryTokenCount();
|
||||
// We don't reset the chat session fully like in COMPRESSED because
|
||||
// this is a lighter-weight intervention.
|
||||
@@ -1265,11 +1255,7 @@ export class GeminiClient {
|
||||
this.config,
|
||||
);
|
||||
if (result.maskedCount > 0) {
|
||||
const turns = result.newHistory.map((c) => ({
|
||||
id: randomUUID(),
|
||||
content: c,
|
||||
}));
|
||||
this.getChat().setHistory(turns);
|
||||
this.getChat().setHistory(result.newHistory);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,6 +323,9 @@ export class GeminiChat {
|
||||
kind: 'main' | 'subagent' = 'main',
|
||||
) {
|
||||
await this.chatRecordingService.initialize(resumedSessionData, kind);
|
||||
// Sync initial history with the recorder to ensure all turns (even bootstrapped ones)
|
||||
// are durable and coordinated.
|
||||
this.chatRecordingService.updateMessagesFromHistory(this.agentHistory.get());
|
||||
}
|
||||
|
||||
setSystemInstruction(sysInstr: string) {
|
||||
@@ -913,7 +916,11 @@ export class GeminiChat {
|
||||
if ('id' in content && 'content' in content) {
|
||||
this.agentHistory.push(content);
|
||||
} else {
|
||||
this.agentHistory.push({ id: randomUUID(), content });
|
||||
const id = this.chatRecordingService.recordSyntheticMessage(
|
||||
content.role === 'user' ? 'user' : 'gemini',
|
||||
content.parts || [],
|
||||
);
|
||||
this.agentHistory.push({ id, content });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,11 +928,16 @@ export class GeminiChat {
|
||||
history: readonly (Content | HistoryTurn)[],
|
||||
options: { silent?: boolean } = {},
|
||||
): void {
|
||||
const wrappedHistory: HistoryTurn[] = history.map((item) =>
|
||||
'id' in item && 'content' in item
|
||||
? item
|
||||
: { id: randomUUID(), content: item },
|
||||
);
|
||||
const wrappedHistory: HistoryTurn[] = history.map((item) => {
|
||||
if ('id' in item && 'content' in item) {
|
||||
return item;
|
||||
}
|
||||
const id = this.chatRecordingService.recordSyntheticMessage(
|
||||
item.role === 'user' ? 'user' : 'gemini',
|
||||
item.parts || [],
|
||||
);
|
||||
return { id, content: item };
|
||||
});
|
||||
this.agentHistory.set(wrappedHistory, options);
|
||||
this.lastPromptTokenCount = estimateTokenCountSync(
|
||||
this.agentHistory.flatMap((c) => c.content.parts || []),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
/**
|
||||
* Derives a stable, deterministic ID from a list of source IDs.
|
||||
* Used for synthetic turns like summaries to ensure that re-summarizing the same
|
||||
* content produces a consistent identity.
|
||||
*/
|
||||
export function deriveStableId(sourceIds: string[]): string {
|
||||
const sortedIds = [...sourceIds].sort();
|
||||
return createHash('sha256')
|
||||
.update(sortedIds.join('|'))
|
||||
.digest('hex')
|
||||
.slice(0, 32);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
import { type Part } from '@google/genai';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
import { type HistoryTurn } from '../core/agentChatHistory.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { deriveStableId } from './cryptoUtils.js';
|
||||
|
||||
export const SYNTHETIC_THOUGHT_SIGNATURE = 'skip_thought_signature_validator';
|
||||
|
||||
@@ -153,7 +153,7 @@ function pairToolsAndEnforceSignatures(
|
||||
targetUserTurn = nextTurn;
|
||||
} else {
|
||||
targetUserTurn = {
|
||||
id: randomUUID(),
|
||||
id: deriveStableId([turn.id, 'sentinel_resp']),
|
||||
content: { role: 'user', parts: [] },
|
||||
};
|
||||
work.splice(i + 1, 0, targetUserTurn);
|
||||
@@ -278,7 +278,7 @@ function enforceRoleConstraints(
|
||||
'[HistoryHardener] Final history starts with model role. Prepending sentinel user turn.',
|
||||
);
|
||||
result.unshift({
|
||||
id: randomUUID(),
|
||||
id: deriveStableId([result[0].id, 'sentinel_start']),
|
||||
content: {
|
||||
role: 'user',
|
||||
parts: [{ text: sentinels.continuation }],
|
||||
@@ -292,7 +292,7 @@ function enforceRoleConstraints(
|
||||
'[HistoryHardener] Final history ends with model role. Appending sentinel user turn.',
|
||||
);
|
||||
result.push({
|
||||
id: randomUUID(),
|
||||
id: deriveStableId([result[result.length - 1].id, 'sentinel_end']),
|
||||
content: {
|
||||
role: 'user',
|
||||
parts: [{ text: 'Please continue.' }],
|
||||
|
||||
Reference in New Issue
Block a user