Skip to content

Migration Guide

The 2.0 release tightens the L0 (gemini-genai-rs) surface. Nothing changes in how a session behaves; what changes is how a few things are named and constructed. Everything below is mechanical.

Area1.x2.0
Model typeGeminiModel enum (Gemini2_0FlashLive, GeminiLive2_5FlashNativeAudio, Gemini2_0Flash, Custom(s))ModelId string newtype: ModelId::new("…"), "…".into(), ModelId::from_static("…"); constants ModelId::LIVE_2_5_FLASH_NATIVE_AUDIO (Vertex GA), ModelId::FLASH_2_5_NATIVE_AUDIO_LATEST (Google AI alias), ModelId::FLASH_LATEST (text). GeminiModel::Custom(s)ModelId::new(s)
Choosing a model.model(GeminiModel::…) was effectively requiredSessionConfig.model is Option<ModelId>; leave it unset and connect resolves ModelId::live_default(vertex) (honours GEMINI_LIVE_MODEL, then GEMINI_MODEL; prefixes bare names with models/). Text agents read GEMINI_TEXT_MODEL, then GEMINI_MODEL. Live::builder().model(..) and AgentBuilder::model(..) take a ModelId
Connectingconnect(config, TransportConfig::default())connect(config)
Connecting with optionsconnect_with(config, tc, transport, codec) / ConnectBuilder::new(config).build()ConnectBuilder::new(config).transport_config(tc).transport(t).codec(c).connect().await (connect_with is crate-private)
Shortcutsquick_connect(key, model), quick_connect_vertex(..)connect(SessionConfig::new(key)).await
REST client → LiveClient::live(model) returning a LiveSessionBuilderClient::live(Option<ModelId>) returns a ConnectBuilder: client.live(None).connect().await (tune with .configure(..))
Vertex credentialsString access token onlyAccessTokenStatic(String) or Dynamic(closure); SessionConfig::from_vertex(..), ApiEndpoint::vertex(..) and Live::builder().connect_vertex(..) accept Into<AccessToken> (a &str/String still works); ApiEndpoint::vertex_refreshing(project, location, || token()) and Client::from_vertex_refreshable(..) refresh on every (re)connect
Token accessSessionConfig::bearer_token() -> Option<&str>-> Option<String>, read on every connection attempt (None on Google AI); ApiEndpoint’s Debug output redacts secrets
Error eventSessionEvent::Error(String)SessionEvent::Error(SessionError) — print with {e} or match Codec(..), WebSocket(..), Timeout { phase, .. }, SetupFailed(..); SessionError gained Codec(CodecError)
GoAwaySessionEvent::GoAway(Option<String>), GoAwayPayload.time_left: Option<String>SessionEvent::GoAway(Option<Duration>), GoAwayPayload.time_left: Option<Duration>
Audio / video payloadsSessionCommand::SendAudio(Vec<u8>) / SendVideo(Vec<u8>); SessionWriter::send_audio(Vec<u8>)Bytes throughout: SessionCommand::SendAudio(Bytes), SessionWriter::send_audio(Bytes), SessionHandle::send_audio(impl Into<Bytes>); L1 LiveHandle/AgentSession::send_audio/send_video take impl Into<Bytes> (a Vec<u8> still works), InputEvent::Audio carries Bytes
SPSC ringSpscRing::new(cap) returned one shared, Sync object with write(&self) / read(&self); capacity rounded up to a power of twoSpscRing::channel(cap) returns (SpscProducer, SpscConsumer) — each Send, neither Sync, so one-producer/one-consumer is enforced by the type system (backed by rtrb); exact capacity; is_abandoned() on both halves. No unsafe remains in the workspace
#[tool] from the L2 preludeExpanded to ::gemini_adk_rs::…, so every crate using it needed gemini-adk-rs as a direct dependency (and its own schemars)The macro locates the runtime at expansion time (a direct gemini-adk-rs dependency under any name, else gemini-adk-fluent-rs’s re-export) and routes schemars through it too — use gemini_adk_fluent_rs::prelude::*; #[tool("…")] async fn … compiles with one dependency
Transcription settersenable_input_transcription(), enable_output_transcription()input_transcription(true), output_transcription(true)
Thoughtsinclude_thoughts()include_thoughts(true) (L2 Live::builder().include_thoughts() is unchanged)
Resumption setterssession_resumption(None), session_resumption(Some(h))session_resumption(), resume_from(h)
Audio format settersinput_audio(format, rate), output_audio(format, rate), fields output_audio_format, input_sample_rate, output_sample_rateRemoved — input_audio_format(fmt) remains; rates are fixed by the API (16 kHz in, 24 kHz out)
Capability checkssupports_async_tools()supports_async_tools() plus new supports_thinking(); Voice implements Display
SessionHandle fieldspublic command_tx, stateprivate; SessionHandle::resume_handle() added; event_sender() is for runtimes only
Prelude: wire envelopesSetupPayload, RealtimeInputPayload, ServerMessageWrapper, MediaChunk, ActivityStart/ActivityEnd, GoAwayPayload, TranscriptionPayload, … in prelude::*gemini_genai_rs::protocol::messages::* (ServerMessage stays in the prelude)
Prelude: REST & toolingClient, File/FileSource/FileState, TaskType, Candidate, ModelInfo, BatchJob, TelemetryConfig, FileWireRecorder, MemoryWireRecorder, WireRecorder, WireEntry, WireDirection, read_wire_log, ReplayTransport, ReplayControl in prelude::*gemini_genai_rs::Client, the REST modules, gemini_genai_rs::telemetry::TelemetryConfig, gemini_genai_rs::transport::… for recording/replay
Prelude additionsAccessToken, ModelId, TungsteniteError, VadState, BufferState
Removed typesPlatform enum, ToolDeclaration aliasUse ApiEndpoint (host/version) and FunctionDeclaration
Error typesTungsteniteError::WebSocket(tungstenite::Error); REST errors Auth(String)TungsteniteError::WebSocket boxes its source (no tungstenite::Error in the public API); GenerateError, TokensError, … carry Auth(AuthError); FilesError gained Decode(String)
L1 flow namesflow::ToolPolicy (the set of tools a flow reasons about; collided with tool::ToolPolicy), CompiledFlow::tool_policy(); flow::Mode (deprecated) / root FlowMode; flow::run(agent, mode) / root run_on_enter; root render_groundflow::ToolSurface, CompiledFlow::tool_surface(); one name Enforcement (root, flow, L2 prelude); flow::on_enter(agent, mode) (root on_enter); render_ground lives in flow only. Root and L2 prelude ToolPolicy now means tool::ToolPolicy (timeout/cache/confirm)
Orchestration namesorchestration::Mode, orchestration::call (root/L2 aliases AgentMode, call_agent)Defined as orchestration::AgentMode, orchestration::call_agent — same names everywhere
“Why is it blocked?”LiveHandle::why_blocked(), FlowMonitor::why_blocked(&state) (aliases)LiveHandle::explain(), FlowMonitor::explain(&state)
Blocking vs concurrentlive::callbacks::CallbackMode (callbacks) and live::reactor::EffectMode (effects)One live::ExecutionMode { Blocking, Concurrent } used by both
Closure aliaseslive::phase::StateGuard / workflow::GuardFn; live::phase::PhaseHook; live::BoxFuture; private orchestration::FetchFn / extract::FieldFetchFn / workflow::FunctionFngemini_adk_rs::StatePredicate; live::SessionHook (also used by temporal patterns); gemini_adk_rs::BoxFuture (re-exported from live); gemini_adk_rs::AsyncSourceFn<In = State> (AsyncSourceFn<Value> for extraction-kit field resolvers)
Phase history recordlive::phase::PhaseTransitionlive::TransitionRecord (Transition remains the declared edge)
Wire session phase callbackEventCallbacks::on_phase / PhaseCallback; L2 Live::on_phase(..)on_session_phase / SessionPhaseCallback; L2 Live::on_session_phase(..) (it is the transport phase, not the PhaseMachine)
Registry verbsWatcherRegistry::add, TemporalRegistry::addregister (matching ComputedRegistry::register)
ToolsetToolset::get_tools()Toolset::tools()
Builder verbsRunner::{with_middleware, with_plugin, with_state}; LiveSessionBuilder::with_state; L2 Live::with_stateRunner::{middleware, plugin, state}; LiveSessionBuilder::state; L2 Live::state
Text runnertext_runner::InMemoryRunnertext_runner::TextRunner
Removed modules/typesgemini_adk_rs::callback (BeforeToolCallback, AfterToolCallback, BeforeToolResult, ToolCallResult — unreferenced); public agents::generated (transpiler shadow types)Deleted / crate-private. Use Middleware hooks (before_tool/after_tool) instead of the callback aliases
State readsState::get<T> returns None for a present-but-mistyped valueget/get_key unchanged (lenient); new State::try_get<T> / try_get_key return Result<Option<T>, StateError> with StateError::WrongType { key, source }. ReadOnlyPrefixedState (the type of state.derived()) is exported from the crate root
Configuration errorsComputedRegistry::register panicked on a dependency cycle; Flow::validateResult<(), Vec<String>>, FlowBuilder::buildResult<Flow, Vec<String>>, PhaseMachine::validate / ComputedRegistry::validateResult<(), String>ComputedRegistry::registerResult<(), ConfigError> (never panics; a rejected registration leaves the registry unchanged — L2 Live::computed(..) defers the error to connect); all three validates and FlowBuilder::build return error::ConfigError { issues: Vec<String> } (Display joins with "; "; From<ConfigError> for AgentError)
Session persistence errorsSessionPersistence::{save, load, delete}Result<_, Box<dyn Error + Send + Sync>>Result<_, PersistenceError> (Io, Serde, NotFound, Backend(String))
Combinator middlewarewith_middleware_chain on LoopTextAgent, FallbackTextAgent, RouteTextAgent onlyAlso on Sequential, Parallel, Race, Timeout, MapOver, Dispatch, Join text agents (AgentStarted/AgentCompleted, LoopIteration, Timeout on_events); TapTextAgent documents why it has none
Default featuresgemini-adk-rs / gemini-adk-fluent-rs default = ["tls-native"] (text generation needed features = ["gemini-llm"])default = ["tls-native", "gemini-llm"] on both — GeminiLlm generates out of the box; --no-default-features still builds. voice-io stays opt-in and is the only way to get talk()
L2 Agent namespub type Agent = AgentBuilder; the L1 trait re-exported as AgentTrait (prelude, agents)Alias and AgentTrait removed: Agent in the L2 prelude/agents is the L1 trait; write AgentBuilder::new(..)
Registering toolsLive::tools(ToolDispatcher), Live::with_tools(ToolComposite), AgentBuilder::tools(ToolComposite), AgentBuilder::tool(Arc<dyn ToolFunction>)Live::tools(impl Into<ToolComposite>) and AgentBuilder::tools(impl Into<ToolComposite>) (a T:: composite or any single ToolFunctionFrom<F: ToolFunction>); Live::tool(f) / AgentBuilder::tool(f) take impl ToolFunction (a #[tool] fn’s value, a SimpleTool, an Arc<dyn ToolFunction> — L1 now implements ToolFunction for Arc<T>); Live::dispatcher(ToolDispatcher) is the escape hatch; with_tools removed
Building text agentsAgentBuilder::build(llm) -> Arc<dyn TextAgent>, Composable::compile(llm) -> Arc<dyn TextAgent>; T::mcp on a text agent was dropped with a warningBoth return Result<Arc<dyn TextAgent>, ConfigError>; a T::mcp entry is a build error naming the tool (only Live::connect performs the MCP handshake)
Unimplemented tool kindsT::a2a, T::openapi, T::search (connect-time “not yet implemented” errors), ToolCompositeEntry::{A2a, OpenApi, Search}, DeferredTool::{A2a, OpenApi, Search}Removed — they had no consumer. DeferredTool has one variant, Mcp
Showing state to the modelPhaseBuilder::with_state(&[..]), PhaseDefaults::with_state, P::with_stateshow_state on all three
Step-enter agentsLive::on_enter(step, agent, mode)Live::on_step_enter(step, agent, mode) (PhaseBuilder::on_enter(f) unchanged)
Conversation stage exitsConversation::done(guard), Conversation::done_overlay()complete_when(guard), end_overlay() (PhaseBuilder::done() and WatchBuilder::then(f) unchanged)
L2 boolean setterstranscription(bool, bool), session_resume(bool), affective_dialog(bool), proactive_audio(bool), prompt_on_enter(bool), tool_advisory(bool)Off-by-default capabilities are no-arg verbs: transcription() (both), input_transcription(), output_transcription(), session_resume(), affective_dialog(), proactive_audio(), prompt_on_enter(); the on-by-default advisory is disabled by no_tool_advisory(). Rule documented in live/config.rs
Context window compressionOff unless .context_compression(trigger, target) was called; a long call ended when the model’s context filledOn by default in Live::builder(): sliding window triggers at DEFAULT_COMPRESSION_TRIGGER_TOKENS (100k) and compresses to DEFAULT_COMPRESSION_TARGET_TOKENS (50k). .context_compression(..) still tunes it; .no_context_compression() turns it off. L0 SessionConfig and L1 LiveSessionBuilder are unchanged (off unless set)
Response latencytelemetry().snapshot() carried last/avg/min/max_response_latency_ms scalars onlytelemetry().latency() returns LatencyStats (last/min/max/mean, p50/p90/p99 over the last 256 turns, fixed-bucket histogram; Display is one log line); the snapshot keeps the scalars and adds response_latency. SessionTelemetry::record_audio_out / record_text_out return Option<Duration> (the turn’s latency, once). New state key session:last_response_latency_ms
Turn tracingNo spans; RUST_LOG showed unstructured linesEvery processor lane instruments its work with a turn span (id = turn number); RUST_LOG=gemini_adk_rs::live=debug shows VAD edges, response latency, tool calls with durations, flow denials, interruptions and turn boundaries per turn
_concurrent twinson_interrupted, on_turn_boundary, on_teardown blocking onlyon_interrupted_concurrent, on_turn_boundary_concurrent, on_teardown_concurrent added (L1: EventCallbacks::{on_interrupted_mode, on_turn_boundary_mode, on_teardown_concurrent}); transcript callbacks document their bool as is_final
Algebra dead entriesC::last/none/recent/rolling/fresh/compact/budget, G::rate_limit/max_turns/output/input, Loop::max, free spec::run_tests, Ctx::builder()Removed (C::window/empty/exclude_tools/truncate, G::custom, Loop::max_iterations, SessionSpec::run_tests()); Ctx::section(name) starts a context
Named combinatorsPipeline/FanOut/Loop::builder(name) and .describe(desc) discarded their argumentsStored as name/description fields (the name becomes the compiled agent’s name; both appear in Debug)
Map patternsmap_over(agent, concurrency) -> MapOver, map_reduce(mapper, reducer, concurrency) -> MapReduce (inert structs)map_over(agent, list_key) -> Composable (Composable::MapOver, compiles to MapOverTextAgent), map_reduce(mapper, reducer, list_key) -> Composable (a pipeline); MapReduce removed
Composite namesContextPolicyChain, StateTransformChain, GComposite/GGuard, EComposite/ECriterion, judge::Verdict; compose::middleware was #[doc(hidden)]ContextComposite, StateComposite, GuardComposite/GuardRule, EvalComposite/EvalCriterion, JudgeVerdict; all composites #[non_exhaustive]; compose::middleware documented
Composite parametersAgentBuilder::middleware(MiddlewareComposite) (also Live, Composable, Loop, Fallback); EvalSuite::criteria(&[&str]) + criteria_names: Vec<String>; E::persona(&'static str, &'static str), E::custom(&'static str, ..), LlmJudge::with_context(&'static str)impl Into<MiddlewareComposite> (From<Arc<dyn Middleware>>); criteria(impl Into<EvalComposite>) with field criteria: EvalComposite; impl Into<String> names/labels
A2A servera2a::A2AServera2a::A2aServer (matches A2aRegistry)
Mic chainvoice::MicProcessor (process), pump_processed(.., Vec<Box<dyn MicProcessor>>, ..), NoiseGate::new(threshold_rms, hang_frames)One trait: L1 InputAudioProcessor (process_frame), re-exported as voice::InputAudioProcessor; NoiseGate::new(threshold_rms, hold_frames)

Toolchain: the workspace is Rust edition 2024 with MSRV 1.93. gemini-genai-rs default features are ["live", "tls-native"] (tls-rustls is the alternative; vad, vad-wavekat, tracing-subscriber, and the REST features are opt-in; there is no opus feature). gemini-adk-rs no longer gates tracing behind tracing-support. The OTel endpoint is configured via TelemetryConfig.otel_endpoint / TelemetrySetup::with_otlp(..), not an environment variable.

This guide shows the same voice agent implemented at all three layers, so you can see what each layer adds and decide where to build.

Each layer removes a category of boilerplate:

What you writeL0 (gemini-genai-rs)L1 (gemini-adk-rs)L2 (gemini-adk-fluent-rs)
WebSocket connectionManualManualOne line
Event loop (select!)ManualAutomaticAutomatic
Tool dispatch + responseManualAutomaticAutomatic
State managementNoneBuilt-inBuilt-in
Phase transitionsManualPhaseMachine.phase() builder
Turn extractionNoneTurnExtractor.extract_turns::<T>()
TelemetryNoneSessionTelemetryAuto-collected
Instruction updatesManualinstruction_template.instruction_template()

The tradeoff is control. L0 gives you total control over every message. L2 handles the common patterns automatically but gives you less room to customize the event processing loop itself.

gemini_adk_fluent_rs::prelude is a kernel, not an everything-glob. It re-exports the ~40 types a typical application touches; everything else lives in a focused, discoverable submodule. Start with the prelude and reach for a submodule when the compiler says a name isn’t found.

In the kernel prelude:

  • Builders & composition: AgentBuilder, the S·C·T·P·M·A·E·G·Ctx algebra, operators (>> | * /) and patterns (until, review_loop, fan_out_merge, supervised, map_over), Live; the L1 Agent trait.
  • State: State, StateKey.
  • Flow (core): Flow, Guard, FlowMonitor, Enforcement, Verdict.
  • Tools (core): SimpleTool, TypedTool, ToolFunction, ToolDispatcher, ToolPolicy (the per-tool timeout/cache/confirm policy), #[tool], Extract, Frame.
  • LLM (core): BaseLlm, GeminiLlm.
  • Errors: AgentError, AgentResult, ConfigError, ToolError.
  • Callback contexts: CallbackContext, ToolContext.
  • Common Live types: LiveHandle, EventCallbacks, SteeringMode, ContextDelivery, RepairConfig, SessionPersistence, PersistenceError, FsPersistence, MemoryPersistence, TurnExtractor, ExtractionTrigger, LlmExtractor, SoftTurnDetector, TranscriptBuffer, TranscriptTurn.
  • Text-agent combinators (LlmTextAgent, SequentialTextAgent, …).
  • Build-time validation: check_contracts, ContractViolation, diagnose, infer_data_flow, AgentHarness, DataFlowEdge.
  • The L0 wire prelude (ModelId, AccessToken, Voice, Content, Part, Role, …).

Moved to submodules (import the named module):

Symbol(s)Home
Full Live control plane: LiveEvent, RuntimeContract, FieldPromotion, DeferredWriter, PendingContext, NeedsFulfillment, RepairAction, SessionSnapshot, LiveSessionBuilder, ExecutionMode, ToolExecutionMode, the *Contract types, …gemini_adk_fluent_rs::live
Text-agent runtime internalsgemini_adk_fluent_rs::text
Toolset, StaticToolset, ConfirmationProvider, Recognizer, RecordExtractor, FrameSpec, SlotSpec, …gemini_adk_fluent_rs::tools
SlotEvidence, prefix-scope helpersgemini_adk_fluent_rs::state
CompiledFlow, StepAction, Violation, FlowExplanation, ToolSurface, on_enter, render_ground, …gemini_adk_fluent_rs::flow
Agent (L1 Agent trait), call_agent, AgentMode, provenance, Resolver, agent_session::*gemini_adk_fluent_rs::agents
LlmRequest, LlmResponse, GeminiLlmParams, LlmRegistrygemini_adk_fluent_rs::llm
Conversation, ConversationSpec, CompiledConversation, FlowStack, …gemini_adk_fluent_rs::conversation
A2aServer, RemoteAgent, SkillDeclarationgemini_adk_fluent_rs::a2a
Scenario, Sim, SimStepgemini_adk_fluent_rs::simulation
Motif, CommitPolicy, Policygemini_adk_fluent_rs::{motifs, policy}
Raw L0 wire typesgemini_adk_fluent_rs::wire

Agent (in both prelude and agents) is the L1 trait; the L2 builder is AgentBuilder and has no Agent alias.

gemini-genai-rs default features contracted to ["live", "tls-native"]:

  • ML VAD is opt-in. The wavekat VAD model is no longer compiled by default. Enable vad-wavekat (available as a passthrough feature on gemini-adk-rs and gemini-adk-fluent-rs too). The lightweight energy VAD (vad) is still enabled by gemini-adk-rs.
  • TLS backend is selectable. tls-native (default) or tls-rustls; both the WebSocket transport and the optional REST client follow the choice. To go rustls: default-features = false, features = ["live", "tls-rustls"].
  • Tracing facade vs subscriber. The tracing facade is always compiled (spans/events are no-ops without a subscriber). TelemetryConfig::init’s console-logging machinery now sits behind the tracing-subscriber feature. The old tracing-support feature (on gemini-genai-rs and gemini-adk-rs) is a no-op kept only so existing manifests resolve — tracing is unconditional.
  • No more tokio/full. The published crates declare only the tokio features they use; applications control their own tokio feature set.

At L0, you work directly with SessionHandle, SessionEvent, and SessionCommand. You write your own event loop, dispatch tools manually, and manage all state yourself.

Here is a weather assistant with one tool:

use gemini_genai_rs::prelude::*;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Build session config with tool declaration
// (no `.model(..)`: connect resolves the platform's default Live model)
let config = SessionConfig::from_endpoint(
ApiEndpoint::google_ai(std::env::var("GEMINI_API_KEY")?)
)
.system_instruction("You are a weather assistant. Use get_weather for queries.")
.add_tool(Tool {
function_declarations: Some(vec![FunctionDeclaration {
name: "get_weather".into(),
description: "Get current weather for a city".into(),
parameters: Some(json!({
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" }
},
"required": ["city"]
})),
behavior: None,
}]),
..Default::default()
});
// 2. Connect
let handle = connect(config).await?;
handle.wait_for_phase(SessionPhase::Active).await;
// 3. Subscribe to events
let mut events = handle.subscribe();
// 4. Send a question
handle.send_text("What's the weather in Tokyo?").await?;
// 5. Manual event loop
while let Some(event) = recv_event(&mut events).await {
match event {
SessionEvent::TextDelta(text) => {
print!("{text}");
}
SessionEvent::TurnComplete => {
println!();
}
SessionEvent::ToolCall(calls) => {
// Manual tool dispatch
let mut responses = Vec::new();
for call in calls {
let result = match call.name.as_str() {
"get_weather" => {
let city = call.args.get("city")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
json!({ "city": city, "temp_c": 22, "condition": "sunny" })
}
_ => json!({ "error": "unknown tool" }),
};
responses.push(FunctionResponse {
name: call.name.clone(),
id: call.id.clone(),
response: result,
scheduling: None,
});
}
// Manual response send
handle.send_tool_response(responses).await?;
}
SessionEvent::Error(e) => eprintln!("session error: {e}"),
SessionEvent::Disconnected(_) => break,
_ => {}
}
}
Ok(())
}

