From 4bd8118136ec60c692a63ffdd2d656347f9dcf89 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Mon, 9 Feb 2026 13:06:34 +1100 Subject: [PATCH] leaning up --- README.md | 4 + .../goose-mcp/src/developer/rmcp_developer.rs | 420 +++++------------- crates/goose/src/prompts/system.md | 73 +-- 3 files changed, 127 insertions(+), 370 deletions(-) diff --git a/README.md b/README.md index de9bdde7f2..a4e1caf081 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ Designed for maximum flexibility, goose works with any LLM and supports multi-mo > > Because it always helps them "migrate" their code to production! 🚀 +> What did goose say when it finished debugging the code? +> +> "That bug didn't stand a *gander*!" 🐛 + # goose around with us - [Discord](https://discord.gg/goose-oss) - [YouTube](https://www.youtube.com/@goose-oss) diff --git a/crates/goose-mcp/src/developer/rmcp_developer.rs b/crates/goose-mcp/src/developer/rmcp_developer.rs index 556123c7af..f11a9c0c2d 100644 --- a/crates/goose-mcp/src/developer/rmcp_developer.rs +++ b/crates/goose-mcp/src/developer/rmcp_developer.rs @@ -3,7 +3,7 @@ use base64::Engine; use etcetera::AppStrategy; use ignore::gitignore::{Gitignore, GitignoreBuilder}; use include_dir::{include_dir, Dir}; -use indoc::{formatdoc, indoc}; +use indoc::formatdoc; use once_cell::sync::Lazy; use rmcp::{ handler::server::{router::tool::ToolRouter, wrapper::Parameters}, @@ -63,9 +63,7 @@ use crate::developer::{paths::get_shell_path_dirs, shell::ShellConfig}; use super::analyze::{types::AnalyzeParams, CodeAnalyzer}; use super::editor_models::{create_editor_model, EditorModel}; use super::shell::{configure_shell_command, expand_path, is_absolute_path, kill_process_group}; -use super::text_editor::{ - text_editor_insert, text_editor_replace, text_editor_undo, text_editor_view, text_editor_write, -}; +use super::text_editor::{text_editor_replace, text_editor_view, text_editor_write}; /// Parameters for the screen_capture tool #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -79,38 +77,41 @@ pub struct ScreenCaptureParams { pub window_title: Option, } -/// Parameters for the text_editor tool +/// Parameters for the read tool #[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct TextEditorParams { - /// Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`. +pub struct ReadParams { + /// Path to file or directory (relative or absolute) pub path: String, - /// The operation to perform. Allowed options are: `view`, `write`, `str_replace`, `insert`, `undo_edit`. - pub command: String, - - /// Unified diff to apply. Supports editing multiple files simultaneously. Cannot create or delete files - /// Example: "--- a/file\n+++ b/file\n@@ -1,3 +1,3 @@\n context\n-old\n+new\n context" - /// Preferred edit method. - pub diff: Option, - - /// Optional array of two integers specifying the start and end line numbers to view. - /// Line numbers are 1-indexed, and -1 for the end line means read to the end of the file. - /// This parameter only applies when viewing files, not directories. + /// Optional line range [start, end] to view. 1-indexed, -1 for end of file. pub view_range: Option>, - - /// The content to write to the file. Required for `write` command. - pub file_text: Option, - - /// The old string to replace. - pub old_str: Option, - - /// The new string to replace with. Required for `insert` command. - pub new_str: Option, - - /// The line number after which to insert text (0 for beginning). Required for `insert` command. - pub insert_line: Option, } +/// Parameters for the edit tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct EditParams { + /// Path to the file to edit (relative or absolute) + pub path: String, + + /// Exact text to find and replace (must match exactly including whitespace) + pub old_str: String, + + /// New text to replace with + pub new_str: String, +} + +/// Parameters for the write tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct WriteParams { + /// Path to the file to create or overwrite (relative or absolute) + pub path: String, + + /// Content to write to the file + pub content: String, +} + + + /// Parameters for the shell tool #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ShellParams { @@ -247,181 +248,27 @@ impl ServerHandler for DeveloperServer { let os = std::env::consts::OS; let in_container = Self::is_definitely_container(); - let base_instructions = match os { - "windows" => formatdoc! {r#" - The developer extension gives you the capabilities to edit code files and run shell commands, - and can be used to solve a wide range of problems. + let shell_info = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); - You can use the shell tool to run Windows commands (PowerShell or CMD). - When using paths, you can use either backslashes or forward slashes. + let instructions = formatdoc! {r#" + Tools: read, edit, write, shell - Use the shell tool as needed to locate files or interact with the project. + - read: View file contents (use view_range for partial reads) + - edit: Replace exact text (old_str must match exactly) + - write: Create or overwrite files + - shell: Run commands (prefer rg over grep/find) - Leverage `analyze` through `return_last_only=true` subagents for deep codebase understanding with lean context - - delegate analysis, retain summaries - - Your windows/screen tools can be used for visual debugging. You should not use these tools unless - prompted to, but you can mention they are available if they are relevant. - - operating system: {os} - current directory: {cwd} - {container_info} - "#, - os=os, - cwd=cwd.to_string_lossy(), - container_info=if in_container { "container: true" } else { "" }, - }, - _ => { - let shell_info = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); - - formatdoc! {r#" - The developer extension gives you the capabilities to edit code files and run shell commands, - and can be used to solve a wide range of problems. - - You can use the shell tool to run any command that would work on the relevant operating system. - Use the shell tool as needed to locate files or interact with the project. - - Leverage `analyze` through `return_last_only=true` subagents for deep codebase understanding with lean context - - delegate analysis, retain summaries - - Your windows/screen tools can be used for visual debugging. You should not use these tools unless - prompted to, but you can mention they are available if they are relevant. - - Always prefer ripgrep (rg -C 3) to grep. - - operating system: {os} - current directory: {cwd} + os: {os} + cwd: {cwd} shell: {shell} {container_info} - "#, - os=os, - cwd=cwd.to_string_lossy(), - shell=shell_info, - container_info=if in_container { "container: true" } else { "" }, - } - } + "#, + os=os, + cwd=cwd.to_string_lossy(), + shell=shell_info, + container_info=if in_container { "container: true" } else { "" }, }; - // Check if editor model exists and augment with custom llm editor tool description - let editor_description = if let Some(ref editor) = self.editor_model { - formatdoc! {r#" - - Additional Text Editor Tool Instructions: - - Perform text editing operations on files. - The `command` parameter specifies the operation to perform. Allowed options are: - - `view`: View the content of a file. - - `write`: Create or overwrite a file with the given content - - `str_replace`: Replace text in one or more files. - - `insert`: Insert text at a specific line location in the file. - - `undo_edit`: Undo the last edit made to a file. - - To use the write command, you must specify `file_text` which will become the new content of the file. Be careful with - existing files! This is a full overwrite, so you must include everything - not just sections you are modifying. - - To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end) - and `new_str` (the text to insert). - - To use the str_replace command to edit multiple files, use the `diff` parameter with a unified diff. - To use the str_replace command to edit one file, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one - unique section of the original file, including any whitespace. Make sure to include enough context that the match is not - ambiguous. The entire original string will be replaced with `new_str` - - When possible, batch file edits together by using a multi-file unified `diff` within a single str_replace tool call. - - {} - - "#, editor.get_str_replace_description()} - } else { - formatdoc! {r#" - - Additional Text Editor Tool Instructions: - - Perform text editing operations on files. - - The `command` parameter specifies the operation to perform. Allowed options are: - - `view`: View the content of a file. - - `write`: Create or overwrite a file with the given content - - `str_replace`: Replace text in one or more files. - - `insert`: Insert text at a specific line location in the file. - - `undo_edit`: Undo the last edit made to a file. - - To use the write command, you must specify `file_text` which will become the new content of the file. Be careful with - existing files! This is a full overwrite, so you must include everything - not just sections you are modifying. - - To use the str_replace command to edit multiple files, use the `diff` parameter with a unified diff. - To use the str_replace command to edit one file, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one - unique section of the original file, including any whitespace. Make sure to include enough context that the match is not - ambiguous. The entire original string will be replaced with `new_str` - - When possible, batch file edits together by using a multi-file unified `diff` within a single str_replace tool call. - - To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning, -1 for end) - and `new_str` (the text to insert). - - - "#} - }; - - // Create comprehensive shell tool instructions - let common_shell_instructions = indoc! {r#" - Additional Shell Tool Instructions: - Execute a command in the shell. - - This will return the output and error concatenated into a single string, as - you would see from running on the command line. There will also be an indication - of if the command succeeded or failed. - - Avoid commands that produce a large amount of output, and consider piping those outputs to files. - - **Important**: Each shell command runs in its own process. Things like directory changes or - sourcing files do not persist between tool calls. So you may need to repeat them each time by - stringing together commands. - - If fetching web content, consider adding Accept: text/markdown header - "#}; - - let windows_specific = indoc! {r#" - **Important**: For searching files and code: - - Preferred: Use ripgrep (`rg`) when available - it respects .gitignore and is fast: - - To locate a file by name: `rg --files | rg example.py` - - To locate content inside files: `rg 'class Example'` - - Alternative Windows commands (if ripgrep is not installed): - - To locate a file by name: `dir /s /b example.py` - - To locate content inside files: `findstr /s /i "class Example" *.py` - - Note: Alternative commands may show ignored/hidden files that should be excluded. - - - Multiple commands: Use && to chain commands, avoid newlines - - Example: `cd example && dir` or `activate.bat && pip install numpy` - - **Important**: Use forward slashes in paths (e.g., `C:/Users/name`) to avoid - escape character issues with backslashes, i.e. \n in a path could be - mistaken for a newline. - "#}; - - let unix_specific = indoc! {r#" - If you need to run a long lived command, background it - e.g. `uvicorn main:app &` so that - this tool does not run indefinitely. - - **Important**: Use ripgrep - `rg` - exclusively when you need to locate a file or a code reference, - other solutions may produce too large output because of hidden files! For example *do not* use `find` or `ls -r` - - List files by name: `rg --files | rg ` - - List files that contain a regex: `rg '' -l` - - - Multiple commands: Use && to chain commands, avoid newlines - - Example: `cd example && ls` or `source env/bin/activate && pip install numpy` - "#}; - - let shell_tool_desc = match os { - "windows" => format!("{}{}", common_shell_instructions, windows_specific), - _ => format!("{}{}", common_shell_instructions, unix_specific), - }; - - let instructions = format!("{base_instructions}{editor_description}\n{shell_tool_desc}"); - ServerInfo { server_info: Implementation { name: "goose-developer".to_string(), @@ -744,132 +591,91 @@ impl DeveloperServer { ])) } - /// Perform text editing operations on files. - /// - /// The `command` parameter specifies the operation to perform. Allowed options are: - /// - `view`: View the content of a file. - /// - `write`: Create or overwrite a file with the given content - /// - `str_replace`: Replace old_str with new_str in the file. - /// - `insert`: Insert text at a specific line location in the file. - /// - `undo_edit`: Undo the last edit made to a file. + /// Read file contents or list directory. #[tool( - name = "text_editor", - description = "Perform text editing operations on files. Commands: view (show file content), write (create/overwrite file), str_replace (edit file), insert (insert at line), undo_edit (undo last change)." + name = "read", + description = "Read file contents. Use view_range [start, end] for partial reads." )] - pub async fn text_editor( + pub async fn read( &self, - params: Parameters, + params: Parameters, ) -> Result { let params = params.0; let path = self.resolve_path(¶ms.path)?; - // Check if file is ignored before proceeding with any text editor operation if self.is_ignored(&path) { return Err(ErrorData::new( ErrorCode::INTERNAL_ERROR, - format!( - "Access to '{}' is restricted by .gooseignore", - path.display() - ), + format!("Access to '{}' is restricted by .gooseignore", path.display()), None, )); } - match params.command.as_str() { - "view" => { - let view_range = params.view_range.as_ref().and_then(|vr| { - if vr.len() == 2 { - Some((vr[0] as usize, vr[1])) - } else { - None - } - }); - let content = text_editor_view(&path, view_range).await?; - Ok(CallToolResult::success(content)) + let view_range = params.view_range.as_ref().and_then(|vr| { + if vr.len() == 2 { + Some((vr[0] as usize, vr[1])) + } else { + None } - "write" => { - let file_text = params.file_text.ok_or_else(|| { - ErrorData::new( - ErrorCode::INVALID_PARAMS, - "Missing 'file_text' parameter for write command".to_string(), - None, - ) - })?; - let content = text_editor_write(&path, &file_text).await?; - Ok(CallToolResult::success(content)) - } - "str_replace" => { - // Check if diff parameter is provided - if let Some(ref diff) = params.diff { - // When diff is provided, old_str and new_str are not required - let content = text_editor_replace( - &path, - "", // old_str not used with diff - "", // new_str not used with diff - Some(diff), - &self.editor_model, - &self.file_history, - ) - .await?; - Ok(CallToolResult::success(content)) - } else { - // Traditional str_replace with old_str and new_str - let old_str = params.old_str.ok_or_else(|| { - ErrorData::new( - ErrorCode::INVALID_PARAMS, - "Missing 'old_str' parameter for str_replace command".to_string(), - None, - ) - })?; - let new_str = params.new_str.ok_or_else(|| { - ErrorData::new( - ErrorCode::INVALID_PARAMS, - "Missing 'new_str' parameter for str_replace command".to_string(), - None, - ) - })?; - let content = text_editor_replace( - &path, - &old_str, - &new_str, - None, - &self.editor_model, - &self.file_history, - ) - .await?; - Ok(CallToolResult::success(content)) - } - } - "insert" => { - let insert_line = params.insert_line.ok_or_else(|| { - ErrorData::new( - ErrorCode::INVALID_PARAMS, - "Missing 'insert_line' parameter for insert command".to_string(), - None, - ) - })? as usize; - let new_str = params.new_str.ok_or_else(|| { - ErrorData::new( - ErrorCode::INVALID_PARAMS, - "Missing 'new_str' parameter for insert command".to_string(), - None, - ) - })?; - let content = - text_editor_insert(&path, insert_line as i64, &new_str, &self.file_history) - .await?; - Ok(CallToolResult::success(content)) - } - "undo_edit" => { - let content = text_editor_undo(&path, &self.file_history).await?; - Ok(CallToolResult::success(content)) - } - _ => Err(ErrorData::new( - ErrorCode::INVALID_PARAMS, - format!("Unknown command '{}'", params.command), + }); + let content = text_editor_view(&path, view_range).await?; + Ok(CallToolResult::success(content)) + } + + /// Edit a file by replacing exact text. + #[tool( + name = "edit", + description = "Edit a file by replacing exact text. old_str must match exactly including whitespace." + )] + pub async fn edit( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let path = self.resolve_path(¶ms.path)?; + + if self.is_ignored(&path) { + return Err(ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Access to '{}' is restricted by .gooseignore", path.display()), None, - )), + )); } + + let content = text_editor_replace( + &path, + ¶ms.old_str, + ¶ms.new_str, + None, + &self.editor_model, + &self.file_history, + ) + .await?; + Ok(CallToolResult::success(content)) + } + + /// Create or overwrite a file. + #[tool( + name = "write", + description = "Create or overwrite a file with the given content." + )] + pub async fn write( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let path = self.resolve_path(¶ms.path)?; + + if self.is_ignored(&path) { + return Err(ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Access to '{}' is restricted by .gooseignore", path.display()), + None, + )); + } + + let content = text_editor_write(&path, ¶ms.content).await?; + Ok(CallToolResult::success(content)) } /// Execute a command in the shell. diff --git a/crates/goose/src/prompts/system.md b/crates/goose/src/prompts/system.md index c282d75497..67410da62a 100644 --- a/crates/goose/src/prompts/system.md +++ b/crates/goose/src/prompts/system.md @@ -1,71 +1,18 @@ -You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal. -goose is being developed as an open-source software project. - -goose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o, -claude-sonnet-4, o1, llama-3.2, deepseek-r1, etc). -These models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10 -months prior to the current date. -{% if not code_execution_mode %} - -# Extensions - -Extensions allow other applications to provide context to goose. Extensions connect goose to different data sources and -tools. -You are capable of dynamically plugging into new extensions and learning how to use them. You solve higher level -problems using the tools in these extensions, and can interact with multiple at once. - -If the Extension Manager extension is enabled, you can use the search_available_extensions tool to discover additional -extensions that can help with your task. To enable or disable extensions, use the manage_extensions tool with the -extension_name. You should only enable extensions found from the search_available_extensions tool. -If Extension Manager is not available, you can only work with currently enabled extensions and cannot dynamically load -new ones. +You are an expert coding assistant called goose. You help users by reading files, executing commands, and editing code. {% if (extensions is defined) and extensions %} -Because you dynamically load extensions, your conversation history may refer -to interactions with extensions that are not currently active. The currently -active extensions are below. Each of these extensions provides tools that are -in your tool specification. - {% for extension in extensions %} - ## {{extension.name}} - -{% if extension.has_resources %} -{{extension.name}} supports resources, you can use extensionmanager__read_resource, -and extensionmanager__list_resources on this extension. -{% endif %} -{% if extension.instructions %}### Instructions -{{extension.instructions}}{% endif %} +{% if extension.instructions %}{{extension.instructions}}{% endif %} {% endfor %} - -{% else %} -No extensions are defined. You should let the user know that they should add extensions. -{% endif %} {% endif %} -{% if extension_tool_limits is defined and not code_execution_mode %} -{% with (extension_count, tool_count) = extension_tool_limits %} -# Suggestion +Guidelines: +- Use read to examine files before editing +- Use edit for precise changes (old_str must match exactly) +- Use write only for new files or complete rewrites +- Use shell for commands (ls, grep, find, git, etc.) +- Be concise +- Show file paths clearly -The user currently has enabled {{extension_count}} extensions with a total of {{tool_count}} tools. -Since this exceeds the recommended limits ({{max_extensions}} extensions or {{max_tools}} tools), -you should ask the user if they would like to disable some extensions for this session. - -Use the search_available_extensions tool to find extensions available to disable. -You should only disable extensions found from the search_available_extensions tool. -List all the extensions available to disable in the response. -Explain that minimizing extensions helps with the recall of the correct tools to use. -{% endwith %} -{% endif %} - -# Response Guidelines - -- Use Markdown formatting for all responses. -- Follow best practices for Markdown, including: - - Using headers for organization. - - Bullet points for lists. - - Links formatted correctly, either as linked text (e.g., [this is linked text](https://example.com)) or automatic - links using angle brackets (e.g., ). -- For code examples, use fenced code blocks by placing triple backticks (` ``` `) before and after the code. Include the - language identifier after the opening backticks (e.g., ` ```python `) to enable syntax highlighting. -- Ensure clarity, conciseness, and proper formatting to enhance readability and usability. +Current date and time: {{current_date_time}}