Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1054,6 +1065,7 @@ pub async fn cli() -> anyhow::Result<()> {
scheduled_job_id,
quiet,
additional_sub_recipes,
output_format,
provider,
model,
}) => {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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?;
Expand Down
1 change: 1 addition & 0 deletions crates/goose-cli/src/commands/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
12 changes: 11 additions & 1 deletion crates/goose-cli/src/scenario_tests/scenario_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
50 changes: 47 additions & 3 deletions crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
65 changes: 58 additions & 7 deletions crates/goose-cli/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -145,6 +161,7 @@ impl CliSession {
max_turns,
edit_mode,
retry_config,
output_format,
}
}

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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)))) => {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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(())
}
Expand Down