Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions clippy-baselines/too_many_lines.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ crates/goose/src/providers/formats/google.rs::format_messages
crates/goose/src/providers/formats/openai.rs::format_messages
crates/goose/src/providers/formats/openai.rs::response_to_streaming_message
crates/goose/src/providers/snowflake.rs::post
crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs::run
2 changes: 2 additions & 0 deletions crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
)
.await;

agent_ptr.ensure_subagent_extension(&session_id).await;

// Determine editor mode
let edit_mode = config
.get_param::<String>("EDIT_MODE")
Expand Down
2 changes: 1 addition & 1 deletion crates/goose-cli/src/session/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) {
"developer__text_editor" => render_text_editor_request(call, debug),
"developer__shell" => render_shell_request(call, debug),
"code_execution__execute_code" => render_execute_code_request(call, debug),
"subagent" => render_subagent_request(call, debug),
"subagent__delegate" => render_subagent_request(call, debug),
"todo__write" => render_todo_request(call, debug),
_ => render_default_request(call, debug),
},
Expand Down
7 changes: 5 additions & 2 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,9 @@ async fn update_from_session(
.await
{
Ok(Some(recipe)) => {
if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await {
if let Some(prompt) =
apply_recipe_to_agent(&agent, &recipe, &payload.session_id, true).await
{
update_prompt = prompt;
}
}
Expand Down Expand Up @@ -700,7 +702,8 @@ async fn restart_agent_internal(
.await
{
Ok(Some(recipe)) => {
if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await {
if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, session_id, true).await
{
update_prompt = prompt;
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-server/src/routes/recipe_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ pub async fn build_recipe_with_parameter_values(
pub async fn apply_recipe_to_agent(
agent: &Arc<Agent>,
recipe: &Recipe,
session_id: &str,
include_final_output_tool: bool,
) -> Option<String> {
agent
Expand All @@ -170,6 +171,8 @@ pub async fn apply_recipe_to_agent(
)
.await;

agent.ensure_subagent_extension(session_id).await;

recipe.instructions.as_ref().map(|instructions| {
let mut context: HashMap<&str, Value> = HashMap::new();
context.insert("recipe_instructions", Value::String(instructions.clone()));
Expand Down
2 changes: 1 addition & 1 deletion crates/goose-server/src/routes/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ async fn update_session_user_recipe_values(
message: format!("Failed to get agent: {}", status),
status,
})?;
if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, false).await {
if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, &session_id, false).await {
agent.extend_system_prompt(prompt).await;
}
Ok(Json(UpdateSessionUserRecipeValuesResponse { recipe }))
Expand Down
147 changes: 67 additions & 80 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use uuid::Uuid;

use super::final_output_tool::FinalOutputTool;
use super::platform_tools;
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
use super::tool_execution::{DeferredToolCall, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
use crate::action_required_manager::ActionRequiredManager;
use crate::agents::extension::{ExtensionConfig, ExtensionResult, ToolInfo};
use crate::agents::extension_manager::{get_parameter_names, normalize, ExtensionManager};
Expand All @@ -19,13 +19,10 @@ use crate::agents::final_output_tool::{FINAL_OUTPUT_CONTINUATION_MESSAGE, FINAL_
use crate::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME;
use crate::agents::prompt_manager::PromptManager;
use crate::agents::retry::{RetryManager, RetryResult};
use crate::agents::subagent_task_config::TaskConfig;
use crate::agents::subagent_tool::{
create_subagent_tool, handle_subagent_tool, SUBAGENT_TOOL_NAME,
};
use crate::agents::subagent_client;
use crate::agents::types::{FrontendTool, SessionConfig, SharedProvider, ToolResultReceiver};
use crate::config::permission::PermissionManager;
use crate::config::{get_enabled_extensions, Config, GooseMode};
use crate::config::{get_enabled_extensions, is_extension_enabled, Config, GooseMode};
use crate::context_mgmt::{
check_if_compaction_needed, compact_messages, DEFAULT_COMPACTION_THRESHOLD,
};
Expand Down Expand Up @@ -116,7 +113,7 @@ pub struct Agent {
pub config: AgentConfig,

pub extension_manager: Arc<ExtensionManager>,
pub(super) sub_recipes: Mutex<HashMap<String, SubRecipe>>,
pub(super) sub_recipes: Arc<tokio::sync::RwLock<HashMap<String, SubRecipe>>>,
pub(super) final_output_tool: Arc<Mutex<Option<FinalOutputTool>>>,
pub(super) frontend_tools: Mutex<HashMap<String, FrontendTool>>,
pub(super) frontend_instructions: Mutex<Option<String>>,
Expand Down Expand Up @@ -197,11 +194,18 @@ impl Agent {

let session_manager = Arc::clone(&config.session_manager);
let permission_manager = Arc::clone(&config.permission_manager);
let goose_mode = config.goose_mode;
let sub_recipes = Arc::new(tokio::sync::RwLock::new(HashMap::new()));
Self {
provider: provider.clone(),
config,
extension_manager: Arc::new(ExtensionManager::new(provider.clone(), session_manager)),
sub_recipes: Mutex::new(HashMap::new()),
extension_manager: Arc::new(ExtensionManager::new(
provider.clone(),
session_manager,
Some(sub_recipes.clone()),
goose_mode,
)),
sub_recipes,
final_output_tool: Arc::new(Mutex::new(None)),
frontend_tools: Mutex::new(HashMap::new()),
frontend_instructions: Mutex::new(None),
Expand Down Expand Up @@ -340,10 +344,10 @@ impl Agent {

async fn handle_approved_and_denied_tools(
&self,
session_id: &str,
permission_check_result: &PermissionCheckResult,
request_to_response_map: &HashMap<String, Arc<Mutex<Message>>>,
cancel_token: Option<tokio_util::sync::CancellationToken>,
session: &Session,
) -> Result<Vec<(String, ToolStream)>> {
let mut tool_futures: Vec<(String, ToolStream)> = Vec::new();

Expand All @@ -352,10 +356,10 @@ impl Agent {
if let Ok(tool_call) = request.tool_call.clone() {
let (req_id, tool_result) = self
.dispatch_tool_call(
session_id,
tool_call,
request.id.clone(),
cancel_token.clone(),
session,
)
.await;

Expand Down Expand Up @@ -427,8 +431,12 @@ impl Agent {
self.extend_system_prompt(final_output_system_prompt).await;
}

pub fn sub_recipes(&self) -> Arc<tokio::sync::RwLock<HashMap<String, SubRecipe>>> {
self.sub_recipes.clone()
}

pub async fn add_sub_recipes(&self, sub_recipes_to_add: Vec<SubRecipe>) {
let mut sub_recipes = self.sub_recipes.lock().await;
let mut sub_recipes = self.sub_recipes.write().await;
for sr in sub_recipes_to_add {
sub_recipes.insert(sr.name.clone(), sr);
}
Expand All @@ -440,8 +448,8 @@ impl Agent {
response: Option<Response>,
include_final_output: bool,
) {
if let Some(sub_recipes) = sub_recipes {
self.add_sub_recipes(sub_recipes).await;
if let Some(ref sub_recipes) = sub_recipes {
self.add_sub_recipes(sub_recipes.clone()).await;
}

if include_final_output {
Expand All @@ -451,27 +459,45 @@ impl Agent {
}
}

pub async fn ensure_subagent_extension(&self, session_id: &str) {
if !self.subagents_enabled(session_id).await {
return;
}

if self
.extension_manager
.is_extension_enabled(subagent_client::EXTENSION_NAME)
.await
{
return;
}

if let Err(e) = self
.extension_manager
.add_extension_with_working_dir(
ExtensionConfig::Platform {
name: subagent_client::EXTENSION_NAME.to_string(),
description: "Delegate tasks to independent subagents".to_string(),
bundled: Some(true),
available_tools: vec![],
},
None,
)
.await
{
warn!("Failed to enable subagent extension: {}", e);
}
}

/// Dispatch a single tool call to the appropriate client
#[instrument(skip(self, tool_call, request_id), fields(input, output))]
#[instrument(skip(self, session_id, tool_call, request_id), fields(input, output))]
pub async fn dispatch_tool_call(
&self,
session_id: &str,
tool_call: CallToolRequestParams,
request_id: String,
cancellation_token: Option<CancellationToken>,
session: &Session,
) -> (String, Result<ToolCallResult, ErrorData>) {
// Prevent subagents from creating other subagents
if session.session_type == SessionType::SubAgent && tool_call.name == SUBAGENT_TOOL_NAME {
return (
request_id,
Err(ErrorData::new(
ErrorCode::INVALID_REQUEST,
"Subagents cannot create other subagents".to_string(),
None,
)),
);
}

) -> (String, Result<DeferredToolCall, ErrorData>) {
if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME {
let arguments = tool_call
.arguments
Expand All @@ -486,7 +512,7 @@ impl Agent {
is_error: Some(false),
meta: None,
});
return (request_id, Ok(ToolCallResult::from(wrapped_result)));
return (request_id, Ok(DeferredToolCall::from(wrapped_result)));
}

if tool_call.name == FINAL_OUTPUT_TOOL_NAME {
Expand All @@ -506,53 +532,18 @@ impl Agent {
}

debug!("WAITING_TOOL_START: {}", tool_call.name);
let result: ToolCallResult = if tool_call.name == SUBAGENT_TOOL_NAME {
let provider = match self.provider().await {
Ok(p) => p,
Err(_) => {
return (
request_id,
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Provider is required".to_string(),
None,
)),
);
}
};

let extensions = self.get_extension_configs().await;
let task_config =
TaskConfig::new(provider, &session.id, &session.working_dir, extensions);
let sub_recipes = self.sub_recipes.lock().await.clone();

let arguments = tool_call
.arguments
.clone()
.map(Value::Object)
.unwrap_or(Value::Object(serde_json::Map::new()));

handle_subagent_tool(
&self.config,
arguments,
task_config,
sub_recipes,
session.working_dir.clone(),
cancellation_token,
)
} else if self.is_frontend_tool(&tool_call.name).await {
// For frontend tools, return an error indicating we need frontend execution
ToolCallResult::from(Err(ErrorData::new(
// Note: SUBAGENT_TOOL_NAME is now handled as a platform extension, not here
let result: DeferredToolCall = if self.is_frontend_tool(&tool_call.name).await {
DeferredToolCall::from(Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Frontend tool execution required".to_string(),
None,
)))
} else {
// Clone the result to ensure no references to extension_manager are returned
let result = self
.extension_manager
.dispatch_tool_call(
&session.id,
session_id,
tool_call.clone(),
cancellation_token.unwrap_or_default(),
)
Expand All @@ -566,15 +557,15 @@ impl Agent {
let error_data = e.downcast::<ErrorData>().unwrap_or_else(|e| {
ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None)
});
ToolCallResult::from(Err(error_data))
DeferredToolCall::from(Err(error_data))
})
};

debug!("WAITING_TOOL_END: {}", tool_call.name);

(
request_id,
Ok(ToolCallResult {
Ok(DeferredToolCall {
notification_stream: result.notification_stream,
result: Box::new(
result
Expand Down Expand Up @@ -750,6 +741,9 @@ impl Agent {
}

pub async fn subagents_enabled(&self, session_id: &str) -> bool {
if !is_extension_enabled(subagent_client::EXTENSION_NAME) {
return false;
}
if self.config.goose_mode != GooseMode::Auto {
return false;
}
Expand Down Expand Up @@ -788,7 +782,6 @@ impl Agent {
.await
.unwrap_or_default();

let subagents_enabled = self.subagents_enabled(session_id).await;
if (extension_name.is_none() || extension_name.as_deref() == Some("platform"))
&& self.config.scheduler_service.is_some()
{
Expand All @@ -799,12 +792,6 @@ impl Agent {
if let Some(final_output_tool) = self.final_output_tool.lock().await.as_ref() {
prefixed_tools.push(final_output_tool.tool());
}

if subagents_enabled {
let sub_recipes = self.sub_recipes.lock().await;
let sub_recipes_vec: Vec<_> = sub_recipes.values().cloned().collect();
prefixed_tools.push(create_subagent_tool(&sub_recipes_vec));
}
}

prefixed_tools
Expand Down Expand Up @@ -1252,20 +1239,20 @@ impl Agent {
}

let mut tool_futures = self.handle_approved_and_denied_tools(
&session_config.id,
&permission_check_result,
&request_to_response_map,
cancel_token.clone(),
&session,
).await?;

let tool_futures_arc = Arc::new(Mutex::new(tool_futures));

let mut tool_approval_stream = self.handle_approval_tool_requests(
&session_config.id,
&permission_check_result.needs_approval,
tool_futures_arc.clone(),
&request_to_response_map,
cancel_token.clone(),
&session,
&inspection_results,
);

Expand Down
Loading
Loading