@@ -711,6 +711,9 @@ enum Command {
|
|||||||
verbose: bool,
|
verbose: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[command(about = "Check that your Goose setup is working")]
|
||||||
|
Doctor {},
|
||||||
|
|
||||||
/// Manage system prompts and behaviors
|
/// Manage system prompts and behaviors
|
||||||
#[command(about = "Run one of the mcp servers bundled with goose")]
|
#[command(about = "Run one of the mcp servers bundled with goose")]
|
||||||
Mcp {
|
Mcp {
|
||||||
@@ -1026,6 +1029,7 @@ pub struct InputConfig {
|
|||||||
fn get_command_name(command: &Option<Command>) -> &'static str {
|
fn get_command_name(command: &Option<Command>) -> &'static str {
|
||||||
match command {
|
match command {
|
||||||
Some(Command::Configure {}) => "configure",
|
Some(Command::Configure {}) => "configure",
|
||||||
|
Some(Command::Doctor {}) => "doctor",
|
||||||
Some(Command::Info { .. }) => "info",
|
Some(Command::Info { .. }) => "info",
|
||||||
Some(Command::Mcp { .. }) => "mcp",
|
Some(Command::Mcp { .. }) => "mcp",
|
||||||
Some(Command::Acp { .. }) => "acp",
|
Some(Command::Acp { .. }) => "acp",
|
||||||
@@ -1755,6 +1759,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some(Command::Configure {}) => handle_configure().await,
|
Some(Command::Configure {}) => handle_configure().await,
|
||||||
|
Some(Command::Doctor {}) => crate::commands::doctor::handle_doctor().await,
|
||||||
Some(Command::Info { verbose }) => handle_info(verbose),
|
Some(Command::Info { verbose }) => handle_info(verbose),
|
||||||
Some(Command::Mcp { server }) => handle_mcp_command(server).await,
|
Some(Command::Mcp { server }) => handle_mcp_command(server).await,
|
||||||
Some(Command::Acp { builtins }) => goose_acp::server::run(builtins).await,
|
Some(Command::Acp { builtins }) => goose_acp::server::run(builtins).await,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::session::build_session;
|
||||||
|
use crate::session::SessionBuilderConfig;
|
||||||
|
|
||||||
|
pub async fn handle_doctor() -> Result<()> {
|
||||||
|
let mut session = build_session(SessionBuilderConfig {
|
||||||
|
no_session: true,
|
||||||
|
interactive: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session.interactive(Some("/doctor".to_string())).await
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod configure;
|
pub mod configure;
|
||||||
|
pub mod doctor;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ static COMMANDS: &[CommandDef] = &[
|
|||||||
name: "skills",
|
name: "skills",
|
||||||
description: "List installed skills and other available sources",
|
description: "List installed skills and other available sources",
|
||||||
},
|
},
|
||||||
|
CommandDef {
|
||||||
|
name: "doctor",
|
||||||
|
description: "Check that your Goose setup is working",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn list_commands() -> &'static [CommandDef] {
|
pub fn list_commands() -> &'static [CommandDef] {
|
||||||
@@ -77,6 +81,7 @@ impl Agent {
|
|||||||
"compact" => self.handle_compact_command(session_id).await,
|
"compact" => self.handle_compact_command(session_id).await,
|
||||||
"clear" => self.handle_clear_command(session_id).await,
|
"clear" => self.handle_clear_command(session_id).await,
|
||||||
"skills" => self.handle_skills_command(session_id).await,
|
"skills" => self.handle_skills_command(session_id).await,
|
||||||
|
"doctor" => Ok(Some(crate::doctor::run(self, session_id).await?)),
|
||||||
_ => {
|
_ => {
|
||||||
self.handle_recipe_command(command, params_str, session_id)
|
self.handle_recipe_command(command, params_str, session_id)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
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();
|
||||||
|
config.set_goose_provider(provider.get_name()).ok();
|
||||||
|
config
|
||||||
|
.set_goose_model(&provider.get_model_config().model_name)
|
||||||
|
.ok();
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ pub mod config;
|
|||||||
pub mod context_mgmt;
|
pub mod context_mgmt;
|
||||||
pub mod conversation;
|
pub mod conversation;
|
||||||
pub mod dictation;
|
pub mod dictation;
|
||||||
|
pub mod doctor;
|
||||||
pub mod download_manager;
|
pub mod download_manager;
|
||||||
pub mod execution;
|
pub mod execution;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
use zip::write::SimpleFileOptions;
|
use zip::write::SimpleFileOptions;
|
||||||
use zip::ZipWriter;
|
use zip::ZipWriter;
|
||||||
@@ -70,6 +71,69 @@ pub fn get_system_info() -> SystemInfo {
|
|||||||
SystemInfo::collect()
|
SystemInfo::collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn config_path() -> PathBuf {
|
||||||
|
Paths::config_dir().join("config.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_server_log_path() -> Option<PathBuf> {
|
||||||
|
let server_dir = Paths::in_state_dir("logs").join("server");
|
||||||
|
let latest_date_dir = latest_entry_by_name(&server_dir)?;
|
||||||
|
latest_entry_by_name(&latest_date_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_llm_log_path() -> Option<PathBuf> {
|
||||||
|
let path = Paths::in_state_dir("logs").join("llm_request.0.jsonl");
|
||||||
|
path.exists().then_some(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_tail(path: &std::path::Path, max_lines: usize) -> Option<String> {
|
||||||
|
let content = fs::read_to_string(path).ok()?;
|
||||||
|
let lines: Vec<&str> = content.lines().collect();
|
||||||
|
let start = lines.len().saturating_sub(max_lines);
|
||||||
|
Some(lines[start..].join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_capped(path: &std::path::Path, max_bytes: usize) -> Option<String> {
|
||||||
|
let content = fs::read_to_string(path).ok()?;
|
||||||
|
if content.len() <= max_bytes {
|
||||||
|
return Some(content);
|
||||||
|
}
|
||||||
|
let half = max_bytes / 2;
|
||||||
|
let head: String = content
|
||||||
|
.chars()
|
||||||
|
.take_while({
|
||||||
|
let mut n = 0;
|
||||||
|
move |c| {
|
||||||
|
n += c.len_utf8();
|
||||||
|
n <= half
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let tail: String = {
|
||||||
|
let skip = content.len().saturating_sub(half);
|
||||||
|
let mut chars = content.chars();
|
||||||
|
let mut skipped = 0;
|
||||||
|
for c in chars.by_ref() {
|
||||||
|
skipped += c.len_utf8();
|
||||||
|
if skipped >= skip {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chars.collect()
|
||||||
|
};
|
||||||
|
let omitted = content.len() - head.len() - tail.len();
|
||||||
|
Some(format!(
|
||||||
|
"{}\n\n... ({} bytes omitted) ...\n\n{}",
|
||||||
|
head, omitted, tail,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latest_entry_by_name(dir: &std::path::Path) -> Option<PathBuf> {
|
||||||
|
let mut entries: Vec<_> = fs::read_dir(dir).ok()?.filter_map(|e| e.ok()).collect();
|
||||||
|
entries.sort_by_key(|e| e.file_name());
|
||||||
|
entries.last().map(|e| e.path())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn generate_diagnostics(
|
pub async fn generate_diagnostics(
|
||||||
session_manager: &SessionManager,
|
session_manager: &SessionManager,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
@@ -101,6 +165,14 @@ pub async fn generate_diagnostics(
|
|||||||
zip.write_all(&fs::read(&path)?)?;
|
zip.write_all(&fs::read(&path)?)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(server_log) = latest_server_log_path() {
|
||||||
|
if let Ok(content) = fs::read(&server_log) {
|
||||||
|
let name = server_log.file_name().unwrap().to_str().unwrap();
|
||||||
|
zip.start_file(format!("logs/server/{}", name), options)?;
|
||||||
|
zip.write_all(&content)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let session_data = session_manager.export_session(session_id).await?;
|
let session_data = session_manager.export_session(session_id).await?;
|
||||||
zip.start_file("session.json", options)?;
|
zip.start_file("session.json", options)?;
|
||||||
zip.write_all(session_data.as_bytes())?;
|
zip.write_all(session_data.as_bytes())?;
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ pub mod extension_data;
|
|||||||
mod legacy;
|
mod legacy;
|
||||||
pub mod session_manager;
|
pub mod session_manager;
|
||||||
|
|
||||||
pub use diagnostics::{generate_diagnostics, get_system_info, SystemInfo};
|
pub use diagnostics::{
|
||||||
|
config_path, generate_diagnostics, get_system_info, latest_llm_log_path,
|
||||||
|
latest_server_log_path, read_capped, read_tail, SystemInfo,
|
||||||
|
};
|
||||||
pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState};
|
pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState};
|
||||||
pub use session_manager::{
|
pub use session_manager::{
|
||||||
Session, SessionInsights, SessionManager, SessionType, SessionUpdateBuilder,
|
Session, SessionInsights, SessionManager, SessionType, SessionUpdateBuilder,
|
||||||
|
|||||||
Reference in New Issue
Block a user