State Management
The State type is a shared, concurrent key-value store that flows through every
component of a live session: callbacks, tool calls, extractors, watchers, phases,
and computed variables. Values are stored as serde_json::Value and deserialized
on read, giving you type safety without a rigid schema.
Reading and Writing
Section titled “Reading and Writing”use gemini_adk_rs::State;
let state = State::new();
// Write any serializable valuestate.set("customer_name", "Alice");state.set("turn_count", 5u32);state.set("scores", vec![0.8, 0.9, 0.7]);
// Read with type inferencelet name: Option<String> = state.get("customer_name");let count: Option<u32> = state.get("turn_count");
// Read with a default fallbacklet count: u32 = state.get("turn_count").unwrap_or(0);
// `get` is lenient: a value that exists but is the wrong type reads as `None`.// `try_get` tells the two apart (`StateError::WrongType` names the key).let count: Option<u32> = state.try_get("turn_count")?;
// Check existenceif state.contains("customer_name") { // ...}
// Remove a keylet removed: Option<serde_json::Value> = state.remove("customer_name");Zero-Copy Reads
Section titled “Zero-Copy Reads”For hot paths where you want to avoid cloning, use with() to borrow the
underlying Value directly through the DashMap ref-guard:
// Borrow without cloninglet len = state.with("customer_name", |v| v.as_str().unwrap().len());
// Avoids: state.get_raw("key").map(|v| ...) which clones the ValuePrefix Scoping
Section titled “Prefix Scoping”Every state key belongs to a namespace defined by its prefix. Prefixes establish ownership, lifecycle, and read/write rules:
| Prefix | Who writes | Lifecycle | Example keys |
|---|---|---|---|
session: | SDK (SessionSignals) | Entire session | session:is_user_speaking |
derived: | SDK (ComputedRegistry) | Recomputed each turn | derived:sentiment_score |
turn: | SDK / User code | Cleared each turn | turn:transcript |
app: | User code | Entire session | app:order_total |
user: | User code | Entire session | user:name |
bg: | Background tasks | Entire session | bg:search_failed |
temp: | User code | No automatic lifecycle | temp:scratch |
Keys without a prefix (e.g. "customer_name") are valid and commonly used for
extractor-populated fields. The prefix convention is for organizational clarity,
not enforcement — the store does not reject unprefixed keys.
Scoped Accessors
Section titled “Scoped Accessors”Each prefix has a corresponding accessor that automatically prepends the prefix. This reduces typos and keeps code clean:
// These two are equivalent:state.set("app:flag", true);state.app().set("flag", true);
// Readinglet flag: Option<bool> = state.app().get("flag");
// Listing keys in a scope (prefix stripped from results)let app_keys: Vec<String> = state.app().keys();// Returns: ["flag"] not ["app:flag"]
// Other scoped accessorsstate.session().set("turn_count", 5);state.user().set("name", "Alice");state.turn().set("transcript", "hello");state.bg().set("task_id", "abc-123");state.temp().set("scratch", 42);
// derived() is read-only -- no set() or remove()let score: Option<f64> = state.derived().get("sentiment_score");Atomic Modify
Section titled “Atomic Modify”When multiple components read-modify-write the same key, use modify() to avoid
lost updates. It reads the current value (or a default), applies your function,
and writes the result back:
// Increment a counter (uses 0 if key doesn't exist yet)let new_count = state.modify("turn_count", 0u32, |n| n + 1);
// Toggle a booleanstate.modify("muted", false, |b| !b);
// Append to a running totalstate.modify("app:total_score", 0.0f64, |total| total + new_score);Note: modify() uses the same DashMap as get/set. It is atomic in the sense
that no other modify on the same key can interleave, but it is not a database
transaction.
Derived Fallback
Section titled “Derived Fallback”When you call state.get("risk") and the key "risk" does not exist, State
automatically checks "derived:risk" as a fallback. This means computed variables
are accessible without the prefix tax:
// ComputedRegistry writes to "derived:risk_level"// You can read it either way:let risk: Option<String> = state.get("derived:risk_level");let risk: Option<String> = state.get("risk_level"); // same result
// Direct key wins if both exist:state.set("score", 1.0);state.set("derived:score", 0.5);let score: f64 = state.get("score").unwrap(); // returns 1.0The fallback only triggers for unprefixed keys. state.get("app:risk") will
never fall back to "derived:risk".
StateKey — Type-Safe Keys
Section titled “StateKey — Type-Safe Keys”For keys used in multiple places, define a StateKey<T> constant to eliminate
string typos and enforce type consistency at compile time:
use gemini_adk_rs::state::StateKey;
const TURN_COUNT: StateKey<u32> = StateKey::new("session:turn_count");const SENTIMENT: StateKey<f64> = StateKey::new("derived:sentiment_score");const USER_NAME: StateKey<String> = StateKey::new("user:name");
// Usagestate.set_key(&TURN_COUNT, 5);let count: Option<u32> = state.get_key(&TURN_COUNT);
// Zero-copy borrow with typed keylet val = state.with_key(&TURN_COUNT, |v| v.as_u64().unwrap());
// Interoperable with raw string accessassert_eq!(state.get::<u32>("session:turn_count"), Some(5));Delta Tracking
Section titled “Delta Tracking”Delta tracking creates a transactional view of state. Writes go to a separate delta map that can be committed or rolled back:
let state = State::new();state.set("committed_key", "original");
// Create a delta-tracking view (shares the same backing store)let tracked = state.with_delta_tracking();
// Writes go to delta, not to the committed storetracked.set("new_key", "pending");assert!(tracked.contains("new_key")); // visible through trackedassert!(!state.contains("new_key")); // NOT visible in original
// Reads check delta first, then committed storelet val: String = tracked.get("committed_key").unwrap(); // reads from committed
// Commit: merges delta into the committed storetracked.commit();assert!(state.contains("new_key")); // now visible everywhere
// Or rollback: discards all pending changestracked.rollback();Useful for extractor pipelines where you want to validate extracted data before committing it to the shared state.
State in Tool Calls
Section titled “State in Tool Calls”The on_tool_call callback receives State so you can promote tool results into
state keys that watchers and phase transitions react to:
Live::builder() .on_tool_call(|calls, state| async move { // Let the dispatcher handle execution, but promote results None // returning None means "auto-dispatch" }) .before_tool_response(|responses, state| async move { // Inspect tool results and promote to state for r in &responses { if r.name == "verify_identity" { if r.response.get("verified") == Some(&json!(true)) { state.set("identity_verified", true); } } } responses })Auto-Tracked Session State
Section titled “Auto-Tracked Session State”SessionSignals automatically writes session-level signals to the session:
prefix. You never need to set these manually:
| Key | Type | Updated on |
|---|---|---|
session:is_user_speaking | bool | VoiceActivityStart/End |
session:is_model_speaking | bool | PhaseChanged(ModelSpeaking) |
session:interrupt_count | u64 | Each interruption |
session:error_count | u64 | Each error event |
session:last_error | String | Each error event |
session:silence_ms | u64 | Periodic flush (~100ms) |
session:elapsed_ms | u64 | Periodic flush (~100ms) |
session:remaining_budget_ms | u64 | Periodic flush (~100ms) |
session:go_away_received | bool | GoAway from server |
session:go_away_time_left_ms | u64 | GoAway with time left |
session:resumable | bool | SessionResumeHandle |
session:total_token_count | u32 | Each UsageMetadata event |
session:prompt_token_count | u32 | Each UsageMetadata event |
session:response_token_count | u32 | Each UsageMetadata event |
session:cached_content_token_count | u32 | Each UsageMetadata event |
session:thoughts_token_count | u32 | Each UsageMetadata event |
session:last_input_transcription | String | Each input transcription |
session:last_output_transcription | String | Each output transcription |
session:phase | String | PhaseChanged |
session:session_type | String | Connected / mark_video_sent |
session:disconnected | bool | Disconnected |
Read them anywhere:
let speaking: bool = state.session().get("is_user_speaking").unwrap_or(false);let elapsed: u64 = state.session().get("elapsed_ms").unwrap_or(0);let budget: u64 = state.session().get("remaining_budget_ms").unwrap_or(0);Utility Methods
Section titled “Utility Methods”// Snapshot specific keys (for diffing later)let snap = state.snapshot_values(&["score", "mood"]);
// Diff against a previous snapshotstate.set("score", 99);let diffs = state.diff_values(&snap, &["score", "mood"]);// diffs: [("score", old_value, new_value)]
// Pick a subset of keys into a new Statelet subset = state.pick(&["name", "score"]);
// Merge another state in (overwrites on conflict)state.merge(&other_state);
// Rename a keystate.rename("old_key", "new_key");
// Clear all keys with a given prefixstate.clear_prefix("turn:");See also
Section titled “See also”- State Watchers — reactive triggers and computed variables built on
State - Extraction Pipeline — how extracted values land in
derived:prefix - S.C.T.P.M.A Operator Algebra —
S::state transforms used in agent pipelines - cookbook 07 — state transforms