Skip to content

Commit aab51ef

Browse files
damianmomotgooglecopybara-github
authored andcommitted
fix: resume Kotlin workflows behind an adapted Java session service
- Gives ADK Java callers each workflow node's output in Java types. - Propagates workflow routes set by adapted ADK Java tools and plugins. - Updates `google-adk-tokt` to ADK Kotlin 1.4.0. - Updates the Kotlin compiler and `kotlin-stdlib` in `google-adk-tokt` to 2.3.21. PiperOrigin-RevId: 1003945610
1 parent 5f49fea commit aab51ef

7 files changed

Lines changed: 550 additions & 18 deletions

File tree

‎pom.xml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@
7272
<jspecify.version>1.0.0</jspecify.version>
7373
<rxjava.version>3.1.12</rxjava.version>
7474
<!-- ADK Kotlin engine interop (google-adk-tokt module). -->
75-
<kotlin.version>2.1.20</kotlin.version>
75+
<kotlin.version>2.3.21</kotlin.version>
7676
<dokka.version>2.2.0</dokka.version>
7777
<kotlinx-coroutines.version>1.11.0</kotlinx-coroutines.version>
78-
<adk-kotlin.version>1.3.0</adk-kotlin.version>
78+
<adk-kotlin.version>1.4.0</adk-kotlin.version>
7979
<reactor-core.version>3.7.0</reactor-core.version>
8080
<wiremock.version>2.35.1</wiremock.version>
8181
<assertj.version>3.27.7</assertj.version>

