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:
@@ -720,6 +720,16 @@ enum Command {
|
|||||||
)]
|
)]
|
||||||
additional_sub_recipes: Vec<String>,
|
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)
|
/// Provider to use for this run (overrides environment variable)
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "provider",
|
long = "provider",
|
||||||
@@ -974,6 +984,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
sub_recipes: None,
|
sub_recipes: None,
|
||||||
final_output_response: None,
|
final_output_response: None,
|
||||||
retry_config: None,
|
retry_config: None,
|
||||||
|
output_format: "text".to_string(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1054,6 +1065,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
scheduled_job_id,
|
scheduled_job_id,
|
||||||
quiet,
|
quiet,
|
||||||
additional_sub_recipes,
|
additional_sub_recipes,
|
||||||
|
output_format,
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -1183,6 +1195,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|r| r.final_output_response.clone()),
|
.and_then(|r| r.final_output_response.clone()),
|
||||||
retry_config: recipe_info.as_ref().and_then(|r| r.retry_config.clone()),
|
retry_config: recipe_info.as_ref().and_then(|r| r.retry_config.clone()),
|
||||||
|
output_format,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1366,6 +1379,7 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||||||
sub_recipes: None,
|
sub_recipes: None,
|
||||||
final_output_response: None,
|
final_output_response: None,
|
||||||
retry_config: None,
|
retry_config: None,
|
||||||
|
output_format: "text".to_string(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
session.interactive(None).await?;
|
session.interactive(None).await?;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub async fn agent_generator(
|
|||||||
sub_recipes: None,
|
sub_recipes: None,
|
||||||
final_output_response: None,
|
final_output_response: None,
|
||||||
retry_config: None,
|
retry_config: None,
|
||||||
|
output_format: "text".to_string(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,17 @@ where
|
|||||||
SessionType::Hidden,
|
SessionType::Hidden,
|
||||||
)
|
)
|
||||||
.await?;
|
.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;
|
let mut error = None;
|
||||||
for message in &messages {
|
for message in &messages {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use tokio::task::JoinSet;
|
|||||||
///
|
///
|
||||||
/// This struct contains all the parameters needed to create a new session,
|
/// This struct contains all the parameters needed to create a new session,
|
||||||
/// including session identification, extension configuration, and debug settings.
|
/// including session identification, extension configuration, and debug settings.
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SessionBuilderConfig {
|
pub struct SessionBuilderConfig {
|
||||||
/// Session id, optional need to deduce from context
|
/// Session id, optional need to deduce from context
|
||||||
pub session_id: Option<String>,
|
pub session_id: Option<String>,
|
||||||
@@ -68,6 +68,39 @@ pub struct SessionBuilderConfig {
|
|||||||
pub final_output_response: Option<Response>,
|
pub final_output_response: Option<Response>,
|
||||||
/// Retry configuration for automated validation and recovery
|
/// Retry configuration for automated validation and recovery
|
||||||
pub retry_config: Option<RetryConfig>,
|
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
|
/// 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,
|
SessionType::Hidden,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let mut debug_session =
|
let mut debug_session = CliSession::new(
|
||||||
CliSession::new(debug_agent, session.id, false, None, None, None, None).await;
|
debug_agent,
|
||||||
|
session.id,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"text".to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// Process the debugging request
|
// Process the debugging request
|
||||||
println!("{}", style("Analyzing the extension failure...").yellow());
|
println!("{}", style("Analyzing the extension failure...").yellow());
|
||||||
@@ -459,6 +501,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
|||||||
session_config.max_turns,
|
session_config.max_turns,
|
||||||
edit_mode,
|
edit_mode,
|
||||||
session_config.retry_config.clone(),
|
session_config.retry_config.clone(),
|
||||||
|
session_config.output_format.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -643,6 +686,7 @@ mod tests {
|
|||||||
sub_recipes: None,
|
sub_recipes: None,
|
||||||
final_output_response: None,
|
final_output_response: None,
|
||||||
retry_config: None,
|
retry_config: None,
|
||||||
|
output_format: "text".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(config.extensions.len(), 1);
|
assert_eq!(config.extensions.len(), 1);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ use goose::config::paths::Paths;
|
|||||||
use goose::conversation::message::{Message, MessageContent};
|
use goose::conversation::message::{Message, MessageContent};
|
||||||
use rand::{distributions::Alphanumeric, Rng};
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
use rustyline::EditMode;
|
use rustyline::EditMode;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -50,6 +51,18 @@ use tokio;
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::warn;
|
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 {
|
pub enum RunMode {
|
||||||
Normal,
|
Normal,
|
||||||
Plan,
|
Plan,
|
||||||
@@ -66,6 +79,7 @@ pub struct CliSession {
|
|||||||
max_turns: Option<u32>,
|
max_turns: Option<u32>,
|
||||||
edit_mode: Option<EditMode>,
|
edit_mode: Option<EditMode>,
|
||||||
retry_config: Option<RetryConfig>,
|
retry_config: Option<RetryConfig>,
|
||||||
|
output_format: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache structure for completion data
|
// Cache structure for completion data
|
||||||
@@ -120,6 +134,7 @@ pub async fn classify_planner_response(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CliSession {
|
impl CliSession {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
@@ -128,6 +143,7 @@ impl CliSession {
|
|||||||
max_turns: Option<u32>,
|
max_turns: Option<u32>,
|
||||||
edit_mode: Option<EditMode>,
|
edit_mode: Option<EditMode>,
|
||||||
retry_config: Option<RetryConfig>,
|
retry_config: Option<RetryConfig>,
|
||||||
|
output_format: String,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let messages = SessionManager::get_session(&session_id, true)
|
let messages = SessionManager::get_session(&session_id, true)
|
||||||
.await
|
.await
|
||||||
@@ -145,6 +161,7 @@ impl CliSession {
|
|||||||
max_turns,
|
max_turns,
|
||||||
edit_mode,
|
edit_mode,
|
||||||
retry_config,
|
retry_config,
|
||||||
|
output_format,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,6 +777,9 @@ impl CliSession {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let cancel_token_clone = cancel_token.clone();
|
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 {
|
let session_config = SessionConfig {
|
||||||
id: self.session_id.clone(),
|
id: self.session_id.clone(),
|
||||||
schedule_id: self.scheduled_job_id.clone(),
|
schedule_id: self.scheduled_job_id.clone(),
|
||||||
@@ -891,11 +911,16 @@ impl CliSession {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.messages.push(message.clone());
|
self.messages.push(message.clone());
|
||||||
|
|
||||||
if interactive {output::hide_thinking()};
|
if interactive {output::hide_thinking()};
|
||||||
let _ = progress_bars.hide();
|
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)))) => {
|
Some(Ok(AgentEvent::McpNotification((_id, message)))) => {
|
||||||
@@ -967,17 +992,21 @@ impl CliSession {
|
|||||||
// TODO: proper display for subagent notifications
|
// TODO: proper display for subagent notifications
|
||||||
if interactive {
|
if interactive {
|
||||||
let _ = progress_bars.hide();
|
let _ = progress_bars.hide();
|
||||||
println!("{}", console::style(&formatted_message).green().dim());
|
if !is_json_mode {
|
||||||
} else {
|
println!("{}", console::style(&formatted_message).green().dim());
|
||||||
|
}
|
||||||
|
} else if !is_json_mode {
|
||||||
progress_bars.log(&formatted_message);
|
progress_bars.log(&formatted_message);
|
||||||
}
|
}
|
||||||
} else if let Some(ref notification_type) = message_notification_type {
|
} else if let Some(ref notification_type) = message_notification_type {
|
||||||
if notification_type == TASK_EXECUTION_NOTIFICATION_TYPE {
|
if notification_type == TASK_EXECUTION_NOTIFICATION_TYPE {
|
||||||
if interactive {
|
if interactive {
|
||||||
let _ = progress_bars.hide();
|
let _ = progress_bars.hide();
|
||||||
print!("{}", formatted_message);
|
if !is_json_mode {
|
||||||
std::io::stdout().flush().unwrap();
|
print!("{}", formatted_message);
|
||||||
} else {
|
std::io::stdout().flush().unwrap();
|
||||||
|
}
|
||||||
|
} else if !is_json_mode {
|
||||||
print!("{}", formatted_message);
|
print!("{}", formatted_message);
|
||||||
std::io::stdout().flush().unwrap();
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user