Runtime¶
What is runtime?¶
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.
Think of the Runtime as the "engine" of your agentic application. You define the parts (agents, tools), and the Runtime handles how they connect and run together to fulfill a user's request.
Core Idea: The Event Loop¶
At its heart, the ADK Runtime operates on an Event Loop. This loop facilitates a back-and-forth communication between the Runner
component and your defined "Execution Logic" (which includes your Agents, the LLM calls they make, Callbacks, and Tools).
In simple terms:
- The
Runner
receives a user query and asks the mainAgent
to start processing. - The
Agent
(and its associated logic) runs until it has something to report (like a response, a request to use a tool, or a state change) – it then yields or emits anEvent
. - The
Runner
receives thisEvent
, processes any associated actions (like saving state changes viaServices
), and forwards the event onwards (e.g., to the user interface). - Only after the
Runner
has processed the event does theAgent
's logic resume from where it paused, now potentially seeing the effects of the changes committed by the Runner. - This cycle repeats until the agent has no more events to yield for the current user query.
This event-driven loop is the fundamental pattern governing how ADK executes your agent code.
The Heartbeat: The Event Loop - Inner workings¶
The Event Loop is the core operational pattern defining the interaction between the Runner
and your custom code (Agents, Tools, Callbacks, collectively referred to as "Execution Logic" or "Logic Components" in the design document). It establishes a clear division of responsibilities:
Note
The specific method names and parameter names may vary slightly by SDK language (e.g., agent_to_run.runAsync(...)
in Java, agent_to_run.run_async(...)
in Python). Refer to the language-specific API documentation for details.
Runner's Role (Orchestrator)¶
The Runner
acts as the central coordinator for a single user invocation. Its responsibilities in the loop are:
- Initiation: Receives the end user's query (
new_message
) and typically appends it to the session history via theSessionService
. - Kick-off: Starts the event generation process by calling the main agent's execution method (e.g.,
agent_to_run.run_async(...)
). - Receive & Process: Waits for the agent logic to
yield
oremit
anEvent
. Upon receiving an event, the Runner promptly processes it. This involves:- Using configured
Services
(SessionService
,ArtifactService
,MemoryService
) to commit changes indicated inevent.actions
(likestate_delta
,artifact_delta
). - Performing other internal bookkeeping.
- Using configured
- Yield Upstream: Forwards the processed event onwards (e.g., to the calling application or UI for rendering).
- Iterate: Signals the agent logic that processing is complete for the yielded event, allowing it to resume and generate the next event.
Conceptual Runner Loop:
# Simplified view of Runner's main loop logic
def run(new_query, ...) -> Generator[Event]:
# 1. Append new_query to session event history (via SessionService)
session_service.append_event(session, Event(author='user', content=new_query))
# 2. Kick off event loop by calling the agent
agent_event_generator = agent_to_run.run_async(context)
async for event in agent_event_generator:
# 3. Process the generated event and commit changes
session_service.append_event(session, event) # Commits state/artifact deltas etc.
# memory_service.update_memory(...) # If applicable
# artifact_service might have already been called via context during agent run
# 4. Yield event for upstream processing (e.g., UI rendering)
yield event
# Runner implicitly signals agent generator can continue after yielding
// Simplified conceptual view of the Runner's main loop logic in Java.
public Flowable<Event> runConceptual(
Session session,
InvocationContext invocationContext,
Content newQuery
) {
// 1. Append new_query to session event history (via SessionService)
// ...
sessionService.appendEvent(session, userEvent).blockingGet();
// 2. Kick off event stream by calling the agent
Flowable<Event> agentEventStream = agentToRun.runAsync(invocationContext);
// 3. Process each generated event, commit changes, and "yield" or "emit"
return agentEventStream.map(event -> {
// This mutates the session object (adds event, applies stateDelta).
// The return value of appendEvent (a Single<Event>) is conceptually
// just the event itself after processing.
sessionService.appendEvent(session, event).blockingGet(); // Simplified blocking call
// memory_service.update_memory(...) // If applicable - conceptual
// artifact_service might have already been called via context during agent run
// 4. "Yield" event for upstream processing
// In RxJava, returning the event in map effectively yields it to the next operator or subscriber.
return event;
});
}
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:
- Execute: Runs its logic based on the current
InvocationContext
, including the session state as it was when execution resumed. - Yield: When the logic needs to communicate (send a message, call a tool, report a state change), it constructs an
Event
containing the relevant content and actions, and thenyield
s this event back to theRunner
. - Pause: Crucially, execution of the agent logic pauses immediately after the
yield
statement (orreturn
in RxJava). It waits for theRunner
to complete step 3 (processing and committing). - Resume: Only after the
Runner
has processed the yielded event does the agent logic resume execution from the statement immediately following theyield
. - See Updated State: Upon resumption, the agent logic can now reliably access the session state (
ctx.session.state
) reflecting the changes that were committed by theRunner
from the previously yielded event.
Conceptual Execution Logic:
# Simplified view of logic inside Agent.run_async, callbacks, or tools
# ... previous code runs based on current state ...
# 1. Determine a change or output is needed, construct the event
# Example: Updating state
update_data = {'field_1': 'value_2'}
event_with_state_change = Event(
author=self.name,
actions=EventActions(state_delta=update_data),
content=types.Content(parts=[types.Part(text="State updated.")])
# ... other event fields ...
)
# 2. Yield the event to the Runner for processing & commit
yield event_with_state_change
# <<<<<<<<<<<< EXECUTION PAUSES HERE >>>>>>>>>>>>
# <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>>
# 3. Resume execution ONLY after Runner is done processing the above event.
# Now, the state committed by the Runner is reliably reflected.
# Subsequent code can safely assume the change from the yielded event happened.
val = ctx.session.state['field_1']
# here `val` is guaranteed to be "value_2" (assuming Runner committed successfully)
print(f"Resumed execution. Value of field_1 is now: {val}")
# ... subsequent code continues ...
# Maybe yield another event later...
// Simplified view of logic inside Agent.runAsync, callbacks, or tools
// ... previous code runs based on current state ...
// 1. Determine a change or output is needed, construct the event
// Example: Updating state
ConcurrentMap<String, Object> updateData = new ConcurrentHashMap<>();
updateData.put("field_1", "value_2");
EventActions actions = EventActions.builder().stateDelta(updateData).build();
Content eventContent = Content.builder().parts(Part.fromText("State updated.")).build();
Event eventWithStateChange = Event.builder()