‎tokt/src/main/kotlin/com/google/adk/tokt/JavaAdkToKt.kt‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ import kotlinx.coroutines.CoroutineDispatcher
6565
* `EventActions`, the adapter throws once the Java call returns - the engine's event actions have
6666
* no equivalent.
6767
*
68-
* Writes to the context's `skipSummarization`, `requestedToolConfirmations`, `agentState`, and
69-
* `rewindBeforeInvocationId` do cross to the engine.
68+
* Writes to the context's `skipSummarization`, `requestedToolConfirmations`, `agentState`,
69+
* `rewindBeforeInvocationId`, and `route` do cross to the engine.
7070
*/
7171
object JavaAdkToKt {
7272

@@ -139,9 +139,9 @@ object JavaAdkToKt {
139139
/**
140140
* Adapts an ADK Java session service for the Kotlin engine, running its calls on `dispatcher`. A
141141
* Java view of a Kotlin service is unwrapped to that Kotlin service, with no `dispatcher` hop.
142-
* Resumption works across the adapter (`EventActions.agentState` crosses);
143-
* `rewindBeforeInvocationId` crosses too, but a rewind drops the rewound turns only if the Java
144-
* service stores that field.
142+
* Resumption and rewinds work across the adapter if the Java service, like
143+
* `InMemorySessionService`, returns the fields they read unchanged: `EventActions.agentState`,
144+
* `rewindBeforeInvocationId`, and a workflow node's output, node info, and routes.
145145
*/
146146
@JvmStatic
147147
@JvmOverloads

‎tokt/src/main/kotlin/com/google/adk/tokt/adapters/ActionReconciler.kt‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ import com.google.adk.events.EventActions as JavaEventActions
2020
import com.google.adk.kt.events.EventActions as KtEventActions
2121
import com.google.adk.tokt.codecs.EventCompactionCodec
2222
import com.google.adk.tokt.codecs.ToolConfirmationCodec
23+
import com.google.adk.tokt.codecs.WorkflowCodec
2324
import com.google.adk.tokt.codecs.agentStateFromJava
2425
import com.google.adk.tokt.codecs.reconcileRemovedSentinels
2526

2627
/**
2728
* Copies what a bridged tool or plugin callback wrote on its [JavaEventActions] onto the live
28-
* [KtEventActions] the engine reads. Fields the live view already writes through are re-applied
29-
* idempotently; skip-summarization, tool confirmations, agentState, compaction and
30-
* rewindBeforeInvocationId are carried here. A signal the engine cannot represent
31-
* (requestedAuthConfigs, deletedArtifactIds) throws.
29+
* [KtEventActions] the engine reads. Fields the live view writes through, such as the workflow
30+
* route, are re-applied idempotently, which also carries them after a wholesale `setActions(...)`;
31+
* skip-summarization, tool confirmations, and agentState are carried only here. A signal the engine
32+
* cannot represent (requestedAuthConfigs, deletedArtifactIds) throws.
3233
*/
3334
internal fun reconcileActionsToKt(java: JavaEventActions, kt: KtEventActions) {
3435
require(java.requestedAuthConfigs().isNullOrEmpty()) {
@@ -43,6 +44,7 @@ internal fun reconcileActionsToKt(java: JavaEventActions, kt: KtEventActions) {
4344
java.agentState().ifPresent { kt.agentState = agentStateFromJava(it) }
4445
java.compaction().ifPresent { kt.compaction = EventCompactionCodec.fromJava(it) }
4546
java.rewindBeforeInvocationId().ifPresent { kt.rewindBeforeInvocationId = it }
47+
java.route().ifPresent { routes -> kt.route = routes.map { WorkflowCodec.routeFromJava(it) } }
4648
kt.endOfAgent = kt.endOfAgent || java.endOfAgent()
4749
// Merge deltas only when the tool replaced the actions (else they are the same live Kotlin map).
4850
if (java.stateDelta() !== kt.stateDelta) kt.stateDelta.putAll(java.stateDelta())

‎tokt/src/main/kotlin/com/google/adk/tokt/codecs/EventCodec.kt‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ import kotlin.jvm.optionals.getOrNull
3131
* Carries the identity (`id`, `invocationId`, `author`, `branch`), the [content][ContentCodec], the
3232
* [actions][KtEventActions] (state/artifact deltas, the control-flow flags - transfer, escalate,
3333
* skipSummarization, endOfAgent - the requested tool confirmations, the context-compaction summary,
34-
* the resumable-run `agentState`, and the rewind marker `rewindBeforeInvocationId`), the
35-
* streaming/error signals (`partial`, `turnComplete`, `interrupted`, `errorMessage`, `errorCode`,
36-
* `finishReason`), the token `usageMetadata` ([UsageMetadataCodec]) and `groundingMetadata`
37-
* ([GroundingMetadataCodec]), long-running tool ids, and the timestamp.
34+
* the resumable-run `agentState`, the rewind marker `rewindBeforeInvocationId`, and the workflow
35+
* `route`), the streaming/error signals (`partial`, `turnComplete`, `interrupted`, `errorMessage`,
36+
* `errorCode`, `finishReason`), the token `usageMetadata` ([UsageMetadataCodec]) and
37+
* `groundingMetadata` ([GroundingMetadataCodec]), a workflow node's `output` and `nodeInfo`
38+
* ([WorkflowCodec]), long-running tool ids, and the timestamp.
3839
*/
3940
internal object EventCodec {
4041

@@ -62,6 +63,8 @@ internal object EventCodec {
6263
event.groundingMetadata?.let { builder.groundingMetadata(GroundingMetadataCodec.toJava(it)) }
6364
event.modelVersion?.let { builder.modelVersion(it) }
6465
event.customMetadata?.let { builder.customMetadata(CustomMetadataCodec.toJava(it)) }
66+
event.output?.let { builder.output(WorkflowCodec.outputToJava(it)) }
67+
event.nodeInfo?.let { builder.nodeInfo(WorkflowCodec.nodeInfoToJava(it)) }
6568
builder.timestamp(event.timestamp)
6669
return builder.build()
6770
}
@@ -93,6 +96,7 @@ internal object EventCodec {
9396
javaActions.compaction().getOrNull()?.let { EventCompactionCodec.fromJava(it) },
9497
agentState = agentStateFromJava(javaActions.agentState().getOrNull()),
9598
rewindBeforeInvocationId = javaActions.rewindBeforeInvocationId().getOrNull(),
99+
route = javaActions.route().getOrNull()?.map { WorkflowCodec.routeFromJava(it) },
96100
),
97101
branch = event.branch().getOrNull(),
98102
partial = event.partial().getOrNull() ?: false,
@@ -109,6 +113,8 @@ internal object EventCodec {
109113
event.groundingMetadata().getOrNull()?.let { GroundingMetadataCodec.fromJava(it) },
110114
modelVersion = event.modelVersion().getOrNull(),
111115
customMetadata = event.customMetadata().getOrNull()?.let { CustomMetadataCodec.fromJava(it) },
116+
output = event.output().getOrNull()?.let { WorkflowCodec.outputFromJava(it) },
117+
nodeInfo = event.nodeInfo().getOrNull()?.let { WorkflowCodec.nodeInfoFromJava(it) },
112118
timestamp = event.timestamp(),
113119
)
114120
}

‎tokt/src/main/kotlin/com/google/adk/tokt/codecs/KtToJavaCodecs.kt‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import com.google.adk.kt.sessions.SessionKey as KtSessionKey
2525
import com.google.adk.kt.sessions.State as KtState
2626
import com.google.adk.sessions.Session as JavaSession
2727
import com.google.adk.sessions.State as JavaState
28+
import com.google.adk.workflow.Route as JavaRoute
29+
import com.google.common.collect.ImmutableList
2830
import java.util.AbstractList
2931
import java.util.AbstractMap
3032
import java.util.Optional
@@ -81,6 +83,17 @@ internal class KtEventActionsToJavaView(private val actions: KtEventActions) : J
8183
actions.rewindBeforeInvocationId = rewindBeforeInvocationId
8284
}
8385

86+
override fun route(): Optional<ImmutableList<JavaRoute>> =
87+
Optional.ofNullable(
88+
actions.route?.let { routes ->
89+
ImmutableList.copyOf(routes.map { WorkflowCodec.routeToJava(it) })
90+
}
91+
)
92+
93+
override fun setRoute(route: List<JavaRoute>?) {
94+
actions.route = route?.map { WorkflowCodec.routeFromJava(it) }
95+
}
96+
8497
// Overridden so the write reaches the Kotlin side; the base setters write private fields the
8598
// overridden getters never read.
8699

@@ -113,9 +126,9 @@ internal class KtEventActionsToJavaView(private val actions: KtEventActions) : J
113126
* Builds a read-only Java [JavaEventActions] snapshot from a Kotlin [KtEventActions], carrying the
114127
* deltas, the control-flow signals (skip-summarization, transfer, escalate, end-of-agent), the
115128
* requested tool confirmations, the compaction summary, the resumable-run `agentState`
116-
* ([agentStateToJava]), and the rewind marker (`rewindBeforeInvocationId`). Used when exposing an
117-
* existing Kotlin event to a Java runner ([EventCodec.toJava]); unlike [KtEventActionsToJavaView]
118-
* (a live write sink), nothing writes back through it.
129+
* ([agentStateToJava]), the rewind marker (`rewindBeforeInvocationId`), and the workflow `route`.
130+
* Used when exposing an existing Kotlin event to a Java runner ([EventCodec.toJava]); unlike
131+
* [KtEventActionsToJavaView] (a live write sink), nothing writes back through it.
119132
*/
120133
internal fun eventActionsToJava(actions: KtEventActions): JavaEventActions =
121134
JavaEventActions.builder()
@@ -131,6 +144,7 @@ internal fun eventActionsToJava(actions: KtEventActions): JavaEventActions =
131144
.compaction(actions.compaction?.let { EventCompactionCodec.toJava(it) })
132145
.agentState(agentStateToJava(actions.agentState))
133146
.rewindBeforeInvocationId(actions.rewindBeforeInvocationId)
147+
.route(actions.route?.map { WorkflowCodec.routeToJava(it) })
134148
.build()
135149

136150
/** Copies a Kotlin state delta to Java, mapping the Kotlin [KtState.REMOVED] deletion sentinel. */
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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.codecs
18+
19+
import com.google.adk.JsonBaseModel
20+
import com.google.adk.kt.annotations.FrameworkInternalApi
21+
import com.google.adk.kt.serialization.anyToJsonElement
22+
import com.google.adk.kt.serialization.jsonElementToAny
23+
import com.google.adk.kt.types.Content as KtContent
24+
import com.google.adk.kt.workflow.NodeInfo as KtNodeInfo
25+
import com.google.adk.kt.workflow.Route as KtRoute
26+
import com.google.adk.workflow.NodeInfo as JavaNodeInfo
27+
import com.google.adk.workflow.Route as JavaRoute
28+
import com.google.genai.types.Content as GenaiContent
29+
30+
/**
31+
* Converts a workflow node's output, its [NodeInfo][KtNodeInfo], and the routes on `actions.route`
32+
* between ADK Kotlin and the ADK Java facade.
33+
*/
34+
internal object WorkflowCodec {
35+
36+
/**
37+
* Returns the Java form of a node's [output]: a Kotlin `Content` becomes a genai `Content`, and a
38+
* kotlinx `JsonElement` becomes the plain maps, lists, and values of an event read from JSON.
39+
* Maps and lists are copied with each entry converted; a value with no JSON form throws.
40+
*/
41+
@OptIn(FrameworkInternalApi::class)
42+
fun outputToJava(output: Any?): Any? =
43+
when (output) {
44+
null,
45+
is String,
46+
is Boolean,
47+
is Number -> output
48+
is KtContent -> ContentCodec.toJava(output)
49+
// A JsonObject or JsonArray is also a Map or List, so it converts entry by entry here.
50+
is Map<*, *> ->
51+
output.entries.associate { (key, value) -> key.toString() to outputToJava(value) }
52+
is List<*> -> output.map { outputToJava(it) }
53+
// Unwraps a JSON primitive or JSON null; throws if a value has no JSON form.
54+
else -> jsonElementToAny(anyToJsonElement(output))
55+
}
56+
57+
/**
58+
* Returns the Kotlin form of a node's [output]: a genai `Content` becomes a Kotlin `Content`, and
59+
* any other non-JSON-native object becomes its JSON form, as in an event read from JSON. Maps and
60+
* lists are copied with each entry converted.
61+
*/
62+
fun outputFromJava(output: Any?): Any? =
63+
when (output) {
64+
null,
65+
is String,
66+
is Boolean,
67+
is Int,
68+
is Long,
69+
is Float,
70+
is Double,
71+
is Short,
72+
is Byte -> output
73+
// ADK Kotlin has no JSON form for BigDecimal or BigInteger, so convert to Long or Double.
74+
is Number -> output.toString().toLongOrNull() ?: output.toDouble()
75+
is GenaiContent -> ContentCodec.fromJava(output)
76+
is Map<*, *> ->
77+
output.entries.associate { (key, value) -> key.toString() to outputFromJava(value) }
78+
is List<*> -> output.map { outputFromJava(it) }
79+
is Array<*> -> output.map { outputFromJava(it) }
80+
// Write, then read: convertValue can return an embedded value, such as a byte[], unchanged.
81+
else ->
82+
outputFromJava(
83+
JsonBaseModel.getMapper().let {
84+
it.readValue(it.writeValueAsString(output), Any::class.java)
85+
}
86+
)
87+
}
88+
89+
/** Returns the Java [JavaNodeInfo] copy of the Kotlin [info]. */
90+
fun nodeInfoToJava(info: KtNodeInfo): JavaNodeInfo =
91+
JavaNodeInfo.builder()
92+
.path(info.path)
93+
.outputFor(info.outputFor.orEmpty())
94+
.messageAsOutput(info.messageAsOutput)
95+
.build()
96+
97+
/**
98+
* Returns the Kotlin [KtNodeInfo] copy of the Java [info]. An empty `outputFor` reads as null,
99+
* the Kotlin default, since the Java list is never null.
100+
*/
101+
fun nodeInfoFromJava(info: JavaNodeInfo): KtNodeInfo =
102+
KtNodeInfo(
103+
path = info.path(),
104+
outputFor = info.outputFor().ifEmpty { null },
105+
messageAsOutput = info.messageAsOutput(),
106+
)
107+
108+
/** Returns the Java [JavaRoute] copy of the Kotlin [route]. */
109+
fun routeToJava(route: KtRoute): JavaRoute =
110+
when (route) {
111+
is KtRoute.Tag -> JavaRoute.Tag(route.value)
112+
is KtRoute.Num -> JavaRoute.Num(route.value)
113+
is KtRoute.Flag -> JavaRoute.Flag(route.value)
114+
KtRoute.Default -> JavaRoute.Default.INSTANCE
115+
}
116+
117+
/** Returns the Kotlin [KtRoute] copy of the Java [route]. */
118+
fun routeFromJava(route: JavaRoute): KtRoute =
119+
when (route) {
120+
is JavaRoute.Tag -> KtRoute.Tag(route.value())
121+
is JavaRoute.Num -> KtRoute.Num(route.value())
122+
is JavaRoute.Flag -> KtRoute.Flag(route.value())
123+
is JavaRoute.Default -> KtRoute.Default
124+
}
125+
}

0 commit comments

Comments
 (0)