Zustandsverwaltung

Der Session-Zustand in ADK-Rust ermöglicht es Agents, Daten zu speichern und abzurufen, die über mehrere Konversationsrunden hinweg bestehen bleiben. Der Zustand wird mithilfe von Schlüsselpräfixen organisiert, die den Gültigkeitsbereich und die Lebensdauer der Daten bestimmen.

Überblick

Der Zustand wird als Schlüssel-Wert-Paare gespeichert, wobei:

  • Schlüssel Strings mit optionalen Präfixen sind
  • Werte JSON-Werte (serde_json::Value) sind

Das Präfixsystem ermöglicht verschiedene Gültigkeitsbereiche:

  • Session-scoped: Standard, an eine einzelne Session gebunden
  • User-scoped: Über alle Sessions eines Benutzers hinweg geteilt
  • App-scoped: Über alle Benutzer einer Anwendung hinweg geteilt
  • Temporary: Nach jeder Ausführung gelöscht

State Trait

Das State trait definiert die Schnittstelle für den State-Zugriff:

use serde_json::Value;
use std::collections::HashMap;

pub trait State: Send + Sync {
    /// Get a value by key
    fn get(&self, key: &str) -> Option<Value>;
    
    /// Set a value
    fn set(&mut self, key: String, value: Value);
    
    /// Get all state as a map
    fn all(&self) -> HashMap<String, Value>;
}

Es gibt auch ein ReadonlyState trait für den schreibgeschützten Zugriff:

pub trait ReadonlyState: Send + Sync {
    fn get(&self, key: &str) -> Option<Value>;
    fn all(&self) -> HashMap<String, Value>;
}

State-Schlüsselpräfixe

ADK-Rust verwendet drei Schlüsselpräfixe, um den State-Gültigkeitsbereich zu steuern:

PräfixKonstanteGültigkeitsbereich
app:KEY_PREFIX_APPGeteilt über alle Benutzer und Sessions
user:KEY_PREFIX_USERGeteilt über alle Sessions für einen Benutzer
temp:KEY_PREFIX_TEMPNach jeder Ausführung gelöscht
(keine)-Session-bezogen (Standard)

app: - Anwendungszustand

Der Zustand wird über alle Benutzer und Sessions einer Anwendung hinweg geteilt.

use adk_session::KEY_PREFIX_APP;

// KEY_PREFIX_APP = "app:"
let key = format!("{}settings", KEY_PREFIX_APP);  // "app:settings"

Anwendungsfälle:

  • Anwendungskonfiguration
  • Geteilte Ressourcen
  • Globale Zähler oder Statistiken

user: - Benutzerzustand

Der Zustand wird über alle Sessions für einen bestimmten Benutzer hinweg geteilt.

use adk_session::KEY_PREFIX_USER;

// KEY_PREFIX_USER = "user:"
let key = format!("{}preferences", KEY_PREFIX_USER);  // "user:preferences"

Anwendungsfälle:

  • Benutzereinstellungen
  • Benutzerprofildaten
  • Benutzerkontext über mehrere Sessions hinweg

temp: - Temporärer Zustand

Der Zustand, der nach jeder Ausführung gelöscht wird. Wird nicht gespeichert.

use adk_session::KEY_PREFIX_TEMP;

// KEY_PREFIX_TEMP = "temp:"
let key = format!("{}current_step", KEY_PREFIX_TEMP);  // "temp:current_step"

Anwendungsfälle:

  • Zwischenergebnisse von Berechnungen
  • Kontext der aktuellen Operation
  • Daten, die nicht dauerhaft gespeichert werden sollen

Kein Präfix - Session-Zustand

Schlüssel ohne Präfix sind Session-bezogen (Standardverhalten).

let key = "conversation_topic";  // Session-scoped

Anwendungsfälle:

  • Konversationskontext
  • Session-spezifische Daten
  • Zustand von Runde zu Runde

Initialen Zustand festlegen

Der Zustand kann bei der Erstellung einer Session initialisiert werden:

use adk_session::{InMemorySessionService, SessionService, CreateRequest, KEY_PREFIX_APP, KEY_PREFIX_USER};
use serde_json::json;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut initial_state = HashMap::new();

    // App-scoped state
    initial_state.insert(
        format!("{}version", KEY_PREFIX_APP),
        json!("1.0.0")
    );

    // User-scoped state
    initial_state.insert(
        format!("{}name", KEY_PREFIX_USER),
        json!("Alice")
    );

    // Session-scoped state
    initial_state.insert(
        "topic".to_string(),
        json!("Getting started")
    );

    let service = InMemorySessionService::new();
    let session = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "user_123".to_string(),
        session_id: None,
        state: initial_state,
    }).await?;
    
    Ok(())
}

Status lesen

Greifen Sie über die state()-Methode der Session auf den Status zu:

let state = session.state();

// Get a specific key
if let Some(value) = state.get("topic") {
    println!("Topic: {}", value);
}

// Get app-scoped state
if let Some(version) = state.get("app:version") {
    println!("App version: {}", version);
}

// Get all state
let all_state = state.all();
for (key, value) in all_state {
    println!("{}: {}", key, value);
}

Statusaktualisierungen über Events

