Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions tokt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# tokt - ADK Java on the ADK Kotlin engine

`tokt` ("to Kotlin") is a one-way interop that lets you run **existing ADK
Java** components - tools, toolsets, plugins, models, and services - on the
**ADK Kotlin** engine, without rewriting them.

- `JavaAdkToKt` adapts individual ADK Java components into their ADK Kotlin
equivalents. Assemble the adapted pieces into a native Kotlin `LlmAgent` /
`App`.
- `KotlinAdkToJava.asJavaRunner` wraps the resulting Kotlin-engine `Runner`
back in the ADK Java `Runner` API, so existing Java call sites stay
unchanged.

## Run existing ADK Java components on the Kotlin engine

```java
import com.google.adk.kt.agents.LlmAgent; // ADK Kotlin-engine agent
import com.google.adk.kt.apps.App;
import com.google.adk.kt.runners.InMemoryRunner;
import com.google.adk.runner.Runner; // ADK Java Runner API
import com.google.adk.tokt.JavaAdkToKt;
import com.google.adk.tokt.KotlinAdkToJava;

// Your existing ADK Java components:
BaseLlm model = new Gemini("gemini-flash-latest", client);
BaseTool weatherTool = FunctionTool.create(WeatherTools.class, "getWeather");
BaseToolset mathToolset = new MathToolset();
BasePlugin loggingPlugin = new LoggingPlugin();

// Adapt them and assemble a native Kotlin-engine agent + app:
LlmAgent agent =
LlmAgent.builder()
.name("assistant")
.model(JavaAdkToKt.asKtModel(model))
.tools(JavaAdkToKt.asKtTools(List.of(weatherTool)))
.toolsets(JavaAdkToKt.asKtToolsets(List.of(mathToolset)))
.build();
App app =
App.builder()
.appName("assistant")
.rootAgent(agent)
.plugins(JavaAdkToKt.asKtPlugins(List.of(loggingPlugin)))
.build();

// Drive the Kotlin-engine runner through the familiar ADK Java Runner API:
Runner runner = KotlinAdkToJava.asJavaRunner(InMemoryRunner.builder().app(app).build());
List<Event> events =
runner
.runAsync(
"user", "session", message, RunConfig.builder().autoCreateSession(true).build())
.toList()
.blockingGet();
```

See the `JavaAdkToKt` KDoc for the full set of adapters (including session,
artifact, and memory services) and the documented interop limits.
5 changes: 5 additions & 0 deletions tokt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.truth</groupId>
<artifactId>truth</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
34 changes: 34 additions & 0 deletions tokt/src/main/kotlin/com/google/adk/tokt/KotlinAdkToJava.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.tokt

import com.google.adk.kt.runners.Runner as KtRunner
import com.google.adk.runner.Runner as JavaRunner

/**
* Reverse interop entry point: exposes an ADK Kotlin-engine [KtRunner] through the ADK Java
* [JavaRunner] surface, so it can be injected into code written against the Java runner. The
* returned runner is a real [JavaRunner] whose `runAsync` streams `Event`s backed by the Kotlin
* engine; live mode is not bridged. The forward direction (Java components onto the Kotlin engine)
* lives in [com.google.adk.tokt.JavaAdkToKt].
*/
object KotlinAdkToJava {

/** Exposes a Kotlin-engine [runner] as an ADK Java [JavaRunner]. */
@JvmStatic
fun asJavaRunner(runner: KtRunner): JavaRunner = KtRunnerToJava(runner)
}
146 changes: 146 additions & 0 deletions tokt/src/main/kotlin/com/google/adk/tokt/KtRunnerToJava.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.tokt

import com.google.adk.agents.LiveRequestQueue
import com.google.adk.agents.RunConfig as JavaRunConfig
import com.google.adk.artifacts.BaseArtifactService as JavaArtifactService
import com.google.adk.artifacts.InMemoryArtifactService as JavaInMemoryArtifactService
import com.google.adk.events.Event as JavaEvent
import com.google.adk.kt.runners.Runner as KtRunner
import com.google.adk.plugins.PluginManager as JavaPluginManager
import com.google.adk.runner.Runner as JavaRunner
import com.google.adk.sessions.Session as JavaSession
import com.google.adk.tokt.adapters.ktAgentAsJava
import com.google.adk.tokt.adapters.ktPluginManagerAsJava
import com.google.adk.tokt.codecs.ContentCodec
import com.google.adk.tokt.codecs.EventCodec
import com.google.adk.tokt.codecs.RunConfigCodec
import com.google.adk.tokt.codecs.stateDeltaFromJava
import com.google.adk.tokt.services.ktArtifactServiceAsJava
import com.google.adk.tokt.services.ktMemoryServiceAsJava
import com.google.adk.tokt.services.ktSessionServiceAsJava
import com.google.genai.types.Content as GenaiContent
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import java.util.Optional
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.rx3.asFlowable

