diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfig.kt b/core/src/commonMain/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfig.kt index e4aa3420..2e583518 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfig.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfig.kt @@ -27,11 +27,19 @@ package com.google.adk.kt.summarizer * @property summarizer The [EventSummarizer] used to produce the compaction summary. When `null`, * the runner defaults it to an [LlmEventSummarizer] backed by the root agent's model, requiring * the root to be an `LlmAgent` (otherwise it throws at construction). + * @property tokenThreshold The most recent prompt token count that triggers + * intra-invocation token-threshold (tail-retention) compaction before an LLM call. Must be + * strictly positive when set, and must be set together with [eventRetentionSize]. + * @property eventRetentionSize The number of most recent events kept raw (un-compacted) when + * token-threshold compaction fires; the older events are summarized into a single event. Must be + * non-negative when set, and must be set together with [tokenThreshold]. */ data class EventsCompactionConfig( val compactionInterval: Int? = null, val overlapSize: Int? = null, val summarizer: EventSummarizer? = null, + val tokenThreshold: Int? = null, + val eventRetentionSize: Int? = null, ) { init { if (compactionInterval != null) { @@ -46,8 +54,23 @@ data class EventsCompactionConfig( "compactionInterval and overlapSize must be set together or both null " + "(got compactionInterval=$compactionInterval, overlapSize=$overlapSize)." } + if (tokenThreshold != null) { + require(tokenThreshold > 0) { "tokenThreshold must be > 0, but was $tokenThreshold." } + } + if (eventRetentionSize != null) { + require(eventRetentionSize >= 0) { + "eventRetentionSize must be >= 0, but was $eventRetentionSize." + } + } + require((tokenThreshold == null) == (eventRetentionSize == null)) { + "tokenThreshold and eventRetentionSize must be set together or both null " + + "(got tokenThreshold=$tokenThreshold, eventRetentionSize=$eventRetentionSize)." + } } /** Returns true when sliding-window compaction is fully configured. */ fun hasSlidingWindowConfig(): Boolean = compactionInterval != null && overlapSize != null + + /** Returns true when token-threshold compaction is fully configured. */ + fun hasTokenThresholdConfig(): Boolean = tokenThreshold != null && eventRetentionSize != null } diff --git a/core/src/commonTest/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfigTest.kt b/core/src/commonTest/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfigTest.kt index 6202e909..4eff4c5e 100644 --- a/core/src/commonTest/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfigTest.kt +++ b/core/src/commonTest/kotlin/com/google/adk/kt/summarizer/EventsCompactionConfigTest.kt @@ -29,6 +29,7 @@ class EventsCompactionConfigTest { val config = EventsCompactionConfig() assertFalse(config.hasSlidingWindowConfig()) + assertFalse(config.hasTokenThresholdConfig()) } @Test @@ -83,6 +84,77 @@ class EventsCompactionConfigTest { } } + @Test + fun construct_validTokenThreshold_succeeds() { + val config = + EventsCompactionConfig( + tokenThreshold = 50000, + eventRetentionSize = 5, + summarizer = NoopSummarizer, + ) + + assertTrue(config.hasTokenThresholdConfig()) + assertFalse(config.hasSlidingWindowConfig()) + assertEquals(50000, config.tokenThreshold) + assertEquals(5, config.eventRetentionSize) + } + + @Test + fun construct_eventRetentionSizeZero_succeeds() { + val config = EventsCompactionConfig(tokenThreshold = 50000, eventRetentionSize = 0) + + assertTrue(config.hasTokenThresholdConfig()) + } + + @Test + fun construct_bothStrategiesConfigured_succeeds() { + val config = + EventsCompactionConfig( + compactionInterval = 3, + overlapSize = 1, + tokenThreshold = 50000, + eventRetentionSize = 5, + ) + + assertTrue(config.hasSlidingWindowConfig()) + assertTrue(config.hasTokenThresholdConfig()) + } + + @Test + fun construct_tokenThresholdZero_throwsIllegalArgumentException() { + assertFailsWith { + EventsCompactionConfig(tokenThreshold = 0, eventRetentionSize = 5) + } + } + + @Test + fun construct_tokenThresholdNegative_throwsIllegalArgumentException() { + assertFailsWith { + EventsCompactionConfig(tokenThreshold = -1, eventRetentionSize = 5) + } + } + + @Test + fun construct_eventRetentionSizeNegative_throwsIllegalArgumentException() { + assertFailsWith { + EventsCompactionConfig(tokenThreshold = 50000, eventRetentionSize = -1) + } + } + + @Test + fun construct_tokenThresholdWithoutEventRetentionSize_throwsIllegalArgumentException() { + assertFailsWith { + EventsCompactionConfig(tokenThreshold = 50000, eventRetentionSize = null) + } + } + + @Test + fun construct_eventRetentionSizeWithoutTokenThreshold_throwsIllegalArgumentException() { + assertFailsWith { + EventsCompactionConfig(tokenThreshold = null, eventRetentionSize = 5) + } + } + private object NoopSummarizer : EventSummarizer { override suspend fun summarizeEvents(events: List): Event? = null }