Gestión de Estado

El estado de sesión en adk-rust permite a los Agent almacenar y recuperar datos que persisten a través de los turnos de conversación. El estado se organiza utilizando prefijos de clave que determinan el ámbito y la vida útil de los datos.

Resumen

El estado se almacena como pares clave-valor donde:

  • Las claves son strings con prefijos opcionales
  • Los valores son valores JSON (serde_json::Value)

El sistema de prefijos permite diferentes niveles de alcance:

  • Ámbito de sesión: Predeterminado, vinculado a una única Session
  • Ámbito de usuario: Compartido en todas las Sessiones para un User
  • Ámbito de aplicación: Compartido en todos los User de una aplicación
  • Temporal: Se borra después de cada invocación

Trait State

El trait State define la interfaz para el acceso al estado:

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>;
}

También existe un trait ReadonlyState para acceso de solo lectura:

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

Prefijos de Clave de Estado

adk-rust utiliza tres prefijos de clave para controlar el alcance del estado:

PrefixConstantScope
app:KEY_PREFIX_APPCompartido entre todos los User y Sessiones
user:KEY_PREFIX_USERCompartido entre todas las Sessiones para un User
temp:KEY_PREFIX_TEMPSe borra después de cada invocación
(none)-Ámbito de sesión (predeterminado)

app: - Estado de la Aplicación

Estado compartido entre todos los User y Sessiones de una aplicación.

use adk_session::KEY_PREFIX_APP;

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

Casos de uso:

  • Configuración de la aplicación
  • Recursos compartidos
  • Contadores o estadísticas globales

user: - Estado del Usuario

Estado compartido entre todas las Sessiones para un User específico.

use adk_session::KEY_PREFIX_USER;

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

Casos de uso:

  • Preferencias de User
  • Datos de perfil de User
  • Contexto de User entre Sessiones

temp: - Estado Temporal

Estado que se borra después de cada invocación. No persiste.

use adk_session::KEY_PREFIX_TEMP;

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

Casos de uso:

  • Resultados de cómputos intermedios
  • Contexto de la operación actual
  • Datos que no deben persistir

Sin Prefijo - Estado de Sesión

Las claves sin prefijo tienen un alcance de Session (comportamiento predeterminado).

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

Casos de uso:

  • Contexto de la conversación
  • Datos específicos de la Session
  • Estado de turno por turno

Establecer el Estado Inicial

El estado se puede inicializar al crear una Session:

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(())
}

Lectura del estado

Acceda al estado a través del método state() de la sesión:

let state = session.state();

// Obtener una clave específica
if let Some(value) = state.get("topic") {
    println!("Topic: {}", value);
}

// Obtener estado con alcance de aplicación
if let Some(version) = state.get("app:version") {
    println!("App version: {}", version);
}

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

Actualizaciones de estado a través de eventos

El estado se actualiza típicamente a través de acciones de eventos. Cuando se añade un evento a una sesión, se aplica su state_delta:

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()
};

// Cuando se añade este evento, el estado se actualiza
service.append_event(session.id(), event).await?;

Comportamiento del alcance del estado

El servicio de sesión gestiona automáticamente el alcance del estado:

Al crear una sesión

  1. Extraer claves con prefijo app: → Almacenar en el estado de la aplicación
  2. Extraer claves con prefijo user: → Almacenar en el estado del usuario
  3. Claves restantes (excepto temp:) → Almacenar en el estado de la sesión
  4. Fusionar todos los alcances para la sesión devuelta

Al recuperar una sesión

  1. Cargar el estado de la aplicación
  2. Cargar el estado del usuario
  3. Cargar el estado de la sesión
  4. Fusionar todos los alcances (aplicación → usuario → sesión)

Al añadir un evento

  1. Extraer el state_delta del evento
  2. Filtrar claves temp: (no se persisten)
  3. Aplicar deltas app: al estado de la aplicación
  4. Aplicar deltas user: al estado del usuario
  5. Aplicar deltas restantes al estado de la sesión

Ejemplo completo

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();
    
    // Crear la primera sesión con estado inicial
    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?;
    
    // Crear la segunda sesión para el mismo usuario
    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?;
    
    // La Sesión 2 hereda el estado de la aplicación y del usuario
    let s2_state = session2.state();
    
    // El estado de la aplicación se comparte
    assert_eq!(s2_state.get("app:theme"), Some(json!("dark")));
    
    // El estado del usuario se comparte
    assert_eq!(s2_state.get("user:language"), Some(json!("en")));
    
    // El estado de la sesión es independiente
    assert_eq!(s2_state.get("context"), Some(json!("session2")));
    
    println!("State scoping works correctly!");
    Ok(())
}

Plantillas de instrucciones con estado

Los valores de estado se pueden inyectar en las instrucciones del Agent usando la sintaxis {key}:

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()?;

Cuando se ejecuta el Agent, {user:name}, {topic} y {user:language} se reemplazan con valores del estado de la sesión.

Mejores Prácticas

1. Usar Ámbitos Apropiados

// ✅ Bien: Preferencias de usuario en el ámbito de usuario
"user:theme"
"user:timezone"

// ✅ Bien: Contexto específico de la sesión sin prefijo
"current_task"
"conversation_summary"

// ✅ Bien: Configuración de la aplicación en el ámbito de la aplicación
"app:model_version"
"app:feature_flags"

// ❌ Mal: Datos de usuario en el ámbito de la sesión (se pierden entre sesiones)
"user_preferences"  // Debería ser "user:preferences"

2. Usar Estado Temporal para Datos Intermedios

// ✅ Bien: Resultados intermedios en el ámbito temporal
"temp:search_results"
"temp:current_step"

// ❌ Mal: Datos intermedios persistidos innecesariamente
"search_results"  // Se guardará en la base de datos

3. Mantener las Claves de Estado Consistentes

// ✅ Bien: Convención de nomenclatura consistente
"user:preferences.theme"
"user:preferences.language"

// ❌ Mal: Nomenclatura inconsistente
"user:theme"
"userLanguage"
"user-timezone"

Relacionado

  • Sessions - Visión general de la gestión de sesiones
  • Events - Estructura de eventos y state_delta
  • LlmAgent - Plantillas de instrucciones

Anterior: ← Sessions | Siguiente: Callbacks →