Migrate diagnostics to JSON report (#9964)

Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
Douwe Osinga
2026-06-23 21:32:56 -04:00
committed by GitHub
parent 6782d1f506
commit e4f8b61b58
29 changed files with 1029 additions and 177 deletions
Generated
-1
View File
@@ -4886,7 +4886,6 @@ dependencies = [
"which 8.0.3",
"winapi",
"wiremock",
"zip 8.6.0",
]
[[package]]
-2
View File
@@ -580,11 +580,9 @@ enum SessionCommand {
},
#[command(name = "diagnostics")]
Diagnostics {
/// Session identifier for generating diagnostics
#[command(flatten)]
identifier: Option<Identifier>,
/// Output path for the diagnostics zip file (optional, defaults to current directory)
#[arg(short = 'o', long)]
output: Option<PathBuf>,
},
+17 -12
View File
@@ -7,7 +7,9 @@ use etcetera::home_dir;
use goose::config::Config;
#[cfg(feature = "nostr")]
use goose::session::nostr_share;
use goose::session::{generate_diagnostics, Session, SessionManager, SessionType};
use goose::session::{
generate_diagnostics, DiagnosticsLevel, Session, SessionManager, SessionType,
};
use goose::utils::safe_truncate;
use regex::Regex;
use std::fs;
@@ -322,24 +324,27 @@ pub async fn handle_session_import(input: String, nostr: bool) -> Result<()> {
pub async fn handle_diagnostics(session_id: &str, output_path: Option<PathBuf>) -> Result<()> {
println!(
"Generating diagnostics bundle for session '{}'...",
"Generating diagnostics report for session '{}'...",
session_id
);
let session_manager = SessionManager::instance();
let diagnostics_data = generate_diagnostics(&session_manager, session_id)
.await
.with_context(|| {
format!(
"Failed to write to generate diagnostics bundle for session '{}'",
session_id
)
})?;
let diagnostics_report =
generate_diagnostics(&session_manager, session_id, DiagnosticsLevel::Full)
.await
.with_context(|| {
format!(
"Failed to generate diagnostics report for session '{}'",
session_id
)
})?;
let diagnostics_data = serde_json::to_vec_pretty(&diagnostics_report)
.context("Failed to serialize diagnostics report")?;
let output_file = if let Some(path) = output_path {
path.clone()
} else {
PathBuf::from(format!("diagnostics_{}.zip", session_id))
PathBuf::from(format!("diagnostics_{}.json", session_id))
};
let mut file = fs::File::create(&output_file).context(format!(
@@ -350,7 +355,7 @@ pub async fn handle_diagnostics(session_id: &str, output_path: Option<PathBuf>)
file.write_all(&diagnostics_data)
.context("Failed to write diagnostics data")?;
println!("Diagnostics bundle saved to: {}", output_file.display());
println!("Diagnostics report saved to: {}", output_file.display());
Ok(())
}
+9 -3
View File
@@ -280,9 +280,15 @@ pub async fn handle_term_run(prompt: Vec<String>) -> Result<()> {
};
if let Some(oldest_user) = user_messages_after_last_assistant.last() {
session_manager
.truncate_conversation(&session_id, oldest_user.created)
.await?;
if let Some(message_id) = oldest_user.id.as_deref() {
session_manager
.truncate_conversation_from_message(&session_id, message_id)
.await?;
} else {
session_manager
.truncate_conversation(&session_id, oldest_user.created)
.await?;
}
}
let prompt_with_context = if user_messages_after_last_assistant.is_empty() {
@@ -165,6 +165,31 @@ pub struct SteerSessionResponse {
pub message_id: String,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/diagnostics/get",
response = DiagnosticsGetResponse
)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsGetRequest {
pub session_id: String,
#[serde(default)]
pub level: DiagnosticsReportLevel,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticsReportLevel {
#[default]
Summary,
Full,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
pub struct DiagnosticsGetResponse {
pub report: serde_json::Value,
}
/// Delete a session.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "session/delete", response = EmptyResponse)]
+14 -1
View File
@@ -7,7 +7,11 @@ use goose::conversation::token_usage::Usage;
use goose::conversation::Conversation;
use goose::download_manager::{DownloadProgress, DownloadStatus};
use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderType};
use goose::session::{Session, SessionType, SystemInfo};
use goose::session::{
DiagnosticsConfig, DiagnosticsError, DiagnosticsExtensions, DiagnosticsLevel, DiagnosticsLogs,
DiagnosticsPrompt, DiagnosticsReport, DiagnosticsScheduledRecipe, DiagnosticsTextFile, Session,
SessionType, SystemInfo,
};
use goose_providers::model::ModelConfig;
use goose_providers::permission::Permission;
use goose_providers::permission::PrincipalType;
@@ -583,6 +587,15 @@ derive_utoipa!(IconTheme as IconThemeSchema);
goose_providers::goose_mode::GooseMode,
SessionType,
SystemInfo,
DiagnosticsConfig,
DiagnosticsError,
DiagnosticsExtensions,
DiagnosticsLevel,
DiagnosticsLogs,
DiagnosticsPrompt,
DiagnosticsReport,
DiagnosticsScheduledRecipe,
DiagnosticsTextFile,
Conversation,
IconSchema,
IconThemeSchema,
+25 -26
View File
@@ -1,9 +1,9 @@
use axum::body::Body;
use axum::extract::State;
use axum::http::HeaderValue;
use axum::response::IntoResponse;
use axum::{extract::Path, http::StatusCode, routing::get, Json, Router};
use goose::session::{generate_diagnostics, get_system_info, SystemInfo};
use axum::extract::{Path, Query, State};
use axum::{http::StatusCode, routing::get, Json, Router};
use goose::session::{
generate_diagnostics, get_system_info, DiagnosticsLevel, DiagnosticsReport, SystemInfo,
};
use serde::Deserialize;
use std::sync::Arc;
use crate::state::AppState;
@@ -26,34 +26,33 @@ async fn system_info() -> Json<SystemInfo> {
Json(get_system_info())
}
#[derive(Debug, Default, Deserialize, utoipa::IntoParams)]
struct DiagnosticsQuery {
level: Option<DiagnosticsLevel>,
}
#[utoipa::path(get, path = "/diagnostics/{session_id}",
params(
DiagnosticsQuery,
),
responses(
(status = 200, description = "Diagnostics zip file", content_type = "application/zip", body = Vec<u8>),
(status = 200, description = "Diagnostics report", body = DiagnosticsReport),
(status = 500, description = "Failed to generate diagnostics"),
)
)]
async fn diagnostics(
State(state): State<Arc<AppState>>,
Path(session_id): Path<String>,
) -> impl IntoResponse {
match generate_diagnostics(state.session_manager(), &session_id).await {
Ok(zip_data) => {
let filename = format!("attachment; filename=\"diagnostics_{}.zip\"", session_id);
let headers = [
(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/zip"),
),
(
http::header::CONTENT_DISPOSITION,
HeaderValue::from_str(&filename).map_err(|_e| StatusCode::BAD_REQUEST)?,
),
];
Ok((headers, Body::from(zip_data)))
}
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
Query(query): Query<DiagnosticsQuery>,
) -> Result<Json<DiagnosticsReport>, StatusCode> {
generate_diagnostics(
state.session_manager(),
&session_id,
query.level.unwrap_or(DiagnosticsLevel::Full),
)
.await
.map(Json)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
-1
View File
@@ -168,7 +168,6 @@ byteorder = { version = "1.5", default-features = false, features = ["std"], opt
tokenizers = { version = "0.23", default-features = false, features = ["onig"], optional = true }
symphonia = { version = "0.5", default-features = false, features = ["aac", "adpcm", "alac", "isomp4", "mkv", "mp3", "pcm", "vorbis", "wav"], optional = true }
rubato = { version = "0.16", default-features = false, optional = true }
zip = { workspace = true }
sys-info = { version = "0.9", default-features = false }
llama-cpp-2 = { workspace = true, optional = true }
+5
View File
@@ -40,6 +40,11 @@
"requestType": "SteerSessionRequest_unstable",
"responseType": "SteerSessionResponse_unstable"
},
{
"method": "_goose/unstable/diagnostics/get",
"requestType": "DiagnosticsGetRequest_unstable",
"responseType": "DiagnosticsGetResponse_unstable"
},
{
"method": "session/delete",
"requestType": "DeleteSessionRequest",
+52
View File
@@ -973,6 +973,41 @@
"x-side": "agent",
"x-method": "_goose/unstable/session/steer"
},
"DiagnosticsGetRequest_unstable": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
},
"level": {
"$ref": "#/$defs/DiagnosticsReportLevel",
"default": "summary"
}
},
"required": [
"sessionId"
],
"x-side": "agent",
"x-method": "_goose/unstable/diagnostics/get"
},
"DiagnosticsReportLevel": {
"type": "string",
"enum": [
"summary",
"full"
]
},
"DiagnosticsGetResponse_unstable": {
"type": "object",
"properties": {
"report": {}
},
"required": [
"report"
],
"x-side": "agent",
"x-method": "_goose/unstable/diagnostics/get"
},
"DeleteSessionRequest": {
"type": "object",
"properties": {
@@ -4613,6 +4648,15 @@
"description": "Params for _goose/unstable/session/steer",
"title": "SteerSessionRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/DiagnosticsGetRequest_unstable"
}
],
"description": "Params for _goose/unstable/diagnostics/get",
"title": "DiagnosticsGetRequest_unstable"
},
{
"allOf": [
{
@@ -5250,6 +5294,14 @@
],
"title": "SteerSessionResponse_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/DiagnosticsGetResponse_unstable"
}
],
"title": "DiagnosticsGetResponse_unstable"
},
{
"allOf": [
{
+1
View File
@@ -84,6 +84,7 @@ mod agent_requests;
pub use agent_requests::agent_request_schemas;
mod config;
mod custom_dispatch;
mod diagnostics;
mod dictation;
mod dispatch;
mod elicitation;
@@ -82,6 +82,14 @@ impl GooseAcpAgent {
self.on_steer_session(req).await
}
#[custom_method(DiagnosticsGetRequest)]
async fn dispatch_get_diagnostics(
&self,
req: DiagnosticsGetRequest,
) -> Result<DiagnosticsGetResponse, agent_client_protocol::Error> {
self.on_get_diagnostics(req).await
}
#[custom_method(DeleteSessionRequest)]
async fn dispatch_delete_session(
&self,
@@ -0,0 +1,20 @@
use super::*;
use crate::session::{generate_diagnostics, DiagnosticsLevel};
impl GooseAcpAgent {
pub(super) async fn on_get_diagnostics(
&self,
req: DiagnosticsGetRequest,
) -> Result<DiagnosticsGetResponse, agent_client_protocol::Error> {
let level = match req.level {
DiagnosticsReportLevel::Summary => DiagnosticsLevel::Summary,
DiagnosticsReportLevel::Full => DiagnosticsLevel::Full,
};
let report = generate_diagnostics(&self.session_manager, &req.session_id, level)
.await
.internal_err()?;
let report = serde_json::to_value(report).internal_err()?;
Ok(DiagnosticsGetResponse { report })
}
}
+254 -59
View File
@@ -6,14 +6,22 @@ use crate::providers::utils::LOGS_TO_KEEP;
use crate::session::SessionManager;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Cursor;
use std::io::Write;
use std::path::PathBuf;
use utoipa::ToSchema;
use zip::write::SimpleFileOptions;
use zip::ZipWriter;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
const SERVER_LOG_TAIL_LINES: usize = 400;
const LLM_LOG_MAX_BYTES: usize = 2 * 1024 * 1024;
const CONFIG_MAX_BYTES: usize = 256 * 1024;
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticsLevel {
#[default]
Summary,
Full,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
pub struct SystemInfo {
pub app_version: String,
pub os: String,
@@ -24,6 +32,73 @@ pub struct SystemInfo {
pub enabled_extensions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsConfig {
pub config_path: String,
pub config_yaml: Option<String>,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsExtensions {
pub enabled: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsTextFile {
pub path: String,
pub content: String,
pub truncated: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsLogs {
pub server: Option<DiagnosticsTextFile>,
pub llm: Vec<DiagnosticsTextFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsPrompt {
pub name: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsScheduledRecipe {
pub path: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsError {
pub path: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticsReport {
pub schema_version: u32,
pub generated_at: String,
pub level: DiagnosticsLevel,
pub system: SystemInfo,
pub config: Option<DiagnosticsConfig>,
pub extensions: DiagnosticsExtensions,
pub session: Option<serde_json::Value>,
pub logs: DiagnosticsLogs,
pub prompts: Vec<DiagnosticsPrompt>,
pub schedule: Option<serde_json::Value>,
pub scheduled_recipes: Vec<DiagnosticsScheduledRecipe>,
pub errors: Vec<DiagnosticsError>,
}
impl SystemInfo {
pub fn collect() -> Self {
let config = Config::global();
@@ -86,6 +161,68 @@ pub fn latest_llm_log_path() -> Option<PathBuf> {
path.exists().then_some(path)
}
fn recent_llm_log_paths() -> Vec<PathBuf> {
let logs_dir = Paths::in_state_dir("logs");
let paths: Vec<_> = fs::read_dir(logs_dir)
.ok()
.into_iter()
.flatten()
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("llm_request.") && name.ends_with(".jsonl"))
})
.collect();
let (mut numbered, mut temp): (Vec<_>, Vec<_>) = paths
.into_iter()
.partition(|path| llm_log_index(path).is_some());
numbered.sort_by_key(|path| llm_log_index(path).unwrap_or(usize::MAX));
temp.sort_by(|left, right| {
llm_log_modified(right)
.cmp(&llm_log_modified(left))
.then_with(|| llm_log_name(left).cmp(&llm_log_name(right)))
});
if temp.is_empty() || numbered.len() < LOGS_TO_KEEP {
numbered.extend(temp);
numbered.truncate(LOGS_TO_KEEP);
numbered
} else {
let temp_slots = 1;
let numbered_slots = LOGS_TO_KEEP.saturating_sub(temp_slots);
temp.truncate(temp_slots);
numbered.truncate(numbered_slots);
temp.extend(numbered);
temp
}
}
fn llm_log_index(path: &std::path::Path) -> Option<usize> {
let name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default();
name.strip_prefix("llm_request.")
.and_then(|name| name.strip_suffix(".jsonl"))
.and_then(|name| name.parse::<usize>().ok())
}
fn llm_log_modified(path: &std::path::Path) -> std::time::SystemTime {
path.metadata()
.and_then(|metadata| metadata.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
}
fn llm_log_name(path: &std::path::Path) -> String {
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
.to_string()
}
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();
@@ -128,6 +265,10 @@ pub fn read_capped(path: &std::path::Path, max_bytes: usize) -> Option<String> {
))
}
fn was_truncated(content: &str) -> bool {
content.contains("... (") && content.contains(" bytes omitted) ...")
}
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());
@@ -137,81 +278,135 @@ fn latest_entry_by_name(dir: &std::path::Path) -> Option<PathBuf> {
pub async fn generate_diagnostics(
session_manager: &SessionManager,
session_id: &str,
) -> anyhow::Result<Vec<u8>> {
let logs_dir = Paths::in_state_dir("logs");
let config_dir = Paths::config_dir();
let config_path = config_dir.join("config.yaml");
level: DiagnosticsLevel,
) -> anyhow::Result<DiagnosticsReport> {
let config_path = config_path();
let data_dir = Paths::data_dir();
let system_info = SystemInfo::collect();
let is_full = matches!(level, DiagnosticsLevel::Full);
let mut errors = Vec::new();
let mut buffer = Vec::new();
{
let mut zip = ZipWriter::new(Cursor::new(&mut buffer));
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let mut log_files: Vec<_> = fs::read_dir(&logs_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "jsonl"))
.collect();
log_files.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
for entry in log_files.iter().rev().take(LOGS_TO_KEEP) {
let path = entry.path();
let name = path.file_name().unwrap().to_str().unwrap();
zip.start_file(format!("logs/{}", name), options)?;
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 = if is_full {
let session_data = session_manager.export_session(session_id).await?;
zip.start_file("session.json", options)?;
zip.write_all(session_data.as_bytes())?;
Some(serde_json::from_str(&session_data)?)
} else {
None
};
if config_path.exists() {
zip.start_file("config.yaml", options)?;
zip.write_all(&fs::read(&config_path)?)?;
let config = if is_full {
let config_yaml = if config_path.exists() {
read_capped(&config_path, CONFIG_MAX_BYTES)
} else {
None
};
let truncated = config_yaml.as_deref().is_some_and(was_truncated);
Some(DiagnosticsConfig {
config_path: config_path.display().to_string(),
config_yaml,
truncated,
})
} else {
None
};
let logs = if is_full {
DiagnosticsLogs {
server: latest_server_log_path().and_then(|path| {
read_tail(&path, SERVER_LOG_TAIL_LINES).map(|content| DiagnosticsTextFile {
path: path.display().to_string(),
content,
truncated: true,
})
}),
llm: recent_llm_log_paths()
.into_iter()
.filter_map(|path| {
read_capped(&path, LLM_LOG_MAX_BYTES).map(|content| {
let truncated = was_truncated(&content);
DiagnosticsTextFile {
path: path.display().to_string(),
content,
truncated,
}
})
})
.collect(),
}
} else {
DiagnosticsLogs::default()
};
zip.start_file("system.txt", options)?;
zip.write_all(system_info.to_text().as_bytes())?;
let prompts = if is_full {
list_templates()
.into_iter()
.map(|template| DiagnosticsPrompt {
name: template.name,
content: template.user_content.unwrap_or(template.default_content),
})
.collect()
} else {
Vec::new()
};
let schedule = if is_full {
let schedule_json = data_dir.join("schedule.json");
if schedule_json.exists() {
zip.start_file("schedule.json", options)?;
zip.write_all(&fs::read(&schedule_json)?)?;
fs::read_to_string(&schedule_json).ok().and_then(|content| {
match serde_json::from_str(&content) {
Ok(value) => Some(value),
Err(err) => {
errors.push(DiagnosticsError {
path: Some(schedule_json.display().to_string()),
message: err.to_string(),
});
None
}
}
})
} else {
None
}
} else {
None
};
let mut scheduled_recipes = Vec::new();
if is_full {
let scheduled_recipes_dir = data_dir.join("scheduled_recipes");
if scheduled_recipes_dir.exists() && scheduled_recipes_dir.is_dir() {
for entry in fs::read_dir(&scheduled_recipes_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let name = path.file_name().unwrap().to_str().unwrap();
zip.start_file(format!("scheduled_recipes/{}", name), options)?;
zip.write_all(&fs::read(&path)?)?;
match fs::read_to_string(&path) {
Ok(content) => scheduled_recipes.push(DiagnosticsScheduledRecipe {
path: path.display().to_string(),
content,
}),
Err(err) => errors.push(DiagnosticsError {
path: Some(path.display().to_string()),
message: err.to_string(),
}),
}
}
}
}
for template in list_templates() {
let content = template.user_content.unwrap_or(template.default_content);
zip.start_file(format!("prompts/{}.txt", template.name), options)?;
zip.write_all(content.as_bytes())?;
}
zip.finish()?;
}
Ok(buffer)
Ok(DiagnosticsReport {
schema_version: 1,
generated_at: chrono::Utc::now().to_rfc3339(),
level,
system: system_info.clone(),
config,
extensions: DiagnosticsExtensions {
enabled: system_info.enabled_extensions,
},
session,
logs,
prompts,
schedule,
scheduled_recipes,
errors,
})
}
+3 -1
View File
@@ -11,7 +11,9 @@ mod session_naming;
pub use diagnostics::{
config_path, generate_diagnostics, get_system_info, latest_llm_log_path,
latest_server_log_path, read_capped, read_tail, SystemInfo,
latest_server_log_path, read_capped, read_tail, DiagnosticsConfig, DiagnosticsError,
DiagnosticsExtensions, DiagnosticsLevel, DiagnosticsLogs, DiagnosticsPrompt, DiagnosticsReport,
DiagnosticsScheduledRecipe, DiagnosticsTextFile, SystemInfo,
};
pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState};
pub use session_manager::{
+120
View File
@@ -451,6 +451,16 @@ impl SessionManager {
.await
}
pub async fn truncate_conversation_from_message(
&self,
session_id: &str,
message_id: &str,
) -> Result<()> {
self.storage
.truncate_conversation_from_message(session_id, message_id)
.await
}
async fn system_generated_name_update(
&self,
id: &str,
@@ -1948,6 +1958,38 @@ impl SessionStorage {
Ok(())
}
async fn truncate_conversation_from_message(
&self,
session_id: &str,
message_id: &str,
) -> Result<()> {
let pool = self.pool().await?;
let mut tx = pool.begin_with("BEGIN IMMEDIATE").await?;
let boundary = sqlx::query_as::<_, (i64, i64)>(
"SELECT id, created_timestamp FROM messages WHERE session_id = ? AND message_id = ? ORDER BY created_timestamp, id LIMIT 1",
)
.bind(session_id)
.bind(message_id)
.fetch_optional(&mut *tx)
.await?;
if let Some((boundary_id, boundary_timestamp)) = boundary {
sqlx::query(
"DELETE FROM messages WHERE session_id = ? AND (created_timestamp > ? OR (created_timestamp = ? AND id >= ?))",
)
.bind(session_id)
.bind(boundary_timestamp)
.bind(boundary_timestamp)
.bind(boundary_id)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(())
}
async fn search_chat_history(
&self,
query: &str,
@@ -2227,12 +2269,90 @@ mod tests {
.unwrap();
}
async fn set_message_timestamp(
sm: &SessionManager,
session_id: &str,
message_id: &str,
timestamp: &str,
) {
let pool = sm.storage().pool().await.unwrap();
let timestamp = chrono::DateTime::parse_from_rfc3339(timestamp).unwrap();
let timestamp_string = timestamp.format("%Y-%m-%d %H:%M:%S").to_string();
sqlx::query(
"UPDATE messages SET timestamp = ?, created_timestamp = ? WHERE session_id = ? AND message_id = ?",
)
.bind(&timestamp_string)
.bind(timestamp.timestamp())
.bind(session_id)
.bind(message_id)
.execute(pool)
.await
.unwrap();
}
async fn add_user_message(sm: &SessionManager, session_id: &str) {
sm.add_message(session_id, &Message::user().with_text("hello world"))
.await
.unwrap();
}
#[tokio::test]
async fn test_truncate_conversation_from_message_keeps_same_second_previous_rows() {
let temp_dir = TempDir::new().unwrap();
let sm = SessionManager::new(temp_dir.path().to_path_buf());
let session = sm
.create_session(
temp_dir.path().to_path_buf(),
"Same second truncation".to_string(),
SessionType::User,
GooseMode::default(),
)
.await
.unwrap();
let timestamp = "2026-06-23T12:00:00Z";
sm.add_message(
&session.id,
&Message::assistant()
.with_text("assistant reply")
.with_id("assistant"),
)
.await
.unwrap();
set_message_timestamp(&sm, &session.id, "assistant", timestamp).await;
sm.add_message(
&session.id,
&Message::user()
.with_text("terminal history")
.with_id("terminal-history"),
)
.await
.unwrap();
set_message_timestamp(&sm, &session.id, "terminal-history", timestamp).await;
sm.add_message(
&session.id,
&Message::user()
.with_text("next prompt")
.with_id("next-prompt"),
)
.await
.unwrap();
set_message_timestamp(&sm, &session.id, "next-prompt", timestamp).await;
sm.truncate_conversation_from_message(&session.id, "terminal-history")
.await
.unwrap();
let reloaded = sm.get_session(&session.id, true).await.unwrap();
let messages = reloaded.conversation.unwrap().messages().to_vec();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].id.as_deref(), Some("assistant"));
assert_eq!(messages[0].as_concat_text(), "assistant reply");
}
#[tokio::test]
async fn test_maybe_update_name_updates_eligible_session() {
let temp_dir = TempDir::new().unwrap();
@@ -351,13 +351,13 @@ goose session export --path ./my-session.jsonl -o exported.md
---
#### session diagnostics [options]
Generate a comprehensive diagnostics bundle for troubleshooting issues with a specific session.
Generate a comprehensive diagnostics JSON report for troubleshooting issues with a specific session.
**Options:**
- **`--session-id <session_id>`**: Generate diagnostics for a specific session by ID
- **`-n, --name <name>`**: Generate diagnostics for a specific session by name
- **`--path <path>`**: Generate diagnostics for a specific session by file path (legacy)
- **`-o, --output <file>`**: Save diagnostics bundle to a specific file path (default: `diagnostics_{session_id}.zip`)
- **`-o, --output <file>`**: Save diagnostics report to a specific file path (default: `diagnostics_{session_id}.json`)
**What's included:**
- **System Information**: App version, operating system, architecture, and timestamp
@@ -374,18 +374,18 @@ goose session diagnostics --session-id 20251108_5
goose session diagnostics -n my-project-session
# Save diagnostics to a custom location
goose session diagnostics --session-id 20251108_5 -o /path/to/my-diagnostics.zip
goose session diagnostics --session-id 20251108_5 -o /path/to/my-diagnostics.json
# Interactive selection (prompts you to choose a session)
goose session diagnostics
```
:::warning Privacy Notice
Diagnostics bundles contain your session messages and system information. If your session includes sensitive data (API keys, personal information, proprietary code), review the contents before sharing publicly.
Diagnostics reports contain your session messages and system information. If your session includes sensitive data (API keys, personal information, proprietary code), review the contents before sharing publicly.
:::
:::tip
Generate diagnostics before reporting bugs to provide technical details that help with faster resolution. The ZIP file can be attached to GitHub issues or shared with support.
Generate diagnostics before reporting bugs to provide technical details that help with faster resolution. The JSON file can be attached to GitHub issues or shared with support.
:::
---
@@ -12,13 +12,13 @@ goose provides several built-in features to help you get support, report issues,
| Feature | Purpose | Location | Output |
|---------|---------|----------|---------|
| **Diagnostics** | Generate troubleshooting data | Chat input toolbar | ZIP file with system info, logs, and session data |
| **Diagnostics** | Generate troubleshooting data | Chat input toolbar | JSON report with system info, logs, and session data |
| **Report a Bug** | Submit bug reports | Chat input toolbar OR Settings → App → Help & feedback | Opens GitHub issue template |
| **Request a Feature** | Suggest new features | Settings → App → Help & feedback | Opens GitHub issue template |
## Diagnostics System
The diagnostics feature creates a comprehensive troubleshooting bundle that includes system information, session data, configuration files, and recent logs. This is invaluable for debugging issues or getting technical support.
The diagnostics feature creates a comprehensive troubleshooting JSON report that includes system information, session data, configuration files, and recent logs. This is invaluable for debugging issues or getting technical support.
### Generating Diagnostics
@@ -27,8 +27,10 @@ The diagnostics feature creates a comprehensive troubleshooting bundle that incl
1. In an active chat session, look for the <Bug className="inline" size={16} /> icon in the bottom toolbar
2. Click the diagnostics button
3. Review the information in the modal about what data will be collected
4. Click `Download` to generate and save the diagnostics bundle
5. The ZIP file will be saved as `diagnostics_{session_id}.zip`
4. Click `Download` to generate and save the diagnostics report
5. The JSON file will be saved as `diagnostics_{session_id}.json`
You can use `scripts/diagnostics-viewer.py` to inspect downloaded diagnostics reports; by default it looks in `~/Downloads`.
:::tip
The diagnostics button is only available when you have an active session, as it needs a session ID to generate the bundle.
@@ -45,7 +47,7 @@ The diagnostics feature creates a comprehensive troubleshooting bundle that incl
goose session diagnostics
# Save to a custom location
goose session diagnostics --session-id <session_id> --output /path/to/diagnostics.zip
goose session diagnostics --session-id <session_id> --output /path/to/diagnostics.json
```
To find your session ID, first list available sessions:
@@ -65,17 +67,18 @@ The diagnostics feature creates a comprehensive troubleshooting bundle that incl
### Using Diagnostics Data
The diagnostics ZIP file contains several folders:
The diagnostics JSON file contains structured sections:
```
diagnostics_abc123def.zip
├── logs/
│ ├── goose-2024-01-15.jsonl
│ ├── goose-2024-01-14.jsonl
│ └── ...
├── session.json # Your session messages
├── config.yaml # Configuration files (if they exist)
└── system.txt # System information
```json
{
"system": {},
"session": {},
"config": {},
"logs": {},
"prompts": [],
"schedule": {},
"errors": []
}
```
**When to generate diagnostics:**
@@ -154,4 +157,3 @@ For issues not resolved by diagnostics:
- **[Session and System Logs](/docs/guides/logs)**: View detailed logs for debugging individual sessions
- **[Telemetry Export](/docs/guides/environment-variables#observability)**: Configure telemetry for performance analysis and production monitoring
+108 -24
View File
@@ -6,9 +6,9 @@
WARNING: entirely vibe coded. use as a throwaway tool
Diagnostics Viewer - Browse and inspect Goose diagnostics bundles.
Diagnostics Viewer - Browse and inspect Goose diagnostics reports.
Scans for diagnostics zip files, displays their sessions, and provides
Scans for diagnostics JSON reports and legacy zip files, displays their sessions, and provides
an interactive viewer for examining session data, logs, and other files.
"""
import json
@@ -188,34 +188,114 @@ class SearchOverlay(Container):
class DiagnosticsSession:
"""Represents a diagnostics bundle."""
"""Represents a diagnostics report or legacy diagnostics bundle."""
def __init__(self, zip_path: Path):
self.zip_path = zip_path
def __init__(self, path: Path):
self.path = path
self.is_zip = path.suffix == ".zip"
self.name = "Unknown Session"
self.session_id = zip_path.stem
self.created_at = zip_path.stat().st_mtime
self.session_id = path.stem
self.created_at = path.stat().st_mtime
self.report = None
self._load_session_name()
def _load_session_name(self):
"""Extract session name from session.json."""
"""Extract session name from the report."""
if not self.is_zip:
self._load_json_report()
session = (self.report or {}).get("session") or {}
self.name = session.get("name", "Unknown Session")
self.session_id = session.get("id", self.path.stem)
return
try:
with zipfile.ZipFile(self.zip_path, 'r') as zf:
with zipfile.ZipFile(self.path, 'r') as zf:
# Find session.json
for name in zf.namelist():
if name.endswith('session.json'):
with zf.open(name) as f:
data = json.load(f)
self.name = data.get('name', 'Unknown Session')
self.session_id = data.get('id', self.zip_path.stem)
self.session_id = data.get('id', self.path.stem)
break
except Exception as e:
self.name = f"Error loading: {e}"
def get_file_list(self) -> list[str]:
"""Get list of files in the zip, sorted with system.txt first."""
def _load_json_report(self):
if self.report is not None:
return
try:
with zipfile.ZipFile(self.zip_path, 'r') as zf:
self.report = json.loads(self.path.read_text())
except Exception as e:
self.report = {"error": f"Error loading: {e}"}
def _json_virtual_files(self) -> dict[str, str]:
self._load_json_report()
report = self.report or {}
files = {
"diagnostics.json": json.dumps(report, indent=2),
}
for key, filename in [
("system", "system.json"),
("config", "config.json"),
("extensions", "extensions.json"),
("session", "session.json"),
("schedule", "schedule.json"),
("errors", "errors.json"),
]:
value = report.get(key)
if value is not None:
files[filename] = json.dumps(value, indent=2)
logs = report.get("logs") or {}
server = logs.get("server")
if isinstance(server, dict) and server.get("content") is not None:
files["logs/server.txt"] = server["content"]
llm_logs = logs.get("llm") or []
for index, entry in enumerate(llm_logs):
if isinstance(entry, dict) and entry.get("content") is not None:
path = Path(entry.get("path") or f"llm_request.{index}.jsonl")
files[f"logs/{path.name}"] = entry["content"]
config = report.get("config") or {}
if isinstance(config, dict) and config.get("configYaml"):
files["config.yaml"] = config["configYaml"]
for prompt in report.get("prompts") or []:
if isinstance(prompt, dict) and prompt.get("name") and prompt.get("content") is not None:
files[f"prompts/{prompt['name']}.txt"] = prompt["content"]
for recipe in report.get("scheduledRecipes") or []:
if isinstance(recipe, dict) and recipe.get("path") and recipe.get("content") is not None:
path = Path(recipe["path"])
files[f"scheduled_recipes/{path.name}"] = recipe["content"]
return files
def get_file_list(self) -> list[str]:
"""Get list of report files, sorted with system first."""
if not self.is_zip:
files = list(self._json_virtual_files().keys())
def sort_key(f):
if f == "system.json":
return (0, f)
elif f == "session.json":
return (1, f)
elif f == "config.yaml" or f == "config.json":
return (2, f)
elif f == "diagnostics.json":
return (3, f)
else:
return (4, f)
return sorted(files, key=sort_key)
try:
with zipfile.ZipFile(self.path, 'r') as zf:
files = zf.namelist()
# Sort: system.txt first, then session.json, then alphabetically
@@ -234,13 +314,16 @@ class DiagnosticsSession:
return []
def read_file(self, filename: str) -> Optional[str]:
"""Read a file from the zip.
"""Read a file from the report.
Returns:
File content as string, or None if file cannot be read.
"""
if not self.is_zip:
return self._json_virtual_files().get(filename)
try:
with zipfile.ZipFile(self.zip_path, 'r') as zf:
with zipfile.ZipFile(self.path, 'r') as zf:
with zf.open(filename) as f:
return f.read().decode('utf-8', errors='replace')
except Exception:
@@ -591,8 +674,8 @@ class SessionList(Vertical):
list_view = self.query_one(ListView)
for session in self.sessions:
item = ListItem(
Label(f"{session.name}\n[dim]{session.zip_path.name}[/dim]"),
name=session.zip_path.name
Label(f"{session.name}\n[dim]{session.path.name}[/dim]"),
name=session.path.name
)
list_view.append(item)
@@ -752,13 +835,14 @@ class DiagnosticsApp(App):
self.show_session_list()
def scan_diagnostics(self):
"""Scan for diagnostics zip files."""
"""Scan for diagnostics JSON reports and legacy zip files."""
self.sessions = []
# Find all diagnostics zip files
for zip_path in self.diagnostics_dir.glob("diagnostics*.zip"):
session = DiagnosticsSession(zip_path)
self.sessions.append(session)
for path in [
*self.diagnostics_dir.glob("diagnostics*.json"),
*self.diagnostics_dir.glob("diagnostics*.zip"),
]:
self.sessions.append(DiagnosticsSession(path))
# Sort by creation time (newest first)
self.sessions.sort(key=lambda s: s.created_at, reverse=True)
@@ -781,9 +865,9 @@ class DiagnosticsApp(App):
def on_list_view_selected(self, event: ListView.Selected):
"""Handle session selection."""
# Find the session by zip name
# Find the session by diagnostics file name
session_name = event.item.name
session = next((s for s in self.sessions if s.zip_path.name == session_name), None)
session = next((s for s in self.sessions if s.path.name == session_name), None)
if session:
self.show_session_viewer(session)
+210 -4
View File
@@ -1730,6 +1730,19 @@
],
"operationId": "diagnostics",
"parameters": [
{
"name": "level",
"in": "query",
"required": false,
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/DiagnosticsLevel"
}
],
"nullable": true
}
},
{
"name": "session_id",
"in": "path",
@@ -1741,12 +1754,11 @@
],
"responses": {
"200": {
"description": "Diagnostics zip file",
"description": "Diagnostics report",
"content": {
"application/zip": {
"application/json": {
"schema": {
"type": "string",
"format": "binary"
"$ref": "#/components/schemas/DiagnosticsReport"
}
}
}
@@ -4617,6 +4629,200 @@
}
}
},
"DiagnosticsConfig": {
"type": "object",
"required": [
"configPath",
"truncated"
],
"properties": {
"configPath": {
"type": "string"
},
"configYaml": {
"type": "string",
"nullable": true
},
"truncated": {
"type": "boolean"
}
}
},
"DiagnosticsError": {
"type": "object",
"required": [
"message"
],
"properties": {
"message": {
"type": "string"
},
"path": {
"type": "string",
"nullable": true
}
}
},
"DiagnosticsExtensions": {
"type": "object",
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"DiagnosticsLevel": {
"type": "string",
"enum": [
"summary",
"full"
]
},
"DiagnosticsLogs": {
"type": "object",
"required": [
"llm"
],
"properties": {
"llm": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DiagnosticsTextFile"
}
},
"server": {
"allOf": [
{
"$ref": "#/components/schemas/DiagnosticsTextFile"
}
],
"nullable": true
}
}
},
"DiagnosticsPrompt": {
"type": "object",
"required": [
"name",
"content"
],
"properties": {
"content": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"DiagnosticsReport": {
"type": "object",
"required": [
"schemaVersion",
"generatedAt",
"level",
"system",
"extensions",
"logs",
"prompts",
"scheduledRecipes",
"errors"
],
"properties": {
"config": {
"allOf": [
{
"$ref": "#/components/schemas/DiagnosticsConfig"
}
],
"nullable": true
},
"errors": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DiagnosticsError"
}
},
"extensions": {
"$ref": "#/components/schemas/DiagnosticsExtensions"
},
"generatedAt": {
"type": "string"
},
"level": {
"$ref": "#/components/schemas/DiagnosticsLevel"
},
"logs": {
"$ref": "#/components/schemas/DiagnosticsLogs"
},
"prompts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DiagnosticsPrompt"
}
},
"schedule": {
"nullable": true
},
"scheduledRecipes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DiagnosticsScheduledRecipe"
}
},
"schemaVersion": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"session": {
"nullable": true
},
"system": {
"$ref": "#/components/schemas/SystemInfo"
}
}
},
"DiagnosticsScheduledRecipe": {
"type": "object",
"required": [
"path",
"content"
],
"properties": {
"content": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"DiagnosticsTextFile": {
"type": "object",
"required": [
"path",
"content",
"truncated"
],
"properties": {
"content": {
"type": "string"
},
"path": {
"type": "string"
},
"truncated": {
"type": "boolean"
}
}
},
"DictationProvider": {
"type": "string",
"enum": [
+16
View File
@@ -0,0 +1,16 @@
import { getAcpClient } from './acpConnection';
import type { DiagnosticsReport } from '../api';
export type DiagnosticsLevel = 'summary' | 'full';
export async function getDiagnosticsReport(
sessionId: string,
level: DiagnosticsLevel
): Promise<DiagnosticsReport> {
const client = await getAcpClient();
const response = await client.goose.diagnosticsGet_unstable({
sessionId,
level,
});
return response.report as DiagnosticsReport;
}
File diff suppressed because one or more lines are too long
+58 -3
View File
@@ -247,6 +247,59 @@ export type DeleteRecipeRequest = {
id: string;
};
export type DiagnosticsConfig = {
configPath: string;
configYaml?: string | null;
truncated: boolean;
};
export type DiagnosticsError = {
message: string;
path?: string | null;
};
export type DiagnosticsExtensions = {
enabled: Array<string>;
};
export type DiagnosticsLevel = 'summary' | 'full';
export type DiagnosticsLogs = {
llm: Array<DiagnosticsTextFile>;
server?: DiagnosticsTextFile | null;
};
export type DiagnosticsPrompt = {
content: string;
name: string;
};
export type DiagnosticsReport = {
config?: DiagnosticsConfig | null;
errors: Array<DiagnosticsError>;
extensions: DiagnosticsExtensions;
generatedAt: string;
level: DiagnosticsLevel;
logs: DiagnosticsLogs;
prompts: Array<DiagnosticsPrompt>;
schedule?: unknown;
scheduledRecipes: Array<DiagnosticsScheduledRecipe>;
schemaVersion: number;
session?: unknown;
system: SystemInfo;
};
export type DiagnosticsScheduledRecipe = {
content: string;
path: string;
};
export type DiagnosticsTextFile = {
content: string;
path: string;
truncated: boolean;
};
export type DictationProvider = 'openai' | 'elevenlabs' | 'groq' | 'local';
export type DictationProviderStatus = {
@@ -3098,7 +3151,9 @@ export type DiagnosticsData = {
path: {
session_id: string;
};
query?: never;
query?: {
level?: DiagnosticsLevel | null;
};
url: '/diagnostics/{session_id}';
};
@@ -3111,9 +3166,9 @@ export type DiagnosticsErrors = {
export type DiagnosticsResponses = {
/**
* Diagnostics zip file
* Diagnostics report
*/
200: Blob | File;
200: DiagnosticsReport;
};
export type DiagnosticsResponse = DiagnosticsResponses[keyof DiagnosticsResponses];
+11 -13
View File
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
import { AlertTriangle, Download, Github } from 'lucide-react';
import { Button } from './button';
import { toastError } from '../../toasts';
import { diagnostics, systemInfo } from '../../api';
import { defineMessages, useIntl } from '../../i18n';
import { getDiagnosticsReport } from '../../acp/diagnostics';
const i18n = defineMessages({
reportProblem: {
@@ -13,7 +13,7 @@ const i18n = defineMessages({
description: {
id: 'diagnosticsModal.description',
defaultMessage:
'You can download a diagnostics zip file to share with the team, or file a bug directly on GitHub with your system details pre-filled. A diagnostics report contains the following:',
'You can download a diagnostics JSON report to share with the team, or file a bug directly on GitHub with your system details pre-filled. A diagnostics report contains the following:',
},
systemInfo: {
id: 'diagnosticsModal.systemInfo',
@@ -66,7 +66,7 @@ const i18n = defineMessages({
},
diagnosticsErrorMsg: {
id: 'diagnosticsModal.diagnosticsErrorMsg',
defaultMessage: 'Failed to download diagnostics',
defaultMessage: 'Failed to download diagnostics report',
},
systemInfoErrorTitle: {
id: 'diagnosticsModal.systemInfoErrorTitle',
@@ -97,16 +97,14 @@ export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({
setIsDownloading(true);
try {
const response = await diagnostics({
path: { session_id: sessionId },
throwOnError: true,
const report = await getDiagnosticsReport(sessionId, 'full');
const blob = new Blob([`${JSON.stringify(report, null, 2)}\n`], {
type: 'application/json',
});
const blob = new Blob([response.data], { type: 'application/zip' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `diagnostics_${sessionId}.zip`;
a.download = `diagnostics_${sessionId}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
@@ -127,12 +125,12 @@ export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({
setIsFilingBug(true);
try {
const response = await systemInfo({ throwOnError: true });
const info = response.data;
const report = await getDiagnosticsReport(sessionId, 'summary');
const info = report.system;
const providerModel =
info.provider && info.model
? `${info.provider} ${info.model}`
? `${info.provider} - ${info.model}`
: info.provider || info.model || '[e.g. Google gemini-1.5-pro]';
const extensions =
@@ -145,7 +143,7 @@ export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({
💡 Before filing, please check common issues:
https://goose-docs.ai/docs/troubleshooting
📦 To help us debug faster, attach your **diagnostics zip** if possible.
📦 To help us debug faster, attach your **diagnostics JSON report** if possible.
👉 How to capture it: https://goose-docs.ai/docs/troubleshooting/diagnostics-and-reporting/
A clear and concise description of what the bug is.
+2 -2
View File
@@ -804,10 +804,10 @@
"defaultMessage": "Configuration settings"
},
"diagnosticsModal.description": {
"defaultMessage": "You can download a diagnostics zip file to share with the team, or file a bug directly on GitHub with your system details pre-filled. A diagnostics report contains the following:"
"defaultMessage": "You can download a diagnostics JSON report to share with the team, or file a bug directly on GitHub with your system details pre-filled. A diagnostics report contains the following:"
},
"diagnosticsModal.diagnosticsErrorMsg": {
"defaultMessage": "Failed to download diagnostics"
"defaultMessage": "Failed to download diagnostics report"
},
"diagnosticsModal.diagnosticsErrorTitle": {
"defaultMessage": "Diagnostics Error"
+15
View File
@@ -30,6 +30,8 @@ import type {
DeleteRecipeRequest_unstable,
DeleteSessionRequest,
DeleteSourceRequest_unstable,
DiagnosticsGetRequest_unstable,
DiagnosticsGetResponse_unstable,
DictationConfigRequest_unstable,
DictationConfigResponse_unstable,
DictationModelCancelRequest_unstable,
@@ -135,6 +137,7 @@ import {
zCustomProviderUpdateResponse_unstable,
zDecodeRecipeResponse_unstable,
zDefaultsReadResponse_unstable,
zDiagnosticsGetResponse_unstable,
zDictationConfigResponse_unstable,
zDictationModelDownloadProgressResponse_unstable,
zDictationModelsListResponse_unstable,
@@ -251,6 +254,18 @@ export class GooseExtClient {
) as SteerSessionResponse_unstable;
}
async diagnosticsGet_unstable(
params: DiagnosticsGetRequest_unstable,
): Promise<DiagnosticsGetResponse_unstable> {
const raw = await this.conn.extMethod(
"_goose/unstable/diagnostics/get",
params,
);
return zDiagnosticsGetResponse_unstable.parse(
raw,
) as DiagnosticsGetResponse_unstable;
}
async sessionDelete(params: DeleteSessionRequest): Promise<void> {
await this.conn.extMethod("session/delete", params);
}
File diff suppressed because one or more lines are too long
+13 -2
View File
@@ -489,6 +489,17 @@ export type SteerSessionResponse_unstable = {
messageId: string;
};
export type DiagnosticsGetRequest_unstable = {
sessionId: string;
level?: DiagnosticsReportLevel;
};
export type DiagnosticsReportLevel = 'summary' | 'full';
export type DiagnosticsGetResponse_unstable = {
report: unknown;
};
/**
* Delete a session.
*/
@@ -1806,14 +1817,14 @@ export type RecipeParamsAction = 'submit' | 'cancel';
export type ExtRequest = {
id: string;
method: string;
params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
params?: AddSessionExtensionRequest_unstable | RemoveSessionExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DiagnosticsGetRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | EncodeRecipeRequest_unstable | DecodeRecipeRequest_unstable | ScanRecipeRequest_unstable | ListRecipesRequest_unstable | DeleteRecipeRequest_unstable | ScheduleRecipeRequest_unstable | SetRecipeSlashCommandRequest_unstable | SaveRecipeRequest_unstable | ParseRecipeRequest_unstable | RecipeToYamlRequest_unstable | GetSessionInfoRequest_unstable | TruncateSessionConversationRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | {
[key: string]: unknown;
} | null;
};
export type ExtResponse = {
id: string;
result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | SteerSessionResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown;
result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | SteerSessionResponse_unstable | DiagnosticsGetResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | EncodeRecipeResponse_unstable | DecodeRecipeResponse_unstable | ScanRecipeResponse_unstable | ListRecipesResponse_unstable | SaveRecipeResponse_unstable | ParseRecipeResponse_unstable | RecipeToYamlResponse_unstable | GetSessionInfoResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown;
} | {
error: {
code: number;
+13
View File
@@ -458,6 +458,17 @@ export const zSteerSessionResponse_unstable = z.object({
messageId: z.string()
});
export const zDiagnosticsReportLevel = z.enum(['summary', 'full']);
export const zDiagnosticsGetRequest_unstable = z.object({
sessionId: z.string(),
level: zDiagnosticsReportLevel.optional().default('summary')
});
export const zDiagnosticsGetResponse_unstable = z.object({
report: z.unknown()
});
/**
* Delete a session.
*/
@@ -1921,6 +1932,7 @@ export const zExtRequest = z.object({
zUpdateWorkingDirRequest_unstable,
zSetSessionSystemPromptRequest_unstable,
zSteerSessionRequest_unstable,
zDiagnosticsGetRequest_unstable,
zDeleteSessionRequest,
zGetConfigExtensionsRequest_unstable,
zGetAvailableExtensionsRequest_unstable,
@@ -2002,6 +2014,7 @@ export const zExtResponse = z.union([
zGooseToolCallResponse_unstable,
zReadResourceResponse_unstable,
zSteerSessionResponse_unstable,
zDiagnosticsGetResponse_unstable,
zGetConfigExtensionsResponse_unstable,
zGetAvailableExtensionsResponse_unstable,
zGetSessionExtensionsResponse_unstable,