Skip to content

Commit e4b4f44

Browse files
damianmomotgooglecopybara-github
authored andcommitted
fix: keep context cache metadata in events and model responses
PiperOrigin-RevId: 1007188274
1 parent dcbdf96 commit e4b4f44

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
@@ -94,6 +95,7 @@ import com.google.adk.kt.workflow.Workflow as KtWorkflow
9495
import com.google.adk.memory.InMemoryMemoryService as JavaInMemoryMemoryService
9596
import com.google.adk.models.BaseLlm as JavaBaseLlm
9697
import com.google.adk.models.BaseLlmConnection as JavaBaseLlmConnection
98+
import com.google.adk.models.CacheMetadata as JavaCacheMetadata
9799
import com.google.adk.models.LlmRequest as JavaLlmRequest
98100
import com.google.adk.models.LlmResponse as JavaLlmResponse
99101
import com.google.adk.plugins.BasePlugin as JavaBasePlugin
@@ -3292,6 +3294,52 @@ class KtRunnerInteropTest {
32923294
)
32933295
}
32943296

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

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

0 commit comments

Comments
 (0)