use super::AgentEngineState;
use adk_core::{AdkError, Content, ErrorCategory, ErrorComponent, Result, SessionId, UserId};
use serde::Deserialize;
use serde_json::{Map, Value, json};
use std::collections::HashMap;
use std::str::FromStr;
const MAX_EVENTS: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiMode {
Sync,
Async,
Stream,
AsyncStream,
}
impl ApiMode {
pub fn as_str(&self) -> &'static str {
match self {
ApiMode::Sync => "",
ApiMode::Async => "async",
ApiMode::Stream => "stream",
ApiMode::AsyncStream => "async_stream",
}
}
pub fn is_streaming(&self) -> bool {
match self {
ApiMode::Sync | ApiMode::Async => false,
ApiMode::Stream | ApiMode::AsyncStream => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClassMethod {
CreateSession,
AsyncCreateSession,
GetSession,
AsyncGetSession,
ListSessions,
AsyncListSessions,
DeleteSession,
AsyncDeleteSession,
StreamQuery,
AsyncStreamQuery,
StreamingAgentRunWithEvents,
AsyncAddSessionToMemory,
AsyncSearchMemory,
RegisterOperations,
}
impl ClassMethod {
pub const ALL: [ClassMethod; 14] = [
ClassMethod::CreateSession,
ClassMethod::AsyncCreateSession,
ClassMethod::GetSession,
ClassMethod::AsyncGetSession,
ClassMethod::ListSessions,
ClassMethod::AsyncListSessions,
ClassMethod::DeleteSession,
ClassMethod::AsyncDeleteSession,
ClassMethod::StreamQuery,
ClassMethod::AsyncStreamQuery,
ClassMethod::StreamingAgentRunWithEvents,
ClassMethod::AsyncAddSessionToMemory,
ClassMethod::AsyncSearchMemory,
ClassMethod::RegisterOperations,
];
pub fn as_str(&self) -> &'static str {
match self {
ClassMethod::CreateSession => "create_session",
ClassMethod::AsyncCreateSession => "async_create_session",
ClassMethod::GetSession => "get_session",
ClassMethod::AsyncGetSession => "async_get_session",
ClassMethod::ListSessions => "list_sessions",
ClassMethod::AsyncListSessions => "async_list_sessions",
ClassMethod::DeleteSession => "delete_session",
ClassMethod::AsyncDeleteSession => "async_delete_session",
ClassMethod::StreamQuery => "stream_query",
ClassMethod::AsyncStreamQuery => "async_stream_query",
ClassMethod::StreamingAgentRunWithEvents => "streaming_agent_run_with_events",
ClassMethod::AsyncAddSessionToMemory => "async_add_session_to_memory",
ClassMethod::AsyncSearchMemory => "async_search_memory",
ClassMethod::RegisterOperations => "register_operations",
}
}
pub fn api_mode(&self) -> ApiMode {
match self {
ClassMethod::CreateSession
| ClassMethod::GetSession
| ClassMethod::ListSessions
| ClassMethod::DeleteSession
| ClassMethod::RegisterOperations => ApiMode::Sync,
ClassMethod::AsyncCreateSession
| ClassMethod::AsyncGetSession
| ClassMethod::AsyncListSessions
| ClassMethod::AsyncDeleteSession
| ClassMethod::AsyncAddSessionToMemory
| ClassMethod::AsyncSearchMemory => ApiMode::Async,
ClassMethod::StreamQuery => ApiMode::Stream,
ClassMethod::AsyncStreamQuery | ClassMethod::StreamingAgentRunWithEvents => {
ApiMode::AsyncStream
}
}
}
}
impl FromStr for ClassMethod {
type Err = AdkError;
fn from_str(s: &str) -> Result<Self> {
match s {
"create_session" => Ok(ClassMethod::CreateSession),
"async_create_session" => Ok(ClassMethod::AsyncCreateSession),
"get_session" => Ok(ClassMethod::GetSession),
"async_get_session" => Ok(ClassMethod::AsyncGetSession),
"list_sessions" => Ok(ClassMethod::ListSessions),
"async_list_sessions" => Ok(ClassMethod::AsyncListSessions),
"delete_session" => Ok(ClassMethod::DeleteSession),
"async_delete_session" => Ok(ClassMethod::AsyncDeleteSession),
"stream_query" => Ok(ClassMethod::StreamQuery),
"async_stream_query" => Ok(ClassMethod::AsyncStreamQuery),
"streaming_agent_run_with_events" => Ok(ClassMethod::StreamingAgentRunWithEvents),
"async_add_session_to_memory" => Ok(ClassMethod::AsyncAddSessionToMemory),
"async_search_memory" => Ok(ClassMethod::AsyncSearchMemory),
"register_operations" => Ok(ClassMethod::RegisterOperations),
unknown => Err(AdkError::new(
ErrorComponent::Server,
ErrorCategory::InvalidInput,
"agent_engine.unknown_class_method",
format!(
"unknown class_method '{unknown}'; call register_operations for the \
supported operation set"
),
)),
}
}
}
#[derive(Debug, Deserialize)]
pub struct CreateSessionInput {
pub user_id: String,
pub session_id: Option<String>,
pub state: Option<Map<String, Value>>,
}
#[derive(Debug, Deserialize)]
pub struct GetSessionInput {
pub user_id: String,
pub session_id: String,
}
#[derive(Debug, Deserialize)]
pub struct ListSessionsInput {
pub user_id: String,
}
#[derive(Debug, Deserialize)]
pub struct DeleteSessionInput {
pub user_id: String,
pub session_id: String,
}
#[derive(Debug, Deserialize)]
pub struct StreamQueryInput {
pub user_id: String,
pub session_id: Option<String>,
pub message: Value,
}
#[derive(Debug, Deserialize)]
pub struct StreamingAgentRunWithEventsInput {
pub request_json: String,
}
#[derive(Debug, Deserialize)]
pub struct AgentRunRequest {
#[serde(default, alias = "appName")]
pub app_name: Option<String>,
#[serde(alias = "userId")]
pub user_id: String,
#[serde(alias = "sessionId")]
pub session_id: String,
#[serde(alias = "newMessage")]
pub new_message: Content,
#[serde(default)]
pub streaming: bool,
#[serde(default, alias = "stateDelta")]
pub state_delta: Option<Map<String, Value>>,
}
#[derive(Debug, Deserialize)]
pub struct AddSessionToMemoryInput {
pub user_id: String,
pub session_id: String,
}
#[derive(Debug, Deserialize)]
pub struct SearchMemoryInput {
pub user_id: String,
pub query: String,
}
pub(crate) fn typed_input<T: serde::de::DeserializeOwned>(input: Option<Value>) -> Result<T> {
let value = input.unwrap_or_else(|| Value::Object(Map::new()));
serde_json::from_value(value).map_err(|err| {
AdkError::new(
ErrorComponent::Server,
ErrorCategory::InvalidInput,
"agent_engine.invalid_input",
format!("input does not match the class_method's schema: {err}"),
)
})
}
pub(crate) fn session_to_value(session: &dyn adk_session::Session) -> Value {
let events: Vec<Value> = session
.events()
.all()
.into_iter()
.take(MAX_EVENTS)
.map(|event| serde_json::to_value(event).unwrap_or(Value::Null))
.collect();
json!({
"id": session.id(),
"app_name": session.app_name(),
"user_id": session.user_id(),
"state": session.state().all(),
"events": events,
"last_update_time": session.last_update_time().timestamp_millis() as f64 / 1000.0,
})
}
pub(crate) async fn handle_create_session(
state: &AgentEngineState,
input: CreateSessionInput,
) -> Result<Value> {
let initial_state: HashMap<String, Value> =
input.state.unwrap_or_default().into_iter().collect();
let session = state
.session_service()
.create(adk_session::CreateRequest {
app_name: state.app_name().to_string(),
user_id: input.user_id,
session_id: input.session_id,
state: initial_state,
})
.await?;
Ok(session_to_value(session.as_ref()))
}
pub(crate) async fn handle_get_session(
state: &AgentEngineState,
input: GetSessionInput,
) -> Result<Value> {
let session = state
.session_service()
.get(adk_session::GetRequest {
app_name: state.app_name().to_string(),
user_id: input.user_id,
session_id: input.session_id,
num_recent_events: None,
after: None,
})
.await?;
Ok(session_to_value(session.as_ref()))
}
pub(crate) async fn handle_list_sessions(
state: &AgentEngineState,
input: ListSessionsInput,
) -> Result<Value> {
let sessions = state
.session_service()
.list(adk_session::ListRequest {
app_name: state.app_name().to_string(),
user_id: input.user_id,
limit: None,
offset: None,
})
.await?;
let sessions: Vec<Value> =
sessions.iter().map(|session| session_to_value(session.as_ref())).collect();
Ok(json!({ "sessions": sessions }))
}
pub(crate) async fn handle_delete_session(
state: &AgentEngineState,
input: DeleteSessionInput,
) -> Result<Value> {
state
.session_service()
.delete(adk_session::DeleteRequest {
app_name: state.app_name().to_string(),
user_id: input.user_id,
session_id: input.session_id,
})
.await?;
Ok(Value::Null)
}
pub(crate) fn handle_register_operations() -> Value {
let mut map = Map::new();
for mode in [ApiMode::Sync, ApiMode::Async, ApiMode::Stream, ApiMode::AsyncStream] {
let names: Vec<Value> = ClassMethod::ALL
.iter()
.filter(|method| method.api_mode() == mode)
.map(|method| Value::String(method.as_str().to_string()))
.collect();
map.insert(mode.as_str().to_string(), Value::Array(names));
}
Value::Object(map)
}
fn memory_unavailable() -> AdkError {
AdkError::new(
ErrorComponent::Server,
ErrorCategory::Unsupported,
"agent_engine.memory_unavailable",
"no memory service is configured on this engine; configure one via \
AgentEngineState::with_memory_service (Memory Bank support arrives with the \
vertex memory backend)",
)
}
pub(crate) async fn handle_add_session_to_memory(
state: &AgentEngineState,
input: AddSessionToMemoryInput,
) -> Result<Value> {
let Some(memory_service) = state.memory_service() else {
return Err(memory_unavailable());
};
let session = state
.session_service()
.get(adk_session::GetRequest {
app_name: state.app_name().to_string(),
user_id: input.user_id.clone(),
session_id: input.session_id.clone(),
num_recent_events: None,
after: None,
})
.await?;
let entries: Vec<adk_memory::MemoryEntry> = session
.events()
.all()
.into_iter()
.filter_map(|event| {
event.llm_response.content.clone().map(|content| adk_memory::MemoryEntry {
content,
author: event.author.clone(),
timestamp: event.timestamp,
})
})
.collect();
memory_service
.add_session(state.app_name(), &input.user_id, &input.session_id, entries)
.await?;
Ok(Value::Null)
}
pub(crate) async fn handle_search_memory(
state: &AgentEngineState,
input: SearchMemoryInput,
) -> Result<Value> {
let Some(memory_service) = state.memory_service() else {
return Err(memory_unavailable());
};
let response = memory_service
.search(adk_memory::SearchRequest {
query: input.query,
user_id: input.user_id,
app_name: state.app_name().to_string(),
limit: None,
min_score: None,
project_id: None,
})
.await?;
let memories: Vec<Value> = response
.memories
.into_iter()
.map(|entry| {
json!({
"content": entry.content,
"author": entry.author,
"timestamp": entry.timestamp,
})
})
.collect();
Ok(json!({ "memories": memories }))
}
pub(crate) fn message_to_content(message: Value) -> Result<Content> {
match message {
Value::String(text) => Ok(Content::new("user").with_text(text)),
Value::Object(_) => serde_json::from_value(message).map_err(|err| {
AdkError::new(
ErrorComponent::Server,
ErrorCategory::InvalidInput,
"agent_engine.invalid_message",
format!("message object is not a valid Content: {err}"),
)
}),
other => Err(AdkError::new(
ErrorComponent::Server,
ErrorCategory::InvalidInput,
"agent_engine.invalid_message",
format!("message must be a string or a Content object, got {other}"),
)),
}
}
pub(crate) async fn resolve_session(
state: &AgentEngineState,
user_id: &str,
session_id: Option<String>,
initial_state: HashMap<String, Value>,
) -> Result<(String, bool)> {
let session_id = match session_id {
Some(id) => id,
None => uuid::Uuid::new_v4().to_string(),
};
let existing = state
.session_service()
.get(adk_session::GetRequest {
app_name: state.app_name().to_string(),
user_id: user_id.to_string(),
session_id: session_id.clone(),
num_recent_events: None,
after: None,
})
.await;
match existing {
Ok(_) => Ok((session_id, false)),
Err(err) if err.is_not_found() => {
state
.session_service()
.create(adk_session::CreateRequest {
app_name: state.app_name().to_string(),
user_id: user_id.to_string(),
session_id: Some(session_id.clone()),
state: initial_state,
})
.await?;
Ok((session_id, true))
}
Err(err) => Err(err),
}
}
pub(crate) async fn apply_state_delta(
state: &AgentEngineState,
user_id: &str,
session_id: &str,
state_delta: HashMap<String, Value>,
) -> Result<()> {
if state_delta.is_empty() {
return Ok(());
}
let identity = adk_core::AdkIdentity {
app_name: adk_core::AppName::try_from(state.app_name())?,
user_id: adk_core::UserId::try_from(user_id)?,
session_id: adk_core::SessionId::try_from(session_id)?,
};
let mut event = adk_core::Event::new(format!("agent-engine-input-{}", uuid::Uuid::new_v4()));
event.author = "agent_engine_dispatch".to_string();
event.actions.state_delta = state_delta;
state
.session_service()
.append_event_for_identity(adk_session::AppendEventRequest { identity, event })
.await
}
pub(crate) fn typed_identity(user_id: &str, session_id: &str) -> Result<(UserId, SessionId)> {
let user_id = UserId::try_from(user_id)?;
let session_id = SessionId::try_from(session_id)?;
Ok((user_id, session_id))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn class_method_round_trips_through_from_str() {
for method in ClassMethod::ALL {
assert_eq!(ClassMethod::from_str(method.as_str()).unwrap(), method);
}
}
#[test]
fn unknown_class_method_is_invalid_input() {
let err = ClassMethod::from_str("does_not_exist").unwrap_err();
assert_eq!(err.http_status_code(), 400);
}
#[test]
fn register_operations_covers_every_variant_exactly_once() {
let map = handle_register_operations();
let advertised: Vec<&str> = map
.as_object()
.unwrap()
.values()
.flat_map(|names| names.as_array().unwrap())
.map(|name| name.as_str().unwrap())
.collect();
let expected: Vec<&str> = ClassMethod::ALL.iter().map(ClassMethod::as_str).collect();
assert_eq!(advertised.len(), expected.len());
for name in expected {
assert_eq!(advertised.iter().filter(|n| **n == name).count(), 1, "{name}");
}
}
#[test]
fn api_modes_partition_the_streaming_endpoints() {
for method in ClassMethod::ALL {
let streaming = matches!(
method,
ClassMethod::StreamQuery
| ClassMethod::AsyncStreamQuery
| ClassMethod::StreamingAgentRunWithEvents
);
assert_eq!(method.api_mode().is_streaming(), streaming, "{}", method.as_str());
}
}
#[test]
fn agent_run_request_accepts_both_casings() {
let camel: AgentRunRequest = serde_json::from_str(
r#"{"appName":"a","userId":"u","sessionId":"s","newMessage":{"role":"user","parts":[{"text":"hi"}]}}"#,
)
.unwrap();
let snake: AgentRunRequest = serde_json::from_str(
r#"{"app_name":"a","user_id":"u","session_id":"s","new_message":{"role":"user","parts":[{"text":"hi"}]}}"#,
)
.unwrap();
assert_eq!(camel.user_id, snake.user_id);
assert_eq!(camel.session_id, snake.session_id);
assert_eq!(
serde_json::to_value(&camel.new_message).unwrap(),
serde_json::to_value(&snake.new_message).unwrap()
);
}
}