diff --git a/pom.xml b/pom.xml
index e28446971..e16b17347 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,7 +74,7 @@
2.1.20
1.11.0
- 1.0.0
+ 1.1.0
3.7.0
2.35.1
3.27.7
diff --git a/tokt/src/main/kotlin/com/google/adk/tokt/JavaAdkToKt.kt b/tokt/src/main/kotlin/com/google/adk/tokt/JavaAdkToKt.kt
index 6e948efb4..4ed0f166a 100644
--- a/tokt/src/main/kotlin/com/google/adk/tokt/JavaAdkToKt.kt
+++ b/tokt/src/main/kotlin/com/google/adk/tokt/JavaAdkToKt.kt
@@ -48,11 +48,13 @@ import com.google.adk.tools.BaseToolset as JavaBaseToolset
* control-flow writes reach the engine. Blocking work is fine: calls are dispatched off the thread
* driving the agent.
*
+ * A bridged plugin's error callbacks fire: `onRunErrorCallback` is notification-only -- the engine
+ * re-raises the run's error to the caller afterwards regardless, so it cannot recover the run (it
+ * is for logging, telemetry, or cleanup) -- while the `onModelErrorCallback` and
+ * `onToolErrorCallback` recovery hooks fire and can recover.
+ *
* The interop surfaces a signal the engine cannot honor rather than silently dropping it:
* - Setting `branch` on a bridged context throws. The branch is the engine's to set.
- * - A bridged plugin that overrides `onRunErrorCallback` is skipped with a warning (not run): the
- * engine surfaces run-level errors through the returned event stream to the caller, not to
- * plugins. The `onModelErrorCallback` and `onToolErrorCallback` recovery hooks do fire.
* - A bridged tool's or plugin's `requestedAuthConfigs` or `deletedArtifactIds` write throws - the
* engine's event actions have no equivalent. Its `skipSummarization`,
* `requestedToolConfirmations` and `agentState` writes do cross, from a tool and a plugin alike.
diff --git a/tokt/src/main/kotlin/com/google/adk/tokt/adapters/JavaPluginToKt.kt b/tokt/src/main/kotlin/com/google/adk/tokt/adapters/JavaPluginToKt.kt
index 0aa50be66..93af75452 100644
--- a/tokt/src/main/kotlin/com/google/adk/tokt/adapters/JavaPluginToKt.kt
+++ b/tokt/src/main/kotlin/com/google/adk/tokt/adapters/JavaPluginToKt.kt
@@ -16,7 +16,6 @@
package com.google.adk.tokt.adapters
-import com.google.adk.agents.InvocationContext as JavaInvocationContext
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
@@ -40,7 +39,6 @@ import com.google.adk.tokt.context.ktCallbackContextToJava
import com.google.adk.tokt.context.ktToolContextToJava
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Maybe
-import java.util.logging.Logger
import kotlinx.coroutines.rx3.await
import kotlinx.coroutines.rx3.awaitSingleOrNull
import kotlinx.coroutines.withContext
@@ -55,30 +53,6 @@ import kotlinx.coroutines.withContext
*/
internal class JavaPluginToKt(internal val plugin: JavaPlugin) : KtPlugin {
- private companion object {
- val logger: Logger = Logger.getLogger(JavaPluginToKt::class.java.name)
- }
-
- init {
- // The engine surfaces run-level errors through the event stream, not to plugins, so an
- // onRunErrorCallback override cannot fire here; warn and skip it rather than failing
- // adaptation.
- val onRunError =
- plugin.javaClass.getMethod(
- "onRunErrorCallback",
- JavaInvocationContext::class.java,
- Throwable::class.java,
- )
- // declaringClass is Plugin only when neither the class nor an intermediate interface overrode
- // it.
- if (onRunError.declaringClass != JavaPlugin::class.java) {
- logger.warning(
- "Bridged Java plugin '${plugin.name}' overrides onRunErrorCallback, which the ADK Kotlin " +
- "engine does not invoke; the override is skipped (run errors surface via the event stream)."
- )
- }
- }
-
override val name: String
get() = plugin.name
@@ -128,6 +102,11 @@ internal class JavaPluginToKt(internal val plugin: JavaPlugin) : KtPlugin {
completeOnIo { plugin.afterRunCallback(javaContext) }
}
+ override suspend fun onRunError(invocationContext: KtInvocationContext, error: Throwable) {
+ val javaContext = KtInvocationContextToJavaView(invocationContext)
+ completeOnIo { plugin.onRunErrorCallback(javaContext, error) }
+ }
+
// Agent-level callbacks.
override suspend fun beforeAgent(
diff --git a/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt b/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt
index 727e1e133..ff617cdc3 100644
--- a/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt
+++ b/tokt/src/test/kotlin/com/google/adk/tokt/KtRunnerInteropTest.kt
@@ -26,6 +26,8 @@ import com.google.adk.artifacts.InMemoryArtifactService as JavaInMemoryArtifactS
import com.google.adk.events.Event as JavaEvent
import com.google.adk.events.EventActions as JavaEventActions
import com.google.adk.events.EventCompaction as JavaEventCompaction
+import com.google.adk.kt.agents.BaseAgent as KtBaseAgent
+import com.google.adk.kt.agents.InvocationContext as KtInvocationContext
import com.google.adk.kt.agents.LlmAgent as KtLlmAgent
import com.google.adk.kt.agents.RunConfig as KtRunConfig
import com.google.adk.kt.agents.StreamingMode as KtStreamingMode
@@ -149,6 +151,8 @@ import kotlin.test.assertSame
import kotlin.test.assertTrue
import kotlin.test.fail
import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
@@ -451,12 +455,21 @@ class KtRunnerInteropTest {
}
}
- /** A Java plugin that overrides onRunErrorCallback, which the engine cannot fire. */
+ /** A Java plugin that records every error its onRunErrorCallback is notified of. */
private class OnRunErrorJavaPlugin : JavaBasePlugin("on_run_error_plugin") {
+ val errors = CopyOnWriteArrayList()
+
override fun onRunErrorCallback(
invocationContext: JavaInvocationContext,
error: Throwable,
- ): Completable = Completable.complete()
+ ): Completable = Completable.fromAction { errors.add(error) }
+ }
+
+ /** A native Kotlin agent that always fails, to drive the run-error path. */
+ private class FailingKtAgent : KtBaseAgent(name = "failing") {
+ override fun runAsyncImpl(context: KtInvocationContext): Flow = flow {
+ throw RuntimeException("boom")
+ }
}
/** A Java plugin that requests an auth config the engine cannot represent, from before-agent. */
@@ -3444,12 +3457,25 @@ class KtRunnerInteropTest {
}
@Test
- fun asKtPlugin_pluginOverridingOnRunError_isSkippedNotRejected() {
- // The engine never fires onRunErrorCallback, so the override is skipped with a warning rather
- // than rejected; adaptation still succeeds and the plugin's other callbacks run.
- val plugin = JavaAdkToKt.asKtPlugin(OnRunErrorJavaPlugin())
+ fun ktRunner_javaPluginOnRunErrorCallback_firesWhenRunFails() = runBlocking {
+ // A bridged Java plugin's onRunErrorCallback fires (notification-only) when the run fails, and
+ // the error still propagates to the caller.
+ val plugin = OnRunErrorJavaPlugin()
+ val runner =
+ KtInMemoryRunner(
+ app =
+ KtApp(
+ appName = "app",
+ rootAgent = FailingKtAgent(),
+ plugins = listOf(JavaAdkToKt.asKtPlugin(plugin)),
+ )
+ )
+
+ val thrown = assertFailsWith { runner.turn() }
- assertEquals("on_run_error_plugin", plugin.name)
+ // The plugin was notified exactly once, with the same error instance that failed the run.
+ assertEquals(1, plugin.errors.size)
+ assertSame(thrown, plugin.errors.single())
}
@Test