mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-14 05:17:18 -07:00
check-in(broken)
This commit is contained in:
@@ -18,7 +18,6 @@ import { ContextEnvironmentImpl } from './sidecar/environmentImpl.js';
|
||||
import { SidecarLoader } from './sidecar/SidecarLoader.js';
|
||||
import { ContextTracer } from './tracer.js';
|
||||
import { ContextEventBus } from './eventBus.js';
|
||||
import { ContextTokenCalculator } from './utils/contextTokenCalculator.js';
|
||||
import type { Content } from '@google/genai';
|
||||
import type { BaseLlmClient } from '../core/baseLlmClient.js';
|
||||
import type { Episode } from './ir/types.js';
|
||||
@@ -140,7 +139,7 @@ describe('ContextManager Golden Tests', () => {
|
||||
const history = createLargeHistory();
|
||||
(
|
||||
contextManager as unknown as { pristineEpisodes: Episode[] }
|
||||
).pristineEpisodes = (contextManager as any).env.irMapper.toIr(history, new ContextTokenCalculator(4));
|
||||
).pristineEpisodes = (contextManager as any).env.irMapper.toIr(history, (contextManager as any).env.tokenCalculator);
|
||||
const result = await contextManager.projectCompressedHistory();
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ export class ContextManager {
|
||||
}
|
||||
|
||||
this.historyObserver = new HistoryObserver(
|
||||
this.chatHistory,
|
||||
chatHistory,
|
||||
this.env.eventBus,
|
||||
this.tracer,
|
||||
this.env.tokenCalculator,
|
||||
|
||||
@@ -1,133 +1,43 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Content, Part } from '@google/genai';
|
||||
import type {
|
||||
ConcreteNode,
|
||||
UserPrompt,
|
||||
AgentThought,
|
||||
ToolExecution,
|
||||
AgentYield,
|
||||
MaskedTool,
|
||||
} from './types.js';
|
||||
import { isAgentThought, isAgentYield, isSystemEvent, isSnapshot, isRollingSummary, isMaskedTool, isToolExecution, isUserPrompt } from './graphUtils.js';
|
||||
import type { ConcreteNode } from './types.js';
|
||||
import type { IrSerializationWriter, IrNodeBehaviorRegistry } from './behaviorRegistry.js';
|
||||
|
||||
export function fromIr(ship: readonly ConcreteNode[]): Content[] {
|
||||
const history: Content[] = [];
|
||||
const agentParts: Part[] = [];
|
||||
class IrSerializer implements IrSerializationWriter {
|
||||
private history: Content[] = [];
|
||||
private currentModelParts: Part[] = [];
|
||||
|
||||
const flushAgentParts = () => {
|
||||
if (agentParts.length > 0) {
|
||||
history.push({ role: 'model', parts: [...agentParts] });
|
||||
agentParts.length = 0;
|
||||
appendContent(content: Content) {
|
||||
this.flushModelParts();
|
||||
this.history.push(content);
|
||||
}
|
||||
|
||||
appendModelPart(part: Part) {
|
||||
this.currentModelParts.push(part);
|
||||
}
|
||||
|
||||
appendUserPart(part: Part) {
|
||||
this.flushModelParts();
|
||||
this.history.push({ role: 'user', parts: [part] });
|
||||
}
|
||||
|
||||
flushModelParts() {
|
||||
if (this.currentModelParts.length > 0) {
|
||||
this.history.push({ role: 'model', parts: [...this.currentModelParts] });
|
||||
this.currentModelParts = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getContents(): Content[] {
|
||||
this.flushModelParts();
|
||||
return this.history;
|
||||
}
|
||||
}
|
||||
|
||||
export function fromIr(ship: readonly ConcreteNode[], registry: IrNodeBehaviorRegistry): Content[] {
|
||||
const writer = new IrSerializer();
|
||||
for (const node of ship) {
|
||||
if (isUserPrompt(node)) {
|
||||
flushAgentParts();
|
||||
const content = serializeUserPrompt(node);
|
||||
if (content) history.push(content);
|
||||
} else if (isSystemEvent(node)) {
|
||||
flushAgentParts();
|
||||
// System events do not map strictly to Gemini Content parts unless synthesized.
|
||||
} else if (isAgentThought(node)) {
|
||||
agentParts.push(serializeAgentThought(node));
|
||||
} else if (isToolExecution(node)) {
|
||||
const parts = serializeToolExecution(node);
|
||||
agentParts.push(parts.call);
|
||||
flushAgentParts();
|
||||
history.push({ role: 'user', parts: [parts.response] });
|
||||
} else if (isMaskedTool(node)) {
|
||||
const parts = serializeMaskedTool(node);
|
||||
agentParts.push(parts.call);
|
||||
flushAgentParts();
|
||||
history.push({ role: 'user', parts: [parts.response] });
|
||||
} else if (isAgentYield(node)) {
|
||||
agentParts.push(serializeAgentYield(node));
|
||||
flushAgentParts();
|
||||
} else if (isSnapshot(node)) {
|
||||
flushAgentParts();
|
||||
history.push({ role: 'user', parts: [{ text: (node).text }] });
|
||||
} else if (isRollingSummary(node)) {
|
||||
flushAgentParts();
|
||||
history.push({ role: 'user', parts: [{ text: (node).text }] });
|
||||
}
|
||||
const behavior = registry.get(node.type);
|
||||
behavior.serialize(node, writer);
|
||||
}
|
||||
|
||||
flushAgentParts();
|
||||
return history;
|
||||
}
|
||||
|
||||
export function serializeUserPrompt(prompt: UserPrompt): Content | null {
|
||||
const parts: Part[] = [];
|
||||
for (const sp of prompt.semanticParts) {
|
||||
if (sp.type === 'text') {
|
||||
parts.push({ text: sp.text });
|
||||
} else if (sp.type === 'inline_data') {
|
||||
parts.push({
|
||||
inlineData: { mimeType: sp.mimeType, data: sp.data },
|
||||
});
|
||||
} else if (sp.type === 'file_data') {
|
||||
parts.push({
|
||||
fileData: { mimeType: sp.mimeType, fileUri: sp.fileUri },
|
||||
});
|
||||
} else if (sp.type === 'raw_part') {
|
||||
parts.push(sp.part);
|
||||
}
|
||||
}
|
||||
return parts.length > 0 ? { role: 'user', parts } : null;
|
||||
}
|
||||
|
||||
export function serializeAgentThought(thought: AgentThought): Part {
|
||||
return { text: thought.text };
|
||||
}
|
||||
|
||||
export function serializeToolExecution(
|
||||
tool: ToolExecution,
|
||||
): { call: Part; response: Part } {
|
||||
return {
|
||||
call: {
|
||||
functionCall: {
|
||||
id: tool.id,
|
||||
name: tool.toolName,
|
||||
args: tool.intent,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
functionResponse: {
|
||||
id: tool.id,
|
||||
name: tool.toolName,
|
||||
response: typeof tool.observation === "string" ? { message: tool.observation } : tool.observation,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeMaskedTool(
|
||||
tool: MaskedTool,
|
||||
): { call: Part; response: Part } {
|
||||
return {
|
||||
call: {
|
||||
functionCall: {
|
||||
id: tool.id,
|
||||
name: tool.toolName,
|
||||
args: tool.intent ?? {},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
functionResponse: {
|
||||
id: tool.id,
|
||||
name: tool.toolName,
|
||||
response: typeof tool.observation === 'string' ? { message: tool.observation } : (tool.observation ?? {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeAgentYield(yieldNode: AgentYield): Part {
|
||||
return { text: yieldNode.text };
|
||||
return writer.getContents();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js';
|
||||
|
||||
describe('HistorySquashingProcessor', () => {
|
||||
it('should truncate nodes that exceed maxTokensPerNode', async () => {
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1) as any;
|
||||
const env = createMockEnvironment();
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as any;
|
||||
mockTokenCalculator.tokensToChars = vi.fn().mockReturnValue(10); // Limit is 10 chars
|
||||
|
||||
mockTokenCalculator.estimateTokensForString = vi.fn((text: string) => {
|
||||
@@ -24,9 +25,7 @@ describe('HistorySquashingProcessor', () => {
|
||||
});
|
||||
mockTokenCalculator.estimateTokensForParts = vi.fn(() => 1);
|
||||
|
||||
const env = createMockEnvironment({
|
||||
tokenCalculator: mockTokenCalculator
|
||||
});
|
||||
(env as any).tokenCalculator = mockTokenCalculator;
|
||||
|
||||
const processor = HistorySquashingProcessor.create(env, {
|
||||
maxTokensPerNode: 1, // Will equal 10 chars limit
|
||||
@@ -80,7 +79,8 @@ describe('HistorySquashingProcessor', () => {
|
||||
});
|
||||
|
||||
it('should stop truncating once the deficit is cleared', async () => {
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1) as any;
|
||||
const env = createMockEnvironment();
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as any;
|
||||
mockTokenCalculator.tokensToChars = vi.fn().mockReturnValue(10);
|
||||
mockTokenCalculator.estimateTokensForString = vi.fn((text: string) => {
|
||||
if (text.includes('OMITTED')) return 0; // Huge savings
|
||||
@@ -88,9 +88,7 @@ describe('HistorySquashingProcessor', () => {
|
||||
});
|
||||
mockTokenCalculator.estimateTokensForParts = vi.fn(() => 0);
|
||||
|
||||
const env = createMockEnvironment({
|
||||
tokenCalculator: mockTokenCalculator
|
||||
});
|
||||
(env as any).tokenCalculator = mockTokenCalculator;
|
||||
|
||||
const processor = HistorySquashingProcessor.create(env, {
|
||||
maxTokensPerNode: 1,
|
||||
|
||||
@@ -18,22 +18,23 @@ describe('SemanticCompressionProcessor', () => {
|
||||
it('should trigger summarization via LLM for long text parts', async () => {
|
||||
const mockLlmClient = {
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
candidates: [{ content: { parts: [{ text: 'Mocked Summary!' }] } }],
|
||||
text: 'Mocked Summary!',
|
||||
}),
|
||||
};
|
||||
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1) as any;
|
||||
const env = createMockEnvironment({
|
||||
llmClient: mockLlmClient as any,
|
||||
});
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as any;
|
||||
mockTokenCalculator.tokensToChars = vi.fn().mockReturnValue(10);
|
||||
mockTokenCalculator.estimateTokensForParts = vi.fn((parts: any) => {
|
||||
if (parts[0]?.text === 'Mocked Summary!') return 5;
|
||||
if (parts[0]?.functionResponse?.response?.summary === 'Mocked Summary!') return 10;
|
||||
return 5000;
|
||||
});
|
||||
mockTokenCalculator.getTokenCost = vi.fn().mockReturnValue(5000);
|
||||
|
||||
const env = createMockEnvironment({
|
||||
llmClient: mockLlmClient as any,
|
||||
tokenCalculator: mockTokenCalculator
|
||||
});
|
||||
(env as any).tokenCalculator = mockTokenCalculator;
|
||||
|
||||
const processor = SemanticCompressionProcessor.create(env, {
|
||||
nodeThresholdTokens: 10,
|
||||
@@ -69,18 +70,17 @@ describe('SemanticCompressionProcessor', () => {
|
||||
|
||||
// 1. User Prompt
|
||||
const compressedPrompt = result[0] as UserPrompt;
|
||||
expect(compressedPrompt.id).toBe('mock-uuid-1');
|
||||
expect(compressedPrompt.id).not.toBe(prompt.id);
|
||||
expect(compressedPrompt.semanticParts[0].type).toBe('text');
|
||||
expect((compressedPrompt.semanticParts[0] as any).text).toBe('Mocked Summary!');
|
||||
// 2. Agent Thought
|
||||
const compressedThought = result[1] as AgentThought;
|
||||
expect(compressedThought.id).toBe('mock-uuid-2');
|
||||
expect(compressedThought.id).toMatch(/^mock-uuid-/);
|
||||
expect(compressedThought.id).not.toBe(thought.id);
|
||||
expect(compressedThought.text).toBe('Mocked Summary!');
|
||||
|
||||
const compressedTool = result[2] as ToolExecution;
|
||||
expect(compressedTool.id).toBe('mock-uuid-3');
|
||||
expect(compressedTool.id).toMatch(/^mock-uuid-/);
|
||||
expect(compressedTool.id).not.toBe(tool.id);
|
||||
expect(compressedTool.observation).toEqual({ summary: 'Mocked Summary!' });
|
||||
|
||||
@@ -90,22 +90,23 @@ describe('SemanticCompressionProcessor', () => {
|
||||
it('should stop summarizing once the deficit is cleared', async () => {
|
||||
const mockLlmClient = {
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
candidates: [{ content: { parts: [{ text: 'Mocked Summary!' }] } }],
|
||||
text: 'Mocked Summary!',
|
||||
}),
|
||||
};
|
||||
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1) as any;
|
||||
const env = createMockEnvironment({
|
||||
llmClient: mockLlmClient as any,
|
||||
});
|
||||
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as any;
|
||||
mockTokenCalculator.tokensToChars = vi.fn().mockReturnValue(10);
|
||||
// Returning 0 tokens for the summary to maximize savings
|
||||
mockTokenCalculator.estimateTokensForParts = vi.fn((parts: any) => {
|
||||
if (parts[0]?.text === 'Mocked Summary!') return 0;
|
||||
return 5000;
|
||||
});
|
||||
mockTokenCalculator.getTokenCost = vi.fn().mockReturnValue(5000);
|
||||
|
||||
const env = createMockEnvironment({
|
||||
llmClient: mockLlmClient as any,
|
||||
tokenCalculator: mockTokenCalculator
|
||||
});
|
||||
(env as any).tokenCalculator = mockTokenCalculator;
|
||||
|
||||
const processor = SemanticCompressionProcessor.create(env, {
|
||||
nodeThresholdTokens: 10,
|
||||
@@ -137,7 +138,6 @@ describe('SemanticCompressionProcessor', () => {
|
||||
|
||||
// 1. User Prompt (was summarized because deficit was > 0)
|
||||
const compressedPrompt = result[0] as UserPrompt;
|
||||
expect(compressedPrompt.id).toBe('mock-uuid-1');
|
||||
expect(compressedPrompt.id).not.toBe(prompt.id);
|
||||
|
||||
// 2. Agent Thought (was NOT summarized because deficit hit 0)
|
||||
|
||||
@@ -123,12 +123,6 @@ export class SemanticCompressionProcessor implements ContextProcessor {
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
newParts.map(p => {
|
||||
if (p.type === 'text') return { text: p.text };
|
||||
if (p.type === 'inline_data') return { inlineData: { mimeType: p.mimeType, data: p.data } };
|
||||
if (p.type === 'file_data') return { fileData: { mimeType: p.mimeType, fileUri: p.fileUri } };
|
||||
return (p as Extract<import('../ir/types.js').SemanticPart, { type: 'raw_part' }>).part;
|
||||
});
|
||||
returnedNodes.push({
|
||||
...prompt,
|
||||
id: this.env.idGenerator.generateId(),
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { ContextTracer } from '../tracer.js';
|
||||
import type { IFileSystem } from '../system/IFileSystem.js';
|
||||
import type { IIdGenerator } from '../system/IIdGenerator.js';
|
||||
import type { LiveInbox } from './inbox.js';
|
||||
import type { IrNodeBehaviorRegistry } from '../ir/behaviorRegistry.js';
|
||||
import type { IrMapper } from '../ir/mapper.js';
|
||||
|
||||
export type { ContextTracer, ContextEventBus };
|
||||
|
||||
@@ -26,6 +28,6 @@ export interface ContextEnvironment {
|
||||
readonly idGenerator: IIdGenerator;
|
||||
readonly eventBus: ContextEventBus;
|
||||
readonly inbox: LiveInbox;
|
||||
readonly behaviorRegistry: import('../ir/behaviorRegistry.js').IrNodeBehaviorRegistry;
|
||||
readonly irMapper: import('../ir/mapper.js').IrMapper;
|
||||
readonly behaviorRegistry: IrNodeBehaviorRegistry;
|
||||
readonly irMapper: IrMapper;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ export class ContextEnvironmentImpl implements ContextEnvironment {
|
||||
readonly fileSystem: IFileSystem;
|
||||
readonly idGenerator: IIdGenerator;
|
||||
readonly inbox: LiveInbox;
|
||||
readonly behaviorRegistry: import('../ir/behaviorRegistry.js').IrNodeBehaviorRegistry;
|
||||
readonly irMapper: import('../ir/mapper.js').IrMapper;
|
||||
readonly behaviorRegistry: IrNodeBehaviorRegistry;
|
||||
readonly irMapper: IrMapper;
|
||||
|
||||
constructor(
|
||||
readonly llmClient: BaseLlmClient,
|
||||
@@ -40,13 +40,12 @@ export class ContextEnvironmentImpl implements ContextEnvironment {
|
||||
fileSystem?: IFileSystem,
|
||||
idGenerator?: IIdGenerator,
|
||||
) {
|
||||
this.tokenCalculator = new ContextTokenCalculator(this.charsPerToken);
|
||||
this.behaviorRegistry = new IrNodeBehaviorRegistry();
|
||||
registerBuiltInBehaviors(this.behaviorRegistry);
|
||||
this.tokenCalculator = new ContextTokenCalculator(this.charsPerToken, this.behaviorRegistry);
|
||||
this.fileSystem = fileSystem || new NodeFileSystem();
|
||||
this.idGenerator = idGenerator || new NodeIdGenerator();
|
||||
this.inbox = new LiveInbox();
|
||||
|
||||
this.behaviorRegistry = new IrNodeBehaviorRegistry();
|
||||
registerBuiltInBehaviors(this.behaviorRegistry);
|
||||
this.irMapper = new IrMapper(this.behaviorRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export function createMockEnvironment(
|
||||
eventBus: new ContextEventBus(),
|
||||
tracer: new ContextTracer({ targetDir: '/tmp', sessionId: 'mock-session' }),
|
||||
charsPerToken: 1,
|
||||
tokenCalculator: new ContextTokenCalculator(1),
|
||||
tokenCalculator: new ContextTokenCalculator(1, registry),
|
||||
fileSystem: new InMemoryFileSystem(),
|
||||
idGenerator: new DeterministicIdGenerator('mock-uuid-'),
|
||||
behaviorRegistry: registry,
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
import type { Part } from '@google/genai';
|
||||
import { estimateTokenCountSync as baseEstimate } from '../../utils/tokenCalculation.js';
|
||||
import type { ConcreteNode } from '../ir/types.js';
|
||||
import { isUserPrompt, isAgentThought, isToolExecution, isMaskedTool, isAgentYield, isSnapshot, isRollingSummary } from '../ir/graphUtils.js';
|
||||
import { serializeUserPrompt, serializeAgentThought, serializeToolExecution, serializeMaskedTool, serializeAgentYield } from '../ir/fromIr.js';
|
||||
import type { IrNodeBehaviorRegistry } from '../ir/behaviorRegistry.js';
|
||||
|
||||
/**
|
||||
* The flat token cost assigned to a single multi-modal asset (like an image tile)
|
||||
@@ -19,7 +18,10 @@ import { serializeUserPrompt, serializeAgentThought, serializeToolExecution, ser
|
||||
export class ContextTokenCalculator {
|
||||
private readonly tokenCache = new Map<string, number>();
|
||||
|
||||
constructor(private readonly charsPerToken: number) {}
|
||||
constructor(
|
||||
private readonly charsPerToken: number,
|
||||
private readonly registry: IrNodeBehaviorRegistry
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Estimates tokens for a simple string based on character count.
|
||||
@@ -42,23 +44,9 @@ export class ContextTokenCalculator {
|
||||
* Because nodes are immutable, this cost never changes for this node ID.
|
||||
*/
|
||||
cacheNodeTokens(node: ConcreteNode): number {
|
||||
let tokens = 0;
|
||||
if (isUserPrompt(node)) {
|
||||
const content = serializeUserPrompt(node);
|
||||
if (content && content.parts) tokens = this.estimateTokensForParts(content.parts as Part[]);
|
||||
} else if (isAgentThought(node)) {
|
||||
tokens = this.estimateTokensForParts([serializeAgentThought(node)]);
|
||||
} else if (isToolExecution(node)) {
|
||||
const parts = serializeToolExecution(node);
|
||||
tokens = this.estimateTokensForParts([parts.call, parts.response]);
|
||||
} else if (isMaskedTool(node)) {
|
||||
const parts = serializeMaskedTool(node);
|
||||
tokens = this.estimateTokensForParts([parts.call, parts.response]);
|
||||
} else if (isAgentYield(node)) {
|
||||
tokens = this.estimateTokensForParts([serializeAgentYield(node)]);
|
||||
} else if (isSnapshot(node) || isRollingSummary(node)) {
|
||||
tokens = this.estimateTokensForParts([{ text: node.text }]);
|
||||
}
|
||||
const behavior = this.registry.get(node.type);
|
||||
const parts = behavior.getEstimatableParts(node);
|
||||
const tokens = this.estimateTokensForParts(parts);
|
||||
this.tokenCache.set(node.id, tokens);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user