Runner

The execution runtime from adk-runner that orchestrates agent execution.

Overview

The Runner manages the complete lifecycle of agent execution:

  • Session management (create/retrieve sessions)
  • Memory injection (search and inject relevant memories)
  • Artifact handling (scoped artifact access)
  • Event streaming (process and forward events)
  • Agent transfers (handle multi-agent handoffs)
Rendering architecture…

Installation

[dependencies]
adk-runner = "2.0.0"

RunnerConfig

Configure the runner with required services:

use adk_runner::{Runner, RunnerConfig};
use adk_session::InMemorySessionService;
use adk_artifact::InMemoryArtifactService;
use std::sync::Arc;

let config = RunnerConfig {
    app_name: "my_app".to_string(),
    agent: Arc::new(my_agent),
    session_service: Arc::new(InMemorySessionService::new()),
    artifact_service: Some(Arc::new(InMemoryArtifactService::new())),
    memory_service: None,
    plugin_manager: None,
    run_config: None,
    compaction_config: None,
    context_cache_config: None,
    cache_capable: None,
    request_context: None,
    cancellation_token: None,
};

let runner = Runner::new(config)?;

Use the typestate builder to construct a Runner. The builder enforces required fields at compile time and defaults all optional fields, so adding new fields in future releases won't break your code:

use adk_runner::Runner;

let runner = Runner::builder()
    .app_name("my_app")
    .agent(Arc::new(my_agent))
    .session_service(Arc::new(InMemorySessionService::new()))
    // Optional fields β€” only set what you need
    .artifact_service(Arc::new(InMemoryArtifactService::new()))
    .build()?;

The builder requires three fields: app_name, agent, and session_service. Everything else is optional and has sensible defaults. The build() method is only available once all three required fields are set β€” missing one is a compile-time error, not a runtime one.

Configuration Fields

FieldTypeRequiredDescription
app_nameStringYesApplication identifier
agentArc<dyn Agent>YesRoot agent to execute
session_serviceArc<dyn SessionService>YesSession storage backend
artifact_serviceOption<Arc<dyn ArtifactService>>NoArtifact storage
memory_serviceOption<Arc<dyn Memory>>NoLong-term memory
plugin_managerOption<Arc<PluginManager>>NoPlugin lifecycle hooks
compaction_configOption<EventsCompactionConfig>NoContext compaction settings
run_configOption<RunConfig>NoExecution options
context_cache_configOption<ContextCacheConfig>NoRunner-level context cache lifecycle (experimental β€” see below)
cache_capableOption<Arc<dyn CacheCapable>>NoCache-capable model reference (experimental β€” see below)
request_contextOption<RequestContext>NoAuth middleware context
cancellation_tokenOption<CancellationToken>NoCooperative cancellation

Prompt caching

Caching is a provider-level concern and needs no Runner configuration. Each provider integration handles it where the request is assembled:

ProviderMechanismDefault
Anthropic / Bedrockcache_control breakpointson (AnthropicConfig::prompt_caching, opt out with with_prompt_caching(false))
OpenAIserver-side prompt caching, PromptCacheRetention for retentionautomatic
Geminiimplicit caching on 2.5/3.x β€” a shared prefix earns a discount with no code changeautomatic

Cache hits are observable without any extra wiring: the Gemini integration records cachedContentTokenCount on each response.

context_cache_config and cache_capable are experimental and should be left unset. They drive Gemini's explicit cachedContents API from the Runner. That API requires the cache to replace system_instruction, tools, and tool_config β€” sending a cache alongside any of them is rejected with INVALID_ARGUMENT. The Runner selects a cache before the agent resolves its tools, so it cannot assemble that request, and enabling these fields does not currently produce cache hits. Guaranteed (rather than best-effort) caching for Gemini belongs in the model integration, alongside how the other providers do it.

Running Agents

Execute an agent with user input:

use adk_core::{Content, SessionId, UserId};
use futures::StreamExt;

