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

FieldTypeMeaning
idSessionId (String)Unique within (app_name, user_id).
app_nameStringOwning application.
user_idStringOwning user.
stateStateMutable key/value map; keys may carry a scope prefix.
eventsVec<Event>Every event ever appended, oldest first.
last_update_timef64Seconds 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:

PrefixScopeBehaviour
app:StateScope::AppShared across all users and sessions of an app.
user:StateScope::UserShared across all sessions of one (app, user).
temp:StateScope::TempInvocation-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::SessionPinned 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 route app:/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 id is None. 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 SessionMeta for (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_time bump, 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

TypeModuleFeatureNotes
InMemorySessionServiceadk_rs::services::memalways onVolatile 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.
SqliteSessionServiceadk_rs::services::sqlsqliteSqliteSessionService::connect("sqlite::memory:") or sqlite:///path.db; runs its migrations on connect.
PostgresSessionServiceadk_rs::services::sqlpostgresSame API over postgres:// URLs.
SqlSessionServiceadk_rs::services::sqlsqlite / postgresCompatibility alias; points at SQLite when both backend features are enabled.

How state flows

  1. Tools accumulate writes in ToolContext.state_delta, the per-call delta accumulator (see Function tools).
  2. Events carry deltas in EventActions.state_delta; an LlmAgent with output_key stamps the final response text (or, with output_schema, the parsed JSON) into the final event’s delta.
  3. Appending applies the delta: apply_event_to_session copies temp: keys into the live state, trims them from the persisted delta, merges the rest into session.state, and pushes the event. Scope-aware backends additionally route app:/user: keys to shared storage via State::partition_by_scope.

Example: app and user scopes

Scoped state across sessionsrust
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());