feat: Externalize research tool as embedded skill
Replaces the built-in research/research_status tools with a portable skill-based approach: - Add embedded skills infrastructure (skills compiled into binary) - Add repo-local skills/ directory support (highest priority) - Create research skill with SKILL.md and g3-research shell script - Script extraction to .g3/bin/ with version tracking - Filesystem-based handoff via .g3/research/<id>/status.json - Remove PendingResearchManager and all research tool code - Update system prompt to reference skill instead of tool Benefits: - No special tool infrastructure needed (just shell + read_file) - Context-efficient (reports stay on disk until needed) - Crash-resilient (state persisted to filesystem) - Portable (skill can be overridden per-workspace) Breaking change: research tool calls now return a deprecation message pointing to the research skill.
This commit is contained in:
@@ -12,7 +12,6 @@ use serde_json::json;
|
||||
pub struct ToolConfig {
|
||||
pub webdriver: bool,
|
||||
pub computer_control: bool,
|
||||
pub exclude_research: bool,
|
||||
}
|
||||
|
||||
impl ToolConfig {
|
||||
@@ -20,16 +19,8 @@ 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.
|
||||
@@ -37,7 +28,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(config.exclude_research);
|
||||
let mut tools = create_core_tools();
|
||||
|
||||
if config.webdriver {
|
||||
tools.extend(create_webdriver_tools());
|
||||
@@ -47,7 +38,7 @@ pub fn create_tool_definitions(config: ToolConfig) -> Vec<Tool> {
|
||||
}
|
||||
|
||||
/// Create the core tools that are always available
|
||||
fn create_core_tools(exclude_research: bool) -> Vec<Tool> {
|
||||
fn create_core_tools() -> Vec<Tool> {
|
||||
let mut tools = vec![
|
||||
Tool {
|
||||
name: "shell".to_string(),
|
||||
@@ -195,40 +186,6 @@ fn create_core_tools(exclude_research: bool) -> Vec<Tool> {
|
||||
},
|
||||
];
|
||||
|
||||
// Conditionally add the research tool (excluded for scout agent to prevent recursion)
|
||||
if !exclude_research {
|
||||
tools.push(Tool {
|
||||
name: "research".to_string(),
|
||||
description: "Initiate web-based research on a topic. This tool is ASYNCHRONOUS - it spawns a research agent in the background and returns immediately with a research_id. Results are automatically injected into the conversation when ready. Use this when you need to research APIs, SDKs, libraries, approaches, bugs, or documentation. If you need the results before continuing, say so and yield the turn to the user. Check status with research_status tool.".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"]
|
||||
}),
|
||||
});
|
||||
|
||||
// research_status tool - check status of pending research
|
||||
tools.push(Tool {
|
||||
name: "research_status".to_string(),
|
||||
description: "Check the status of pending research tasks. Call without arguments to list all pending research, or with a research_id to check a specific task. Use this to see if research has completed before it's automatically injected.".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"research_id": {
|
||||
"type": "string",
|
||||
"description": "Optional: specific research_id to check. If omitted, lists all pending research tasks."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Plan Mode tools
|
||||
tools.push(Tool {
|
||||
name: "plan_read".to_string(),
|
||||
@@ -499,12 +456,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_core_tools_count() {
|
||||
let tools = create_core_tools(false);
|
||||
let tools = create_core_tools();
|
||||
// Core tools: shell, background_process, read_file, read_image,
|
||||
// write_file, str_replace, code_search,
|
||||
// research, research_status, remember, plan_read, plan_write, plan_approve
|
||||
// (14 total - memory is auto-loaded, only remember tool needed)
|
||||
assert_eq!(tools.len(), 14);
|
||||
assert_eq!(tools.len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -518,7 +475,7 @@ mod tests {
|
||||
fn test_create_tool_definitions_core_only() {
|
||||
let config = ToolConfig::default();
|
||||
let tools = create_tool_definitions(config);
|
||||
assert_eq!(tools.len(), 14);
|
||||
assert_eq!(tools.len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -526,28 +483,16 @@ mod tests {
|
||||
let config = ToolConfig::new(true, true);
|
||||
let tools = create_tool_definitions(config);
|
||||
// 14 core + 15 webdriver = 29
|
||||
assert_eq!(tools.len(), 29);
|
||||
assert_eq!(tools.len(), 27);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_has_required_fields() {
|
||||
let tools = create_core_tools(false);
|
||||
let tools = create_core_tools();
|
||||
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(), 14);
|
||||
assert_eq!(tools_without_research.len(), 12); // research + research_status both excluded
|
||||
|
||||
assert!(tools_with_research.iter().any(|t| t.name == "research"));
|
||||
assert!(!tools_without_research.iter().any(|t| t.name == "research"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user