diff --git a/pom.xml b/pom.xml
index 52475d377..5534a4df8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
tutorials/city-time-weather
tutorials/live-audio-single-agent
a2a
+ tokt
@@ -67,6 +68,10 @@
1.4.5
1.0.0
3.1.12
+
+ 2.1.20
+ 1.10.2
+ 0.6.0
3.7.0
2.35.1
3.27.7
diff --git a/tokt/pom.xml b/tokt/pom.xml
new file mode 100644
index 000000000..6e933396f
--- /dev/null
+++ b/tokt/pom.xml
@@ -0,0 +1,142 @@
+
+
+
+ 4.0.0
+
+
+ com.google.adk
+ google-adk-parent
+ 1.7.1-SNAPSHOT
+
+
+ google-adk-tokt
+ Agent Development Kit - Kotlin engine interop
+ One-way interop that adapts ADK Java agents, tools, toolsets, plugins, services, and models so they can run on the ADK Kotlin engine.
+
+
+
+
+ com.google.adk
+ google-adk
+ ${project.version}
+
+
+
+ com.google.adk
+ google-adk-kotlin-core-jvm
+ ${adk-kotlin.version}
+
+
+ com.google.genai
+ google-genai
+
+
+ io.reactivex.rxjava3
+ rxjava
+
+
+ com.google.guava
+ guava
+ 33.0.0-jre
+
+
+ org.jspecify
+ jspecify
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core-jvm
+ ${kotlinx-coroutines.version}
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-rx3
+ ${kotlinx-coroutines.version}
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactive
+ ${kotlinx-coroutines.version}
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit
+ ${kotlin.version}
+ test
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+ ${java.version}
+
+ -opt-in=kotlin.time.ExperimentalTime
+
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ ${project.basedir}/src/main/java
+
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+ ${project.basedir}/src/test/java
+
+
+
+
+
+
+ maven-surefire-plugin
+
+
+
+
diff --git a/tokt/src/main/java/com/google/adk/tokt/JavaAdkToKt.kt b/tokt/src/main/java/com/google/adk/tokt/JavaAdkToKt.kt
new file mode 100644
index 000000000..dd164a257
--- /dev/null
+++ b/tokt/src/main/java/com/google/adk/tokt/JavaAdkToKt.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.artifacts.BaseArtifactService as JavaArtifactService
+import com.google.adk.kt.artifacts.ArtifactService as KtArtifactService
+import com.google.adk.kt.memory.MemoryService as KtMemoryService
+import com.google.adk.kt.models.Model as KtModel
+import com.google.adk.kt.plugins.Plugin as KtPlugin
+import com.google.adk.kt.sessions.SessionService as KtSessionService
+import com.google.adk.kt.tools.BaseTool as KtBaseTool
+import com.google.adk.kt.tools.Toolset as KtToolset
+import com.google.adk.memory.BaseMemoryService as JavaMemoryService
+import com.google.adk.models.BaseLlm as JavaBaseLlm
+import com.google.adk.plugins.Plugin as JavaPlugin
+import com.google.adk.sessions.BaseSessionService as JavaSessionService
+import com.google.adk.tokt.adapters.JavaModelToKt
+import com.google.adk.tokt.adapters.JavaPluginToKt
+import com.google.adk.tokt.adapters.JavaToolToKt
+import com.google.adk.tokt.adapters.JavaToolsetToKt
+import com.google.adk.tokt.services.javaArtifactServiceAsKt
+import com.google.adk.tokt.services.javaMemoryServiceAsKt
+import com.google.adk.tokt.services.javaSessionServiceAsKt
+import com.google.adk.tools.BaseTool as JavaBaseTool
+import com.google.adk.tools.BaseToolset as JavaBaseToolset
+
+/**
+ * Forward interop entry point: adapt ADK Java tools, toolsets, plugins, services, and models so
+ * they can run on the ADK Kotlin engine. Whole-agent conversion is intentionally omitted (running a
+ * Java agent's own multi-step flow on the Kotlin runner is not yet supported); wrap the Java SPI
+ * pieces into a Kotlin `LlmAgent` instead.
+ *
+ * **Session is re-projected, not a live handle.** An adapted Java tool / toolset / plugin sees the
+ * session through `context.session()`, which is re-projected on each call, so a *fresh* read is
+ * always current. The returned `Session` is a snapshot though: unlike native ADK Java its
+ * `events()` list does not grow in place and its state is a copy. Re-read `context.session()` each
+ * time; do not cache a `Session` (or its `events()`) across tool calls or turns. State *writes* via
+ * `toolContext.state()` / `callbackContext.state()` still propagate to the Kotlin.
+ */
+object JavaAdkToKt {
+
+ /**
+ * Adapts an ADK Java tool. Its `ToolContext.session()` is a per-call snapshot; see [JavaAdkToKt].
+ */
+ @JvmStatic fun asKtTool(javaTool: JavaBaseTool): KtBaseTool = JavaToolToKt(javaTool)
+
+ /** Adapts a whole collection of ADK Java tools (e.g. an `LlmAgent`'s `tools`). */
+ @JvmStatic
+ fun asKtTools(javaTools: List): List = javaTools.map { asKtTool(it) }
+
+ /**
+ * Adapts an ADK Java toolset. Its `ReadonlyContext` session is a per-call snapshot; see
+ * [JavaAdkToKt].
+ */
+ @JvmStatic fun asKtToolset(javaToolset: JavaBaseToolset): KtToolset = JavaToolsetToKt(javaToolset)
+
+ /** Adapts a whole collection of ADK Java toolsets. */
+ @JvmStatic
+ fun asKtToolsets(javaToolsets: List): List = javaToolsets.map {
+ asKtToolset(it)
+ }
+
+ /**
+ * Adapts an ADK Java plugin. Its callback contexts expose a per-call session snapshot; see
+ * [JavaAdkToKt].
+ */
+ @JvmStatic fun asKtPlugin(javaPlugin: JavaPlugin): KtPlugin = JavaPluginToKt(javaPlugin)
+
+ /** Adapts a whole collection of ADK Java plugins (e.g. a `Runner`'s `plugins`). */
+ @JvmStatic
+ fun asKtPlugins(javaPlugins: List): List = javaPlugins.map {
+ asKtPlugin(it)
+ }
+
+ @JvmStatic fun asKtModel(javaLlm: JavaBaseLlm): KtModel = JavaModelToKt(javaLlm)
+
+ @JvmStatic
+ fun asKtSessionService(service: JavaSessionService): KtSessionService =
+ javaSessionServiceAsKt(service)
+
+ @JvmStatic
+ fun asKtArtifactService(service: JavaArtifactService): KtArtifactService =
+ javaArtifactServiceAsKt(service)
+
+ @JvmStatic
+ fun asKtMemoryService(service: JavaMemoryService): KtMemoryService =
+ javaMemoryServiceAsKt(service)
+}
diff --git a/tokt/src/main/java/com/google/adk/tokt/adapters/JavaModelToKt.kt b/tokt/src/main/java/com/google/adk/tokt/adapters/JavaModelToKt.kt
new file mode 100644
index 000000000..fb3c3a175
--- /dev/null
+++ b/tokt/src/main/java/com/google/adk/tokt/adapters/JavaModelToKt.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.adapters
+
+import com.google.adk.kt.models.LlmRequest
+import com.google.adk.kt.models.LlmResponse
+import com.google.adk.kt.models.Model
+import com.google.adk.models.BaseLlm as JavaBaseLlm
+import com.google.adk.tokt.codecs.LlmRequestCodec
+import com.google.adk.tokt.codecs.LlmResponseCodec
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.reactive.asFlow
+
+/**
+ * Java -> Kotlin adapter: presents a user's ADK Java [JavaBaseLlm] as the Kotlin's [Model] so the
+ * ADK Kotlin agent loop can call it. The model seam for routing ADK Java's `LlmAgent` onto the
+ * Kotlin.
+ *
+ * The Kotlin `LlmRequest` is converted to a Java `LlmRequest` ([LlmRequestCodec]), the Java model's
+ * RxJava `Flowable` is consumed as a coroutine [Flow] (via
+ * kotlinx-coroutines-reactive) and each response is converted back ([LlmResponseCodec]).
+ */
+internal class JavaModelToKt(private val javaLlm: JavaBaseLlm) : Model {
+
+ override val name: String = javaLlm.model()
+
+ // Deferred into `flow {}` and dispatched on IO: the Java model's request build + generation run
+ // off the engine dispatcher (RxJava is synchronous by default), and a synchronous throw is routed
+ // through the Flow's error channel rather than escaping at collection time.
+ override fun generateContent(request: LlmRequest, stream: Boolean): Flow =
+ flow {
+ emitAll(
+ javaLlm.generateContent(LlmRequestCodec.toJava(request), stream).asFlow().map {
+ LlmResponseCodec.fromJava(it)
+ }
+ )
+ }
+ .flowOn(Dispatchers.IO)
+}
diff --git a/tokt/src/main/java/com/google/adk/tokt/adapters/JavaPluginToKt.kt b/tokt/src/main/java/com/google/adk/tokt/adapters/JavaPluginToKt.kt
new file mode 100644
index 000000000..9bce79be6
--- /dev/null
+++ b/tokt/src/main/java/com/google/adk/tokt/adapters/JavaPluginToKt.kt
@@ -0,0 +1,251 @@
+/*
+ * 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.adapters
+
+import com.google.adk.kt.agents.CallbackContext as KtCallbackContext
+import com.google.adk.kt.agents.InvocationContext as KtInvocationContext
+import com.google.adk.kt.callbacks.CallbackChoice
+import com.google.adk.kt.events.Event as KtEvent
+import com.google.adk.kt.events.EventActions as KtEventActions
+import com.google.adk.kt.models.LlmRequest as KtLlmRequest
+import com.google.adk.kt.models.LlmResponse as KtLlmResponse
+import com.google.adk.kt.plugins.Plugin as KtPlugin
+import com.google.adk.kt.tools.BaseTool as KtBaseTool
+import com.google.adk.kt.tools.ToolContext as KtToolContext
+import com.google.adk.kt.types.Content as KtContent
+import com.google.adk.plugins.Plugin as JavaPlugin
+import com.google.adk.tokt.codecs.ContentCodec
+import com.google.adk.tokt.codecs.EventCodec
+import com.google.adk.tokt.codecs.LlmRequestCodec
+import com.google.adk.tokt.codecs.LlmResponseCodec
+import com.google.adk.tokt.codecs.reconcileRemovedSentinels
+import com.google.adk.tokt.context.ktCallbackContextToJava
+import com.google.adk.tokt.context.ktInvocationContextToJava
+import com.google.adk.tokt.context.ktToolContextToJava
+import io.reactivex.rxjava3.core.Flowable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+import kotlinx.coroutines.withContext
+
+/**
+ * Java -> Kotlin adapter: exposes an ADK Java [JavaPlugin] as a Kotlin [KtPlugin] so a Java app's
+ * plugins run on the Kotlin runner. Each callback converts the Kotlin context to its Java facade
+ * ([ktInvocationContextToJava] / [ktCallbackContextToJava] / [ktToolContextToJava]), invokes the
+ * Java plugin, and awaits its RxJava result as a coroutine.
+ *
+ * The Java agent each callback sees is the running engine agent wrapped via [ktAgentAsJava], so no
+ * root agent has to be supplied. Tool-level callbacks apply only to Kt-backed Java tools
+ * ([JavaToolToKt]); native Kotlin tools pass through unchanged.
+ */
+internal class JavaPluginToKt(private val plugin: JavaPlugin) : KtPlugin {
+
+ override val name: String
+ get() = plugin.name
+
+ /**
+ * Runs a Java plugin callback off the engine dispatcher. Plugin callbacks may do blocking I/O
+ * (logging, metrics, network); RxJava is synchronous by default, so running them on the coroutine
+ * that drives the agent loop could stall it.
+ */
+ private suspend fun onIo(source: () -> Flowable): T? =
+ withContext(Dispatchers.IO) { source().awaitFirstOrNull() }
+
+ /**
+ * Maps any Java removal sentinel a plugin wrote through the live callback state (which is backed
+ * by the Kotlin delta map) to the Kotlin sentinel, so the engine recognizes the deletion.
+ */
+ private fun reconcileState(context: KtCallbackContext) =
+ reconcileRemovedSentinels(context.eventActions.stateDelta)
+
+ /**
+ * As [reconcileState], for the live tool-context actions a tool-callback plugin writes through.
+ */
+ private fun reconcileToolState(context: KtToolContext) =
+ reconcileRemovedSentinels(context.actions.stateDelta)
+
+ // Run-level callbacks.
+
+ override suspend fun onUserMessage(
+ invocationContext: KtInvocationContext,
+ userMessage: KtContent,
+ ): KtContent {
+ val javaContext =
+ ktInvocationContextToJava(invocationContext, ktAgentAsJava(invocationContext.agent))
+ val replacement = onIo {
+ plugin.onUserMessageCallback(javaContext, ContentCodec.toJava(userMessage)).toFlowable()
+ }
+ return replacement?.let { ContentCodec.fromJava(it) } ?: userMessage
+ }
+
+ override suspend fun beforeRun(
+ invocationContext: KtInvocationContext
+ ): CallbackChoice {
+ val javaContext =
+ ktInvocationContextToJava(invocationContext, ktAgentAsJava(invocationContext.agent))
+ val halt = onIo { plugin.beforeRunCallback(javaContext).toFlowable() }
+ return if (halt != null) CallbackChoice.Break(ContentCodec.fromJava(halt))
+ else CallbackChoice.Continue(Unit)
+ }
+
+ override suspend fun onEvent(invocationContext: KtInvocationContext, event: KtEvent): KtEvent {
+ val javaContext =
+ ktInvocationContextToJava(invocationContext, ktAgentAsJava(invocationContext.agent))
+ val replacement = onIo {
+ plugin.onEventCallback(javaContext, EventCodec.toJava(event)).toFlowable()
+ }
+ return replacement?.let { EventCodec.fromJava(it) } ?: event
+ }
+
+ override suspend fun afterRun(invocationContext: KtInvocationContext) {
+ val javaContext =
+ ktInvocationContextToJava(invocationContext, ktAgentAsJava(invocationContext.agent))
+ onIo { plugin.afterRunCallback(javaContext).toFlowable() }
+ }
+
+ // Agent-level callbacks.
+
+ override suspend fun beforeAgent(
+ context: KtCallbackContext
+ ): CallbackChoice {
+ val javaAgent = ktAgentAsJava(context.agent)
+ val override = onIo {
+ plugin
+ .beforeAgentCallback(javaAgent, ktCallbackContextToJava(context, javaAgent))
+ .toFlowable()
+ }
+ reconcileState(context)
+ return if (override != null) CallbackChoice.Break(ContentCodec.fromJava(override))
+ else CallbackChoice.Continue(KtEventActions())
+ }
+
+ override suspend fun afterAgent(context: KtCallbackContext): CallbackChoice {
+ val javaAgent = ktAgentAsJava(context.agent)
+ val override = onIo {
+ plugin.afterAgentCallback(javaAgent, ktCallbackContextToJava(context, javaAgent)).toFlowable()
+ }
+ reconcileState(context)
+ return if (override != null) CallbackChoice.Break(ContentCodec.fromJava(override))
+ else CallbackChoice.Continue(Unit)
+ }
+
+ // Model-level callbacks.
+
+ override suspend fun beforeModel(
+ context: KtCallbackContext,
+ request: KtLlmRequest,
+ ): CallbackChoice {
+ val builder = LlmRequestCodec.toJava(request).toBuilder()
+ val override = onIo {
+ plugin
+ .beforeModelCallback(
+ ktCallbackContextToJava(context, ktAgentAsJava(context.agent)),
+ builder,
+ )
+ .toFlowable()
+ }
+ reconcileState(context)
+ return if (override != null) CallbackChoice.Break(LlmResponseCodec.fromJava(override))
+ // Re-apply onto the original request to preserve Kotlin-only fields (toolsDict, cache config);
+ // LlmRequestCodec.fromJava would build a fresh request and drop them, breaking agent transfer.
+ else CallbackChoice.Continue(reapplyJavaRequest(request, builder.build()))
+ }
+
+ override suspend fun afterModel(
+ context: KtCallbackContext,
+ response: KtLlmResponse,
+ ): KtLlmResponse {
+ val override = onIo {
+ plugin
+ .afterModelCallback(
+ ktCallbackContextToJava(context, ktAgentAsJava(context.agent)),
+ LlmResponseCodec.toJava(response),
+ )
+ .toFlowable()
+ }
+ reconcileState(context)
+ return if (override != null) LlmResponseCodec.fromJava(override) else response
+ }
+
+ override suspend fun onModelError(
+ context: KtCallbackContext,
+ request: KtLlmRequest,
+ error: Throwable,
+ ): CallbackChoice {
+ val builder = LlmRequestCodec.toJava(request).toBuilder()
+ val fallback = onIo {
+ plugin
+ .onModelErrorCallback(
+ ktCallbackContextToJava(context, ktAgentAsJava(context.agent)),
+ builder,
+ error,
+ )
+ .toFlowable()
+ }
+ reconcileState(context)
+ return if (fallback != null) CallbackChoice.Break(LlmResponseCodec.fromJava(fallback))
+ else CallbackChoice.Continue(Unit)
+ }
+
+ // Tool-level callbacks (only Kt-backed Java tools).
+
+ override suspend fun beforeTool(
+ context: KtToolContext,
+ tool: KtBaseTool,
+ args: Map,
+ ): CallbackChoice