mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: persist GooseMode per-session via session DB (#7854)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -375,12 +375,14 @@ impl GooseAcpAgent {
|
||||
&self,
|
||||
cx: Option<&JrConnectionCx<AgentToClient>>,
|
||||
session_id: Option<&SessionId>,
|
||||
goose_mode: Option<GooseMode>,
|
||||
) -> Result<Arc<Agent>> {
|
||||
let mode = goose_mode.unwrap_or(self.goose_mode);
|
||||
let agent = Agent::with_config(AgentConfig::new(
|
||||
Arc::clone(&self.session_manager),
|
||||
Arc::clone(&self.permission_manager),
|
||||
None,
|
||||
self.goose_mode,
|
||||
mode,
|
||||
self.disable_session_naming,
|
||||
GoosePlatform::GooseCli,
|
||||
));
|
||||
@@ -806,6 +808,7 @@ impl GooseAcpAgent {
|
||||
args.cwd.clone(),
|
||||
"ACP Session".to_string(),
|
||||
SessionType::User,
|
||||
self.goose_mode,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -815,7 +818,7 @@ impl GooseAcpAgent {
|
||||
let session_id = SessionId::new(goose_session.id.clone());
|
||||
|
||||
let agent = self
|
||||
.create_agent_for_session(Some(cx), Some(&session_id))
|
||||
.create_agent_for_session(Some(cx), Some(&session_id), None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to create agent: {}", e))
|
||||
@@ -925,10 +928,11 @@ impl GooseAcpAgent {
|
||||
.data(format!("Failed to load session {}: {}", session_id, e))
|
||||
})?;
|
||||
|
||||
let loaded_mode = goose_session.goose_mode;
|
||||
let acp_session_id = SessionId::new(session_id.clone());
|
||||
|
||||
let agent = self
|
||||
.create_agent_for_session(Some(cx), Some(&acp_session_id))
|
||||
.create_agent_for_session(Some(cx), Some(&acp_session_id), Some(loaded_mode))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to create agent: {}", e))
|
||||
@@ -1014,8 +1018,7 @@ impl GooseAcpAgent {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
sessions.insert(session_id.clone(), session);
|
||||
|
||||
// TODO: read from goose_session.goose_mode after #7603
|
||||
let goose_mode = self.goose_mode;
|
||||
let goose_mode = loaded_mode;
|
||||
|
||||
info!(
|
||||
session_id = %session_id,
|
||||
@@ -1168,15 +1171,13 @@ impl GooseAcpAgent {
|
||||
sacp::Error::invalid_params().data(format!("Invalid mode: {}", mode_id))
|
||||
})?;
|
||||
|
||||
self.get_session_agent(session_id, None).await?;
|
||||
|
||||
// Reject mode changes until per-session persistence lands (#7603)
|
||||
if mode != self.goose_mode {
|
||||
return Err(sacp::Error::invalid_params().data(format!(
|
||||
"Mode change not supported: session is {}, requested {}",
|
||||
self.goose_mode, mode_id
|
||||
)));
|
||||
}
|
||||
let agent = self.get_session_agent(session_id, None).await?;
|
||||
agent
|
||||
.update_goose_mode(mode, session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to update mode: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(SetSessionModeResponse::new())
|
||||
}
|
||||
|
||||
@@ -753,7 +753,6 @@ macro_rules! tests_mode_set_error {
|
||||
($conn:ty) => {
|
||||
#[test_case::test_case("not_a_mode", None, sacp::Error::invalid_params().data("Invalid mode: not_a_mode") ; "invalid mode")]
|
||||
#[test_case::test_case("auto", Some("nonexistent-session-id"), sacp::Error::invalid_params().data("Session not found: nonexistent-session-id") ; "session not found")]
|
||||
#[test_case::test_case("approve", None, sacp::Error::invalid_params().data("Mode change not supported: session is auto, requested approve") ; "mode change rejected")]
|
||||
fn test_mode_set_error(
|
||||
mode_id: &'static str,
|
||||
session_id: Option<&'static str>,
|
||||
|
||||
@@ -59,7 +59,6 @@ fn test_load_session_mcp() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: on_set_mode is a no-op until mode is threaded per-session (#7603)"]
|
||||
fn test_mode_set() {
|
||||
run_test(async { run_mode_set::<ClientToProviderConnection>().await });
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ fn test_initialize_doesnt_hit_provider() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: on_set_mode is a no-op until mode is threaded per-session (#7603)"]
|
||||
fn test_load_mode() {
|
||||
run_test(async { run_load_mode::<ClientToAgentConnection>().await });
|
||||
}
|
||||
@@ -53,7 +52,6 @@ fn test_load_session_mcp() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: on_set_mode is a no-op until mode is threaded per-session (#7603)"]
|
||||
fn test_mode_set() {
|
||||
run_test(async { run_mode_set::<ClientToAgentConnection>().await });
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||
use clap_complete::{generate, Shell as ClapShell};
|
||||
use goose::builtin_extension::register_builtin_extensions;
|
||||
use goose::config::Config;
|
||||
use goose::config::{Config, GooseMode};
|
||||
use goose::posthog::get_telemetry_choice;
|
||||
use goose::recipe::Recipe;
|
||||
use goose_mcp::mcp_server_runner::{serve, McpCommand};
|
||||
@@ -356,6 +356,7 @@ async fn get_or_create_session_id(
|
||||
identifier: Option<Identifier>,
|
||||
resume: bool,
|
||||
no_session: bool,
|
||||
goose_mode: GooseMode,
|
||||
) -> Result<Option<String>> {
|
||||
if no_session {
|
||||
return Ok(None);
|
||||
@@ -399,6 +400,7 @@ async fn get_or_create_session_id(
|
||||
std::env::current_dir()?,
|
||||
"CLI Session".to_string(),
|
||||
SessionType::User,
|
||||
goose_mode,
|
||||
)
|
||||
.await?;
|
||||
return Ok(Some(session.id));
|
||||
@@ -411,7 +413,12 @@ async fn get_or_create_session_id(
|
||||
let has_user_provided_name = id.name.is_some();
|
||||
let name = id.name.unwrap_or_else(|| "CLI Session".to_string());
|
||||
let session = session_manager
|
||||
.create_session(std::env::current_dir()?, name.clone(), SessionType::User)
|
||||
.create_session(
|
||||
std::env::current_dir()?,
|
||||
name.clone(),
|
||||
SessionType::User,
|
||||
goose_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if has_user_provided_name {
|
||||
@@ -1129,7 +1136,8 @@ async fn handle_interactive_session(
|
||||
}
|
||||
}
|
||||
|
||||
let mut session_id = get_or_create_session_id(identifier, resume, false).await?;
|
||||
let goose_mode = Config::global().get_goose_mode().unwrap_or_default();
|
||||
let mut session_id = get_or_create_session_id(identifier, resume, false, goose_mode).await?;
|
||||
|
||||
if fork {
|
||||
if let Some(id) = session_id {
|
||||
@@ -1342,8 +1350,14 @@ async fn handle_run_command(
|
||||
}
|
||||
}
|
||||
|
||||
let session_id =
|
||||
get_or_create_session_id(identifier, run_behavior.resume, run_behavior.no_session).await?;
|
||||
let goose_mode = Config::global().get_goose_mode().unwrap_or_default();
|
||||
let session_id = get_or_create_session_id(
|
||||
identifier,
|
||||
run_behavior.resume,
|
||||
run_behavior.no_session,
|
||||
goose_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut session = build_session(SessionBuilderConfig {
|
||||
session_id,
|
||||
@@ -1629,7 +1643,8 @@ async fn handle_default_session() -> Result<()> {
|
||||
configure_telemetry_consent_dialog()?;
|
||||
}
|
||||
|
||||
let session_id = get_or_create_session_id(None, false, false).await?;
|
||||
let goose_mode = Config::global().get_goose_mode().unwrap_or_default();
|
||||
let session_id = get_or_create_session_id(None, false, false, goose_mode).await?;
|
||||
|
||||
let mut session = build_session(SessionBuilderConfig {
|
||||
session_id,
|
||||
|
||||
@@ -1557,6 +1557,7 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
|
||||
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
|
||||
"Tool Permission Configuration".to_string(),
|
||||
SessionType::Hidden,
|
||||
agent.config.goose_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono;
|
||||
use goose::config::Config;
|
||||
use goose::conversation::message::{Message, MessageContent, MessageMetadata};
|
||||
use goose::session::{SessionManager, SessionType};
|
||||
use rmcp::model::Role;
|
||||
@@ -138,6 +139,7 @@ pub async fn handle_term_init(
|
||||
working_dir,
|
||||
"Goose Term Session".to_string(),
|
||||
SessionType::Terminal,
|
||||
Config::global().get_goose_mode().unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ where
|
||||
PathBuf::default(),
|
||||
"scenario-runner".to_string(),
|
||||
SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::CliSession;
|
||||
use console::style;
|
||||
use goose::agents::{Agent, Container, ExtensionError};
|
||||
use goose::config::resolve_extensions_for_new_session;
|
||||
use goose::config::{get_all_extensions, Config, ExtensionConfig};
|
||||
use goose::config::{get_all_extensions, Config, ExtensionConfig, GooseMode};
|
||||
use goose::providers::create;
|
||||
use goose::recipe::Recipe;
|
||||
use goose::session::session_manager::SessionType;
|
||||
@@ -201,6 +201,7 @@ async fn offer_extension_debugging_help(
|
||||
std::env::current_dir()?,
|
||||
"CLI Session".to_string(),
|
||||
SessionType::Hidden,
|
||||
debug_agent.config.goose_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -400,11 +401,17 @@ fn resolve_provider_and_model(
|
||||
async fn resolve_session_id(
|
||||
session_config: &SessionBuilderConfig,
|
||||
session_manager: &goose::session::session_manager::SessionManager,
|
||||
goose_mode: GooseMode,
|
||||
) -> String {
|
||||
if session_config.no_session {
|
||||
let working_dir = std::env::current_dir().expect("Could not get working directory");
|
||||
let session = session_manager
|
||||
.create_session(working_dir, "CLI Session".to_string(), SessionType::Hidden)
|
||||
.create_session(
|
||||
working_dir,
|
||||
"CLI Session".to_string(),
|
||||
SessionType::Hidden,
|
||||
goose_mode,
|
||||
)
|
||||
.await
|
||||
.expect("Could not create session");
|
||||
session.id
|
||||
@@ -606,7 +613,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
.apply_recipe_components(recipe.and_then(|r| r.response.clone()), true)
|
||||
.await;
|
||||
|
||||
let session_id = resolve_session_id(&session_config, &session_manager).await;
|
||||
let session_id =
|
||||
resolve_session_id(&session_config, &session_manager, agent.config.goose_mode).await;
|
||||
|
||||
if session_config.resume {
|
||||
handle_resumed_session_workdir(&agent, &session_id, session_config.interactive).await;
|
||||
@@ -661,6 +669,14 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
agent
|
||||
.update_goose_mode(agent.config.goose_mode, &session_id)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
output::render_error(&format!("Failed to set session mode: {}", e));
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
if let Some(recipe) = session_config.recipe.clone() {
|
||||
if let Err(e) = session_manager
|
||||
.update(&session_id)
|
||||
|
||||
@@ -886,7 +886,7 @@ impl CliSession {
|
||||
self.run_mode = RunMode::Normal;
|
||||
// set goose mode: auto if that isn't already the case
|
||||
let config = Config::global();
|
||||
let curr_goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
let curr_goose_mode = config.get_goose_mode().unwrap_or_default();
|
||||
if curr_goose_mode != GooseMode::Auto {
|
||||
config.set_goose_mode(GooseMode::Auto).unwrap();
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::agent_add_extension,
|
||||
super::routes::agent::agent_remove_extension,
|
||||
super::routes::agent::update_agent_provider,
|
||||
super::routes::agent::update_session,
|
||||
super::routes::action_required::confirm_tool_action,
|
||||
super::routes::reply::reply,
|
||||
super::routes::session::list_sessions,
|
||||
@@ -579,6 +580,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
ModelInfo,
|
||||
ModelConfig,
|
||||
Session,
|
||||
goose::config::goose_mode::GooseMode,
|
||||
SessionInsights,
|
||||
SessionType,
|
||||
SystemInfo,
|
||||
@@ -625,6 +627,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
goose::agents::types::RetryConfig,
|
||||
goose::agents::types::SuccessCheck,
|
||||
super::routes::agent::UpdateProviderRequest,
|
||||
super::routes::agent::UpdateSessionRequest,
|
||||
super::routes::agent::GetToolsQuery,
|
||||
super::routes::agent::ReadResourceRequest,
|
||||
super::routes::agent::ReadResourceResponse,
|
||||
|
||||
@@ -50,6 +50,12 @@ pub struct UpdateProviderRequest {
|
||||
request_params: Option<std::collections::HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateSessionRequest {
|
||||
session_id: String,
|
||||
goose_mode: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct GetToolsQuery {
|
||||
extension_name: Option<String>,
|
||||
@@ -233,9 +239,16 @@ async fn start_agent(
|
||||
let name = "New Chat".to_string();
|
||||
|
||||
let manager = state.session_manager();
|
||||
let config = Config::global();
|
||||
let current_mode = config.get_goose_mode().unwrap_or_default();
|
||||
|
||||
let mut session = manager
|
||||
.create_session(PathBuf::from(&working_dir), name, SessionType::User)
|
||||
.create_session(
|
||||
PathBuf::from(&working_dir),
|
||||
name,
|
||||
SessionType::User,
|
||||
current_mode,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to create session: {}", err);
|
||||
@@ -251,6 +264,7 @@ async fn start_agent(
|
||||
.and_then(|r| r.extensions.as_deref());
|
||||
let extensions_to_use =
|
||||
resolve_extensions_for_new_session(recipe_extensions, extension_overrides);
|
||||
|
||||
let mut extension_data = session.extension_data.clone();
|
||||
let extensions_state = EnabledExtensionsState::new(extensions_to_use);
|
||||
if let Err(e) = extensions_state.to_extension_data(&mut extension_data) {
|
||||
@@ -492,10 +506,9 @@ async fn get_tools(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<GetToolsQuery>,
|
||||
) -> Result<Json<Vec<ToolInfo>>, StatusCode> {
|
||||
let config = Config::global();
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
let session_id = query.session_id;
|
||||
let agent = state.get_agent_for_route(session_id.clone()).await?;
|
||||
let goose_mode = agent.goose_mode().await;
|
||||
let permission_manager = agent.config.permission_manager.clone();
|
||||
|
||||
let mut tools: Vec<ToolInfo> = agent
|
||||
@@ -594,6 +607,59 @@ async fn update_agent_provider(
|
||||
)
|
||||
})?;
|
||||
|
||||
// Propagate session mode to the new provider
|
||||
let mode = agent.goose_mode().await;
|
||||
agent
|
||||
.update_goose_mode(mode, &payload.session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to propagate mode to provider: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/update_session",
|
||||
request_body = UpdateSessionRequest,
|
||||
responses(
|
||||
(status = 200, description = "Session updated"),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 500, description = "Internal error")
|
||||
)
|
||||
)]
|
||||
async fn update_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<UpdateSessionRequest>,
|
||||
) -> Result<(), (StatusCode, String)> {
|
||||
let agent = state
|
||||
.get_agent_for_route(payload.session_id.clone())
|
||||
.await
|
||||
.map_err(|e| (e, "No agent for session id".to_owned()))?;
|
||||
|
||||
if let Some(mode_str) = payload.goose_mode {
|
||||
let mode: GooseMode = mode_str.parse().map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid mode: {}", mode_str),
|
||||
)
|
||||
})?;
|
||||
|
||||
agent
|
||||
.update_goose_mode(mode, &payload.session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to update mode: {}", e),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1231,6 +1297,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/agent/export_app/{name}", get(export_app))
|
||||
.route("/agent/import_app", post(import_app))
|
||||
.route("/agent/update_provider", post(update_agent_provider))
|
||||
.route("/agent/update_session", post(update_session))
|
||||
.route("/agent/update_from_session", post(update_from_session))
|
||||
.route("/agent/add_extension", post(agent_add_extension))
|
||||
.route("/agent/remove_extension", post(agent_remove_extension))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use dotenvy::dotenv;
|
||||
use futures::StreamExt;
|
||||
use goose::agents::{Agent, AgentEvent, ExtensionConfig, SessionConfig};
|
||||
use goose::config::{DEFAULT_EXTENSION_DESCRIPTION, DEFAULT_EXTENSION_TIMEOUT};
|
||||
use goose::config::{GooseMode, DEFAULT_EXTENSION_DESCRIPTION, DEFAULT_EXTENSION_TIMEOUT};
|
||||
use goose::conversation::message::Message;
|
||||
use goose::providers::create_with_named_model;
|
||||
use goose::providers::databricks::DATABRICKS_DEFAULT_MODEL;
|
||||
@@ -24,6 +24,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
PathBuf::default(),
|
||||
"max-turn-test".to_string(),
|
||||
SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -286,28 +286,33 @@ impl Provider for AcpProvider {
|
||||
}
|
||||
|
||||
async fn update_mode(&self, session_id: &str, mode: GooseMode) -> Result<(), ProviderError> {
|
||||
let _acp_session_id = self
|
||||
.goose_to_acp_id
|
||||
.lock()
|
||||
let map = self.goose_to_acp_id.lock().await;
|
||||
if map.is_empty() {
|
||||
// Pre-initialization: no ACP session yet, just store the mode.
|
||||
// The shared Arc<Mutex<GooseMode>> is read at session creation time.
|
||||
drop(map);
|
||||
} else if let Some(acp_session_id) = map.get(session_id).map(|r| r.session_id.clone()) {
|
||||
drop(map);
|
||||
self.send_untyped(
|
||||
"session/set_mode",
|
||||
serde_json::json!({
|
||||
"sessionId": acp_session_id,
|
||||
"modeId": mode.to_string().to_lowercase()
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.get(session_id)
|
||||
.map(|r| r.session_id.clone())
|
||||
.ok_or_else(|| {
|
||||
ProviderError::RequestFailed(format!("Session not found: {session_id}"))
|
||||
})?;
|
||||
|
||||
let current = self
|
||||
.goose_mode
|
||||
.lock()
|
||||
.map_err(|_| ProviderError::RequestFailed("Failed to read mode".into()))?;
|
||||
|
||||
if mode != *current {
|
||||
// TODO: "session/set_mode" when session-scoped mode lands (#7603)
|
||||
.map_err(|e| ProviderError::RequestFailed(format!("Failed to set mode: {e}")))?;
|
||||
} else {
|
||||
return Err(ProviderError::RequestFailed(format!(
|
||||
"Mode change not supported: session is {}, requested {}",
|
||||
current, mode
|
||||
"Session not found: {session_id}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut current = self
|
||||
.goose_mode
|
||||
.lock()
|
||||
.map_err(|_| ProviderError::RequestFailed("Failed to update mode".into()))?;
|
||||
*current = mode;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ impl AgentConfig {
|
||||
pub struct Agent {
|
||||
pub(super) provider: SharedProvider,
|
||||
pub config: AgentConfig,
|
||||
pub(super) current_goose_mode: Mutex<GooseMode>,
|
||||
|
||||
pub extension_manager: Arc<ExtensionManager>,
|
||||
pub(super) final_output_tool: Arc<Mutex<Option<FinalOutputTool>>>,
|
||||
@@ -203,14 +204,13 @@ where
|
||||
|
||||
impl Agent {
|
||||
pub fn new() -> Self {
|
||||
let config = Config::global();
|
||||
Self::with_config(AgentConfig::new(
|
||||
Arc::new(SessionManager::instance()),
|
||||
PermissionManager::instance(),
|
||||
None,
|
||||
Config::global().get_goose_mode().unwrap_or(GooseMode::Auto),
|
||||
Config::global()
|
||||
.get_goose_disable_session_naming()
|
||||
.unwrap_or(false),
|
||||
config.get_goose_mode().unwrap_or_default(),
|
||||
config.get_goose_disable_session_naming().unwrap_or(false),
|
||||
GoosePlatform::GooseCli,
|
||||
))
|
||||
}
|
||||
@@ -220,6 +220,7 @@ impl Agent {
|
||||
let provider = Arc::new(Mutex::new(None));
|
||||
|
||||
let goose_platform = config.goose_platform.clone();
|
||||
let initial_mode = config.goose_mode;
|
||||
let capabilities = match config.goose_platform {
|
||||
GoosePlatform::GooseDesktop => ExtensionManagerCapabilities { mcpui: true },
|
||||
GoosePlatform::GooseCli => ExtensionManagerCapabilities { mcpui: false },
|
||||
@@ -229,6 +230,7 @@ impl Agent {
|
||||
Self {
|
||||
provider: provider.clone(),
|
||||
config,
|
||||
current_goose_mode: Mutex::new(initial_mode),
|
||||
extension_manager: Arc::new(ExtensionManager::new(
|
||||
provider.clone(),
|
||||
session_manager,
|
||||
@@ -351,7 +353,9 @@ impl Agent {
|
||||
.prepare_tools_and_prompt(session_id, working_dir)
|
||||
.await?;
|
||||
|
||||
if self.config.goose_mode == GooseMode::SmartApprove {
|
||||
let goose_mode = *self.current_goose_mode.lock().await;
|
||||
|
||||
if goose_mode == GooseMode::SmartApprove {
|
||||
self.tool_inspection_manager.apply_tool_annotations(&tools);
|
||||
}
|
||||
|
||||
@@ -360,7 +364,7 @@ impl Agent {
|
||||
tools,
|
||||
toolshim_tools,
|
||||
system_prompt,
|
||||
goose_mode: self.config.goose_mode,
|
||||
goose_mode,
|
||||
tool_call_cut_off: Config::global()
|
||||
.get_param::<usize>("GOOSE_TOOL_CALL_CUTOFF")
|
||||
.unwrap_or(10),
|
||||
@@ -1721,6 +1725,28 @@ impl Agent {
|
||||
.context("Failed to persist provider config to session")
|
||||
}
|
||||
|
||||
pub async fn update_goose_mode(&self, mode: GooseMode, session_id: &str) -> Result<()> {
|
||||
if let Some(provider) = self.provider.lock().await.as_ref() {
|
||||
provider
|
||||
.update_mode(session_id, mode)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Provider rejected mode update: {e}"))?;
|
||||
}
|
||||
*self.current_goose_mode.lock().await = mode;
|
||||
self.config
|
||||
.session_manager
|
||||
.clone()
|
||||
.update(session_id)
|
||||
.goose_mode(mode)
|
||||
.apply()
|
||||
.await
|
||||
.context("Failed to persist goose_mode to session")
|
||||
}
|
||||
|
||||
pub async fn goose_mode(&self) -> GooseMode {
|
||||
*self.current_goose_mode.lock().await
|
||||
}
|
||||
|
||||
/// Restore the provider from session data or fall back to global config
|
||||
/// This is used when resuming a session to restore the provider state
|
||||
pub async fn restore_provider_from_session(&self, session: &Session) -> Result<()> {
|
||||
@@ -1752,7 +1778,16 @@ impl Agent {
|
||||
.await
|
||||
.map_err(|e| anyhow!("Could not create provider: {}", e))?;
|
||||
|
||||
self.update_provider(provider, &session.id).await
|
||||
self.update_provider(provider, &session.id).await?;
|
||||
// Propagate session mode to the new provider
|
||||
if let Some(provider) = self.provider.lock().await.as_ref() {
|
||||
provider
|
||||
.update_mode(&session.id, session.goose_mode)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to propagate mode to provider: {}", e))?;
|
||||
}
|
||||
*self.current_goose_mode.lock().await = session.goose_mode;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Override the system prompt with a custom template
|
||||
@@ -1864,12 +1899,14 @@ impl Agent {
|
||||
let model_name = &model_config.model_name;
|
||||
tracing::debug!("Using model: {}", model_name);
|
||||
|
||||
let goose_mode = *self.current_goose_mode.lock().await;
|
||||
let prompt_manager = self.prompt_manager.lock().await;
|
||||
let system_prompt = prompt_manager
|
||||
.builder()
|
||||
.with_extensions(extensions_info.into_iter())
|
||||
.with_frontend_instructions(self.frontend_instructions.lock().await.clone())
|
||||
.with_extension_and_tool_counts(extension_count, tool_count)
|
||||
.with_goose_mode(goose_mode)
|
||||
.build();
|
||||
|
||||
let recipe_prompt = prompt_manager.get_recipe_prompt().await;
|
||||
@@ -2220,7 +2257,10 @@ mod tests {
|
||||
);
|
||||
|
||||
let prompt_manager = agent.prompt_manager.lock().await;
|
||||
let system_prompt = prompt_manager.builder().build();
|
||||
let system_prompt = prompt_manager
|
||||
.builder()
|
||||
.with_goose_mode(GooseMode::default())
|
||||
.build();
|
||||
|
||||
let final_output_tool_ref = agent.final_output_tool.lock().await;
|
||||
let final_output_tool_system_prompt =
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::agents::subagent_task_config::{TaskConfig, DEFAULT_SUBAGENT_MAX_TURNS
|
||||
use crate::agents::tool_execution::ToolCallContext;
|
||||
use crate::agents::AgentConfig;
|
||||
use crate::config::paths::Paths;
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, GooseMode};
|
||||
use crate::providers;
|
||||
use crate::recipe::build_recipe::build_recipe_from_template;
|
||||
use crate::recipe::local_recipes::load_local_recipe_file;
|
||||
@@ -1243,11 +1243,14 @@ impl SummonClient {
|
||||
.await
|
||||
.map_err(|e| format!("Failed to build task config: {}", e))?;
|
||||
|
||||
// Subagents must use Auto until get_agent_messages forwards
|
||||
// ActionRequired messages to the parent. Until then, any mode
|
||||
// that requires approval will hang on the subagent's confirmation_rx.
|
||||
let agent_config = AgentConfig::new(
|
||||
self.context.session_manager.clone(),
|
||||
crate::config::permission::PermissionManager::instance(),
|
||||
None,
|
||||
crate::config::GooseMode::Auto,
|
||||
GooseMode::Auto,
|
||||
true, // disable session naming for subagents
|
||||
crate::agents::GoosePlatform::GooseCli,
|
||||
);
|
||||
@@ -1259,6 +1262,7 @@ impl SummonClient {
|
||||
working_dir,
|
||||
"Delegated task".to_string(),
|
||||
SessionType::SubAgent,
|
||||
GooseMode::Auto,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create subagent session: {}", e))?;
|
||||
@@ -1704,11 +1708,14 @@ impl SummonClient {
|
||||
|
||||
let description = truncate(&Self::get_task_description(¶ms), 40);
|
||||
|
||||
// Subagents must use Auto until get_agent_messages forwards
|
||||
// ActionRequired messages to the parent. Until then, any mode
|
||||
// that requires approval will hang on the subagent's confirmation_rx.
|
||||
let agent_config = AgentConfig::new(
|
||||
self.context.session_manager.clone(),
|
||||
crate::config::permission::PermissionManager::instance(),
|
||||
None,
|
||||
crate::config::GooseMode::Auto,
|
||||
GooseMode::Auto,
|
||||
true, // disable session naming for subagents
|
||||
crate::agents::GoosePlatform::GooseCli,
|
||||
);
|
||||
@@ -1716,7 +1723,12 @@ impl SummonClient {
|
||||
let subagent_session = self
|
||||
.context
|
||||
.session_manager
|
||||
.create_session(working_dir, description.clone(), SessionType::SubAgent)
|
||||
.create_session(
|
||||
working_dir,
|
||||
description.clone(),
|
||||
SessionType::SubAgent,
|
||||
GooseMode::Auto,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create subagent session: {}", e))?;
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ pub struct SystemPromptBuilder<'a, M> {
|
||||
subagents_enabled: bool,
|
||||
hints: Option<String>,
|
||||
code_execution_mode: bool,
|
||||
goose_mode: Option<GooseMode>,
|
||||
}
|
||||
|
||||
impl<'a> SystemPromptBuilder<'a, PromptManager> {
|
||||
@@ -106,6 +107,11 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_goose_mode(mut self, mode: GooseMode) -> Self {
|
||||
self.goose_mode = Some(mode);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> String {
|
||||
let mut extensions_info = self.extensions_info;
|
||||
|
||||
@@ -128,8 +134,9 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let config = Config::global();
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
let goose_mode = self
|
||||
.goose_mode
|
||||
.unwrap_or_else(|| Config::global().get_goose_mode().unwrap_or_default());
|
||||
|
||||
let extension_tool_limits = self
|
||||
.extension_tool_count
|
||||
@@ -250,6 +257,7 @@ impl PromptManager {
|
||||
subagents_enabled: false,
|
||||
hints: None,
|
||||
code_execution_mode: false,
|
||||
goose_mode: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,6 +411,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_all_platform_extensions() {
|
||||
use crate::agents::platform_extensions::{PlatformExtensionContext, PLATFORM_EXTENSIONS};
|
||||
use crate::config::GooseMode;
|
||||
use crate::session::SessionManager;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -413,6 +422,7 @@ mod tests {
|
||||
tmp_dir.path().to_path_buf(),
|
||||
"test session".to_owned(),
|
||||
crate::session::SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -181,6 +181,8 @@ impl Agent {
|
||||
let provider = self.provider().await?;
|
||||
let model_config = provider.get_model_config();
|
||||
|
||||
let goose_mode = *self.current_goose_mode.lock().await;
|
||||
|
||||
let prompt_manager = self.prompt_manager.lock().await;
|
||||
let mut system_prompt = prompt_manager
|
||||
.builder()
|
||||
@@ -189,6 +191,7 @@ impl Agent {
|
||||
.with_extension_and_tool_counts(extension_count, tool_count)
|
||||
.with_code_execution_mode(code_execution_active)
|
||||
.with_hints(working_dir)
|
||||
.with_goose_mode(goose_mode)
|
||||
.build();
|
||||
|
||||
// Handle toolshim if enabled
|
||||
@@ -435,6 +438,7 @@ impl Agent {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::GooseMode;
|
||||
use crate::conversation::message::Message;
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::{Provider, ProviderUsage, Usage};
|
||||
@@ -483,6 +487,7 @@ mod tests {
|
||||
std::env::current_dir().unwrap(),
|
||||
"test-prepare-tools".to_string(),
|
||||
SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct AgentManager {
|
||||
scheduler: Arc<dyn SchedulerTrait>,
|
||||
session_manager: Arc<SessionManager>,
|
||||
default_provider: Arc<RwLock<Option<Arc<dyn crate::providers::base::Provider>>>>,
|
||||
default_mode: GooseMode,
|
||||
}
|
||||
|
||||
impl AgentManager {
|
||||
@@ -28,6 +29,7 @@ impl AgentManager {
|
||||
session_manager: Arc<SessionManager>,
|
||||
schedule_file_path: std::path::PathBuf,
|
||||
max_sessions: Option<usize>,
|
||||
default_mode: GooseMode,
|
||||
) -> Result<Self> {
|
||||
let scheduler = Scheduler::new(schedule_file_path, session_manager.clone()).await?;
|
||||
|
||||
@@ -39,6 +41,7 @@ impl AgentManager {
|
||||
scheduler,
|
||||
session_manager,
|
||||
default_provider: Arc::new(RwLock::new(None)),
|
||||
default_mode,
|
||||
};
|
||||
|
||||
Ok(manager)
|
||||
@@ -47,13 +50,20 @@ impl AgentManager {
|
||||
pub async fn instance() -> Result<Arc<Self>> {
|
||||
AGENT_MANAGER
|
||||
.get_or_try_init(|| async {
|
||||
let max_sessions = Config::global()
|
||||
let config = Config::global();
|
||||
let max_sessions = config
|
||||
.get_goose_max_active_agents()
|
||||
.unwrap_or(DEFAULT_MAX_SESSION);
|
||||
let default_mode = config.get_goose_mode().unwrap_or_default();
|
||||
let schedule_file_path = Paths::data_dir().join("schedule.json");
|
||||
let session_manager = Arc::new(SessionManager::instance());
|
||||
let manager =
|
||||
Self::new(session_manager, schedule_file_path, Some(max_sessions)).await?;
|
||||
let manager = Self::new(
|
||||
session_manager,
|
||||
schedule_file_path,
|
||||
Some(max_sessions),
|
||||
default_mode,
|
||||
)
|
||||
.await?;
|
||||
Ok(Arc::new(manager))
|
||||
})
|
||||
.await
|
||||
@@ -82,8 +92,14 @@ impl AgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
let mode = Config::global().get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
let mut mode = self.default_mode;
|
||||
let permission_manager = PermissionManager::instance();
|
||||
|
||||
if let Ok(session) = self.session_manager.get_session(&session_id, false).await {
|
||||
mode = session.goose_mode;
|
||||
info!(goose_mode = %mode, session_id = %session_id, "Session loaded");
|
||||
}
|
||||
|
||||
let config = AgentConfig::new(
|
||||
Arc::clone(&self.session_manager),
|
||||
permission_manager,
|
||||
@@ -118,6 +134,10 @@ impl AgentManager {
|
||||
agent
|
||||
.update_provider(Arc::clone(provider), &session_id)
|
||||
.await?;
|
||||
provider
|
||||
.update_mode(&session_id, mode)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to propagate mode to provider: {}", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +173,9 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::config::GooseMode;
|
||||
use crate::execution::SessionExecutionMode;
|
||||
use crate::session::SessionManager;
|
||||
|
||||
@@ -161,9 +184,14 @@ mod tests {
|
||||
async fn create_test_manager(temp_dir: &TempDir) -> AgentManager {
|
||||
let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf()));
|
||||
let schedule_path = temp_dir.path().join("schedule.json");
|
||||
AgentManager::new(session_manager, schedule_path, Some(100))
|
||||
.await
|
||||
.unwrap()
|
||||
AgentManager::new(
|
||||
session_manager,
|
||||
schedule_path,
|
||||
Some(100),
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -369,4 +397,59 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("not found"));
|
||||
}
|
||||
|
||||
#[test_case(GooseMode::Approve ; "approve")]
|
||||
#[test_case(GooseMode::Chat ; "chat")]
|
||||
#[test_case(GooseMode::SmartApprove ; "smart_approve")]
|
||||
#[tokio::test]
|
||||
async fn test_agent_inherits_session_mode(mode: GooseMode) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let manager = create_test_manager(&temp_dir).await;
|
||||
|
||||
let session = manager
|
||||
.session_manager()
|
||||
.create_session(
|
||||
temp_dir.path().to_path_buf(),
|
||||
"test".into(),
|
||||
crate::session::SessionType::User,
|
||||
mode,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let agent = manager.get_or_create_agent(session.id).await.unwrap();
|
||||
assert_eq!(agent.goose_mode().await, mode);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_session_mode_isolation() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let manager = create_test_manager(&temp_dir).await;
|
||||
let sm = manager.session_manager();
|
||||
|
||||
let s1 = sm
|
||||
.create_session(
|
||||
temp_dir.path().to_path_buf(),
|
||||
"s1".into(),
|
||||
crate::session::SessionType::User,
|
||||
GooseMode::Approve,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let s2 = sm
|
||||
.create_session(
|
||||
temp_dir.path().to_path_buf(),
|
||||
"s2".into(),
|
||||
crate::session::SessionType::User,
|
||||
GooseMode::Auto,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let a1 = manager.get_or_create_agent(s1.id).await.unwrap();
|
||||
let a2 = manager.get_or_create_agent(s2.id).await.unwrap();
|
||||
|
||||
assert_eq!(a1.goose_mode().await, GooseMode::Approve);
|
||||
assert_eq!(a2.goose_mode().await, GooseMode::Auto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,14 +128,19 @@ impl GatewayHandler {
|
||||
user.display_name.as_deref().unwrap_or(&user.user_id)
|
||||
);
|
||||
|
||||
let config = Config::global();
|
||||
let session = self
|
||||
.agent_manager
|
||||
.session_manager()
|
||||
.create_session(working_dir, session_name, SessionType::Gateway)
|
||||
.create_session(
|
||||
working_dir,
|
||||
session_name,
|
||||
SessionType::Gateway,
|
||||
config.get_goose_mode().unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let manager = self.agent_manager.session_manager();
|
||||
let config = Config::global();
|
||||
|
||||
// Store the current provider and model config on the session so the agent
|
||||
// can be restored after LRU eviction, matching the start_agent flow.
|
||||
@@ -197,6 +202,7 @@ impl GatewayHandler {
|
||||
let current_provider = config.get_goose_provider().ok();
|
||||
let current_model_name = config.get_goose_model().ok();
|
||||
let current_extensions = get_enabled_extensions();
|
||||
let current_mode = config.get_goose_mode().unwrap_or_default();
|
||||
|
||||
// --- what the session has ---
|
||||
let session_extensions: Vec<ExtensionConfig> =
|
||||
@@ -208,8 +214,9 @@ impl GatewayHandler {
|
||||
let model_changed = current_model_name.as_deref()
|
||||
!= session.model_config.as_ref().map(|m| m.model_name.as_str());
|
||||
let extensions_changed = current_extensions != session_extensions;
|
||||
let mode_changed = current_mode != session.goose_mode;
|
||||
|
||||
if !provider_changed && !model_changed && !extensions_changed {
|
||||
if !provider_changed && !model_changed && !extensions_changed && !mode_changed {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -218,6 +225,7 @@ impl GatewayHandler {
|
||||
provider_changed,
|
||||
model_changed,
|
||||
extensions_changed,
|
||||
mode_changed,
|
||||
"syncing gateway session with current config"
|
||||
);
|
||||
|
||||
@@ -242,6 +250,10 @@ impl GatewayHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if mode_changed {
|
||||
update = update.goose_mode(current_mode);
|
||||
}
|
||||
|
||||
update.apply().await?;
|
||||
Ok(extensions_changed)
|
||||
}
|
||||
|
||||
@@ -429,6 +429,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn sets_limits_from_canonical_model() {
|
||||
let _guard = env_lock::lock_env([
|
||||
("GOOSE_MAX_TOKENS", None::<&str>),
|
||||
("GOOSE_CONTEXT_LIMIT", None::<&str>),
|
||||
]);
|
||||
let config = ModelConfig::new_or_fail("gpt-4o").with_canonical_limits("openai");
|
||||
|
||||
assert_eq!(config.context_limit, Some(128_000));
|
||||
@@ -438,6 +442,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn does_not_override_existing_context_limit() {
|
||||
let _guard = env_lock::lock_env([
|
||||
("GOOSE_MAX_TOKENS", None::<&str>),
|
||||
("GOOSE_CONTEXT_LIMIT", None::<&str>),
|
||||
]);
|
||||
let mut config = ModelConfig::new_or_fail("gpt-4o");
|
||||
config.context_limit = Some(64_000);
|
||||
let config = config.with_canonical_limits("openai");
|
||||
@@ -447,6 +455,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn does_not_override_existing_max_tokens() {
|
||||
let _guard = env_lock::lock_env([
|
||||
("GOOSE_MAX_TOKENS", None::<&str>),
|
||||
("GOOSE_CONTEXT_LIMIT", None::<&str>),
|
||||
]);
|
||||
let mut config = ModelConfig::new_or_fail("gpt-4o");
|
||||
config.max_tokens = Some(1_000);
|
||||
let config = config.with_canonical_limits("openai");
|
||||
@@ -456,6 +468,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unknown_model_leaves_fields_none() {
|
||||
let _guard = env_lock::lock_env([
|
||||
("GOOSE_MAX_TOKENS", None::<&str>),
|
||||
("GOOSE_CONTEXT_LIMIT", None::<&str>),
|
||||
]);
|
||||
let config =
|
||||
ModelConfig::new_or_fail("totally-unknown-model").with_canonical_limits("openai");
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ use super::canonical::{map_to_canonical_model, CanonicalModelRegistry};
|
||||
use super::errors::ProviderError;
|
||||
use super::retry::RetryConfig;
|
||||
use crate::config::base::ConfigValue;
|
||||
use crate::config::goose_mode::GooseMode;
|
||||
use crate::config::ExtensionConfig;
|
||||
use crate::config::{ExtensionConfig, GooseMode};
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
use crate::conversation::Conversation;
|
||||
use crate::model::ModelConfig;
|
||||
@@ -708,6 +707,10 @@ pub trait Provider: Send + Sync {
|
||||
))
|
||||
}
|
||||
|
||||
async fn update_mode(&self, _session_id: &str, _mode: GooseMode) -> Result<(), ProviderError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn permission_routing(&self) -> PermissionRouting {
|
||||
PermissionRouting::Noop
|
||||
}
|
||||
@@ -719,10 +722,6 @@ pub trait Provider: Send + Sync {
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn update_mode(&self, _session_id: &str, _mode: GooseMode) -> Result<(), ProviderError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A message stream yields partial text content but complete tool calls, all within the Message object
|
||||
|
||||
@@ -270,6 +270,8 @@ pub struct ClaudeCodeProvider {
|
||||
#[serde(skip)]
|
||||
pending_confirmations:
|
||||
Arc<tokio::sync::Mutex<HashMap<String, oneshot::Sender<PermissionConfirmation>>>>,
|
||||
#[serde(skip)]
|
||||
initial_mode: tokio::sync::Mutex<Option<GooseMode>>,
|
||||
}
|
||||
|
||||
impl ClaudeCodeProvider {
|
||||
@@ -623,6 +625,7 @@ impl ProviderDef for ClaudeCodeProvider {
|
||||
mcp_config_file,
|
||||
cli_process: tokio::sync::OnceCell::new(),
|
||||
pending_confirmations: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
||||
initial_mode: tokio::sync::Mutex::new(None),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -669,6 +672,19 @@ impl Provider for ClaudeCodeProvider {
|
||||
Ok(extract_model_aliases(response.ok().flatten().as_ref()))
|
||||
}
|
||||
|
||||
async fn update_mode(&self, _session_id: &str, mode: GooseMode) -> Result<(), ProviderError> {
|
||||
// Mode is baked into the subprocess at spawn; claude-acp replaces
|
||||
// this provider (#7801).
|
||||
let mut guard = self.initial_mode.lock().await;
|
||||
let current = *guard.get_or_insert(mode);
|
||||
if current != mode {
|
||||
return Err(ProviderError::RequestFailed(format!(
|
||||
"Mode change not supported: session is {current}, requested {mode}",
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn permission_routing(&self) -> PermissionRouting {
|
||||
PermissionRouting::ActionRequired
|
||||
}
|
||||
@@ -1192,6 +1208,7 @@ mod tests {
|
||||
mcp_config_file: None,
|
||||
cli_process: tokio::sync::OnceCell::new(),
|
||||
pending_confirmations: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
|
||||
initial_mode: tokio::sync::Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use async_trait::async_trait;
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use futures::future::BoxFuture;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
@@ -54,6 +55,8 @@ pub struct CodexProvider {
|
||||
skip_git_check: bool,
|
||||
/// CLI config overrides for MCP servers
|
||||
mcp_config_overrides: Vec<String>,
|
||||
#[serde(skip)]
|
||||
mode_by_session: tokio::sync::RwLock<HashMap<String, GooseMode>>,
|
||||
}
|
||||
|
||||
impl CodexProvider {
|
||||
@@ -69,11 +72,11 @@ impl CodexProvider {
|
||||
true
|
||||
}
|
||||
|
||||
/// Apply permission flags based on GOOSE_MODE setting
|
||||
fn apply_permission_flags(cmd: &mut Command) -> Result<(), ProviderError> {
|
||||
let config = Config::global();
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
|
||||
/// Apply permission flags based on GooseMode
|
||||
fn apply_permission_flags(
|
||||
cmd: &mut Command,
|
||||
goose_mode: GooseMode,
|
||||
) -> Result<(), ProviderError> {
|
||||
match goose_mode {
|
||||
GooseMode::Auto => {
|
||||
// --yolo is shorthand for --dangerously-bypass-approvals-and-sandbox
|
||||
@@ -101,6 +104,7 @@ impl CodexProvider {
|
||||
system: &str,
|
||||
messages: &[Message],
|
||||
_tools: &[Tool],
|
||||
goose_mode: GooseMode,
|
||||
) -> Result<Vec<String>, ProviderError> {
|
||||
// Single pass: text → prompt (stdin), images → temp files (-i flags)
|
||||
let image_dir = Paths::state_dir().join("codex/images");
|
||||
@@ -151,8 +155,7 @@ impl CodexProvider {
|
||||
// JSON output format for structured parsing
|
||||
cmd.arg("--json");
|
||||
|
||||
// Apply permission mode based on GOOSE_MODE
|
||||
Self::apply_permission_flags(&mut cmd)?;
|
||||
Self::apply_permission_flags(&mut cmd, goose_mode)?;
|
||||
|
||||
// Skip git repo check if configured
|
||||
if self.skip_git_check {
|
||||
@@ -653,6 +656,7 @@ impl ProviderDef for CodexProvider {
|
||||
reasoning_effort,
|
||||
skip_git_check,
|
||||
mcp_config_overrides: codex_mcp_config_overrides(&resolved),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -671,7 +675,7 @@ impl Provider for CodexProvider {
|
||||
async fn stream(
|
||||
&self,
|
||||
model_config: &ModelConfig,
|
||||
_session_id: &str, // CLI has no external session-id flag to propagate.
|
||||
session_id: &str,
|
||||
system: &str,
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
@@ -687,7 +691,13 @@ impl Provider for CodexProvider {
|
||||
));
|
||||
}
|
||||
|
||||
let lines = self.execute_command(system, messages, tools).await?;
|
||||
let goose_mode = {
|
||||
let map = self.mode_by_session.read().await;
|
||||
map.get(session_id).copied().unwrap_or_default()
|
||||
};
|
||||
let lines = self
|
||||
.execute_command(system, messages, tools, goose_mode)
|
||||
.await?;
|
||||
|
||||
let (message, usage) = self.parse_response(&lines)?;
|
||||
|
||||
@@ -720,6 +730,14 @@ impl Provider for CodexProvider {
|
||||
))
|
||||
}
|
||||
|
||||
async fn update_mode(&self, session_id: &str, mode: GooseMode) -> Result<(), ProviderError> {
|
||||
self.mode_by_session
|
||||
.write()
|
||||
.await
|
||||
.insert(session_id.to_string(), mode);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_supported_models(&self) -> Result<Vec<String>, ProviderError> {
|
||||
Ok(CODEX_KNOWN_MODELS.iter().map(|s| s.to_string()).collect())
|
||||
}
|
||||
@@ -908,6 +926,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let lines = vec!["Hello, world!".to_string()];
|
||||
@@ -928,6 +947,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
// Test with actual Codex CLI output format
|
||||
@@ -961,6 +981,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let lines: Vec<String> = vec![];
|
||||
@@ -1009,6 +1030,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
@@ -1034,6 +1056,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
@@ -1106,6 +1129,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
|
||||
@@ -1122,6 +1146,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
@@ -1212,6 +1237,7 @@ mod tests {
|
||||
reasoning_effort: "high".to_string(),
|
||||
skip_git_check: false,
|
||||
mcp_config_overrides: Vec::new(),
|
||||
mode_by_session: tokio::sync::RwLock::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
@@ -1239,4 +1265,19 @@ mod tests {
|
||||
fn test_default_model() {
|
||||
assert_eq!(CODEX_DEFAULT_MODEL, "gpt-5.2-codex");
|
||||
}
|
||||
|
||||
#[test_case(GooseMode::Auto, &["--yolo"] ; "auto_yolo")]
|
||||
#[test_case(GooseMode::SmartApprove, &["--full-auto"] ; "smart_approve_full_auto")]
|
||||
#[test_case(GooseMode::Approve, &[] as &[&str] ; "approve_no_flags")]
|
||||
#[test_case(GooseMode::Chat, &["--sandbox", "read-only"] ; "chat_read_only")]
|
||||
fn test_apply_permission_flags(mode: GooseMode, expected: &[&str]) {
|
||||
let mut cmd = tokio::process::Command::new("codex");
|
||||
CodexProvider::apply_permission_flags(&mut cmd, mode).unwrap();
|
||||
let args: Vec<&str> = cmd
|
||||
.as_std()
|
||||
.get_args()
|
||||
.map(|a| a.to_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(args, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use super::openai_compatible::handle_status_openai_compat;
|
||||
use super::retry::ProviderRetry;
|
||||
use super::utils::{ImageFormat, RequestLog};
|
||||
use crate::config::declarative_providers::DeclarativeProviderConfig;
|
||||
use crate::config::GooseMode;
|
||||
use crate::conversation::message::Message;
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::formats::ollama::{create_request, response_to_streaming_message_ollama};
|
||||
@@ -220,19 +219,11 @@ impl Provider for OllamaProvider {
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
) -> Result<MessageStream, ProviderError> {
|
||||
let config = crate::config::Config::global();
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
let filtered_tools = if goose_mode == GooseMode::Chat {
|
||||
&[]
|
||||
} else {
|
||||
tools
|
||||
};
|
||||
|
||||
let mut payload = create_request(
|
||||
model_config,
|
||||
system,
|
||||
messages,
|
||||
filtered_tools,
|
||||
tools,
|
||||
&ImageFormat::OpenAi,
|
||||
true,
|
||||
)?;
|
||||
|
||||
@@ -819,6 +819,7 @@ async fn execute_job(
|
||||
std::env::current_dir()?,
|
||||
format!("Scheduled job: {}", job.id),
|
||||
SessionType::Scheduled,
|
||||
agent.config.goose_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::config::paths::Paths;
|
||||
use crate::config::GooseMode;
|
||||
use crate::conversation::message::Message;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::model::ModelConfig;
|
||||
@@ -18,7 +19,7 @@ use std::sync::{Arc, LazyLock};
|
||||
use tracing::{info, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const CURRENT_SCHEMA_VERSION: i32 = 7;
|
||||
pub const CURRENT_SCHEMA_VERSION: i32 = 8;
|
||||
pub const SESSIONS_FOLDER: &str = "sessions";
|
||||
pub const DB_NAME: &str = "sessions.db";
|
||||
|
||||
@@ -93,6 +94,8 @@ pub struct Session {
|
||||
pub message_count: usize,
|
||||
pub provider_name: Option<String>,
|
||||
pub model_config: Option<ModelConfig>,
|
||||
#[serde(default)]
|
||||
pub goose_mode: GooseMode,
|
||||
}
|
||||
|
||||
pub struct SessionUpdateBuilder<'a> {
|
||||
@@ -114,6 +117,7 @@ pub struct SessionUpdateBuilder<'a> {
|
||||
user_recipe_values: Option<Option<HashMap<String, String>>>,
|
||||
provider_name: Option<Option<String>>,
|
||||
model_config: Option<Option<ModelConfig>>,
|
||||
goose_mode: Option<GooseMode>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug)]
|
||||
@@ -144,6 +148,7 @@ impl<'a> SessionUpdateBuilder<'a> {
|
||||
user_recipe_values: None,
|
||||
provider_name: None,
|
||||
model_config: None,
|
||||
goose_mode: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +246,11 @@ impl<'a> SessionUpdateBuilder<'a> {
|
||||
self.model_config = Some(Some(model_config));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn goose_mode(mut self, mode: GooseMode) -> Self {
|
||||
self.goose_mode = Some(mode);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionManager {
|
||||
@@ -269,9 +279,10 @@ impl SessionManager {
|
||||
working_dir: PathBuf,
|
||||
name: String,
|
||||
session_type: SessionType,
|
||||
goose_mode: GooseMode,
|
||||
) -> Result<Session> {
|
||||
self.storage
|
||||
.create_session(working_dir, name, session_type)
|
||||
.create_session(working_dir, name, session_type, goose_mode)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -417,6 +428,7 @@ impl Default for Session {
|
||||
message_count: 0,
|
||||
provider_name: None,
|
||||
model_config: None,
|
||||
goose_mode: GooseMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -481,6 +493,11 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
|
||||
message_count: row.try_get("message_count").unwrap_or(0) as usize,
|
||||
provider_name: row.try_get("provider_name").ok().flatten(),
|
||||
model_config,
|
||||
goose_mode: row
|
||||
.try_get::<String, _>("goose_mode")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -579,7 +596,8 @@ impl SessionStorage {
|
||||
recipe_json TEXT,
|
||||
user_recipe_values_json TEXT,
|
||||
provider_name TEXT,
|
||||
model_config_json TEXT
|
||||
model_config_json TEXT,
|
||||
goose_mode TEXT NOT NULL DEFAULT 'auto'
|
||||
)
|
||||
"#,
|
||||
)
|
||||
@@ -692,8 +710,8 @@ impl SessionStorage {
|
||||
total_tokens, input_tokens, output_tokens,
|
||||
accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens,
|
||||
schedule_id, recipe_json, user_recipe_values_json,
|
||||
provider_name, model_config_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
provider_name, model_config_json, goose_mode
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&session.id)
|
||||
@@ -715,6 +733,7 @@ impl SessionStorage {
|
||||
.bind(user_recipe_values_json)
|
||||
.bind(&session.provider_name)
|
||||
.bind(model_config_json)
|
||||
.bind(session.goose_mode.to_string())
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -887,6 +906,15 @@ impl SessionStorage {
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
}
|
||||
8 => {
|
||||
sqlx::query(
|
||||
r#"
|
||||
ALTER TABLE sessions ADD COLUMN goose_mode TEXT NOT NULL DEFAULT 'auto'
|
||||
"#,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!("Unknown migration version: {}", version);
|
||||
}
|
||||
@@ -900,6 +928,7 @@ impl SessionStorage {
|
||||
working_dir: PathBuf,
|
||||
name: String,
|
||||
session_type: SessionType,
|
||||
goose_mode: GooseMode,
|
||||
) -> Result<Session> {
|
||||
let pool = self.pool().await?;
|
||||
let mut tx = pool.begin_with("BEGIN IMMEDIATE").await?;
|
||||
@@ -907,7 +936,7 @@ impl SessionStorage {
|
||||
let today = chrono::Utc::now().format("%Y%m%d").to_string();
|
||||
let session = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO sessions (id, name, user_set_name, session_type, working_dir, extension_data)
|
||||
INSERT INTO sessions (id, name, user_set_name, session_type, working_dir, extension_data, goose_mode)
|
||||
VALUES (
|
||||
? || '_' || CAST(COALESCE((
|
||||
SELECT MAX(CAST(SUBSTR(id, 10) AS INTEGER))
|
||||
@@ -918,7 +947,8 @@ impl SessionStorage {
|
||||
FALSE,
|
||||
?,
|
||||
?,
|
||||
'{}'
|
||||
'{}',
|
||||
?
|
||||
)
|
||||
RETURNING *
|
||||
"#,
|
||||
@@ -928,6 +958,7 @@ impl SessionStorage {
|
||||
.bind(&name)
|
||||
.bind(session_type.to_string())
|
||||
.bind(&*working_dir.to_string_lossy())
|
||||
.bind(goose_mode.to_string())
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -944,7 +975,7 @@ impl SessionStorage {
|
||||
total_tokens, input_tokens, output_tokens,
|
||||
accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens,
|
||||
schedule_id, recipe_json, user_recipe_values_json,
|
||||
provider_name, model_config_json
|
||||
provider_name, model_config_json, goose_mode
|
||||
FROM sessions
|
||||
WHERE id = ?
|
||||
"#,
|
||||
@@ -1007,6 +1038,7 @@ impl SessionStorage {
|
||||
add_update!(builder.user_recipe_values, "user_recipe_values_json");
|
||||
add_update!(builder.provider_name, "provider_name");
|
||||
add_update!(builder.model_config, "model_config_json");
|
||||
add_update!(builder.goose_mode, "goose_mode");
|
||||
|
||||
if updates.is_empty() {
|
||||
return Ok(());
|
||||
@@ -1072,6 +1104,9 @@ impl SessionStorage {
|
||||
.transpose()?;
|
||||
q = q.bind(model_config_json);
|
||||
}
|
||||
if let Some(goose_mode) = builder.goose_mode {
|
||||
q = q.bind(goose_mode.to_string());
|
||||
}
|
||||
|
||||
let pool = self.pool().await?;
|
||||
let mut tx = pool.begin_with("BEGIN IMMEDIATE").await?;
|
||||
@@ -1216,7 +1251,7 @@ impl SessionStorage {
|
||||
s.total_tokens, s.input_tokens, s.output_tokens,
|
||||
s.accumulated_total_tokens, s.accumulated_input_tokens, s.accumulated_output_tokens,
|
||||
s.schedule_id, s.recipe_json, s.user_recipe_values_json,
|
||||
s.provider_name, s.model_config_json,
|
||||
s.provider_name, s.model_config_json, s.goose_mode,
|
||||
COUNT(m.id) as message_count
|
||||
FROM sessions s
|
||||
INNER JOIN messages m ON s.id = m.session_id
|
||||
@@ -1304,6 +1339,7 @@ impl SessionStorage {
|
||||
import.working_dir.clone(),
|
||||
import.name.clone(),
|
||||
import.session_type,
|
||||
import.goose_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1347,6 +1383,7 @@ impl SessionStorage {
|
||||
original_session.working_dir.clone(),
|
||||
new_name,
|
||||
original_session.session_type,
|
||||
original_session.goose_mode,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1357,7 +1394,7 @@ impl SessionStorage {
|
||||
.recipe(original_session.recipe)
|
||||
.user_recipe_values(original_session.user_recipe_values);
|
||||
|
||||
// Preserve provider and model config from original session
|
||||
// Preserve provider, model config, and goose_mode from original session
|
||||
if let Some(provider_name) = original_session.provider_name {
|
||||
builder = builder.provider_name(provider_name);
|
||||
}
|
||||
@@ -1366,6 +1403,8 @@ impl SessionStorage {
|
||||
builder = builder.model_config(model_config);
|
||||
}
|
||||
|
||||
builder = builder.goose_mode(original_session.goose_mode);
|
||||
|
||||
builder.apply().await?;
|
||||
|
||||
if let Some(conversation) = original_session.conversation {
|
||||
@@ -1458,6 +1497,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
use tempfile::TempDir;
|
||||
use test_case::test_case;
|
||||
|
||||
const NUM_CONCURRENT_SESSIONS: i32 = 10;
|
||||
|
||||
@@ -1529,6 +1569,7 @@ mod tests {
|
||||
PathBuf::from("/tmp/lock-upgrade-test"),
|
||||
"Lock Upgrade Session".to_string(),
|
||||
SessionType::User,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1566,7 +1607,12 @@ mod tests {
|
||||
let description = format!("Test session {}", i);
|
||||
|
||||
let session = sm
|
||||
.create_session(working_dir.clone(), description, SessionType::User)
|
||||
.create_session(
|
||||
working_dir.clone(),
|
||||
description,
|
||||
SessionType::User,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1654,6 +1700,7 @@ mod tests {
|
||||
PathBuf::from("/tmp/test"),
|
||||
DESCRIPTION.to_string(),
|
||||
SessionType::User,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1733,4 +1780,77 @@ mod tests {
|
||||
assert!(imported.user_set_name);
|
||||
assert_eq!(imported.working_dir, PathBuf::from("/tmp/test"));
|
||||
}
|
||||
|
||||
#[test_case(GooseMode::Approve)]
|
||||
#[test_case(GooseMode::SmartApprove)]
|
||||
#[test_case(GooseMode::Chat)]
|
||||
#[tokio::test]
|
||||
async fn test_goose_mode_persists(mode: GooseMode) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let sm = SessionManager::new(temp_dir.path().to_path_buf());
|
||||
|
||||
let session = sm
|
||||
.create_session(
|
||||
temp_dir.path().to_path_buf(),
|
||||
"test".into(),
|
||||
SessionType::User,
|
||||
mode,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let reloaded = sm.get_session(&session.id, false).await.unwrap();
|
||||
assert_eq!(reloaded.goose_mode, mode);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_goose_mode_update() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let sm = SessionManager::new(temp_dir.path().to_path_buf());
|
||||
|
||||
let session = sm
|
||||
.create_session(
|
||||
temp_dir.path().to_path_buf(),
|
||||
"test".into(),
|
||||
SessionType::User,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sm.update(&session.id)
|
||||
.goose_mode(GooseMode::Approve)
|
||||
.apply()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let reloaded = sm.get_session(&session.id, false).await.unwrap();
|
||||
assert_eq!(reloaded.goose_mode, GooseMode::Approve);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_goose_mode_malformed_defaults_to_auto() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let sm = SessionManager::new(temp_dir.path().to_path_buf());
|
||||
|
||||
let session = sm
|
||||
.create_session(
|
||||
temp_dir.path().to_path_buf(),
|
||||
"test".into(),
|
||||
SessionType::User,
|
||||
GooseMode::Approve,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let pool = &sm.storage().pool;
|
||||
sqlx::query("UPDATE sessions SET goose_mode = 'garbage' WHERE id = ?")
|
||||
.bind(&session.id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let reloaded = sm.get_session(&session.id, false).await.unwrap();
|
||||
assert_eq!(reloaded.goose_mode, GooseMode::default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +340,7 @@ mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use goose::agents::SessionConfig;
|
||||
use goose::config::GooseMode;
|
||||
use goose::conversation::message::{Message, MessageContent};
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::base::{
|
||||
@@ -427,6 +428,7 @@ mod tests {
|
||||
PathBuf::default(),
|
||||
"max-turn-test".to_string(),
|
||||
SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -542,6 +544,7 @@ mod tests {
|
||||
std::path::PathBuf::from("."),
|
||||
"Test Session".to_string(),
|
||||
SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create session");
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use goose::agents::{Agent, AgentEvent, SessionConfig};
|
||||
use goose::config::GooseMode;
|
||||
use goose::conversation::message::{Message, MessageContent};
|
||||
use goose::conversation::Conversation;
|
||||
use goose::model::ModelConfig;
|
||||
@@ -215,6 +216,7 @@ async fn setup_test_session(
|
||||
temp_dir.path().to_path_buf(),
|
||||
session_name.to_string(),
|
||||
SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ struct ProviderTestConfig {
|
||||
expected_session_id: fn() -> Arc<dyn ExpectedSessionId>,
|
||||
test_permissions: bool,
|
||||
test_smart_approve: bool,
|
||||
test_mode_update: bool,
|
||||
test_context_length_exceeded: bool,
|
||||
expect_context_length_exceeded: bool,
|
||||
context_length_exceeded: usize,
|
||||
@@ -141,6 +142,7 @@ impl ProviderTestConfig {
|
||||
expected_session_id: || Arc::new(EnforceSessionId::default()),
|
||||
test_permissions: true,
|
||||
test_smart_approve: true,
|
||||
test_mode_update: true,
|
||||
test_context_length_exceeded: true,
|
||||
expect_context_length_exceeded: true,
|
||||
context_length_exceeded: 600_000,
|
||||
@@ -188,6 +190,7 @@ impl ProviderTestConfig {
|
||||
skip,
|
||||
expected_session_id: || Arc::new(IgnoreSessionId),
|
||||
test_smart_approve: false,
|
||||
test_mode_update: false,
|
||||
test_context_length_exceeded: false,
|
||||
..Self::with_llm_provider(name, model_name, &[])
|
||||
}
|
||||
@@ -246,6 +249,7 @@ impl ProviderFixture {
|
||||
std::env::current_dir()?,
|
||||
"provider_test".to_string(),
|
||||
SessionType::User,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await?;
|
||||
let session_id = session.id;
|
||||
@@ -586,6 +590,23 @@ impl ProviderFixture {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn test_mode_update(&self) -> Result<()> {
|
||||
// Start in Auto mode (fixture default), tools auto-approved.
|
||||
// Switch to Approve mode dynamically via agent.
|
||||
self.agent
|
||||
.update_goose_mode(GooseMode::Approve, &self.session_id)
|
||||
.await?;
|
||||
// Verify tool call now requires permission (ActionRequired).
|
||||
// Cancel prevents the task from completing → tool fails.
|
||||
self.run_permission_test(
|
||||
Permission::Cancel,
|
||||
true,
|
||||
"Use the get_code tool and output only its result.",
|
||||
"mode_update",
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn load_env() {
|
||||
@@ -669,6 +690,9 @@ async fn test_provider(config: ProviderTestConfig) -> Result<()> {
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
if config.test_mode_update {
|
||||
run_test(GooseMode::Auto).await?.test_mode_update().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
@@ -663,6 +663,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agent/update_session": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::agent"
|
||||
],
|
||||
"operationId": "update_session",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateSessionRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Session updated"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agent/update_working_dir": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -5198,6 +5227,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"GooseMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"auto",
|
||||
"approve",
|
||||
"smart_approve",
|
||||
"chat"
|
||||
]
|
||||
},
|
||||
"HfGgufFile": {
|
||||
"type": "object",
|
||||
"description": "A single downloadable GGUF file (used internally and for downloads).",
|
||||
@@ -7478,6 +7516,9 @@
|
||||
"extension_data": {
|
||||
"$ref": "#/components/schemas/ExtensionData"
|
||||
},
|
||||
"goose_mode": {
|
||||
"$ref": "#/components/schemas/GooseMode"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -8496,6 +8537,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateSessionRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"session_id"
|
||||
],
|
||||
"properties": {
|
||||
"goose_mode": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateSessionUserRecipeValuesRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -158,7 +158,7 @@ const PairRouteWrapper = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
const SettingsRoute = () => {
|
||||
const SettingsRoute = ({ activeSessionId }: { activeSessionId?: string }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -174,7 +174,7 @@ const SettingsRoute = () => {
|
||||
viewOptions.section = sectionFromUrl;
|
||||
}
|
||||
|
||||
return <SettingsView onClose={() => navigate('/')} setView={setView} viewOptions={viewOptions} />;
|
||||
return <SettingsView onClose={() => navigate('/')} setView={setView} viewOptions={{...viewOptions, sessionId: activeSessionId}} />;
|
||||
};
|
||||
|
||||
const SessionsRoute = () => {
|
||||
@@ -667,7 +667,7 @@ export function AppInner() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="settings" element={<SettingsRoute />} />
|
||||
<Route path="settings" element={<SettingsRoute activeSessionId={activeSessions[activeSessions.length - 1]?.sessionId} />} />
|
||||
<Route
|
||||
path="extensions"
|
||||
element={
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -492,6 +492,8 @@ export type GooseApp = McpAppResource & (WindowProps | null) & {
|
||||
prd?: string | null;
|
||||
};
|
||||
|
||||
export type GooseMode = 'auto' | 'approve' | 'smart_approve' | 'chat';
|
||||
|
||||
/**
|
||||
* A single downloadable GGUF file (used internally and for downloads).
|
||||
*/
|
||||
@@ -1200,6 +1202,7 @@ export type Session = {
|
||||
conversation?: Conversation | null;
|
||||
created_at: string;
|
||||
extension_data: ExtensionData;
|
||||
goose_mode?: GooseMode;
|
||||
id: string;
|
||||
input_tokens?: number | null;
|
||||
message_count: number;
|
||||
@@ -1548,6 +1551,11 @@ export type UpdateSessionNameRequest = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type UpdateSessionRequest = {
|
||||
goose_mode?: string | null;
|
||||
session_id: string;
|
||||
};
|
||||
|
||||
export type UpdateSessionUserRecipeValuesRequest = {
|
||||
/**
|
||||
* Recipe parameter values entered by the user
|
||||
@@ -2079,6 +2087,31 @@ export type UpdateAgentProviderResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type UpdateSessionData = {
|
||||
body: UpdateSessionRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/agent/update_session';
|
||||
};
|
||||
|
||||
export type UpdateSessionErrors = {
|
||||
/**
|
||||
* Invalid request
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Internal error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type UpdateSessionResponses = {
|
||||
/**
|
||||
* Session updated
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type UpdateWorkingDirData = {
|
||||
body: UpdateWorkingDirRequest;
|
||||
path?: never;
|
||||
|
||||
@@ -1571,7 +1571,7 @@ export default function ChatInput({
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="w-px h-4 bg-border-primary mx-2" />
|
||||
<BottomMenuModeSelection />
|
||||
<BottomMenuModeSelection sessionId={sessionId} />
|
||||
<div className="w-px h-4 bg-border-primary mx-2" />
|
||||
<BottomMenuExtensionSelection sessionId={sessionId} />
|
||||
{sessionId && messages.length > 0 && (
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
|
||||
|
||||
let mockConfig: Record<string, unknown> = {};
|
||||
const mockUpdateSession = vi.fn().mockResolvedValue({});
|
||||
const mockGetSession = vi.fn().mockResolvedValue({ data: null });
|
||||
|
||||
vi.mock('../ConfigContext', () => ({
|
||||
useConfig: () => ({
|
||||
config: mockConfig,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/analytics', () => ({
|
||||
trackModeChanged: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../api', () => ({
|
||||
updateSession: (...args: unknown[]) => mockUpdateSession(...args),
|
||||
getSession: (...args: unknown[]) => mockGetSession(...args),
|
||||
}));
|
||||
|
||||
// Radix dropdown doesn't open in jsdom — render children directly
|
||||
vi.mock('../ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
describe('BottomMenuModeSelection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConfig = {};
|
||||
});
|
||||
|
||||
it('displays mode from config when no session', async () => {
|
||||
mockConfig.GOOSE_MODE = 'approve';
|
||||
render(<BottomMenuModeSelection sessionId={null} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('manual')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to auto when config has no mode', async () => {
|
||||
mockConfig.GOOSE_MODE = undefined;
|
||||
render(<BottomMenuModeSelection sessionId={null} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('autonomous')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches mode from session when sessionId is present', async () => {
|
||||
mockConfig.GOOSE_MODE = 'auto';
|
||||
mockGetSession.mockResolvedValue({ data: { goose_mode: 'approve' } });
|
||||
render(<BottomMenuModeSelection sessionId="test-session-123" />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('manual')).toBeInTheDocument();
|
||||
});
|
||||
expect(mockGetSession).toHaveBeenCalledWith({
|
||||
path: { session_id: 'test-session-123' },
|
||||
});
|
||||
});
|
||||
|
||||
it('calls updateSession and does not write global config', async () => {
|
||||
mockConfig.GOOSE_MODE = 'auto';
|
||||
render(<BottomMenuModeSelection sessionId="test-session-123" />);
|
||||
|
||||
fireEvent.click(screen.getByText('Manual'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateSession).toHaveBeenCalledWith({
|
||||
body: { session_id: 'test-session-123', goose_mode: 'approve' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call updateSession when sessionId is null', async () => {
|
||||
mockConfig.GOOSE_MODE = 'auto';
|
||||
render(<BottomMenuModeSelection sessionId={null} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Manual'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('manual')).toBeInTheDocument();
|
||||
});
|
||||
expect(mockUpdateSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores stale session fetch after sessionId changes', async () => {
|
||||
let resolveA: (value: unknown) => void;
|
||||
const promiseA = new Promise((resolve) => {
|
||||
resolveA = resolve;
|
||||
});
|
||||
|
||||
mockGetSession
|
||||
.mockImplementationOnce(() => promiseA)
|
||||
.mockResolvedValueOnce({ data: { goose_mode: 'auto' } });
|
||||
|
||||
const { rerender } = render(<BottomMenuModeSelection sessionId="session-A" />);
|
||||
rerender(<BottomMenuModeSelection sessionId="session-B" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('autonomous')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
resolveA!({ data: { goose_mode: 'approve' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('autonomous')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('manual')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tornado } from 'lucide-react';
|
||||
import { all_goose_modes, ModeSelectionItem } from '../settings/mode/ModeSelectionItem';
|
||||
import { useConfig } from '../ConfigContext';
|
||||
@@ -9,25 +9,30 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
import { trackModeChanged } from '../../utils/analytics';
|
||||
import { getSession, updateSession } from '../../api';
|
||||
|
||||
export const BottomMenuModeSelection = () => {
|
||||
export const BottomMenuModeSelection = ({ sessionId }: { sessionId: string | null }) => {
|
||||
const [gooseMode, setGooseMode] = useState('auto');
|
||||
const { read, upsert } = useConfig();
|
||||
const { config } = useConfig();
|
||||
|
||||
const fetchCurrentMode = useCallback(async () => {
|
||||
try {
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (sessionId) {
|
||||
getSession({ path: { session_id: sessionId } }).then((res) => {
|
||||
if (!cancelled && res.data?.goose_mode) {
|
||||
setGooseMode(res.data.goose_mode);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const mode = config.GOOSE_MODE as string | undefined;
|
||||
if (mode) {
|
||||
setGooseMode(mode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
}
|
||||
}, [read]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentMode();
|
||||
}, [fetchCurrentMode]);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [sessionId, config.GOOSE_MODE]);
|
||||
|
||||
const handleModeChange = async (newMode: string) => {
|
||||
if (gooseMode === newMode) {
|
||||
@@ -35,7 +40,9 @@ export const BottomMenuModeSelection = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await upsert('GOOSE_MODE', newMode, false);
|
||||
if (sessionId) {
|
||||
await updateSession({ body: { session_id: sessionId, goose_mode: newMode } });
|
||||
}
|
||||
setGooseMode(newMode);
|
||||
trackModeChanged(gooseMode, newMode);
|
||||
} catch (error) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export type SettingsViewOptions = {
|
||||
deepLinkConfig?: ExtensionConfig;
|
||||
showEnvVars?: boolean;
|
||||
section?: string;
|
||||
sessionId?: string;
|
||||
};
|
||||
|
||||
export default function SettingsView({
|
||||
@@ -191,7 +192,7 @@ export default function SettingsView({
|
||||
value="chat"
|
||||
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
|
||||
>
|
||||
<ChatSettingsSection />
|
||||
<ChatSettingsSection sessionId={viewOptions.sessionId} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GoosehintsSection } from './GoosehintsSection';
|
||||
import { SpellcheckToggle } from './SpellcheckToggle';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
|
||||
export default function ChatSettingsSection() {
|
||||
export default function ChatSettingsSection({ sessionId }: { sessionId?: string }) {
|
||||
return (
|
||||
<div className="space-y-4 pr-4 pb-8 mt-1">
|
||||
<Card className="pb-2 rounded-lg">
|
||||
@@ -15,7 +15,7 @@ export default function ChatSettingsSection() {
|
||||
<CardDescription>Configure how Goose interacts with tools and extensions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2">
|
||||
<ModeSection />
|
||||
<ModeSection sessionId={sessionId} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -2,14 +2,18 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import { ConversationLimitsDropdown } from './ConversationLimitsDropdown';
|
||||
import { updateSession } from '../../../api';
|
||||
|
||||
export const ModeSection = () => {
|
||||
export const ModeSection = ({ sessionId }: { sessionId?: string }) => {
|
||||
const [currentMode, setCurrentMode] = useState('auto');
|
||||
const [maxTurns, setMaxTurns] = useState<number>(1000);
|
||||
const { read, upsert } = useConfig();
|
||||
const { config, read, upsert } = useConfig();
|
||||
|
||||
const handleModeChange = async (newMode: string) => {
|
||||
try {
|
||||
if (sessionId) {
|
||||
await updateSession({ body: { session_id: sessionId, goose_mode: newMode } });
|
||||
}
|
||||
await upsert('GOOSE_MODE', newMode, false);
|
||||
setCurrentMode(newMode);
|
||||
} catch (error) {
|
||||
@@ -18,16 +22,12 @@ export const ModeSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCurrentMode = useCallback(async () => {
|
||||
try {
|
||||
const mode = (await read('GOOSE_MODE', false)) as string;
|
||||
if (mode) {
|
||||
setCurrentMode(mode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching current mode:', error);
|
||||
useEffect(() => {
|
||||
const mode = config.GOOSE_MODE as string | undefined;
|
||||
if (mode) {
|
||||
setCurrentMode(mode);
|
||||
}
|
||||
}, [read]);
|
||||
}, [config.GOOSE_MODE]);
|
||||
|
||||
const fetchMaxTurns = useCallback(async () => {
|
||||
try {
|
||||
@@ -50,9 +50,8 @@ export const ModeSection = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentMode();
|
||||
fetchMaxTurns();
|
||||
}, [fetchCurrentMode, fetchMaxTurns]);
|
||||
}, [fetchMaxTurns]);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
listSessions,
|
||||
getSession,
|
||||
updateAgentProvider,
|
||||
updateSession,
|
||||
upsertConfig,
|
||||
reply,
|
||||
} from '../../src/api';
|
||||
import { execSync } from 'child_process';
|
||||
@@ -110,6 +112,7 @@ extensions:
|
||||
const session = startResponse.data!;
|
||||
expect(session.id).toBeDefined();
|
||||
expect(session.name).toBeDefined();
|
||||
expect(session.goose_mode).toBe('auto');
|
||||
|
||||
const getResponse = await getSession({
|
||||
client: ctx.client,
|
||||
@@ -129,6 +132,105 @@ extensions:
|
||||
expect(sessionsResponse.data!.sessions).toBeDefined();
|
||||
expect(Array.isArray(sessionsResponse.data!.sessions)).toBe(true);
|
||||
});
|
||||
|
||||
it('should persist goose_mode on the session', async () => {
|
||||
await upsertConfig({
|
||||
client: ctx.client,
|
||||
body: { key: 'GOOSE_MODE', value: 'approve', is_secret: false },
|
||||
});
|
||||
|
||||
try {
|
||||
const startResponse = await startAgent({
|
||||
client: ctx.client,
|
||||
body: { working_dir: os.tmpdir() },
|
||||
});
|
||||
expect(startResponse.response).toBeOkResponse();
|
||||
expect(startResponse.data!.goose_mode).toBe('approve');
|
||||
|
||||
const getResponse = await getSession({
|
||||
client: ctx.client,
|
||||
path: { session_id: startResponse.data!.id },
|
||||
});
|
||||
expect(getResponse.response).toBeOkResponse();
|
||||
expect(getResponse.data!.goose_mode).toBe('approve');
|
||||
} finally {
|
||||
// Restore default so subsequent tests don't inherit approve mode
|
||||
await upsertConfig({
|
||||
client: ctx.client,
|
||||
body: { key: 'GOOSE_MODE', value: 'auto', is_secret: false },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should update goose_mode on an active session via /agent/update_session', async () => {
|
||||
const startResponse = await startAgent({
|
||||
client: ctx.client,
|
||||
body: { working_dir: os.tmpdir() },
|
||||
});
|
||||
expect(startResponse.response).toBeOkResponse();
|
||||
const sessionId = startResponse.data!.id;
|
||||
|
||||
const updateResponse = await updateSession({
|
||||
client: ctx.client,
|
||||
body: { session_id: sessionId, goose_mode: 'approve' },
|
||||
});
|
||||
expect(updateResponse.response).toBeOkResponse();
|
||||
|
||||
const getResponse = await getSession({
|
||||
client: ctx.client,
|
||||
path: { session_id: sessionId },
|
||||
});
|
||||
expect(getResponse.response).toBeOkResponse();
|
||||
expect(getResponse.data!.goose_mode).toBe('approve');
|
||||
});
|
||||
|
||||
it('should preserve goose_mode after provider swap via /agent/update_provider', async (testContext) => {
|
||||
const configResponse = await readConfig({
|
||||
client: ctx.client,
|
||||
body: { key: 'GOOSE_PROVIDER', is_secret: false },
|
||||
});
|
||||
const providerName = configResponse.data as string | null | undefined;
|
||||
if (!providerName) {
|
||||
testContext.skip('Skipping - no GOOSE_PROVIDER configured');
|
||||
return;
|
||||
}
|
||||
|
||||
const modelResponse = await readConfig({
|
||||
client: ctx.client,
|
||||
body: { key: 'GOOSE_MODEL', is_secret: false },
|
||||
});
|
||||
const modelName = (modelResponse.data as string | null) || undefined;
|
||||
|
||||
const startResponse = await startAgent({
|
||||
client: ctx.client,
|
||||
body: { working_dir: os.tmpdir() },
|
||||
});
|
||||
expect(startResponse.response).toBeOkResponse();
|
||||
const sessionId = startResponse.data!.id;
|
||||
|
||||
const updateResponse = await updateSession({
|
||||
client: ctx.client,
|
||||
body: { session_id: sessionId, goose_mode: 'approve' },
|
||||
});
|
||||
expect(updateResponse.response).toBeOkResponse();
|
||||
|
||||
const providerResponse = await updateAgentProvider({
|
||||
client: ctx.client,
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
provider: providerName,
|
||||
model: modelName,
|
||||
},
|
||||
});
|
||||
expect(providerResponse.response).toBeOkResponse();
|
||||
|
||||
const getResponse = await getSession({
|
||||
client: ctx.client,
|
||||
path: { session_id: sessionId },
|
||||
});
|
||||
expect(getResponse.response).toBeOkResponse();
|
||||
expect(getResponse.data!.goose_mode).toBe('approve');
|
||||
});
|
||||
});
|
||||
|
||||
describe('messaging', () => {
|
||||
|
||||
Reference in New Issue
Block a user