From 2897ea1bfb4ec1e789f53adad2229958d4839ee0 Mon Sep 17 00:00:00 2001 From: Jeff Agapitos <233853744+jeffa-block@users.noreply.github.com> Date: Sat, 30 May 2026 13:44:10 +1000 Subject: [PATCH 1/6] feat(summon): add context parameter for delegate knowledge injection Delegates can now receive reference context that is injected into their system prompt under a '# Reference Context' heading, separate from the task instructions (prompt). This addresses the most common delegation failure mode: subagents lacking background information that the parent has but cannot transfer. Usage: delegate( instructions: "Review this module for security issues", context: "This codebase uses axum 0.7, auth is handled by...", ) The context appears before any source-provided instructions in the subagent's system prompt, giving it reference material without conflating it with the task directive. --- .../src/agents/platform_extensions/summon.rs | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 2c67d38dd716..01c05ee0da96 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1,17 +1,17 @@ +use crate::agents::AgentConfig; use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; -use crate::agents::subagent_handler::{run_subagent_task, OnMessageCallback, SubagentRunParams}; -use crate::agents::subagent_task_config::{TaskConfig, DEFAULT_SUBAGENT_MAX_TURNS}; +use crate::agents::subagent_handler::{OnMessageCallback, SubagentRunParams, run_subagent_task}; +use crate::agents::subagent_task_config::{DEFAULT_SUBAGENT_MAX_TURNS, TaskConfig}; use crate::agents::tool_execution::ToolCallContext; -use crate::agents::AgentConfig; use crate::config::paths::Paths; use crate::config::{Config, GooseMode}; use crate::providers; use crate::recipe::build_recipe::build_recipe_from_template; use crate::recipe::local_recipes::load_local_recipe_file; -use crate::recipe::{Recipe, RecipeParameter, Settings, RECIPE_FILE_EXTENSIONS}; -use crate::session::extension_data::EnabledExtensionsState; +use crate::recipe::{RECIPE_FILE_EXTENSIONS, Recipe, RecipeParameter, Settings}; use crate::session::SessionType; +use crate::session::extension_data::EnabledExtensionsState; use crate::sources::parse_frontmatter; use crate::utils::safe_truncate; use anyhow::Result; @@ -24,10 +24,10 @@ use rmcp::model::{ use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -58,6 +58,7 @@ pub struct DelegateParams { pub model: Option, pub temperature: Option, pub max_turns: Option, + pub context: Option, #[serde(default)] pub r#async: bool, } @@ -534,6 +535,10 @@ impl SummonClient { "minimum": 1, "description": "Maximum turns for this delegate. Overrides recipe settings.max_turns and GOOSE_SUBAGENT_MAX_TURNS." }, + "context": { + "type": "string", + "description": "Reference context to inject into the delegate's system prompt. Use for background information, file contents, or constraints the delegate needs but that aren't part of the task instructions." + }, "async": { "type": "boolean", "default": false, @@ -1167,12 +1172,22 @@ impl SummonClient { session_id: &str, working_dir: &Path, ) -> Result { - if let Some(source_name) = ¶ms.source { + let mut recipe = if let Some(source_name) = ¶ms.source { self.build_source_recipe(source_name, params, session_id, working_dir) - .await + .await? } else { - self.build_adhoc_recipe(params) + self.build_adhoc_recipe(params)? + }; + + if let Some(ref context) = params.context { + let existing = recipe.instructions.unwrap_or_default(); + recipe.instructions = Some(format!( + "# Reference Context\n\n{}\n\n{}", + context, existing + )); } + + Ok(recipe) } fn build_adhoc_recipe(&self, params: &DelegateParams) -> Result { @@ -1212,7 +1227,7 @@ impl SummonClient { return Err(format!( "Source '{}' has kind '{}' which cannot be delegated from summon", source_name, source.source_type - )) + )); } }; @@ -2428,11 +2443,13 @@ You review code."#; assert!(text.contains("5 turns")); assert!(text.contains("Task completed successfully with output")); - assert!(!client - .completed_tasks - .lock() - .await - .contains_key("20260204_2")); + assert!( + !client + .completed_tasks + .lock() + .await + .contains_key("20260204_2") + ); let result = client .handle_load_task_result("20260204_3", false) @@ -2484,10 +2501,12 @@ You review code."#; assert!(text.contains("20260204_1")); assert!(text.contains("Cancellable task")); assert!(token.is_cancelled()); - assert!(!client - .background_tasks - .lock() - .await - .contains_key("20260204_1")); + assert!( + !client + .background_tasks + .lock() + .await + .contains_key("20260204_1") + ); } } From 7249c7e3104d568591baf54d340cf877e7de7e55 Mon Sep 17 00:00:00 2001 From: Jeff Agapitos <233853744+jeffa-block@users.noreply.github.com> Date: Sat, 30 May 2026 21:19:29 +1000 Subject: [PATCH 2/6] style: apply cargo fmt import ordering --- .../src/agents/platform_extensions/summon.rs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 01c05ee0da96..06b2203f9262 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1,17 +1,17 @@ -use crate::agents::AgentConfig; use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; -use crate::agents::subagent_handler::{OnMessageCallback, SubagentRunParams, run_subagent_task}; -use crate::agents::subagent_task_config::{DEFAULT_SUBAGENT_MAX_TURNS, TaskConfig}; +use crate::agents::subagent_handler::{run_subagent_task, OnMessageCallback, SubagentRunParams}; +use crate::agents::subagent_task_config::{TaskConfig, DEFAULT_SUBAGENT_MAX_TURNS}; use crate::agents::tool_execution::ToolCallContext; +use crate::agents::AgentConfig; use crate::config::paths::Paths; use crate::config::{Config, GooseMode}; use crate::providers; use crate::recipe::build_recipe::build_recipe_from_template; use crate::recipe::local_recipes::load_local_recipe_file; -use crate::recipe::{RECIPE_FILE_EXTENSIONS, Recipe, RecipeParameter, Settings}; -use crate::session::SessionType; +use crate::recipe::{Recipe, RecipeParameter, Settings, RECIPE_FILE_EXTENSIONS}; use crate::session::extension_data::EnabledExtensionsState; +use crate::session::SessionType; use crate::sources::parse_frontmatter; use crate::utils::safe_truncate; use anyhow::Result; @@ -24,10 +24,10 @@ use rmcp::model::{ use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{Mutex, mpsc}; +use tokio::sync::{mpsc, Mutex}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -2443,13 +2443,11 @@ You review code."#; assert!(text.contains("5 turns")); assert!(text.contains("Task completed successfully with output")); - assert!( - !client - .completed_tasks - .lock() - .await - .contains_key("20260204_2") - ); + assert!(!client + .completed_tasks + .lock() + .await + .contains_key("20260204_2")); let result = client .handle_load_task_result("20260204_3", false) @@ -2501,12 +2499,10 @@ You review code."#; assert!(text.contains("20260204_1")); assert!(text.contains("Cancellable task")); assert!(token.is_cancelled()); - assert!( - !client - .background_tasks - .lock() - .await - .contains_key("20260204_1") - ); + assert!(!client + .background_tasks + .lock() + .await + .contains_key("20260204_1")); } } From 6691499b075551c7d4e8ed3f674e32976bba54d8 Mon Sep 17 00:00:00 2001 From: Jeff Agapitos <233853744+jeffa-block@users.noreply.github.com> Date: Sat, 30 May 2026 21:51:41 +1000 Subject: [PATCH 3/6] fix: add heading separator for existing instructions when context is injected --- .../src/agents/platform_extensions/summon.rs | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 06b2203f9262..46b1d71b22ac 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1,17 +1,17 @@ +use crate::agents::AgentConfig; use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; -use crate::agents::subagent_handler::{run_subagent_task, OnMessageCallback, SubagentRunParams}; -use crate::agents::subagent_task_config::{TaskConfig, DEFAULT_SUBAGENT_MAX_TURNS}; +use crate::agents::subagent_handler::{OnMessageCallback, SubagentRunParams, run_subagent_task}; +use crate::agents::subagent_task_config::{DEFAULT_SUBAGENT_MAX_TURNS, TaskConfig}; use crate::agents::tool_execution::ToolCallContext; -use crate::agents::AgentConfig; use crate::config::paths::Paths; use crate::config::{Config, GooseMode}; use crate::providers; use crate::recipe::build_recipe::build_recipe_from_template; use crate::recipe::local_recipes::load_local_recipe_file; -use crate::recipe::{Recipe, RecipeParameter, Settings, RECIPE_FILE_EXTENSIONS}; -use crate::session::extension_data::EnabledExtensionsState; +use crate::recipe::{RECIPE_FILE_EXTENSIONS, Recipe, RecipeParameter, Settings}; use crate::session::SessionType; +use crate::session::extension_data::EnabledExtensionsState; use crate::sources::parse_frontmatter; use crate::utils::safe_truncate; use anyhow::Result; @@ -24,10 +24,10 @@ use rmcp::model::{ use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -1181,9 +1181,14 @@ impl SummonClient { if let Some(ref context) = params.context { let existing = recipe.instructions.unwrap_or_default(); + let task_section = if existing.is_empty() { + String::new() + } else { + format!("# Task Instructions\n\n{}", existing) + }; recipe.instructions = Some(format!( "# Reference Context\n\n{}\n\n{}", - context, existing + context, task_section )); } @@ -2443,11 +2448,13 @@ You review code."#; assert!(text.contains("5 turns")); assert!(text.contains("Task completed successfully with output")); - assert!(!client - .completed_tasks - .lock() - .await - .contains_key("20260204_2")); + assert!( + !client + .completed_tasks + .lock() + .await + .contains_key("20260204_2") + ); let result = client .handle_load_task_result("20260204_3", false) @@ -2499,10 +2506,12 @@ You review code."#; assert!(text.contains("20260204_1")); assert!(text.contains("Cancellable task")); assert!(token.is_cancelled()); - assert!(!client - .background_tasks - .lock() - .await - .contains_key("20260204_1")); + assert!( + !client + .background_tasks + .lock() + .await + .contains_key("20260204_1") + ); } } From 35967effc71b503c892a762e6fa95f81f6fa2c0a Mon Sep 17 00:00:00 2001 From: Jeff Agapitos <233853744+jeffa-block@users.noreply.github.com> Date: Sun, 31 May 2026 01:48:03 +1000 Subject: [PATCH 4/6] style: apply cargo fmt --all (import ordering + assert formatting) --- .../src/agents/platform_extensions/summon.rs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 46b1d71b22ac..8c23073c765c 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1,17 +1,17 @@ -use crate::agents::AgentConfig; use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; -use crate::agents::subagent_handler::{OnMessageCallback, SubagentRunParams, run_subagent_task}; -use crate::agents::subagent_task_config::{DEFAULT_SUBAGENT_MAX_TURNS, TaskConfig}; +use crate::agents::subagent_handler::{run_subagent_task, OnMessageCallback, SubagentRunParams}; +use crate::agents::subagent_task_config::{TaskConfig, DEFAULT_SUBAGENT_MAX_TURNS}; use crate::agents::tool_execution::ToolCallContext; +use crate::agents::AgentConfig; use crate::config::paths::Paths; use crate::config::{Config, GooseMode}; use crate::providers; use crate::recipe::build_recipe::build_recipe_from_template; use crate::recipe::local_recipes::load_local_recipe_file; -use crate::recipe::{RECIPE_FILE_EXTENSIONS, Recipe, RecipeParameter, Settings}; -use crate::session::SessionType; +use crate::recipe::{Recipe, RecipeParameter, Settings, RECIPE_FILE_EXTENSIONS}; use crate::session::extension_data::EnabledExtensionsState; +use crate::session::SessionType; use crate::sources::parse_frontmatter; use crate::utils::safe_truncate; use anyhow::Result; @@ -24,10 +24,10 @@ use rmcp::model::{ use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{Mutex, mpsc}; +use tokio::sync::{mpsc, Mutex}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -2448,13 +2448,11 @@ You review code."#; assert!(text.contains("5 turns")); assert!(text.contains("Task completed successfully with output")); - assert!( - !client - .completed_tasks - .lock() - .await - .contains_key("20260204_2") - ); + assert!(!client + .completed_tasks + .lock() + .await + .contains_key("20260204_2")); let result = client .handle_load_task_result("20260204_3", false) @@ -2506,12 +2504,10 @@ You review code."#; assert!(text.contains("20260204_1")); assert!(text.contains("Cancellable task")); assert!(token.is_cancelled()); - assert!( - !client - .background_tasks - .lock() - .await - .contains_key("20260204_1") - ); + assert!(!client + .background_tasks + .lock() + .await + .contains_key("20260204_1")); } } From d2f0532c0264e464722862e9fecd6276282ad675 Mon Sep 17 00:00:00 2001 From: Douwe M Osinga Date: Mon, 15 Jun 2026 15:43:20 -0400 Subject: [PATCH 5/6] refactor: extract reference-context helper and add tests Pull the context-injection wrapping into a pure prepend_reference_context helper, drop the trailing newline when there are no existing instructions, and add tests for the ad-hoc injection path and the wrapping logic. Signed-off-by: Douwe M Osinga --- .../src/agents/platform_extensions/summon.rs | 56 ++++++++++++++++--- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 734ff032c8d5..2d3f230c5578 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -286,6 +286,17 @@ pub fn discover_filesystem_sources(working_dir: &Path) -> Vec { sources } +fn prepend_reference_context(context: &str, instructions: &str) -> String { + if instructions.is_empty() { + format!("# Reference Context\n\n{}", context) + } else { + format!( + "# Reference Context\n\n{}\n\n# Task Instructions\n\n{}", + context, instructions + ) + } +} + fn build_subagent_instructions(session: Option<&crate::session::Session>) -> String { let Some(session) = session else { return String::new(); @@ -1181,15 +1192,7 @@ impl SummonClient { if let Some(ref context) = params.context { let existing = recipe.instructions.unwrap_or_default(); - let task_section = if existing.is_empty() { - String::new() - } else { - format!("# Task Instructions\n\n{}", existing) - }; - recipe.instructions = Some(format!( - "# Reference Context\n\n{}\n\n{}", - context, task_section - )); + recipe.instructions = Some(prepend_reference_context(context, &existing)); } Ok(recipe) @@ -2107,6 +2110,41 @@ You review code."#; ); } + #[tokio::test] + async fn test_context_injected_into_adhoc_recipe() { + let temp_dir = TempDir::new().unwrap(); + let client = SummonClient::new(create_test_context()).unwrap(); + + let params = DelegateParams { + instructions: Some("do the task".to_string()), + context: Some("background info".to_string()), + ..Default::default() + }; + + let recipe = client + .build_delegate_recipe(¶ms, "test", temp_dir.path()) + .await + .unwrap(); + + assert_eq!( + recipe.instructions.as_deref(), + Some("# Reference Context\n\nbackground info") + ); + assert_eq!(recipe.prompt.as_deref(), Some("do the task")); + } + + #[test] + fn test_prepend_reference_context_wraps_existing_instructions() { + assert_eq!( + prepend_reference_context("background info", "Run deploy steps"), + "# Reference Context\n\nbackground info\n\n# Task Instructions\n\nRun deploy steps" + ); + assert_eq!( + prepend_reference_context("background info", ""), + "# Reference Context\n\nbackground info" + ); + } + #[test] fn test_validate_delegate_params_rejects_zero_max_turns() { let context = create_test_context(); From 793170007c45aa1875a7d2b0d17416f133ef3a61 Mon Sep 17 00:00:00 2001 From: Douwe M Osinga Date: Mon, 15 Jun 2026 15:58:21 -0400 Subject: [PATCH 6/6] refactor: clarify context-injection helper Rename to build_instructions_with_context (it constructs the full system prompt rather than prepending) and build it incrementally to remove the duplicated reference-context formatting between branches. Signed-off-by: Douwe M Osinga --- .../src/agents/platform_extensions/summon.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 2d3f230c5578..29419660247e 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -286,15 +286,12 @@ pub fn discover_filesystem_sources(working_dir: &Path) -> Vec { sources } -fn prepend_reference_context(context: &str, instructions: &str) -> String { - if instructions.is_empty() { - format!("# Reference Context\n\n{}", context) - } else { - format!( - "# Reference Context\n\n{}\n\n# Task Instructions\n\n{}", - context, instructions - ) +fn build_instructions_with_context(context: &str, instructions: &str) -> String { + let mut result = format!("# Reference Context\n\n{}", context); + if !instructions.is_empty() { + result.push_str(&format!("\n\n# Task Instructions\n\n{}", instructions)); } + result } fn build_subagent_instructions(session: Option<&crate::session::Session>) -> String { @@ -1192,7 +1189,7 @@ impl SummonClient { if let Some(ref context) = params.context { let existing = recipe.instructions.unwrap_or_default(); - recipe.instructions = Some(prepend_reference_context(context, &existing)); + recipe.instructions = Some(build_instructions_with_context(context, &existing)); } Ok(recipe) @@ -2134,13 +2131,13 @@ You review code."#; } #[test] - fn test_prepend_reference_context_wraps_existing_instructions() { + fn test_build_instructions_with_context_wraps_existing_instructions() { assert_eq!( - prepend_reference_context("background info", "Run deploy steps"), + build_instructions_with_context("background info", "Run deploy steps"), "# Reference Context\n\nbackground info\n\n# Task Instructions\n\nRun deploy steps" ); assert_eq!( - prepend_reference_context("background info", ""), + build_instructions_with_context("background info", ""), "# Reference Context\n\nbackground info" ); }