Sessions & state
The Session data model, scoped state with app, user, and temp prefixes, the SessionService trait, and the available storage backends.
A Session is a conversation owned by an (app_name, user_id, id) triple: an append-only log of events plus a mutable state map. State keys carry scope prefixes — app:, user:, temp: — that decide where a value lives and who can see it.
The Session struct
| Field | Type | Meaning |
|---|---|---|
id | SessionId (String) | Unique within (app_name, user_id). |
app_name | String | Owning application. |
user_id | String | Owning user. |
state | State | Mutable key/value map; keys may carry a scope prefix. |
events | Vec<Event> | Every event ever appended, oldest first. |
last_update_time | f64 | Seconds since the epoch; bumped on every append. |
Session::new(app_name, user_id, id) builds an empty session. List APIs return SessionMeta instead — the same identity fields plus last_update_time, without events or state — wrapped in ListSessionsResponse { sessions: Vec<SessionMeta> }.
State, StateDelta, and scope prefixes
State (in adk_rs::core::state) is a transparent wrapper around an IndexMap<String, Value>; StateDelta is a plain IndexMap<String, Value> applied as a batch. The lexical prefix of a key determines its StateScope:
| Prefix | Scope | Behaviour |
|---|---|---|
app: | StateScope::App | Shared across all users and sessions of an app. |
user: | StateScope::User | Shared across all sessions of one (app, user). |
temp: | StateScope::Temp | Invocation-scoped. Visible to the live session during the run, stripped by State::trim_temp_keys before any event is persisted — it never survives a get_session. |
| (none) | StateScope::Session | Pinned to this one session. |
fn get(&self, key: &str) -> Option<&Value>- Borrowed lookup.
fn set(&mut self, key, value: Value) -> Option<Value>- Insert; returns the previous value.
fn apply(&mut self, delta: &StateDelta)- Merge a delta in insertion order.
fn partition_by_scope(delta: &StateDelta) -> (app, user, session, temp)- Split a delta into four
StateDeltas by prefix. Backends use this to routeapp:/user:keys to shared storage. fn trim_temp_keys(delta: &StateDelta) -> StateDelta- Drop every
temp:key — called before persisting events. fn StateScope::of(key: &str) -> StateScope- Derive the scope of a key from its prefix.
The SessionService trait
All persistence goes through adk_rs::core::SessionService. get_session takes a GetSessionConfig with two optional filters: num_recent_events (keep only the most recent N events) and after_timestamp (keep events with timestamp >= t).
async fn create_session(&self, app_name, user_id, state: Option<State>, id: Option<&str>) -> Result<Session>- Create a session; generates an id when
idisNone. Initial state is routed by scope. async fn get_session(&self, app_name, user_id, session_id, config: GetSessionConfig) -> Result<Option<Session>>- Fetch a session, optionally filtered.
async fn list_sessions(&self, app_name, user_id) -> Result<ListSessionsResponse>- List
SessionMetafor(app, user). async fn delete_session(&self, app_name, user_id, session_id) -> Result<()>- Delete a session.
async fn append_event(&self, session: &mut Session, event: Event) -> Result<Event>- Append + persist. The default impl calls
apply_event_to_session: temp propagation, delta trim + apply,last_update_timebump, push. async fn append_event_locked(&self, session_lock: &Arc<Mutex<Session>>, event: Event) -> Result<Event>- Race-free read-modify-write through the live
Arc<Mutex<Session>>— the path the runner uses. The default applies the event under one short critical section; durable backends override to add their own atomic write. async fn flush(&self) -> Result<()>- Optional flush hook for buffering backends. Default: no-op.
Backends
| Type | Module | Feature | Notes |
|---|---|---|---|
InMemorySessionService | adk_rs::services::mem | always on | Volatile DashMap store. Keeps dedicated app:/user: stores so scoped keys are visible across sessions; get_session overlays app + user + session state (session keys win). Appends merge the event into the stored session (append with id-dedup, then apply the state delta) rather than replacing it with the caller’s snapshot, so concurrent invocations holding independent snapshots of the same session don’t lose each other’s events. |
SqliteSessionService | adk_rs::services::sql | sqlite | SqliteSessionService::connect("sqlite::memory:") or sqlite:///path.db; runs its migrations on connect. |
PostgresSessionService | adk_rs::services::sql | postgres | Same API over postgres:// URLs. |
SqlSessionService | adk_rs::services::sql | sqlite / postgres | Compatibility alias; points at SQLite when both backend features are enabled. |
How state flows
- Tools accumulate writes in
ToolContext.state_delta, the per-call delta accumulator (see Function tools). - Events carry deltas in
EventActions.state_delta; anLlmAgentwithoutput_keystamps the final response text (or, withoutput_schema, the parsed JSON) into the final event’s delta. - Appending applies the delta:
apply_event_to_sessioncopiestemp:keys into the live state, trims them from the persisted delta, merges the rest intosession.state, and pushes the event. Scope-aware backends additionally routeapp:/user:keys to shared storage viaState::partition_by_scope.
Example: app and user scopes
use adk_rs::core::{Event, LlmResponse, SessionService};
use adk_rs::services::mem::InMemorySessionService;
use parking_lot::Mutex;
use std::sync::Arc;
let svc: Arc<dyn SessionService> = Arc::new(InMemorySessionService::new());
let s1 = svc.create_session("shop", "alice", None, None).await?;
let lock = Arc::new(Mutex::new(s1.clone()));
// Write one key per scope through an event.
let mut ev = Event::new("agent", LlmResponse::default());
ev.actions.state_delta.insert("app:catalog_rev".into(), serde_json::json!(42));
ev.actions.state_delta.insert("user:currency".into(), serde_json::json!("EUR"));
ev.actions.state_delta.insert("cart".into(), serde_json::json!(["sku-1"]));
ev.actions.state_delta.insert("temp:scratch".into(), serde_json::json!(true));
svc.append_event_locked(&lock, ev).await?;
// A different user's session sees app: but not user:/session keys.
let s2 = svc.create_session("shop", "bob", None, None).await?;
let bob = svc
.get_session("shop", "bob", &s2.id, Default::default())
.await?
.unwrap();
assert_eq!(bob.state.get("app:catalog_rev"), Some(&serde_json::json!(42)));
assert!(bob.state.get("user:currency").is_none());
assert!(bob.state.get("cart").is_none());
// temp: never survives persistence.
let alice = svc
.get_session("shop", "alice", &s1.id, Default::default())
.await?
.unwrap();
assert!(alice.state.get("temp:scratch").is_none());Related pages
- Events —
EventActions.state_deltaandapply_event_to_sessionin detail. - The Runner — who calls
append_event_locked. - Persistent sessions guide — SQLite/PostgreSQL in practice.
- Memory — long-term recall across sessions.