managed history imporvements

This commit is contained in:
Your Name
2026-05-14 03:17:11 +00:00
parent c7ba026c18
commit cc3b17a32f
11 changed files with 78 additions and 61 deletions
+2 -2
View File
@@ -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,
+9 -23
View File
@@ -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);
}
}
+18 -6
View File
@@ -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 || []),
+20
View File
@@ -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);
}
+4 -4
View File
@@ -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.' }],