mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-10 18:10:43 -07:00
fix(context): implement loose boundary policy for gc backstop. (#26594)
This commit is contained in:
@@ -61,4 +61,169 @@ describe('render', () => {
|
||||
|
||||
expect(result.history).toEqual([{ text: '1' }, { text: '2' }]);
|
||||
});
|
||||
|
||||
it('simulates the boundary knapsack problem (loose boundary policy)', async () => {
|
||||
// 10k, 20k, 40k, 5k
|
||||
const mockNodes: ConcreteNode[] = [
|
||||
{
|
||||
id: 'D',
|
||||
type: NodeType.USER_PROMPT,
|
||||
payload: {} as Part,
|
||||
} as unknown as ConcreteNode,
|
||||
{
|
||||
id: 'C',
|
||||
type: NodeType.AGENT_THOUGHT,
|
||||
payload: {} as Part,
|
||||
} as unknown as ConcreteNode,
|
||||
{
|
||||
id: 'B',
|
||||
type: NodeType.USER_PROMPT,
|
||||
payload: {} as Part,
|
||||
} as unknown as ConcreteNode,
|
||||
{
|
||||
id: 'A',
|
||||
type: NodeType.AGENT_THOUGHT,
|
||||
payload: {} as Part,
|
||||
} as unknown as ConcreteNode,
|
||||
];
|
||||
|
||||
const tokenMap: Record<string, number> = {
|
||||
D: 5000,
|
||||
C: 40000,
|
||||
B: 20000,
|
||||
A: 10000,
|
||||
};
|
||||
|
||||
const orchestrator = {
|
||||
executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) =>
|
||||
nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id)),
|
||||
),
|
||||
} as unknown as PipelineOrchestrator;
|
||||
|
||||
const sidecar = {
|
||||
config: {
|
||||
budget: { maxTokens: 150000, retainedTokens: 65000 },
|
||||
},
|
||||
} as unknown as ContextProfile;
|
||||
|
||||
const currentTokens = 160000;
|
||||
|
||||
const env = {
|
||||
llmClient: {
|
||||
countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }),
|
||||
},
|
||||
tokenCalculator: {
|
||||
calculateConcreteListTokens: vi.fn((nodes) => {
|
||||
if (nodes.length === 1) return tokenMap[nodes[0].id];
|
||||
return currentTokens;
|
||||
}),
|
||||
calculateTokenBreakdown: vi.fn(() => ({})),
|
||||
},
|
||||
graphMapper: {
|
||||
fromGraph: vi.fn((nodes: readonly ConcreteNode[]) =>
|
||||
nodes.map((n) => ({ text: n.id })),
|
||||
),
|
||||
},
|
||||
} as unknown as ContextEnvironment;
|
||||
|
||||
const tracer = {
|
||||
logEvent: vi.fn(),
|
||||
} as unknown as ContextTracer;
|
||||
|
||||
const result = await render(
|
||||
mockNodes,
|
||||
orchestrator,
|
||||
sidecar,
|
||||
tracer,
|
||||
env,
|
||||
new Map(),
|
||||
0,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const surviving = result.history.map((c: any) => c.text);
|
||||
// Loose Boundary: A (10k), B (20k), C (40k). Total = 70k.
|
||||
// Adding C pushes rolling total (70k) above retainedTokens (65k).
|
||||
// Under loose policy, C survives. D is strictly older and drops.
|
||||
expect(surviving).toEqual(['C', 'B', 'A']); // D is dropped
|
||||
});
|
||||
|
||||
it('drops nodes that are STRICTLY older than the boundary node', async () => {
|
||||
const mockNodes: ConcreteNode[] = [
|
||||
{
|
||||
id: 'A',
|
||||
type: NodeType.USER_PROMPT,
|
||||
payload: {} as Part,
|
||||
} as unknown as ConcreteNode,
|
||||
{
|
||||
id: 'B',
|
||||
type: NodeType.AGENT_THOUGHT,
|
||||
payload: {} as Part,
|
||||
} as unknown as ConcreteNode,
|
||||
{
|
||||
id: 'C',
|
||||
type: NodeType.USER_PROMPT,
|
||||
payload: {} as Part,
|
||||
} as unknown as ConcreteNode,
|
||||
];
|
||||
|
||||
const tokenMap: Record<string, number> = {
|
||||
C: 40000,
|
||||
B: 40000,
|
||||
A: 10000,
|
||||
};
|
||||
|
||||
const orchestrator = {
|
||||
executeTriggerSync: vi.fn(async (trigger, nodes, agedOutNodes) =>
|
||||
nodes.filter((n: ConcreteNode) => !agedOutNodes.has(n.id)),
|
||||
),
|
||||
} as unknown as PipelineOrchestrator;
|
||||
|
||||
const sidecar = {
|
||||
config: {
|
||||
budget: { maxTokens: 150000, retainedTokens: 65000 },
|
||||
},
|
||||
} as unknown as ContextProfile;
|
||||
|
||||
const currentTokens = 160000;
|
||||
|
||||
const env = {
|
||||
llmClient: {
|
||||
countTokens: vi.fn().mockResolvedValue({ totalTokens: 1000 }),
|
||||
},
|
||||
tokenCalculator: {
|
||||
calculateConcreteListTokens: vi.fn((nodes) => {
|
||||
if (nodes.length === 1) return tokenMap[nodes[0].id];
|
||||
return currentTokens;
|
||||
}),
|
||||
calculateTokenBreakdown: vi.fn(() => ({})),
|
||||
},
|
||||
graphMapper: {
|
||||
fromGraph: vi.fn((nodes: readonly ConcreteNode[]) =>
|
||||
nodes.map((n) => ({ text: n.id })),
|
||||
),
|
||||
},
|
||||
} as unknown as ContextEnvironment;
|
||||
|
||||
const tracer = {
|
||||
logEvent: vi.fn(),
|
||||
} as unknown as ContextTracer;
|
||||
|
||||
const result = await render(
|
||||
mockNodes,
|
||||
orchestrator,
|
||||
sidecar,
|
||||
tracer,
|
||||
env,
|
||||
new Map(),
|
||||
0,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const surviving = result.history.map((c: any) => c.text);
|
||||
// C(40k), B(40k). Adding B pushes total to 80k. B is the boundary node and survives. A drops.
|
||||
expect(surviving).toEqual(['B', 'C']); // A is dropped
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ContextTracer } from '../tracer.js';
|
||||
import type { ContextProfile } from '../config/profiles.js';
|
||||
import type { PipelineOrchestrator } from '../pipeline/orchestrator.js';
|
||||
import type { ContextEnvironment } from '../pipeline/environment.js';
|
||||
import { performCalibration } from '../utils/tokenCalibration.js';
|
||||
|
||||
/**
|
||||
* Maps the Episodic Context Graph back into a raw Gemini Content[] array for transmission.
|
||||
@@ -68,6 +69,7 @@ export async function render(
|
||||
tracer.logEvent('Render', 'Render Context for LLM', {
|
||||
renderedContext: contents,
|
||||
});
|
||||
performCalibration(env, visibleNodes, contents);
|
||||
return { history: contents, didApplyManagement: false };
|
||||
}
|
||||
const targetDelta = currentTokens - sidecar.config.budget.retainedTokens;
|
||||
@@ -83,9 +85,12 @@ export async function render(
|
||||
// Start from newest and count backwards
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const node = nodes[i];
|
||||
const priorTokens = rollingTokens;
|
||||
const nodeTokens = env.tokenCalculator.calculateConcreteListTokens([node]);
|
||||
rollingTokens += nodeTokens;
|
||||
if (rollingTokens > sidecar.config.budget.retainedTokens) {
|
||||
|
||||
// Loose Boundary Policy: Keep the node that crosses the boundary
|
||||
if (priorTokens > sidecar.config.budget.retainedTokens) {
|
||||
agedOutNodes.add(node.id);
|
||||
}
|
||||
}
|
||||
@@ -113,5 +118,6 @@ export async function render(
|
||||
tracer.logEvent('Render', 'Render Sanitized Context for LLM', {
|
||||
renderedContextSanitized: contents,
|
||||
});
|
||||
performCalibration(env, visibleNodes, contents);
|
||||
return { history: contents, didApplyManagement: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user