From f801b9abee1006217dab900fbcd8807fc5b13750 Mon Sep 17 00:00:00 2001 From: Mateusz Krawiec Date: Wed, 15 Jul 2026 06:05:47 -0700 Subject: [PATCH] ADK Kotlin changes PiperOrigin-RevId: 948291783 --- .../google/adk/kt/agents/InvocationContext.kt | 20 +++++++- .../com/google/adk/kt/agents/LlmAgentTurn.kt | 4 ++ .../kotlin/com/google/adk/kt/events/Event.kt | 3 +- .../google/adk/kt/types/GenaiConverters.kt | 28 +++++++++-- .../com/google/adk/kt/types/GroundingChunk.kt | 2 + .../google/adk/kt/types/GroundingChunkMaps.kt | 30 ++++++++++++ .../google/adk/kt/types/GroundingMetadata.kt | 5 +- .../com/google/adk/kt/types/UsageMetadata.kt | 4 ++ .../adk/kt/types/GenaiConvertersTest.kt | 16 ++++++- settings.gradle.kts | 7 +++ webserver/build.gradle.kts | 2 +- .../google/adk/kt/webserver/AdkWebServer.kt | 29 +++++++++++- .../adk/kt/webserver/models/ApiModels.kt | 11 +++++ .../adk/kt/webserver/routes/RunRoutes.kt | 47 +++++++++++++------ 14 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunkMaps.kt diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/agents/InvocationContext.kt b/core/src/commonMain/kotlin/com/google/adk/kt/agents/InvocationContext.kt index bce99019..cb522bb9 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/agents/InvocationContext.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/agents/InvocationContext.kt @@ -531,7 +531,7 @@ data class InvocationContext( functionResponse = FunctionResponse( name = tool.name, - response = toFinalResponseMap(toolResult), + response = toResponseMapPreservingNulls(toolResult), id = toolContext.functionCallId, ) ) @@ -642,6 +642,24 @@ data class InvocationContext( @OptIn(FrameworkInternalApi::class) private fun toTraceJson(payload: Any?): JsonElement = anyToJsonElement(payload) + /** + * Like [toFinalResponseMap] but preserves null values in the response dict (e.g. `{result: + * null}`); only entries with non-String keys are dropped. The genai `FunctionResponse` allows + * null values, so the function-response *event* keeps them. (The after-tool callback path still + * receives the null-dropped [toFinalResponseMap] to preserve its non-null `Map` + * contract.) + * + * TODO(b/536808100): unify with [toFinalResponseMap] so the callback path preserves nulls too. + */ + private fun toResponseMapPreservingNulls(payload: Any?): Map = + if (payload !is Map<*, *>) { + mapOf(BaseTool.RESULT_KEY to payload) + } else { + payload.entries + .mapNotNull { entry -> (entry.key as? String)?.let { key -> key to entry.value } } + .toMap() + } + private fun safeCastToMapStringAny(value: Any?): Map { if (value !is Map<*, *>) return emptyMap() return value.entries diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/agents/LlmAgentTurn.kt b/core/src/commonMain/kotlin/com/google/adk/kt/agents/LlmAgentTurn.kt index 0aef6cde..6929d751 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/agents/LlmAgentTurn.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/agents/LlmAgentTurn.kt @@ -419,6 +419,10 @@ internal class LlmAgentTurn( partial = response.partial, interrupted = response.interrupted, cacheMetadata = response.cacheMetadata, + modelVersion = response.modelVersion, + avgLogProbs = response.avgLogprobs, + groundingMetadata = response.groundingMetadata, + citationMetadata = response.citationMetadata, ) .populateClientFunctionCallId() diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/events/Event.kt b/core/src/commonMain/kotlin/com/google/adk/kt/events/Event.kt index 05155647..d873e6bf 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/events/Event.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/events/Event.kt @@ -28,6 +28,7 @@ import com.google.adk.kt.types.GroundingMetadata import com.google.adk.kt.types.UsageMetadata import kotlin.time.Clock import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** @@ -72,7 +73,7 @@ data class Event( val errorMessage: String? = null, val finishReason: FinishReason? = null, val usageMetadata: UsageMetadata? = null, - val avgLogProbs: Double? = null, + @SerialName("avgLogprobs") val avgLogProbs: Double? = null, val interrupted: Boolean = false, val branch: String? = null, val groundingMetadata: GroundingMetadata? = null, diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt b/core/src/commonMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt index 5d021805..99478944 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/types/GenaiConverters.kt @@ -41,6 +41,7 @@ import com.google.genai.kotlin.types.GenerationConfigRoutingConfigManualRoutingM import com.google.genai.kotlin.types.GoogleMaps as GenAiGoogleMaps import com.google.genai.kotlin.types.GoogleSearch as GenAiGoogleSearch import com.google.genai.kotlin.types.GroundingChunk as GenAiGroundingChunk +import com.google.genai.kotlin.types.GroundingChunkMaps as GenAiGroundingChunkMaps import com.google.genai.kotlin.types.GroundingChunkRetrievedContext as GenAiGroundingChunkRetrievedContext import com.google.genai.kotlin.types.GroundingChunkWeb as GenAiGroundingChunkWeb import com.google.genai.kotlin.types.GroundingMetadata as GenAiGroundingMetadata @@ -467,10 +468,11 @@ internal fun GenerateContentResponse.toGenaiSdk(): GenAiGenerateContentResponse /** Converts a [GenAiGroundingMetadata] from the GenAI SDK to an ADK [GroundingMetadata]. */ internal fun GenAiGroundingMetadata.fromGenaiSdk(): GroundingMetadata = GroundingMetadata( - imageSearchQueries = imageSearchQueries ?: emptyList(), + imageSearchQueries = imageSearchQueries, groundingChunks = groundingChunks?.map { it.fromGenaiSdk() }, groundingSupports = groundingSupports?.map { it.fromGenaiSdk() }, webSearchQueries = webSearchQueries, + retrievalQueries = retrievalQueries, searchEntryPoint = searchEntryPoint?.fromGenaiSdk(), retrievalMetadata = retrievalMetadata?.fromGenaiSdk(), ) @@ -482,6 +484,7 @@ internal fun GroundingMetadata.toGenaiSdk(): GenAiGroundingMetadata = groundingChunks = groundingChunks?.map { it.toGenaiSdk() }, groundingSupports = groundingSupports?.map { it.toGenaiSdk() }, webSearchQueries = webSearchQueries, + retrievalQueries = retrievalQueries, searchEntryPoint = searchEntryPoint?.toGenaiSdk(), retrievalMetadata = retrievalMetadata?.toGenaiSdk(), ) @@ -489,11 +492,28 @@ internal fun GroundingMetadata.toGenaiSdk(): GenAiGroundingMetadata = // --- GroundingChunk --- /** Converts a [GenAiGroundingChunk] from the GenAI SDK to an ADK [GroundingChunk]. */ internal fun GenAiGroundingChunk.fromGenaiSdk(): GroundingChunk = - GroundingChunk(web = web?.fromGenaiSdk(), retrievedContext = retrievedContext?.fromGenaiSdk()) + GroundingChunk( + web = web?.fromGenaiSdk(), + retrievedContext = retrievedContext?.fromGenaiSdk(), + maps = maps?.fromGenaiSdk(), + ) /** Converts an ADK [GroundingChunk] to a [GenAiGroundingChunk] for the GenAI SDK. */ internal fun GroundingChunk.toGenaiSdk(): GenAiGroundingChunk = - GenAiGroundingChunk(web = web?.toGenaiSdk(), retrievedContext = retrievedContext?.toGenaiSdk()) + GenAiGroundingChunk( + web = web?.toGenaiSdk(), + retrievedContext = retrievedContext?.toGenaiSdk(), + maps = maps?.toGenaiSdk(), + ) + +// --- GroundingChunkMaps --- +/** Converts a [GenAiGroundingChunkMaps] from the GenAI SDK to an ADK [GroundingChunkMaps]. */ +internal fun GenAiGroundingChunkMaps.fromGenaiSdk(): GroundingChunkMaps = + GroundingChunkMaps(uri = uri, title = title, placeId = placeId) + +/** Converts an ADK [GroundingChunkMaps] to a [GenAiGroundingChunkMaps] for the GenAI SDK. */ +internal fun GroundingChunkMaps.toGenaiSdk(): GenAiGroundingChunkMaps = + GenAiGroundingChunkMaps(uri = uri, title = title, placeId = placeId) // --- GroundingChunkWeb --- /** Converts a [GenAiGroundingChunkWeb] from the GenAI SDK to an ADK [GroundingChunkWeb]. */ @@ -761,6 +781,7 @@ internal fun GenAiGenerateContentResponseUsageMetadata.fromGenaiSdk(): UsageMeta cachedContentTokenCount = cachedContentTokenCount, promptTokensDetails = promptTokensDetails?.map { it.fromGenaiSdk() }, candidatesTokensDetails = candidatesTokensDetails?.map { it.fromGenaiSdk() }, + toolUsePromptTokensDetails = toolUsePromptTokensDetails?.map { it.fromGenaiSdk() }, ) /** @@ -777,6 +798,7 @@ internal fun UsageMetadata.toGenaiSdk(): GenAiGenerateContentResponseUsageMetada cachedContentTokenCount = cachedContentTokenCount, promptTokensDetails = promptTokensDetails?.map { it.toGenaiSdk() }, candidatesTokensDetails = candidatesTokensDetails?.map { it.toGenaiSdk() }, + toolUsePromptTokensDetails = toolUsePromptTokensDetails?.map { it.toGenaiSdk() }, ) // --- Part --- diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunk.kt b/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunk.kt index 43d23007..6a9adf36 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunk.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunk.kt @@ -25,4 +25,6 @@ data class GroundingChunk( val web: GroundingChunkWeb? = null, /** A chunk sourced from a retrieval (RAG) context. */ val retrievedContext: GroundingChunkRetrievedContext? = null, + /** A chunk sourced from Google Maps grounding. */ + val maps: GroundingChunkMaps? = null, ) diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunkMaps.kt b/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunkMaps.kt new file mode 100644 index 00000000..1464c1e4 --- /dev/null +++ b/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingChunkMaps.kt @@ -0,0 +1,30 @@ +/* + * 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.kt.types + +import kotlinx.serialization.Serializable + +/** A grounding chunk sourced from Google Maps. */ +@Serializable +data class GroundingChunkMaps( + /** The URI of the place on Google Maps. */ + val uri: String? = null, + /** The title (name) of the place. */ + val title: String? = null, + /** The Google Maps place id (e.g. `places/ChIJ...`). */ + val placeId: String? = null, +) diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingMetadata.kt b/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingMetadata.kt index 54b03c7d..f6c7a809 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingMetadata.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/types/GroundingMetadata.kt @@ -21,13 +21,16 @@ import kotlinx.serialization.Serializable /** Metadata returned to client when grounding is enabled. */ @Serializable data class GroundingMetadata( - val imageSearchQueries: List = emptyList(), + /** The image search queries used to retrieve grounding sources. */ + val imageSearchQueries: List? = null, /** The cited source chunks that ground the response. */ val groundingChunks: List? = null, /** Maps response segments to the grounding chunks that support them. */ val groundingSupports: List? = null, /** The web search queries used to retrieve grounding sources. */ val webSearchQueries: List? = null, + /** The retrieval queries used to retrieve grounding sources. */ + val retrievalQueries: List? = null, /** The Google Search entry point for rendering search suggestions. */ val searchEntryPoint: SearchEntryPoint? = null, /** Metadata about the retrieval step. */ diff --git a/core/src/commonMain/kotlin/com/google/adk/kt/types/UsageMetadata.kt b/core/src/commonMain/kotlin/com/google/adk/kt/types/UsageMetadata.kt index 0d79c0a5..cf4946f4 100644 --- a/core/src/commonMain/kotlin/com/google/adk/kt/types/UsageMetadata.kt +++ b/core/src/commonMain/kotlin/com/google/adk/kt/types/UsageMetadata.kt @@ -37,4 +37,8 @@ data class UsageMetadata( val promptTokensDetails: List? = null, /** A per-modality breakdown of the candidates token count. */ val candidatesTokensDetails: List? = null, + /** A per-modality breakdown of the tool-use prompt token count. */ + val toolUsePromptTokensDetails: List? = null, + /** The traffic type for the request (e.g. "ON_DEMAND", "PROVISIONED_THROUGHPUT"). */ + val trafficType: String? = null, ) diff --git a/core/src/commonTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt b/core/src/commonTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt index fb8a9e23..5d51069f 100644 --- a/core/src/commonTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt +++ b/core/src/commonTest/kotlin/com/google/adk/kt/types/GenaiConvertersTest.kt @@ -367,6 +367,7 @@ class GenaiConvertersTest { val adkGroundingMetadata = GroundingMetadata( webSearchQueries = listOf("kotlin coroutines"), + retrievalQueries = listOf("kotlin release date"), groundingChunks = listOf( GroundingChunk( @@ -376,7 +377,15 @@ class GenaiConvertersTest { title = "Example", domain = "example.com", ) - ) + ), + GroundingChunk( + maps = + GroundingChunkMaps( + uri = "https://maps.google.com/?cid=1", + title = "Googleplex", + placeId = "places/ABC", + ) + ), ), groundingSupports = listOf( @@ -392,7 +401,8 @@ class GenaiConvertersTest { val genaiGroundingMetadata = adkGroundingMetadata.toGenaiSdk() assertEquals(listOf("kotlin coroutines"), genaiGroundingMetadata.webSearchQueries) - assertEquals("example.com", genaiGroundingMetadata.groundingChunks?.single()?.web?.domain) + assertEquals("example.com", genaiGroundingMetadata.groundingChunks?.get(0)?.web?.domain) + assertEquals("places/ABC", genaiGroundingMetadata.groundingChunks?.get(1)?.maps?.placeId) val convertedBack = genaiGroundingMetadata.fromGenaiSdk() assertEquals(adkGroundingMetadata, convertedBack) @@ -655,6 +665,8 @@ class GenaiConvertersTest { listOf(ModalityTokenCount(modality = MediaModality.TEXT, tokenCount = 1)), candidatesTokensDetails = listOf(ModalityTokenCount(modality = MediaModality.IMAGE, tokenCount = 2)), + toolUsePromptTokensDetails = + listOf(ModalityTokenCount(modality = MediaModality.TEXT, tokenCount = 7)), ) val genaiUsageMetadata = adkUsageMetadata.toGenaiSdk() assertEquals(1, genaiUsageMetadata.promptTokenCount) diff --git a/settings.gradle.kts b/settings.gradle.kts index dda5f8e1..c0b9423d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -61,3 +61,10 @@ project(":google-adk-kotlin-litertlm").projectDir = file("litertlm") include(":google-adk-kotlin-examples-android") project(":google-adk-kotlin-examples-android").projectDir = file("examples/android") + +// copybara:strip_begin(internal) +// Internal-only module (config API + conformance harness), stripped from the public GitHub mirror. +include(":google-adk-kotlin-internal-conformance") + +project(":google-adk-kotlin-internal-conformance").projectDir = + file("internal/conformance") // copybara:strip_end diff --git a/webserver/build.gradle.kts b/webserver/build.gradle.kts index c2e7f808..571979d8 100644 --- a/webserver/build.gradle.kts +++ b/webserver/build.gradle.kts @@ -16,7 +16,6 @@ plugins { kotlin("jvm") - id("application") id("java-library") id("maven-publish") } @@ -44,6 +43,7 @@ sourceSets { dependencies { implementation(project(":google-adk-kotlin-core")) implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.coroutines.core) implementation(libs.graphviz.java) diff --git a/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/AdkWebServer.kt b/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/AdkWebServer.kt index ebbe69e5..64a9a751 100644 --- a/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/AdkWebServer.kt +++ b/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/AdkWebServer.kt @@ -17,11 +17,13 @@ package com.google.adk.kt.webserver import com.google.adk.kt.artifacts.ArtifactService +import com.google.adk.kt.plugins.Plugin import com.google.adk.kt.runners.Runner import com.google.adk.kt.sessions.SessionService import com.google.adk.kt.telemetry.TelemetryConfig import com.google.adk.kt.webserver.AdkWebServer.StatusAwareLogger import com.google.adk.kt.webserver.loaders.AgentLoader +import com.google.adk.kt.webserver.models.VersionInfo import com.google.adk.kt.webserver.routes.appRoutes import com.google.adk.kt.webserver.routes.artifactRoutes import com.google.adk.kt.webserver.routes.debugRoutes @@ -32,8 +34,10 @@ import com.google.adk.kt.webserver.routes.sessionRoutes import com.google.adk.kt.webserver.routes.staticRoutes import com.google.adk.kt.webserver.telemetry.ApiServerSpanExporter import com.google.adk.kt.webserver.telemetry.OpenTelemetryConfig +import com.google.gson.GsonBuilder import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import io.ktor.serialization.gson.gson import io.ktor.server.application.Application @@ -46,6 +50,7 @@ import io.ktor.server.plugins.callloging.CallLogging import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.request.httpMethod import io.ktor.server.request.uri +import io.ktor.server.response.respond import io.ktor.server.response.respondText import io.ktor.server.routing.get import io.ktor.server.routing.routing @@ -68,6 +73,8 @@ class AdkWebServer( private val agentLoader: AgentLoader, private val apiServerSpanExporter: ApiServerSpanExporter, private val captureMessageContent: Boolean = false, + private val plugins: List = emptyList(), + private val gsonConfigurer: (GsonBuilder.() -> Unit)? = null, ) { @Deprecated( message = "Use constructor without runner", @@ -96,6 +103,9 @@ class AdkWebServer( companion object { private val logger = LoggerFactory.getLogger(AdkWebServer::class.java) + + /** The ADK Kotlin version reported by the `/version` endpoint. */ + fun adkVersion(): String = com.google.adk.kt.VERSION } private var server: ApplicationEngine? = null @@ -111,6 +121,8 @@ class AdkWebServer( agentLoader, apiServerSpanExporter, captureMessageContent, + plugins, + gsonConfigurer, ) } .start(wait = wait) @@ -133,7 +145,7 @@ class AdkWebServer( } override fun read(reader: JsonReader): Instant? { - if (reader.peek() == com.google.gson.stream.JsonToken.NULL) { + if (reader.peek() == JsonToken.NULL) { reader.nextNull() return null } @@ -158,6 +170,8 @@ fun Application.adkModule( agentLoader: AgentLoader, apiServerSpanExporter: ApiServerSpanExporter, captureMessageContent: Boolean = false, + plugins: List = emptyList(), + gsonConfigurer: (GsonBuilder.() -> Unit)? = null, ) { install(CallLogging) { level = Level.INFO @@ -173,6 +187,8 @@ fun Application.adkModule( gson { setPrettyPrinting() registerTypeAdapter(Instant::class.java, AdkWebServer.InstantTypeAdapter()) + // Callers can register extra Gson type adapters here. + gsonConfigurer?.invoke(this) } } @@ -199,12 +215,21 @@ fun Application.adkModule( routing { get("/api/health") { call.respondText("OK") } + get("/version") { + call.respond( + VersionInfo( + version = AdkWebServer.adkVersion(), + language = "kotlin", + languageVersion = System.getProperty("java.version", "unknown"), + ) + ) + } appRoutes(agentLoader) artifactRoutes(artifactService) debugRoutes(apiServerSpanExporter) evalRoutes() graphRoutes(agentLoader, sessionService) - runRoutes(agentLoader, sessionService, artifactService) + runRoutes(agentLoader, sessionService, artifactService, plugins) sessionRoutes(sessionService) staticRoutes(this@adkModule) } diff --git a/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/models/ApiModels.kt b/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/models/ApiModels.kt index 17b8a5c4..3998c756 100644 --- a/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/models/ApiModels.kt +++ b/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/models/ApiModels.kt @@ -17,6 +17,17 @@ package com.google.adk.kt.webserver.models import com.google.adk.kt.types.Content +import com.google.gson.annotations.SerializedName + +/** + * Response for the `/version` endpoint. The [languageVersion] field is serialized as + * `language_version`. + */ +data class VersionInfo( + val version: String, + val language: String, + @SerializedName("language_version") val languageVersion: String, +) data class AgentRunRequest( val appName: String, diff --git a/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/RunRoutes.kt b/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/RunRoutes.kt index 2faeb2ca..60dd484f 100644 --- a/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/RunRoutes.kt +++ b/webserver/src/jvmMain/kotlin/com/google/adk/kt/webserver/routes/RunRoutes.kt @@ -16,9 +16,12 @@ package com.google.adk.kt.webserver.routes +import com.google.adk.kt.agents.BaseAgent import com.google.adk.kt.agents.RunConfig import com.google.adk.kt.agents.StreamingMode +import com.google.adk.kt.apps.App import com.google.adk.kt.artifacts.ArtifactService +import com.google.adk.kt.plugins.Plugin import com.google.adk.kt.runners.InMemoryRunner import com.google.adk.kt.sessions.SessionService import com.google.adk.kt.webserver.loaders.AgentLoader @@ -41,6 +44,7 @@ fun Route.runRoutes( agentLoader: AgentLoader, sessionService: SessionService, artifactService: ArtifactService, + plugins: List = emptyList(), ) { route("/run") { post { @@ -49,13 +53,7 @@ fun Route.runRoutes( if (agent == null) { return@post call.respond(HttpStatusCode.NotFound, "Agent not found") } - val runner = - InMemoryRunner( - agent = agent, - sessionService = sessionService, - artifactService = artifactService, - appName = request.appName, - ) + val runner = buildRunner(agent, request.appName, sessionService, artifactService, plugins) val runConfig = RunConfig(streamingMode = if (request.streaming) StreamingMode.NONE else StreamingMode.NONE) @@ -84,13 +82,7 @@ fun Route.runRoutes( if (agent == null) { return@post call.respond(HttpStatusCode.NotFound, "Agent not found") } - val runner = - InMemoryRunner( - agent = agent, - sessionService = sessionService, - artifactService = artifactService, - appName = request.appName, - ) + val runner = buildRunner(agent, request.appName, sessionService, artifactService, plugins) val runConfig = RunConfig(streamingMode = if (request.streaming) StreamingMode.SSE else StreamingMode.NONE) @@ -116,3 +108,30 @@ fun Route.runRoutes( } } } + +/** + * Builds an [InMemoryRunner]. With no [plugins] (the default web-server case) it uses the runner + * constructor that accepts any [appName]. When [plugins] are supplied it routes through [App], + * whose [App.appName] must be a valid identifier. + */ +private fun buildRunner( + agent: BaseAgent, + appName: String, + sessionService: SessionService, + artifactService: ArtifactService, + plugins: List, +): InMemoryRunner = + if (plugins.isEmpty()) { + InMemoryRunner( + agent = agent, + appName = appName, + sessionService = sessionService, + artifactService = artifactService, + ) + } else { + InMemoryRunner( + app = App(appName = appName, rootAgent = agent, plugins = plugins), + sessionService = sessionService, + artifactService = artifactService, + ) + }