Skip to content

Commit 2c84291

Browse files
damianmomotgooglecopybara-github
authored andcommitted
fix: keep context cache metadata in events and model responses
PiperOrigin-RevId: 1007347132
1 parent beca776 commit 2c84291

4 files changed

Lines changed: 135 additions & 5 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.tokt.codecs
18+
19+
import com.google.adk.kt.models.CacheMetadata as KtCacheMetadata
20+
import com.google.adk.models.CacheMetadata as JavaCacheMetadata
21+
import java.time.Instant
22+
import kotlin.jvm.optionals.getOrNull
23+
24+
/**
25+
* Converts context cache metadata between ADK Kotlin and ADK Java. Cache times are epoch
26+
* milliseconds on the Kotlin side and [Instant]s on the Java side. Shared by [EventCodec] and
27+
* [LlmResponseCodec].
28+
*/
29+
internal object CacheMetadataCodec {
30+
31+
/** Returns the Java [JavaCacheMetadata] view of the Kotlin [metadata]. */
32+
fun toJava(metadata: KtCacheMetadata): JavaCacheMetadata =
33+
JavaCacheMetadata.builder()
34+
.fingerprint(metadata.fingerprint)
35+
.contentsCount(metadata.contentsCount)
36+
.cacheName(metadata.cacheName)
37+
.expireTime(metadata.expireTime?.let { Instant.ofEpochMilli(it) })
38+
.invocationsUsed(metadata.invocationsUsed)
39+
.createdAt(metadata.createdAt?.let { Instant.ofEpochMilli(it) })
40+
.build()
41+
42+
/** Returns the Kotlin [KtCacheMetadata] view of the Java [metadata]. */
43+
fun fromJava(metadata: JavaCacheMetadata): KtCacheMetadata =
44+
KtCacheMetadata(
45+
fingerprint = metadata.fingerprint(),
46+
contentsCount = metadata.contentsCount(),
47+
cacheName = metadata.cacheName().getOrNull(),
48+
expireTime = metadata.expireTime().getOrNull()?.toEpochMilli(),
49+
invocationsUsed = metadata.invocationsUsed().getOrNull(),
50+
createdAt = metadata.createdAt().getOrNull()?.toEpochMilli(),
51+
)
52+
}

