Add ProviderManager and route session providers through it (#12745)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Douwe Osinga 2026-10-07 19:21:55 +00:00 • committed by GitHub
parent ce0c490083
commit 723a825fb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 596 additions and 747 deletions

View file

@ -1819,10 +1819,8 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> {
return Ok(());
}
let extensions = extension_config.into_iter().collect::<Vec<_>>();
let new_provider = create(&provider_name, extensions).await?;
agent
.update_provider(new_provider, model_config, &session.id)
.switch_provider(&session.id, &provider_name, model_config)
.await?;
let permission_manager = PermissionManager::instance();

View file

@ -1,7 +1,9 @@
use crate::cli::StreamableHttpOptions;
use super::output;
use super::{derive_extension_name_from_command, split_extension_name_prefix, CliSession};
use super::{
derive_extension_name_from_command, session_provider, split_extension_name_prefix, CliSession,
};
use console::style;
use goose::agents::final_output_tool::FinalOutputTool;
use goose::agents::{Agent, Container, ExtensionError};
@ -9,7 +11,6 @@ use goose::config::extensions::name_to_key;
use goose::config::resolve_extensions_for_new_session;
use goose::config::{Config, ExtensionConfig, GooseMode};
use goose::model_config::model_config_from_user_config;
use goose::providers::create;
use goose::recipe::Recipe;
use goose::session::session_manager::SessionType;
use goose::session::{EnabledExtensionsState, SessionManager};
@ -715,9 +716,16 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
process::exit(1);
}
};
if let Err(e) = agent
.persist_extension_configs(&session_id, extensions_for_provider.clone())
.await
{
output::render_error(&format!("Failed to save session extensions: {}", e));
process::exit(1);
}
let (new_provider, effective_provider_name, effective_model_name, effective_model_config) =
match create(&resolved.provider_name, extensions_for_provider.clone()).await {
match session_provider(&agent, &session_id, &resolved.provider_name).await {
Ok(provider) => (
provider,
resolved.provider_name.clone(),
@ -758,7 +766,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
if !session_config.interactive {
fallback_model_config = fallback_model_config.with_cache_ttl_clamped();
}
match create(&fallback_provider, extensions_for_provider.clone()).await {
match session_provider(&agent, &session_id, &fallback_provider).await {
Ok(provider) => (
provider,
fallback_provider,
@ -804,7 +812,11 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
});
agent
.update_provider(new_provider, effective_model_config, &session_id)
.switch_provider(
&session_id,
&effective_provider_name,
effective_model_config,
)
.await
.unwrap_or_else(|e| {
output::render_error(&format!("Failed to initialize agent: {}", e));
@ -837,19 +849,11 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
// Extensions are loaded after session creation because we may change
// directory when resuming.
let agent_ptr = Arc::new(agent);
let loading_handle = match agent_ptr
.persist_extension_configs(&session_id, extensions_for_provider.clone())
.await
{
Ok(()) => AbortOnDropHandle::new(tokio::spawn({
let agent = agent_ptr.clone();
let sid = session_id.clone();
async move { load_extensions(agent, extensions_for_provider, &sid).await }
})),
Err(error) => AbortOnDropHandle::new(tokio::spawn(async move {
vec![ExtensionFailure { label: None, error }]
})),
};
let loading_handle = AbortOnDropHandle::new(tokio::spawn({
let agent = agent_ptr.clone();
let sid = session_id.clone();
async move { load_extensions(agent, extensions_for_provider, &sid).await }
}));
let edit_mode = config
.get_param::<String>("EDIT_MODE")

View file

@ -27,7 +27,7 @@ use goose::agents::platform_extensions::developer::shell::{
use goose::agents::AgentEvent;
use goose::agents::SUBAGENT_TOOL_REQUEST_TYPE;
use goose::permission::Permission;
use goose::providers::base::ProviderUsage;
use goose::providers::base::{Provider, ProviderUsage};
use goose::utils::safe_truncate;
use anyhow::Result;
@ -786,7 +786,7 @@ impl CliSession {
history.save(editor);
self.push_message(Message::user().with_text(content));
let _provider = self.agent.provider().await?;
let _provider = self.agent.provider(&self.session_id).await?;
println!();
output::run_status_hook("thinking");
@ -880,7 +880,7 @@ impl CliSession {
}
async fn handle_model(&mut self, options: input::ModelCommandOptions) -> Result<()> {
let provider = self.agent.provider().await?;
let provider = self.agent.provider(&self.session_id).await?;
let current_provider_name = provider.get_name().to_string();
let current_model_config = self
.agent
@ -992,8 +992,13 @@ impl CliSession {
)
.await?;
let extensions = self.agent.get_extension_configs().await;
let new_provider = match goose::providers::create(target_provider_name, extensions).await {
let new_provider = match session_provider(
&self.agent,
&self.session_id,
target_provider_name,
)
.await
{
Ok(p) => p,
Err(e) => {
output::render_error(&format!(
@ -1031,12 +1036,9 @@ impl CliSession {
}
self.agent
.update_provider(new_provider, new_model_config, &self.session_id)
.switch_provider(&self.session_id, target_provider_name, new_model_config)
.await?;
let mode = self.agent.goose_mode().await;
self.agent.update_goose_mode(mode, &self.session_id).await?;
self.update_completion_cache().await?;
if provider_unchanged {
@ -1054,7 +1056,7 @@ impl CliSession {
}
async fn handle_clear(&mut self) -> Result<()> {
let provider = self.agent.provider().await?;
let provider = self.agent.provider(&self.session_id).await?;
if provider.manages_own_context() {
output::render_error(&context_management_unsupported_message(
"clear",
@ -1101,7 +1103,7 @@ impl CliSession {
}
async fn handle_new(&mut self) -> Result<()> {
let provider = self.agent.provider().await?;
let provider = self.agent.provider(&self.session_id).await?;
if provider.manages_own_context() {
output::render_error(&format!(
"Starting a new session is not supported for provider '{}' because it manages its own conversation context.",
@ -1130,6 +1132,7 @@ impl CliSession {
.await;
self.agent.discard_pending_steers(&self.session_id).await;
self.agent.config.providers.release(&self.session_id);
self.session_id = new_session_id;
self.messages.clear();
@ -1250,7 +1253,7 @@ impl CliSession {
}
async fn handle_compact(&mut self) -> Result<()> {
let provider = self.agent.provider().await?;
let provider = self.agent.provider(&self.session_id).await?;
if provider.manages_own_context() {
output::render_error(&context_management_unsupported_message(
"compact",
@ -1766,7 +1769,7 @@ impl CliSession {
) -> Result<()> {
let prompts = agent.list_extension_prompts(session_id).await;
let all_providers = goose::providers::providers().await;
let session_provider = agent.provider().await?.get_name().to_string();
let session_provider = agent.provider(session_id).await?.get_name().to_string();
let provider_ids: Vec<String> = all_providers.iter().map(|(m, _)| m.name.clone()).collect();
let inventory_models: HashMap<String, Vec<String>> = {
@ -1896,7 +1899,7 @@ impl CliSession {
/// Display enhanced context usage with session totals
pub async fn display_context_usage(&self) -> Result<()> {
let provider = self.agent.provider().await?;
let provider = self.agent.provider(&self.session_id).await?;
let model_config = self
.agent
.model_config_for_session(&self.session_id)
@ -2015,6 +2018,22 @@ impl CliSession {
}
}
/// The provider `session_id` would get with `provider_name`, without switching
/// the session to it.
pub(crate) async fn session_provider(
agent: &Agent,
session_id: &str,
provider_name: &str,
) -> anyhow::Result<Arc<dyn Provider>> {
let mut session = agent
.config
.session_manager
.get_session(session_id, false)
.await?;
session.provider_name = Some(provider_name.to_string());
agent.config.providers.provider_for(&session).await
}
async fn create_successor_session(
session_manager: &SessionManager,
old_session: &goose::session::Session,
@ -3177,11 +3196,11 @@ mod tests {
extension_loading: Option<AbortOnDropHandle<Vec<ExtensionFailure>>>,
refresh_completions: bool,
) -> CliSession {
let temp_dir = tempfile::TempDir::new().unwrap();
let session_manager = SessionManager::new(temp_dir.path().to_path_buf());
let data_dir = tempfile::TempDir::new().unwrap().keep();
let session_manager = SessionManager::new(data_dir.clone());
let session = session_manager
.create_session(
temp_dir.path().to_path_buf(),
data_dir.clone(),
"Loading gate test".to_string(),
goose::session::SessionType::User,
GooseMode::default(),
@ -3191,9 +3210,7 @@ mod tests {
let agent = goose::agents::Agent::with_config(goose::agents::AgentConfig::new(
Arc::new(session_manager),
Arc::new(goose::config::PermissionManager::new(
temp_dir.path().to_path_buf(),
)),
Arc::new(goose::config::PermissionManager::new(data_dir.clone())),
None,
GooseMode::default(),
// Disable background session naming so the test agent starts no

View file

@ -3,7 +3,7 @@ use futures::StreamExt;
use goose::agents::{Agent, AgentEvent, ExtensionConfig, SessionConfig};
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::create;
use goose::session::session_manager::SessionType;
use goose_providers::databricks::DATABRICKS_DEFAULT_MODEL;
use std::path::PathBuf;
@ -12,7 +12,7 @@ use std::path::PathBuf;
async fn main() -> anyhow::Result<()> {
let _ = dotenv();
let provider = create_with_named_model("databricks", Vec::new()).await?;
let provider = create("databricks", Vec::new()).await?;
let model_config =
goose::model_config::model_config_from_user_config("databricks", DATABRICKS_DEFAULT_MODEL)?;

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use dotenvy::dotenv;
use goose::conversation::message::Message;
use goose::providers::create_with_named_model;
use goose::providers::create;
use goose_providers::databricks::DATABRICKS_DEFAULT_MODEL;
#[tokio::main]
@ -10,7 +10,7 @@ async fn main() -> Result<()> {
std::env::remove_var("DATABRICKS_TOKEN");
let provider = create_with_named_model("databricks", Vec::new()).await?;
let provider = create("databricks", Vec::new()).await?;
let message = Message::user().with_text("Tell me a short joke about programming.");

View file

@ -3,7 +3,7 @@ use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use dotenvy::dotenv;
use goose::conversation::message::Message;
use goose::providers::anthropic::ANTHROPIC_DEFAULT_MODEL;
use goose::providers::create_with_named_model;
use goose::providers::create;
use goose::providers::openai::OPEN_AI_DEFAULT_MODEL;
use goose_providers::databricks::DATABRICKS_DEFAULT_MODEL;
use rmcp::model::{CallToolRequestParams, ContentBlock, Tool};
@ -22,18 +22,18 @@ async fn main() -> Result<()> {
goose_providers::model::ModelConfig,
)> = vec![
(
create_with_named_model("databricks", Vec::new()).await?,
create("databricks", Vec::new()).await?,
goose::model_config::model_config_from_user_config(
"databricks",
DATABRICKS_DEFAULT_MODEL,
)?,
),
(
create_with_named_model("openai", Vec::new()).await?,
create("openai", Vec::new()).await?,
goose::model_config::model_config_from_user_config("openai", OPEN_AI_DEFAULT_MODEL)?,
),
(
create_with_named_model("anthropic", Vec::new()).await?,
create("anthropic", Vec::new()).await?,
goose::model_config::model_config_from_user_config(
"anthropic",
ANTHROPIC_DEFAULT_MODEL,

View file

@ -235,8 +235,11 @@ pub(super) fn build_mode_state(
/// The provider decides whether goose owns the effort menu; a session without a
/// live provider keeps the model-name based path.
pub(super) async fn agent_thinking_effort_support(agent: &Agent) -> ThinkingEffortSupport {
match agent.provider().await {
pub(super) async fn agent_thinking_effort_support(
agent: &Agent,
session_id: &str,
) -> ThinkingEffortSupport {
match agent.provider(session_id).await {
Ok(provider) => provider.thinking_effort_support(),
Err(_) => ThinkingEffortSupport::Unspecified,
}

View file

@ -130,16 +130,8 @@ mod tool_calls;
mod tool_notifications;
mod tools;
pub type AcpProviderFactory = Arc<
dyn Fn(
String,
Vec<ExtensionConfig>,
Option<PathBuf>,
bool,
) -> BoxFuture<'static, Result<Arc<dyn Provider>>>
+ Send
+ Sync,
>;
pub type AcpProviderFactory =
Arc<dyn Fn(String) -> BoxFuture<'static, Result<Arc<dyn Provider>>> + Send + Sync>;
const ACP_VISIBLE_SESSION_TYPES: [SessionType; 3] =
[SessionType::User, SessionType::Scheduled, SessionType::Acp];
@ -894,7 +886,7 @@ impl GooseAcpAgent {
let provider_name = session.provider_name.clone();
let agent = self.get_session_agent(session_id).await?;
let provider = agent
.provider()
.provider(session_id)
.await
.internal_err_ctx("Failed to resolve session provider")?;
let context_limit =
@ -907,7 +899,7 @@ impl GooseAcpAgent {
.await
.internal_err_ctx("Failed to refresh session for setup notifications")?;
let current_provider = agent
.provider()
.provider(session_id)
.await
.internal_err_ctx("Failed to refresh session provider")?;
let refreshed_model_name = session
@ -1007,20 +999,8 @@ impl GooseAcpAgent {
Ok(Config::global())
}
async fn create_provider(
&self,
provider_name: &str,
extensions: Vec<ExtensionConfig>,
working_dir: Option<PathBuf>,
use_default_model: bool,
) -> Result<Arc<dyn Provider>> {
(self.provider_factory)(
provider_name.to_string(),
extensions,
working_dir,
use_default_model,
)
.await
async fn create_provider(&self, provider_name: &str) -> Result<Arc<dyn Provider>> {
(self.provider_factory)(provider_name.to_string()).await
}
/// Warm the provider model-list cache after session creation.
@ -1047,7 +1027,7 @@ impl GooseAcpAgent {
if !should_refresh_inventory_for_session_init(&inventory) {
return;
}
let provider = match agent.provider().await {
let provider = match agent.provider(&session_id).await {
Ok(provider) => provider,
Err(error) => {
warn!(
@ -1253,7 +1233,7 @@ impl GooseAcpAgent {
}
async fn subscribe_thinking_effort_updates(&self, session_id: &str, agent: &Arc<Agent>) {
let Ok(provider) = agent.provider().await else {
let Ok(provider) = agent.provider(session_id).await else {
return;
};
let Some(mut updates) = provider.subscribe_thinking_effort_support() else {
@ -2036,7 +2016,7 @@ impl GooseAcpAgent {
session_id: &str,
agent: &Arc<Agent>,
) -> Result<(), agent_client_protocol::Error> {
let Ok(provider) = agent.provider().await else {
let Ok(provider) = agent.provider(session_id).await else {
return Ok(());
};
if provider.get_name() != "local" {
@ -2072,7 +2052,7 @@ impl GooseAcpAgent {
agent: &Arc<Agent>,
) -> Result<usize, agent_client_protocol::Error> {
let provider = agent
.provider()
.provider(&session.id)
.await
.internal_err_ctx("Failed to resolve session provider")?;
let model = session.model_config.as_ref().ok_or_else(|| {
@ -2443,7 +2423,7 @@ impl GooseAcpAgent {
) -> Result<(), agent_client_protocol::Error> {
let agent = self.get_session_agent(session_id).await?;
let current_provider = agent
.provider()
.provider(session_id)
.await
.internal_err_ctx("Failed to get provider")?;
let provider_name = current_provider.get_name().to_string();
@ -2461,12 +2441,9 @@ impl GooseAcpAgent {
)
.invalid_params_err_ctx("Invalid model config")?;
agent
.recreate_provider_for_session(session_id, &provider_name, model_config)
.switch_provider(session_id, &provider_name, model_config)
.await
.internal_err_ctx("Failed to recreate provider")?;
self.subscribe_thinking_effort_updates(session_id, &agent)
.await;
// model_config is already updated on the session by the agent's update_provider call.
.internal_err_ctx("Failed to switch provider")?;
Ok(())
}
@ -2481,7 +2458,7 @@ impl GooseAcpAgent {
.internal_err()?;
let agent = self.get_session_agent(&session_id.0).await?;
let provider = agent
.provider()
.provider(&session_id.0)
.await
.internal_err_ctx("Failed to get provider")?;
let provider_name = provider.get_name().to_string();
@ -2564,7 +2541,7 @@ impl GooseAcpAgent {
let config = self.config()?;
let agent = self.get_session_agent(session_id).await?;
let current_provider = agent
.provider()
.provider(session_id)
.await
.internal_err_ctx("Failed to get provider")?;
let current_provider_name = current_provider.get_name();
@ -2608,14 +2585,13 @@ impl GooseAcpAgent {
)
.invalid_params_err_ctx("Invalid model config")?;
agent.config.providers.release(session_id);
agent
.recreate_provider_for_session(session_id, &resolved_provider_name, model_config)
.switch_provider(session_id, &resolved_provider_name, model_config)
.await
.internal_err_ctx("Failed to recreate provider")?;
.internal_err_ctx("Failed to switch provider")?;
self.subscribe_thinking_effort_updates(session_id, &agent)
.await;
// provider_name is already updated on the session by the agent's update_provider call.
Ok(())
}
@ -3658,11 +3634,9 @@ print(\"hello, world\")
let root = tempfile::tempdir().unwrap();
let active_runs = Arc::new(ActiveRunRegistry::default());
let live_voice = Arc::new(LiveVoiceService::from_config(active_runs.clone()));
let provider_factory: AcpProviderFactory = Arc::new(
|_provider_name, _extensions, _working_dir, _use_default_model| {
Box::pin(async { Err(anyhow::anyhow!("unused provider factory")) })
},
);
let provider_factory: AcpProviderFactory = Arc::new(|_provider_name| {
Box::pin(async { Err(anyhow::anyhow!("unused provider factory")) })
});
let agent = GooseAcpAgent::new(GooseAcpAgentOptions {
provider_factory,
builtin_selection: AcpBuiltinSelection::default(),
@ -3791,11 +3765,9 @@ print(\"hello, world\")
let root = tempfile::tempdir().unwrap();
let active_runs = Arc::new(ActiveRunRegistry::default());
let live_voice = Arc::new(LiveVoiceService::from_config(active_runs.clone()));
let provider_factory: AcpProviderFactory = Arc::new(
|_provider_name, _extensions, _working_dir, _use_default_model| {
Box::pin(async { Err(anyhow::anyhow!("unused provider factory")) })
},
);
let provider_factory: AcpProviderFactory = Arc::new(|_provider_name| {
Box::pin(async { Err(anyhow::anyhow!("unused provider factory")) })
});
let server = Arc::new(
GooseAcpAgent::new(GooseAcpAgentOptions {
provider_factory,

View file

@ -244,7 +244,7 @@ impl HandleDispatchFrom<Client> for GooseAcpHandler {
let session_agent =
agent_bg.get_session_agent(&session_id_bg.0).await?;
let provider = session_agent
.provider()
.provider(&session_id_bg.0)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let provider_name = provider.get_name().to_string();

View file

@ -57,11 +57,11 @@ impl GooseAcpAgent {
self.register_acp_session(goose_session.id.clone(), agent.clone())
.await;
let provider = agent
.provider()
.provider(&goose_session.id)
.await
.internal_err_ctx("Failed to get provider while forking ACP session")?;
resume_saved_provider_session(&provider, goose_session.conversation.as_ref()).await;
let effort_support = agent_thinking_effort_support(&agent).await;
let effort_support = agent_thinking_effort_support(&agent, &goose_session.id).await;
let acp_session_id = SessionId::new(new_session_id.clone());
let mut meta = session_meta(&goose_session);

View file

@ -416,7 +416,7 @@ impl GooseAcpAgent {
self.register_acp_session(session_id_str.clone(), agent.clone())
.await;
let provider = agent
.provider()
.provider(&session.id)
.await
.internal_err_ctx("Failed to get provider while loading ACP session")?;
resume_saved_provider_session(&provider, session.conversation.as_ref()).await;
@ -434,7 +434,7 @@ impl GooseAcpAgent {
let (mode_state, config_options) = build_session_setup_config(
&self.provider_inventory,
&session,
&agent_thinking_effort_support(&agent).await,
&agent_thinking_effort_support(&agent, &session.id).await,
)
.await?;

View file

@ -34,6 +34,7 @@ impl GooseAcpAgent {
.get_session(session_id, false)
.await
.internal_err_ctx("Failed to reload session")?;
agent.config.providers.release(&session.id);
agent
.restore_provider_from_session(&session)
.await

View file

@ -92,7 +92,7 @@ impl GooseAcpAgent {
.build_new_session_response(
&reloaded_session,
&extension_results,
&super::agent_thinking_effort_support(&agent).await,
&super::agent_thinking_effort_support(&agent, &reloaded_session.id).await,
)
.await?;
Ok(response)

View file

@ -504,7 +504,7 @@ impl GooseAcpAgent {
req: ProviderSupportedModelsListRequest,
) -> Result<ProviderSupportedModelsListResponse, agent_client_protocol::Error> {
let provider = self
.create_provider(&req.provider_id, Vec::new(), None, true)
.create_provider(&req.provider_id)
.await
.internal_err_ctx("Failed to initialize provider")?;
let models = match provider.fetch_supported_models().await {
@ -546,7 +546,7 @@ impl GooseAcpAgent {
let result = tokio::time::timeout(
ACP_READINESS_TIMEOUT,
self.create_provider(&req.provider_id, Vec::new(), None, true),
self.create_provider(&req.provider_id),
)
.await;
let (ready, error) = match result {
@ -865,11 +865,10 @@ impl GooseAcpAgent {
let toolshim = refresh_job.toolshim;
tokio::spawn(async move {
let mut refresh_guard = provider_inventory.refresh_guard(&identity);
let provider_result = AssertUnwindSafe(async {
provider_factory(provider_id.clone(), Vec::new(), None, true).await
})
.catch_unwind()
.await;
let provider_result =
AssertUnwindSafe(async { provider_factory(provider_id.clone()).await })
.catch_unwind()
.await;
let fetch_result: Result<Vec<String>> = match provider_result {
Ok(Ok(provider)) => {

View file

@ -106,28 +106,11 @@ impl AcpServer {
scheduler.list_scheduled_jobs().await;
}
let provider_factory: AcpProviderFactory = Arc::new(
move |provider_name, extensions, working_dir, use_default_model| {
Box::pin(async move {
if use_default_model {
crate::providers::create_with_default_model(&provider_name, extensions)
.await
} else {
match working_dir {
Some(working_dir) => {
crate::providers::create_with_working_dir(
&provider_name,
extensions,
working_dir,
)
.await
}
None => crate::providers::create(&provider_name, extensions).await,
}
}
})
},
);
let provider_factory: AcpProviderFactory = Arc::new(|provider_name| {
Box::pin(async move {
crate::providers::create_with_default_model(&provider_name, Vec::new()).await
})
});
let agent = GooseAcpAgent::new(GooseAcpAgentOptions {
provider_factory,

View file

@ -30,6 +30,7 @@ use crate::agents::final_output_tool::{
FINAL_OUTPUT_TOOL_NAME,
};
use crate::agents::prompt_manager::PromptManager;
use crate::agents::provider_manager::ProviderManager;
use crate::agents::retry::{RetryManager, RetryResult};
use crate::agents::state_machine::ops_recipe;
use crate::agents::state_machine::{
@ -44,8 +45,7 @@ use crate::agents::state_machine::{
};
use crate::agents::subagent_handler::ForegroundSubagentRunner;
use crate::agents::types::{
SessionConfig, SharedProvider, DEFAULT_ON_FAILURE_TIMEOUT_SECONDS,
DEFAULT_RETRY_TIMEOUT_SECONDS,
SessionConfig, DEFAULT_ON_FAILURE_TIMEOUT_SECONDS, DEFAULT_RETRY_TIMEOUT_SECONDS,
};
use crate::agents::AgentEvent;
use crate::config::extensions::name_to_key;
@ -226,6 +226,7 @@ pub struct AgentConfig {
pub session_name_update_tx: Option<mpsc::UnboundedSender<SessionNameUpdate>>,
pub use_login_shell_path: Option<bool>,
pub is_subagent: bool,
pub providers: Arc<ProviderManager>,
}
impl AgentConfig {
@ -250,6 +251,7 @@ impl AgentConfig {
session_name_update_tx: None,
use_login_shell_path: None,
is_subagent: false,
providers: Arc::default(),
}
}
@ -282,7 +284,6 @@ fn resolve_use_login_shell_path(explicit: Option<bool>, platform: &GoosePlatform
/// The main goose Agent
pub struct Agent {
pub(super) provider: SharedProvider,
pub config: AgentConfig,
pub(super) current_goose_mode: Mutex<GooseMode>,
@ -424,7 +425,7 @@ impl Agent {
}
pub fn with_config(config: AgentConfig) -> Self {
let provider = Arc::new(Mutex::new(None));
let providers = config.providers.clone();
let goose_platform = config.goose_platform.clone();
let initial_mode = config.goose_mode;
@ -454,11 +455,10 @@ impl Agent {
let use_login_shell_path = config.resolve_use_login_shell_path();
let is_subagent = config.is_subagent;
Self {
provider: provider.clone(),
config,
current_goose_mode: Mutex::new(initial_mode),
extension_manager: Arc::new(ExtensionManager::new(
provider.clone(),
providers.clone(),
session_manager,
scheduler,
client_name,
@ -472,7 +472,7 @@ impl Agent {
retry_manager: RetryManager::new(),
tool_inspection_manager: Self::create_tool_inspection_manager(
permission_manager,
provider.clone(),
providers,
inspection_session_manager,
),
hook_manager: if is_subagent {
@ -795,7 +795,7 @@ impl Agent {
/// Create a tool inspection manager with default inspectors
fn create_tool_inspection_manager(
permission_manager: Arc<PermissionManager>,
provider: SharedProvider,
providers: Arc<ProviderManager>,
session_manager: Arc<SessionManager>,
) -> ToolInspectionManager {
let mut tool_inspection_manager = ToolInspectionManager::new();
@ -806,14 +806,14 @@ impl Agent {
// Add adversary inspector (LLM-based review, enabled by ~/.config/goose/adversary.md)
tool_inspection_manager.add_inspector(Box::new(AdversaryInspector::new(
provider.clone(),
providers.clone(),
session_manager.clone(),
)));
// Add permission inspector (medium-high priority)
tool_inspection_manager.add_inspector(Box::new(PermissionInspector::new(
permission_manager,
provider,
providers,
session_manager,
)));
@ -895,7 +895,7 @@ impl Agent {
{
Ok(v) => v,
Err(_) => {
let context_limit = match self.provider().await {
let context_limit = match self.config.providers.provider_for(&session).await {
Ok(provider) => crate::context_limit::get_context_limit(
provider.as_ref(),
&model_config.model_name,
@ -990,44 +990,25 @@ impl Agent {
}
}
/// Get a reference count clone to the provider
pub async fn provider(&self) -> Result<Arc<dyn Provider>, anyhow::Error> {
match &*self.provider.lock().await {
Some(provider) => Ok(Arc::clone(provider)),
None => Err(anyhow!("Provider not set")),
}
pub async fn provider(&self, session_id: &str) -> Result<Arc<dyn Provider>> {
let session = self
.config
.session_manager
.get_session(session_id, false)
.await?;
self.config.providers.provider_for(&session).await
}
/// Resolve the active model config for a session.
///
/// The session is the source of truth for the selected model and its
/// settings. When the session has no stored config (e.g. before the
/// provider has been persisted), fall back to the configured provider
/// defaults.
pub async fn model_config_for_session(
&self,
session_id: &str,
) -> Result<goose_providers::model::ModelConfig> {
if let Ok(session) = self
let session = self
.config
.session_manager
.get_session(session_id, false)
.await
{
if let Some(model_config) = session.model_config {
return Ok(model_config);
}
}
let config = Config::global();
let provider_name = config
.get_goose_provider()
.map_err(|_| anyhow!("Could not resolve model config: missing provider"))?;
let model_name = config
.get_goose_model()
.map_err(|_| anyhow!("Could not resolve model config: missing model"))?;
crate::model_config::model_config_from_user_config(&provider_name, &model_name)
.map_err(|e| anyhow!("Could not resolve model config: {e}"))
.await?;
crate::agents::provider_manager::model_config_for(&session)
}
pub(super) async fn effective_model_config_for_session(
@ -1035,7 +1016,7 @@ impl Agent {
session_id: &str,
) -> Result<goose_providers::model::ModelConfig> {
let model_config = self.model_config_for_session(session_id).await?;
let provider_name = self.provider().await?.get_name().to_string();
let provider_name = self.provider(session_id).await?.get_name().to_string();
match crate::providers::get_from_registry(&provider_name).await {
Ok(entry) => Ok(entry
.normalize_model_config(model_config.clone())
@ -1254,7 +1235,9 @@ impl Agent {
};
let manages_own_context = self
.provider()
.config
.providers
.provider_for(session)
.await
.map(|p| p.manages_own_context())
.unwrap_or(false);
@ -1577,7 +1560,7 @@ impl Agent {
permission: permission.clone(),
};
if self
.try_route_tool_confirmation_to_provider(request_id, &confirmation)
.try_route_tool_confirmation_to_provider(session_id, request_id, &confirmation)
.await
{
if state.contains_request(request_id) {
@ -1619,11 +1602,11 @@ impl Agent {
async fn try_route_tool_confirmation_to_provider(
&self,
session_id: &str,
request_id: &str,
confirmation: &PermissionConfirmation,
) -> bool {
let provider = self.provider.lock().await.clone();
if let Some(provider) = provider.as_ref() {
if let Ok(provider) = self.provider(session_id).await {
if provider.permission_routing() == PermissionRouting::ActionRequired
&& provider
.handle_permission_confirmation(request_id, confirmation)
@ -1642,7 +1625,7 @@ impl Agent {
confirmation: PermissionConfirmation,
) {
if self
.try_route_tool_confirmation_to_provider(&request_id, &confirmation)
.try_route_tool_confirmation_to_provider(session_id, &request_id, &confirmation)
.await
{
return;
@ -1656,11 +1639,10 @@ impl Agent {
}
}
pub async fn supports_action_required_permissions(&self) -> bool {
if let Some(provider) = self.provider.lock().await.as_ref() {
return provider.permission_routing() == PermissionRouting::ActionRequired;
}
false
pub async fn supports_action_required_permissions(&self, session_id: &str) -> bool {
self.provider(session_id).await.is_ok_and(|provider| {
provider.permission_routing() == PermissionRouting::ActionRequired
})
}
pub(super) async fn create_state_machine(
@ -1850,12 +1832,7 @@ impl Agent {
.await?;
if !self.config.disable_session_naming {
let provider = self
.provider
.lock()
.await
.clone()
.ok_or_else(|| anyhow!("Provider not set"))?;
let provider = self.provider(&session_config.id).await?;
let manager = session_manager.clone();
let tx = self.config.session_name_update_tx.clone();
let id = session_id.clone();
@ -2060,12 +2037,7 @@ impl Agent {
) -> Result<BoxStream<'_, Result<AgentEvent>>> {
let session_manager = self.config.session_manager.clone();
let session_id = session_config.id.clone();
let provider = self
.provider
.lock()
.await
.clone()
.ok_or_else(|| anyhow!("Provider not set"))?;
let provider = self.provider(&session_id).await?;
let model_config = self.effective_model_config_for_session(&session_id).await?;
let context_limit =
@ -2228,27 +2200,6 @@ impl Agent {
}
}
// Doctor repairs the provider by writing it to the session, not to this agent, and must
// run even when restoring the session's provider is what fails.
let is_doctor =
crate::agents::execute_commands::parse_slash_command(&message_text_for_trace)
.is_some_and(|parsed| parsed.command == "doctor");
let session = session_manager
.get_session(&session_config.id, false)
.await?;
let live_provider_name = self
.provider
.lock()
.await
.as_ref()
.map(|provider| provider.get_name().to_string());
if !is_doctor
&& session.provider_name.is_some()
&& session.provider_name != live_provider_name
{
self.restore_provider_from_session(&session).await?;
}
if use_state_machine {
tracing::info!("dispatching reply via experimental state machine");
return self
@ -2428,7 +2379,7 @@ impl Agent {
let has_final_output_tool = final_output_tool.is_some();
*self.final_output_tool.lock().await = final_output_tool;
if has_final_output_tool {
let provider = self.provider().await?;
let provider = self.provider(&session_config.id).await?;
if !provider.supports_builtin_tools() {
let provider_name = provider.get_name();
warn!(
@ -2452,7 +2403,7 @@ impl Agent {
}
let needs_auto_compact = check_if_compaction_needed(
self.provider().await?.as_ref(),
self.provider(&session_config.id).await?.as_ref(),
&conversation,
None,
&session,
@ -2498,7 +2449,7 @@ impl Agent {
let compact_model_config = self.model_config_for_session(&session_config.id).await?;
match compact_messages(
self.provider().await?.as_ref(),
self.provider(&session_config.id).await?.as_ref(),
&compact_model_config,
&session_config.id,
&conversation_to_compact,
@ -2569,7 +2520,7 @@ impl Agent {
self.reset_retry_attempts().await;
let provider = self.provider().await?;
let provider = self.provider(&session_config.id).await?;
let provider_name = provider.get_name().to_string();
let saved_provider_session_id =
super::latest_provider_session_id(conversation.messages(), &provider_name);
@ -2839,7 +2790,7 @@ impl Agent {
}
let mut stream = crate::agents::reply_parts::stream_response_from_provider(
self.provider().await?,
self.provider(&session_config.id).await?,
model_config.clone(),
&session_config.id,
&system_prompt,
@ -2859,7 +2810,7 @@ impl Agent {
None
} else {
crate::context_mgmt::maybe_summarize_tool_pairs(
self.provider().await?,
self.provider(&session_config.id).await?,
model_config.clone(),
session_config.id.clone(),
conversation.clone(),
@ -3294,7 +3245,7 @@ impl Agent {
);
match compact_messages(
self.provider().await?.as_ref(),
self.provider(&session_config.id).await?.as_ref(),
&model_config,
&session_config.id,
&conversation,
@ -3695,6 +3646,8 @@ impl Agent {
Ok(inner)
}
/// Pins `provider` to the session instead of letting the provider manager
/// build one.
pub async fn update_provider(
&self,
provider: Arc<dyn Provider>,
@ -3702,18 +3655,48 @@ impl Agent {
session_id: &str,
) -> Result<()> {
let provider_name = provider.get_name().to_string();
let registry_entry = crate::providers::get_from_registry(&provider_name)
self.config
.providers
.set_provider(session_id, provider)
.await;
self.switch_provider(session_id, &provider_name, model_config)
.await
}
pub async fn switch_provider(
&self,
session_id: &str,
provider_name: &str,
model_config: goose_providers::model::ModelConfig,
) -> Result<()> {
let mut session = self
.config
.session_manager
.get_session(session_id, false)
.await?;
let registry_entry = crate::providers::get_from_registry(provider_name)
.await
.ok();
let model_config = if registry_entry.is_some() {
crate::model_config::materialize_model_config(&provider_name, model_config.clone())
crate::model_config::materialize_model_config(provider_name, model_config.clone())
.unwrap_or(model_config)
} else {
model_config
};
let effort_support = provider.thinking_effort_support();
let model_config = normalize_legacy_provider_thinking_effort(model_config, &effort_support);
session.provider_name = Some(provider_name.to_string());
session.model_config = Some(model_config.clone());
let provider = self
.config
.providers
.provider_for(&session)
.await
.map_err(|error| provider_creation_error(error, "Could not create provider"))?;
let model_config = normalize_legacy_provider_thinking_effort(
model_config,
&provider.thinking_effort_support(),
);
let effective_model_config = match registry_entry {
Some(entry) => entry
.normalize_model_config(model_config.clone())
@ -3721,15 +3704,9 @@ impl Agent {
None => model_config.clone(),
};
{
let mut current_provider = self.provider.lock().await;
*current_provider = Some(Arc::clone(&provider));
}
// A freshly created provider that manages its own model starts on its
// own default, so the session's selection has to be pushed to it before
// the next config snapshot is built. Failures are not fatal here: the
// selection is re-applied at stream time.
// A provider that manages its own model has to be told about the
// selection before the next config snapshot is built. Failures are not
// fatal here: the selection is re-applied at stream time.
if let Err(e) = provider
.apply_model_selection(&effective_model_config)
.await
@ -3741,7 +3718,7 @@ impl Agent {
.session_manager
.clone()
.update(session_id)
.provider_name(&provider_name)
.provider_name(provider_name)
.model_config(model_config)
.apply()
.await
@ -3749,7 +3726,7 @@ impl Agent {
}
pub async fn update_goose_mode(&self, mode: GooseMode, session_id: &str) -> Result<()> {
if let Some(provider) = self.provider.lock().await.as_ref() {
if let Ok(provider) = self.provider(session_id).await {
provider
.update_mode(session_id, mode)
.await
@ -3770,44 +3747,11 @@ impl Agent {
*self.current_goose_mode.lock().await
}
pub async fn recreate_provider_for_session(
&self,
session_id: &str,
provider_name: &str,
model_config: goose_providers::model::ModelConfig,
) -> Result<()> {
let session = self
.config
.session_manager
.get_session(session_id, false)
.await
.context("Failed to get session")?;
let extensions = EnabledExtensionsState::extensions_or_default(
Some(&session.extension_data),
Config::global(),
);
let provider = crate::providers::create_with_working_dir(
provider_name,
extensions,
session.working_dir.clone(),
)
.await
.map_err(|error| provider_creation_error(error, "Could not create provider"))?;
self.update_provider(provider, model_config, session_id)
.await?;
let mode = self.goose_mode().await;
self.update_goose_mode(mode, session_id).await
}
/// Apply a thinking-effort selection. `effort` is the raw option value: a
/// provider that manages effort through a harness has its own vocabulary,
/// which is not always a `ThinkingEffort` member.
pub async fn update_thinking_effort(&self, session_id: &str, effort: &str) -> Result<()> {
let current_provider = self.provider().await?;
let current_provider = self.provider(session_id).await?;
// Context rather than a formatted string: the caller distinguishes a
// value rejection from an operational failure by downcasting to
// `ProviderError`, which stringifying would destroy.
@ -3819,8 +3763,6 @@ impl Agent {
let model_config = self.model_config_for_session(session_id).await?;
if provider_handled {
// The provider applied the value live; recreating it would discard
// the very session state we just configured.
let model_config = model_config.with_merged_request_params(HashMap::from([(
"thinking_effort".to_string(),
Value::String(effort.to_string()),
@ -3842,7 +3784,7 @@ impl Agent {
)))
})?;
let provider_name = current_provider.get_name().to_string();
self.recreate_provider_for_session(
self.switch_provider(
session_id,
&provider_name,
model_config.with_thinking_effort(effort),
@ -3850,17 +3792,12 @@ impl Agent {
.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
/// Returns true if the session's provider was replaced with a fallback.
/// Points the session at its stored provider, falling back to the global
/// provider when the stored one no longer exists. Returns true if it fell
/// back.
pub async fn restore_provider_from_session(&self, session: &Session) -> Result<bool> {
let config = Config::global();
let provider_name = session
.provider_name
.clone()
.or_else(|| config.get_goose_provider().ok())
.ok_or_else(|| anyhow!("Could not configure agent: missing provider"))?;
let provider_name = crate::agents::provider_manager::provider_name_for(session)?;
let mut model_config = match session.model_config.clone() {
Some(saved_config) => crate::model_config::with_rederived_cache_ttl(saved_config)
@ -3889,92 +3826,55 @@ impl Agent {
}
}
let extensions =
EnabledExtensionsState::extensions_or_default(Some(&session.extension_data), config);
let (provider, active_model_config, provider_changed) =
if crate::providers::get_from_registry(&provider_name)
.await
.is_ok()
{
let p = crate::providers::create_with_working_dir(
&provider_name,
extensions,
session.working_dir.clone(),
)
.await
.map_err(|error| provider_creation_error(error, "Could not create provider"))?;
(p, model_config, false)
} else {
let fallback_provider_name = config
.get_goose_provider()
.ok()
.filter(|name| name != &provider_name)
.ok_or_else(|| {
anyhow!(
"Could not create provider: provider '{}' not found",
provider_name
)
})?;
tracing::warn!(
"Session provider '{}' unavailable, falling back to '{}'",
provider_name,
fallback_provider_name
);
let fallback_model_name = config.get_goose_model().ok().ok_or_else(|| {
anyhow!("Could not configure fallback provider: missing model")
})?;
let fallback_model_config = crate::model_config::model_config_from_user_config(
&fallback_provider_name,
&fallback_model_name,
)
.map_err(|e| {
anyhow!("Could not configure fallback provider: invalid model {}", e)
})?;
let fallback_provider = crate::providers::create_with_working_dir(
&fallback_provider_name,
extensions,
session.working_dir.clone(),
)
.await
.map_err(|error| {
provider_creation_error(
error,
format!(
"Could not create provider '{provider_name}' or fallback '{fallback_provider_name}'"
),
)
})?;
if let Err(e) = self
.config
.session_manager
.update(&session.id)
.provider_name(&fallback_provider_name)
.model_config(fallback_model_config.clone())
.apply()
.await
{
tracing::warn!("Failed to update session provider: {}", e);
}
(fallback_provider, fallback_model_config, true)
};
self.update_provider(provider, active_model_config, &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(provider_changed)
if crate::providers::get_from_registry(&provider_name)
.await
.is_ok()
{
self.switch_provider(&session.id, &provider_name, model_config)
.await?;
return Ok(false);
}
let fallback_provider_name = config
.get_goose_provider()
.ok()
.filter(|name| name != &provider_name)
.ok_or_else(|| {
anyhow!(
"Could not create provider: provider '{}' not found",
provider_name
)
})?;
tracing::warn!(
"Session provider '{}' unavailable, falling back to '{}'",
provider_name,
fallback_provider_name
);
let fallback_model_name = config
.get_goose_model()
.ok()
.ok_or_else(|| anyhow!("Could not configure fallback provider: missing model"))?;
let fallback_model_config = crate::model_config::model_config_from_user_config(
&fallback_provider_name,
&fallback_model_name,
)
.map_err(|e| anyhow!("Could not configure fallback provider: invalid model {}", e))?;
self.switch_provider(&session.id, &fallback_provider_name, fallback_model_config)
.await
.map_err(|error| {
provider_creation_error(
error,
format!(
"Could not create provider '{provider_name}' or fallback '{fallback_provider_name}'"
),
)
})?;
Ok(true)
}
pub async fn list_extension_prompts(&self, session_id: &str) -> HashMap<String, Vec<Prompt>> {
@ -4853,8 +4753,14 @@ mod tests {
async fn test_submit_tool_confirmation_routes_to_provider() {
let (agent, session, _data_dir) = tracing_test_agent_and_session().await;
let provider = Arc::new(ActionRequiredProvider::new());
*agent.provider.lock().await =
Some(provider.clone() as Arc<dyn crate::providers::base::Provider>);
agent
.update_provider(
provider.clone(),
goose_providers::model::ModelConfig::new("test-model"),
&session.id,
)
.await
.unwrap();
// Known request_id → provider handles it, confirmation_router NOT called
agent
@ -5178,7 +5084,10 @@ mod tests {
Some("xhigh")
);
// The unregistered test provider was not respawned.
assert_eq!(agent.provider().await.unwrap().get_name(), "test-effort");
assert_eq!(
agent.provider(&session_id).await.unwrap().get_name(),
"test-effort"
);
}
#[tokio::test]

View file

@ -184,7 +184,7 @@ impl Agent {
}
async fn handle_compact_command(&self, session_id: &str) -> Result<Option<Message>> {
let provider = self.provider().await?;
let provider = self.provider(session_id).await?;
if provider.manages_own_context() {
return Err(anyhow!(context_management_unsupported_message(
"compact",
@ -226,7 +226,7 @@ impl Agent {
async fn handle_clear_command(&self, session_id: &str) -> Result<Option<Message>> {
use crate::conversation::Conversation;
let provider = self.provider().await?;
let provider = self.provider(session_id).await?;
if provider.manages_own_context() {
return Err(anyhow!(context_management_unsupported_message(
"clear",
@ -265,7 +265,7 @@ impl Agent {
}
async fn handle_status_command(&self, session_id: &str) -> Result<Option<Message>> {
let provider = self.provider().await?;
let provider = self.provider(session_id).await?;
let model_config = self.model_config_for_session(session_id).await?;
let context_limit =
crate::context_limit::get_context_limit(provider.as_ref(), &model_config.model_name)

View file

@ -18,11 +18,11 @@ use super::extension::{
ExtensionConfig, ExtensionError, ExtensionResult, PlatformExtensionContext, PLATFORM_EXTENSIONS,
};
use super::tool_execution::ToolCallResult;
use super::types::SharedProvider;
use crate::action_required_manager::ActionRequiredManager;
use crate::agents::mcp_client::{
ConnectContext, GooseMcpClientCapabilities, GooseMcpHostInfo, McpClientTrait,
};
use crate::agents::provider_manager::ProviderManager;
use crate::config::extensions::name_to_key;
use crate::config::{get_extension_by_name, Config};
use crate::oauth::GooseCredentialStore;
@ -288,7 +288,6 @@ pub struct ExtensionManager {
mutation_lock: Mutex<()>,
directory_lock: RwLock<()>,
context: PlatformExtensionContext,
provider: SharedProvider,
client_name: String,
capabilities: ExtensionManagerCapabilities,
}
@ -501,7 +500,7 @@ impl ExtensionManager {
}
pub fn new(
provider: SharedProvider,
providers: Arc<ProviderManager>,
session_manager: Arc<crate::session::SessionManager>,
scheduler: Option<Arc<dyn crate::scheduler_trait::SchedulerTrait>>,
client_name: String,
@ -514,21 +513,20 @@ impl ExtensionManager {
directory_lock: RwLock::new(()),
context: PlatformExtensionContext {
extension_manager: None,
provider: provider.clone(),
providers,
session_manager,
scheduler,
use_login_shell_path,
},
provider,
client_name,
capabilities,
}
}
pub fn new_without_provider(data_dir: std::path::PathBuf) -> Self {
pub fn with_data_dir(data_dir: std::path::PathBuf) -> Self {
let session_manager = Arc::new(crate::session::SessionManager::new(data_dir));
Self::new(
Arc::new(Mutex::new(None)),
Default::default(),
session_manager,
None,
"goose-cli".to_string(),
@ -546,10 +544,6 @@ impl ExtensionManager {
&self.context
}
pub fn get_provider(&self) -> &SharedProvider {
&self.provider
}
fn hydrate_mcp_apps(&self) -> bool {
match &self.capabilities.host_info {
Some(host_info) if host_info.explicit_extensions => host_info.mcpui_enabled(),
@ -1256,8 +1250,7 @@ mod tests {
async fn dispatch_notification_methods(ctx: ToolCallContext) -> Vec<String> {
let temp_dir = tempfile::tempdir().unwrap();
let extension_manager =
ExtensionManager::new_without_provider(temp_dir.path().to_path_buf());
let extension_manager = ExtensionManager::with_data_dir(temp_dir.path().to_path_buf());
extension_manager
.add_mock_extension(
"notifications".to_string(),
@ -1312,8 +1305,7 @@ mod tests {
#[tokio::test]
async fn dispatch_reuses_existing_notification_emitter() {
let temp_dir = tempfile::tempdir().unwrap();
let extension_manager =
ExtensionManager::new_without_provider(temp_dir.path().to_path_buf());
let extension_manager = ExtensionManager::with_data_dir(temp_dir.path().to_path_buf());
extension_manager
.add_mock_extension(
"notifications".to_string(),
@ -1453,8 +1445,7 @@ mod tests {
#[tokio::test]
async fn app_dispatch_rejects_colliding_flattened_name_from_sibling_owner() {
let temp_dir = tempfile::tempdir().unwrap();
let extension_manager =
ExtensionManager::new_without_provider(temp_dir.path().to_path_buf());
let extension_manager = ExtensionManager::with_data_dir(temp_dir.path().to_path_buf());
extension_manager
.add_mock_extension(
"ext_a__ext_b".to_string(),
@ -1561,8 +1552,7 @@ mod tests {
#[tokio::test]
async fn resolve_leaves_out_an_extension_running_under_a_different_config() {
let temp_dir = tempfile::tempdir().unwrap();
let extension_manager =
ExtensionManager::new_without_provider(temp_dir.path().to_path_buf());
let extension_manager = ExtensionManager::with_data_dir(temp_dir.path().to_path_buf());
extension_manager
.add_mock_extension("ext_a".to_string(), Arc::new(MockClient {}))
.await;
@ -1586,7 +1576,7 @@ mod tests {
let data_dir = tempfile::tempdir().unwrap();
let old_working_dir = tempfile::tempdir().unwrap();
let new_working_dir = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
let manager = Arc::new(ExtensionManager::with_data_dir(
data_dir.path().to_path_buf(),
));
let session = manager
@ -1647,7 +1637,7 @@ mod tests {
async fn stale_working_dir_reconnect_does_not_restore_removed_extension() {
let data_dir = tempfile::tempdir().unwrap();
let new_working_dir = tempfile::tempdir().unwrap();
let extension_manager = Arc::new(ExtensionManager::new_without_provider(
let extension_manager = Arc::new(ExtensionManager::with_data_dir(
data_dir.path().to_path_buf(),
));
let config = ExtensionConfig::Platform {
@ -1703,7 +1693,7 @@ mod tests {
#[tokio::test]
async fn extension_manager_tools_follow_resource_support() {
let temp_dir = tempfile::tempdir().unwrap();
let extension_manager = Arc::new(ExtensionManager::new_without_provider(
let extension_manager = Arc::new(ExtensionManager::with_data_dir(
temp_dir.path().to_path_buf(),
));
extension_manager
@ -1770,7 +1760,7 @@ mod tests {
#[tokio::test]
async fn extension_manager_resource_tools_use_the_calling_lease() {
let temp_dir = tempfile::tempdir().unwrap();
let extension_manager = Arc::new(ExtensionManager::new_without_provider(
let extension_manager = Arc::new(ExtensionManager::with_data_dir(
temp_dir.path().to_path_buf(),
));
extension_manager
@ -1859,8 +1849,7 @@ mod tests {
#[tokio::test]
async fn tool_list_changed_during_fetch_prevents_stale_cache() {
let temp_dir = tempfile::tempdir().unwrap();
let extension_manager =
ExtensionManager::new_without_provider(temp_dir.path().to_path_buf());
let extension_manager = ExtensionManager::with_data_dir(temp_dir.path().to_path_buf());
let tools_client = Arc::new(BlockingToolsClient {
calls: AtomicUsize::new(0),
first_fetch_started: Semaphore::new(0),
@ -1904,7 +1893,7 @@ mod tests {
#[tokio::test]
async fn successful_mutation_is_persisted_when_another_mutation_fails() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
let manager = Arc::new(ExtensionManager::with_data_dir(
temp_dir.path().to_path_buf(),
));
let session = manager

View file

@ -13,6 +13,7 @@ pub mod platform_extensions;
#[cfg(feature = "scheduler")]
pub mod platform_tools;
pub mod prompt_manager;
pub mod provider_manager;
pub mod reply_parts;
pub mod retry;
#[cfg(feature = "scheduler")]

View file

@ -47,18 +47,24 @@ pub(super) async fn compute_compaction_info(
let session_model_config = session
.as_ref()
.and_then(|session| session.model_config.clone());
let context_limit = if let Some(model_config) = session_model_config.as_ref() {
let provider = extension_manager.get_provider().lock().await.clone();
match provider {
Some(provider) => {
crate::context_limit::get_context_limit(provider.as_ref(), &model_config.model_name)
.await
.ok()
let context_limit = match (session.as_ref(), session_model_config.as_ref()) {
(Some(session), Some(model_config)) => {
match extension_manager
.get_context()
.providers
.provider_for(session)
.await
{
Ok(provider) => crate::context_limit::get_context_limit(
provider.as_ref(),
&model_config.model_name,
)
.await
.ok(),
Err(_) => None,
}
None => None,
}
} else {
None
_ => None,
};
let total_tokens = session
.as_ref()
@ -91,18 +97,24 @@ pub async fn turn_context_message(
let session_model_config = session
.as_ref()
.and_then(|session| session.model_config.clone());
let context_limit = if let Some(model_config) = session_model_config.as_ref() {
let provider = extension_manager.get_provider().lock().await.clone();
match provider {
Some(provider) => {
crate::context_limit::get_context_limit(provider.as_ref(), &model_config.model_name)
.await
.ok()
let context_limit = match (session.as_ref(), session_model_config.as_ref()) {
(Some(session), Some(model_config)) => {
match extension_manager
.get_context()
.providers
.provider_for(session)
.await
{
Ok(provider) => crate::context_limit::get_context_limit(
provider.as_ref(),
&model_config.model_name,
)
.await
.ok(),
Err(_) => None,
}
None => None,
}
} else {
None
_ => None,
};
if should_skip_moim(context_limit) {
return None;
@ -278,7 +290,7 @@ mod tests {
async fn session_and_manager() -> (String, ExtensionManager, tempfile::TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
let em = ExtensionManager::new_without_provider(temp_dir.path().to_path_buf());
let em = ExtensionManager::with_data_dir(temp_dir.path().to_path_buf());
let session = em
.get_context()
.session_manager

View file

@ -278,7 +278,7 @@ mod tests {
fn ctx() -> PlatformExtensionContext {
PlatformExtensionContext {
extension_manager: None,
provider: Arc::new(tokio::sync::Mutex::new(None)),
providers: Default::default(),
session_manager: Arc::new(SessionManager::new(std::env::temp_dir())),
scheduler: None,
use_login_shell_path: false,

View file

@ -262,19 +262,6 @@ impl AppsManagerClient {
.result_with_platform_notification(result, EXTENSION_NAME, event_type, params)
}
async fn get_provider(&self) -> Result<Arc<dyn Provider>, String> {
let provider = self
.context
.provider
.lock()
.await
.as_ref()
.ok_or("Provider not available")?
.clone();
Ok(provider)
}
async fn effective_model_config(
&self,
session_id: &str,
@ -320,7 +307,7 @@ impl AppsManagerClient {
session_id: &str,
prd: &str,
) -> Result<CreateAppContentResponse, String> {
let provider = self.get_provider().await?;
let provider = self.context.provider_for_session(session_id).await?;
let existing_apps = self.list_stored_apps().unwrap_or_default();
let existing_names = existing_apps.join(", ");
@ -371,7 +358,7 @@ impl AppsManagerClient {
existing_prd: &str,
feedback: &str,
) -> Result<UpdateAppContentResponse, String> {
let provider = self.get_provider().await?;
let provider = self.context.provider_for_session(session_id).await?;
let context: HashMap<&str, &str> = HashMap::new();
let system_prompt = render_template("apps_iterate.md", &context)
@ -828,7 +815,7 @@ mod tests {
info: AppsManagerClient::create_info(),
context: PlatformExtensionContext {
extension_manager: None,
provider: Arc::new(tokio::sync::Mutex::new(None)),
providers: Default::default(),
session_manager: Arc::new(SessionManager::new(apps_dir.join("sessions"))),
scheduler: None,
use_login_shell_path: false,

View file

@ -446,7 +446,7 @@ mod tests {
let client = ChatRecallClient::new(PlatformExtensionContext {
extension_manager: None,
provider: Arc::new(tokio::sync::Mutex::new(None)),
providers: Default::default(),
session_manager,
scheduler: None,
use_login_shell_path: false,

View file

@ -801,9 +801,7 @@ mod tests {
#[tokio::test]
async fn callback_configs_exclude_tools_hidden_from_model() {
let temp = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
temp.path().join("manager"),
));
let manager = Arc::new(ExtensionManager::with_data_dir(temp.path().join("manager")));
manager
.add_client(
ExtensionConfig::Builtin {
@ -836,9 +834,7 @@ mod tests {
#[tokio::test]
async fn callback_configs_leave_delegate_to_direct_tool_calls() {
let temp = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
temp.path().join("manager"),
));
let manager = Arc::new(ExtensionManager::with_data_dir(temp.path().join("manager")));
let session = manager
.get_context()
.session_manager
@ -881,9 +877,7 @@ mod tests {
#[tokio::test]
async fn callback_uses_lease_and_applies_extension_mutation() {
let temp = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
temp.path().join("manager"),
));
let manager = Arc::new(ExtensionManager::with_data_dir(temp.path().join("manager")));
let session = manager
.get_context()
.session_manager
@ -1173,9 +1167,7 @@ mod tests {
#[tokio::test]
async fn moim_uses_the_supplied_tool_snapshot() {
let temp = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
temp.path().join("manager"),
));
let manager = Arc::new(ExtensionManager::with_data_dir(temp.path().join("manager")));
let mut context = manager.get_context().clone();
context.extension_manager = Some(Arc::downgrade(&manager));
let client = CodeExecutionClient::new(context, ToolDisclosure::Catalog).unwrap();
@ -1198,7 +1190,7 @@ mod tests {
let client = CodeExecutionClient::new(
PlatformExtensionContext {
extension_manager: None,
provider: Arc::new(tokio::sync::Mutex::new(None)),
providers: Default::default(),
session_manager: Arc::new(crate::session::SessionManager::new(
temp.path().join("sessions"),
)),

View file

@ -294,7 +294,7 @@ mod tests {
fn test_context(data_dir: std::path::PathBuf) -> PlatformExtensionContext {
PlatformExtensionContext {
extension_manager: None,
provider: Arc::new(tokio::sync::Mutex::new(None)),
providers: Default::default(),
session_manager: Arc::new(SessionManager::new(data_dir)),
scheduler: None,
use_login_shell_path: false,

View file

@ -512,7 +512,7 @@ mod tests {
fn client_for(manager: &Arc<ExtensionManager>) -> ExtensionManagerClient {
ExtensionManagerClient::new(PlatformExtensionContext {
extension_manager: None,
provider: manager.get_provider().clone(),
providers: manager.get_context().providers.clone(),
session_manager: manager.get_context().session_manager.clone(),
scheduler: None,
use_login_shell_path: false,
@ -568,7 +568,7 @@ mod tests {
#[tokio::test]
async fn manage_extensions_emits_a_mutation_and_refuses_subagents() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
let manager = Arc::new(ExtensionManager::with_data_dir(
temp_dir.path().to_path_buf(),
));
let client = client_for(&manager);
@ -608,7 +608,7 @@ mod tests {
#[tokio::test]
async fn extension_manager_cannot_disable_itself() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
let manager = Arc::new(ExtensionManager::with_data_dir(
temp_dir.path().to_path_buf(),
));
let client = client_for(&manager);
@ -635,7 +635,7 @@ mod tests {
#[tokio::test]
async fn subagent_and_unknown_callers_are_not_offered_extension_management() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = Arc::new(ExtensionManager::new_without_provider(
let manager = Arc::new(ExtensionManager::with_data_dir(
temp_dir.path().to_path_buf(),
));
let client = client_for(&manager);

View file

@ -223,13 +223,28 @@ pub struct PlatformExtensionContext {
/// below.
pub extension_manager:
Option<std::sync::Weak<crate::agents::extension_manager::ExtensionManager>>,
pub provider: crate::agents::types::SharedProvider,
pub providers: std::sync::Arc<crate::agents::provider_manager::ProviderManager>,
pub session_manager: std::sync::Arc<crate::session::SessionManager>,
pub scheduler: Option<std::sync::Arc<dyn crate::scheduler_trait::SchedulerTrait>>,
pub use_login_shell_path: bool,
}
impl PlatformExtensionContext {
pub async fn provider_for_session(
&self,
session_id: &str,
) -> Result<std::sync::Arc<dyn crate::providers::base::Provider>, String> {
let session = self
.session_manager
.get_session(session_id, false)
.await
.map_err(|e| e.to_string())?;
self.providers
.provider_for(&session)
.await
.map_err(|e| e.to_string())
}
pub async fn model_config_for_session(
&self,
session_id: &str,

View file

@ -48,19 +48,6 @@ impl SummarizeClient {
Ok(Self { info, context })
}
async fn get_provider(&self) -> Result<Arc<dyn Provider>, String> {
let provider = self
.context
.provider
.lock()
.await
.as_ref()
.ok_or("Provider not available")?
.clone();
Ok(provider)
}
fn get_tools() -> Vec<Tool> {
let schema = schema_for!(SummarizeParams);
let schema_value =
@ -134,7 +121,7 @@ impl McpClientTrait for SummarizeClient {
)]));
}
let provider = match self.get_provider().await {
let provider = match self.context.provider_for_session(&ctx.session_id).await {
Ok(p) => p,
Err(e) => {
return Ok(CallToolResult::error(vec![ContentBlock::text(format!(

View file

@ -1273,8 +1273,8 @@ impl SummonClient {
.await?;
let provider = match providers::get_from_registry(&config.provider_name).await {
Ok(entry) => entry.create(config.extensions.clone()).await?,
Err(error) => match self.context.provider.lock().await.clone() {
Some(provider)
Err(error) => match self.context.providers.provider_for(session).await {
Ok(provider)
if provider.get_name() == config.provider_name
&& !provider.manages_own_context() =>
{
@ -1640,7 +1640,7 @@ mod tests {
) -> PlatformExtensionContext {
PlatformExtensionContext {
extension_manager: None,
provider: Arc::new(tokio::sync::Mutex::new(None)),
providers: Default::default(),
session_manager,
scheduler: None,
use_login_shell_path: false,
@ -1782,7 +1782,7 @@ mod tests {
.await
.unwrap();
assert_eq!(
reloaded_agent.provider().await.unwrap().get_name(),
reloaded_agent.provider(&child.id).await.unwrap().get_name(),
"openai"
);
}
@ -2462,15 +2462,19 @@ You review code."#;
)
.unwrap(),
);
let mut context = create_test_context();
context.provider = Arc::new(tokio::sync::Mutex::new(Some(Arc::clone(&parent_provider))));
let context = create_test_context();
let providers = context.providers.clone();
let client = SummonClient::new(context).unwrap();
let session = crate::session::Session {
id: "unregistered-parent".to_string(),
provider_name: Some(parent_provider.get_name().to_string()),
model_config: Some(goose_providers::model::ModelConfig::new("test-model")),
working_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
providers
.set_provider(&session.id, Arc::clone(&parent_provider))
.await;
let params = DelegateParams {
instructions: Some("Review the change".to_string()),
@ -2497,9 +2501,7 @@ You review code."#;
async fn test_build_task_config_recreates_registered_parent_provider() {
let temp_dir = TempDir::new().unwrap();
let parent_provider = providers::create("openai", Vec::new()).await.unwrap();
let mut context = create_test_context();
context.provider = Arc::new(tokio::sync::Mutex::new(Some(Arc::clone(&parent_provider))));
let client = SummonClient::new(context).unwrap();
let client = SummonClient::new(create_test_context()).unwrap();
let session = crate::session::Session {
provider_name: Some(parent_provider.get_name().to_string()),
model_config: Some(goose_providers::model::ModelConfig::new("test-model")),

View file

@ -518,7 +518,7 @@ mod tests {
.unwrap();
let context = PlatformExtensionContext {
extension_manager: None,
provider: Arc::new(tokio::sync::Mutex::new(None)),
providers: Default::default(),
session_manager,
scheduler: Some(scheduler),
use_login_shell_path: false,

View file

@ -0,0 +1,109 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use anyhow::{anyhow, Result};
use goose_providers::model::ModelConfig;
use tracing::warn;
use crate::config::Config;
use crate::providers::base::Provider;
use crate::providers::provider_registry::ProviderEntry;
use crate::session::extension_data::EnabledExtensionsState;
use crate::session::Session;
type SessionSlot = Arc<tokio::sync::Mutex<Option<Arc<dyn Provider>>>>;
type SharedProviders = HashMap<String, (u64, Arc<dyn Provider>)>;
#[derive(Default)]
pub struct ProviderManager {
shared: tokio::sync::Mutex<SharedProviders>,
sessions: Mutex<HashMap<String, SessionSlot>>,
}
impl ProviderManager {
pub async fn provider_for(&self, session: &Session) -> Result<Arc<dyn Provider>> {
let name = provider_name_for(session)?;
let slot = self.slot(&session.id);
let mut slot = slot.lock().await;
if let Some(provider) = slot.as_ref().filter(|p| p.get_name() == name) {
return Ok(provider.clone());
}
let entry = crate::providers::get_from_registry(&name).await?;
if !entry.session_bound() {
let provider = self.shared(&name, &entry).await?;
*slot = None;
return Ok(provider);
}
let extensions = EnabledExtensionsState::extensions_or_default(
Some(&session.extension_data),
Config::global(),
);
let provider = entry
.create_with_working_dir(extensions, session.working_dir.clone())
.await?;
let model_config = entry.normalize_model_config(model_config_for(session)?)?;
if let Err(e) = provider.apply_model_selection(&model_config).await {
warn!("Failed to apply model selection to provider: {e}");
}
provider
.update_mode(&session.id, session.goose_mode)
.await
.map_err(|e| anyhow!("Failed to propagate mode to provider: {e}"))?;
*slot = Some(provider.clone());
Ok(provider)
}
pub async fn set_provider(&self, session_id: &str, provider: Arc<dyn Provider>) {
*self.slot(session_id).lock().await = Some(provider);
}
pub fn release(&self, session_id: &str) {
self.sessions.lock().unwrap().remove(session_id);
}
fn slot(&self, session_id: &str) -> SessionSlot {
self.sessions
.lock()
.unwrap()
.entry(session_id.to_string())
.or_default()
.clone()
}
async fn shared(&self, name: &str, entry: &ProviderEntry) -> Result<Arc<dyn Provider>> {
let generation = Config::global().generation();
let mut shared = self.shared.lock().await;
if let Some((cached_generation, provider)) = shared.get(name) {
if *cached_generation == generation {
return Ok(provider.clone());
}
}
let provider = entry.create(Vec::new()).await?;
shared.insert(name.to_string(), (generation, provider.clone()));
Ok(provider)
}
}
pub fn provider_name_for(session: &Session) -> Result<String> {
match &session.provider_name {
Some(name) => Ok(name.clone()),
None => Config::global()
.get_goose_provider()
.map_err(|_| anyhow!("Could not configure agent: missing provider")),
}
}
pub fn model_config_for(session: &Session) -> Result<ModelConfig> {
if let Some(model_config) = &session.model_config {
return Ok(model_config.clone());
}
let config = Config::global();
let provider_name = provider_name_for(session)?;
let model_name = config
.get_goose_model()
.map_err(|_| anyhow!("Could not resolve model config: missing model"))?;
crate::model_config::model_config_from_user_config(&provider_name, &model_name)
.map_err(|e| anyhow!("Could not resolve model config: {e}"))
}

View file

@ -781,9 +781,10 @@ async fn build_test_pipeline(
} else {
provider
};
let shared_provider = Arc::new(TokioMutex::new(Some(provider.clone())));
let providers: Arc<crate::agents::provider_manager::ProviderManager> = Default::default();
providers.set_provider(&session.id, provider.clone()).await;
let extension_manager = Arc::new(ExtensionManager::new(
shared_provider.clone(),
providers.clone(),
session_manager.clone(),
scheduler
.clone()
@ -802,7 +803,7 @@ async fn build_test_pipeline(
tool_inspection_manager.add_inspector(Box::new(SecurityInspector::enabled()));
tool_inspection_manager.add_inspector(Box::new(PermissionInspector::new(
permission_manager.clone(),
shared_provider,
providers,
session_manager.clone(),
)));
let model_config = session

View file

@ -100,14 +100,6 @@ pub(crate) async fn from_foreground_subagent_session(
.ok_or_else(|| anyhow!("Subagent {session_id} has no saved extension selection"))?;
let extensions = EnabledExtensionsState::from_value(saved_extensions)?.extensions;
let provider = crate::providers::create_with_working_dir(
provider_name,
extensions.clone(),
session.working_dir.clone(),
)
.await?;
provider.apply_model_selection(model_config).await?;
let mut config = AgentConfig::new(
session_manager,
PermissionManager::instance(),
@ -119,7 +111,9 @@ pub(crate) async fn from_foreground_subagent_session(
.with_use_login_shell_path(use_login_shell_path);
config.is_subagent = true;
let agent = Agent::with_config(config);
*agent.provider.lock().await = Some(provider);
agent
.switch_provider(session_id, provider_name, model_config.clone())
.await?;
for extension in extensions {
let name = extension.name();
if let Err(e) = agent.add_extension_inner(extension, session_id).await {

View file

@ -1,9 +1,4 @@
use crate::providers::base::Provider;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
pub type SharedProvider = Arc<Mutex<Option<Arc<dyn Provider>>>>;
/// Default timeout for retry operations (5 minutes)
pub const DEFAULT_RETRY_TIMEOUT_SECONDS: u64 = 300;

View file

@ -9,7 +9,7 @@
///
use anyhow::{Context, Result};
use clap::Parser;
use goose::providers::create_with_named_model;
use goose::providers::create;
use goose_providers::canonical::ModelMapping;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@ -433,7 +433,7 @@ async fn check_provider(
) -> Result<(Vec<String>, Vec<ModelMapping>, Vec<String>)> {
println!("Checking provider: {}", provider_name);
let provider = match create_with_named_model(provider_name, Vec::new()).await {
let provider = match create(provider_name, Vec::new()).await {
Ok(p) => p,
Err(e) => {
println!(" ⚠ Failed to create provider: {}", e);

View file

@ -14,6 +14,7 @@ use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use thiserror::Error;
@ -124,6 +125,7 @@ pub struct Config {
secrets: SecretStorage,
guard: Mutex<()>,
secrets_cache: Arc<Mutex<Option<HashMap<String, Value>>>>,
generation: AtomicU64,
}
enum SecretStorage {
@ -211,6 +213,7 @@ impl Default for Config {
},
guard: Mutex::new(()),
secrets_cache: Arc::new(Mutex::new(None)),
generation: AtomicU64::new(0),
};
let keyring_disabled = env::var("GOOSE_DISABLE_KEYRING").is_ok()
@ -223,6 +226,7 @@ impl Default for Config {
secrets,
guard: Mutex::new(()),
secrets_cache: Arc::new(Mutex::new(None)),
generation: AtomicU64::new(0),
}
}
}
@ -441,6 +445,7 @@ impl Config {
secrets,
guard: Mutex::new(()),
secrets_cache: Arc::new(Mutex::new(None)),
generation: AtomicU64::new(0),
})
}
@ -459,6 +464,7 @@ impl Config {
},
guard: Mutex::new(()),
secrets_cache: Arc::new(Mutex::new(None)),
generation: AtomicU64::new(0),
})
}
@ -473,6 +479,7 @@ impl Config {
},
guard: Mutex::new(()),
secrets_cache: Arc::new(Mutex::new(None)),
generation: AtomicU64::new(0),
})
}
@ -663,6 +670,7 @@ impl Config {
// Atomically replace the original file
std::fs::rename(&temp_path, &target_path)?;
self.generation.fetch_add(1, Ordering::Relaxed);
Ok(())
}
@ -1186,6 +1194,12 @@ impl Config {
pub fn invalidate_secrets_cache(&self) {
let mut cache = self.secrets_cache.lock().unwrap();
*cache = None;
self.generation.fetch_add(1, Ordering::Relaxed);
}
/// Changes whenever this process writes config or secrets.
pub fn generation(&self) -> u64 {
self.generation.load(Ordering::Relaxed)
}
/// Check if an error string indicates a keyring availability issue that should trigger fallback

View file

@ -11,7 +11,7 @@ use std::num::NonZeroUsize;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex, OnceCell, RwLock};
use tokio_util::sync::CancellationToken;
use tracing::{debug, info};
use tracing::info;
const DEFAULT_MAX_SESSION: usize = 100;
@ -33,7 +33,6 @@ pub struct AgentManagerGetResult {
pub struct AgentManager {
sessions: Arc<RwLock<LruCache<String, Arc<Agent>>>>,
agent_config: AgentConfig,
default_provider: Arc<RwLock<Option<Arc<dyn crate::providers::base::Provider>>>>,
cancel_tokens: Arc<RwLock<HashMap<String, CancellationToken>>>,
/// Per-session creation locks. When `get_or_create_agent` misses the
/// `sessions` cache it acquires the per-session lock before doing the
@ -54,7 +53,6 @@ impl AgentManager {
let manager = Self {
sessions: Arc::new(RwLock::new(LruCache::new(capacity))),
agent_config,
default_provider: Arc::new(RwLock::new(None)),
cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
creation_locks: Arc::new(Mutex::new(HashMap::new())),
};
@ -95,11 +93,6 @@ impl AgentManager {
self.agent_config.session_manager.as_ref()
}
pub async fn set_default_provider(&self, provider: Arc<dyn crate::providers::base::Provider>) {
debug!("Setting default provider on AgentManager");
*self.default_provider.write().await = Some(provider);
}
pub async fn get_or_create_agent(&self, session_id: String) -> Result<Arc<Agent>> {
Ok(self
.get_or_create_agent_with_runtime_context(session_id, RuntimeContext::default())
@ -141,8 +134,8 @@ impl AgentManager {
// Funnel the fallible work through a helper so we can prune the
// per-session creation lock on every error exit. Without this
// the provider-setup path (update_provider / update_mode) could
// bail out via `?`, leaving a permanent `creation_locks` entry
// provider restore or recipe setup could bail out via `?`,
// leaving a permanent `creation_locks` entry
// for a session that never made it into the LRU cache and that
// no one will ever call `remove_session` on.
let result = self.create_agent_locked(&session_id, runtime_context).await;
@ -228,31 +221,6 @@ impl AgentManager {
extension_results = agent.load_extensions_from_session(&session).await;
}
if agent.provider().await.is_err() {
if let Some(provider) = &*self.default_provider.read().await {
let config = crate::config::Config::global();
let model_config = config
.get_goose_provider()
.ok()
.zip(config.get_goose_model().ok())
.and_then(|(provider_name, model_name)| {
crate::model_config::model_config_from_user_config(
&provider_name,
&model_name,
)
.ok()
})
.unwrap_or_else(|| goose_providers::model::ModelConfig::new("unknown"));
agent
.update_provider(Arc::clone(provider), model_config, session_id)
.await?;
provider
.update_mode(session_id, mode)
.await
.map_err(|e| anyhow::anyhow!("Failed to propagate mode to provider: {}", e))?;
}
}
let mut sessions = self.sessions.write().await;
if let Some(existing) = sessions.get(session_id) {
return Ok(AgentManagerGetResult {
@ -266,12 +234,15 @@ impl AgentManager {
// key so we can also drop its creation lock below, otherwise the
// `creation_locks` HashMap would grow without bound in long-lived
// processes that churn through many sessions.
let evicted = sessions
.push(session_id.to_string(), agent.clone())
.map(|(k, _)| k);
let evicted = sessions.push(session_id.to_string(), agent.clone());
drop(sessions);
if let Some(evicted_id) = evicted {
if let Some((evicted_id, evicted_agent)) = evicted {
// A run may still hold the evicted agent; its session-bound
// provider has to outlive that run.
if Arc::strong_count(&evicted_agent) == 1 {
self.agent_config.providers.release(&evicted_id);
}
self.prune_creation_lock(&evicted_id).await;
}
@ -311,6 +282,7 @@ impl AgentManager {
.pop(session_id)
.ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?;
drop(sessions);
self.agent_config.providers.release(session_id);
// Best-effort prune of the per-session creation lock so the
// HashMap doesn't grow unbounded. Any caller still holding a
// clone of the Arc keeps the underlying Mutex alive until it
@ -325,11 +297,11 @@ impl AgentManager {
if let Some(token) = self.cancel_tokens.write().await.remove(session_id) {
token.cancel();
}
let mut sessions = self.sessions.write().await;
if sessions.pop(session_id).is_none() {
let removed = self.sessions.write().await.pop(session_id);
self.agent_config.providers.release(session_id);
if removed.is_none() {
return Ok(());
}
drop(sessions);
self.prune_creation_lock(session_id).await;
info!("Removed session {}", session_id);
Ok(())
@ -625,78 +597,6 @@ mod tests {
);
}
#[tokio::test]
async fn test_failed_creation_prunes_creation_lock() {
// Regression test for the Codex review note on PR #9357: when the
// provider-setup path in `create_agent_locked` returns Err, the
// outer `get_or_create_agent` must also drop its local Arc clone
// of the creation lock before pruning. Otherwise
// `Arc::strong_count` stays > 1 and the failed session leaks a
// permanent entry in `creation_locks`.
use async_trait::async_trait;
use rmcp::model::Tool;
use crate::conversation::message::Message;
use crate::providers::base::{MessageStream, Provider};
use goose_providers::conversation::token_usage::{ProviderUsage, Usage};
use goose_providers::errors::ProviderError;
use goose_providers::model::ModelConfig;
struct FailingProvider;
#[async_trait]
impl Provider for FailingProvider {
fn get_name(&self) -> &str {
"failing-test-provider"
}
async fn stream(
&self,
_model_config: &ModelConfig,
_system: &str,
_messages: &[Message],
_tools: &[Tool],
) -> std::result::Result<MessageStream, ProviderError> {
Ok(crate::providers::base::stream_from_single_message(
Message::assistant().with_text("unused"),
ProviderUsage::new("failing-test-provider".into(), Usage::default()),
))
}
async fn update_mode(
&self,
_session_id: &str,
_mode: GooseMode,
) -> std::result::Result<(), ProviderError> {
Err(ProviderError::ExecutionError(
"intentional failure for test".into(),
))
}
}
let temp_dir = TempDir::new().unwrap();
let manager = create_test_manager(&temp_dir).await;
manager
.set_default_provider(Arc::new(FailingProvider))
.await;
let session_id = String::from("failed-creation-test");
let result = manager.get_or_create_agent(session_id.clone()).await;
assert!(
result.is_err(),
"expected provider mode-update failure to propagate"
);
assert!(
manager.creation_locks.lock().await.is_empty(),
"creation_locks must be empty after a failed agent creation"
);
assert!(
!manager.has_session(&session_id).await,
"failed creation must not insert into the LRU cache"
);
}
#[tokio::test]
async fn test_lru_eviction_prunes_creation_lock() {
// Sessions can disappear from the LRU cache without going through

View file

@ -1,5 +1,5 @@
use crate::agents::platform_extensions::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
use crate::agents::types::SharedProvider;
use crate::agents::provider_manager::ProviderManager;
use crate::config::permission::PermissionLevel;
use crate::config::{GooseMode, PermissionManager};
use crate::conversation::message::{Message, ToolRequest};
@ -14,7 +14,7 @@ use std::sync::{Arc, RwLock};
/// Permission Inspector that handles tool permission checking
pub struct PermissionInspector {
pub permission_manager: Arc<PermissionManager>,
provider: SharedProvider,
providers: Arc<ProviderManager>,
session_manager: Arc<crate::session::SessionManager>,
readonly_tools: RwLock<HashSet<String>>,
}
@ -36,12 +36,12 @@ fn cache_non_readonly_decision(
impl PermissionInspector {
pub fn new(
permission_manager: Arc<PermissionManager>,
provider: SharedProvider,
providers: Arc<ProviderManager>,
session_manager: Arc<crate::session::SessionManager>,
) -> Self {
Self {
permission_manager,
provider,
providers,
session_manager,
readonly_tools: RwLock::new(HashSet::new()),
}
@ -228,7 +228,11 @@ impl ToolInspector for PermissionInspector {
// LLM-based read-only detection for deferred SmartApprove candidates
if !llm_detect_candidates.is_empty() {
let detected_request_ids: HashSet<String> = match self.provider.lock().await.clone() {
let provider = match self.session_manager.get_session(session_id, false).await {
Ok(session) => self.providers.provider_for(&session).await.ok(),
Err(_) => None,
};
let detected_request_ids: HashSet<String> = match provider {
Some(provider) => detect_read_only_requests(
provider,
&self.session_manager,
@ -276,7 +280,6 @@ mod tests {
use rmcp::object;
use std::sync::Arc;
use test_case::test_case;
use tokio::sync::Mutex;
async fn inspect_tool(
mode: GooseMode,
@ -295,7 +298,7 @@ mod tests {
tempfile::tempdir().unwrap().keep(),
));
let inspector =
PermissionInspector::new(Arc::clone(&pm), Arc::new(Mutex::new(None)), session_manager);
PermissionInspector::new(Arc::clone(&pm), Default::default(), session_manager);
if smart_approved {
*inspector.readonly_tools.write().unwrap() = ["tool".to_string()].into_iter().collect();
}

View file

@ -46,6 +46,7 @@ impl goose_providers::base::ProviderDescriptor for AmpAcpProvider {
impl ProviderDef for AmpAcpProvider {
type Provider = AcpProvider;
const SESSION_BOUND: bool = true;
fn from_env(
extensions: Vec<crate::config::ExtensionConfig>,

View file

@ -31,6 +31,10 @@ pub(crate) fn current_working_dir() -> PathBuf {
pub trait ProviderDef: ProviderDescriptor + Send + Sync {
type Provider: Provider + 'static;
/// Instances hold a remote session or are built from one session's
/// extensions and working directory, so they cannot be shared.
const SESSION_BOUND: bool = false;
fn from_env(
extensions: Vec<ExtensionConfig>,
tls_config: Option<TlsConfig>,

View file

@ -48,6 +48,7 @@ impl goose_providers::base::ProviderDescriptor for ClaudeAcpProvider {
impl ProviderDef for ClaudeAcpProvider {
type Provider = AcpProvider;
const SESSION_BOUND: bool = true;
fn from_env(
extensions: Vec<crate::config::ExtensionConfig>,

View file

@ -640,6 +640,7 @@ impl goose_providers::base::ProviderDescriptor for ClaudeCodeProvider {
impl ProviderDef for ClaudeCodeProvider {
type Provider = Self;
const SESSION_BOUND: bool = true;
fn from_env(
extensions: Vec<ExtensionConfig>,

View file

@ -637,6 +637,7 @@ impl goose_providers::base::ProviderDescriptor for CodexProvider {
impl ProviderDef for CodexProvider {
type Provider = Self;
const SESSION_BOUND: bool = true;
fn from_env(
extensions: Vec<ExtensionConfig>,

View file

@ -56,6 +56,7 @@ impl goose_providers::base::ProviderDescriptor for CodexAcpProvider {
impl ProviderDef for CodexAcpProvider {
type Provider = AcpProvider;
const SESSION_BOUND: bool = true;
fn from_env(
extensions: Vec<crate::config::ExtensionConfig>,

View file

@ -105,6 +105,7 @@ impl CopilotAcpProvider {
impl ProviderDef for CopilotAcpProvider {
type Provider = AcpProvider;
const SESSION_BOUND: bool = true;
fn from_env(
extensions: Vec<crate::config::ExtensionConfig>,

View file

@ -183,6 +183,7 @@ impl goose_providers::base::ProviderDescriptor for GeminiCliProvider {
impl ProviderDef for GeminiCliProvider {
type Provider = Self;
const SESSION_BOUND: bool = true;
fn from_env(
_extensions: Vec<crate::config::ExtensionConfig>,

View file

@ -1,4 +1,3 @@
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
#[cfg(feature = "aws-providers")]
@ -277,15 +276,6 @@ pub async fn create(name: &str, extensions: Vec<ExtensionConfig>) -> Result<Arc<
entry.create(extensions).await
}
pub async fn create_with_working_dir(
name: &str,
extensions: Vec<ExtensionConfig>,
working_dir: PathBuf,
) -> Result<Arc<dyn Provider>> {
let entry = get_from_registry(name).await?;
entry.create_with_working_dir(extensions, working_dir).await
}
pub async fn create_with_default_model(
name: impl AsRef<str>,
extensions: Vec<ExtensionConfig>,
@ -310,13 +300,6 @@ pub async fn cleanup_provider(name: &str) -> Result<()> {
Ok(())
}
pub async fn create_with_named_model(
provider_name: &str,
extensions: Vec<ExtensionConfig>,
) -> Result<Arc<dyn Provider>> {
create(provider_name, extensions).await
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -100,8 +100,7 @@ pub mod xai;
pub mod xai_oauth;
pub use init::{
cleanup_provider, create, create_with_default_model, create_with_named_model,
create_with_working_dir, get_from_registry, inventory_identity, providers,
refresh_custom_providers,
cleanup_provider, create, create_with_default_model, get_from_registry, inventory_identity,
providers, refresh_custom_providers,
};
pub use retry::{retry_operation, RetryConfig};

View file

@ -88,6 +88,7 @@ impl PiAcpProvider {
impl ProviderDef for PiAcpProvider {
type Provider = AcpProvider;
const SESSION_BOUND: bool = true;
fn from_env(
extensions: Vec<crate::config::ExtensionConfig>,

View file

@ -33,6 +33,7 @@ pub struct ProviderEntry {
supports_inventory_refresh: bool,
tls_config: Option<TlsConfig>,
toolshim: bool,
session_bound: bool,
}
impl ProviderEntry {
@ -44,6 +45,10 @@ impl ProviderEntry {
self.provider_type
}
pub fn session_bound(&self) -> bool {
self.session_bound
}
pub fn supports_inventory_refresh(&self) -> bool {
self.supports_inventory_refresh
}
@ -154,6 +159,7 @@ impl ProviderRegistry {
supports_inventory_refresh: inventory.supports_refresh,
tls_config: self.tls_config.clone(),
toolshim: false,
session_bound: F::SESSION_BOUND,
},
);
}
@ -319,6 +325,7 @@ impl ProviderRegistry {
supports_inventory_refresh,
tls_config: self.tls_config.clone(),
toolshim: config.toolshim,
session_bound: false,
},
);
}

View file

@ -18,7 +18,6 @@ use crate::conversation::message::Message;
use crate::conversation::Conversation;
#[cfg(feature = "telemetry")]
use crate::posthog;
use crate::providers::create;
use crate::recipe::build_recipe::build_recipe_from_template;
use crate::recipe::validate_recipe::{
recipe_file_format, validate_recipe_for_scheduling, SchedulerRecipeError,
@ -895,9 +894,8 @@ async fn execute_job(
agent.add_extension(ext.clone(), &session.id).await?;
}
let agent_provider = create(&provider_name, extensions).await?;
agent
.update_provider(agent_provider, model_config, &session.id)
.switch_provider(&session.id, &provider_name, model_config)
.await?;
agent
.update_goose_mode(GooseMode::Auto, &session.id)

View file

@ -3,7 +3,7 @@ use async_trait::async_trait;
use chrono::Utc;
use std::sync::{Arc, OnceLock};
use crate::agents::types::SharedProvider;
use crate::agents::provider_manager::ProviderManager;
use crate::config::paths::Paths;
use crate::config::GooseMode;
use crate::conversation::message::{Message, MessageContent, ToolRequest};
@ -13,28 +13,6 @@ use crate::utils::safe_truncate;
const DEFAULT_TOOLS: &[&str] = &["shell"];
async fn resolve_model_config(
session_manager: &crate::session::SessionManager,
session_id: &str,
) -> Result<goose_providers::model::ModelConfig> {
if !session_id.is_empty() {
if let Ok(session) = session_manager.get_session(session_id, false).await {
if let Some(model_config) = session.model_config {
return Ok(model_config);
}
}
}
let config = crate::config::Config::global();
let provider_name = config
.get_goose_provider()
.map_err(|_| anyhow::anyhow!("missing provider"))?;
let model_name = config
.get_goose_model()
.map_err(|_| anyhow::anyhow!("missing model"))?;
crate::model_config::model_config_from_user_config(&provider_name, &model_name)
}
const DEFAULT_RULES: &str = r#"BLOCK if the command:
- Exfiltrates data (curl/wget posting to unknown URLs, piping secrets out)
- Is destructive beyond the project scope (rm -rf /, modifying system files)
@ -71,7 +49,7 @@ struct AdversaryConfig {
/// If the file is absent, this inspector is disabled.
/// If the review fails, the inspector fails open (allows the tool call).
pub struct AdversaryInspector {
provider: SharedProvider,
providers: Arc<ProviderManager>,
session_manager: Arc<crate::session::SessionManager>,
config: OnceLock<Option<AdversaryConfig>>,
config_path: Option<std::path::PathBuf>,
@ -79,11 +57,11 @@ pub struct AdversaryInspector {
impl AdversaryInspector {
pub fn new(
provider: SharedProvider,
providers: Arc<ProviderManager>,
session_manager: Arc<crate::session::SessionManager>,
) -> Self {
Self {
provider,
providers,
session_manager,
config: OnceLock::new(),
config_path: None,
@ -91,12 +69,12 @@ impl AdversaryInspector {
}
pub fn with_config_dir(
provider: SharedProvider,
providers: Arc<ProviderManager>,
session_manager: Arc<crate::session::SessionManager>,
config_dir: std::path::PathBuf,
) -> Self {
Self {
provider,
providers,
session_manager,
config: OnceLock::new(),
config_path: Some(config_dir.join("adversary.md")),
@ -276,12 +254,12 @@ impl AdversaryInspector {
recent_messages: &[String],
rules: &str,
) -> Result<(bool, String)> {
let provider_guard = self.provider.lock().await;
let provider = match provider_guard.clone() {
Some(p) => p,
None => return Ok((true, "No provider available".to_string())),
let Ok(session) = self.session_manager.get_session(session_id, false).await else {
return Ok((true, "No provider available".to_string()));
};
let Ok(provider) = self.providers.provider_for(&session).await else {
return Ok((true, "No provider available".to_string()));
};
drop(provider_guard);
let history_section = if !recent_messages.is_empty() {
let mut s = String::from("Recent user messages (oldest first):\n");
@ -321,9 +299,7 @@ impl AdversaryInspector {
)];
let conversation = Conversation::new_unvalidated(check_messages);
let model_config = resolve_model_config(&self.session_manager, session_id)
.await
.map_err(|e| anyhow::anyhow!("Could not resolve model config: {}", e))?;
let model_config = crate::agents::provider_manager::model_config_for(&session)?;
let (response, _usage) = crate::session_context::with_session_id(
Some(session_id.to_string()),
provider.complete(&model_config, system_prompt, conversation.messages(), &[]),
@ -499,7 +475,6 @@ mod tests {
use rmcp::model::CallToolRequestParams;
use rmcp::object;
use std::sync::Arc;
use tokio::sync::Mutex;
#[test]
fn test_parse_with_tools_frontmatter() {
@ -683,12 +658,11 @@ mod tests {
async fn test_disabled_when_no_adversary_md() {
let tmp = tempfile::tempdir().unwrap();
let provider: SharedProvider = Arc::new(Mutex::new(None));
let session_manager = Arc::new(crate::session::SessionManager::new(
tmp.path().to_path_buf(),
));
let inspector = AdversaryInspector::with_config_dir(
provider,
Default::default(),
session_manager,
tmp.path().to_path_buf(),
);

View file

@ -33,7 +33,7 @@ pub(crate) async fn generate_tool_title(
session_id: &str,
tool_request: &ToolRequest,
) -> Option<String> {
let provider = agent.provider().await.ok()?;
let provider = agent.provider(session_id).await.ok()?;
if provider.manages_own_context() {
return None;
}
@ -72,7 +72,7 @@ pub(crate) async fn generate_tool_chain_summary(
return None;
}
let provider = agent.provider().await.ok()?;
let provider = agent.provider(session_id).await.ok()?;
if provider.manages_own_context() {
return None;
}

View file

@ -448,7 +448,7 @@ pub async fn run_fs_write_text_file_true<C: Connection>() {
pub async fn run_initialize_doesnt_hit_provider<C: Connection>() {
let provider_factory: AcpProviderFactory =
Arc::new(|_, _, _, _| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) }));
Arc::new(|_| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) }));
let openai = OpenAiFixture::new(vec![], C::expected_session_id()).await;
let config = TestConnectionConfig {

View file

@ -1136,21 +1136,18 @@ fn test_custom_provider_supported_models_lists_raw_provider_models() {
write_acp_global_config(DEFAULT_ACP_TEST_CONFIG);
run_test(async move {
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
let provider_factory: AcpProviderFactory = Arc::new(
|provider_name, _extensions, _working_dir, use_default_model| {
assert!(use_default_model);
Box::pin(async move {
Ok(Arc::new(MockProvider {
name: provider_name,
recommended_models: vec!["canonical-filtered-model".to_string()],
supported_models: Ok(vec![
"goose-claude-opus-4-8".to_string(),
"raw-databricks-endpoint".to_string(),
]),
}) as Arc<dyn Provider>)
})
},
);
let provider_factory: AcpProviderFactory = Arc::new(|provider_name| {
Box::pin(async move {
Ok(Arc::new(MockProvider {
name: provider_name,
recommended_models: vec!["canonical-filtered-model".to_string()],
supported_models: Ok(vec![
"goose-claude-opus-4-8".to_string(),
"raw-databricks-endpoint".to_string(),
]),
}) as Arc<dyn Provider>)
})
});
let conn = AcpServerConnection::new(
TestConnectionConfig {
provider_factory: Some(provider_factory),
@ -1188,7 +1185,7 @@ fn test_custom_provider_supported_models_maps_not_configured_error() {
write_acp_global_config(DEFAULT_ACP_TEST_CONFIG);
run_test(async move {
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
let provider_factory: AcpProviderFactory = Arc::new(|provider_name, _, _, _| {
let provider_factory: AcpProviderFactory = Arc::new(|provider_name| {
Box::pin(async move {
Ok(Arc::new(MockProvider {
name: provider_name,
@ -1225,7 +1222,7 @@ fn test_custom_provider_supported_models_maps_authentication_error() {
write_acp_global_config(DEFAULT_ACP_TEST_CONFIG);
run_test(async move {
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
let provider_factory: AcpProviderFactory = Arc::new(|provider_name, _, _, _| {
let provider_factory: AcpProviderFactory = Arc::new(|provider_name| {
Box::pin(async move {
Ok(Arc::new(MockProvider {
name: provider_name,

View file

@ -382,21 +382,19 @@ pub async fn spawn_acp_server_in_process(
write_global_test_config(&config_path, openai_base_url);
let provider_factory = provider_factory.unwrap_or_else(|| {
let base_url = openai_base_url.to_string();
Arc::new(
move |_provider_name, _extensions, _working_dir, _use_default_model| {
let base_url = base_url.clone();
Box::pin(async move {
let api_client = ApiClient::new_with_tls(
base_url,
ApiAuthMethod::BearerToken("test-key".to_string()),
None,
)
.unwrap();
let provider: Arc<dyn Provider> = Arc::new(OpenAiProvider::new(api_client));
Ok(provider)
})
},
)
Arc::new(move |_provider_name| {
let base_url = base_url.clone();
Box::pin(async move {
let api_client = ApiClient::new_with_tls(
base_url,
ApiAuthMethod::BearerToken("test-key".to_string()),
None,
)
.unwrap();
let provider: Arc<dyn Provider> = Arc::new(OpenAiProvider::new(api_client));
Ok(provider)
})
})
});
let active_runs = Arc::new(goose::acp::server::ActiveRunRegistry::default());

View file

@ -45,15 +45,13 @@ impl Provider for MockProvider {
}
fn mock_provider_factory() -> goose::acp::server::AcpProviderFactory {
Arc::new(
|provider_name, _extensions, _working_dir, _use_default_model| {
Box::pin(async move {
Ok(Arc::new(MockProvider {
name: provider_name,
}) as Arc<dyn Provider>)
})
},
)
Arc::new(|provider_name| {
Box::pin(async move {
Ok(Arc::new(MockProvider {
name: provider_name,
}) as Arc<dyn Provider>)
})
})
}
fn write_config(config_dir: &std::path::Path) {

View file

@ -5,7 +5,6 @@ use goose::tool_inspection::ToolInspector;
use rmcp::model::CallToolRequestParams;
use rmcp::object;
use std::sync::Arc;
use tokio::sync::Mutex;
fn make_request(
id: &str,
@ -29,9 +28,8 @@ fn write_adversary_md(dir: &std::path::Path, content: &str) {
async fn test_adversary_disabled_without_config_file() {
let tmp = tempfile::tempdir().unwrap();
let provider = Arc::new(Mutex::new(None));
let inspector = AdversaryInspector::with_config_dir(
provider,
Default::default(),
Arc::new(goose::session::SessionManager::new(
tmp.path().to_path_buf(),
)),
@ -63,9 +61,8 @@ async fn test_adversary_enabled_default_tools() {
let tmp = tempfile::tempdir().unwrap();
write_adversary_md(tmp.path(), "BLOCK everything for testing");
let provider = Arc::new(Mutex::new(None));
let inspector = AdversaryInspector::with_config_dir(
provider,
Default::default(),
Arc::new(goose::session::SessionManager::new(
tmp.path().to_path_buf(),
)),
@ -127,9 +124,8 @@ async fn test_adversary_custom_tool_filter() {
"tools: shell, developer__shell\n---\nBLOCK bad stuff",
);
let provider = Arc::new(Mutex::new(None));
let inspector = AdversaryInspector::with_config_dir(
provider,
Default::default(),
Arc::new(goose::session::SessionManager::new(
tmp.path().to_path_buf(),
)),

View file

@ -95,7 +95,7 @@ async fn fixture(mcpui: bool, protocol_version: Option<ProtocolVersion>) -> Fixt
temp_dir.path().to_path_buf(),
));
let manager = Arc::new(ExtensionManager::new(
Arc::new(tokio::sync::Mutex::new(None)),
Default::default(),
session_manager.clone(),
None,
"extension-contract".to_string(),
@ -902,7 +902,7 @@ async fn test_replayed_session(
temp_dir.path().to_path_buf(),
));
let extension_manager = Arc::new(ExtensionManager::new(
Arc::new(tokio::sync::Mutex::new(None)),
Default::default(),
session_manager,
None,
GoosePlatform::GooseDesktop.to_string(),

View file

@ -14,7 +14,7 @@ use goose::providers::base::Provider;
use goose::providers::bedrock::BEDROCK_DEFAULT_MODEL;
use goose::providers::claude_code::CLAUDE_CODE_DEFAULT_MODEL;
use goose::providers::codex::CODEX_DEFAULT_MODEL;
use goose::providers::create_with_named_model;
use goose::providers::create;
use goose::providers::google::GOOGLE_DEFAULT_MODEL;
use goose::providers::litellm::LITELLM_DEFAULT_MODEL;
use goose::providers::openai::OPEN_AI_DEFAULT_MODEL;
@ -241,7 +241,7 @@ impl ProviderFixture {
available_tools: vec![],
};
let provider = create_with_named_model(
let provider = create(
&config.name.to_lowercase(),
vec![mcp_extension.clone(), developer_extension.clone()],
)