feat: agent-to-agent handoff and delegation
* feat: add agent-to-agent delegation tool Add `delegate` tool enabling multi-agent workflows where a primary agent can hand off subtasks to specialized sub-agents with different provider/model configurations. - New `DelegateAgentConfig` in config schema with provider, model, system_prompt, api_key, temperature, and max_depth fields - `delegate` tool with recursion depth limits to prevent infinite loops - Agents configured via `[agents.<name>]` TOML sections - Sub-agents use `ReliableProvider` with fallback API key support - Backward-compatible: empty agents map when section is absent Closes #218 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: encrypt agent API keys and tighten delegation input validation Address CodeRabbit review comments on PR #224: 1. Agent API key encryption (schema.rs): - Config::load_or_init() now decrypts agents.*.api_key via SecretStore - Config::save() encrypts plaintext agent API keys before writing - Updated doc comment to document encryption behavior - Added tests for encrypt-on-save and plaintext-when-disabled 2. Delegation input validation (delegate.rs): - Added "additionalProperties": false to schema - Added "minLength": 1 for agent and prompt fields - Trim agent/prompt/context inputs, reject empty after trim - Added tests for blank agent, blank prompt, whitespace trimming Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(delegate): replace mutable depth counter with immutable field - Replace `current_depth: Arc<AtomicU32>` with `depth: u32` set at construction time, eliminating TOCTOU race and cancel/panic safety issues from fetch_add/fetch_sub pattern - When sub-agents get their own tool registry, construct via `with_depth(agents, key, parent.depth + 1)` for proper propagation - Add tokio::time::timeout (120s) around provider calls to prevent indefinite blocking from misbehaving sub-agent providers - Rename misleading test whitespace_agent_name_not_found → whitespace_agent_name_trimmed_and_found Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix rustfmt formatting issues Fixed all formatting issues reported by cargo fmt to pass CI lint checks. - Line length adjustments - Chain formatting consistency - Trailing whitespace cleanup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Edvard <ecschoye@stud.ntnu.no> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e04e7191ac
commit
c8ca6ff059
6 changed files with 764 additions and 7 deletions
|
|
@ -1,6 +1,7 @@
|
|||
pub mod browser;
|
||||
pub mod browser_open;
|
||||
pub mod composio;
|
||||
pub mod delegate;
|
||||
pub mod file_read;
|
||||
pub mod file_write;
|
||||
pub mod image_info;
|
||||
|
|
@ -14,6 +15,7 @@ pub mod traits;
|
|||
pub use browser::BrowserTool;
|
||||
pub use browser_open::BrowserOpenTool;
|
||||
pub use composio::ComposioTool;
|
||||
pub use delegate::DelegateTool;
|
||||
pub use file_read::FileReadTool;
|
||||
pub use file_write::FileWriteTool;
|
||||
pub use image_info::ImageInfoTool;
|
||||
|
|
@ -26,9 +28,11 @@ pub use traits::Tool;
|
|||
#[allow(unused_imports)]
|
||||
pub use traits::{ToolResult, ToolSpec};
|
||||
|
||||
use crate::config::DelegateAgentConfig;
|
||||
use crate::memory::Memory;
|
||||
use crate::runtime::{NativeRuntime, RuntimeAdapter};
|
||||
use crate::security::SecurityPolicy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Create the default tool registry
|
||||
|
|
@ -54,6 +58,8 @@ pub fn all_tools(
|
|||
memory: Arc<dyn Memory>,
|
||||
composio_key: Option<&str>,
|
||||
browser_config: &crate::config::BrowserConfig,
|
||||
agents: &HashMap<String, DelegateAgentConfig>,
|
||||
fallback_api_key: Option<&str>,
|
||||
) -> Vec<Box<dyn Tool>> {
|
||||
all_tools_with_runtime(
|
||||
security,
|
||||
|
|
@ -61,6 +67,8 @@ pub fn all_tools(
|
|||
memory,
|
||||
composio_key,
|
||||
browser_config,
|
||||
agents,
|
||||
fallback_api_key,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +79,8 @@ pub fn all_tools_with_runtime(
|
|||
memory: Arc<dyn Memory>,
|
||||
composio_key: Option<&str>,
|
||||
browser_config: &crate::config::BrowserConfig,
|
||||
agents: &HashMap<String, DelegateAgentConfig>,
|
||||
fallback_api_key: Option<&str>,
|
||||
) -> Vec<Box<dyn Tool>> {
|
||||
let mut tools: Vec<Box<dyn Tool>> = vec![
|
||||
Box::new(ShellTool::new(security.clone(), runtime)),
|
||||
|
|
@ -105,6 +115,14 @@ pub fn all_tools_with_runtime(
|
|||
}
|
||||
}
|
||||
|
||||
// Add delegation tool when agents are configured
|
||||
if !agents.is_empty() {
|
||||
tools.push(Box::new(DelegateTool::new(
|
||||
agents.clone(),
|
||||
fallback_api_key.map(String::from),
|
||||
)));
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +156,7 @@ mod tests {
|
|||
session_name: None,
|
||||
};
|
||||
|
||||
let tools = all_tools(&security, mem, None, &browser);
|
||||
let tools = all_tools(&security, mem, None, &browser, &HashMap::new(), None);
|
||||
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
|
||||
assert!(!names.contains(&"browser_open"));
|
||||
}
|
||||
|
|
@ -160,7 +178,7 @@ mod tests {
|
|||
session_name: None,
|
||||
};
|
||||
|
||||
let tools = all_tools(&security, mem, None, &browser);
|
||||
let tools = all_tools(&security, mem, None, &browser, &HashMap::new(), None);
|
||||
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
|
||||
assert!(names.contains(&"browser_open"));
|
||||
}
|
||||
|
|
@ -258,4 +276,53 @@ mod tests {
|
|||
assert_eq!(parsed.name, "test");
|
||||
assert_eq!(parsed.description, "A test tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tools_includes_delegate_when_agents_configured() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let security = Arc::new(SecurityPolicy::default());
|
||||
let mem_cfg = MemoryConfig {
|
||||
backend: "markdown".into(),
|
||||
..MemoryConfig::default()
|
||||
};
|
||||
let mem: Arc<dyn Memory> =
|
||||
Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
|
||||
|
||||
let browser = BrowserConfig::default();
|
||||
|
||||
let mut agents = HashMap::new();
|
||||
agents.insert(
|
||||
"researcher".to_string(),
|
||||
DelegateAgentConfig {
|
||||
provider: "ollama".to_string(),
|
||||
model: "llama3".to_string(),
|
||||
system_prompt: None,
|
||||
api_key: None,
|
||||
temperature: None,
|
||||
max_depth: 3,
|
||||
},
|
||||
);
|
||||
|
||||
let tools = all_tools(&security, mem, None, &browser, &agents, Some("sk-test"));
|
||||
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
|
||||
assert!(names.contains(&"delegate"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tools_excludes_delegate_when_no_agents() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let security = Arc::new(SecurityPolicy::default());
|
||||
let mem_cfg = MemoryConfig {
|
||||
backend: "markdown".into(),
|
||||
..MemoryConfig::default()
|
||||
};
|
||||
let mem: Arc<dyn Memory> =
|
||||
Arc::from(crate::memory::create_memory(&mem_cfg, tmp.path(), None).unwrap());
|
||||
|
||||
let browser = BrowserConfig::default();
|
||||
|
||||
let tools = all_tools(&security, mem, None, &browser, &HashMap::new(), None);
|
||||
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
|
||||
assert!(!names.contains(&"delegate"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue