diff --git a/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md b/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md index 073e34683dcf..9099eaff1747 100644 --- a/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md +++ b/crates/goose-mcp/src/developer/editor_models/EDITOR_API_EXAMPLE.md @@ -36,7 +36,7 @@ export GOOSE_EDITOR_MODEL="claude-3-5-sonnet-20241022" ```bash export GOOSE_EDITOR_API_KEY="sk-..." export GOOSE_EDITOR_HOST="https://api.morphllm.com/v1" -export GOOSE_EDITOR_MODEL="morph-v0" +export GOOSE_EDITOR_MODEL="morph-v3-large" ``` **Relace** diff --git a/crates/goose-mcp/src/developer/editor_models/mod.rs b/crates/goose-mcp/src/developer/editor_models/mod.rs index d442aa56c32b..0a91e1f41df5 100644 --- a/crates/goose-mcp/src/developer/editor_models/mod.rs +++ b/crates/goose-mcp/src/developer/editor_models/mod.rs @@ -23,21 +23,23 @@ impl EditorModel { original_code: &str, old_str: &str, update_snippet: &str, + instruction: &str, ) -> Result { match self { EditorModel::MorphLLM(editor) => { + // Only MorphLLM uses the instruction parameter editor - .edit_code(original_code, old_str, update_snippet) + .edit_code(original_code, old_str, update_snippet, instruction) .await } EditorModel::OpenAICompatible(editor) => { editor - .edit_code(original_code, old_str, update_snippet) + .edit_code(original_code, old_str, update_snippet, instruction) .await } EditorModel::Relace(editor) => { editor - .edit_code(original_code, old_str, update_snippet) + .edit_code(original_code, old_str, update_snippet, instruction) .await } } @@ -51,6 +53,15 @@ impl EditorModel { EditorModel::Relace(editor) => editor.get_str_replace_description(), } } + + /// Check if this editor supports/requires the instruction parameter + pub fn supports_instruction_parameter(&self) -> bool { + match self { + EditorModel::MorphLLM(_) => true, + EditorModel::OpenAICompatible(_) => false, + EditorModel::Relace(_) => false, + } + } } /// Trait for individual editor implementations @@ -61,6 +72,7 @@ pub trait EditorModelImpl { original_code: &str, old_str: &str, update_snippet: &str, + instruction: &str, ) -> Result; /// Get the description for the str_replace command when this editor is active diff --git a/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs b/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs index 8c5d60f8f813..e6b26b783b20 100644 --- a/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs +++ b/crates/goose-mcp/src/developer/editor_models/morphllm_editor.rs @@ -27,6 +27,7 @@ impl EditorModelImpl for MorphLLMEditor { original_code: &str, _old_str: &str, update_snippet: &str, + instruction: &str, ) -> Result { eprintln!("Calling MorphLLM Editor API"); @@ -42,10 +43,10 @@ impl EditorModelImpl for MorphLLMEditor { // Create the client let client = Client::new(); - // Format the prompt as specified in the Python example + // Format the prompt according to MorphLLM's new format with instruction let user_prompt = format!( - "{}\n{}", - original_code, update_snippet + "{}\n{}\n{}", + instruction, original_code, update_snippet ); // Prepare the request body for OpenAI-compatible API @@ -98,6 +99,7 @@ impl EditorModelImpl for MorphLLMEditor { fn get_str_replace_description(&self) -> &'static str { "Use the edit_file to propose an edit to an existing file. + This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write. When writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines. @@ -114,6 +116,13 @@ impl EditorModelImpl for MorphLLMEditor { Each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity. If you plan on deleting a section, you must provide surrounding context to indicate the deletion. DO NOT omit spans of pre-existing code without using the // ... existing code ... comment to indicate its absence. + + **IMPORTANT**: You must also provide an `instruction` parameter - a single sentence written in the first person describing what you are going to do for the sketched edit. This instruction helps the less intelligent model understand and apply your edit correctly. + + Examples of good instructions: + - \"I am adding error handling to the user authentication function and removing the old authentication method\" + + The instruction should be specific enough to disambiguate any uncertainty in your edit. " } } diff --git a/crates/goose-mcp/src/developer/editor_models/openai_compatible_editor.rs b/crates/goose-mcp/src/developer/editor_models/openai_compatible_editor.rs index 313b7a873d97..4fe64ac7abfa 100644 --- a/crates/goose-mcp/src/developer/editor_models/openai_compatible_editor.rs +++ b/crates/goose-mcp/src/developer/editor_models/openai_compatible_editor.rs @@ -27,6 +27,7 @@ impl EditorModelImpl for OpenAICompatibleEditor { original_code: &str, _old_str: &str, update_snippet: &str, + _instruction: &str, // Not used by OpenAI compatible editor ) -> Result { eprintln!("Calling OpenAI-compatible Editor API"); diff --git a/crates/goose-mcp/src/developer/editor_models/relace_editor.rs b/crates/goose-mcp/src/developer/editor_models/relace_editor.rs index 3cc40bfb13a3..5d017ead1a3a 100644 --- a/crates/goose-mcp/src/developer/editor_models/relace_editor.rs +++ b/crates/goose-mcp/src/developer/editor_models/relace_editor.rs @@ -27,6 +27,7 @@ impl EditorModelImpl for RelaceEditor { original_code: &str, _old_str: &str, update_snippet: &str, + _instruction: &str, // Not used by Relace editor ) -> Result { eprintln!("Calling Relace Editor API"); diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index ddc81831cda6..0752d1120361 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -259,83 +259,168 @@ impl DeveloperRouter { }), ); - // Create text editor tool with different descriptions based on editor API configuration - let (text_editor_desc, str_replace_command) = if let Some(ref editor) = editor_model { - ( - formatdoc! {r#" - 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 - - `edit_file`: Edit the file with the new content. - - `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 edit_file command, you must specify both `old_str` and `new_str` - {}. - - To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) - and `new_str` (the text to insert). - "#, editor.get_str_replace_description()}, - "edit_file", - ) - } else { - (indoc! {r#" - 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 a string in a file with a new string. - - `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, 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`. - - To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) - and `new_str` (the text to insert). - "#}.to_string(), "str_replace") + // Create text editor tool with different descriptions and schemas based on editor API configuration + let (text_editor_desc, schema_properties) = match &editor_model { + Some(editor) if editor.supports_instruction_parameter() => { + // Editors that support/require instruction parameter (like MorphLLM) + ( + formatdoc! {r#" + 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 + - `edit_file`: Edit the file with the new content. + - `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 edit_file command, you must specify both `old_str` and `new_str` - {}. + **IMPORTANT**: You must also provide an `instruction` parameter with your edit_file command. + + To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) + and `new_str` (the text to insert). + "#, editor.get_str_replace_description()}, + json!({ + "path": { + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.", + "type": "string" + }, + "command": { + "type": "string", + "enum": ["view", "write", "edit_file", "insert", "undo_edit"], + "description": "Allowed options are: `view`, `write`, `edit_file`, `insert`, `undo_edit`." + }, + "view_range": { + "type": "array", + "items": {"type": "integer"}, + "minItems": 2, + "maxItems": 2, + "description": "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." + }, + "insert_line": { + "type": "integer", + "description": "The line number after which to insert the text (0 for beginning of file). This parameter is required when using the insert command." + }, + "old_str": {"type": "string"}, + "new_str": {"type": "string"}, + "instruction": { + "type": "string", + "description": "A single sentence written in the first person describing what you are going to do for the sketched edit. This helps the apply model understand and apply your edit correctly. Use it to disambiguate uncertainty in the edit." + }, + "file_text": {"type": "string"} + }), + ) + } + Some(editor) => { + // Other editors (OpenAI, etc...) - no instruction parameter + ( + formatdoc! {r#" + 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 + - `edit_file`: Edit the file with the new content. + - `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 edit_file command, you must specify both `old_str` and `new_str` - {}. + + To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) + and `new_str` (the text to insert). + "#, editor.get_str_replace_description()}, + json!({ + "path": { + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.", + "type": "string" + }, + "command": { + "type": "string", + "enum": ["view", "write", "edit_file", "insert", "undo_edit"], + "description": "Allowed options are: `view`, `write`, `edit_file`, `insert`, `undo_edit`." + }, + "view_range": { + "type": "array", + "items": {"type": "integer"}, + "minItems": 2, + "maxItems": 2, + "description": "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." + }, + "insert_line": { + "type": "integer", + "description": "The line number after which to insert the text (0 for beginning of file). This parameter is required when using the insert command." + }, + "old_str": {"type": "string"}, + "new_str": {"type": "string"}, + "file_text": {"type": "string"} + }), + ) + } + None => { + // No editor - traditional str_replace + ( + indoc! {r#" + 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 a string in a file with a new string. + - `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, 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`. + + To use the insert command, you must specify both `insert_line` (the line number after which to insert, 0 for beginning) + and `new_str` (the text to insert). + "#}.to_string(), + json!({ + "path": { + "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.", + "type": "string" + }, + "command": { + "type": "string", + "enum": ["view", "write", "str_replace", "insert", "undo_edit"], + "description": "Allowed options are: `view`, `write`, `str_replace`, `insert`, `undo_edit`." + }, + "view_range": { + "type": "array", + "items": {"type": "integer"}, + "minItems": 2, + "maxItems": 2, + "description": "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." + }, + "insert_line": { + "type": "integer", + "description": "The line number after which to insert the text (0 for beginning of file). This parameter is required when using the insert command." + }, + "old_str": {"type": "string"}, + "new_str": {"type": "string"}, + "file_text": {"type": "string"} + }) + ) + } }; let text_editor_tool = Tool::new( "text_editor".to_string(), - text_editor_desc.to_string(), + text_editor_desc, json!({ "type": "object", "required": ["command", "path"], - "properties": { - "path": { - "description": "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.", - "type": "string" - }, - "command": { - "type": "string", - "enum": ["view", "write", str_replace_command, "insert", "undo_edit"], - "description": format!("Allowed options are: `view`, `write`, `{}`, `insert`, `undo_edit`.", str_replace_command) - }, - "view_range": { - "type": "array", - "items": {"type": "integer"}, - "minItems": 2, - "maxItems": 2, - "description": "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." - }, - "insert_line": { - "type": "integer", - "description": "The line number after which to insert the text (0 for beginning of file). This parameter is required when using the insert command." - }, - "old_str": {"type": "string"}, - "new_str": {"type": "string"}, - "file_text": {"type": "string"} - } + "properties": schema_properties }), None, ); @@ -881,7 +966,21 @@ impl DeveloperRouter { ToolError::InvalidParameters("Missing 'new_str' parameter".into()) })?; - self.text_editor_replace(&path, old_str, new_str).await + // Only extract instruction parameter when editors that support it are being used + let instruction = match &self.editor_model { + Some(editor) if editor.supports_instruction_parameter() => params + .get("instruction") + .and_then(|v| v.as_str()) + .ok_or_else(|| { + ToolError::InvalidParameters( + "Missing 'instruction' parameter required for this editor".into(), + ) + })?, + _ => "", // Empty instruction for other editors or no editor + }; + + self.text_editor_replace(&path, old_str, new_str, instruction) + .await } "insert" => { let insert_line = params @@ -1082,6 +1181,7 @@ impl DeveloperRouter { path: &PathBuf, old_str: &str, new_str: &str, + instruction: &str, ) -> Result, ToolError> { // Check if file exists and is active if !path.exists() { @@ -1100,7 +1200,10 @@ impl DeveloperRouter { // Editor API path - save history then call API directly self.save_file_history(path)?; - match editor.edit_code(&content, old_str, new_str).await { + match editor + .edit_code(&content, old_str, new_str, instruction) + .await + { Ok(updated_content) => { // Write the updated content directly let normalized_content = normalize_line_endings(&updated_content); diff --git a/documentation/docs/guides/enhanced-code-editing.md b/documentation/docs/guides/enhanced-code-editing.md index 6c894f688b6a..900dfc52cab4 100644 --- a/documentation/docs/guides/enhanced-code-editing.md +++ b/documentation/docs/guides/enhanced-code-editing.md @@ -50,7 +50,7 @@ export GOOSE_EDITOR_MODEL="claude-3-5-sonnet-20241022" ```bash export GOOSE_EDITOR_API_KEY="sk-..." export GOOSE_EDITOR_HOST="https://api.morphllm.com/v1" -export GOOSE_EDITOR_MODEL="morph-v0" +export GOOSE_EDITOR_MODEL="morph-v3-large" ``` **Relace:**