feat: simplify developer extension (#7466)
Co-authored-by: Alex Hancock <alexhancock@block.xyz>
This commit is contained in:
@@ -6,9 +6,7 @@ use goose::config::Config;
|
||||
use goose::posthog::get_telemetry_choice;
|
||||
use goose::recipe::Recipe;
|
||||
use goose_mcp::mcp_server_runner::{serve, McpCommand};
|
||||
use goose_mcp::{
|
||||
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
|
||||
};
|
||||
use goose_mcp::{AutoVisualiserRouter, ComputerControllerServer, MemoryServer, TutorialServer};
|
||||
|
||||
use crate::commands::configure::{configure_telemetry_consent_dialog, handle_configure};
|
||||
use crate::commands::info::handle_info;
|
||||
@@ -1060,7 +1058,6 @@ async fn handle_mcp_command(server: McpCommand) -> Result<()> {
|
||||
McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?,
|
||||
McpCommand::Memory => serve(MemoryServer::new()).await?,
|
||||
McpCommand::Tutorial => serve(TutorialServer::new()).await?,
|
||||
McpCommand::Developer => serve(DeveloperServer::new()).await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY;
|
||||
use cliclack::spinner;
|
||||
use console::style;
|
||||
use goose::agents::extension::ToolInfo;
|
||||
use goose::agents::extension::{ToolInfo, PLATFORM_EXTENSIONS};
|
||||
use goose::agents::extension_manager::get_parameter_names;
|
||||
use goose::agents::Agent;
|
||||
use goose::agents::{extension::Envs, ExtensionConfig};
|
||||
@@ -983,24 +983,35 @@ fn configure_builtin_extension() -> anyhow::Result<()> {
|
||||
select = select.item(id, name, desc);
|
||||
}
|
||||
let extension = select.interact()?.to_string();
|
||||
let timeout = prompt_extension_timeout()?;
|
||||
|
||||
let (display_name, description) = extensions
|
||||
.iter()
|
||||
.find(|(id, _, _)| id == &extension)
|
||||
.map(|(_, name, desc)| (name.to_string(), desc.to_string()))
|
||||
.unwrap_or_else(|| (extension.clone(), extension.clone()));
|
||||
|
||||
set_extension(ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Builtin {
|
||||
let config = if PLATFORM_EXTENSIONS.contains_key(extension.as_str()) {
|
||||
ExtensionConfig::Platform {
|
||||
name: extension.clone(),
|
||||
description,
|
||||
display_name: Some(display_name),
|
||||
bundled: Some(true),
|
||||
available_tools: Vec::new(),
|
||||
}
|
||||
} else {
|
||||
let timeout = prompt_extension_timeout()?;
|
||||
ExtensionConfig::Builtin {
|
||||
name: extension.clone(),
|
||||
display_name: Some(display_name),
|
||||
timeout: Some(timeout),
|
||||
bundled: Some(true),
|
||||
description,
|
||||
available_tools: Vec::new(),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
set_extension(ExtensionEntry {
|
||||
enabled: true,
|
||||
config,
|
||||
});
|
||||
|
||||
cliclack::outro(format!("Enabled {} extension", style(extension).green()))?;
|
||||
@@ -1741,12 +1752,11 @@ pub async fn handle_openrouter_auth() -> anyhow::Result<()> {
|
||||
if !has_developer {
|
||||
set_extension(ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Builtin {
|
||||
config: ExtensionConfig::Platform {
|
||||
name: "developer".to_string(),
|
||||
display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()),
|
||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||
bundled: Some(true),
|
||||
description: "Developer extension".to_string(),
|
||||
display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()),
|
||||
bundled: Some(true),
|
||||
available_tools: Vec::new(),
|
||||
},
|
||||
});
|
||||
@@ -1811,12 +1821,11 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> {
|
||||
if !has_developer {
|
||||
set_extension(ExtensionEntry {
|
||||
enabled: true,
|
||||
config: ExtensionConfig::Builtin {
|
||||
config: ExtensionConfig::Platform {
|
||||
name: "developer".to_string(),
|
||||
display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()),
|
||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||
bundled: Some(true),
|
||||
description: "Developer extension".to_string(),
|
||||
display_name: Some(goose::config::DEFAULT_DISPLAY_NAME.to_string()),
|
||||
bundled: Some(true),
|
||||
available_tools: Vec::new(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -112,6 +112,14 @@ fn value_to_markdown(value: &Value, depth: usize, export_full_strings: bool) ->
|
||||
md_string
|
||||
}
|
||||
|
||||
fn is_shell_tool_name(tool_name: &str) -> bool {
|
||||
matches!(tool_name, "shell")
|
||||
}
|
||||
|
||||
fn is_developer_file_tool_name(tool_name: &str) -> bool {
|
||||
matches!(tool_name, "write" | "edit")
|
||||
}
|
||||
|
||||
pub fn tool_request_to_markdown(req: &ToolRequest, export_all_content: bool) -> String {
|
||||
let mut md = String::new();
|
||||
match &req.tool_call {
|
||||
@@ -119,6 +127,10 @@ pub fn tool_request_to_markdown(req: &ToolRequest, export_all_content: bool) ->
|
||||
let parts: Vec<_> = call.name.rsplitn(2, "__").collect();
|
||||
let (namespace, tool_name_only) = if parts.len() == 2 {
|
||||
(parts[1], parts[0])
|
||||
} else if is_shell_tool_name(call.name.as_ref())
|
||||
|| is_developer_file_tool_name(call.name.as_ref())
|
||||
{
|
||||
("developer", parts[0])
|
||||
} else {
|
||||
("Tool", parts[0])
|
||||
};
|
||||
@@ -130,7 +142,7 @@ pub fn tool_request_to_markdown(req: &ToolRequest, export_all_content: bool) ->
|
||||
md.push_str("**Arguments:**\n");
|
||||
|
||||
match call.name.as_ref() {
|
||||
"developer__shell" => {
|
||||
name if is_shell_tool_name(name) => {
|
||||
if let Some(Value::String(command)) =
|
||||
call.arguments.as_ref().and_then(|args| args.get("command"))
|
||||
{
|
||||
@@ -157,39 +169,25 @@ pub fn tool_request_to_markdown(req: &ToolRequest, export_all_content: bool) ->
|
||||
));
|
||||
}
|
||||
}
|
||||
"developer__text_editor" => {
|
||||
name if is_developer_file_tool_name(name) => {
|
||||
if let Some(Value::String(path)) =
|
||||
call.arguments.as_ref().and_then(|args| args.get("path"))
|
||||
{
|
||||
md.push_str(&format!("* **path**: `{}`\n", path));
|
||||
}
|
||||
if let Some(Value::String(code_edit)) = call
|
||||
.arguments
|
||||
.as_ref()
|
||||
.and_then(|args| args.get("code_edit"))
|
||||
{
|
||||
md.push_str(&format!(
|
||||
"* **code_edit**:\n ```\n{}\n ```\n",
|
||||
code_edit
|
||||
));
|
||||
}
|
||||
|
||||
let other_args: serde_json::Map<String, Value> = call
|
||||
.arguments
|
||||
.as_ref()
|
||||
.map(|obj| {
|
||||
obj.iter()
|
||||
.filter(|(k, _)| k.as_str() != "path" && k.as_str() != "code_edit")
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if !other_args.is_empty() {
|
||||
md.push_str(&value_to_markdown(
|
||||
&Value::Object(other_args),
|
||||
0,
|
||||
export_all_content,
|
||||
));
|
||||
if let Some(args) = &call.arguments {
|
||||
let mut other_args = args.clone();
|
||||
other_args.remove("path");
|
||||
if !other_args.is_empty() {
|
||||
md.push_str(&value_to_markdown(
|
||||
&Value::Object(other_args),
|
||||
0,
|
||||
export_all_content,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
md.push_str("*No arguments*\n");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -529,7 +527,7 @@ mod tests {
|
||||
let tool_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "ls -la",
|
||||
"working_dir": "/home/user"
|
||||
@@ -552,14 +550,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_request_to_markdown_text_editor() {
|
||||
fn test_tool_request_to_markdown_edit() {
|
||||
let tool_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__text_editor".into(),
|
||||
name: "edit".into(),
|
||||
arguments: Some(object!({
|
||||
"path": "/path/to/file.txt",
|
||||
"code_edit": "print('Hello World')"
|
||||
"before": "Hello",
|
||||
"after": "World"
|
||||
})),
|
||||
};
|
||||
let tool_request = ToolRequest {
|
||||
@@ -570,10 +569,11 @@ mod tests {
|
||||
};
|
||||
|
||||
let result = tool_request_to_markdown(&tool_request, true);
|
||||
assert!(result.contains("#### Tool Call: `text_editor`"));
|
||||
assert!(result.contains("#### Tool Call: `edit`"));
|
||||
assert!(result.contains("namespace: `developer`"));
|
||||
assert!(result.contains("**path**: `/path/to/file.txt`"));
|
||||
assert!(result.contains("**code_edit**:"));
|
||||
assert!(result.contains("print('Hello World')"));
|
||||
assert!(result.contains("**before**"));
|
||||
assert!(result.contains("**after**"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -702,7 +702,7 @@ mod tests {
|
||||
let tool_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "cat main.py"
|
||||
})),
|
||||
@@ -758,7 +758,7 @@ if __name__ == "__main__":
|
||||
let git_status_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "git status --porcelain"
|
||||
})),
|
||||
@@ -806,7 +806,7 @@ if __name__ == "__main__":
|
||||
let cargo_build_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "cargo build"
|
||||
})),
|
||||
@@ -860,7 +860,7 @@ warning: unused variable `x`
|
||||
let curl_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "curl -s https://api.github.com/repos/microsoft/vscode/releases/latest"
|
||||
})),
|
||||
@@ -912,15 +912,14 @@ warning: unused variable `x`
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_editor_tool_with_code_creation() {
|
||||
fn test_write_tool_with_code_creation() {
|
||||
let editor_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__text_editor".into(),
|
||||
name: "write".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "write",
|
||||
"path": "/tmp/fibonacci.js",
|
||||
"file_text": "function fibonacci(n) {\n if (n <= 1) return n;\n return fibonacci(n - 1) + fibonacci(n - 2);\n}\n\nconsole.log(fibonacci(10));"
|
||||
"content": "function fibonacci(n) {\n if (n <= 1) return n;\n return fibonacci(n - 1) + fibonacci(n - 2);\n}\n\nconsole.log(fibonacci(10));"
|
||||
})),
|
||||
};
|
||||
let tool_request = ToolRequest {
|
||||
@@ -951,10 +950,10 @@ warning: unused variable `x`
|
||||
let request_result = tool_request_to_markdown(&tool_request, true);
|
||||
let response_result = tool_response_to_markdown(&tool_response, true);
|
||||
|
||||
// Check request formatting - should format code in file_text properly
|
||||
assert!(request_result.contains("#### Tool Call: `text_editor`"));
|
||||
// Check request formatting - should format code in content properly
|
||||
assert!(request_result.contains("#### Tool Call: `write`"));
|
||||
assert!(request_result.contains("**path**: `/tmp/fibonacci.js`"));
|
||||
assert!(request_result.contains("**file_text**:"));
|
||||
assert!(request_result.contains("**content**:"));
|
||||
assert!(request_result.contains("function fibonacci(n)"));
|
||||
assert!(request_result.contains("return fibonacci(n - 1)"));
|
||||
|
||||
@@ -962,72 +961,12 @@ warning: unused variable `x`
|
||||
assert!(response_result.contains("File created successfully"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_editor_tool_view_code() {
|
||||
let editor_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__text_editor".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "view",
|
||||
"path": "/src/utils.py"
|
||||
})),
|
||||
};
|
||||
let _tool_request = ToolRequest {
|
||||
id: "editor-view".to_string(),
|
||||
tool_call: Ok(editor_call),
|
||||
metadata: None,
|
||||
tool_meta: None,
|
||||
};
|
||||
|
||||
let python_code = r#"import os
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
def load_config(config_path: str) -> Dict:
|
||||
"""Load configuration from JSON file."""
|
||||
if not os.path.exists(config_path):
|
||||
raise FileNotFoundError(f"Config file not found: {config_path}")
|
||||
|
||||
with open(config_path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def process_data(data: List[Dict]) -> List[Dict]:
|
||||
"""Process a list of data dictionaries."""
|
||||
return [item for item in data if item.get('active', False)]"#;
|
||||
|
||||
let text_content = TextContent {
|
||||
raw: RawTextContent {
|
||||
text: python_code.to_string(),
|
||||
meta: None,
|
||||
},
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "editor-view".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let response_result = tool_response_to_markdown(&tool_response, true);
|
||||
|
||||
// Text content is output as plain text
|
||||
assert!(response_result.contains("import os"));
|
||||
assert!(response_result.contains("def load_config"));
|
||||
assert!(response_result.contains("typing import Dict"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shell_tool_with_error_output() {
|
||||
let error_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "python nonexistent_script.py"
|
||||
})),
|
||||
@@ -1072,7 +1011,7 @@ Command failed with exit code 2"#;
|
||||
let script_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "python -c \"import sys; print(f'Python {sys.version}'); [print(f'{i}^2 = {i**2}') for i in range(1, 6)]\""
|
||||
})),
|
||||
@@ -1128,7 +1067,7 @@ Command failed with exit code 2"#;
|
||||
let multi_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "cd /tmp && ls -la | head -5 && pwd"
|
||||
})),
|
||||
@@ -1182,7 +1121,7 @@ drwx------ 3 user staff 96 Dec 6 16:20 com.apple.launchd.abc
|
||||
let grep_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "rg 'async fn' --type rust -n"
|
||||
})),
|
||||
@@ -1235,7 +1174,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
let tool_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "echo '{\"test\": \"json\"}'"
|
||||
})),
|
||||
@@ -1279,7 +1218,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
let npm_call = CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "developer__shell".into(),
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({
|
||||
"command": "npm install express typescript @types/node --save-dev"
|
||||
})),
|
||||
|
||||
@@ -485,8 +485,8 @@ fn render_thinking_streaming(
|
||||
fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) {
|
||||
match &req.tool_call {
|
||||
Ok(call) => match call.name.to_string().as_str() {
|
||||
"developer__text_editor" => render_text_editor_request(call, debug),
|
||||
"developer__shell" => render_shell_request(call, debug),
|
||||
name if is_shell_tool_name(name) => render_shell_request(call, debug),
|
||||
name if is_file_tool_name(name) => render_text_editor_request(call, debug),
|
||||
"execute" | "execute_code" => render_execute_code_request(call, debug),
|
||||
"delegate" => render_delegate_request(call, debug),
|
||||
"subagent" => render_delegate_request(call, debug),
|
||||
@@ -534,6 +534,14 @@ fn render_tool_response(resp: &ToolResponse, theme: Theme, debug: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_shell_tool_name(name: &str) -> bool {
|
||||
matches!(name, "shell")
|
||||
}
|
||||
|
||||
fn is_file_tool_name(name: &str) -> bool {
|
||||
matches!(name, "write" | "edit")
|
||||
}
|
||||
|
||||
pub fn render_error(message: &str) {
|
||||
println!("\n {} {}\n", style("error:").red().bold(), message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user