feat: ActionRequired (#5897)

This commit is contained in:
Alex Hancock
2025-12-02 11:48:28 -05:00
committed by GitHub
parent bf188cd9e2
commit 131c7e7228
22 changed files with 428 additions and 224 deletions
+11 -1
View File
@@ -1,4 +1,6 @@
use goose::conversation::message::{Message, MessageContent, ToolRequest, ToolResponse};
use goose::conversation::message::{
ActionRequiredData, Message, MessageContent, ToolRequest, ToolResponse,
};
use goose::utils::safe_truncate;
use rmcp::model::{RawContent, ResourceContents, Role};
use serde_json::Value;
@@ -340,6 +342,14 @@ pub fn message_to_markdown(message: &Message, export_all_content: bool) -> Strin
let mut md = String::new();
for content in &message.content {
match content {
MessageContent::ActionRequired(action) => match &action.data {
ActionRequiredData::ToolConfirmation { tool_name, .. } => {
md.push_str(&format!(
"**Action Required** (tool_confirmation): {}\n\n",
tool_name
));
}
},
MessageContent::Text(text) => {
md.push_str(&text.text);
md.push_str("\n\n");
+23 -12
View File
@@ -40,7 +40,7 @@ use rmcp::model::ServerNotification;
use rmcp::model::{ErrorCode, ErrorData};
use goose::config::paths::Paths;
use goose::conversation::message::{Message, MessageContent};
use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
use rand::{distributions::Alphanumeric, Rng};
use rustyline::EditMode;
use serde::{Deserialize, Serialize};
@@ -852,12 +852,24 @@ impl CliSession {
result = stream.next() => {
match result {
Some(Ok(AgentEvent::Message(message))) => {
// If it's a confirmation request, get approval but otherwise do not render/persist
if let Some(MessageContent::ToolConfirmationRequest(confirmation)) = message.content.first() {
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
}
});
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) = &confirmation.prompt {
let prompt = if let Some(security_message) = &security_prompt {
println!("\n{}", security_message);
"Do you allow this tool call?".to_string()
} else {
@@ -865,7 +877,7 @@ impl CliSession {
};
// Get confirmation from user
let permission_result = if confirmation.prompt.is_none() {
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")
@@ -883,13 +895,12 @@ impl CliSession {
};
let permission = match permission_result {
Ok(p) => p, // If Ok, use the selected permission
Ok(p) => p,
Err(e) => {
// Check if the error is an interruption (Ctrl+C/Cmd+C, Escape)
if e.kind() == std::io::ErrorKind::Interrupted {
Permission::Cancel // If interrupted, set permission to Cancel
Permission::Cancel
} else {
return Err(e.into()); // Otherwise, convert and propagate the original error
return Err(e.into());
}
}
};
@@ -899,7 +910,7 @@ impl CliSession {
let mut response_message = Message::user();
response_message.content.push(MessageContent::tool_response(
confirmation.id.clone(),
id.clone(),
Err(ErrorData { code: ErrorCode::INVALID_REQUEST, message: std::borrow::Cow::from("Tool call cancelled by user".to_string()), data: None })
));
self.messages.push(response_message);
@@ -907,10 +918,10 @@ impl CliSession {
drop(stream);
break;
} else {
self.agent.handle_confirmation(confirmation.id.clone(), PermissionConfirmation {
self.agent.handle_confirmation(id.clone(), PermissionConfirmation {
principal_type: PrincipalType::Tool,
permission,
},).await;
}).await;
}
}
else {
+8 -1
View File
@@ -2,7 +2,9 @@ use anstream::println;
use bat::WrappingMode;
use console::{measure_text_width, style, Color, Term};
use goose::config::Config;
use goose::conversation::message::{Message, MessageContent, ToolRequest, ToolResponse};
use goose::conversation::message::{
ActionRequiredData, Message, MessageContent, ToolRequest, ToolResponse,
};
use goose::providers::pricing::get_model_pricing;
use goose::providers::pricing::parse_model_id;
use goose::utils::safe_truncate;
@@ -166,6 +168,11 @@ pub fn render_message(message: &Message, debug: bool) {
for content in &message.content {
match content {
MessageContent::ActionRequired(action) => match &action.data {
ActionRequiredData::ToolConfirmation { tool_name, .. } => {
println!("action_required(tool_confirmation): {}", tool_name)
}
},
MessageContent::Text(text) => print_markdown(&text.text, theme),
MessageContent::ToolRequest(req) => render_tool_request(req, theme, debug),
MessageContent::ToolResponse(resp) => render_tool_response(resp, theme, debug),