Der Status wird typischerweise über Event-Aktionen aktualisiert. Wenn ein Event an eine Session angehängt wird, wird sein state_delta angewendet:

use adk_session::{Event, EventActions};
use serde_json::json;
use std::collections::HashMap;

let mut state_delta = HashMap::new();
state_delta.insert("counter".to_string(), json!(42));
state_delta.insert("user:last_seen".to_string(), json!("2024-01-15"));

let mut event = Event::new("invocation_123");
event.actions = EventActions {
    state_delta,
    ..Default::default()
};

// When this event is appended, state is updated
service.append_event(session.id(), event).await?;

Verhalten der Statusbereichsdefinition (State Scoping)

Der Session Service handhabt die Statusbereichsdefinition automatisch:

Bei der Session-Erstellung

  1. Extrahieren Sie mit app: präfixierte Schlüssel → Speichern Sie im App-Status
  2. Extrahieren Sie mit user: präfixierte Schlüssel → Speichern Sie im Benutzer-Status
  3. Verbleibende Schlüssel (außer temp:) → Speichern Sie im Session-Status
  4. Führen Sie alle Bereiche für die zurückgegebene Session zusammen

Beim Abrufen einer Session

  1. Laden Sie den App-Status für die Anwendung
  2. Laden Sie den Benutzer-Status für den Benutzer
  3. Laden Sie den Session-Status
  4. Führen Sie alle Bereiche zusammen (App → Benutzer → Session)

Beim Anhängen eines Events

  1. Extrahieren Sie das State-Delta aus dem Event
  2. Filtern Sie temp: Schlüssel heraus (nicht persistent)
  3. Wenden Sie app: Deltas auf den App-Status an
  4. Wenden Sie user: Deltas auf den Benutzer-Status an
  5. Wenden Sie die verbleibenden Deltas auf den Session-Status an

Vollständiges Beispiel

use adk_session::{
    InMemorySessionService, SessionService, CreateRequest, GetRequest,
    KEY_PREFIX_APP, KEY_PREFIX_USER,
};
use serde_json::json;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let service = InMemorySessionService::new();
    
    // Create first session with initial state
    let mut state1 = HashMap::new();
    state1.insert(format!("{}theme", KEY_PREFIX_APP), json!("dark"));
    state1.insert(format!("{}language", KEY_PREFIX_USER), json!("en"));
    state1.insert("context".to_string(), json!("session1"));
    
    let session1 = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "alice".to_string(),
        session_id: Some("s1".to_string()),
        state: state1,
    }).await?;
    
    // Create second session for same user
    let mut state2 = HashMap::new();
    state2.insert("context".to_string(), json!("session2"));
    
    let session2 = service.create(CreateRequest {
        app_name: "my_app".to_string(),
        user_id: "alice".to_string(),
        session_id: Some("s2".to_string()),
        state: state2,
    }).await?;
    
    // Session 2 inherits app and user state
    let s2_state = session2.state();
    
    // App state is shared
    assert_eq!(s2_state.get("app:theme"), Some(json!("dark")));
    
    // User state is shared
    assert_eq!(s2_state.get("user:language"), Some(json!("en")));
    
    // Session state is separate
    assert_eq!(s2_state.get("context"), Some(json!("session2")));
    
    println!("State scoping works correctly!");
    Ok(())
}

Anweisungs-Templating mit State

Statuswerte können unter Verwendung der {key}-Syntax in Agent-Anweisungen eingefügt werden:

use adk_rust::prelude::*;
use std::sync::Arc;

let agent = LlmAgentBuilder::new("personalized_assistant")
    .instruction("You are helping {user:name} with {topic}. Their preferred language is {user:language}.")
    .model(Arc::new(model))
    .build()?;

Wenn der Agent ausgeführt wird, werden {user:name}, {topic} und {user:language} durch Werte aus dem Session-Status ersetzt.

Best Practices

1. Geeignete Scopes verwenden

// ✅ Gut: Benutzereinstellungen im Benutzer-Scope
"user:theme"
"user:timezone"

// ✅ Gut: Session-spezifischer Kontext ohne Präfix
"current_task"
"conversation_summary"

// ✅ Gut: App-weite Einstellungen im App-Scope
"app:model_version"
"app:feature_flags"

// ❌ Schlecht: Benutzerdaten im Session-Scope (gehen zwischen Sessions verloren)
"user_preferences"  // Sollte "user:preferences" sein

2. Temporären Zustand für Zwischendaten verwenden

// ✅ Gut: Zwischenergebnisse im temporären Scope
"temp:search_results"
"temp:current_step"

// ❌ Schlecht: Zwischendaten unnötig persistent gespeichert
"search_results"  // Wird in der Datenbank gespeichert

3. Zustandsschlüssel konsistent halten

// ✅ Gut: Konsistente Namenskonvention
"user:preferences.theme"
"user:preferences.language"

// ❌ Schlecht: Inkonsistente Benennung
"user:theme"
"userLanguage"
"user-timezone"

Verwandt

  • Sessions - Übersicht über das Session-Management
  • Events - Event-Struktur und state_delta
  • LlmAgent - Anweisungs-Templating

Zurück: ← Sessions | Weiter: Callbacks →