feat: option to stream json - jsonl really (#6228)
This commit is contained in:
@@ -753,13 +753,13 @@ enum Command {
|
|||||||
)]
|
)]
|
||||||
additional_sub_recipes: Vec<String>,
|
additional_sub_recipes: Vec<String>,
|
||||||
|
|
||||||
/// Output format (text, json)
|
/// Output format (text, json, stream-json)
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "output-format",
|
long = "output-format",
|
||||||
value_name = "FORMAT",
|
value_name = "FORMAT",
|
||||||
help = "Output format (text, json)",
|
help = "Output format (text, json, stream-json)",
|
||||||
default_value = "text",
|
default_value = "text",
|
||||||
value_parser = clap::builder::PossibleValuesParser::new(["text", "json"])
|
value_parser = clap::builder::PossibleValuesParser::new(["text", "json", "stream-json"])
|
||||||
)]
|
)]
|
||||||
output_format: String,
|
output_format: String,
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,42 @@ struct JsonMetadata {
|
|||||||
status: String,
|
status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
enum StreamEvent {
|
||||||
|
Message {
|
||||||
|
message: Message,
|
||||||
|
},
|
||||||
|
Notification {
|
||||||
|
extension_id: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
data: NotificationData,
|
||||||
|
},
|
||||||
|
ModelChange {
|
||||||
|
model: String,
|
||||||
|
mode: String,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
error: String,
|
||||||
|
},
|
||||||
|
Complete {
|
||||||
|
total_tokens: Option<i32>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum NotificationData {
|
||||||
|
Log {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
Progress {
|
||||||
|
progress: f64,
|
||||||
|
total: Option<f64>,
|
||||||
|
message: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
pub enum RunMode {
|
pub enum RunMode {
|
||||||
Normal,
|
Normal,
|
||||||
Plan,
|
Plan,
|
||||||
@@ -812,8 +848,15 @@ impl CliSession {
|
|||||||
interactive: bool,
|
interactive: bool,
|
||||||
cancel_token: CancellationToken,
|
cancel_token: CancellationToken,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Cache the output format check to avoid repeated string comparisons in the hot loop
|
|
||||||
let is_json_mode = self.output_format == "json";
|
let is_json_mode = self.output_format == "json";
|
||||||
|
let is_stream_json_mode = self.output_format == "stream-json";
|
||||||
|
|
||||||
|
// Helper to emit a streaming JSON event
|
||||||
|
let emit_stream_event = |event: &StreamEvent| {
|
||||||
|
if let Ok(json) = serde_json::to_string(event) {
|
||||||
|
println!("{}", json);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let session_config = SessionConfig {
|
let session_config = SessionConfig {
|
||||||
id: self.session_id.clone(),
|
id: self.session_id.clone(),
|
||||||
@@ -1027,13 +1070,15 @@ impl CliSession {
|
|||||||
if interactive {output::hide_thinking()};
|
if interactive {output::hide_thinking()};
|
||||||
let _ = progress_bars.hide();
|
let _ = progress_bars.hide();
|
||||||
|
|
||||||
// Don't render in JSON mode
|
// Handle different output formats
|
||||||
if !is_json_mode {
|
if is_stream_json_mode {
|
||||||
|
emit_stream_event(&StreamEvent::Message { message: message.clone() });
|
||||||
|
} else if !is_json_mode {
|
||||||
output::render_message(&message, self.debug);
|
output::render_message(&message, self.debug);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Ok(AgentEvent::McpNotification((_id, message)))) => {
|
Some(Ok(AgentEvent::McpNotification((extension_id, message)))) => {
|
||||||
match &message {
|
match &message {
|
||||||
ServerNotification::LoggingMessageNotification(notification) => {
|
ServerNotification::LoggingMessageNotification(notification) => {
|
||||||
let data = ¬ification.params.data;
|
let data = ¬ification.params.data;
|
||||||
@@ -1101,9 +1146,14 @@ impl CliSession {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if is_stream_json_mode {
|
||||||
|
emit_stream_event(&StreamEvent::Notification {
|
||||||
|
extension_id: extension_id.clone(),
|
||||||
|
data: NotificationData::Log { message: formatted_message.clone() },
|
||||||
|
});
|
||||||
|
}
|
||||||
// Handle subagent notifications - show immediately
|
// Handle subagent notifications - show immediately
|
||||||
if let Some(_id) = subagent_id {
|
else if let Some(_id) = subagent_id {
|
||||||
// TODO: proper display for subagent notifications
|
|
||||||
if interactive {
|
if interactive {
|
||||||
let _ = progress_bars.hide();
|
let _ = progress_bars.hide();
|
||||||
if !is_json_mode {
|
if !is_json_mode {
|
||||||
@@ -1125,7 +1175,6 @@ impl CliSession {
|
|||||||
std::io::stdout().flush().unwrap();
|
std::io::stdout().flush().unwrap();
|
||||||
}
|
}
|
||||||
} else if notification_type == "shell_output" {
|
} else if notification_type == "shell_output" {
|
||||||
// Hide spinner, print shell output, spinner will resume
|
|
||||||
if interactive {
|
if interactive {
|
||||||
let _ = progress_bars.hide();
|
let _ = progress_bars.hide();
|
||||||
}
|
}
|
||||||
@@ -1145,12 +1194,24 @@ impl CliSession {
|
|||||||
let text = notification.params.message.as_deref();
|
let text = notification.params.message.as_deref();
|
||||||
let total = notification.params.total;
|
let total = notification.params.total;
|
||||||
let token = ¬ification.params.progress_token;
|
let token = ¬ification.params.progress_token;
|
||||||
progress_bars.update(
|
|
||||||
&token.0.to_string(),
|
if is_stream_json_mode {
|
||||||
progress,
|
emit_stream_event(&StreamEvent::Notification {
|
||||||
total,
|
extension_id: extension_id.clone(),
|
||||||
text,
|
data: NotificationData::Progress {
|
||||||
);
|
progress,
|
||||||
|
total,
|
||||||
|
message: text.map(String::from),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
progress_bars.update(
|
||||||
|
&token.0.to_string(),
|
||||||
|
progress,
|
||||||
|
total,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
@@ -1159,32 +1220,44 @@ impl CliSession {
|
|||||||
self.messages = updated_conversation;
|
self.messages = updated_conversation;
|
||||||
}
|
}
|
||||||
Some(Ok(AgentEvent::ModelChange { model, mode })) => {
|
Some(Ok(AgentEvent::ModelChange { model, mode })) => {
|
||||||
// Log model change if in debug mode
|
if is_stream_json_mode {
|
||||||
if self.debug {
|
emit_stream_event(&StreamEvent::ModelChange {
|
||||||
|
model: model.clone(),
|
||||||
|
mode: mode.clone(),
|
||||||
|
});
|
||||||
|
} else if self.debug {
|
||||||
eprintln!("Model changed to {} in {} mode", model, mode);
|
eprintln!("Model changed to {} in {} mode", model, mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Err(e)) => {
|
Some(Err(e)) => {
|
||||||
// TODO(Douwe): Delete this
|
let error_msg = e.to_string();
|
||||||
// Check if it's a ProviderError::ContextLengthExceeded
|
|
||||||
|
if is_stream_json_mode {
|
||||||
|
emit_stream_event(&StreamEvent::Error { error: error_msg.clone() });
|
||||||
|
}
|
||||||
|
|
||||||
if e.downcast_ref::<goose::providers::errors::ProviderError>()
|
if e.downcast_ref::<goose::providers::errors::ProviderError>()
|
||||||
.map(|provider_error| matches!(provider_error, goose::providers::errors::ProviderError::ContextLengthExceeded(_)))
|
.map(|provider_error| matches!(provider_error, goose::providers::errors::ProviderError::ContextLengthExceeded(_)))
|
||||||
.unwrap_or(false) {
|
.unwrap_or(false) {
|
||||||
|
|
||||||
output::render_text(
|
if !is_stream_json_mode {
|
||||||
"Compaction requested. Should have happened in the agent!",
|
output::render_text(
|
||||||
Some(Color::Yellow),
|
"Compaction requested. Should have happened in the agent!",
|
||||||
true
|
Some(Color::Yellow),
|
||||||
);
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
warn!("Compaction requested. Should have happened in the agent!");
|
warn!("Compaction requested. Should have happened in the agent!");
|
||||||
}
|
}
|
||||||
eprintln!("Error: {}", e);
|
if !is_stream_json_mode {
|
||||||
|
eprintln!("Error: {}", error_msg);
|
||||||
|
}
|
||||||
cancel_token_clone.cancel();
|
cancel_token_clone.cancel();
|
||||||
drop(stream);
|
drop(stream);
|
||||||
if let Err(e) = self.handle_interrupted_messages(false).await {
|
if let Err(e) = self.handle_interrupted_messages(false).await {
|
||||||
eprintln!("Error handling interruption: {}", e);
|
eprintln!("Error handling interruption: {}", e);
|
||||||
} else {
|
} else if !is_stream_json_mode {
|
||||||
output::render_error(
|
output::render_error(
|
||||||
"The error above was an exception we were not able to handle.\n\
|
"The error above was an exception we were not able to handle.\n\
|
||||||
These errors are often related to connection or authentication\n\
|
These errors are often related to connection or authentication\n\
|
||||||
@@ -1207,7 +1280,7 @@ impl CliSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output JSON if requested
|
// Output based on format
|
||||||
if is_json_mode {
|
if is_json_mode {
|
||||||
let metadata = match SessionManager::get_session(&self.session_id, false).await {
|
let metadata = match SessionManager::get_session(&self.session_id, false).await {
|
||||||
Ok(session) => JsonMetadata {
|
Ok(session) => JsonMetadata {
|
||||||
@@ -1226,6 +1299,12 @@ impl CliSession {
|
|||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", serde_json::to_string_pretty(&json_output)?);
|
println!("{}", serde_json::to_string_pretty(&json_output)?);
|
||||||
|
} else if is_stream_json_mode {
|
||||||
|
let total_tokens = SessionManager::get_session(&self.session_id, false)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.total_tokens);
|
||||||
|
emit_stream_event(&StreamEvent::Complete { total_tokens });
|
||||||
} else {
|
} else {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user