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 |
crates/goose-acp/src/server.rs
Outdated
| 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; |
crates/goose-acp/src/tools.rs
Outdated
| .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, | |
| )) |
crates/goose-acp/src/tools.rs
Outdated
| 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).
There was a problem hiding this comment.
This seems like a valid concern, no?
| } | ||
|
|
||
| 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 |
codefromthecrypt
left a comment
There was a problem hiding this comment.
This is a clever and elegant way to approach the challenge. While most agents do a tool dispatch solution, this replaces the entire tools, allowing a more direct implementation.
I have a branch which has tests etc as well the read path and I will port over this.
Great job!
rabi
left a comment
There was a problem hiding this comment.
If we all feel this is the way to go, then it's ok. My major reason for not approaching it this way as to not maintaining two different implementations of developer tools. If the plan is to move CLI/GUI to ACP clients, then this duplication is temporary and fine. Otherwise, a DeveloperBackend trait with capability-gated backend wiring would atleast keep the tool definitions in sync.
| use serde::{de::DeserializeOwned, Deserialize, Serialize}; | ||
| use tokio_util::sync::CancellationToken; | ||
|
|
||
| struct Capabilities { |
There was a problem hiding this comment.
Should we not use ClientCapabilities rather than having a separate struct?
| 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.
This seems like a valid concern, no?
| let config_path = self.config_dir.join(CONFIG_YAML_NAME); | ||
| if let Ok(config_file) = Config::new(&config_path, "goose") { | ||
| let extensions = get_enabled_extensions_with_config(&config_file); | ||
| let mut extensions = get_enabled_extensions_with_config(&config_file); |
There was a problem hiding this comment.
The exclude_developer check seems to filter only config-loaded extensions. If passed via --with-builtin developer, it would create duplicate/conflicting tools? Should we remove the --with-builtin flag completely now that user is expected to enable extensions via config and then we filter based on that?
| goose_mode: goose::config::GooseMode, | ||
| disable_session_naming: bool, | ||
| builtins: Vec<String>, | ||
| client_capabilities: Mutex<ClientCapabilities>, |
There was a problem hiding this comment.
Maybe we use tokio::sync::OnceCell? As it's only set during initialize and then can be read freely?
I agree with the reasoning, but one problem which we have now is that the ACP editor methods are a bit more constrained than how the new developer tools work in goose today. For example, if we want all files reads to go through ACP when you are connected to an editor, we would not want goose to read using the shell ( This does mean that the tools seen by the model are different when you are using goose with an ACP-enabled editor vs as a standalone agent. And maybe that's okay? Arguably, as a user, I'd expect goose to be more autonomous when run on its own, and more interactive through an editor. But I'm open to arguments in the other direction. |
|
@jamadeo #7668 I raised last night swaps out the tool impl but re-uses exactly the same tool defs, so I think this balances the debate, correct me if I'm wrong. There's a whole lot more ACP work to complete, so hoping we can close this topic out. There are a lot more complicated things left and time budget is a thing or we'll never get through the spec. |
|
most of this is in #7668 |
This adds tools to use ACP's file system and terminals methods to call back to the ACP client to perform file edits and shell commands. It's enabled when the client advertises this capability.
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