This commit is contained in:
Your Name
2026-03-09 03:14:19 +00:00
parent 6d1c6a9b06
commit c1f551f309
5 changed files with 67 additions and 22 deletions

View File

@@ -4,6 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
// DISCLAIMER: This is a copied version of https://github.com/googleapis/js-genai/blob/main/src/chats.ts with the intention of working around a key bug
// where function responses are not treated as "valid" responses: https://b.corp.google.com/issues/420354090
@@ -167,6 +170,39 @@ export class GeminiChat {
);
}
/**
* Logs the projected history sent to the API to a side-channel file for anomaly analysis.
*/
private logRequestHistory(
requestContents: Content[],
promptId: string,
): void {
try {
const logDir = path.join(
this.config.storage.getProjectTempDir(),
'request_history_logs',
this.config.getSessionId(),
);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const timestamp = Date.now();
const logFile = path.join(
logDir,
`request_${timestamp}_${promptId}.json`,
);
fs.writeFileSync(logFile, JSON.stringify(requestContents, null, 2));
debugLogger.debug(
`[PROJECT CLARITY] Request history logged to ${logFile}`,
);
} catch (error) {
debugLogger.warn(
`[PROJECT CLARITY] Failed to log request history: ${error}`,
);
}
}
/**
* Marks a specific tool call ID for elision from the history.
*/
@@ -242,6 +278,9 @@ export class GeminiChat {
const requestContents =
this.historyManager.getHistoryForRequest(userContent);
// PROJECT CLARITY: Side-channel request logging.
this.logRequestHistory(requestContents, prompt_id);
const stream = async function* (
this: GeminiChat,
): AsyncGenerator<StreamEvent, void, void> {

View File

@@ -5,6 +5,7 @@
*/
import type { Content } from '@google/genai';
import { debugLogger } from '../utils/debugLogger.js';
/**
* Types of side-effects that can be triggered by tools or the system.
@@ -74,6 +75,12 @@ export class SideEffectService {
* Queues a side-effect for later application.
*/
queueSideEffect(effect: SideEffect): void {
debugLogger.debug(`[PROJECT CLARITY] Queuing side-effect: ${effect.type}`, {
payload:
effect.type === SideEffectType.REPLACE_HISTORY
? '<history>'
: effect.payload,
});
this.pendingSideEffects.push(effect);
}

View File

@@ -18,6 +18,7 @@ import {
import { CHECKPOINT_STATE_DEFINITION } from './definitions/coreTools.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import type { Config } from '../config/config.js';
import { debugLogger } from '../utils/debugLogger.js';
interface CheckpointStateParams {
[CHECKPOINT_STATE_PARAM_SUMMARY]: string;
@@ -43,6 +44,7 @@ class CheckpointStateInvocation extends BaseToolInvocation<
override async execute(): Promise<ToolResult> {
const summary = this.params[CHECKPOINT_STATE_PARAM_SUMMARY];
debugLogger.debug(`[PROJECT CLARITY] Executing CheckpointStateTool with summary length: ${summary.length}`);
const chat = this.config.getGeminiClient().getChat();
const previousSummary = chat.getContinuityAnchor();

View File

@@ -20,6 +20,7 @@ import type { Config } from '../config/config.js';
import type { GeminiChat } from '../core/geminiChat.js';
import { CompressionStatus } from '../core/compression-status.js';
import type { ShellExecutionConfig } from 'src/services/shellExecutionService.js';
import { debugLogger } from '../utils/debugLogger.js';
class CompressInvocation extends BaseToolInvocation<
Record<string, never>,
@@ -50,6 +51,7 @@ class CompressInvocation extends BaseToolInvocation<
if (!callId) {
throw new Error('Critical error: callId is required for context compression elision.');
}
debugLogger.debug(`[PROJECT CLARITY] Executing CompressTool (callId: ${callId})`);
try {
const continuityService = this.config.getContinuityCompressionService();
const snapshot = await continuityService.generateSnapshot(

View File

@@ -9,22 +9,19 @@ import {
BaseToolInvocation,
Kind,
type ToolInvocation,
type ToolResult,
type ToolLiveOutput,
type ToolResult,
} from './tools.js';
import {
DISTILL_RESULT_TOOL_NAME,
DISTILL_RESULT_PARAM_REVISED_TEXT,
} from './tool-names.js';
import { DISTILL_RESULT_TOOL_NAME } from './tool-names.js';
import { DISTILL_RESULT_DEFINITION } from './definitions/coreTools.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import type { Config } from '../config/config.js';
import type { GeminiChat } from '../core/geminiChat.js';
import { saveTruncatedToolOutput } from '../utils/fileUtils.js';
import type { ShellExecutionConfig } from '../index.js';
import { debugLogger } from '../utils/debugLogger.js';
import { saveTruncatedToolOutput } from '../utils/tool-output-helper.js';
interface DistillResultParams {
[DISTILL_RESULT_PARAM_REVISED_TEXT]: string;
revised_text: string;
}
class DistillResultInvocation extends BaseToolInvocation<
@@ -43,30 +40,24 @@ class DistillResultInvocation extends BaseToolInvocation<
}
override getDescription(): string {
return 'Distills the last tool output to reduce context entropy.';
return 'Distills the most recent tool output in the history.';
}
override async execute(
_signal: AbortSignal,
_updateOutput?: (output: ToolLiveOutput) => void,
_shellExecutionConfig?: ShellExecutionConfig,
_shellExecutionConfig?: any,
ownCallId?: string,
): Promise<ToolResult> {
if (!ownCallId) {
throw new Error('Critical error: ownCallId is required for distill_result elision.');
}
const revisedText = this.params[DISTILL_RESULT_PARAM_REVISED_TEXT];
const sideEffects = this.config.getSideEffectService();
const history = this.chat.getComprehensiveHistory();
const revisedText = this.params.revised_text;
// 1. Find the last tool response (user message with functionResponse parts)
debugLogger.debug(`[PROJECT CLARITY] Executing DistillResultTool (ownCallId: ${ownCallId})`);
// 1. Find the target: the last function response in history.
const history = this.chat.getHistory();
let lastToolResponseIndex = -1;
for (let i = history.length - 1; i >= 0; i--) {
const content = history[i];
if (
content.role === 'user' &&
content.parts?.some((p) => p.functionResponse)
) {
if (history[i].parts?.some((p) => p.functionResponse)) {
lastToolResponseIndex = i;
break;
}
@@ -91,6 +82,10 @@ class DistillResultInvocation extends BaseToolInvocation<
throw new Error('Target call ID missing from tool response.');
}
debugLogger.debug(`[PROJECT CLARITY] Distill target identified: ${targetCallId} at index ${lastToolResponseIndex}`);
const sideEffects = this.config.getSideEffectService();
// 2. Elide all turns between that tool response and the current turn.
if (ownCallId) {
sideEffects.elideBetween(targetCallId, ownCallId);