Lines of code: ~70 What you manage: Event loop, tool dispatch, tool response serialization, phase waiting, all state.

At L1, LiveSessionBuilder handles the event loop, tool dispatch, and state. You register callbacks and a ToolDispatcher instead of writing a match over every event variant.

Same weather assistant:

use gemini_adk_rs::{SimpleTool, ToolDispatcher, LiveSessionBuilder};
use gemini_genai_rs::prelude::*;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create tool dispatcher
let mut dispatcher = ToolDispatcher::new();
dispatcher.register(SimpleTool::new(
"get_weather",
"Get current weather for a city",
None, // JSON Schema for parameters (None = no declared schema)
|args| async move {
let city = args["city"].as_str().unwrap_or("unknown");
Ok(json!({ "city": city, "temp_c": 22, "condition": "sunny" }))
},
));
// 2. Build session config
let config = SessionConfig::from_endpoint(
ApiEndpoint::google_ai(std::env::var("GEMINI_API_KEY")?)
)
.system_instruction("You are a weather assistant. Use get_weather for queries.");
// 3. Build callbacks
let mut callbacks = gemini_adk_rs::EventCallbacks::default();
callbacks.on_text = Some(Box::new(|t| print!("{t}")));
callbacks.on_turn_complete = Some(std::sync::Arc::new(|| {
Box::pin(async { println!() })
}));
// 4. Build and connect
let handle = LiveSessionBuilder::new(config)
.dispatcher(dispatcher)
.callbacks(callbacks)
.connect()
.await?;
// 5. Send a question (tools are auto-dispatched)
handle.send_text("What's the weather in Tokyo?").await?;
handle.done().await?;
Ok(())
}

