diff --git a/docs/runtime/event-loop.md b/docs/runtime/event-loop.md index 3f41480e1d..c2c2efea7d 100644 --- a/docs/runtime/event-loop.md +++ b/docs/runtime/event-loop.md @@ -1,7 +1,7 @@ # Runtime Event Loop
- Supported in ADKPython v0.1.0Typescript v0.2.0Go v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Typescript v0.2.0Go v0.1.0Java v0.1.0Kotlin v0.1.0
The ADK Runtime is the underlying engine that powers your agent application during user interactions. It's the system that takes your defined agents, tools, and callbacks and orchestrates their execution in response to user input, managing the flow of information, state changes, and interactions with external services like LLMs or storage. @@ -179,6 +179,12 @@ The `Runner` acts as the central coordinator for a single user invocation. Its r } ``` +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunnerLoop.kt:conceptual_loop" + ``` + ### Execution Logic's Role (Agent, Tool, Callback) Your code within agents, tools, and callbacks is responsible for the actual computation and decision-making. Its interaction with the loop involves: @@ -372,6 +378,12 @@ Your code within agents, tools, and callbacks is responsible for the actual comp // If this subsequent code needs to yield another event, it would do so here. ``` +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunnerLoop.kt:execution_logic" + ``` + This cooperative yield/pause/resume cycle between the `Runner` and your Execution Logic, mediated by `Event` objects, forms the core of the ADK Runtime. ## Key components of the Runtime @@ -588,6 +600,12 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // or emitting more events based on the now-updated `ctx.session().state()`. ``` +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunnerLoop.kt:state_update_timing" + ``` + ### "Dirty Reads" of Session State * **Definition:** While commitment happens *after* the yield, code running *later within the same invocation*, but *before* the state-changing event is actually yielded and processed, **can often see the local, uncommitted changes**. This is sometimes called a "dirty read". @@ -666,6 +684,12 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // is yielded *after* this tool runs and is processed by the Runner. ``` +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunnerLoop.kt:dirty_read" + ``` + * **Implications:** * **Benefit:** Allows different parts of your logic within a single complex step (e.g., multiple callbacks or tool calls before the next LLM turn) to coordinate using state without waiting for a full yield/commit cycle. * **Caveat:** Relying heavily on dirty reads for critical logic can be risky. If the invocation fails *before* the event carrying the `state_delta` is yielded and processed by the `Runner`, the uncommitted state change will be lost. For critical state transitions, ensure they are associated with an event that gets successfully processed. diff --git a/docs/runtime/index.md b/docs/runtime/index.md index 463e0934bc..c7b57f69fc 100644 --- a/docs/runtime/index.md +++ b/docs/runtime/index.md @@ -1,7 +1,7 @@ # Agent Runtime
- Supported in ADKPython v0.1.0TypeScript v0.2.0Go v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0TypeScript v0.2.0Go v0.1.0Java v0.1.0Kotlin v0.1.0
ADK provides several ways to run and test your agents during development. Choose diff --git a/docs/runtime/resume.md b/docs/runtime/resume.md index dda948e771..650b12dcf6 100644 --- a/docs/runtime/resume.md +++ b/docs/runtime/resume.md @@ -1,7 +1,7 @@ # Resume stopped agents
- Supported in ADKPython v1.14.0 + Supported in ADKPython v1.14.0Kotlin v0.1.0
An ADK agent's execution can be interrupted by various factors including @@ -23,16 +23,24 @@ Enable the Resume function for an agent workflow by applying a Resumability configuration to the App object of your ADK workflow, as shown in the following code example: -```python -app = App( - name='my_resumable_agent', - root_agent=root_agent, - # Set the resumability config to enable resumability. - resumability_config=ResumabilityConfig( - is_resumable=True, - ), -) -``` +=== "Python" + + ```python + app = App( + name='my_resumable_agent', + root_agent=root_agent, + # Set the resumability config to enable resumability. + resumability_config=ResumabilityConfig( + is_resumable=True, + ), + ) + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunConfigExample.kt:resumability_config" + ``` !!! warning "Caution: Long Running Functions, Confirmations, Authentication" For agents that use @@ -77,13 +85,21 @@ curl -X POST http://localhost:8000/run_sse \ You can also resume a workflow using the Runner object Run Async method, as shown below: -```python -runner.run_async(user_id='u_123', session_id='s_abc', - invocation_id='invocation-123') +=== "Python" -# When new_message is set to a function response, -# we are trying to resume a long running function. -``` + ```python + runner.run_async(user_id='u_123', session_id='s_abc', + invocation_id='invocation-123') + + # When new_message is set to a function response, + # we are trying to resume a long running function. + ``` + +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunConfigExample.kt:resume_usage" + ``` !!! info "Note" Resuming a workflow from the ADK Web user interface or using the ADK diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index 422b49cada..5a8ab2edc6 100644 --- a/docs/runtime/runconfig.md +++ b/docs/runtime/runconfig.md @@ -1,7 +1,7 @@ # Runtime Configuration
- Supported in ADKPython v0.1.0TypeScript v0.2.0Go v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0TypeScript v0.2.0Go v0.1.0Java v0.1.0Kotlin v0.1.0
`RunConfig` controls how agents behave at runtime, including streaming mode, @@ -58,6 +58,12 @@ to `runner.run_async()` or `runner.run_live()` to override default behavior. .build(); ``` +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunConfigExample.kt:basic_usage" + ``` + ## Manage sessions and context
@@ -151,6 +157,12 @@ execute function calls. CFC uses the Live API under the hood. .build(); ``` +=== "Kotlin" + + ```kotlin + --8<-- "examples/kotlin/snippets/runtime/RunConfigExample.kt:streaming_config" + ``` + ## Configure audio and speech
diff --git a/examples/kotlin/snippets/runtime/RunConfigExample.kt b/examples/kotlin/snippets/runtime/RunConfigExample.kt new file mode 100644 index 0000000000..e1f6192079 --- /dev/null +++ b/examples/kotlin/snippets/runtime/RunConfigExample.kt @@ -0,0 +1,103 @@ +/* + * 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.examples.runtime + +import com.google.adk.kt.agents.LlmAgent +import com.google.adk.kt.agents.ResumabilityConfig +import com.google.adk.kt.agents.RunConfig +import com.google.adk.kt.agents.StreamingMode +import com.google.adk.kt.annotations.ExperimentalResumabilityFeature +import com.google.adk.kt.models.Gemini +import com.google.adk.kt.runners.InMemoryRunner +import com.google.adk.kt.sessions.InMemorySessionService +import com.google.adk.kt.types.Content +import com.google.adk.kt.types.Role +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.runBlocking + +// --8<-- [start:basic_usage] +val config = + RunConfig( + streamingMode = StreamingMode.SSE, + ) + +// Pass it to runner.runAsync +// runner.runAsync(..., runConfig = config) +// --8<-- [end:basic_usage] + +// --8<-- [start:full_example] +fun runWithConfig(runner: InMemoryRunner) = + runBlocking { + val config = + RunConfig( + streamingMode = StreamingMode.SSE, + customMetadata = mapOf("priority" to "high"), + ) + + runner + .runAsync( + userId = "user123", + sessionId = "session456", + newMessage = Content.fromText(Role.USER, "Hello!"), + runConfig = config, + ).collect { event -> + // handle events + } + } +// --8<-- [end:full_example] + +// --8<-- [start:custom_metadata] +val metadataConfig = + RunConfig( + customMetadata = mapOf("user_tier" to "premium"), + ) +// --8<-- [end:custom_metadata] + +// --8<-- [start:streaming_config] +val streamingConfig = + RunConfig( + streamingMode = StreamingMode.SSE, + ) +// --8<-- [end:streaming_config] + +private val rootAgent = + LlmAgent(name = "my_agent", model = Gemini(name = "gemini-flash-latest")) + +// --8<-- [start:resumability_config] +@OptIn(ExperimentalResumabilityFeature::class) +val runner = + InMemoryRunner( + agent = rootAgent, + appName = "my_resumable_agent", + sessionService = InMemorySessionService(), + resumabilityConfig = ResumabilityConfig(isResumable = true), + ) +// --8<-- [end:resumability_config] + +// --8<-- [start:resume_usage] +fun resumeAgent(runner: InMemoryRunner) = + runBlocking { + runner + .runAsync( + userId = "user123", + sessionId = "session456", + invocationId = "previous-invocation-id", + ).collect { event -> + // resume execution from previous state + } + } +// --8<-- [end:resume_usage] diff --git a/examples/kotlin/snippets/runtime/RunnerLoop.kt b/examples/kotlin/snippets/runtime/RunnerLoop.kt new file mode 100644 index 0000000000..14fd83cc60 --- /dev/null +++ b/examples/kotlin/snippets/runtime/RunnerLoop.kt @@ -0,0 +1,135 @@ +/* + * 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.examples.runtime + +import com.google.adk.kt.agents.InvocationContext +import com.google.adk.kt.events.Event +import com.google.adk.kt.events.EventActions +import com.google.adk.kt.runners.InMemoryRunner +import com.google.adk.kt.sessions.InMemorySessionService +import com.google.adk.kt.types.Content +import com.google.adk.kt.types.Role +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach + +// --8<-- [start:conceptual_loop] + +/** + * Simplified view of Runner's main loop logic in Kotlin + */ +fun runAsync( + userId: String, + sessionId: String, + newMessage: Content, + runner: InMemoryRunner, + sessionService: InMemorySessionService, +): Flow { + // 1. Append newMessage to session event history (via SessionService) + // 2. Kick off event loop by calling the agent + // 3. Process generated events, commit changes, and yield upstream + return runner + .runAsync( + userId = userId, + sessionId = sessionId, + newMessage = newMessage, + ).onEach { event -> + // Process the event and commit changes to services (done internally by Runner) + // sessionService.appendEvent(...) + } +} +// --8<-- [end:conceptual_loop] + +// --8<-- [start:execution_logic] + +/** + * Simplified view of logic inside Agent.runAsync, callbacks, or tools in Kotlin + */ +suspend fun executionLogic(ctx: InvocationContext) { + // ... previous code runs based on current state ... + + // 1. Determine a change or output is needed, construct the event + val updateData = mapOf("field_1" to "value_2") + val eventWithStateChange = + Event( + author = "my_agent", + actions = EventActions(stateDelta = updateData.toMutableMap()), + content = Content.fromText(Role.MODEL, "State updated."), + ) + + // 2. Yield the event to the Runner for processing & commit + // In Kotlin, this is done by emitting to the Flow + // emit(eventWithStateChange) + + // <<<<<<<<<<<< EXECUTION PAUSES HERE >>>>>>>>>>>> + // (Implicitly, when the Flow consumer collects the event and processes it) + + // <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>> + + // 3. Resume execution ONLY after Runner is done processing. + // Now, the state committed by the Runner is reliably reflected. + val val1 = ctx.session.state["field_1"] + println("Resumed execution. Value of field_1 is now: $val1") +} +// --8<-- [end:execution_logic] + +// --8<-- [start:state_update_timing] + +/** + * Conceptual view of state update timing in Kotlin + */ +suspend fun stateUpdateTiming(ctx: InvocationContext) { + // 1. Modify state + ctx.session.state["status"] = "processing" + val event1 = + Event( + author = "my_agent", + actions = EventActions(stateDelta = mutableMapOf("status" to "processing")), + ) + + // 2. Yield event with the delta (emit to flow) + // emit(event1) + + // --- PAUSE --- Runner processes event1, SessionService commits 'status' = 'processing' --- + + // 3. Resume execution + // Now it's safe to rely on the committed state + val currentStatus = ctx.session.state["status"] // Guaranteed to be 'processing' + println("Status after resuming: $currentStatus") +} +// --8<-- [end:state_update_timing] + +// --8<-- [start:dirty_read] + +/** + * Conceptual view of dirty reads in Kotlin + */ +fun dirtyRead(ctx: InvocationContext) { + // Code in a callback + ctx.session.state["field_1"] = "value_1" + // State is locally set to 'value_1', but not yet committed by Runner + + // ... agent runs ... + + // Code in a tool called later *within the same invocation* + // Readable (dirty read), but 'value_1' isn't guaranteed persistent yet. + val val1 = ctx.session.state["field_1"] // 'val' will likely be 'value_1' here + println("Dirty read value in tool: $val1") + + // Assume the event carrying the state_delta={'field_1': 'value_1'} + // is yielded *after* this tool runs and is processed by the Runner. +} +// --8<-- [end:dirty_read] diff --git a/tools/kotlin-snippets/files_to_test.txt b/tools/kotlin-snippets/files_to_test.txt index 562f398067..7746771855 100644 --- a/tools/kotlin-snippets/files_to_test.txt +++ b/tools/kotlin-snippets/files_to_test.txt @@ -28,4 +28,7 @@ snippets/tools/function-tools/AgentTool.kt snippets/tools/function-tools/PassData.kt snippets/tools/LimitationsWorkaround.kt snippets/get-started/multi_tool_agent/MultiToolAgent.kt +snippets/runtime/RunConfigExample.kt +snippets/runtime/RunnerLoop.kt +