Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,18 @@ export class ContextCompactorRequestProcessor implements BaseLlmRequestProcessor
compactor.shouldCompact(invocationContext),
);
if (shouldCompact) {
const trigger = compactor.trigger ?? ContextCompactionTrigger.Auto;
await invocationContext.pluginManager.runBeforeContextCompaction({
invocationContext,
trigger: ContextCompactionTrigger.Auto,
trigger,
});

const oldEvents = new Set(invocationContext.session.events);
await Promise.resolve(compactor.compact(invocationContext));

await invocationContext.pluginManager.runAfterContextCompaction({
invocationContext,
trigger: ContextCompactionTrigger.Auto,
trigger,
});

const newEvents = invocationContext.session.events.filter(
Expand Down
2 changes: 2 additions & 0 deletions core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export {
type CodeExecutionResult,
type File,
} from './code_executors/code_execution_utils.js';
export {AgentControlledContextCompactor} from './context/agent_controlled_context_compactor.js';
export type {BaseContextCompactor} from './context/base_context_compactor.js';
export type {BaseSummarizer} from './context/summarizers/base_summarizer.js';
export {LlmSummarizer} from './context/summarizers/llm_summarizer.js';
Expand Down Expand Up @@ -215,6 +216,7 @@ export type {
} from './tools/base_tool.js';
export {BaseToolset, isBaseToolset} from './tools/base_toolset.js';
export type {ToolPredicate} from './tools/base_toolset.js';
export {ConsolidateContextTool} from './tools/consolidate_context_tool.js';
export {EXIT_LOOP, ExitLoopTool} from './tools/exit_loop_tool.js';
export {FunctionTool, isFunctionTool} from './tools/function_tool.js';
export type {
Expand Down
106 changes: 106 additions & 0 deletions core/src/context/agent_controlled_context_compactor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {InvocationContext} from '../agents/invocation_context.js';
import {CompactedEvent, isCompactedEvent} from '../events/compacted_event.js';
import {createEvent, Event} from '../events/event.js';
import {ContextCompactionTrigger} from '../plugins/base_plugin.js';
import {BaseContextCompactor} from './base_context_compactor.js';
import {BaseSummarizer} from './summarizers/base_summarizer.js';

/**
* A context compactor that triggers compaction when the agent explicitly
* requests it via the `ConsolidateContextTool`.
*/
export class AgentControlledContextCompactor implements BaseContextCompactor {
readonly trigger = ContextCompactionTrigger.AgentControlled;
private readonly summarizer: BaseSummarizer;

constructor(options: {summarizer: BaseSummarizer}) {
this.summarizer = options.summarizer;
}

shouldCompact(invocationContext: InvocationContext): boolean {
return invocationContext.session.state['temp:consolidate_context'] === true;
}

async compact(invocationContext: InvocationContext): Promise<void> {
const events = invocationContext.session.events;
const activeEvents = this.getActiveEvents(events);

// Find the consolidate_context tool call.
const consolidateToolCallIndex = activeEvents.reduce(
(acc, e, idx) =>
e.content?.parts?.some(
(p) => p.functionCall?.name === 'consolidate_context',
)
? idx
: acc,
-1,
);

if (consolidateToolCallIndex <= 0) {
// If not found (index -1) or tool call is the first event (index 0),
// there is nothing to compact before it. Clear flags and return.
this.clearFlags(invocationContext);
return;
}

// We compact everything BEFORE the consolidate_context tool call.
const eventsToCompact = activeEvents.slice(0, consolidateToolCallIndex);

const detail = invocationContext.session.state[
'temp:consolidate_context_detail'
] as string | undefined;

if (detail) {
eventsToCompact.push(
createEvent({
author: 'system',
timestamp: eventsToCompact[eventsToCompact.length - 1].timestamp,
content: {
role: 'user',
parts: [
{
text: `CRITICAL INSTRUCTION FOR SUMMARY: Please summarize the history. Focus especially on: ${detail}`,
},
],
},
}),
);
}

try {
const compactedEvent = await this.summarizer.summarize(eventsToCompact);
invocationContext.session.events.push(compactedEvent);
} catch (error) {
// If the summarizer fails, log the error, clear the flags, and proceed without compaction.
// (do not block the agent run)
console.error('Compaction failed:', error);
} finally {
this.clearFlags(invocationContext);
}
}

private clearFlags(invocationContext: InvocationContext) {
delete invocationContext.session.state['temp:consolidate_context'];
delete invocationContext.session.state['temp:consolidate_context_detail'];
}

private getActiveEvents(events: Event[]): Event[] {
const latest = events.filter(isCompactedEvent).pop() as
| CompactedEvent
| undefined;
return latest
? [
latest,
...events.filter(
(e) => !isCompactedEvent(e) && e.timestamp > latest.endTime,
),
]
: events;
}
}
4 changes: 4 additions & 0 deletions core/src/context/base_context_compactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
*/

import {InvocationContext} from '../agents/invocation_context.js';
import {ContextCompactionTrigger} from '../plugins/base_plugin.js';

/**
* Interface for compacting the context history in an agent session.
*/
export interface BaseContextCompactor {
/** The trigger associated with this compactor. */
readonly trigger?: ContextCompactionTrigger;

/**
* Determines whether the context should be compacted.
*
Expand Down
1 change: 1 addition & 0 deletions core/src/plugins/base_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {experimental} from '../utils/experimental.js';
export enum ContextCompactionTrigger {
Manual = 'Manual',
Auto = 'Auto',
AgentControlled = 'AgentControlled',
}

/**
Expand Down
63 changes: 63 additions & 0 deletions core/src/tools/consolidate_context_tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {FunctionDeclaration, Type} from '@google/genai';

import {BaseTool, RunAsyncToolRequest} from './base_tool.js';

/**
* Tool for requesting context consolidation (compaction).
*
* When called by an LLM agent, this tool sets the state flags
* `temp:consolidate_context` and optionally `temp:consolidate_context_detail`,
* signalling the context compactor to trigger compaction.
*/
export class ConsolidateContextTool extends BaseTool {
constructor() {
super({
name: 'consolidate_context',
description:
'Requests context consolidation (compaction) to manage history size. ' +
'Use this when a subtask is complete and you want to summarize progress ' +
'and clear detailed history.',
});
}

override _getDeclaration(): FunctionDeclaration {
return {
name: this.name,
description: this.description,
parameters: {
type: Type.OBJECT,
properties: {
detail: {
type: Type.STRING,
description:
'Optional description of what has been accomplished so far. ' +
'This detail will be used to guide the summarization of the history.',
},
},
},
};
}

override async runAsync({
args,
toolContext,
}: RunAsyncToolRequest): Promise<unknown> {
toolContext.state.set('temp:consolidate_context', true);
if (args['detail'] !== undefined) {
toolContext.state.set(
'temp:consolidate_context_detail',
args['detail'] as string,
);
}
return {
status: 'success',
message: 'Context consolidation requested.',
};
}
}
Loading
Loading