feat: add --output-format json flag to goose run command (#5525)

Signed-off-by: sheikhlimon <sheikhlimon404@gmail.com>
Signed-off-by: Sheikh Limon <sheikhlimon404@gmail.com>
This commit is contained in:
Sheikh Limon
2025-11-04 22:33:17 +06:00
committed by GitHub
parent 5b93ee587f
commit d4a7bd39be
5 changed files with 131 additions and 11 deletions
+14
View File
@@ -720,6 +720,16 @@ enum Command {
)]
additional_sub_recipes: Vec<String>,
/// Output format (text, json)
#[arg(
long = "output-format",
value_name = "FORMAT",
help = "Output format (text, json)",
default_value = "text",
value_parser = clap::builder::PossibleValuesParser::new(["text", "json"])
)]
output_format: String,
/// Provider to use for this run (overrides environment variable)
#[arg(
long = "provider",
@@ -974,6 +984,7 @@ pub async fn cli() -> anyhow::Result<()> {
sub_recipes: None,
final_output_response: None,
retry_config: None,
output_format: "text".to_string(),
})
.await;
@@ -1054,6 +1065,7 @@ pub async fn cli() -> anyhow::Result<()> {
scheduled_job_id,
quiet,
additional_sub_recipes,
output_format,
provider,
model,
}) => {
@@ -1183,6 +1195,7 @@ pub async fn cli() -> anyhow::Result<()> {
.as_ref()
.and_then(|r| r.final_output_response.clone()),
retry_config: recipe_info.as_ref().and_then(|r| r.retry_config.clone()),
output_format,
})
.await;
@@ -1366,6 +1379,7 @@ pub async fn cli() -> anyhow::Result<()> {
sub_recipes: None,
final_output_response: None,
retry_config: None,
output_format: "text".to_string(),
})
.await;
session.interactive(None).await?;
+1
View File
@@ -55,6 +55,7 @@ pub async fn agent_generator(
sub_recipes: None,
final_output_response: None,
retry_config: None,
output_format: "text".to_string(),
})
.await;
@@ -225,7 +225,17 @@ where
SessionType::Hidden,
)
.await?;
let mut cli_session = CliSession::new(agent, session.id, false, None, None, None, None).await;
let mut cli_session = CliSession::new(
agent,
session.id,
false,
None,
None,
None,
None,
"text".to_string(),
)
.await;
let mut error = None;
for message in &messages {
+47 -3
View File
@@ -24,7 +24,7 @@ use tokio::task::JoinSet;
///
/// This struct contains all the parameters needed to create a new session,
/// including session identification, extension configuration, and debug settings.
#[derive(Default, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct SessionBuilderConfig {
/// Session id, optional need to deduce from context
pub session_id: Option<String>,
@@ -68,6 +68,39 @@ pub struct SessionBuilderConfig {
pub final_output_response: Option<Response>,
/// Retry configuration for automated validation and recovery
pub retry_config: Option<RetryConfig>,
/// Output format (text, json)
pub output_format: String,
}
/// Manual implementation of Default to ensure proper initialization of output_format
/// This struct requires explicit default value for output_format field
impl Default for SessionBuilderConfig {
fn default() -> Self {
SessionBuilderConfig {
session_id: None,
resume: false,
no_session: false,
extensions: Vec::new(),
remote_extensions: Vec::new(),
streamable_http_extensions: Vec::new(),
builtins: Vec::new(),
extensions_override: None,
additional_system_prompt: None,
settings: None,
provider: None,
model: None,
debug: false,
max_tool_repetitions: None,
max_turns: None,
scheduled_job_id: None,
interactive: false,
quiet: false,
sub_recipes: None,
final_output_response: None,
retry_config: None,
output_format: "text".to_string(),
}
}
}
/// Offers to help debug an extension failure by creating a minimal debugging session
@@ -139,8 +172,17 @@ async fn offer_extension_debugging_help(
SessionType::Hidden,
)
.await?;
let mut debug_session =
CliSession::new(debug_agent, session.id, false, None, None, None, None).await;
let mut debug_session = CliSession::new(
debug_agent,
session.id,
false,
None,
None,
None,
None,
"text".to_string(),
)
.await;
// Process the debugging request
println!("{}", style("Analyzing the extension failure...").yellow());
@@ -459,6 +501,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
session_config.max_turns,
edit_mode,
session_config.retry_config.clone(),
session_config.output_format.clone(),
)
.await;
@@ -643,6 +686,7 @@ mod tests {
sub_recipes: None,
final_output_response: None,
retry_config: None,
output_format: "text".to_string(),
};
assert_eq!(config.extensions.len(), 1);
+58 -7
View File
@@ -41,6 +41,7 @@ use goose::config::paths::Paths;
use goose::conversation::message::{Message, MessageContent};
use rand::{distributions::Alphanumeric, Rng};
use rustyline::EditMode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
@@ -50,6 +51,18 @@ use tokio;
use tokio_util::sync::CancellationToken;
use tracing::warn;
#[derive(Serialize, Deserialize, Debug)]
struct JsonOutput {
messages: Vec<Message>,
metadata: JsonMetadata,
}
#[derive(Serialize, Deserialize, Debug)]
struct JsonMetadata {
total_tokens: Option<i32>,
status: String,
}
pub enum RunMode {
Normal,
Plan,
@@ -66,6 +79,7 @@ pub struct CliSession {
max_turns: Option<u32>,
edit_mode: Option<EditMode>,
retry_config: Option<RetryConfig>,
output_format: String,
}
// Cache structure for completion data
@@ -120,6 +134,7 @@ pub async fn classify_planner_response(
}
impl CliSession {
#[allow(clippy::too_many_arguments)]
pub async fn new(
agent: Agent,
session_id: String,
@@ -128,6 +143,7 @@ impl CliSession {
max_turns: Option<u32>,
edit_mode: Option<EditMode>,
retry_config: Option<RetryConfig>,
output_format: String,
) -> Self {
let messages = SessionManager::get_session(&session_id, true)
.await
@@ -145,6 +161,7 @@ impl CliSession {
max_turns,
edit_mode,
retry_config,
output_format,
}
}
@@ -760,6 +777,9 @@ impl CliSession {
) -> Result<()> {
let cancel_token_clone = cancel_token.clone();
// Cache the output format check to avoid repeated string comparisons in the hot loop
let is_json_mode = self.output_format == "json";
let session_config = SessionConfig {
id: self.session_id.clone(),
schedule_id: self.scheduled_job_id.clone(),
@@ -891,11 +911,16 @@ impl CliSession {
);
}
}
self.messages.push(message.clone());
if interactive {output::hide_thinking()};
let _ = progress_bars.hide();
output::render_message(&message, self.debug);
// Don't render in JSON mode
if !is_json_mode {
output::render_message(&message, self.debug);
}
}
}
Some(Ok(AgentEvent::McpNotification((_id, message)))) => {
@@ -967,17 +992,21 @@ impl CliSession {
// TODO: proper display for subagent notifications
if interactive {
let _ = progress_bars.hide();
println!("{}", console::style(&formatted_message).green().dim());
} else {
if !is_json_mode {
println!("{}", console::style(&formatted_message).green().dim());
}
} else if !is_json_mode {
progress_bars.log(&formatted_message);
}
} else if let Some(ref notification_type) = message_notification_type {
if notification_type == TASK_EXECUTION_NOTIFICATION_TYPE {
if interactive {
let _ = progress_bars.hide();
print!("{}", formatted_message);
std::io::stdout().flush().unwrap();
} else {
if !is_json_mode {
print!("{}", formatted_message);
std::io::stdout().flush().unwrap();
}
} else if !is_json_mode {
print!("{}", formatted_message);
std::io::stdout().flush().unwrap();
}
@@ -1056,7 +1085,29 @@ impl CliSession {
}
}
}
println!();
// Output JSON if requested
if is_json_mode {
let metadata = match SessionManager::get_session(&self.session_id, false).await {
Ok(session) => JsonMetadata {
total_tokens: session.total_tokens,
status: "completed".to_string(),
},
Err(_) => JsonMetadata {
total_tokens: None,
status: "completed".to_string(),
},
};
let json_output = JsonOutput {
messages: self.messages.messages().to_vec(),
metadata,
};
println!("{}", serde_json::to_string_pretty(&json_output)?);
} else {
println!();
}
Ok(())
}