‎tokt/src/main/kotlin/com/google/adk/tokt/codecs/EventCodec.kt‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ import kotlin.jvm.optionals.getOrNull
3333
* skipSummarization, endOfAgent - the requested tool confirmations, the context-compaction summary,
3434
* the resumable-run `agentState`, the rewind marker `rewindBeforeInvocationId`, and the workflow
3535
* `route`), the streaming/error signals (`partial`, `turnComplete`, `interrupted`, `errorMessage`,
36-
* `errorCode`, `finishReason`), the token `usageMetadata` ([UsageMetadataCodec]) and
37-
* `groundingMetadata` ([GroundingMetadataCodec]), a workflow node's `output` and `nodeInfo`
38-
* ([WorkflowCodec]), long-running tool ids, and the timestamp.
36+
* `errorCode`, `finishReason`), the token `usageMetadata` ([UsageMetadataCodec]),
37+
* `groundingMetadata` ([GroundingMetadataCodec]) and context `cacheMetadata`
38+
* ([CacheMetadataCodec]), a workflow node's `output` and `nodeInfo` ([WorkflowCodec]), long-running
39+
* tool ids, and the timestamp.
3940
*/
4041
internal object EventCodec {
4142

@@ -61,6 +62,7 @@ internal object EventCodec {
6162
event.usageMetadata?.let { builder.usageMetadata(UsageMetadataCodec.toJava(it)) }
6263
event.avgLogProbs?.let { builder.avgLogprobs(it) }
6364
event.groundingMetadata?.let { builder.groundingMetadata(GroundingMetadataCodec.toJava(it)) }
65+
event.cacheMetadata?.let { builder.cacheMetadata(CacheMetadataCodec.toJava(it)) }
6466
event.modelVersion?.let { builder.modelVersion(it) }
6567
event.customMetadata?.let { builder.customMetadata(CustomMetadataCodec.toJava(it)) }
6668
event.output?.let { builder.output(WorkflowCodec.outputToJava(it)) }
@@ -111,6 +113,7 @@ internal object EventCodec {
111113
avgLogProbs = event.avgLogprobs().getOrNull(),
112114
groundingMetadata =
113115
event.groundingMetadata().getOrNull()?.let { GroundingMetadataCodec.fromJava(it) },
116+
cacheMetadata = event.cacheMetadata().getOrNull()?.let { CacheMetadataCodec.fromJava(it) },
114117
modelVersion = event.modelVersion().getOrNull(),
115118
customMetadata = event.customMetadata().getOrNull()?.let { CustomMetadataCodec.fromJava(it) },
116119
output = event.output().getOrNull()?.let { WorkflowCodec.outputFromJava(it) },

‎tokt/src/main/kotlin/com/google/adk/tokt/codecs/LlmResponseCodec.kt‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import kotlin.jvm.optionals.getOrNull
2828
* a bridged Java plugin's `afterModel`.
2929
*
3030
* Carries content, error message, streaming flags, model version, finish reason, usage metadata,
31-
* grounding metadata, average log-probs, error code, and custom metadata. Citation metadata and
32-
* logprobs results have no ADK Java facade field, so they are not mapped.
31+
* grounding and context cache metadata, average log-probs, error code, and custom metadata.
32+
* Citation metadata and logprobs results have no ADK Java facade field, so they are not mapped.
3333
*/
3434
internal object LlmResponseCodec {
3535

@@ -45,6 +45,7 @@ internal object LlmResponseCodec {
4545
usageMetadata = response.usageMetadata().getOrNull()?.let { UsageMetadataCodec.fromJava(it) },
4646
groundingMetadata =
4747
response.groundingMetadata().getOrNull()?.let { GroundingMetadataCodec.fromJava(it) },
48+
cacheMetadata = response.cacheMetadata().getOrNull()?.let { CacheMetadataCodec.fromJava(it) },
4849
avgLogprobs = response.avgLogprobs().getOrNull(),
4950
// toString() preserves the raw value; knownEnum().name would collapse an unrecognized code to
5051
// FINISH_REASON_UNSPECIFIED (errorCode is a free-form String on the Kotlin side).
@@ -63,6 +64,7 @@ internal object LlmResponseCodec {
6364
response.finishReason?.let { builder.finishReason(it.toGenai()) }
6465
response.usageMetadata?.let { builder.usageMetadata(UsageMetadataCodec.toJava(it)) }
6566
response.groundingMetadata?.let { builder.groundingMetadata(GroundingMetadataCodec.toJava(it)) }
67+
response.cacheMetadata?.let { builder.cacheMetadata(CacheMetadataCodec.toJava(it)) }
6668
response.avgLogprobs?.let { builder.avgLogprobs(it) }
6769
response.errorCode?.let { builder.errorCode(GenaiFinishReason(it)) }
6870
response.customMetadata?.let { builder.customMetadata(CustomMetadataCodec.toJava(it)) }

‎tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.google.adk.kt.apps.App as KtApp
4040
import com.google.adk.kt.callbacks.AfterModelCallback as KtAfterModelCallback
4141
import com.google.adk.kt.events.Event as KtEvent
4242
import com.google.adk.kt.events.EventActions as KtEventActions
43+
import com.google.adk.kt.models.CacheMetadata as KtCacheMetadata
4344
import com.google.adk.kt.models.LlmResponse as KtLlmResponse
4445
import com.google.adk.kt.runners.InMemoryRunner as KtInMemoryRunner
4546
import com.google.adk.kt.runners.Runner as KtRunner
@@ -95,6 +96,7 @@ import com.google.adk.kt.workflow.Workflow as KtWorkflow
9596
import com.google.adk.memory.InMemoryMemoryService as JavaInMemoryMemoryService
9697
import com.google.adk.models.BaseLlm as JavaBaseLlm
9798
import com.google.adk.models.BaseLlmConnection as JavaBaseLlmConnection
99+
import com.google.adk.models.CacheMetadata as JavaCacheMetadata
98100
import com.google.adk.models.LlmRequest as JavaLlmRequest
99101
import com.google.adk.models.LlmResponse as JavaLlmResponse
100102
import com.google.adk.plugins.BasePlugin as JavaBasePlugin
@@ -3293,6 +3295,52 @@ class KtRunnerInteropTest {
32933295
)
32943296
}
32953297

3298+
@Test
3299+
fun ktRunner_bridgedPluginReturningTheResponse_keepsItsCacheMetadata() = runBlocking {
3300+
// The event keeps the response's cache metadata; the next turn finds its cache there.
3301+
val model =
3302+
object : JavaBaseLlm("java-model") {
3303+
override fun generateContent(
3304+
llmRequest: JavaLlmRequest,
3305+
stream: Boolean,
3306+
): Flowable<JavaLlmResponse> =
3307+
Flowable.just(
3308+
JavaLlmResponse.builder()
3309+
.content(modelText("cached"))
3310+
.cacheMetadata(
3311+
JavaCacheMetadata.builder().fingerprint("abc").contentsCount(2).build()
3312+
)
3313+
.build()
3314+
)
3315+
3316+
override fun connect(llmRequest: JavaLlmRequest): JavaBaseLlmConnection =
3317+
throw UnsupportedOperationException()
3318+
}
3319+
// Returning the response sends it through LlmResponseCodec to Java and back.
3320+
val passthrough =
3321+
object : JavaBasePlugin("passthrough") {
3322+
override fun afterModelCallback(
3323+
callbackContext: JavaCallbackContext,
3324+
llmResponse: JavaLlmResponse,
3325+
): Maybe<JavaLlmResponse> = Maybe.just(llmResponse)
3326+
}
3327+
val runner =
3328+
KtInMemoryRunner(
3329+
KtApp(
3330+
appName = "app",
3331+
rootAgent = KtLlmAgent(name = "a", model = JavaAdkToKt.asKtModel(model)),
3332+
plugins = listOf(JavaAdkToKt.asKtPlugin(passthrough)),
3333+
)
3334+
)
3335+
3336+
val events = runner.turn()
3337+
3338+
assertEquals(
3339+
KtCacheMetadata(fingerprint = "abc", contentsCount = 2),
3340+
events.single { it.author == "a" }.cacheMetadata,
3341+
)
3342+
}
3343+
32963344
@Test
32973345
fun ktRunner_bridgedNoOpPlugin_preservesAgentTransfer() = runBlocking {
32983346
// Regression: bridging any Java plugin must not drop the request's Kotlin-only toolsDict in
@@ -4310,6 +4358,31 @@ class KtRunnerInteropTest {
43104358
)
43114359
}
43124360

4361+
@Test
4362+
fun asKtSessionService_cacheMetadata_survivesTheJavaStore() = runBlocking {
4363+
// The next turn finds the context cache to reuse in the stored events.
4364+
val sessions = JavaAdkToKt.asKtSessionService(JavaInMemorySessionService())
4365+
val key = KtSessionKey("app", "u", "s")
4366+
val session = sessions.createSession(key)
4367+
val fingerprintOnly = KtCacheMetadata(fingerprint = "abc", contentsCount = 2)
4368+
val active =
4369+
KtCacheMetadata(
4370+
fingerprint = "abc",
4371+
contentsCount = 2,
4372+
cacheName = "cachedContents/1",
4373+
expireTime = 2_000_000_000_000L,
4374+
invocationsUsed = 3,
4375+
createdAt = 1_999_999_000_000L,
4376+
)
4377+
4378+
for (cache in listOf(fingerprintOnly, active)) {
4379+
val unused = sessions.appendEvent(session, KtEvent(author = "model", cacheMetadata = cache))
4380+
}
4381+
4382+
val stored = assertNotNull(sessions.getSession(key)).events.map { it.cacheMetadata }
4383+
assertEquals(listOf(fingerprintOnly, active), stored)
4384+
}
4385+
43134386
@Test
43144387
fun eventCodec_workflowFields_roundTripThroughTheJavaEvent() {
43154388
// Java code reads these fields too, so a Kotlin Content in the output crosses as a genai one.

0 commit comments

Comments
 (0)