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:
@@ -7,10 +7,12 @@
|
||||
//! - `todo` - TODO list management
|
||||
//! - `webdriver` - Browser automation via WebDriver
|
||||
//! - `misc` - Other tools (screenshots, code search, etc.)
|
||||
//! - `research` - Web research via scout agent
|
||||
|
||||
pub mod executor;
|
||||
pub mod file_ops;
|
||||
pub mod misc;
|
||||
pub mod research;
|
||||
pub mod shell;
|
||||
pub mod todo;
|
||||
pub mod webdriver;
|
||||
|
||||
102
crates/g3-core/src/tools/research.rs
Normal file
102
crates/g3-core/src/tools/research.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
//! Research tool: spawns a scout agent to perform web-based research.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::process::Stdio;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::ui_writer::UiWriter;
|
||||
use crate::ToolCall;
|
||||
|
||||
use super::executor::ToolContext;
|
||||
|
||||
/// Execute the research tool by spawning a scout agent.
|
||||
///
|
||||
/// This tool:
|
||||
/// 1. Spawns `g3 --agent scout` with the query
|
||||
/// 2. Captures stdout and extracts the last line (file path to report)
|
||||
/// 3. Reads the report file and returns its contents
|
||||
pub async fn execute_research<W: UiWriter>(
|
||||
tool_call: &ToolCall,
|
||||
ctx: &mut ToolContext<'_, W>,
|
||||
) -> Result<String> {
|
||||
let query = tool_call
|
||||
.args
|
||||
.get("query")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required 'query' parameter"))?;
|
||||
|
||||
debug!("Research tool called with query: {}", query);
|
||||
ctx.ui_writer.print_tool_header("research", None);
|
||||
ctx.ui_writer.print_tool_arg("query", query);
|
||||
|
||||
// Find the g3 executable path
|
||||
let g3_path = std::env::current_exe()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("g3"));
|
||||
|
||||
// Spawn the scout agent
|
||||
let mut child = Command::new(&g3_path)
|
||||
.arg("--agent")
|
||||
.arg("scout")
|
||||
.arg("--webdriver") // Scout needs webdriver for web research
|
||||
.arg("--new-session") // Always start fresh for research
|
||||
.arg("--quiet") // Suppress log file creation
|
||||
.arg(query)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to spawn scout agent: {}", e))?;
|
||||
|
||||
// Capture stdout to find the report file path
|
||||
let stdout = child.stdout.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to capture scout agent stdout"))?;
|
||||
|
||||
let mut reader = BufReader::new(stdout).lines();
|
||||
let mut last_line = String::new();
|
||||
|
||||
// Read all lines, keeping track of the last one
|
||||
while let Some(line) = reader.next_line().await? {
|
||||
debug!("Scout output: {}", line);
|
||||
last_line = line;
|
||||
}
|
||||
|
||||
// Wait for the process to complete
|
||||
let status = child.wait().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to wait for scout agent: {}", e))?;
|
||||
|
||||
if !status.success() {
|
||||
return Ok(format!("❌ Scout agent failed with exit code: {:?}", status.code()));
|
||||
}
|
||||
|
||||
// The last line should be the path to the report file
|
||||
let report_path = last_line.trim();
|
||||
|
||||
if report_path.is_empty() {
|
||||
return Ok("❌ Scout agent did not output a report file path".to_string());
|
||||
}
|
||||
|
||||
debug!("Report file path: {}", report_path);
|
||||
|
||||
// Expand tilde if present
|
||||
let expanded_path = if report_path.starts_with('~') {
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
std::path::PathBuf::from(home).join(&report_path[2..]) // Skip "~/"
|
||||
} else {
|
||||
std::path::PathBuf::from(report_path)
|
||||
}
|
||||
} else {
|
||||
std::path::PathBuf::from(report_path)
|
||||
};
|
||||
|
||||
// Read the report file
|
||||
match std::fs::read_to_string(&expanded_path) {
|
||||
Ok(content) => {
|
||||
debug!("Report loaded: {} chars", content.len());
|
||||
Ok(format!("📋 Research Report:\n\n{}", content))
|
||||
}
|
||||
Err(e) => {
|
||||
Ok(format!("❌ Failed to read report file '{}': {}", report_path, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user