Lines of code: ~40 What changed: No event loop. No manual tool dispatch. No manual send_tool_response. The ToolDispatcher handles tool calls automatically: it matches the function name, deserializes args, calls your function, and sends the response back to the model.

You also get State (via handle.state()), SessionTelemetry (via handle.telemetry()), and the full three-lane processor for free.

At L2, Live::builder() wraps everything in a chainable API. The same weather assistant:

use gemini_adk_fluent_rs::prelude::*;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let handle = Live::builder()
.instruction("You are a weather assistant. Use get_weather for queries.")
.tools(
T::simple("get_weather", "Get current weather for a city", |args| async move {
let city = args["city"].as_str().unwrap_or("unknown");
Ok(json!({ "city": city, "temp_c": 22, "condition": "sunny" }))
})
)
.on_text(|t| print!("{t}"))
.on_turn_complete(|| async { println!() })
.connect_google_ai(std::env::var("GEMINI_API_KEY")?)
.await?;
handle.send_text("What's the weather in Tokyo?").await?;
handle.done().await?;
Ok(())
}

Lines of code: ~20 What changed: No SessionConfig construction. No ToolDispatcher setup. No EventCallbacks struct. The builder infers everything:

  • .tools() creates and configures the ToolDispatcher
  • .instruction() sets the system instruction on the underlying SessionConfig
  • .connect_google_ai() builds the endpoint and connects in one call

