Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
457c359
feat: implement AgentManager for session isolation (#4389)
tlongwell-block Sep 19, 2025
648c0af
refactor: migrate tests and update reply routes to use AgentManager
tlongwell-block Sep 19, 2025
5aa7a60
refactor: update agent.rs routes to use session-specific agents
tlongwell-block Sep 19, 2025
e18d752
feat(server): Complete migration to session-specific agents
tlongwell-block Sep 19, 2025
c86158a
feat(agent-manager): Add default provider configuration
tlongwell-block Sep 19, 2025
429b64b
feat(agent-manager): Add default provider configuration
tlongwell-block Sep 19, 2025
aa56960
test: Complete Agent Manager integration testing
tlongwell-block Sep 19, 2025
319fab8
chore: Remove intermediate working documents from git
tlongwell-block Sep 19, 2025
1569783
revert: Remove unrelated changes to computercontroller platform files
tlongwell-block Sep 19, 2025
d2f2edb
remove comment
tlongwell-block Sep 19, 2025
211a558
intermediate removal of deprecated Agent and reset
tlongwell-block Sep 19, 2025
8b77c4a
test work
tlongwell-block Sep 19, 2025
3508f1e
remove test_agent_manager.sh
tlongwell-block Sep 19, 2025
7af6256
openapi
tlongwell-block Sep 19, 2025
175e0e6
fix failing audio test
tlongwell-block Sep 20, 2025
b2b81dd
Fix UI to pass session_id when managing extensions
tlongwell-block Sep 20, 2025
cf2f2d3
ui tests
tlongwell-block Sep 20, 2025
adc429e
fix audio test
tlongwell-block Sep 20, 2025
8e41dad
Additional tests
tlongwell-block Sep 20, 2025
4952054
remove premature adapters
tlongwell-block Sep 20, 2025
cb62920
smaller PR. remove stub for recipe execution
tlongwell-block Sep 20, 2025
db8a088
remove overly verbose comments
tlongwell-block Sep 20, 2025
ea85e9d
pi is fine
tlongwell-block Sep 21, 2025
ab87558
enforce session_id appropriately
tlongwell-block Sep 21, 2025
809ad32
cleanup, fmt
tlongwell-block Sep 21, 2025
23d1306
cleanup comments
tlongwell-block Sep 21, 2025
2face87
comments
tlongwell-block Sep 21, 2025
334aaa5
comments
tlongwell-block Sep 21, 2025
a738d8f
fmt
tlongwell-block Sep 21, 2025
69deb91
clean up agent.rs with helpers
tlongwell-block Sep 21, 2025
fc69280
DRY agent usage
tlongwell-block Sep 22, 2025
b22df79
Changes per review. Make session_id mandatory and just a string. Move…
tlongwell-block Sep 22, 2025
51a2574
revert test, remove trivial comment
tlongwell-block Sep 22, 2025
e0defc1
dedupe ExecutionMode to SessionExecutionMode
tlongwell-block Sep 22, 2025
b65f8a0
SessionExecutionMode take 2
tlongwell-block Sep 22, 2025
f12c66b
clean up warnings
tlongwell-block Sep 22, 2025
5aa1843
require sessionId in ui. Remove useless zero seesion max test
tlongwell-block Sep 23, 2025
98d65fe
remove scheduler redundancy. Remove raw json handling in favor of Add…
tlongwell-block Sep 23, 2025
77e8cdd
rename get_session_agent to simply get_agent
tlongwell-block Sep 23, 2025
6250368
Refactor: AgentManager now owns scheduler initialization
tlongwell-block Sep 23, 2025
1770521
scheduler mandatory in AgentManager. Unify AgentManager new() method
tlongwell-block Sep 23, 2025
5232f12
Thread sessionId through props to ExtensionsSection instead of using …
tlongwell-block Sep 23, 2025
cc1d48e
Merge branch 'main' into agent_manager
tlongwell-block Sep 24, 2025
46232d9
Better handle default provider
tlongwell-block Sep 24, 2025
0c09cdb
ui tests now need session id defined
tlongwell-block Sep 24, 2025
cfa3b2a
more ui testing fixes
tlongwell-block Sep 24, 2025
49f71a1
remove pricing_api_test.rs and LRU comment
tlongwell-block Sep 24, 2025
4e3eae9
fix: make AgentManager thread-safe and self-initializing
tlongwell-block Sep 24, 2025
92c308d
fmt
tlongwell-block Sep 24, 2025
fcb2968
clean up routes getting agents
tlongwell-block Sep 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 1 addition & 49 deletions crates/goose-server/src/commands/agent.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
use std::sync::Arc;

use crate::configuration;
use crate::state;
use anyhow::Result;
use axum::middleware;
use etcetera::{choose_app_strategy, AppStrategy};
use goose::agents::Agent;
use goose::config::APP_STRATEGY;
use goose::scheduler_factory::SchedulerFactory;
use goose_server::auth::check_token;
use tower_http::cors::{Any, CorsLayer};
use tracing::info;
Expand All @@ -32,49 +26,7 @@ pub async fn run() -> Result<()> {
let secret_key =
std::env::var("GOOSE_SERVER__SECRET_KEY").unwrap_or_else(|_| "test".to_string());

let new_agent = Agent::new();

// Only initialize provider and extensions when running in standalone goosed mode
// This prevents breaking the Electron app which manages its own provider setup
if std::env::var("GOOSE_STANDALONE_MODE").unwrap_or_else(|_| "false".to_string()) == "true" {
tracing::info!("Running in standalone mode - initializing provider and extensions");

// Initialize provider like the CLI does
let config = goose::config::Config::global();

let provider_name: String = config
.get_param("GOOSE_PROVIDER")
.expect("No provider configured. Run 'goose configure' first");

let model_name: String = config
.get_param("GOOSE_MODEL")
.expect("No model configured. Run 'goose configure' first");

let model_config = goose::model::ModelConfig::new(&model_name)
.expect("Failed to create model configuration");

let provider = goose::providers::create(&provider_name, model_config)
.expect("Failed to create provider");

new_agent
.update_provider(provider)
.await
.expect("Failed to update agent provider");
}

let agent_ref = Arc::new(new_agent);

let app_state = state::AppState::new(agent_ref.clone());

let schedule_file_path = choose_app_strategy(APP_STRATEGY.clone())?
.data_dir()
.join("schedules.json");

let scheduler_instance = SchedulerFactory::create(schedule_file_path).await?;
app_state.set_scheduler(scheduler_instance.clone()).await;

// NEW: Provide scheduler access to the agent
agent_ref.set_scheduler(scheduler_instance).await;
let app_state = state::AppState::new().await?;

let cors = CorsLayer::new()
.allow_origin(Any)
Expand Down
61 changes: 26 additions & 35 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::state::AppState;
use axum::response::IntoResponse;
use axum::{
extract::{Query, State},
http::StatusCode,
Expand Down Expand Up @@ -28,7 +27,6 @@ use tracing::error;
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ExtendPromptRequest {
extension: String,
#[allow(dead_code)]
session_id: String,
}

Expand All @@ -40,7 +38,6 @@ pub struct ExtendPromptResponse {
#[derive(Deserialize, utoipa::ToSchema)]
pub struct AddSubRecipesRequest {
sub_recipes: Vec<SubRecipe>,
#[allow(dead_code)]
session_id: String,
}

Expand All @@ -53,27 +50,23 @@ pub struct AddSubRecipesResponse {
pub struct UpdateProviderRequest {
provider: String,
model: Option<String>,
#[allow(dead_code)]
session_id: String,
}

#[derive(Deserialize, utoipa::ToSchema)]
pub struct SessionConfigRequest {
response: Option<Response>,
#[allow(dead_code)]
session_id: String,
}

#[derive(Deserialize, utoipa::ToSchema)]
pub struct GetToolsQuery {
extension_name: Option<String>,
#[allow(dead_code)]
session_id: String,
}

#[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateRouterToolSelectorRequest {
#[allow(dead_code)]
session_id: String,
}

Expand Down Expand Up @@ -116,8 +109,6 @@ async fn start_agent(
State(state): State<Arc<AppState>>,
Json(payload): Json<StartAgentRequest>,
) -> Result<Json<StartAgentResponse>, StatusCode> {
state.reset().await;

let session_id = session::generate_session_id();
let counter = state.session_counter.fetch_add(1, Ordering::SeqCst) + 1;

Expand Down Expand Up @@ -203,7 +194,7 @@ async fn add_sub_recipes(
State(state): State<Arc<AppState>>,
Json(payload): Json<AddSubRecipesRequest>,
) -> Result<Json<AddSubRecipesResponse>, StatusCode> {
let agent = state.get_agent().await;
let agent = state.get_agent_for_route(payload.session_id).await?;
agent.add_sub_recipes(payload.sub_recipes.clone()).await;
Ok(Json(AddSubRecipesResponse { success: true }))
}
Expand All @@ -222,7 +213,7 @@ async fn extend_prompt(
State(state): State<Arc<AppState>>,
Json(payload): Json<ExtendPromptRequest>,
) -> Result<Json<ExtendPromptResponse>, StatusCode> {
let agent = state.get_agent().await;
let agent = state.get_agent_for_route(payload.session_id).await?;
agent.extend_system_prompt(payload.extension.clone()).await;
Ok(Json(ExtendPromptResponse { success: true }))
}
Expand All @@ -247,7 +238,7 @@ async fn get_tools(
) -> Result<Json<Vec<ToolInfo>>, StatusCode> {
let config = Config::global();
let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
let agent = state.get_agent().await;
let agent = state.get_agent_for_route(query.session_id).await?;
let permission_manager = PermissionManager::default();

let mut tools: Vec<ToolInfo> = agent
Expand Down Expand Up @@ -298,35 +289,37 @@ async fn get_tools(
async fn update_agent_provider(
State(state): State<Arc<AppState>>,
Json(payload): Json<UpdateProviderRequest>,
) -> Result<StatusCode, impl IntoResponse> {
let agent = state.get_agent().await;
) -> Result<StatusCode, StatusCode> {
let agent = state
.get_agent_for_route(payload.session_id.clone())
.await?;

let config = Config::global();
let model = match payload
.model
.or_else(|| config.get_param("GOOSE_MODEL").ok())
{
Some(m) => m,
None => return Err((StatusCode::BAD_REQUEST, "No model specified".to_string())),
None => {
tracing::error!("No model specified");
return Err(StatusCode::BAD_REQUEST);
}
};

let model_config = ModelConfig::new(&model).map_err(|e| {
(
StatusCode::BAD_REQUEST,
format!("Invalid model config: {}", e),
)
tracing::error!("Invalid model config: {}", e);
StatusCode::BAD_REQUEST
})?;

let new_provider = create(&payload.provider, model_config).map_err(|e| {
(
StatusCode::BAD_REQUEST,
format!("Failed to create provider: {}", e),
)
tracing::error!("Failed to create provider: {}", e);
StatusCode::BAD_REQUEST
})?;

agent
.update_provider(new_provider)
.await
.map_err(|_e| (StatusCode::INTERNAL_SERVER_ERROR, String::new()))?;
agent.update_provider(new_provider).await.map_err(|e| {
tracing::error!("Failed to update provider: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;

Ok(StatusCode::OK)
}
Expand All @@ -344,17 +337,15 @@ async fn update_agent_provider(
)]
async fn update_router_tool_selector(
State(state): State<Arc<AppState>>,
Json(_payload): Json<UpdateRouterToolSelectorRequest>,
) -> Result<Json<String>, Json<ErrorResponse>> {
let agent = state.get_agent().await;
Json(payload): Json<UpdateRouterToolSelectorRequest>,
) -> Result<Json<String>, StatusCode> {
let agent = state.get_agent_for_route(payload.session_id).await?;
agent
.update_router_tool_selector(None, Some(true))
.await
.map_err(|e| {
tracing::error!("Failed to update tool selection strategy: {}", e);
Json(ErrorResponse {
error: format!("Failed to update tool selection strategy: {}", e),
})
StatusCode::INTERNAL_SERVER_ERROR
})?;

Ok(Json(
Expand All @@ -376,8 +367,8 @@ async fn update_router_tool_selector(
async fn update_session_config(
State(state): State<Arc<AppState>>,
Json(payload): Json<SessionConfigRequest>,
) -> Result<Json<String>, Json<ErrorResponse>> {
let agent = state.get_agent().await;
) -> Result<Json<String>, StatusCode> {
let agent = state.get_agent_for_route(payload.session_id).await?;
if let Some(response) = payload.response {
agent.add_final_output_tool(response).await;

Expand Down
48 changes: 13 additions & 35 deletions crates/goose-server/src/routes/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,13 @@ pub fn routes(state: Arc<AppState>) -> Router {
mod tests {
use super::*;
use axum::{body::Body, http::Request};
use serde_json::json;
use tower::ServiceExt;

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_transcribe_endpoint_requires_auth() {
let state = AppState::new(Arc::new(goose::agents::Agent::new()));
let state = AppState::new().await.unwrap();
let app = routes(state);

// Test without auth header
let request = Request::builder()
.uri("/audio/transcribe")
Expand All @@ -413,40 +413,18 @@ mod tests {
.unwrap();

let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert!(
response.status() == StatusCode::PRECONDITION_FAILED
|| response.status() == StatusCode::UNAUTHORIZED
);
}

#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
async fn test_transcribe_endpoint_validates_size() {
let state = AppState::new(Arc::new(goose::agents::Agent::new()));
let app = routes(state);

// Create a large base64 string (simulating > 25MB audio)
let large_audio = BASE64.encode(vec![0u8; MAX_AUDIO_SIZE_BYTES + 1]);

let request = Request::builder()
.uri("/audio/transcribe")
.method("POST")
.header("content-type", "application/json")
.header("x-secret-key", "test-secret")
.body(Body::from(
serde_json::to_string(&serde_json::json!({
"audio": large_audio,
"mime_type": "audio/webm"
}))
.unwrap(),
))
.unwrap();

let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
}

#[tokio::test]
async fn test_transcribe_endpoint_validates_mime_type() {
let state = AppState::new(Arc::new(goose::agents::Agent::new()));
let state = AppState::new().await.unwrap();
let app = routes(state);

let large_data = "a".repeat(30 * 1024 * 1024); // 30MB
let request = Request::builder()
.uri("/audio/transcribe")
.method("POST")
Expand All @@ -468,9 +446,9 @@ mod tests {
);
}

#[tokio::test]
async fn test_transcribe_endpoint_handles_invalid_base64() {
let state = AppState::new(Arc::new(goose::agents::Agent::new()));
#[tokio::test(flavor = "multi_thread")]
async fn test_transcribe_endpoint_validates_mime_type() {
let state = AppState::new().await.unwrap();
let app = routes(state);

let request = Request::builder()
Expand Down
4 changes: 3 additions & 1 deletion crates/goose-server/src/routes/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct ContextManageRequest {
pub messages: Vec<Message>,
/// Operation to perform: "truncation" or "summarize"
pub manage_action: String,
/// Optional session ID for session-specific agent
pub session_id: String,
}

/// Response from context management operations
Expand Down Expand Up @@ -44,7 +46,7 @@ async fn manage_context(
State(state): State<Arc<AppState>>,
Json(request): Json<ContextManageRequest>,
) -> Result<Json<ContextManageResponse>, StatusCode> {
let agent = state.get_agent().await;
let agent = state.get_agent_for_route(request.session_id).await?;

let mut processed_messages = Conversation::new_unvalidated(vec![]);
let mut token_counts: Vec<usize> = vec![];
Expand Down
Loading
Loading