From e4f762c3be550954dea6494e1c0a32fbd4437e74 Mon Sep 17 00:00:00 2001 From: Kamil Tomaszek Date: Tue, 3 Nov 2026 06:38:00 -0800 Subject: [PATCH] refactor(tokt): convert an ADK Java RunConfig for a live run PiperOrigin-RevId: 1012967673 --- .../google/adk/tokt/codecs/RunConfigCodec.kt | 94 ++++++++-- .../google/adk/tokt/KtRunnerInteropTest.kt | 160 ++++++++++++++++++ 2 files changed, 243 insertions(+), 11 deletions(-) diff --git a/tokt/src/main/kotlin/com/google/adk/tokt/codecs/RunConfigCodec.kt b/tokt/src/main/kotlin/com/google/adk/tokt/codecs/RunConfigCodec.kt index 9e7053ba9..d6de2246b 100644 --- a/tokt/src/main/kotlin/com/google/adk/tokt/codecs/RunConfigCodec.kt +++ b/tokt/src/main/kotlin/com/google/adk/tokt/codecs/RunConfigCodec.kt @@ -17,8 +17,11 @@ package com.google.adk.tokt.codecs import com.google.adk.agents.RunConfig as JavaRunConfig +import com.google.adk.kt.agents.LiveRunConfig as KtLiveRunConfig import com.google.adk.kt.agents.RunConfig as KtRunConfig import com.google.adk.kt.agents.StreamingMode as KtStreamingMode +import com.google.adk.kt.annotations.ExperimentalLiveApi +import com.google.adk.kt.types.AudioTranscriptionConfig as KtAudioTranscriptionConfig import com.google.adk.kt.types.Modality as KtModality import com.google.genai.types.Modality as GenaiModality @@ -26,8 +29,9 @@ import com.google.genai.types.Modality as GenaiModality * Converts between the Kotlin `kt.agents.RunConfig` and the ADK Java [JavaRunConfig]. * * Streaming mode, the LLM call budget and custom metadata cross both ways; [toJava] also shows a - * BIDI run's live settings (modalities, speech, transcriptions). [fromJava] rejects a Java setting - * the engine cannot honor rather than dropping it silently. + * BIDI run's live settings (modalities, speech, transcriptions), and [fromJavaLive] reads them for + * a live run. [fromJava] and [fromJavaLive] reject a Java setting the engine cannot honor rather + * than dropping it silently. */ internal object RunConfigCodec { @@ -63,23 +67,15 @@ internal object RunConfigCodec { } /** Returns the Kotlin [KtRunConfig] view of the Java [config]. */ - @Suppress( - "deprecation" - ) // Reads the deprecated groupFunctionResponsesInHistoryOverride to reject it. fun fromJava(config: JavaRunConfig): KtRunConfig { val unsupported = buildList { if (config.streamingMode() == JavaRunConfig.StreamingMode.BIDI) add("streamingMode=BIDI") if (config.saveInputBlobsAsArtifacts()) add("saveInputBlobsAsArtifacts") - if (config.toolExecutionMode() != JavaRunConfig.ToolExecutionMode.NONE) - add("toolExecutionMode") + addUnsupportedByEveryRun(config) if (config.responseModalities().isNotEmpty()) add("responseModalities") if (config.speechConfig() != null) add("speechConfig") - if (config.avatarConfig() != null) add("avatarConfig") if (config.outputAudioTranscription() != null) add("outputAudioTranscription") if (config.inputAudioTranscription() != null) add("inputAudioTranscription") - if (config.groupFunctionResponsesInHistoryOverride().isPresent) - add("groupFunctionResponsesInHistoryOverride") - if (config.dollarEscapedPlaceholders()) add("dollarEscapedPlaceholders") } require(unsupported.isEmpty()) { "RunConfig settings not supported by the ADK Kotlin engine: $unsupported" @@ -92,4 +88,80 @@ internal object RunConfigCodec { customMetadata = config.customMetadata().takeIf { it.isNotEmpty() }, ) } + + /** + * Returns the Kotlin [KtRunConfig] for a live run, applying the ADK Java runner's live defaults: + * no modality means AUDIO, and a missing transcription setting turns transcription on, except + * output transcription when the modalities include text. Rejects a Java setting the engine cannot + * honor, as [fromJava] does, but ignores `saveInputBlobsAsArtifacts`, which ADK Java's own live + * run never reads. + */ + @OptIn(ExperimentalLiveApi::class) + fun fromJavaLive(config: JavaRunConfig): KtRunConfig { + val output = config.outputAudioTranscription() + val input = config.inputAudioTranscription() + val unsupported = buildList { + addUnsupportedByEveryRun(config) + if (output != null) { + addAll(output.unsupportedFields.map { "outputAudioTranscription.$it" }) + } + if (input != null) { + addAll(input.unsupportedFields.map { "inputAudioTranscription.$it" }) + } + } + require(unsupported.isEmpty()) { + "RunConfig settings not supported by the ADK Kotlin engine: $unsupported" + } + val modalities = + config.responseModalities().map { modality -> + // genai reads any name it does not know as MODALITY_UNSPECIFIED. + requireNotNull( + enumByNameOrNull(modality.knownEnum().name)?.takeIf { + it != KtModality.MODALITY_UNSPECIFIED + } + ) { + "Response modality not supported by the ADK Kotlin engine" + } + } + // The ADK Java runner's live defaults (Runner.newInvocationContextForLive). + val responseModalities = modalities.ifEmpty { listOf(KtModality.AUDIO) } + val textRequested = KtModality.TEXT in responseModalities + return KtRunConfig( + // ADK Java's runLive keeps the given mode; BIDI is what shows Java plugins the live settings. + streamingMode = KtStreamingMode.BIDI, + maxLlmCalls = config.maxLlmCalls(), + customMetadata = config.customMetadata().ifEmpty { null }, + live = + KtLiveRunConfig( + responseModalities = responseModalities, + speechConfig = config.speechConfig()?.toKt(), + outputAudioTranscription = + when { + output != null -> output.toKt() + textRequested -> null + else -> KtAudioTranscriptionConfig() + }, + inputAudioTranscription = + if (input != null) { + input.toKt() + } else { + KtAudioTranscriptionConfig() + }, + ), + ) + } + + /** Adds the Java settings that neither a turn-based nor a live run can honor. */ + // Reads the deprecated groupFunctionResponsesInHistoryOverride to reject it. + @Suppress("deprecation") + private fun MutableList.addUnsupportedByEveryRun(config: JavaRunConfig) { + if (config.toolExecutionMode() != JavaRunConfig.ToolExecutionMode.NONE) { + add("toolExecutionMode") + } + if (config.avatarConfig() != null) add("avatarConfig") + if (config.groupFunctionResponsesInHistoryOverride().isPresent) { + add("groupFunctionResponsesInHistoryOverride") + } + if (config.dollarEscapedPlaceholders()) add("dollarEscapedPlaceholders") + } } diff --git a/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt b/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt index 074283018..88106b0f6 100644 --- a/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt +++ b/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt @@ -169,6 +169,7 @@ import com.google.common.truth.Truth.assertThat import com.google.errorprone.annotations.CanIgnoreReturnValue import com.google.genai.types.AudioTranscriptionConfig as GenaiAudioTranscriptionConfig import com.google.genai.types.AudioTranscriptionConfigMode as GenaiAudioTranscriptionConfigMode +import com.google.genai.types.AvatarConfig as GenaiAvatarConfig import com.google.genai.types.CodeExecutionResult as GenaiCodeExecutionResult import com.google.genai.types.Content as GenaiContent import com.google.genai.types.CustomMetadata as GenaiCustomMetadata @@ -2062,6 +2063,145 @@ class KtRunnerInteropTest { } } + @Test + fun runConfigFromJavaLive_appliesTheJavaRunnersLiveDefaults() { + val audio = RunConfigCodec.fromJavaLive(JavaRunConfig.builder().build()) + val text = + RunConfigCodec.fromJavaLive( + JavaRunConfig.builder() + .responseModalities(listOf(GenaiModality(GenaiModality.Known.TEXT))) + .build() + ) + + assertEquals(KtStreamingMode.BIDI, audio.streamingMode) + assertEquals(listOf(KtModality.AUDIO), audio.live.responseModalities, "no modality means AUDIO") + assertEquals(KtAudioTranscriptionConfig(), audio.live.inputAudioTranscription) + assertEquals(KtAudioTranscriptionConfig(), audio.live.outputAudioTranscription) + assertNull(audio.live.speechConfig) + assertNull(audio.customMetadata) + assertEquals(listOf(KtModality.TEXT), text.live.responseModalities) + assertEquals(KtAudioTranscriptionConfig(), text.live.inputAudioTranscription) + assertNull(text.live.outputAudioTranscription, "a text reply turns output transcription off") + } + + @Test + fun runConfigFromJavaLive_speechAndTranscriptionSettings_convert() { + val java = + JavaRunConfig.builder() + .speechConfig(genaiSpeechConfig()) + .outputAudioTranscription(genaiTranscriptionConfig()) + .inputAudioTranscription( + GenaiAudioTranscriptionConfig.builder().languageCodes(listOf("de-DE")).build() + ) + .build() + + val kt = RunConfigCodec.fromJavaLive(java) + + assertEquals(ktSpeechConfig(), kt.live.speechConfig) + assertEquals(ktTranscriptionConfig(), kt.live.outputAudioTranscription) + assertEquals( + KtAudioTranscriptionConfig(languageCodes = listOf("de-DE")), + kt.live.inputAudioTranscription, + ) + } + + @Test + @Suppress("DEPRECATION") // Sets the deprecated grouping override to check that it is rejected. + fun runConfigFromJavaLive_settingsTheEngineCannotHonor_failNamingThem() { + val java = + JavaRunConfig.builder() + .toolExecutionMode(JavaRunConfig.ToolExecutionMode.SEQUENTIAL) + .avatarConfig(GenaiAvatarConfig.builder().avatarName("a").build()) + .groupFunctionResponsesInHistoryOverride(Optional.of(true)) + .dollarEscapedPlaceholders(true) + .outputAudioTranscription( + GenaiAudioTranscriptionConfig.builder() + .languageAuto(GenaiLanguageAuto.builder().build()) + .languageHints(GenaiLanguageHints.builder().languageCodes(listOf("pl-PL")).build()) + .adaptationPhrases(listOf("ADK")) + .mode(GenaiAudioTranscriptionConfigMode("m")) + .build() + ) + .inputAudioTranscription( + GenaiAudioTranscriptionConfig.builder().adaptationPhrases(listOf("ADK")).build() + ) + .build() + + val failure = assertFailsWith { RunConfigCodec.fromJavaLive(java) } + + assertEquals( + "RunConfig settings not supported by the ADK Kotlin engine: [toolExecutionMode, " + + "avatarConfig, groupFunctionResponsesInHistoryOverride, dollarEscapedPlaceholders, " + + "outputAudioTranscription.languageAuto, outputAudioTranscription.languageHints, " + + "outputAudioTranscription.adaptationPhrases, outputAudioTranscription.mode, " + + "inputAudioTranscription.adaptationPhrases]", + failure.message, + ) + } + + @Test + fun runConfigFromJavaLive_ignoresSaveInputBlobsAsArtifacts() { + // ADK Java's own live run never reads it, so a live run it would start must start here too. + val kt = + RunConfigCodec.fromJavaLive(JavaRunConfig.builder().saveInputBlobsAsArtifacts(true).build()) + + assertEquals(KtStreamingMode.BIDI, kt.streamingMode) + } + + @Test + fun runConfigFromJavaLive_unknownModality_fails() { + // genai reads a name it does not know as MODALITY_UNSPECIFIED, which must not reach the model. + val java = JavaRunConfig.builder().responseModalities(listOf(GenaiModality("DOCUMENT"))).build() + + val failure = assertFailsWith { RunConfigCodec.fromJavaLive(java) } + + assertEquals("Response modality not supported by the ADK Kotlin engine", failure.message) + } + + @Test + fun runConfigFromJavaLive_carriesTheCallBudgetAndMetadata() { + val kt = + RunConfigCodec.fromJavaLive( + JavaRunConfig.builder().maxLlmCalls(7).customMetadata(mapOf("k" to "v")).build() + ) + + assertEquals(7, kt.maxLlmCalls) + assertEquals?>(mapOf("k" to "v"), kt.customMetadata) + } + + @Test + fun runConfigFromJavaLive_outputTranscriptionDefault_followsTheJavaRunnersTextRule() { + // Keep an explicit setting with TEXT, and apply a default only when no modality is TEXT. + val textWithOutput = + RunConfigCodec.fromJavaLive( + JavaRunConfig.builder() + .responseModalities(listOf(GenaiModality(GenaiModality.Known.TEXT))) + .outputAudioTranscription(genaiTranscriptionConfig()) + .build() + ) + val audioAndText = + RunConfigCodec.fromJavaLive( + JavaRunConfig.builder() + .responseModalities( + listOf( + GenaiModality(GenaiModality.Known.AUDIO), + GenaiModality(GenaiModality.Known.TEXT), + ) + ) + .build() + ) + val audioOnly = + RunConfigCodec.fromJavaLive( + JavaRunConfig.builder() + .responseModalities(listOf(GenaiModality(GenaiModality.Known.AUDIO))) + .build() + ) + + assertEquals(ktTranscriptionConfig(), textWithOutput.live.outputAudioTranscription) + assertNull(audioAndText.live.outputAudioTranscription) + assertEquals(KtAudioTranscriptionConfig(), audioOnly.live.outputAudioTranscription) + } + @Test fun runConfigToJava_bidiWithLiveSettings_showsLiveSettings() { val kt = @@ -6069,6 +6209,26 @@ class KtRunnerInteropTest { ) } + @Test + @Suppress("DEPRECATION") // Sets the deprecated grouping override to check that it is rejected. + fun runConfigFromJava_settingsNoRunCanHonor_failNamingThem() { + val java = + JavaRunConfig.builder() + .toolExecutionMode(JavaRunConfig.ToolExecutionMode.SEQUENTIAL) + .avatarConfig(GenaiAvatarConfig.builder().avatarName("a").build()) + .groupFunctionResponsesInHistoryOverride(Optional.of(true)) + .dollarEscapedPlaceholders(true) + .build() + + val failure = assertFailsWith { RunConfigCodec.fromJava(java) } + + assertEquals( + "RunConfig settings not supported by the ADK Kotlin engine: [toolExecutionMode, " + + "avatarConfig, groupFunctionResponsesInHistoryOverride, dollarEscapedPlaceholders]", + failure.message, + ) + } + @Test fun runConfigFromJava_unsupportedField_failsRatherThanDroppingIt() { val java = JavaRunConfig.builder().maxLlmCalls(5).saveInputBlobsAsArtifacts(true).build()