chore: refactor read-write lock on agent (#2225)

Co-authored-by: Alice Hau <ahau@squareup.com>
This commit is contained in:
Salman Mohammed
2025-04-23 23:46:22 -03:00
committed by GitHub
parent 85e2ee3984
commit 199fa6adbc
24 changed files with 409 additions and 237 deletions
+7 -2
View File
@@ -1,6 +1,9 @@
use std::sync::Arc;
use crate::configuration;
use crate::state;
use anyhow::Result;
use goose::agents::Agent;
use tower_http::cors::{Any, CorsLayer};
use tracing::info;
@@ -15,8 +18,10 @@ pub async fn run() -> Result<()> {
let secret_key =
std::env::var("GOOSE_SERVER__SECRET_KEY").unwrap_or_else(|_| "test".to_string());
// Create app state - agent will start as None
let state = state::AppState::new(secret_key.clone()).await?;
let new_agent = Agent::new();
// Create app state with agent
let state = state::AppState::new(Arc::new(new_agent), secret_key.clone()).await;
// Create router with CORS support
let cors = CorsLayer::new()
+67 -60
View File
@@ -8,14 +8,15 @@ use axum::{
};
use goose::config::Config;
use goose::config::PermissionManager;
use goose::{agents::Agent, model::ModelConfig, providers};
use goose::model::ModelConfig;
use goose::providers::create;
use goose::{
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
config::permission::PermissionLevel,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::sync::Arc;
#[derive(Serialize)]
struct VersionsResponse {
@@ -33,17 +34,6 @@ struct ExtendPromptResponse {
success: bool,
}
#[derive(Deserialize)]
struct CreateAgentRequest {
provider: String,
model: Option<String>,
}
#[derive(Serialize)]
struct CreateAgentResponse {
version: String,
}
#[derive(Deserialize)]
struct ProviderFile {
name: String,
@@ -66,6 +56,12 @@ struct ProviderList {
details: ProviderDetails,
}
#[derive(Deserialize)]
struct UpdateProviderRequest {
provider: String,
model: Option<String>,
}
#[derive(Deserialize)]
pub struct GetToolsQuery {
extension_name: Option<String>,
@@ -82,53 +78,18 @@ async fn get_versions() -> Json<VersionsResponse> {
}
async fn extend_prompt(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<ExtendPromptRequest>,
) -> Result<Json<ExtendPromptResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let mut agent = state.agent.write().await;
if let Some(ref mut agent) = *agent {
agent.extend_system_prompt(payload.extension).await;
Ok(Json(ExtendPromptResponse { success: true }))
} else {
Err(StatusCode::NOT_FOUND)
}
}
#[axum::debug_handler]
async fn create_agent(
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<CreateAgentRequest>,
) -> Result<Json<CreateAgentResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
// Set the environment variable for the model if provided
if let Some(model) = &payload.model {
let env_var_key = format!("{}_MODEL", payload.provider.to_uppercase());
env::set_var(env_var_key.clone(), model);
println!("Set environment variable: {}={}", env_var_key, model);
}
let config = Config::global();
let model = payload.model.unwrap_or_else(|| {
config
.get_param("GOOSE_MODEL")
.expect("Did not find a model on payload or in env")
});
let model_config = ModelConfig::new(model);
let provider =
providers::create(&payload.provider, model_config).expect("Failed to create provider");
let version = String::from("goose");
let new_agent = Agent::new(provider);
let mut agent = state.agent.write().await;
*agent = Some(new_agent);
Ok(Json(CreateAgentResponse { version }))
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
agent.extend_system_prompt(payload.extension.clone()).await;
Ok(Json(ExtendPromptResponse { success: true }))
}
async fn list_providers() -> Json<Vec<ProviderList>> {
@@ -168,7 +129,7 @@ async fn list_providers() -> Json<Vec<ProviderList>> {
)
)]
async fn get_tools(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Query(query): Query<GetToolsQuery>,
) -> Result<Json<Vec<ToolInfo>>, StatusCode> {
@@ -176,8 +137,10 @@ async fn get_tools(
let config = Config::global();
let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
let agent = state.agent.read().await;
let agent = agent.as_ref().ok_or(StatusCode::PRECONDITION_REQUIRED)?;
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
let permission_manager = PermissionManager::default();
let mut tools: Vec<ToolInfo> = agent
@@ -210,12 +173,56 @@ async fn get_tools(
Ok(Json(tools))
}
pub fn routes(state: AppState) -> Router {
#[utoipa::path(
post,
path = "/agent/update_provider",
responses(
(status = 200, description = "Update provider completed", body = String),
(status = 500, description = "Internal server error")
)
)]
async fn update_agent_provider(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<UpdateProviderRequest>,
) -> Result<StatusCode, StatusCode> {
// Verify secret key
let secret_key = headers
.get("X-Secret-Key")
.and_then(|value| value.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
if secret_key != state.secret_key {
return Err(StatusCode::UNAUTHORIZED);
}
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
let config = Config::global();
let model = payload.model.unwrap_or_else(|| {
config
.get_param("GOOSE_MODEL")
.expect("Did not find a model on payload or in env to update provider with")
});
let model_config = ModelConfig::new(model);
let new_provider = create(&payload.provider, model_config).unwrap();
agent
.update_provider(new_provider)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::OK)
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/agent/versions", get(get_versions))
.route("/agent/providers", get(list_providers))
.route("/agent/prompt", post(extend_prompt))
.route("/agent/tools", get(get_tools))
.route("/agent", post(create_agent))
.route("/agent/update_provider", post(update_agent_provider))
.with_state(state)
}
@@ -18,7 +18,7 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_yaml;
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use utoipa::ToSchema;
#[derive(Serialize, ToSchema)]
@@ -89,7 +89,7 @@ pub struct UpsertPermissionsQuery {
)
)]
pub async fn upsert_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<UpsertConfigQuery>,
) -> Result<Json<Value>, StatusCode> {
@@ -116,7 +116,7 @@ pub async fn upsert_config(
)
)]
pub async fn remove_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<String>, StatusCode> {
@@ -148,7 +148,7 @@ pub async fn remove_config(
)
)]
pub async fn read_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<ConfigKeyQuery>,
) -> Result<Json<Value>, StatusCode> {
@@ -180,7 +180,7 @@ pub async fn read_config(
)
)]
pub async fn get_extensions(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<ExtensionResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
@@ -213,7 +213,7 @@ pub async fn get_extensions(
)
)]
pub async fn add_extension(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(extension_query): Json<ExtensionQuery>,
) -> Result<Json<String>, StatusCode> {
@@ -251,7 +251,7 @@ pub async fn add_extension(
)
)]
pub async fn remove_extension(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
axum::extract::Path(name): axum::extract::Path<String>,
) -> Result<Json<String>, StatusCode> {
@@ -272,7 +272,7 @@ pub async fn remove_extension(
)
)]
pub async fn read_all_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<ConfigResponse>, StatusCode> {
// Use the helper function to verify the secret key
@@ -297,7 +297,7 @@ pub async fn read_all_config(
)
)]
pub async fn providers(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<Vec<ProviderDetails>>, StatusCode> {
verify_secret_key(&headers, &state)?;
@@ -332,7 +332,7 @@ pub async fn providers(
)
)]
pub async fn init_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
@@ -402,7 +402,7 @@ pub async fn init_config(
)
)]
pub async fn upsert_permissions(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(query): Json<UpsertPermissionsQuery>,
) -> Result<Json<String>, StatusCode> {
@@ -435,7 +435,7 @@ pub static APP_STRATEGY: Lazy<AppStrategyArgs> = Lazy::new(|| AppStrategyArgs {
)
)]
pub async fn backup_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<String>, StatusCode> {
verify_secret_key(&headers, &state)?;
@@ -466,7 +466,7 @@ pub async fn backup_config(
}
}
pub fn routes(state: AppState) -> Router {
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/config", get(read_all_config))
.route("/config/upsert", post(upsert_config))
+5 -5
View File
@@ -10,7 +10,7 @@ use http::{HeaderMap, StatusCode};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
#[derive(Serialize)]
struct ConfigResponse {
@@ -26,7 +26,7 @@ struct ConfigRequest {
}
async fn store_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<ConfigRequest>,
) -> Result<Json<ConfigResponse>, StatusCode> {
@@ -148,7 +148,7 @@ pub struct GetConfigResponse {
}
pub async fn get_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Query(query): Query<GetConfigQuery>,
) -> Result<Json<GetConfigResponse>, StatusCode> {
@@ -174,7 +174,7 @@ struct DeleteConfigRequest {
}
async fn delete_config(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<DeleteConfigRequest>,
) -> Result<StatusCode, StatusCode> {
@@ -193,7 +193,7 @@ async fn delete_config(
}
}
pub fn routes(state: AppState) -> Router {
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/configs/providers", post(check_provider_configs))
.route("/configs/get", get(get_config))
+7 -5
View File
@@ -8,6 +8,7 @@ use axum::{
};
use goose::message::Message;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
// Direct message serialization for context mgmt request
#[derive(Debug, Deserialize)]
@@ -26,15 +27,16 @@ pub struct ContextManageResponse {
}
async fn manage_context(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<ContextManageRequest>,
) -> Result<Json<ContextManageResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
// Get a lock on the shared agent
let agent = state.agent.read().await;
let agent = agent.as_ref().ok_or(StatusCode::PRECONDITION_REQUIRED)?;
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
let mut processed_messages: Vec<Message> = vec![];
let mut token_counts: Vec<usize> = vec![];
@@ -57,7 +59,7 @@ async fn manage_context(
}
// Configure routes for this module
pub fn routes(state: AppState) -> Router {
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/context/manage", post(manage_context))
.with_state(state)
+14 -9
View File
@@ -1,5 +1,6 @@
use std::env;
use std::path::Path;
use std::sync::Arc;
use std::sync::OnceLock;
use super::utils::verify_secret_key;
@@ -79,7 +80,7 @@ struct ExtensionResponse {
/// Handler for adding a new extension configuration.
async fn add_extension(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
raw: axum::extract::Json<serde_json::Value>,
) -> Result<Json<ExtensionResponse>, StatusCode> {
@@ -228,9 +229,11 @@ async fn add_extension(
},
};
// Acquire a lock on the agent and attempt to add the extension.
let mut agent = state.agent.write().await;
let agent = agent.as_mut().ok_or(StatusCode::PRECONDITION_REQUIRED)?;
// Get a reference to the agent
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
let response = agent.add_extension(extension_config).await;
// Respond with the result.
@@ -254,15 +257,17 @@ async fn add_extension(
/// Handler for removing an extension by name
async fn remove_extension(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(name): Json<String>,
) -> Result<Json<ExtensionResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
// Acquire a lock on the agent and attempt to remove the extension
let mut agent = state.agent.write().await;
let agent = agent.as_mut().ok_or(StatusCode::PRECONDITION_REQUIRED)?;
// Get a reference to the agent
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
agent.remove_extension(&name).await;
Ok(Json(ExtensionResponse {
@@ -272,7 +277,7 @@ async fn remove_extension(
}
/// Registers the extension management routes with the Axum router.
pub fn routes(state: AppState) -> Router {
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/extensions/add", post(add_extension))
.route("/extensions/remove", post(remove_extension))
+4 -2
View File
@@ -9,10 +9,12 @@ pub mod recipe;
pub mod reply;
pub mod session;
pub mod utils;
use std::sync::Arc;
use axum::Router;
// Function to configure all routes
pub fn configure(state: crate::state::AppState) -> Router {
pub fn configure(state: Arc<crate::state::AppState>) -> Router {
Router::new()
.merge(health::routes())
.merge(reply::routes(state.clone()))
@@ -22,5 +24,5 @@ pub fn configure(state: crate::state::AppState) -> Router {
.merge(configs::routes(state.clone()))
.merge(config_management::routes(state.clone()))
.merge(recipe::routes(state.clone()))
.merge(session::routes(state))
.merge(session::routes(state.clone()))
}
+12 -10
View File
@@ -1,3 +1,5 @@
use std::sync::Arc;
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use goose::message::Message;
use goose::recipe::Recipe;
@@ -34,17 +36,17 @@ pub struct CreateRecipeResponse {
/// Create a Recipe configuration from the current state of an agent
async fn create_recipe(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
Json(request): Json<CreateRecipeRequest>,
) -> Result<Json<CreateRecipeResponse>, (StatusCode, Json<CreateRecipeResponse>)> {
let agent = state.agent.read().await;
let agent = agent.as_ref().ok_or_else(|| {
let error_response = CreateRecipeResponse {
recipe: None,
error: Some("Agent not initialized".to_string()),
};
(StatusCode::PRECONDITION_REQUIRED, Json(error_response))
})?;
let error_response = CreateRecipeResponse {
recipe: None,
error: Some("Missing agent".to_string()),
};
let agent = state
.get_agent()
.await
.map_err(|_| (StatusCode::PRECONDITION_FAILED, Json(error_response)))?;
// Create base recipe from agent state and messages
let recipe_result = agent.create_recipe(request.messages).await;
@@ -82,7 +84,7 @@ async fn create_recipe(
}
}
pub fn routes(state: AppState) -> Router {
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/recipe/create", post(create_recipe))
.with_state(state)
+51 -32
View File
@@ -26,6 +26,7 @@ use std::{
convert::Infallible,
path::PathBuf,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
@@ -101,7 +102,7 @@ async fn stream_event(
}
async fn handler(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<ChatRequest>,
) -> Result<SseResponse, StatusCode> {
@@ -119,15 +120,34 @@ async fn handler(
.session_id
.unwrap_or_else(session::generate_session_id);
// Get a lock on the shared agent
let agent = state.agent.clone();
// Spawn task to handle streaming
tokio::spawn(async move {
let agent = agent.read().await;
let agent = match agent.as_ref() {
Some(agent) => agent,
None => {
let agent = state.get_agent().await;
let agent = match agent {
Ok(agent) => {
let provider = agent.provider().await;
match provider {
Ok(_) => agent,
Err(_) => {
let _ = stream_event(
MessageEvent::Error {
error: "No provider configured".to_string(),
},
&tx,
)
.await;
let _ = stream_event(
MessageEvent::Finish {
reason: "error".to_string(),
},
&tx,
)
.await;
return;
}
}
}
Err(_) => {
let _ = stream_event(
MessageEvent::Error {
error: "No agent configured".to_string(),
@@ -147,7 +167,7 @@ async fn handler(
};
// Get the provider first, before starting the reply stream
let provider = agent.provider();
let provider = agent.provider().await;
let mut stream = match agent
.reply(
@@ -204,7 +224,7 @@ async fn handler(
// Store messages and generate description in background
let session_path = session_path.clone();
let messages = all_messages.clone();
let provider = provider.clone();
let provider = Arc::clone(provider.as_ref().unwrap());
tokio::spawn(async move {
if let Err(e) = session::persist_messages(&session_path, &messages, Some(provider)).await {
tracing::error!("Failed to store session history: {:?}", e);
@@ -262,7 +282,7 @@ struct AskResponse {
// Simple ask an AI for a response, non streaming
async fn ask_handler(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<AskRequest>,
) -> Result<Json<AskResponse>, StatusCode> {
@@ -275,12 +295,13 @@ async fn ask_handler(
.session_id
.unwrap_or_else(session::generate_session_id);
let agent = state.agent.clone();
let agent = agent.write().await;
let agent = agent.as_ref().ok_or(StatusCode::NOT_FOUND)?;
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
// Get the provider first, before starting the reply stream
let provider = agent.provider();
let provider = agent.provider().await;
// Create a single message for the prompt
let messages = vec![Message::user().with_text(request.prompt)];
@@ -339,7 +360,7 @@ async fn ask_handler(
// Store messages and generate description in background
let session_path = session_path.clone();
let messages = all_messages.clone();
let provider = provider.clone();
let provider = Arc::clone(provider.as_ref().unwrap());
tokio::spawn(async move {
if let Err(e) = session::persist_messages(&session_path, &messages, Some(provider)).await {
tracing::error!("Failed to store session history: {:?}", e);
@@ -374,15 +395,16 @@ fn default_principal_type() -> PrincipalType {
)
)]
pub async fn confirm_permission(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(request): Json<PermissionConfirmationRequest>,
) -> Result<Json<Value>, StatusCode> {
verify_secret_key(&headers, &state)?;
let agent = state.agent.clone();
let agent = agent.read().await;
let agent = agent.as_ref().ok_or(StatusCode::NOT_FOUND)?;
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
let permission = match request.action.as_str() {
"always_allow" => Permission::AlwaysAllow,
@@ -410,7 +432,7 @@ struct ToolResultRequest {
}
async fn submit_tool_result(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
raw: axum::extract::Json<serde_json::Value>,
) -> Result<Json<Value>, StatusCode> {
@@ -435,14 +457,16 @@ async fn submit_tool_result(
}
};
let agent = state.agent.read().await;
let agent = agent.as_ref().ok_or(StatusCode::NOT_FOUND)?;
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
agent.handle_tool_result(payload.id, payload.result).await;
Ok(Json(json!({"status": "ok"})))
}
// Configure routes for this module
pub fn routes(state: AppState) -> Router {
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/reply", post(handler))
.route("/ask", post(ask_handler))
@@ -496,9 +520,7 @@ mod tests {
mod integration_tests {
use super::*;
use axum::{body::Body, http::Request};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use tower::ServiceExt;
// This test requires tokio runtime
@@ -509,12 +531,9 @@ mod tests {
let mock_provider = Arc::new(MockProvider {
model_config: mock_model_config,
});
let agent = Agent::new(mock_provider);
let state = AppState {
config: Arc::new(Mutex::new(HashMap::new())),
agent: Arc::new(RwLock::new(Some(agent))),
secret_key: "test-secret".to_string(),
};
let agent = Agent::new();
let _ = agent.update_provider(mock_provider).await;
let state = AppState::new(Arc::new(agent), "test-secret".to_string()).await;
// Build router
let app = routes(state);
+5 -3
View File
@@ -1,4 +1,6 @@
use super::utils::verify_secret_key;
use std::sync::Arc;
use crate::state::AppState;
use axum::{
extract::{Path, State},
@@ -25,7 +27,7 @@ struct SessionHistoryResponse {
// List all available sessions
async fn list_sessions(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<SessionListResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
@@ -38,7 +40,7 @@ async fn list_sessions(
// Get a specific session's history
async fn get_session_history(
State(state): State<AppState>,
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Path(session_id): Path<String>,
) -> Result<Json<SessionHistoryResponse>, StatusCode> {
@@ -65,7 +67,7 @@ async fn get_session_history(
}
// Configure routes for this module
pub fn routes(state: AppState) -> Router {
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/sessions", get(list_sessions))
.route("/sessions/:session_id", get(get_session_history))
+20 -11
View File
@@ -1,25 +1,34 @@
use anyhow::Result;
use goose::agents::Agent;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
/// Shared reference to an Agent that can be cloned cheaply
/// without cloning the underlying Agent object
pub type AgentRef = Arc<Agent>;
/// Thread-safe container for an optional Agent reference
/// Outer Arc: Allows multiple route handlers to access the same Mutex
/// - Mutex provides exclusive access for updates
/// - Option allows for the case where no agent exists yet
///
/// Shared application state
#[allow(dead_code)]
#[derive(Clone)]
pub struct AppState {
pub agent: Arc<RwLock<Option<Agent>>>,
// agent: SharedAgentStore,
agent: Option<AgentRef>,
pub secret_key: String,
pub config: Arc<Mutex<HashMap<String, Value>>>,
}
impl AppState {
pub async fn new(secret_key: String) -> Result<Self> {
Ok(Self {
agent: Arc::new(RwLock::new(None)),
pub async fn new(agent: AgentRef, secret_key: String) -> Arc<AppState> {
Arc::new(Self {
agent: Some(agent.clone()),
secret_key,
config: Arc::new(Mutex::new(HashMap::new())),
})
}
pub async fn get_agent(&self) -> Result<Arc<Agent>, anyhow::Error> {
self.agent
.clone()
.ok_or_else(|| anyhow::anyhow!("Agent needs to be created first."))
}
}