chore: break up process agent response (#6348)

This commit is contained in:
Bradley Axen
2026-01-12 07:37:25 -08:00
committed by GitHub
parent ba702d820f
commit e86b244d0a
+352 -306
View File
@@ -864,13 +864,6 @@ impl CliSession {
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(),
schedule_id: self.scheduled_job_id.clone(),
@@ -908,91 +901,30 @@ impl CliSession {
result = stream.next() => {
match result {
Some(Ok(AgentEvent::Message(message))) => {
let tool_call_confirmation = message.content.iter().find_map(|content| {
if let MessageContent::ActionRequired(action) = content {
#[allow(irrefutable_let_patterns)] // this is a one variant enum right now but it will have more
if let ActionRequiredData::ToolConfirmation { id, tool_name, arguments, prompt } = &action.data {
Some((id.clone(), tool_name.clone(), arguments.clone(), prompt.clone()))
} else {
None
}
} else {
None
}
});
let elicitation_request = message.content.iter().find_map(|content| {
if let MessageContent::ActionRequired(action) = content {
if let ActionRequiredData::Elicitation { id, message, requested_schema } = &action.data {
Some((id.clone(), message.clone(), requested_schema.clone()))
} else {
None
}
} else {
None
}
});
if let Some((id, _tool_name, _arguments, security_prompt)) = tool_call_confirmation {
output::hide_thinking();
// Format the confirmation prompt - use security message if present, otherwise use generic message
let prompt = if let Some(security_message) = &security_prompt {
println!("\n{}", security_message);
"Do you allow this tool call?".to_string()
} else {
"Goose would like to call the above tool, do you allow?".to_string()
};
// Get confirmation from user
let permission_result = if security_prompt.is_none() {
// No security message - show all options including "Always Allow"
cliclack::select(prompt)
.item(Permission::AllowOnce, "Allow", "Allow the tool call once")
.item(Permission::AlwaysAllow, "Always Allow", "Always allow the tool call")
.item(Permission::DenyOnce, "Deny", "Deny the tool call")
.item(Permission::Cancel, "Cancel", "Cancel the AI response and tool call")
.interact()
} else {
// Security message present - don't show "Always Allow"
cliclack::select(prompt)
.item(Permission::AllowOnce, "Allow", "Allow the tool call once")
.item(Permission::DenyOnce, "Deny", "Deny the tool call")
.item(Permission::Cancel, "Cancel", "Cancel the AI response and tool call")
.interact()
};
let permission = match permission_result {
Ok(p) => p,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
Permission::Cancel
} else {
return Err(e.into());
}
}
};
if let Some((id, security_prompt)) = find_tool_confirmation(&message) {
let permission = prompt_tool_confirmation(&security_prompt)?;
if permission == Permission::Cancel {
output::render_text("Tool call cancelled. Returning to chat...", Some(Color::Yellow), true);
let mut response_message = Message::user();
response_message.content.push(MessageContent::tool_response(
id.clone(),
Err(ErrorData { code: ErrorCode::INVALID_REQUEST, message: std::borrow::Cow::from("Tool call cancelled by user".to_string()), data: None })
id,
Err(ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: std::borrow::Cow::from("Tool call cancelled by user"),
data: None,
}),
));
self.messages.push(response_message);
cancel_token_clone.cancel();
drop(stream);
break;
} else {
self.agent.handle_confirmation(id.clone(), PermissionConfirmation {
principal_type: PrincipalType::Tool,
permission,
}).await;
}
}
else if let Some((elicitation_id, elicitation_message, schema)) = elicitation_request {
self.agent.handle_confirmation(id, PermissionConfirmation {
principal_type: PrincipalType::Tool,
permission,
}).await;
} else if let Some((elicitation_id, elicitation_message, schema)) = find_elicitation_request(&message) {
output::hide_thinking();
let _ = progress_bars.hide();
@@ -1000,25 +932,16 @@ impl CliSession {
Ok(Some(user_data)) => {
let user_data_value = serde_json::to_value(user_data)
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
let response_message = Message::user()
.with_content(MessageContent::action_required_elicitation_response(
elicitation_id.clone(),
elicitation_id,
user_data_value,
))
.with_visibility(false, true);
self.messages.push(response_message.clone());
// Elicitation responses return an empty stream - the response
// unblocks the waiting tool call via ActionRequiredManager
let _ = self
.agent
.reply(
response_message,
session_config.clone(),
Some(cancel_token.clone()),
)
.await?;
let _ = self.agent.reply(response_message, session_config.clone(), Some(cancel_token.clone())).await?;
}
Ok(None) => {
output::render_text("Information request cancelled.", Some(Color::Yellow), true);
@@ -1033,57 +956,13 @@ impl CliSession {
break;
}
}
}
else {
for content in &message.content {
if let MessageContent::ToolRequest(tool_request) = content {
if let Ok(tool_call) = &tool_request.tool_call {
tracing::info!(counter.goose.tool_calls = 1,
tool_name = %tool_call.name,
"Tool call started"
);
}
}
if let MessageContent::ToolResponse(tool_response) = content {
let tool_name = self.messages
.iter()
.rev()
.find_map(|msg| {
msg.content.iter().find_map(|c| {
if let MessageContent::ToolRequest(req) = c {
if req.id == tool_response.id {
if let Ok(tool_call) = &req.tool_call {
Some(tool_call.name.clone())
} else {
None
}
} else {
None
}
} else {
None
}
})
})
.unwrap_or_else(|| "unknown".to_string().into());
let success = tool_response.tool_result.is_ok();
let result_status = if success { "success" } else { "error" };
tracing::info!(
counter.goose.tool_completions = 1,
tool_name = %tool_name,
result = %result_status,
"Tool call completed"
);
}
}
} else {
log_tool_metrics(&message, &self.messages);
self.messages.push(message.clone());
if interactive {output::hide_thinking()};
if interactive { output::hide_thinking() };
let _ = progress_bars.hide();
// Handle different output formats
if is_stream_json_mode {
emit_stream_event(&StreamEvent::Message { message: message.clone() });
} else if !is_json_mode {
@@ -1091,181 +970,29 @@ impl CliSession {
}
}
}
Some(Ok(AgentEvent::McpNotification((extension_id, message)))) => {
match &message {
ServerNotification::LoggingMessageNotification(notification) => {
let data = &notification.params.data;
let (formatted_message, subagent_id, message_notification_type) = match data {
Value::String(s) => (s.clone(), None, None),
Value::Object(o) => {
// Check for subagent notification structure first
if let Some(Value::String(msg)) = o.get("message") {
// Extract subagent info for better display
let subagent_id = o.get("subagent_id")
.and_then(|v| v.as_str());
let notification_type = o.get("type")
.and_then(|v| v.as_str());
let formatted = match notification_type {
Some("subagent_created") | Some("completed") | Some("terminated") => {
format!("🤖 {}", msg)
}
Some("tool_usage") | Some("tool_completed") | Some("tool_error") => {
format!("🔧 {}", msg)
}
Some("message_processing") | Some("turn_progress") => {
format!("💭 {}", msg)
}
Some("response_generated") => {
// Check verbosity setting for subagent response content
let config = Config::global();
let min_priority = config
.get_param::<f32>("GOOSE_CLI_MIN_PRIORITY")
.ok()
.unwrap_or(0.5);
if min_priority > 0.1 && !self.debug {
// High/Medium verbosity: show truncated response
if let Some(response_content) = msg.strip_prefix("Responded: ") {
format!("🤖 Responded: {}", safe_truncate(response_content, 100))
} else {
format!("🤖 {}", msg)
}
} else {
// All verbosity or debug: show full response
format!("🤖 {}", msg)
}
}
_ => {
msg.to_string()
}
};
(formatted, subagent_id.map(str::to_string), notification_type.map(str::to_string))
} else if let Some(Value::String(output)) = o.get("output") {
// Extract type if present (e.g., "shell_output")
let notification_type = o.get("type")
.and_then(|v| v.as_str())
.map(str::to_string);
(output.to_owned(), None, notification_type)
} else if let Some(result) = format_task_execution_notification(data) {
result
} else {
(data.to_string(), None, None)
}
},
v => {
(v.to_string(), None, None)
},
};
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
else if let Some(_id) = subagent_id {
if interactive {
let _ = progress_bars.hide();
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();
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();
}
} else if notification_type == "shell_output" {
if interactive {
let _ = progress_bars.hide();
}
if !is_json_mode {
println!("{}", formatted_message);
}
}
}
else if output::is_showing_thinking() {
output::set_thinking_message(&formatted_message);
} else {
progress_bars.log(&formatted_message);
}
},
ServerNotification::ProgressNotification(notification) => {
let progress = notification.params.progress;
let text = notification.params.message.as_deref();
let total = notification.params.total;
let token = &notification.params.progress_token;
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,
);
}
},
_ => (),
}
Some(Ok(AgentEvent::McpNotification((extension_id, notification)))) => {
handle_mcp_notification(
&extension_id,
&notification,
&mut progress_bars,
is_stream_json_mode,
interactive,
is_json_mode,
self.debug,
);
}
Some(Ok(AgentEvent::HistoryReplaced(updated_conversation))) => {
self.messages = updated_conversation;
}
Some(Ok(AgentEvent::ModelChange { model, mode })) => {
if is_stream_json_mode {
emit_stream_event(&StreamEvent::ModelChange {
model: model.clone(),
mode: mode.clone(),
});
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)) => {
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) {
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!");
}
if !is_stream_json_mode {
eprintln!("Error: {}", error_msg);
}
handle_agent_error(&e, is_stream_json_mode);
cancel_token_clone.cancel();
drop(stream);
if let Err(e) = self.handle_interrupted_messages(false).await {
@@ -1293,7 +1020,6 @@ impl CliSession {
}
}
// Output based on format
if is_json_mode {
let metadata = match SessionManager::get_session(&self.session_id, false).await {
Ok(session) => JsonMetadata {
@@ -1305,12 +1031,10 @@ impl CliSession {
status: "completed".to_string(),
},
};
let json_output = JsonOutput {
messages: self.messages.messages().to_vec(),
metadata,
};
println!("{}", serde_json::to_string_pretty(&json_output)?);
} else if is_stream_json_mode {
let total_tokens = SessionManager::get_session(&self.session_id, false)
@@ -1642,6 +1366,328 @@ impl CliSession {
}
}
fn emit_stream_event(event: &StreamEvent) {
if let Ok(json) = serde_json::to_string(event) {
println!("{}", json);
}
}
/// Prompt user for tool call confirmation, returns the Permission selected
fn prompt_tool_confirmation(security_prompt: &Option<String>) -> Result<Permission> {
output::hide_thinking();
let prompt = if let Some(security_message) = security_prompt {
println!("\n{}", security_message);
"Do you allow this tool call?".to_string()
} else {
"Goose would like to call the above tool, do you allow?".to_string()
};
let permission_result = if security_prompt.is_none() {
cliclack::select(prompt)
.item(Permission::AllowOnce, "Allow", "Allow the tool call once")
.item(
Permission::AlwaysAllow,
"Always Allow",
"Always allow the tool call",
)
.item(Permission::DenyOnce, "Deny", "Deny the tool call")
.item(
Permission::Cancel,
"Cancel",
"Cancel the AI response and tool call",
)
.interact()
} else {
cliclack::select(prompt)
.item(Permission::AllowOnce, "Allow", "Allow the tool call once")
.item(Permission::DenyOnce, "Deny", "Deny the tool call")
.item(
Permission::Cancel,
"Cancel",
"Cancel the AI response and tool call",
)
.interact()
};
match permission_result {
Ok(p) => Ok(p),
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
Ok(Permission::Cancel)
} else {
Err(e.into())
}
}
}
}
/// Extract tool confirmation request from a message
fn find_tool_confirmation(message: &Message) -> Option<(String, Option<String>)> {
message.content.iter().find_map(|content| {
if let MessageContent::ActionRequired(action) = content {
if let ActionRequiredData::ToolConfirmation { id, prompt, .. } = &action.data {
return Some((id.clone(), prompt.clone()));
}
}
None
})
}
/// Extract elicitation request from a message
fn find_elicitation_request(message: &Message) -> Option<(String, String, Value)> {
message.content.iter().find_map(|content| {
if let MessageContent::ActionRequired(action) = content {
if let ActionRequiredData::Elicitation {
id,
message,
requested_schema,
} = &action.data
{
return Some((id.clone(), message.clone(), requested_schema.clone()));
}
}
None
})
}
/// Handle MCP notification event (logging or progress)
fn handle_mcp_notification(
extension_id: &str,
notification: &ServerNotification,
progress_bars: &mut output::McpSpinners,
is_stream_json_mode: bool,
interactive: bool,
is_json_mode: bool,
debug: bool,
) {
match notification {
ServerNotification::LoggingMessageNotification(log_notif) => {
let (formatted, subagent_id, notif_type) =
format_logging_notification(&log_notif.params.data, debug);
if is_stream_json_mode {
emit_stream_event(&StreamEvent::Notification {
extension_id: extension_id.to_string(),
data: NotificationData::Log {
message: formatted.clone(),
},
});
} else {
display_log_notification(
&formatted,
subagent_id.as_deref(),
notif_type.as_deref(),
progress_bars,
interactive,
is_json_mode,
);
}
}
ServerNotification::ProgressNotification(prog_notif) => {
if is_stream_json_mode {
emit_stream_event(&StreamEvent::Notification {
extension_id: extension_id.to_string(),
data: NotificationData::Progress {
progress: prog_notif.params.progress,
total: prog_notif.params.total,
message: prog_notif.params.message.clone(),
},
});
} else {
progress_bars.update(
&prog_notif.params.progress_token.0.to_string(),
prog_notif.params.progress,
prog_notif.params.total,
prog_notif.params.message.as_deref(),
);
}
}
_ => (),
}
}
/// Format a logging notification from MCP, returns (formatted_message, subagent_id, notification_type)
fn format_logging_notification(
data: &Value,
debug: bool,
) -> (String, Option<String>, Option<String>) {
match data {
Value::String(s) => (s.clone(), None, None),
Value::Object(o) => {
if let Some(Value::String(msg)) = o.get("message") {
let subagent_id = o.get("subagent_id").and_then(|v| v.as_str());
let notification_type = o.get("type").and_then(|v| v.as_str());
let formatted = match notification_type {
Some("subagent_created") | Some("completed") | Some("terminated") => {
format!("🤖 {}", msg)
}
Some("tool_usage") | Some("tool_completed") | Some("tool_error") => {
format!("🔧 {}", msg)
}
Some("message_processing") | Some("turn_progress") => {
format!("💭 {}", msg)
}
Some("response_generated") => {
let config = Config::global();
let min_priority = config
.get_param::<f32>("GOOSE_CLI_MIN_PRIORITY")
.ok()
.unwrap_or(0.5);
if min_priority > 0.1 && !debug {
if let Some(response_content) = msg.strip_prefix("Responded: ") {
format!("🤖 Responded: {}", safe_truncate(response_content, 100))
} else {
format!("🤖 {}", msg)
}
} else {
format!("🤖 {}", msg)
}
}
_ => msg.to_string(),
};
(
formatted,
subagent_id.map(str::to_string),
notification_type.map(str::to_string),
)
} else if let Some(Value::String(output)) = o.get("output") {
let notification_type = o.get("type").and_then(|v| v.as_str()).map(str::to_string);
(output.to_owned(), None, notification_type)
} else if let Some(result) = format_task_execution_notification(data) {
result
} else {
(data.to_string(), None, None)
}
}
v => (v.to_string(), None, None),
}
}
/// Display a logging notification based on its type and context
fn display_log_notification(
formatted_message: &str,
subagent_id: Option<&str>,
notification_type: Option<&str>,
progress_bars: &mut output::McpSpinners,
interactive: bool,
is_json_mode: bool,
) {
if subagent_id.is_some() {
if interactive {
let _ = progress_bars.hide();
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(ntype) = notification_type {
if ntype == TASK_EXECUTION_NOTIFICATION_TYPE {
if interactive {
let _ = progress_bars.hide();
}
if !is_json_mode {
print!("{}", formatted_message);
std::io::stdout().flush().unwrap();
}
} else if ntype == "shell_output" {
if interactive {
let _ = progress_bars.hide();
}
if !is_json_mode {
println!("{}", formatted_message);
}
}
} else if output::is_showing_thinking() {
output::set_thinking_message(&formatted_message.to_string());
} else {
progress_bars.log(formatted_message);
}
}
/// Log tool request/response metrics
fn log_tool_metrics(message: &Message, messages: &Conversation) {
for content in &message.content {
if let MessageContent::ToolRequest(tool_request) = content {
if let Ok(tool_call) = &tool_request.tool_call {
tracing::info!(
counter.goose.tool_calls = 1,
tool_name = %tool_call.name,
"Tool call started"
);
}
}
if let MessageContent::ToolResponse(tool_response) = content {
let tool_name = messages
.iter()
.rev()
.find_map(|msg| {
msg.content.iter().find_map(|c| {
if let MessageContent::ToolRequest(req) = c {
if req.id == tool_response.id {
req.tool_call.as_ref().ok().map(|tc| tc.name.clone())
} else {
None
}
} else {
None
}
})
})
.unwrap_or_else(|| "unknown".to_string().into());
let result_status = if tool_response.tool_result.is_ok() {
"success"
} else {
"error"
};
tracing::info!(
counter.goose.tool_completions = 1,
tool_name = %tool_name,
result = %result_status,
"Tool call completed"
);
}
}
}
/// Handle and display an agent error
fn handle_agent_error(e: &anyhow::Error, is_stream_json_mode: bool) {
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)
{
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!");
}
if !is_stream_json_mode {
eprintln!("Error: {}", error_msg);
}
}
async fn get_reasoner() -> Result<Arc<dyn Provider>, anyhow::Error> {
use goose::model::ModelConfig;
use goose::providers::create;