-
Notifications
You must be signed in to change notification settings - Fork 173
Feat: Implement Anchored Iterative Summarization in adk-js context compactors #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AmaadMartin
wants to merge
6
commits into
google:main
Choose a base branch
from
AmaadMartin:feat/anchored-summarization
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
d9ed04b
Feat: Implement AnchoredContextCompactor in adk-js
e04d7d1
Fix: Mock Date.now in anchored integration test to prevent CI macos-l…
34dddf9
Chore: Revert package-lock.json changes
c1ab853
Merge remote-tracking branch 'origin/main' into feat/anchored-summari…
0e68707
Merge branch 'main' into feat/anchored-summarization
kalenkevich 151f653
Refactor context compactor retain logic to shared utility
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import {InvocationContext} from '../agents/invocation_context.js'; | ||
| import {CompactedEvent, isScratchpadEvent} from '../events/compacted_event.js'; | ||
| import { | ||
| Event, | ||
| getEventTokens, | ||
| getFunctionCalls, | ||
| getFunctionResponses, | ||
| } from '../events/event.js'; | ||
| import {BaseContextCompactor} from './base_context_compactor.js'; | ||
| import {BaseSummarizer} from './summarizers/base_summarizer.js'; | ||
|
|
||
| export interface AnchoredContextCompactorOptions { | ||
| /** The maximum number of tokens to retain in the session history before compaction. */ | ||
| tokenThreshold: number; | ||
| /** | ||
| * The minimum number of raw events to keep at the end of the session. | ||
| * Compaction will not affect these tail events (unless needed for tool splits). | ||
| */ | ||
| eventRetentionSize: number; | ||
| /** The summarizer used to create the compacted event content. */ | ||
| summarizer: BaseSummarizer; | ||
| } | ||
|
|
||
| /** | ||
| * A context compactor that maintains a single persistent 'Scratchpad' or | ||
| * 'State Tracker' event at the top of the context history. | ||
| * | ||
| * When compaction is triggered, it merges new raw events into the existing | ||
| * Scratchpad event and discards them from the active history view. | ||
| */ | ||
| export class AnchoredContextCompactor implements BaseContextCompactor { | ||
| private readonly tokenThreshold: number; | ||
| private readonly eventRetentionSize: number; | ||
| private readonly summarizer: BaseSummarizer; | ||
|
|
||
| constructor(options: AnchoredContextCompactorOptions) { | ||
| this.tokenThreshold = options.tokenThreshold; | ||
| this.eventRetentionSize = options.eventRetentionSize; | ||
| this.summarizer = options.summarizer; | ||
| } | ||
|
|
||
| private getActiveEvents(events: Event[]): Event[] { | ||
| let latestScratchpad: CompactedEvent | undefined = undefined; | ||
|
|
||
| for (let i = events.length - 1; i >= 0; i--) { | ||
| const e = events[i]; | ||
| if (isScratchpadEvent(e)) { | ||
| latestScratchpad = e; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (!latestScratchpad) { | ||
| return events; | ||
| } | ||
|
|
||
| const activeRawEvents = events.filter( | ||
| (e) => e.timestamp > latestScratchpad!.endTime && !isScratchpadEvent(e), | ||
| ); | ||
|
|
||
| return [latestScratchpad, ...activeRawEvents]; | ||
| } | ||
|
|
||
| shouldCompact( | ||
| invocationContext: InvocationContext, | ||
| ): boolean | Promise<boolean> { | ||
| const events = invocationContext.session.events; | ||
| const activeEvents = this.getActiveEvents(events); | ||
| const hasScratchpad = | ||
| activeEvents.length > 0 && isScratchpadEvent(activeEvents[0]); | ||
| const rawEvents = hasScratchpad ? activeEvents.slice(1) : activeEvents; | ||
|
|
||
| if (rawEvents.length <= this.eventRetentionSize) { | ||
| return false; | ||
| } | ||
|
|
||
| const totalTokens = activeEvents.reduce( | ||
| (sum, event) => sum + getEventTokens(event), | ||
| 0, | ||
| ); | ||
|
|
||
| return totalTokens > this.tokenThreshold; | ||
| } | ||
|
|
||
| async compact(invocationContext: InvocationContext): Promise<void> { | ||
| const events = invocationContext.session.events; | ||
| const activeEvents = this.getActiveEvents(events); | ||
| const hasScratchpad = | ||
| activeEvents.length > 0 && isScratchpadEvent(activeEvents[0]); | ||
| const rawEvents = hasScratchpad ? activeEvents.slice(1) : activeEvents; | ||
|
|
||
| if (rawEvents.length <= this.eventRetentionSize) { | ||
| return; | ||
| } | ||
|
|
||
| // Determine the baseline index to retain from the active raw events. | ||
| let retainStartIndex = Math.max( | ||
| 0, | ||
| rawEvents.length - this.eventRetentionSize, | ||
| ); | ||
|
|
||
| // Prevent splitting between a tool call and its response. | ||
| while (retainStartIndex > 0) { | ||
| const eventToRetain = rawEvents[retainStartIndex]; | ||
| const previousEvent = rawEvents[retainStartIndex - 1]; | ||
|
|
||
| if ( | ||
| getFunctionResponses(eventToRetain).length > 0 && | ||
| getFunctionCalls(previousEvent).length > 0 | ||
| ) { | ||
| retainStartIndex--; | ||
| } else { | ||
| // No conflict, safe to split here. | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (retainStartIndex === 0) { | ||
| // Cannot compact if we have to retain everything | ||
| return; | ||
| } | ||
|
|
||
| // Extract raw events to compact. | ||
| const rawEventsToCompact = rawEvents.slice(0, retainStartIndex); | ||
|
|
||
| let scratchpadEvent: CompactedEvent; | ||
|
|
||
| if (hasScratchpad) { | ||
| const existingScratchpad = activeEvents[0] as CompactedEvent; | ||
| scratchpadEvent = await this.summarizer.summarize([ | ||
| existingScratchpad, | ||
| ...rawEventsToCompact, | ||
| ]); | ||
| } else { | ||
| scratchpadEvent = await this.summarizer.summarize(rawEventsToCompact); | ||
| } | ||
|
|
||
| // Ensure the event is marked as scratchpad and has system author. | ||
| const updatedScratchpad = { | ||
| ...scratchpadEvent, | ||
| isScratchpad: true, | ||
| author: 'system', | ||
| } as CompactedEvent; | ||
|
|
||
| // Reconstruct the events list: inactive events + new scratchpad + active retained events | ||
| const inactiveEvents = events.slice(0, events.indexOf(activeEvents[0])); | ||
| const retainedRawEvents = rawEvents.slice(retainStartIndex); | ||
|
|
||
| const newEventsList = [ | ||
| ...inactiveEvents, | ||
| updatedScratchpad, | ||
| ...retainedRawEvents, | ||
| ]; | ||
|
|
||
| // Mutate the original session events array. | ||
| events.length = 0; | ||
| events.push(...newEventsList); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it should be in shouldCompact and may be mostly duplicated between Token based and this anchored strategy. Consider moving to shouldCompact and maybe extract a common function for both to use.