mod entrypoint;
mod envelope;
mod operations;
pub use entrypoint::{AgentEngineOptions, build_agent_engine_app, serve_agent_engine};
pub use envelope::DispatchRequest;
pub use operations::{
AddSessionToMemoryInput, AgentRunRequest, ApiMode, ClassMethod, CreateSessionInput,
DeleteSessionInput, GetSessionInput, ListSessionsInput, SearchMemoryInput, StreamQueryInput,
StreamingAgentRunWithEventsInput,
};
use adk_core::{AdkError, Content, ErrorCategory, ErrorComponent, Result};
use adk_runner::Runner;
use axum::{
Json, Router,
body::Body,
http::{StatusCode, header},
response::{IntoResponse, Response},
routing::post,
};
use futures::StreamExt;
use serde_json::Value;
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use tracing::{error, info};
#[derive(Clone)]
pub struct AgentEngineState {
runner: Arc<Runner>,
session_service: Arc<dyn adk_session::SessionService>,
memory_service: Option<Arc<dyn adk_memory::MemoryService>>,
artifact_service: Option<Arc<dyn adk_artifact::ArtifactService>>,
app_name: String,
}
impl AgentEngineState {
pub fn new(runner: Arc<Runner>) -> Self {
let session_service = runner.session_service().clone();
let app_name = runner.app_name().to_string();
Self { runner, session_service, memory_service: None, artifact_service: None, app_name }
}
pub fn with_memory_service(
mut self,
memory_service: Arc<dyn adk_memory::MemoryService>,
) -> Self {
self.memory_service = Some(memory_service);
self
}
pub fn with_artifact_service(
mut self,
artifact_service: Arc<dyn adk_artifact::ArtifactService>,
) -> Self {
self.artifact_service = Some(artifact_service);
self
}
pub fn runner(&self) -> &Arc<Runner> {
&self.runner
}
pub fn session_service(&self) -> &Arc<dyn adk_session::SessionService> {
&self.session_service
}
pub fn memory_service(&self) -> Option<&Arc<dyn adk_memory::MemoryService>> {
self.memory_service.as_ref()
}
pub fn artifact_service(&self) -> Option<&Arc<dyn adk_artifact::ArtifactService>> {
self.artifact_service.as_ref()
}
pub fn app_name(&self) -> &str {
&self.app_name
}
}
pub fn agent_engine_router(state: AgentEngineState) -> Router {
Router::new()
.route("/api/reasoning_engine", post(dispatch_unary))
.route("/api/stream_reasoning_engine", post(dispatch_stream))
.with_state(state)
}
fn problem_response(err: &AdkError) -> Response {
let status =
StatusCode::from_u16(err.http_status_code()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(status, Json(err.to_problem_json())).into_response()
}
fn wrong_endpoint(method: ClassMethod, expected: &str) -> AdkError {
AdkError::new(
ErrorComponent::Server,
ErrorCategory::InvalidInput,
"agent_engine.wrong_endpoint",
format!("class_method '{}' must be POSTed to {expected}", method.as_str()),
)
}
async fn dispatch_unary(
axum::extract::State(state): axum::extract::State<AgentEngineState>,
Json(request): Json<DispatchRequest>,
) -> Response {
info!(class_method = %request.class_method, "agent engine unary dispatch");
let method = match ClassMethod::from_str(&request.class_method) {
Ok(method) => method,
Err(err) => return problem_response(&err),
};
if method.api_mode().is_streaming() {
return problem_response(&wrong_endpoint(method, "/api/stream_reasoning_engine"));
}
let output = run_unary(&state, method, request.input).await;
match output {
Ok(output) => Json(envelope::unary_response(output)).into_response(),
Err(err) => {
error!(class_method = %request.class_method, error = %err, "unary dispatch failed");
problem_response(&err)
}
}
}
async fn run_unary(
state: &AgentEngineState,
method: ClassMethod,
input: Option<Value>,
) -> Result<Value> {
match method {
ClassMethod::CreateSession | ClassMethod::AsyncCreateSession => {
operations::handle_create_session(state, operations::typed_input(input)?).await
}
ClassMethod::GetSession | ClassMethod::AsyncGetSession => {
operations::handle_get_session(state, operations::typed_input(input)?).await
}
ClassMethod::ListSessions | ClassMethod::AsyncListSessions => {
operations::handle_list_sessions(state, operations::typed_input(input)?).await
}
ClassMethod::DeleteSession | ClassMethod::AsyncDeleteSession => {
operations::handle_delete_session(state, operations::typed_input(input)?).await
}
ClassMethod::AsyncAddSessionToMemory => {
operations::handle_add_session_to_memory(state, operations::typed_input(input)?).await
}
ClassMethod::AsyncSearchMemory => {
operations::handle_search_memory(state, operations::typed_input(input)?).await
}
ClassMethod::RegisterOperations => Ok(operations::handle_register_operations()),
ClassMethod::StreamQuery
| ClassMethod::AsyncStreamQuery
| ClassMethod::StreamingAgentRunWithEvents => {
Err(wrong_endpoint(method, "/api/stream_reasoning_engine"))
}
}
}
async fn dispatch_stream(
axum::extract::State(state): axum::extract::State<AgentEngineState>,
Json(request): Json<DispatchRequest>,
) -> Response {
info!(class_method = %request.class_method, "agent engine streaming dispatch");
let method = match ClassMethod::from_str(&request.class_method) {
Ok(method) => method,
Err(err) => return problem_response(&err),
};
let prepared = match method {
ClassMethod::StreamQuery | ClassMethod::AsyncStreamQuery => {
prepare_stream_query(&state, request.input).await
}
ClassMethod::StreamingAgentRunWithEvents => {
prepare_agent_run_with_events(&state, request.input).await
}
ClassMethod::CreateSession
| ClassMethod::AsyncCreateSession
| ClassMethod::GetSession
| ClassMethod::AsyncGetSession
| ClassMethod::ListSessions
| ClassMethod::AsyncListSessions
| ClassMethod::DeleteSession
| ClassMethod::AsyncDeleteSession
| ClassMethod::AsyncAddSessionToMemory
| ClassMethod::AsyncSearchMemory
| ClassMethod::RegisterOperations => Err(wrong_endpoint(method, "/api/reasoning_engine")),
};
let (user_id, session_id, content) = match prepared {
Ok(prepared) => prepared,
Err(err) => {
error!(class_method = %request.class_method, error = %err, "streaming dispatch failed");
return problem_response(&err);
}
};
let (typed_user_id, typed_session_id) = match operations::typed_identity(&user_id, &session_id)
{
Ok(identity) => identity,
Err(err) => return problem_response(&err),
};
let event_stream = match state.runner().run(typed_user_id, typed_session_id, content).await {
Ok(stream) => stream,
Err(err) => {
error!(error = %err, "runner failed to start");
return problem_response(&err);
}
};
let body_stream = event_stream.map(|item| {
let line = match item {
Ok(event) => serde_json::to_string(&event).unwrap_or_else(|err| {
error_line(&AdkError::new(
ErrorComponent::Server,
ErrorCategory::Internal,
"agent_engine.event_serialization",
format!("failed to serialize event: {err}"),
))
}),
Err(err) => {
error!(error = %err, "agent event stream failed");
error_line(&err)
}
};
Ok::<_, std::convert::Infallible>(format!("{line}\n"))
});
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from_stream(body_stream))
.unwrap_or_else(|err| {
error!(error = %err, "failed to build streaming response");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
}
fn error_line(err: &AdkError) -> String {
err.to_problem_json().to_string()
}
async fn prepare_stream_query(
state: &AgentEngineState,
input: Option<Value>,
) -> Result<(String, String, Content)> {
let input: StreamQueryInput = operations::typed_input(input)?;
let content = operations::message_to_content(input.message)?;
let (session_id, _created) =
operations::resolve_session(state, &input.user_id, input.session_id, HashMap::new())
.await?;
Ok((input.user_id, session_id, content))
}
async fn prepare_agent_run_with_events(
state: &AgentEngineState,
input: Option<Value>,
) -> Result<(String, String, Content)> {
let input: StreamingAgentRunWithEventsInput = operations::typed_input(input)?;
let request: AgentRunRequest = serde_json::from_str(&input.request_json).map_err(|err| {
AdkError::new(
ErrorComponent::Server,
ErrorCategory::InvalidInput,
"agent_engine.invalid_agent_run_request",
format!("request_json is not a valid AgentRunRequest: {err}"),
)
})?;
let state_delta: HashMap<String, Value> =
request.state_delta.unwrap_or_default().into_iter().collect();
let (session_id, created) = operations::resolve_session(
state,
&request.user_id,
Some(request.session_id),
state_delta.clone(),
)
.await?;
if !created {
operations::apply_state_delta(state, &request.user_id, &session_id, state_delta).await?;
}
Ok((request.user_id, session_id, request.new_message))
}