Add research tool for web-based research via scout agent

New tool that spawns a scout agent to perform web research and return
a structured research brief. The scout agent uses webdriver to browse
the web and returns a decision-ready report.

Changes:
- Added 'research' tool definition (12 core tools total)
- Added research tool dispatch in tool_dispatch.rs
- Created tools/research.rs implementation:
  - Spawns 'g3 --agent scout <query>' as subprocess
  - Captures stdout and extracts last line (report file path)
  - Reads and returns the report file contents
- Added exclude_research flag to ToolConfig
- Scout agent (agent_name == 'scout') does NOT have access to research
  tool to prevent infinite recursion
- Updated system prompts to describe when to use research tool
- Added scout.md agent prompt with research brief output contract

The research tool is preferred for complex research tasks (APIs, SDKs,
libraries, approaches, bugs). WebDriver can still be used directly for
simple lookups or fine-grained control.
This commit is contained in:
Dhanji R. Prasanna
2026-01-09 15:59:19 +11:00
parent de50726eeb
commit 33e5705fc3
7 changed files with 284 additions and 19 deletions

View File

@@ -12,6 +12,7 @@ use serde_json::json;
pub struct ToolConfig {
pub webdriver: bool,
pub computer_control: bool,
pub exclude_research: bool,
}
impl ToolConfig {
@@ -19,8 +20,16 @@ impl ToolConfig {
Self {
webdriver,
computer_control,
exclude_research: false,
}
}
/// Create a config with the research tool excluded.
/// Used for scout agent to prevent recursion.
pub fn with_research_excluded(mut self) -> Self {
self.exclude_research = true;
self
}
}
/// Create tool definitions for native tool calling providers.
@@ -28,7 +37,7 @@ impl ToolConfig {
/// Returns a vector of Tool definitions that describe the available tools
/// and their input schemas.
pub fn create_tool_definitions(config: ToolConfig) -> Vec<Tool> {
let mut tools = create_core_tools();
let mut tools = create_core_tools(config.exclude_research);
if config.webdriver {
tools.extend(create_webdriver_tools());
@@ -38,8 +47,8 @@ pub fn create_tool_definitions(config: ToolConfig) -> Vec<Tool> {
}
/// Create the core tools that are always available
fn create_core_tools() -> Vec<Tool> {
vec![
fn create_core_tools(exclude_research: bool) -> Vec<Tool> {
let mut tools = vec![
Tool {
name: "shell".to_string(),
description: "Execute shell commands".to_string(),
@@ -243,7 +252,27 @@ fn create_core_tools() -> Vec<Tool> {
"required": ["searches"]
}),
},
]
];
// Conditionally add the research tool (excluded for scout agent to prevent recursion)
if !exclude_research {
tools.push(Tool {
name: "research".to_string(),
description: "Perform web-based research on a topic and return a structured research brief. Use this tool when you need to research APIs, SDKs, libraries, approaches, bugs, documentation, or anything else that requires web-based research. The tool spawns a specialized research agent that browses the web and returns a concise, decision-ready report.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The research question or topic to investigate. Be specific about what you need to know."
}
},
"required": ["query"]
}),
});
}
tools
}
/// Create WebDriver browser automation tools
@@ -445,11 +474,11 @@ mod tests {
#[test]
fn test_core_tools_count() {
let tools = create_core_tools();
let tools = create_core_tools(false);
// Should have the core tools: shell, background_process, read_file, read_image,
// write_file, str_replace, final_output, take_screenshot,
// todo_read, todo_write, code_coverage, code_search (11 total)
assert_eq!(tools.len(), 11);
// write_file, str_replace, take_screenshot,
// todo_read, todo_write, code_coverage, code_search, research (12 total)
assert_eq!(tools.len(), 12);
}
#[test]
@@ -463,24 +492,36 @@ mod tests {
fn test_create_tool_definitions_core_only() {
let config = ToolConfig::default();
let tools = create_tool_definitions(config);
assert_eq!(tools.len(), 11);
assert_eq!(tools.len(), 12);
}
#[test]
fn test_create_tool_definitions_all_enabled() {
let config = ToolConfig::new(true, true);
let tools = create_tool_definitions(config);
// 11 core + 15 webdriver = 26
assert_eq!(tools.len(), 26);
// 12 core + 15 webdriver = 27
assert_eq!(tools.len(), 27);
}
#[test]
fn test_tool_has_required_fields() {
let tools = create_core_tools();
let tools = create_core_tools(false);
for tool in tools {
assert!(!tool.name.is_empty(), "Tool name should not be empty");
assert!(!tool.description.is_empty(), "Tool description should not be empty");
assert!(tool.input_schema.is_object(), "Tool input_schema should be an object");
}
}
#[test]
fn test_research_tool_excluded() {
let tools_with_research = create_core_tools(false);
let tools_without_research = create_core_tools(true);
assert_eq!(tools_with_research.len(), 12);
assert_eq!(tools_without_research.len(), 11);
assert!(tools_with_research.iter().any(|t| t.name == "research"));
assert!(!tools_without_research.iter().any(|t| t.name == "research"));
}
}