feat: option to stream json - jsonl really (#6228)
This commit is contained in:
@@ -753,13 +753,13 @@ enum Command {
|
||||
)]
|
||||
additional_sub_recipes: Vec<String>,
|
||||
|
||||
/// Output format (text, json)
|
||||
/// Output format (text, json, stream-json)
|
||||
#[arg(
|
||||
long = "output-format",
|
||||
value_name = "FORMAT",
|
||||
help = "Output format (text, json)",
|
||||
help = "Output format (text, json, stream-json)",
|
||||
default_value = "text",
|
||||
value_parser = clap::builder::PossibleValuesParser::new(["text", "json"])
|
||||
value_parser = clap::builder::PossibleValuesParser::new(["text", "json", "stream-json"])
|
||||
)]
|
||||
output_format: String,
|
||||
|
||||
|
||||
@@ -65,6 +65,42 @@ struct JsonMetadata {
|
||||
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 {
|
||||
Normal,
|
||||
Plan,
|
||||
@@ -812,8 +848,15 @@ impl CliSession {
|
||||
interactive: bool,
|
||||
cancel_token: CancellationToken,
|
||||
) -> 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_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 {
|
||||
id: self.session_id.clone(),
|
||||
@@ -1027,13 +1070,15 @@ impl CliSession {
|
||||
if interactive {output::hide_thinking()};
|
||||
let _ = progress_bars.hide();
|
||||
|
||||
// Don't render in JSON mode
|
||||
if !is_json_mode {
|
||||
// Handle different output formats
|
||||
if is_stream_json_mode {
|
||||
emit_stream_event(&StreamEvent::Message { message: message.clone() });
|
||||
} else if !is_json_mode {
|
||||
output::render_message(&message, self.debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(AgentEvent::McpNotification((_id, message)))) => {
|
||||
Some(Ok(AgentEvent::McpNotification((extension_id, message)))) => {
|
||||
match &message {
|
||||
ServerNotification::LoggingMessageNotification(notification) => {
|
||||
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
|
||||
if let Some(_id) = subagent_id {
|
||||
// TODO: proper display for subagent notifications
|
||||
else if let Some(_id) = subagent_id {
|
||||
if interactive {
|
||||
let _ = progress_bars.hide();
|
||||
if !is_json_mode {
|
||||
@@ -1125,7 +1175,6 @@ impl CliSession {
|
||||
std::io::stdout().flush().unwrap();
|
||||
}
|
||||
} else if notification_type == "shell_output" {
|
||||
// Hide spinner, print shell output, spinner will resume
|
||||
if interactive {
|
||||
let _ = progress_bars.hide();
|
||||
}
|
||||
@@ -1145,12 +1194,24 @@ impl CliSession {
|
||||
let text = notification.params.message.as_deref();
|
||||
let total = notification.params.total;
|
||||
let token = ¬ification.params.progress_token;
|
||||
progress_bars.update(
|
||||
&token.0.to_string(),
|
||||
progress,
|
||||
total,
|
||||
text,
|
||||
);
|
||||
|
||||
if is_stream_json_mode {
|
||||
emit_stream_event(&StreamEvent::Notification {
|
||||
extension_id: extension_id.clone(),
|
||||
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;
|
||||
}
|
||||
Some(Ok(AgentEvent::ModelChange { model, mode })) => {
|
||||
// Log model change if in debug mode
|
||||
if self.debug {
|
||||
if is_stream_json_mode {
|
||||
emit_stream_event(&StreamEvent::ModelChange {
|
||||
model: model.clone(),
|
||||
mode: mode.clone(),
|
||||
});
|
||||
} else if self.debug {
|
||||
eprintln!("Model changed to {} in {} mode", model, mode);
|
||||
}
|
||||
}
|
||||
|
||||
Some(Err(e)) => {
|
||||
// TODO(Douwe): Delete this
|
||||
// Check if it's a ProviderError::ContextLengthExceeded
|
||||
let error_msg = e.to_string();
|
||||
|
||||
if is_stream_json_mode {
|
||||
emit_stream_event(&StreamEvent::Error { error: error_msg.clone() });
|
||||
}
|
||||
|
||||
if e.downcast_ref::<goose::providers::errors::ProviderError>()
|
||||
.map(|provider_error| matches!(provider_error, goose::providers::errors::ProviderError::ContextLengthExceeded(_)))
|
||||
.unwrap_or(false) {
|
||||
|
||||
output::render_text(
|
||||
"Compaction requested. Should have happened in the agent!",
|
||||
Some(Color::Yellow),
|
||||
true
|
||||
);
|
||||
if !is_stream_json_mode {
|
||||
output::render_text(
|
||||
"Compaction requested. Should have happened in the agent!",
|
||||
Some(Color::Yellow),
|
||||
true
|
||||
);
|
||||
}
|
||||
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();
|
||||
drop(stream);
|
||||
if let Err(e) = self.handle_interrupted_messages(false).await {
|
||||
eprintln!("Error handling interruption: {}", e);
|
||||
} else {
|
||||
} else if !is_stream_json_mode {
|
||||
output::render_error(
|
||||
"The error above was an exception we were not able to handle.\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 {
|
||||
let metadata = match SessionManager::get_session(&self.session_id, false).await {
|
||||
Ok(session) => JsonMetadata {
|
||||
@@ -1226,6 +1299,12 @@ impl CliSession {
|
||||
};
|
||||
|
||||
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 {
|
||||
println!();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user