Tools compose with the | operator:

let handle = Live::builder()
.instruction("You are a helpful assistant with access to tools.")
.tools(
T::simple("get_weather", "Get weather", |args| async move {
Ok(json!({ "temp_c": 22 }))
})
| T::simple("get_time", "Get current time", |_| async move {
Ok(json!({ "time": "14:30" }))
})
| T::google_search()
)
.on_text(|t| print!("{t}"))
.connect_google_ai(api_key)
.await?;
FeatureL0L1L2
WebSocket connectionconnect(config) (or ConnectBuilder::new(config)….connect())LiveSessionBuilder::new(config).connect()Live::builder().connect_*()
Event loopManual while let + matchAutomatic (three-lane processor)Automatic
Audio callbackManual match SessionEvent::AudioDatacallbacks.on_audio = Some(...).on_audio(|data| ...)
Tool dispatchManual match + response sendToolDispatcher auto-dispatch.tools() or .tools()
Tool declarationManual Tool + FunctionDeclarationAuto from ToolFunction::parameters()Auto from T::simple()
State managementNone (DIY)State with prefixesState with prefixes
Phase machineNone (DIY)PhaseMachine::new().phase("name").instruction().done()
WatchersNone (DIY)WatcherRegistry.watch("key").became_true().then()
Turn extractionNone (DIY)TurnExtractor trait.extract_turns::<T>(llm, prompt)
Instruction templatehandle.update_instruction()callbacks.instruction_template.instruction_template(|state| ...)
Greetinghandle.send_text() after connectbuilder.greeting("...").greeting("...")
TelemetryNoneSessionTelemetry auto-collectedAuto-collected
Session signalsNoneSessionSignals auto-collectedAuto-collected
Transcription toggleconfig.input_transcription(true)Same.transcription()
Computed stateNoneComputedRegistry.computed("key", &["deps"], |s| ...)
Temporal patternsNoneTemporalRegistry.when_sustained() / .when_rate()
Text agent toolsNoneTextAgentTool.agent_tool("name", "desc", agent)

