mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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>,
|
||||
|
||||
/// 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?;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user