lints clean, but still need further tidies

This commit is contained in:
Your Name
2026-04-09 04:48:07 +00:00
parent 8fd439678b
commit 46c20c6d6e
14 changed files with 84 additions and 77 deletions
@@ -150,7 +150,7 @@ describe('ContextManager Golden Tests', () => {
const history = createLargeHistory();
(
contextManager as unknown as { pristineEpisodes: Episode[] }
).pristineEpisodes = (contextManager as any).env.irMapper.toIr(history, (contextManager as any).env.tokenCalculator);
).pristineEpisodes = (contextManager as unknown as import("../pipeline.js").ContextWorkingBuffer).env.irMapper.toIr(history, (contextManager as unknown as import("../pipeline.js").ContextWorkingBuffer).env.tokenCalculator);
const result = await contextManager.projectCompressedHistory();
expect(result).toMatchSnapshot();
});
@@ -34,6 +34,8 @@ export const UserPromptBehavior: IrNodeBehavior<UserPrompt> = {
case 'raw_part':
parts.push(sp.part);
break;
default:
break;
}
}
return parts;
@@ -3,7 +3,7 @@
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { BlobDegradationProcessor } from './blobDegradationProcessor.js';
import {
createMockEnvironment,
@@ -14,19 +14,17 @@ import type { UserPrompt, SemanticPart } from '../ir/types.js';
describe('BlobDegradationProcessor', () => {
it('should ignore text parts and only target inline_data and file_data', async () => {
const env = createMockEnvironment();
// Simulate each part costing 100 tokens, but text costing 10 tokens
env.tokenCalculator.estimateTokensForParts = vi.fn((parts: any[]) => {
if (parts[0].text) return 10;
return 100;
});
// charsPerToken = 1
// We want the degraded text to be cheaper than the original blob.
// Degraded text is ~100 chars ("...degraded to text...").
// So we make the blob data 200 chars.
const fakeData = 'A'.repeat(200);
const processor = BlobDegradationProcessor.create(env, {});
// Deficit of 50 means budget is NOT satisfied.
const parts: SemanticPart[] = [
{ type: 'text', text: 'Hello' },
{ type: 'inline_data', mimeType: 'image/png', data: 'fake_base64_data' },
{ type: 'inline_data', mimeType: 'image/png', data: fakeData },
{ type: 'text', text: 'World' },
];
@@ -37,16 +35,14 @@ describe('BlobDegradationProcessor', () => {
const targets = [prompt];
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets,
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').InboxSnapshot,
});
// We modified it, so the ID should change and it should have new metadata
expect(result.length).toBe(1);
const modifiedPrompt = result[0] as UserPrompt;
// Original prompt ID was randomUUID(), new one is from idGenerator
expect(modifiedPrompt.id).not.toBe(prompt.id);
expect(modifiedPrompt.semanticParts.length).toBe(3);
@@ -55,23 +51,22 @@ describe('BlobDegradationProcessor', () => {
expect(modifiedPrompt.semanticParts[2]).toEqual(parts[2]);
// The inline_data part should be replaced with text
const degradedPart = modifiedPrompt.semanticParts[1];
const degradedPart = modifiedPrompt.semanticParts[1] as import('../ir/types.js').TextPart;
expect(degradedPart.type).toBe('text');
expect((degradedPart as any).text).toContain('[Multi-Modal Blob (image/png, 0.00MB) degraded to text');
// The transformation should be logged
expect(degradedPart.text).toContain('[Multi-Modal Blob (image/png, 0.00MB) degraded to text');
});
it('should degrade all blobs unconditionally', async () => {
const env = createMockEnvironment();
env.tokenCalculator.estimateTokensForParts = vi.fn((parts: any[]) => {
if (parts[0].text) return 10;
return 100; // saving 90 tokens per degradation
});
const processor = BlobDegradationProcessor.create(env, {});
// Tokens for fileData = 258.
// Degraded text = "[File Reference (video/mp4) degraded to text to preserve context window. Original URI: gs://test1]"
// Degraded text length ~100 characters.
// Since charsPerToken=1, degraded text = 100 tokens.
// Tokens saved = 258 - 100 = 158. This is > 0, so it WILL degrade it!
const prompt = createDummyNode('ep1', 'USER_PROMPT', 100, {
semanticParts: [
{ type: 'file_data', mimeType: 'video/mp4', fileUri: 'gs://test1' },
@@ -82,9 +77,9 @@ describe('BlobDegradationProcessor', () => {
const targets = [prompt];
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets,
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').InboxSnapshot,
});
const modifiedPrompt = result[0] as UserPrompt;
@@ -99,15 +94,14 @@ describe('BlobDegradationProcessor', () => {
const env = createMockEnvironment();
const processor = BlobDegradationProcessor.create(env, {});
const targets: any[] = [];
const targets: Array<import('../ir/types.js').ConcreteNode> = [];
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets,
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').InboxSnapshot,
});
// Should return the exact array ref
expect(result).toBe(targets);
});
});
@@ -70,7 +70,7 @@ export class BlobDegradationProcessor implements ContextProcessor {
let newText = '';
let tokensSaved = 0;
switch (part.type) {
switch (part.type) {
case 'inline_data': {
await ensureDir();
const ext = part.mimeType.split('/')[1] || 'bin';
@@ -114,6 +114,8 @@ export class BlobDegradationProcessor implements ContextProcessor {
tokensSaved = oldTokens - newTokens;
break;
}
default:
break;
}
if (newText && tokensSaved > 0) {
@@ -20,7 +20,7 @@ describe('NodeDistillationProcessor', () => {
};
const env = createMockEnvironment({
llmClient: mockLlmClient as any,
llmClient: mockLlmClient as unknown as import("../pipeline.js").ContextWorkingBuffer,
});
const processor = NodeDistillationProcessor.create(env, {
@@ -46,9 +46,9 @@ describe('NodeDistillationProcessor', () => {
const targets = [prompt, thought, tool];
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets,
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
});
expect(result.length).toBe(3);
@@ -57,7 +57,7 @@ describe('NodeDistillationProcessor', () => {
const compressedPrompt = result[0] as UserPrompt;
expect(compressedPrompt.id).not.toBe(prompt.id);
expect(compressedPrompt.semanticParts[0].type).toBe('text');
expect((compressedPrompt.semanticParts[0] as any).text).toBe('Mocked Summary!');
expect((compressedPrompt.semanticParts[0] as unknown as import("../pipeline.js").ContextWorkingBuffer).text).toBe('Mocked Summary!');
// 2. Agent Thought
const compressedThought = result[1] as AgentThought;
expect(compressedThought.id).toMatch(/^mock-uuid-/);
@@ -78,7 +78,7 @@ describe('NodeDistillationProcessor', () => {
};
const env = createMockEnvironment({
llmClient: mockLlmClient as any,
llmClient: mockLlmClient as unknown as import("../pipeline.js").ContextWorkingBuffer,
});
const processor = NodeDistillationProcessor.create(env, {
@@ -99,9 +99,9 @@ describe('NodeDistillationProcessor', () => {
const targets = [prompt, thought];
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets,
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
});
expect(result.length).toBe(2);
@@ -15,7 +15,7 @@ import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js';
describe('NodeTruncationProcessor', () => {
it('should truncate nodes that exceed maxTokensPerNode', async () => {
const env = createMockEnvironment();
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as any;
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as unknown as import("../pipeline.js").ContextWorkingBuffer;
mockTokenCalculator.tokensToChars = vi.fn().mockReturnValue(10); // Limit is 10 chars
mockTokenCalculator.estimateTokensForString = vi.fn((text: string) => {
@@ -24,7 +24,7 @@ describe('NodeTruncationProcessor', () => {
});
mockTokenCalculator.estimateTokensForParts = vi.fn(() => 1);
(env as any).tokenCalculator = mockTokenCalculator;
(env as unknown as import("../pipeline.js").ContextWorkingBuffer).tokenCalculator = mockTokenCalculator;
const processor = NodeTruncationProcessor.create(env, {
maxTokensPerNode: 1, // Will equal 10 chars limit
@@ -48,9 +48,9 @@ describe('NodeTruncationProcessor', () => {
const targets = [prompt, thought, yieldNode];
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets,
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
});
expect(result.length).toBe(3);
@@ -60,7 +60,7 @@ describe('NodeTruncationProcessor', () => {
expect(squashedPrompt.id).toBe('mock-uuid-1');
expect(squashedPrompt.id).not.toBe(prompt.id);
expect(squashedPrompt.semanticParts[0].type).toBe('text');
expect((squashedPrompt.semanticParts[0] as any).text).toContain('[... OMITTED');
expect((squashedPrompt.semanticParts[0] as unknown as import("../pipeline.js").ContextWorkingBuffer).text).toContain('[... OMITTED');
// 2. Agent Thought
const squashedThought = result[1] as AgentThought;
@@ -77,14 +77,14 @@ describe('NodeTruncationProcessor', () => {
it('should ignore nodes that are below maxTokensPerNode', async () => {
const env = createMockEnvironment();
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as any;
const mockTokenCalculator = new ContextTokenCalculator(1, env.behaviorRegistry) as unknown as import("../pipeline.js").ContextWorkingBuffer;
mockTokenCalculator.tokensToChars = vi.fn().mockReturnValue(100);
mockTokenCalculator.estimateTokensForString = vi.fn((text: string) => text.length);
mockTokenCalculator.estimateTokensForParts = vi.fn(() => 5);
mockTokenCalculator.getTokenCost = vi.fn(() => 5);
(env as any).tokenCalculator = mockTokenCalculator;
(env as unknown as import("../pipeline.js").ContextWorkingBuffer).tokenCalculator = mockTokenCalculator;
const processor = NodeTruncationProcessor.create(env, {
maxTokensPerNode: 100,
@@ -104,9 +104,9 @@ describe('NodeTruncationProcessor', () => {
const targets = [prompt, thought];
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets,
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
});
expect(result.length).toBe(2);
@@ -114,7 +114,7 @@ describe('NodeTruncationProcessor', () => {
// 1. User Prompt (untouched)
const squashedPrompt = result[0] as UserPrompt;
expect(squashedPrompt.id).toBe(prompt.id);
expect((squashedPrompt.semanticParts[0] as any).text).not.toContain('[... OMITTED');
expect((squashedPrompt.semanticParts[0] as unknown as import("../pipeline.js").ContextWorkingBuffer).text).not.toContain('[... OMITTED');
// 2. Agent Thought (untouched)
const untouchedThought = result[1] as AgentThought;
@@ -32,7 +32,7 @@ describe('RollingSummaryProcessor', () => {
createDummyNode('ep1', 'AGENT_YIELD', 50, { text: text50 }, 'id3'),
];
const result = await processor.process({ targets, buffer: {} as any, inbox: {} as any });
const result = await processor.process({ targets, buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer, inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer });
// 3 nodes at 50 cost each.
// The first node (id1) is the initial USER_PROMPT and is always skipped by RollingSummaryProcessor.
@@ -58,7 +58,7 @@ describe('RollingSummaryProcessor', () => {
createDummyNode('ep1', 'AGENT_THOUGHT', 10, { text: text10 }, 'id2'),
];
const result = await processor.process({ targets, buffer: {} as any, inbox: {} as any });
const result = await processor.process({ targets, buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer, inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer });
// Deficit accumulator reaches 10. This is < 100 limit, and total summarizable nodes < 2 anyway.
expect(result.length).toBe(2);
@@ -18,7 +18,7 @@ describe('StateSnapshotProcessor', () => {
const targets = [createDummyNode('ep1', 'USER_PROMPT')];
const inbox = new InboxSnapshotImpl([]);
const result = await processor.process({ buffer: {} as any, targets, inbox });
const result = await processor.process({ buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer, targets, inbox });
expect(result).toBe(targets); // Strict equality
});
@@ -46,7 +46,7 @@ describe('StateSnapshotProcessor', () => {
}
]);
const result = await processor.process({ buffer: {} as any, targets, inbox });
const result = await processor.process({ buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer, targets, inbox });
// Should remove A and B, insert Snapshot, keep C
expect(result.length).toBe(2);
@@ -78,7 +78,7 @@ describe('StateSnapshotProcessor', () => {
}
]);
const result = await processor.process({ buffer: {} as any, targets, inbox });
const result = await processor.process({ buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer, targets, inbox });
// Because deficit is 0, and Inbox was rejected, nothing should change
expect(result.length).toBe(1);
@@ -96,7 +96,7 @@ describe('StateSnapshotProcessor', () => {
const targets = [nodeA, nodeB, nodeC];
const inbox = new InboxSnapshotImpl([]);
const result = await processor.process({ buffer: {} as any, targets, inbox });
const result = await processor.process({ buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer, targets, inbox });
// Should synthesize a new snapshot synchronously
expect(env.llmClient.generateContent).toHaveBeenCalled();
@@ -31,9 +31,9 @@ describe('ToolMaskingProcessor', () => {
});
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets: [toolStep],
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
});
expect(result.length).toBe(1);
@@ -43,7 +43,7 @@ describe('ToolMaskingProcessor', () => {
expect(masked.id).not.toBe(toolStep.id);
// It should have masked the observation
const obs = (masked as any).observation;
const obs = (masked as unknown as import("../pipeline.js").ContextWorkingBuffer).observation;
expect(obs.result).toContain('<tool_output_masked>');
expect(obs.metadata).toBe('short'); // Untouched
@@ -66,9 +66,9 @@ describe('ToolMaskingProcessor', () => {
});
const result = await processor.process({
buffer: {} as any,
buffer: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
targets: [toolStep],
inbox: {} as any,
inbox: undefined as unknown as import('../pipeline.js').ContextWorkingBuffer,
});
// Returned the exact same object reference
@@ -14,6 +14,7 @@ import {
ENTER_PLAN_MODE_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
} from '../../tools/tool-names.js';
import type { Part } from '@google/genai';
const UNMASKABLE_TOOLS = new Set([
ACTIVATE_SKILL_TOOL_NAME,
@@ -229,7 +230,7 @@ export class ToolMaskingProcessor implements ContextProcessor {
},
]);
let obsPart: any = {};
let obsPart: Record<string, unknown> = {};
if (maskedObs) {
obsPart = {
functionResponse: {
@@ -240,7 +241,7 @@ export class ToolMaskingProcessor implements ContextProcessor {
};
}
const newObsTokens = this.env.tokenCalculator.estimateTokensForParts([obsPart]);
const newObsTokens = this.env.tokenCalculator.estimateTokensForParts([obsPart as Part]);
const tokensSaved =
this.env.tokenCalculator.getTokenCost(node) -
+3 -1
View File
@@ -35,7 +35,9 @@ export class InboxSnapshotImpl implements InboxSnapshot {
}
getMessages<T = unknown>(topic: string): ReadonlyArray<InboxMessage<T>> {
return this.messages.filter((m) => m.topic === topic) as ReadonlyArray<InboxMessage<T>>;
const raw = this.messages.filter((m) => m.topic === topic);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return raw as ReadonlyArray<InboxMessage<T>>;
}
consume(messageId: string): void {
@@ -25,10 +25,10 @@ class DummySyncProcessor implements ContextProcessor {
readonly name = 'DummySync';
readonly id = 'DummySync';
readonly options = {};
async process(args: any) {
async process(args: import('../pipeline.js').ProcessArgs) {
const newTargets = [...args.targets];
if (newTargets.length > 0) {
newTargets[0] = { ...newTargets[0], dummyModified: true };
newTargets[0] = { ...newTargets[0], dummyModified: true } as unknown as import('../ir/types.js').ConcreteNode;
}
return newTargets;
}
@@ -42,10 +42,10 @@ class DummyAsyncProcessor implements ContextProcessor {
readonly name = 'DummyAsync';
readonly id = 'DummyAsync';
readonly options = {};
async process(args: any) {
async process(args: import('../pipeline.js').ProcessArgs) {
const newTargets = [...args.targets];
if (newTargets.length > 0) {
newTargets[0] = { ...newTargets[0], dummyModified: true };
newTargets[0] = { ...newTargets[0], dummyModified: true } as unknown as import('../ir/types.js').ConcreteNode;
}
return newTargets;
}
@@ -59,7 +59,7 @@ class ThrowingProcessor implements ContextProcessor {
readonly name = 'Throwing';
readonly id = 'Throwing';
readonly options = {};
async process(): Promise<any> {
async process(): Promise<ReadonlyArray<import('../ir/types.js').ConcreteNode>> {
throw new Error('Processor failed intentionally');
}
}
@@ -13,7 +13,7 @@ describe('SidecarRegistry', () => {
const processorDef: ContextProcessorDef = {
id: 'TestProcessor',
schema: { type: 'object' },
create: () => ({} as any),
create: () => ({} as unknown as import('../pipeline.js').ContextProcessor),
};
registry.registerProcessor(processorDef);
@@ -26,7 +26,7 @@ describe('SidecarRegistry', () => {
const workerDef: ContextWorkerDef = {
id: 'TestWorker',
schema: { type: 'object' },
create: () => ({} as any),
create: () => ({} as unknown as import('../pipeline.js').ContextProcessor),
};
registry.registerWorker(workerDef);
@@ -49,15 +49,15 @@ describe('SidecarRegistry', () => {
registry.registerProcessor({
id: 'TestProcessor',
schema: { title: 'processorSchema' },
create: () => ({} as any),
create: () => ({} as unknown as import('../pipeline.js').ContextProcessor),
});
registry.registerWorker({
id: 'TestWorker',
schema: { title: 'workerSchema' },
create: () => ({} as any),
create: () => ({} as unknown as import('../pipeline.js').ContextProcessor),
});
const schemas = registry.getSchemas() as any[];
const schemas = registry.getSchemas() as unknown as Array<Record<string, unknown>>;
expect(schemas.length).toBe(2);
expect(schemas.find(s => s.title === 'processorSchema')).toBeDefined();
expect(schemas.find(s => s.title === 'workerSchema')).toBeDefined();
@@ -65,8 +65,8 @@ describe('SidecarRegistry', () => {
it('should safely clear the registry', () => {
const registry = new SidecarRegistry();
registry.registerProcessor({ id: 'TestProcessor', schema: {}, create: () => ({} as any) });
registry.registerWorker({ id: 'TestWorker', schema: {}, create: () => ({} as any) });
registry.registerProcessor({ id: 'TestProcessor', schema: {}, create: () => ({} as unknown as import('../pipeline.js').ContextProcessor) });
registry.registerWorker({ id: 'TestWorker', schema: {}, create: () => ({} as unknown as import('../pipeline.js').ContextProcessor) });
registry.clear();
@@ -29,6 +29,7 @@ import type { Content , GenerateContentResponse } from '@google/genai';
* Used to avoid having to manually construct the deeply nested candidate/content/part structure.
*/
export const createMockGenerateContentResponse = (text: string): GenerateContentResponse =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
({
candidates: [{ content: { role: 'model', parts: [{ text }] }, index: 0 }],
}) as GenerateContentResponse;
@@ -46,7 +47,7 @@ export function createDummyNode(
id: id || randomUUID(),
episodeId: logicalParentId,
logicalParentId,
type: type as any,
type,
timestamp: Date.now(),
text: `Dummy ${type}`,
name: type === 'SYSTEM_EVENT' ? 'dummy_event' : undefined,
@@ -94,9 +95,11 @@ export function createDummyToolNode(
export function createMockEnvironment(
overrides?: Partial<ContextEnvironment>,
): ContextEnvironment {
const llmClient = vi.fn().mockReturnValue({
const mockClient: Partial<BaseLlmClient> = {
generateContent: vi.fn().mockResolvedValue(createMockGenerateContentResponse('Mock LLM summary response')),
})() as unknown as BaseLlmClient;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const llmClient = mockClient as BaseLlmClient;
const tracer = new ContextTracer({ targetDir: '/tmp', sessionId: 'mock-session' });
const eventBus = new ContextEventBus();
@@ -114,7 +117,10 @@ export function createMockEnvironment(
new DeterministicIdGenerator('mock-uuid-')
);
return { ...env, ...overrides };
if (overrides) {
Object.assign(env, overrides);
}
return env;
}
/**