L0 is the right choice when you need:

Custom transport: You want to route WebSocket frames through a proxy, use a Unix socket, or implement a custom reconnection strategy.

let handle = ConnectBuilder::new(config)
.transport(MyCustomTransport::new())
.codec(MyCustomCodec::new())
.connect()
.await?;

Non-standard event processing: Your application needs to process events in an order or pattern that does not fit the callback model (e.g., batching audio chunks before processing, custom priority queuing).

Embedding in a larger runtime: You are building your own agent framework and want wire-level access without the L1 runtime’s task spawning.

Minimal binary size: L0 has fewer dependencies than L1/L2.

L1 is the right choice when you need:

Programmatic callback registration: You build callbacks dynamically based on configuration or plugin systems, and the fluent builder syntax gets in the way.

let mut callbacks = EventCallbacks::default();
if config.enable_logging {
callbacks.on_text = Some(Box::new(|t| println!("{t}")));
}
if config.enable_audio {
callbacks.on_audio = Some(Box::new(move |data| {
audio_tx.send(data.clone()).ok();
}));
}

Custom PhaseMachine setup: You need to build the phase machine programmatically (e.g., phases loaded from a database at runtime).

Direct registry access: You want to add/configure ComputedRegistry, WatcherRegistry, or TemporalRegistry objects directly rather than through sub-builders.

