Files
tkmind_go/crates/goose/src/doctor.rs
T
ayourk badb97a01e Structured per-provider config block, non-destructive provider switching (#8977)
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Signed-off-by: Aaron Yourk <ayourk@gmail.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
2026-05-18 18:22:50 +00:00

258 lines
8.8 KiB
Rust

use std::sync::Arc;
use crate::agents::platform_extensions::developer;
use crate::agents::ExtensionConfig;
use crate::config::Config;
use crate::conversation::message::Message;
use crate::model::ModelConfig;
use crate::providers::base::Provider;
use crate::providers::{self, errors::ProviderError};
use crate::session::{
config_path, latest_llm_log_path, latest_server_log_path, read_capped, read_tail, SystemInfo,
};
pub async fn run(agent: &crate::agents::Agent, session_id: &str) -> anyhow::Result<Message> {
if let Some(msg) = ensure_working_provider(agent, session_id).await? {
return Ok(msg);
}
ensure_developer_extension(agent, session_id).await;
let info = SystemInfo::collect();
let extensions = agent.list_extensions().await;
let mut prompt = format!(
"I ran /doctor because something seems off. Here's my system info:\n\n\
{}\n\
Loaded extensions: {}\n\
Config file: {}\n",
info.to_text(),
if extensions.is_empty() {
"none".to_string()
} else {
extensions.join(", ")
},
config_path().display(),
);
if let Some(path) = latest_server_log_path() {
if let Some(tail) = read_tail(&path, 50) {
prompt.push_str(&format!("\nRecent server log:\n```\n{}\n```\n", tail));
}
}
if let Some(path) = latest_llm_log_path() {
if let Some(content) = read_capped(&path, 10_000) {
prompt.push_str(&format!("\nLast LLM request log:\n```\n{}\n```\n", content));
}
}
prompt.push_str(
"\nUse your tools to investigate what might be wrong. \
Check if common developer tools are available (git, etc.) \
and report what you find.",
);
Ok(Message::user().with_text(prompt))
}
async fn ensure_working_provider(
agent: &crate::agents::Agent,
session_id: &str,
) -> anyhow::Result<Option<Message>> {
let config = Config::global();
let mut log: Vec<String> = Vec::new();
let provider_name = config.get_goose_provider().ok();
let model_name = config.get_goose_model().ok();
if let (Some(ref pname), Some(ref mname)) = (&provider_name, &model_name) {
log.push(format!("Checking {} / {} ...", pname, mname));
match try_create_and_test(pname, mname).await {
Ok(_) => {
return Ok(None);
}
Err(e) => {
log.push(format!("{} / {}: {}", pname, mname, describe_error(&e)));
}
}
log.push(format!("Looking for alternative models on {} ...", pname));
if let Some(working) = try_other_models(pname, mname, &mut log).await {
let new_model = working.get_model_config().model_name.clone();
save_and_set(agent, session_id, working).await?;
let preamble = log.join("\n");
return Ok(Some(Message::assistant().with_text(format!(
"**Goose Doctor**\n\n{}\n\n\
Your configured model wasn't working, so I switched to \
**{} / {}**. You can continue chatting now.",
preamble, pname, new_model,
))));
}
} else {
log.push("No provider/model configured.".to_string());
}
log.push("Looking for other configured providers ...".to_string());
let skip = provider_name.as_deref().unwrap_or("");
if let Some(working) = try_other_providers(skip, &mut log).await {
let name = working.get_name().to_string();
let model = working.get_model_config().model_name.clone();
save_and_set(agent, session_id, working).await?;
let preamble = log.join("\n");
return Ok(Some(Message::assistant().with_text(format!(
"**Goose Doctor**\n\n{}\n\n\
Switched to **{} / {}**. You can continue chatting now.",
preamble, name, model,
))));
}
let preamble = log.join("\n");
Ok(Some(Message::assistant().with_text(format!(
"**Goose Doctor**\n\n{}\n\n\
No working provider found. Run `goose configure` to set one up.",
preamble,
))))
}
async fn ensure_developer_extension(agent: &crate::agents::Agent, session_id: &str) {
if agent
.extension_manager
.is_extension_enabled(developer::EXTENSION_NAME)
.await
{
return;
}
let config = ExtensionConfig::Platform {
name: developer::EXTENSION_NAME.to_string(),
description: "Write and edit files, and execute shell commands".to_string(),
display_name: Some("Developer".to_string()),
bundled: None,
available_tools: vec![],
};
if let Err(e) = agent.add_extension(config, session_id).await {
tracing::warn!("Doctor: failed to load developer extension: {}", e);
}
}
async fn save_and_set(
agent: &crate::agents::Agent,
session_id: &str,
provider: Arc<dyn Provider>,
) -> anyhow::Result<()> {
let config = Config::global();
crate::config::set_active_provider(
config,
provider.get_name(),
&provider.get_model_config().model_name,
)?;
agent.update_provider(provider, session_id).await
}
async fn test_provider(provider: &dyn Provider) -> Result<(), ProviderError> {
let messages = vec![Message::user().with_text("Say 'hello' and nothing else.")];
provider
.complete(
&provider.get_model_config(),
"doctor-check",
"Respond as briefly as possible.",
&messages,
&[],
)
.await?;
Ok(())
}
async fn try_create_and_test(
provider_name: &str,
model_name: &str,
) -> Result<Arc<dyn Provider>, ProviderError> {
let model_config = ModelConfig::new(model_name)
.map_err(|e| ProviderError::ExecutionError(e.to_string()))?
.with_canonical_limits(provider_name);
let provider = providers::create(provider_name, model_config, vec![])
.await
.map_err(|e| ProviderError::ExecutionError(e.to_string()))?;
test_provider(provider.as_ref()).await?;
Ok(provider)
}
async fn try_other_models(
provider_name: &str,
skip_model: &str,
log: &mut Vec<String>,
) -> Option<Arc<dyn Provider>> {
let entry = providers::get_from_registry(provider_name).await.ok()?;
let temp = entry.create_with_default_model(vec![]).await.ok()?;
let models = temp.fetch_recommended_models().await.ok()?;
for model in models.iter().filter(|m| m.as_str() != skip_model).take(3) {
log.push(format!(" Trying {} / {} ...", provider_name, model));
match try_create_and_test(provider_name, model).await {
Ok(p) => {
log.push(format!("{} / {} works", provider_name, model));
return Some(p);
}
Err(e) => log.push(format!("{}", describe_error(&e))),
}
}
None
}
async fn try_other_providers(skip: &str, log: &mut Vec<String>) -> Option<Arc<dyn Provider>> {
for (meta, _) in providers::providers().await {
if meta.name == skip {
continue;
}
let entry = match providers::get_from_registry(&meta.name).await {
Ok(e) => e,
Err(_) => continue,
};
let provider = match entry.create_with_default_model(vec![]).await {
Ok(p) => p,
Err(_) => continue,
};
let model_name = provider.get_model_config().model_name.clone();
log.push(format!(" Trying {} / {} ...", meta.name, model_name));
match test_provider(provider.as_ref()).await {
Ok(()) => {
log.push(format!("{} / {} works", meta.name, model_name));
return Some(provider);
}
Err(e) => log.push(format!("{}", describe_error(&e))),
}
}
None
}
fn describe_error(e: &ProviderError) -> String {
match e {
ProviderError::Authentication(_) => {
"Authentication failed — check your API key. Run `goose configure` to update it."
.to_string()
}
ProviderError::CreditsExhausted { top_up_url, .. } => {
let mut msg = "Credits exhausted.".to_string();
if let Some(url) = top_up_url {
msg.push_str(&format!(" Top up at: {}", url));
}
msg
}
ProviderError::RateLimitExceeded { .. } => {
"Rate limited — wait a moment and try again.".to_string()
}
ProviderError::EndpointNotFound(_) => {
"Model not found — the model name may be wrong for this provider.".to_string()
}
ProviderError::NetworkError(_) => {
"Network error — check your internet connection.".to_string()
}
ProviderError::ServerError(_) => {
"Provider server error — the service may be temporarily down.".to_string()
}
other => format!("{}", other),
}
}