let user_content = Content::new("user").with_text("Hello!");

let mut stream = runner.run(
    UserId::new("user-123")?,
    SessionId::new("session-456")?,
    user_content,
).await?;

while let Some(event) = stream.next().await {
    match event {
        Ok(e) => {
            if let Some(content) = e.content() {
                for part in &content.parts {
                    if let Some(text) = part.text() {
                        print!("{}", text);
                    }
                }
            }
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

String Convenience Method

For simple call sites, run_str() accepts plain &str arguments and handles the newtype conversion internally:

let mut stream = runner.run_str(
    "user-123",
    "session-456",
    Content::new("user").with_text("Hello!"),
).await?;

If the string fails validation (empty, contains null bytes, or exceeds the length limit), run_str() returns an error before starting the agent loop. The existing run() method with typed UserId/SessionId remains unchanged.

Interruption and Run Isolation

A run is registered as soon as run() returns its stream, and deregistered when that stream is dropped β€” including when it is dropped without ever being polled.

MethodScope
interrupt(session_id)Cancels every in-flight run for that session ID, across apps and users
interrupt_identity(app_name, user_id, session_id)Cancels runs for one exact identity
active_runs()The identity of every in-flight run; a repeated identity means concurrent runs
active_session_ids()Deduplicated session IDs of in-flight runs
// Cancel one tenant's run without touching another that shares the session ID
let cancelled = runner.interrupt_identity("my-app", "user-1", "session-1");

A session ID is only unique within an app and user, so interrupt(session_id) is the broad form and interrupt_identity the precise one. Prefer interrupt_identity when a single Runner serves more than one app or user.

Runs are tracked by a unique run ID rather than by session ID, so two runs for the same identity are tracked separately and each deregisters only itself.

Persistence Is Identity-Bound

Every event the Runner persists β€” user turns, model responses, transfer events, plugin events, and compaction events β€” is written through SessionService::append_event_for_identity with the full (app_name, user_id, session_id) triple. A SessionService whose natural key is composite can therefore bind each event to its tenant, and can reject or ignore the raw-session-ID append_event path entirely.

Execution Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Runner.run()                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  1. Session Retrieval                       β”‚
β”‚                                                             β”‚
β”‚   SessionService.get(app_name, user_id, session_id)        β”‚
β”‚   β†’ Creates new session if not exists                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  2. Agent Selection                         β”‚
β”‚                                                             β”‚
β”‚   Check session state for active agent                      β”‚
β”‚   β†’ Use root agent or transferred agent                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                3. Context Creation                          β”‚
β”‚                                                             β”‚
β”‚   InvocationContext with:                                   β”‚
β”‚   - Session (mutable)                                       β”‚
β”‚   - Artifacts (scoped to session)                          β”‚
β”‚   - Memory (if configured)                                  β”‚
β”‚   - Run config                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  4. Agent Execution                         β”‚
β”‚                                                             β”‚
β”‚   agent.run(ctx) β†’ EventStream                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 5. Event Processing                         β”‚
β”‚                                                             β”‚
β”‚   For each event:                                           β”‚
β”‚   - Update session state                                    β”‚
β”‚   - Handle transfers                                        β”‚
β”‚   - Forward to caller                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  6. Session Save                            β”‚
β”‚                                                             β”‚
β”‚   SessionService.append_event(session, events)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

InvocationContext

The context provided to agents during execution:

pub trait InvocationContext: CallbackContext {
    /// The agent being executed
    fn agent(&self) -> Arc<dyn Agent>;
    
    /// Memory service (if configured)
    fn memory(&self) -> Option<Arc<dyn Memory>>;
    
    /// Current session
    fn session(&self) -> &dyn Session;
    
    /// Execution configuration
    fn run_config(&self) -> &RunConfig;
    
    /// Signal end of invocation
    fn end_invocation(&self);
    
    /// Check if invocation has ended
    fn ended(&self) -> bool;
}

RunConfig

Execution options:

pub struct RunConfig {
    /// Streaming mode for responses
    pub streaming_mode: StreamingMode,
    // ... other fields (tool_confirmation_decisions, cached_content, etc.)
}

ToolExecutionStrategy

Controls how multiple tool calls from a single LLM response are dispatched:

StrategyBehavior
Sequential (default)Execute tools one at a time in LLM-returned order
ParallelExecute all tools concurrently; the caller owns safety
AutoExecute the safe read-only subset concurrently, then all remaining calls sequentially

Set per-agent via LlmAgentBuilder:

use adk_core::ToolExecutionStrategy;

let agent = LlmAgentBuilder::new("fast_agent")
    .model(model)
    .tool_execution_strategy(ToolExecutionStrategy::Auto)
    .tool(Arc::new(
        search_tool
            .with_read_only(true)
            .with_concurrency_safe(true),
    ))
    .tool(Arc::new(save_tool)) // runs after the concurrent safe subset
    .build()?;

In Auto mode, the dispatch loop queries both is_read_only() and is_concurrency_safe(). Calls whose selected tools return true for both methods run concurrently first; all remaining calls then run sequentially. Parallel bypasses these metadata checks as an explicit caller override. Results are always reassembled in the original LLM-returned order regardless of strategy. Failed tools produce a JSON error response without aborting the batch.

pub enum StreamingMode {
    /// No streaming, return complete response
    None,
    /// Server-Sent Events (default)
    SSE,
    /// Bidirectional streaming (realtime)
    Bidi,
}

Agent Transfers

The Runner handles multi-agent transfers automatically:

// In an agent's tool or callback
if should_transfer {
    // Set transfer in event actions
    ctx.set_actions(EventActions {
        transfer_to_agent: Some("specialist_agent".to_string()),
        ..Default::default()
    });
}

The Runner will:

  1. Detect the transfer request in the event
  2. Find the target agent in sub_agents
  3. Update session state with new active agent
  4. Continue execution with the new agent

Context Compaction

For long-running sessions, enable automatic context compaction to keep the LLM context window bounded:

use adk_runner::{Runner, RunnerConfig, EventsCompactionConfig};
use adk_agent::LlmEventSummarizer;
use std::sync::Arc;

let summarizer = LlmEventSummarizer::new(model.clone());

let config = RunnerConfig {
    // ... other fields ...
    compaction_config: Some(EventsCompactionConfig {
        compaction_interval: 3,  // Compact every 3 invocations
        overlap_size: 1,         // Keep 1 event overlap for continuity
        summarizer: Arc::new(summarizer),
    }),
    // ...
};

When compaction triggers, older events are replaced by a summary event. conversation_history() automatically uses the summary instead of the original events.

See Context Compaction for full documentation.

Integration with Launcher

The Launcher uses Runner internally:

// Launcher creates Runner with default services
Launcher::new(agent)
    .app_name("my_app")
    .run()
    .await?;

// Equivalent to using the builder:
let runner = Runner::builder()
    .app_name("my_app")
    .agent(agent)
    .session_service(Arc::new(InMemorySessionService::new()))
    .build()?;

Custom Runner Usage

For advanced scenarios, use Runner directly:

use adk_runner::Runner;

// Production configuration using the builder
let runner = Runner::builder()
    .app_name("production_app")
    .agent(my_agent)
    .session_service(Arc::new(SqliteSessionService::new(db_pool)))
    .artifact_service(Arc::new(S3ArtifactService::new(s3_client)))
    .memory_service(Arc::new(QdrantMemoryService::new(qdrant_client)))
    .build()?;

// Use in HTTP handler with run_str() for convenience
async fn chat_handler(runner: &Runner, request: ChatRequest) -> Response {
    let stream = runner.run_str(
        &request.user_id,
        &request.session_id,
        request.content,
    ).await?;
    
    // Stream events to client
    Response::sse(stream)
}

Previous: ← Core Types | Next: Launcher β†’