/**
* A [JavaRunner] that delegates to a Kotlin-engine [KtRunner], so a Kotlin runner can be dropped
* into code written against the ADK Java [JavaRunner]: `runAsync` converts the request in and each
* event out, running on the Kotlin engine against its own services and plugins. It honors the Java
* `RunConfig.autoCreateSession` contract (erroring on a missing session unless it is set), but
* per-session request sequencing follows the Kotlin engine, not the Java runner; live mode is not
* bridged, so [runLive] returns a failed stream. [agent], [sessionService], [memoryService],
* [artifactService] and [pluginManager] read the Kotlin runner's own components back through the
* reverse adapters ([memoryService] / [artifactService] return `null` when absent; [pluginManager]
* is read-only and throws on registration).
*/
// Subclassing Runner via its @Deprecated 8-arg super-constructor is intended here.
@Suppress("DEPRECATION")
internal class KtRunnerToJava(private val ktRunner: KtRunner) :
JavaRunner(
// A Java view of the Kotlin runner's agent so agent() reads back; never run (see runAsync).
ktAgentAsJava(ktRunner.agent),
ktRunner.appName,
// Non-null for the Java Runner field; artifactService() returns this bridge when present and
// null when the Kotlin runner has none (the run then uses no artifact service).
ktRunner.artifactService?.let { ktArtifactServiceAsJava(it) } ?: JavaInMemoryArtifactService(),
ktSessionServiceAsJava(ktRunner.sessionService),
ktRunner.memoryService?.let { ktMemoryServiceAsJava(it) },
emptyList(),
null,
null,
) {

override fun runAsync(
userId: String,
sessionId: String,
newMessage: GenaiContent,
runConfig: JavaRunConfig,
stateDelta: MutableMap<String, Any>?,
): Flowable<JavaEvent> {
// Defer so a request-conversion or RunConfig rejection surfaces via onError, not at the call
// site - matching the base Runner and this class's own runLive.
return Flowable.defer {
val events =
ktRunner
.runAsync(
userId = userId,
sessionId = sessionId,
newMessage = ContentCodec.fromJava(newMessage),
// Translate the Java REMOVED sentinel so a caller-passed deletion deletes the key
// rather than storing it as a value (identity-matched by the engine).
stateDelta = stateDelta?.let { stateDeltaFromJava(it) },
runConfig = RunConfigCodec.fromJava(runConfig),
)
.map { EventCodec.toJava(it) }
.asFlowable()
// The Kotlin engine always creates a missing session; the Java Runner errors unless
// autoCreateSession is set, so honor that contract before delegating.
if (runConfig.autoCreateSession()) return@defer events
sessionService()
.getSession(appName(), userId, sessionId, Optional.empty())
.map { true }
.defaultIfEmpty(false)
.flatMapPublisher { exists ->
if (exists) events
else
Flowable.error(
IllegalArgumentException("Session not found and autoCreateSession=false")
)
}
}
}

// Live mode is unsupported; surface it through the stream (like the base Runner) rather than
// throwing eagerly, so callers on the reactive path see it via onError.
override fun runLive(
session: JavaSession,
liveRequestQueue: LiveRequestQueue,
runConfig: JavaRunConfig,
): Flowable<JavaEvent> = Flowable.error(liveUnsupported())

override fun runLive(
userId: String,
sessionId: String,
liveRequestQueue: LiveRequestQueue,
runConfig: JavaRunConfig,
): Flowable<JavaEvent> = Flowable.error(liveUnsupported())

// A read-only Java view of the Kotlin runner's plugins (adapted Java plugins unwrapped); the
// engine's plugins are fixed at construction, so the returned manager throws on registration.
override fun pluginManager(): JavaPluginManager = ktPluginManagerAsJava(ktRunner.pluginManager)

/**
* The bridged artifact service, or `null` when the Kotlin runner has none - unlike a plain
* [JavaRunner], whose accessor is non-null. Mirrors the Kotlin runner's nullable artifactService,
* as [memoryService] does for its own absence.
*/
override fun artifactService(): JavaArtifactService? =
if (ktRunner.artifactService == null) null else super.artifactService()

// ktRunner.close() releases the Kotlin runner's plugins and toolsets; super.close() only reaches
// the Java agent view (which owns none) and the empty plugin manager, but is kept for symmetry.
override fun close(): Completable =
Completable.mergeArrayDelayError(super.close(), Completable.fromAction { ktRunner.close() })

private fun liveUnsupported() =
UnsupportedOperationException("Live mode is not supported when running on the Kotlin engine.")
}
42 changes: 35 additions & 7 deletions tokt/src/main/kotlin/com/google/adk/tokt/codecs/RunConfigCodec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,14 @@ package com.google.adk.tokt.codecs

import com.google.adk.agents.RunConfig as JavaRunConfig
import com.google.adk.kt.agents.RunConfig as KtRunConfig
import com.google.adk.kt.agents.StreamingMode as KtStreamingMode

/**
* Converts the Kotlin `kt.agents.RunConfig` to the ADK Java [JavaRunConfig] a bridged Java
* component reads through its invocation context.
* Converts between the Kotlin `kt.agents.RunConfig` and the ADK Java [JavaRunConfig].
*
* Only the three settings both frameworks model cross: streaming mode, the LLM call budget, and
* custom metadata. The Java-only settings (response modalities, speech and avatar config, audio
* transcription, tool execution mode, save-input-blobs, auto-create-session, and the
* group-function-responses override) keep their Java defaults, as the Kotlin engine has nothing to
* source them from.
* Only three settings cross both ways: streaming mode, the LLM call budget, and custom metadata.
* [toJava] leaves the Java-only settings at their defaults; [fromJava] instead rejects a Java
* setting the engine cannot honor rather than dropping it silently.
*/
internal object RunConfigCodec {

Expand All @@ -42,4 +40,34 @@ internal object RunConfigCodec {
.maxLlmCalls(config.maxLlmCalls)
.customMetadata(config.customMetadata.orEmpty())
.build()

/** 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")
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")
}
require(unsupported.isEmpty()) {
"RunConfig settings not supported by the ADK Kotlin engine: $unsupported"
}
return KtRunConfig(
// Only NONE and SSE reach here; BIDI is rejected above.
streamingMode =
enumByNameOrNull<KtStreamingMode>(config.streamingMode().name) ?: KtStreamingMode.NONE,
maxLlmCalls = config.maxLlmCalls(),
customMetadata = config.customMetadata().takeIf { it.isNotEmpty() },
)
}
}
Loading
Loading