Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn test_format_result_data_for_display() {
assert_eq!(format_result_data_for_display(&json!(true)), "true");
assert_eq!(format_result_data_for_display(&json!(false)), "false");
assert_eq!(format_result_data_for_display(&json!(42)), "42");
assert_eq!(format_result_data_for_display(&json!(3.14)), "3.14");
assert_eq!(format_result_data_for_display(&json!(3.15)), "3.15");
assert_eq!(format_result_data_for_display(&json!(null)), "null");

let partial_obj = json!({
Expand Down
15 changes: 6 additions & 9 deletions crates/goose-server/src/commands/agent.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
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;
Expand All @@ -32,10 +29,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();
let agent_ref = Arc::new(new_agent);

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

let schedule_file_path = choose_app_strategy(APP_STRATEGY.clone())?
.data_dir()
Expand All @@ -44,8 +38,11 @@ pub async fn run() -> Result<()> {
let scheduler_instance = SchedulerFactory::create(schedule_file_path).await?;
app_state.set_scheduler(scheduler_instance.clone()).await;

// NEW: Provide scheduler access to the agent
Comment thread
tlongwell-block marked this conversation as resolved.
agent_ref.set_scheduler(scheduler_instance).await;
// Configure default provider on the agent manager
app_state
.agent_manager()
.configure_default_provider()
.await?;

let cors = CorsLayer::new()
.allow_origin(Any)
Expand Down
74 changes: 53 additions & 21 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ use tracing::error;
#[derive(Deserialize, utoipa::ToSchema)]
pub struct ExtendPromptRequest {
extension: String,
#[allow(dead_code)]
session_id: String,
session_id: Option<String>,
}

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

#[derive(Serialize, utoipa::ToSchema)]
Expand All @@ -53,28 +51,24 @@ pub struct AddSubRecipesResponse {
pub struct UpdateProviderRequest {
provider: String,
model: Option<String>,
#[allow(dead_code)]
session_id: String,
session_id: Option<String>,
}

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

#[derive(Deserialize, utoipa::ToSchema)]
pub struct GetToolsQuery {
extension_name: Option<String>,
#[allow(dead_code)]
session_id: String,
session_id: Option<String>,
Comment thread
tlongwell-block marked this conversation as resolved.
Outdated
}

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

#[derive(Deserialize, utoipa::ToSchema)]
Expand Down Expand Up @@ -116,8 +110,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 +195,13 @@ 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 = match state.get_session_agent(payload.session_id.clone()).await {
Ok(agent) => agent,
Err(e) => {
tracing::error!("Failed to get session agent: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
agent.add_sub_recipes(payload.sub_recipes.clone()).await;
Ok(Json(AddSubRecipesResponse { success: true }))
}
Expand All @@ -222,7 +220,13 @@ 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 = match state.get_session_agent(payload.session_id.clone()).await {
Ok(agent) => agent,
Err(e) => {
tracing::error!("Failed to get session agent: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
agent.extend_system_prompt(payload.extension.clone()).await;
Ok(Json(ExtendPromptResponse { success: true }))
}
Expand All @@ -247,7 +251,13 @@ 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 = match state.get_session_agent(query.session_id.clone()).await {
Comment thread
tlongwell-block marked this conversation as resolved.
Outdated
Ok(agent) => agent,
Err(e) => {
tracing::error!("Failed to get session agent: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
let permission_manager = PermissionManager::default();

let mut tools: Vec<ToolInfo> = agent
Expand Down Expand Up @@ -299,7 +309,15 @@ async fn update_agent_provider(
State(state): State<Arc<AppState>>,
Json(payload): Json<UpdateProviderRequest>,
) -> Result<StatusCode, impl IntoResponse> {
let agent = state.get_agent().await;
let agent = match state.get_session_agent(payload.session_id.clone()).await {
Ok(agent) => agent,
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to get session agent: {}", e),
))
}
};
let config = Config::global();
let model = match payload
.model
Expand Down Expand Up @@ -344,9 +362,16 @@ async fn update_agent_provider(
)]
async fn update_router_tool_selector(
State(state): State<Arc<AppState>>,
Json(_payload): Json<UpdateRouterToolSelectorRequest>,
Json(payload): Json<UpdateRouterToolSelectorRequest>,
) -> Result<Json<String>, Json<ErrorResponse>> {
let agent = state.get_agent().await;
let agent = match state.get_session_agent(payload.session_id.clone()).await {
Ok(agent) => agent,
Err(e) => {
return Err(Json(ErrorResponse {
error: format!("Failed to get session agent: {}", e),
}))
}
};
agent
.update_router_tool_selector(None, Some(true))
.await
Expand Down Expand Up @@ -377,7 +402,14 @@ 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;
let agent = match state.get_session_agent(payload.session_id.clone()).await {
Ok(agent) => agent,
Err(e) => {
return Err(Json(ErrorResponse {
error: format!("Failed to get session agent: {}", e),
}))
}
};
if let Some(response) = payload.response {
agent.add_final_output_tool(response).await;

Expand Down
17 changes: 11 additions & 6 deletions crates/goose-server/src/routes/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,11 @@ mod tests {

#[tokio::test]
async fn test_transcribe_endpoint_requires_auth() {
let state = AppState::new(Arc::new(goose::agents::Agent::new()));
let state = AppState::new();
let app = routes(state);
// Note: This test is checking that the endpoint exists
// In production, authentication is handled by middleware
// applied at the router level, not in individual routes

// Test without auth header
let request = Request::builder()
Expand All @@ -413,12 +416,14 @@ mod tests {
.unwrap();

let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
// Without auth middleware and without OpenAI API key configured,
// the endpoint returns PRECONDITION_FAILED (412)
assert_eq!(response.status(), StatusCode::PRECONDITION_FAILED);
}

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

// Create a large base64 string (simulating > 25MB audio)
Expand All @@ -444,7 +449,7 @@ mod tests {

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

let request = Request::builder()
Expand All @@ -469,8 +474,8 @@ mod tests {
}

#[tokio::test]
async fn test_transcribe_endpoint_handles_invalid_base64() {
let state = AppState::new(Arc::new(goose::agents::Agent::new()));
async fn test_transcribe_endpoint_validates_base64() {
let state = AppState::new();
let app = routes(state);

let request = Request::builder()
Expand Down
11 changes: 10 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: Option<String>,
Comment thread
tlongwell-block marked this conversation as resolved.
Outdated
}

/// Response from context management operations
Expand Down Expand Up @@ -44,7 +46,14 @@ async fn manage_context(
State(state): State<Arc<AppState>>,
Json(request): Json<ContextManageRequest>,
) -> Result<Json<ContextManageResponse>, StatusCode> {
let agent = state.get_agent().await;
// Get session-specific agent
let agent = state
.get_session_agent(request.session_id)
.await
.map_err(|e| {
tracing::error!("Failed to get session agent: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;

let mut processed_messages = Conversation::new_unvalidated(vec![]);
let mut token_counts: Vec<usize> = vec![];
Expand Down
42 changes: 38 additions & 4 deletions crates/goose-server/src/routes/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@ async fn add_extension(
serde_json::to_string_pretty(&raw.0).unwrap()
);

// Try to extract session_id from the raw JSON
let session_id = raw
Comment thread
tlongwell-block marked this conversation as resolved.
Outdated
.0
.get("session_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());

// Remove session_id from the object before parsing the extension config
let mut config_json = raw.0.clone();
config_json
.as_object_mut()
.map(|obj| obj.remove("session_id"));

// Try to parse into our enum
let request: ExtensionConfigRequest = match serde_json::from_value(raw.0.clone()) {
Ok(req) => req,
Expand Down Expand Up @@ -267,7 +280,14 @@ async fn add_extension(
},
};

let agent = state.get_agent().await;
// Get session-specific agent
let agent = state
.get_session_agent(session_id.clone())
.await
.map_err(|e| {
tracing::error!("Failed to get session agent: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
let response = agent.add_extension(extension_config).await;

// Respond with the result.
Expand All @@ -289,13 +309,27 @@ async fn add_extension(
}
}

/// Request for removing an extension
#[derive(Deserialize)]
struct RemoveExtensionRequest {
name: String,
session_id: Option<String>,
}

/// Handler for removing an extension by name
async fn remove_extension(
State(state): State<Arc<AppState>>,
Json(name): Json<String>,
Json(request): Json<RemoveExtensionRequest>,
) -> Result<Json<ExtensionResponse>, StatusCode> {
let agent = state.get_agent().await;
match agent.remove_extension(&name).await {
let agent = state
.get_session_agent(request.session_id)
.await
.map_err(|e| {
tracing::error!("Failed to get session agent: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;

match agent.remove_extension(&request.name).await {
Ok(_) => Ok(Json(ExtensionResponse {
error: false,
message: None,
Expand Down
17 changes: 16 additions & 1 deletion crates/goose-server/src/routes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub struct CreateRecipeRequest {
activities: Option<Vec<String>>,
#[serde(default)]
author: Option<AuthorRequest>,
// Optional session ID for session-specific agent
session_id: Option<String>,
}

#[derive(Debug, Deserialize, ToSchema)]
Expand Down Expand Up @@ -114,7 +116,20 @@ async fn create_recipe(
request.messages.len()
);

let agent = state.get_agent().await;
// Get session-specific agent
let agent = state
.get_session_agent(request.session_id)
.await
.map_err(|e| {
tracing::error!("Failed to get session agent: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(CreateRecipeResponse {
recipe: None,
error: Some(e.to_string()),
}),
)
})?;
Comment thread
tlongwell-block marked this conversation as resolved.
Outdated

// Create base recipe from agent state and messages
let recipe_result = agent
Expand Down
Loading
Loading