Lifei/sub recipe desktop temp (#3576)

This commit is contained in:
Lifei Zhou
2025-07-24 23:37:24 +10:00
committed by GitHub
parent a52e11afc1
commit e682940929
14 changed files with 255 additions and 113 deletions
@@ -2,6 +2,7 @@ use goose::agents::subagent_execution_tool::lib::TaskStatus;
use goose::agents::subagent_execution_tool::notification_events::{
TaskExecutionNotificationEvent, TaskInfo,
};
use goose::utils::safe_truncate;
use serde_json::Value;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -18,7 +19,7 @@ static INITIAL_SHOWN: AtomicBool = AtomicBool::new(false);
fn format_result_data_for_display(result_data: &Value) -> String {
match result_data {
Value::String(s) => strip_ansi_codes(s),
Value::String(s) => s.to_string(),
Value::Object(obj) => {
if let Some(partial_output) = obj.get("partial_output").and_then(|v| v.as_str()) {
format!("Partial output: {}", partial_output)
@@ -45,53 +46,7 @@ fn process_output_for_display(output: &str) -> String {
};
let clean_output = recent_lines.join(" ... ");
let stripped = strip_ansi_codes(&clean_output);
truncate_with_ellipsis(&stripped, OUTPUT_PREVIEW_LENGTH)
}
fn truncate_with_ellipsis(text: &str, max_len: usize) -> String {
if text.len() > max_len {
let mut end = max_len.saturating_sub(3);
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
format!("{}...", &text[..end])
} else {
text.to_string()
}
}
fn strip_ansi_codes(text: &str) -> String {
let mut result = String::new();
let mut chars = text.chars();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if let Some(next_ch) = chars.next() {
if next_ch == '[' {
// This is an ANSI escape sequence, consume until alphabetic character
loop {
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => break,
Some(_) => continue,
None => break,
}
}
} else {
// Not an ANSI sequence, keep both characters
result.push(ch);
result.push(next_ch);
}
} else {
// End of string after \x1b
result.push(ch);
}
} else {
result.push(ch);
}
}
result
safe_truncate(&clean_output, OUTPUT_PREVIEW_LENGTH)
}
pub fn format_task_execution_notification(
@@ -233,7 +188,7 @@ fn format_task_display(task: &TaskInfo) -> String {
if matches!(task.status, TaskStatus::Failed) {
if let Some(error) = &task.error {
let error_preview = truncate_with_ellipsis(error, 80);
let error_preview = safe_truncate(error, 80);
task_display.push_str(&format!(
" ⚠️ {}{}\n",
error_preview.replace('\n', " "),
@@ -4,34 +4,6 @@ use goose::agents::subagent_execution_tool::notification_events::{
};
use serde_json::json;
#[test]
fn test_strip_ansi_codes() {
assert_eq!(strip_ansi_codes("hello world"), "hello world");
assert_eq!(strip_ansi_codes("\x1b[31mred text\x1b[0m"), "red text");
assert_eq!(
strip_ansi_codes("\x1b[1;32mbold green\x1b[0m"),
"bold green"
);
assert_eq!(
strip_ansi_codes("normal\x1b[33myellow\x1b[0mnormal"),
"normalyellownormal"
);
assert_eq!(strip_ansi_codes("\x1bhello"), "\x1bhello");
assert_eq!(strip_ansi_codes("hello\x1b"), "hello\x1b");
assert_eq!(strip_ansi_codes(""), "");
}
#[test]
fn test_truncate_with_ellipsis() {
assert_eq!(truncate_with_ellipsis("hello", 10), "hello");
assert_eq!(truncate_with_ellipsis("hello", 5), "hello");
assert_eq!(truncate_with_ellipsis("hello world", 8), "hello...");
assert_eq!(truncate_with_ellipsis("hello", 3), "...");
assert_eq!(truncate_with_ellipsis("hello", 2), "...");
assert_eq!(truncate_with_ellipsis("hello", 1), "...");
assert_eq!(truncate_with_ellipsis("", 5), "");
}
#[test]
fn test_process_output_for_display() {
assert_eq!(process_output_for_display("hello world"), "hello world");
@@ -49,20 +21,15 @@ fn test_process_output_for_display() {
assert!(result.len() <= 100);
assert!(result.ends_with("..."));
let ansi_output = "\x1b[31mred line 1\x1b[0m\n\x1b[32mgreen line 2\x1b[0m";
let result = process_output_for_display(ansi_output);
assert_eq!(result, "red line 1 ... green line 2");
assert_eq!(process_output_for_display(""), "");
}
#[test]
fn test_format_result_data_for_display() {
let string_val = json!("hello world");
assert_eq!(format_result_data_for_display(&string_val), "hello world");
let ansi_string = json!("\x1b[31mred text\x1b[0m");
assert_eq!(format_result_data_for_display(&ansi_string), "red text");
assert_eq!(
format_result_data_for_display(&json!("red text")),
"red text"
);
assert_eq!(format_result_data_for_display(&json!(true)), "true");
assert_eq!(format_result_data_for_display(&json!(false)), "false");
+3
View File
@@ -305,6 +305,7 @@ derive_utoipa!(ResourceContents as ResourceContentsSchema);
super::routes::config_management::providers,
super::routes::config_management::upsert_permissions,
super::routes::agent::get_tools,
super::routes::agent::add_sub_recipes,
super::routes::reply::confirm_permission,
super::routes::context::manage_context,
super::routes::session::list_sessions,
@@ -394,6 +395,8 @@ derive_utoipa!(ResourceContents as ResourceContentsSchema);
goose::recipe::SubRecipe,
goose::agents::types::RetryConfig,
goose::agents::types::SuccessCheck,
super::routes::agent::AddSubRecipesRequest,
super::routes::agent::AddSubRecipesResponse,
))
)]
pub struct ApiDoc;
+36 -1
View File
@@ -6,7 +6,6 @@ use axum::{
routing::{get, post},
Json, Router,
};
use goose::config::Config;
use goose::config::PermissionManager;
use goose::model::ModelConfig;
use goose::providers::create;
@@ -15,6 +14,7 @@ use goose::{
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
config::permission::PermissionLevel,
};
use goose::{config::Config, recipe::SubRecipe};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
@@ -35,6 +35,16 @@ struct ExtendPromptResponse {
success: bool,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct AddSubRecipesRequest {
sub_recipes: Vec<SubRecipe>,
}
#[derive(Serialize, utoipa::ToSchema)]
pub struct AddSubRecipesResponse {
success: bool,
}
#[derive(Deserialize)]
struct ProviderFile {
name: String,
@@ -88,6 +98,30 @@ async fn get_versions() -> Json<VersionsResponse> {
})
}
#[utoipa::path(
post,
path = "/agent/add_sub_recipes",
request_body = AddSubRecipesRequest,
responses(
(status = 200, description = "added sub recipes to agent successfully", body = AddSubRecipesResponse),
(status = 401, description = "Unauthorized - invalid secret key"),
),
)]
async fn add_sub_recipes(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<AddSubRecipesRequest>,
) -> Result<Json<AddSubRecipesResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
agent.add_sub_recipes(payload.sub_recipes.clone()).await;
Ok(Json(AddSubRecipesResponse { success: true }))
}
async fn extend_prompt(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
@@ -318,5 +352,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
post(update_router_tool_selector),
)
.route("/agent/session_config", post(update_session_config))
.route("/agent/add_sub_recipes", post(add_sub_recipes))
.with_state(state)
}
@@ -193,15 +193,7 @@ async fn collect_results(
expected_count: usize,
) -> Vec<TaskResult> {
let mut results = Vec::new();
while let Some(mut result) = result_rx.recv().await {
// Truncate data to 650 chars if needed
if let Some(data) = result.data.as_mut() {
if let Some(data_str) = data.as_str() {
if data_str.len() > 650 {
*data = serde_json::Value::String(format!("{}...", &data_str[..650]));
}
}
}
while let Some(result) = result_rx.recv().await {
task_execution_tracker
.complete_task(&result.task_id, result.clone())
.await;
@@ -8,6 +8,7 @@ use tokio_util::sync::CancellationToken;
use crate::agents::subagent_execution_tool::task_execution_tracker::TaskExecutionTracker;
use crate::agents::subagent_execution_tool::task_types::{Task, TaskResult, TaskStatus};
use crate::agents::subagent_execution_tool::utils::strip_ansi_codes;
use crate::agents::subagent_handler::run_complete_subagent_task;
use crate::agents::subagent_task_config::TaskConfig;
@@ -70,7 +71,7 @@ async fn get_task_result(
if success {
process_output(stdout_output)
} else {
Err(format!("Command failed:\n{}", stderr_output))
Err(format!("Command failed:\n{}", &stderr_output))
}
}
}
@@ -224,6 +225,7 @@ fn spawn_output_reader(
let mut buffer = String::new();
let mut lines = BufReader::new(reader).lines();
while let Ok(Some(line)) = lines.next_line().await {
let line = strip_ansi_codes(&line);
buffer.push_str(&line);
buffer.push('\n');
@@ -23,5 +23,38 @@ pub fn count_by_status(tasks: &HashMap<String, TaskInfo>) -> (usize, usize, usiz
(total, pending, running, completed, failed)
}
pub fn strip_ansi_codes(text: &str) -> String {
let mut result = String::new();
let mut chars = text.chars();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if let Some(next_ch) = chars.next() {
if next_ch == '[' {
// This is an ANSI escape sequence, consume until alphabetic character
loop {
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => break,
Some(_) => continue,
None => break,
}
}
} else {
// Not an ANSI sequence, keep both characters
result.push(ch);
result.push(next_ch);
}
} else {
// End of string after \x1b
result.push(ch);
}
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests;
@@ -1,5 +1,7 @@
use crate::agents::subagent_execution_tool::task_types::{Task, TaskInfo, TaskStatus};
use crate::agents::subagent_execution_tool::utils::{count_by_status, get_task_name};
use crate::agents::subagent_execution_tool::utils::{
count_by_status, get_task_name, strip_ansi_codes,
};
use serde_json::json;
use std::collections::HashMap;
@@ -152,3 +154,24 @@ mod count_by_status {
);
}
}
mod strip_ansi_codes {
use super::*;
#[test]
fn test_strip_ansi_codes() {
assert_eq!(strip_ansi_codes("hello world"), "hello world");
assert_eq!(strip_ansi_codes("\x1b[31mred text\x1b[0m"), "red text");
assert_eq!(
strip_ansi_codes("\x1b[1;32mbold green\x1b[0m"),
"bold green"
);
assert_eq!(
strip_ansi_codes("normal\x1b[33myellow\x1b[0mnormal"),
"normalyellownormal"
);
assert_eq!(strip_ansi_codes("\x1bhello"), "\x1bhello");
assert_eq!(strip_ansi_codes("hello\x1b"), "hello\x1b");
assert_eq!(strip_ansi_codes(""), "");
}
}