Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an --acp-editor-tools flag to goose-acp that replaces goose's standard developer tools with ACP-based editor tools. These tools use the Agent Client Protocol's file system and terminal methods to delegate file operations and shell commands back to the ACP client (like Zed editor), providing a better integrated experience.
Changes:
- Introduces
ToolCallContextstruct to pass session ID, working directory, and tool call request ID through the tool dispatch chain - Refactors all
call_toolimplementations to accept context instead of individual parameters - Adds new ACP tools module implementing read, write, str_replace, insert, and shell tools using ACP protocol
- Refactors text editor functions to separate in-memory logic from file I/O for reusability
- Updates extension manager to expose tools unprefixed when only one client is active
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/goose/src/agents/tool_execution.rs | Adds ToolCallContext struct to encapsulate tool call parameters |
| crates/goose/src/agents/extension_manager.rs | Updates dispatch_tool_call to use ToolCallContext; fixes tool prefixing logic |
| crates/goose/src/agents/agent.rs | Creates ToolCallContext when dispatching tool calls |
| crates/goose/src/agents/mcp_client.rs | Updates McpClientTrait call_tool signature to use ToolCallContext |
| crates/goose/src/agents/platform_extensions/*.rs | Updates all platform extension call_tool implementations |
| crates/goose-acp/src/tools.rs | New module implementing ACP-based file and shell tools |
| crates/goose-acp/src/server.rs | Adds acp_editor_tools flag and conditionally adds ACP tools |
| crates/goose-acp/src/server_factory.rs | Passes acp_editor_tools flag through configuration |
| crates/goose-mcp/src/developer/text_editor.rs | Refactors replace/insert to extract in-memory logic |
| crates/goose-cli/src/cli.rs | Adds CLI flag for acp_editor_tools |
| crates/goose-server/src/routes/agent.rs | Updates tool call dispatch to use ToolCallContext |
| All test files | Updates test code to construct ToolCallContext |
| let is_acp_tool = tool_request | ||
| .and_then(|req| req.tool_call.as_ref().ok()) | ||
| // TODO: something more robust here | ||
| .is_some_and(|req| { | ||
| req.name == "read" | ||
| || req.name == "write" | ||
| || req.name == "str_replace" | ||
| || req.name == "insert" | ||
| || req.name == "shell" | ||
| }); |
There was a problem hiding this comment.
Hard-coding tool names for detection is fragile and creates a maintenance burden. Consider adding a method to AcpTools or a marker to identify these tools programmatically, or store the extension name in the tool metadata and check against "acp-tools" instead.
| ($t:ty) => { | ||
| serde_json::to_value(schema_for!($t)) | ||
| .unwrap() | ||
| .as_object() | ||
| .unwrap() | ||
| .clone() | ||
| }; |
There was a problem hiding this comment.
The schema macro uses unwrap() which will panic at initialization if the schema conversion fails. While this is unlikely to fail for these simple parameter types, consider handling the error more gracefully or using a static assertion approach to catch schema issues at compile time rather than at runtime initialization.
| ($t:ty) => { | |
| serde_json::to_value(schema_for!($t)) | |
| .unwrap() | |
| .as_object() | |
| .unwrap() | |
| .clone() | |
| }; | |
| ($t:ty) => {{ | |
| let value = match serde_json::to_value(schema_for!($t)) { | |
| Ok(v) => v, | |
| Err(_) => serde_json::Value::Object(JsonObject::new()), | |
| }; | |
| match value.as_object() { | |
| Some(map) => map.clone(), | |
| None => JsonObject::new(), | |
| } | |
| }}; |
crates/goose-acp/src/tools.rs
Outdated
| @@ -0,0 +1,400 @@ | |||
| use std::{cell::LazyCell, fmt::Display, future::Future, path::PathBuf}; | |||
There was a problem hiding this comment.
Replace std::cell::LazyCell with once_cell::sync::Lazy to match the codebase convention. The codebase consistently uses once_cell for lazy static initialization throughout (see usage in goose/src/agents/extension_manager.rs, goose-mcp/src/developer/rmcp_developer.rs, and many other files).
| use std::{cell::LazyCell, fmt::Display, future::Future, path::PathBuf}; | |
| use std::{fmt::Display, future::Future, path::PathBuf}; | |
| use once_cell::sync::Lazy; |
| .map_err(|_| { | ||
| ServiceError::McpError(ErrorData::internal_error("failed to replace", None)) |
There was a problem hiding this comment.
Error information is being discarded when mapping to internal error. The ErrorData from text_editor_replace_inmem contains useful details (like "'old_str' must appear exactly once") that would help users understand what went wrong. Consider propagating the original error message instead of using a generic "failed to replace" message.
| .map_err(|_| { | |
| ServiceError::McpError(ErrorData::internal_error("failed to replace", None)) | |
| .map_err(|e| { | |
| ServiceError::McpError(ErrorData::internal_error( | |
| &format!("failed to replace: {e}"), | |
| None, | |
| )) |
| params.position, | ||
| ¶ms.new_str.as_str(), | ||
| ) | ||
| .map_err(|_| ServiceError::McpError(ErrorData::internal_error("failed to insert", None)))?; |
There was a problem hiding this comment.
Error information is being discarded when mapping to internal error. The ErrorData from text_editor_insert_inmem contains useful details (like "Insert line X is beyond the end of the file") that would help users understand what went wrong. Consider propagating the original error message instead of using a generic "failed to insert" message.
| .map_err(|_| ServiceError::McpError(ErrorData::internal_error("failed to insert", None)))?; | |
| .map_err(ServiceError::McpError)?; |
| params: WriteParams, | ||
| ) -> Result<CallToolResult, ServiceError> { | ||
| let path = self.working_dir.join(params.path); | ||
| let content = read_file(&path, &self.cx, self.session_id.clone()).await?; |
There was a problem hiding this comment.
The write operation reads the old file content at line 182, but this is unnecessary unless the file already exists. For new files, this will fail. Consider only reading the old content if the file exists, or handle the error case when the file doesn't exist (which is valid for a write operation).
| } | ||
|
|
||
| fn get_info(&self) -> Option<&InitializeResult> { | ||
| todo!() |
There was a problem hiding this comment.
This unimplemented method will panic if called. Since this implements the McpClientTrait, get_info might be called by the framework. Either implement it with a proper value or return None if this information isn't available for ACP tools.
| todo!() | |
| None |
This adds an option to goose-acp,
--acp-editor-tools, that replaces goose's standard developer tools with ACP based editor tools. These use ACP's file system and terminals methods to call back to the ACP client to perform file edits and shell commands.This makes for a nicer experience when using goose over ACP in an editor like Zed.
Before:
https://github.com/user-attachments/assets/c3ee6a05-a4ba-411b-a881-02ae909b6274
After:
https://github.com/user-attachments/assets/247730ee-c83e-454f-b71c-b4b07630305a