The layers are designed to compose. Common patterns:

L0 config + L2 builder: Build a SessionConfig at L0 and pass it to the L2 builder. Useful when build_session_config() handles credential detection for you:

let config = build_session_config(Some("gemini-2.0-flash-live"))?
.voice(Voice::Kore)
.response_modalities(vec![Modality::Audio])
.system_instruction("You are a helpful assistant.");
let handle = Live::builder()
.on_audio(|data| { /* play */ })
.on_text(|t| print!("{t}"))
.connect(config)
.await?;

L1 types in L2 callbacks: The on_tool_call callback receives State (an L1 type) that you can query and mutate:

let handle = Live::builder()
.on_tool_call(|calls, state| async move {
// Promote tool context to state
state.set("last_tool", calls[0].name.clone());
None // auto-dispatch
})
.connect_google_ai(api_key)
.await?;

L0 handle from L2: Access the underlying SessionHandle for operations not exposed on LiveHandle:

let live_handle = Live::builder()
.connect_google_ai(api_key)
.await?;
// Access raw L0 handle
let session = live_handle.session();
let events = session.subscribe();
let phase = session.phase();

When migrating from L0 to L2:

  1. Replace SessionConfig::from_endpoint(...) with Live::builder().instruction() (the model stays optional at every layer)
  2. Replace manual Tool declarations with .dispatcher(dispatcher) or .tools(T::simple(...))
  3. Replace the while let Some(event) = recv_event(...) loop with callbacks
  4. Replace match SessionEvent::AudioData with .on_audio()
  5. Replace match SessionEvent::TextDelta with .on_text()
  6. Replace manual send_tool_response() with ToolDispatcher auto-dispatch
  7. Replace connect(config) / ConnectBuilder::new(config).connect() with .connect_google_ai() or .connect_vertex()
  8. Replace manual phase tracking with .phase("name").instruction().transition().done()
  9. Replace manual state HashMaps with .extract_turns::<T>() and handle.state()
  10. Remove the tokio::select! loop — the three-lane processor handles it