Skip to content

Commit 93fdf84

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: add one-way ADK Java to Kotlin engine interop module
Adapts ADK Java tools, toolsets, plugins, services, and models so they can be run on the ADK Kotlin engine. PiperOrigin-RevId: 943801126
1 parent c1bda9c commit 93fdf84

40 files changed

Lines changed: 5951 additions & 0 deletions

core/src/main/java/com/google/adk/sessions/Session.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,27 @@ public Builder events(List<Event> events) {
127127
return this;
128128
}
129129

130+
/**
131+
* Backs {@link Session#events()} with the given list directly, <b>without copying</b>, so the
132+
* session reflects a caller-owned live or converting view (e.g. an engine-interop adapter
133+
* exposing another framework's session, whose events grow as that engine appends).
134+
*
135+
* <p>For framework and interop adapters; ordinary code should use {@link #events(List)}, which
136+
* defensively copies. The caller keeps ownership, so the list also keeps its own semantics: it
137+
* is responsible for thread-safety, a read-only view throws on {@code add}, and {@code
138+
* synchronized (session.events())} no longer excludes the owner's appends.
139+
*
140+
* @deprecated Not deprecated in the ordinary sense - nothing replaces it and it is not going
141+
* away. Marked so that application code is warned off a seam that exists for ADK's own
142+
* framework and interop adapters, which keep using it. Use {@link #events(List)} instead.
143+
*/
144+
@Deprecated
145+
@CanIgnoreReturnValue
146+
public Builder eventsView(List<Event> events) {
147+
this.events = events;
148+
return this;
149+
}
150+
130151
@CanIgnoreReturnValue
131152
public Builder lastUpdateTime(Instant lastUpdateTime) {
132153
this.lastUpdateTime = lastUpdateTime;

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<module>tutorials/city-time-weather</module>
3737
<module>tutorials/live-audio-single-agent</module>
3838
<module>a2a</module>
39+
<module>tokt</module>
3940
</modules>
4041

4142
<properties>
@@ -70,6 +71,10 @@
7071
<truth.version>1.4.5</truth.version>
7172
<jspecify.version>1.0.0</jspecify.version>
7273
<rxjava.version>3.1.12</rxjava.version>
74+
<!-- ADK Kotlin engine interop (google-adk-tokt module). -->
75+
<kotlin.version>2.1.20</kotlin.version>
76+
<kotlinx-coroutines.version>1.11.0</kotlinx-coroutines.version>
77+
<adk-kotlin.version>0.8.0</adk-kotlin.version>
7378
<reactor-core.version>3.7.0</reactor-core.version>
7479
<wiremock.version>2.35.1</wiremock.version>
7580
<assertj.version>3.27.7</assertj.version>

tokt/pom.xml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
Copyright 2026 Google LLC
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
18+
<modelVersion>4.0.0</modelVersion>
19+
20+
<parent>
21+
<groupId>com.google.adk</groupId>
22+
<artifactId>google-adk-parent</artifactId>
23+
<version>1.8.1-SNAPSHOT</version><!-- {x-version-update:google-adk:current} -->
24+
</parent>
25+
26+
<artifactId>google-adk-tokt</artifactId>
27+
<name>Agent Development Kit - Kotlin engine interop</name>
28+
<description>One-way interop that adapts ADK Java tools, toolsets, plugins, services, and models so they can run on the ADK Kotlin engine. The name is short for "to Kotlin", matching the JavaAdkToKt entry point.</description>
29+
30+
<dependencies>
31+
<!-- ADK Java facade types being adapted. -->
32+
<dependency>
33+
<groupId>com.google.adk</groupId>
34+
<artifactId>google-adk</artifactId>
35+
<version>${project.version}</version>
36+
</dependency>
37+
<!-- ADK Kotlin engine the Java types are adapted to. -->
38+
<dependency>
39+
<groupId>com.google.adk</groupId>
40+
<artifactId>google-adk-kotlin-core-jvm</artifactId>
41+
<version>${adk-kotlin.version}</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>com.google.genai</groupId>
45+
<artifactId>google-genai</artifactId>
46+
</dependency>
47+
<dependency>
48+
<groupId>io.reactivex.rxjava3</groupId>
49+
<artifactId>rxjava</artifactId>
50+
</dependency>
51+
<dependency>
52+
<groupId>com.google.guava</groupId>
53+
<artifactId>guava</artifactId>
54+
<version>33.0.0-jre</version>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.jspecify</groupId>
58+
<artifactId>jspecify</artifactId>
59+
</dependency>
60+
<dependency>
61+
<groupId>org.jetbrains.kotlin</groupId>
62+
<artifactId>kotlin-stdlib</artifactId>
63+
<version>${kotlin.version}</version>
64+
</dependency>
65+
<dependency>
66+
<groupId>org.jetbrains.kotlinx</groupId>
67+
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
68+
<version>${kotlinx-coroutines.version}</version>
69+
</dependency>
70+
<dependency>
71+
<groupId>org.jetbrains.kotlinx</groupId>
72+
<artifactId>kotlinx-coroutines-rx3</artifactId>
73+
<version>${kotlinx-coroutines.version}</version>
74+
</dependency>
75+
<dependency>
76+
<groupId>org.jetbrains.kotlinx</groupId>
77+
<artifactId>kotlinx-coroutines-reactive</artifactId>
78+
<version>${kotlinx-coroutines.version}</version>
79+
</dependency>
80+
81+
<!-- Test. -->
82+
<dependency>
83+
<groupId>org.jetbrains.kotlin</groupId>
84+
<artifactId>kotlin-test-junit</artifactId>
85+
<version>${kotlin.version}</version>
86+
<scope>test</scope>
87+
</dependency>
88+
<dependency>
89+
<groupId>junit</groupId>
90+
<artifactId>junit</artifactId>
91+
<version>4.13.2</version>
92+
<scope>test</scope>
93+
</dependency>
94+
</dependencies>
95+
96+
<build>
97+
<plugins>
98+
<!-- Kotlin sources live under src/main/java, so point the Kotlin compiler at those
99+
directories explicitly. -->
100+
<plugin>
101+
<groupId>org.jetbrains.kotlin</groupId>
102+
<artifactId>kotlin-maven-plugin</artifactId>
103+
<version>${kotlin.version}</version>
104+
<configuration>
105+
<jvmTarget>${java.version}</jvmTarget>
106+
<args>
107+
<arg>-opt-in=kotlin.time.ExperimentalTime</arg>
108+
</args>
109+
</configuration>
110+
<executions>
111+
<execution>
112+
<id>compile</id>
113+
<phase>compile</phase>
114+
<goals>
115+
<goal>compile</goal>
116+
</goals>
117+
<configuration>
118+
<sourceDirs>
119+
<sourceDir>${project.basedir}/src/main/java</sourceDir>
120+
</sourceDirs>
121+
</configuration>
122+
</execution>
123+
<execution>
124+
<id>test-compile</id>
125+
<phase>test-compile</phase>
126+
<goals>
127+
<goal>test-compile</goal>
128+
</goals>
129+
<configuration>
130+
<sourceDirs>
131+
<sourceDir>${project.basedir}/src/test/java</sourceDir>
132+
</sourceDirs>
133+
</configuration>
134+
</execution>
135+
</executions>
136+
</plugin>
137+
<plugin>
138+
<artifactId>maven-surefire-plugin</artifactId>
139+
</plugin>
140+
</plugins>
141+
</build>
142+
</project>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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
18+
19+
import kotlinx.coroutines.CoroutineDispatcher
20+
import kotlinx.coroutines.Dispatchers
21+
22+
/**
23+
* The dispatcher every crossing in this module hops to.
24+
*
25+
* ADK Java's SPI is RxJava, which is synchronous unless the implementation says otherwise, so a
26+
* user-authored Java tool, plugin or service may block. Running it on the engine's dispatcher would
27+
* stall the coroutine driving the agent loop, so each adapter moves the call here.
28+
*/
29+
internal val InteropDispatcher: CoroutineDispatcher = Dispatchers.IO
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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
18+
19+
import com.google.adk.artifacts.BaseArtifactService as JavaArtifactService
20+
import com.google.adk.kt.artifacts.ArtifactService as KtArtifactService
21+
import com.google.adk.kt.memory.MemoryService as KtMemoryService
22+
import com.google.adk.kt.models.Model as KtModel
23+
import com.google.adk.kt.plugins.Plugin as KtPlugin
24+
import com.google.adk.kt.sessions.SessionService as KtSessionService
25+
import com.google.adk.kt.tools.BaseTool as KtBaseTool
26+
import com.google.adk.kt.tools.Toolset as KtToolset
27+
import com.google.adk.memory.BaseMemoryService as JavaMemoryService
28+
import com.google.adk.models.BaseLlm as JavaBaseLlm
29+
import com.google.adk.plugins.Plugin as JavaPlugin
30+
import com.google.adk.sessions.BaseSessionService as JavaSessionService
31+
import com.google.adk.tokt.adapters.JavaModelToKt
32+
import com.google.adk.tokt.adapters.JavaPluginToKt
33+
import com.google.adk.tokt.adapters.JavaToolToKt
34+
import com.google.adk.tokt.adapters.JavaToolsetToKt
35+
import com.google.adk.tokt.services.javaArtifactServiceAsKt
36+
import com.google.adk.tokt.services.javaMemoryServiceAsKt
37+
import com.google.adk.tokt.services.javaSessionServiceAsKt
38+
import com.google.adk.tools.BaseTool as JavaBaseTool
39+
import com.google.adk.tools.BaseToolset as JavaBaseToolset
40+
41+
/**
42+
* Forward interop entry point: adapts ADK Java tools, toolsets, plugins, services, and models so
43+
* they can run on the ADK Kotlin engine. Wrap the adapted pieces in a Kotlin `LlmAgent`; this does
44+
* not convert a whole Java agent.
45+
*
46+
* An adapted component behaves as it does on ADK Java. It sees the session as it currently stands,
47+
* including events and state written earlier in the same turn, and its state, artifact and
48+
* control-flow writes reach the engine. Blocking work is fine: calls are dispatched off the thread
49+
* driving the agent.
50+
*
51+
* Two things to know before relying on them:
52+
* - Setting `branch` on a bridged context throws. The branch is the engine's to set.
53+
* - Behind an adapted Java session service ([asKtSessionService]), a resumable workflow restarts
54+
* instead of resuming, because ADK Java has nowhere to store the engine's resumption state. Keep
55+
* the Kotlin session service if you rely on resumability.
56+
*/
57+
object JavaAdkToKt {
58+
59+
/** Adapts an ADK Java tool. */
60+
@JvmStatic fun asKtTool(javaTool: JavaBaseTool): KtBaseTool = JavaToolToKt(javaTool)
61+
62+
/**
63+
* Adapts a whole collection of ADK Java tools (e.g. an `LlmAgent`'s `tools`). Kept alongside
64+
* [asKtTool] for Java callers, who would otherwise write `stream().map(...).toList()`.
65+
*/
66+
@JvmStatic
67+
fun asKtTools(javaTools: List<JavaBaseTool>): List<KtBaseTool> = javaTools.map { asKtTool(it) }
68+
69+
/** Adapts an ADK Java toolset. */
70+
@JvmStatic fun asKtToolset(javaToolset: JavaBaseToolset): KtToolset = JavaToolsetToKt(javaToolset)
71+
72+
/** Adapts a whole collection of ADK Java toolsets. */
73+
@JvmStatic
74+
fun asKtToolsets(javaToolsets: List<JavaBaseToolset>): List<KtToolset> = javaToolsets.map {
75+
asKtToolset(it)
76+
}
77+
78+
/** Adapts an ADK Java plugin. */
79+
@JvmStatic fun asKtPlugin(javaPlugin: JavaPlugin): KtPlugin = JavaPluginToKt(javaPlugin)
80+
81+
/** Adapts a whole collection of ADK Java plugins (e.g. a `Runner`'s `plugins`). */
82+
@JvmStatic
83+
fun asKtPlugins(javaPlugins: List<JavaPlugin>): List<KtPlugin> = javaPlugins.map {
84+
asKtPlugin(it)
85+
}
86+
87+
/** Adapts an ADK Java model so the Kotlin engine can call it. */
88+
@JvmStatic fun asKtModel(javaLlm: JavaBaseLlm): KtModel = JavaModelToKt(javaLlm)
89+
90+
/**
91+
* Adapts an ADK Java session service for the Kotlin engine. Passing a service that is itself an
92+
* adapted Kotlin one returns the original rather than stacking a second adapter. Note the
93+
* `agentState` / `rewindBeforeInvocationId` loss described in [JavaAdkToKt].
94+
*/
95+
@JvmStatic
96+
fun asKtSessionService(service: JavaSessionService): KtSessionService =
97+
javaSessionServiceAsKt(service)
98+
99+
/**
100+
* Adapts an ADK Java artifact service for the Kotlin engine, unwrapping a round-tripped Kotlin
101+
* one rather than stacking adapters. An empty or unmapped artifact part is rejected outright.
102+
*/
103+
@JvmStatic
104+
fun asKtArtifactService(service: JavaArtifactService): KtArtifactService =
105+
javaArtifactServiceAsKt(service)
106+
107+
/**
108+
* Adapts an ADK Java memory service for the Kotlin engine, unwrapping a round-tripped Kotlin one
109+
* rather than stacking adapters.
110+
*/
111+
@JvmStatic
112+
fun asKtMemoryService(service: JavaMemoryService): KtMemoryService =
113+
javaMemoryServiceAsKt(service)
114+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.adapters
18+
19+
import com.google.adk.kt.models.LlmRequest
20+
import com.google.adk.kt.models.LlmResponse
21+
import com.google.adk.kt.models.Model
22+
import com.google.adk.models.BaseLlm as JavaBaseLlm
23+
import com.google.adk.tokt.InteropDispatcher
24+
import com.google.adk.tokt.codecs.LlmRequestCodec
25+
import com.google.adk.tokt.codecs.LlmResponseCodec
26+
import kotlinx.coroutines.flow.Flow
27+
import kotlinx.coroutines.flow.emitAll
28+
import kotlinx.coroutines.flow.flow
29+
import kotlinx.coroutines.flow.flowOn
30+
import kotlinx.coroutines.flow.map
31+
import kotlinx.coroutines.reactive.asFlow
32+
33+
/**
34+
* Java -> Kotlin adapter: presents a user's ADK Java [JavaBaseLlm] as a Kotlin [Model] so the ADK
35+
* Kotlin agent loop can call it. The model seam for routing ADK Java's `LlmAgent` onto the Kotlin
36+
* engine.
37+
*
38+
* The Kotlin `LlmRequest` is converted to a Java `LlmRequest` ([LlmRequestCodec]), the Java model's
39+
* RxJava `Flowable<LlmResponse>` is consumed as a coroutine [Flow] (via
40+
* kotlinx-coroutines-reactive) and each response is converted back ([LlmResponseCodec]).
41+
*/
42+
internal class JavaModelToKt(private val javaLlm: JavaBaseLlm) : Model {
43+
44+
override val name: String = javaLlm.model()
45+
46+
// Deferred into `flow {}` and dispatched on IO: the Java model's request build + generation run
47+
// off the engine dispatcher (RxJava is synchronous by default), and a synchronous throw is routed
48+
// through the Flow's error channel rather than escaping at collection time.
49+
override fun generateContent(request: LlmRequest, stream: Boolean): Flow<LlmResponse> =
50+
flow {
51+
emitAll(
52+
javaLlm.generateContent(LlmRequestCodec.toJava(request), stream).asFlow().map {
53+
LlmResponseCodec.fromJava(it)
54+
}
55+
)
56+
}
57+
.flowOn(InteropDispatcher)
58+
}

0 commit comments

Comments
 (0)