Skip to content
Open
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
16 changes: 16 additions & 0 deletions core/src/main/java/com/google/adk/sessions/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ public Builder events(List<Event> events) {
return this;
}

/**
* Backs {@link Session#events()} with the given list directly, <b>without copying</b>, so the
* session reflects a caller-owned live or converting view (e.g. an engine-interop adapter
* exposing another framework's session, whose events grow as that engine appends).
*
* <p>For framework and interop adapters; ordinary code should use {@link #events(List)}, which
* defensively copies. The caller keeps ownership, so the list also keeps its own semantics: it
* is responsible for thread-safety, a read-only view throws on {@code add}, and {@code
* synchronized (session.events())} no longer excludes the owner's appends.
*/
@CanIgnoreReturnValue
public Builder eventsView(List<Event> events) {
this.events = events;
return this;
}

@CanIgnoreReturnValue
public Builder lastUpdateTime(Instant lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<module>tutorials/city-time-weather</module>
<module>tutorials/live-audio-single-agent</module>
<module>a2a</module>
<module>tokt</module>
</modules>

<properties>
Expand Down Expand Up @@ -67,6 +68,10 @@
<truth.version>1.4.5</truth.version>
<jspecify.version>1.0.0</jspecify.version>
<rxjava.version>3.1.12</rxjava.version>
<!-- ADK Kotlin engine interop (google-adk-tokt module). -->
<kotlin.version>2.1.20</kotlin.version>
<kotlinx-coroutines.version>1.11.0</kotlinx-coroutines.version>
<adk-kotlin.version>0.6.0</adk-kotlin.version>
<reactor-core.version>3.7.0</reactor-core.version>
<wiremock.version>2.35.1</wiremock.version>
<assertj.version>3.27.7</assertj.version>
Expand Down
142 changes: 142 additions & 0 deletions tokt/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-parent</artifactId>
<version>1.7.1-SNAPSHOT</version><!-- {x-version-update:google-adk:current} -->
</parent>

<artifactId>google-adk-tokt</artifactId>
<name>Agent Development Kit - Kotlin engine interop</name>
<description>One-way interop that adapts ADK Java tools, toolsets, plugins, services, and models so they can run on the ADK Kotlin engine.</description>

<dependencies>
<!-- ADK Java facade types being adapted. -->
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>${project.version}</version>
</dependency>
<!-- ADK Kotlin engine the Java types are adapted to. -->
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-kotlin-core-jvm</artifactId>
<version>${adk-kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.google.genai</groupId>
<artifactId>google-genai</artifactId>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava3</groupId>
<artifactId>rxjava</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.0.0-jre</version>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-rx3</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactive</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>

<!-- Test. -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<!-- Kotlin sources live under src/main/java (mirroring the Blaze layout), so point the
Kotlin compiler at those directories explicitly. -->
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<jvmTarget>${java.version}</jvmTarget>
<args>
<arg>-opt-in=kotlin.time.ExperimentalTime</arg>
</args>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
134 changes: 134 additions & 0 deletions tokt/src/main/java/com/google/adk/tokt/JavaAdkToKt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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 a live view.** An adapted Java tool / toolset / plugin sees the session through
* `context.session()`, whose `events()` and `state()` read through live to the current Kotlin
* session (converting on access), so reads always reflect events the runner appended and state a
* tool set -- including earlier in the same turn. The `Session` may be held across calls: its
* `events()` (read-only) and `state()` never go stale, though `lastUpdateTime` is captured when the
* context is built. State *writes* via `toolContext.state()` / `callbackContext.state()` still
* propagate to the Kotlin engine through the live actions view. Setting `branch` on a bridged
* context is a no-op rather than an error: the Kotlin branch is immutable.
*
* **What does not cross.** The two type systems do not line up everywhere, and where the Kotlin
* side has no counterpart the value is dropped:
* - A genai `Part` carrying only `executableCode` or `codeExecutionResult` -- what
* `BuiltInCodeExecutionTool` produces -- has no Kotlin equivalent, so the whole part is dropped
* from the content. `PartCodec` logs this once per kind.
* - Kotlin `EventActions.agentState` and `rewindBeforeInvocationId` have no Java equivalent, so
* they are lost when events round-trip through an adapted Java session service
* ([asKtSessionService]). In-process this is masked, because the engine also keeps its own live
* session; it bites when a *persistent* Java service is read back in a new process, where a
* resumable workflow then restarts instead of resuming and a rewind marker is forgotten. Keep the
* Kotlin session service if you need either.
* - Smaller, same reason: `Event.cacheMetadata`, `LlmResponse.cacheMetadata`, and `MemoryEntry`'s
* `id` / `customMetadata`.
*/
object JavaAdkToKt {

/** Adapts an ADK Java tool. Its `ToolContext.session()` is a live view; see [JavaAdkToKt]. */
@JvmStatic fun asKtTool(javaTool: JavaBaseTool): KtBaseTool = JavaToolToKt(javaTool)

/**
* Adapts a whole collection of ADK Java tools (e.g. an `LlmAgent`'s `tools`). Kept alongside
* [asKtTool] for Java callers, who would otherwise write `stream().map(...).toList()`.
*/
@JvmStatic
fun asKtTools(javaTools: List<JavaBaseTool>): List<KtBaseTool> = javaTools.map { asKtTool(it) }

/**
* Adapts an ADK Java toolset. Its `ReadonlyContext` session is a live view; see [JavaAdkToKt].
*/
@JvmStatic fun asKtToolset(javaToolset: JavaBaseToolset): KtToolset = JavaToolsetToKt(javaToolset)

/** Adapts a whole collection of ADK Java toolsets. */
@JvmStatic
fun asKtToolsets(javaToolsets: List<JavaBaseToolset>): List<KtToolset> = javaToolsets.map {
asKtToolset(it)
}

/**
* Adapts an ADK Java plugin. Its callback contexts expose a live session view; 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<JavaPlugin>): List<KtPlugin> = javaPlugins.map {
asKtPlugin(it)
}

/**
* Adapts an ADK Java model so the Kotlin engine can call it. Callbacks it triggers see the live
* session view; see [JavaAdkToKt].
*/
@JvmStatic fun asKtModel(javaLlm: JavaBaseLlm): KtModel = JavaModelToKt(javaLlm)

/**
* Adapts an ADK Java session service for the Kotlin engine. Passing a service that is itself an
* adapted Kotlin one returns the original rather than stacking a second adapter. Note the
* `agentState` / `rewindBeforeInvocationId` loss described in [JavaAdkToKt].
*/
@JvmStatic
fun asKtSessionService(service: JavaSessionService): KtSessionService =
javaSessionServiceAsKt(service)

/**
* Adapts an ADK Java artifact service for the Kotlin engine, unwrapping a round-tripped Kotlin
* one rather than stacking adapters. An empty or unmapped artifact part is rejected outright.
*/
@JvmStatic
fun asKtArtifactService(service: JavaArtifactService): KtArtifactService =
javaArtifactServiceAsKt(service)

/**
* Adapts an ADK Java memory service for the Kotlin engine, unwrapping a round-tripped Kotlin one
* rather than stacking adapters.
*/
@JvmStatic
fun asKtMemoryService(service: JavaMemoryService): KtMemoryService =
javaMemoryServiceAsKt(service)
}
58 changes: 58 additions & 0 deletions tokt/src/main/java/com/google/adk/tokt/adapters/JavaModelToKt.kt
Original file line number Diff line number Diff line change
@@ -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 a Kotlin [Model] so the ADK
* Kotlin agent loop can call it. The model seam for routing ADK Java's `LlmAgent` onto the Kotlin
* engine.
*
* The Kotlin `LlmRequest` is converted to a Java `LlmRequest` ([LlmRequestCodec]), the Java model's
* RxJava `Flowable<LlmResponse>` 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<LlmResponse> =
flow {
emitAll(
javaLlm.generateContent(LlmRequestCodec.toJava(request), stream).asFlow().map {
LlmResponseCodec.fromJava(it)
}
)
}
.flowOn(Dispatchers.IO)
}
Loading
Loading