diff --git a/CUSTOM_DISTROS.md b/CUSTOM_DISTROS.md index d4f7c15e..ad59da65 100644 --- a/CUSTOM_DISTROS.md +++ b/CUSTOM_DISTROS.md @@ -418,7 +418,7 @@ For the full ACP specification, see the [Agent Client Protocol documentation](ht **ACP**: - ACP server implementation: `crates/goose/src/acp/server.rs` - CLI integration: `crates/goose-cli/src/cli.rs` (Command::Acp) -- Protocol library: `sacp` crate (Rust implementation of ACP) +- Protocol library: `agent-client-protocol` crate (Rust implementation of ACP) - Test client example: `test_acp_client.py` --- diff --git a/Cargo.lock b/Cargo.lock index febf6e6f..926f7b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,17 +29,54 @@ dependencies = [ ] [[package]] -name = "agent-client-protocol-schema" -version = "0.11.3" +name = "agent-client-protocol" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96daddd0d00f2eab88f8099d38190881bf8d6c5e46b6fa21751f482775d0dba7" +checksum = "2af62fb84df2af0f933d8f5fd78b843fa5eb0ec5a48fa1b528c41951d0bbe36c" +dependencies = [ + "agent-client-protocol-derive", + "agent-client-protocol-schema", + "anyhow", + "futures", + "futures-concurrency", + "jsonrpcmsg", + "rmcp", + "rustc-hash 2.1.1", + "schemars 1.2.1", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + +[[package]] +name = "agent-client-protocol-derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce42c2d3c048c12897eef2e577dfff1e3355c632c9f1625cc953b9df48b44631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "agent-client-protocol-schema" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49bae57dad1c28a362fbdcf7bab0583316a02b45a70792109fced55780a3b63c" dependencies = [ "anyhow", "derive_more", "schemars 1.2.1", "serde", "serde_json", + "serde_with", "strum 0.28.0", + "tracing", ] [[package]] @@ -140,7 +177,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -151,7 +188,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1389,12 +1426,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "boxfnonce" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426" - [[package]] name = "bpaf" version = "0.9.24" @@ -3009,6 +3040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -3165,7 +3197,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3480,7 +3512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4335,6 +4367,7 @@ dependencies = [ name = "goose" version = "1.33.0" dependencies = [ + "agent-client-protocol", "agent-client-protocol-schema", "ahash", "anyhow", @@ -4406,7 +4439,6 @@ dependencies = [ "reqwest 0.13.2", "rmcp", "rubato", - "sacp", "schemars 1.2.1", "sec1", "serde", @@ -4552,8 +4584,8 @@ dependencies = [ name = "goose-sdk" version = "1.33.0" dependencies = [ + "agent-client-protocol", "agent-client-protocol-schema", - "sacp", "schemars 1.2.1", "serde", "serde_json", @@ -5469,7 +5501,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6283,7 +6315,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8239,7 +8271,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8298,7 +8330,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8359,40 +8391,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" -[[package]] -name = "sacp" -version = "11.0.0" -source = "git+https://github.com/agentclientprotocol/symposium-acp?rev=14086c9#14086c93482ea0618ea6ca7a4d1288953569147e" -dependencies = [ - "agent-client-protocol-schema", - "anyhow", - "boxfnonce", - "futures", - "futures-concurrency", - "jsonrpcmsg", - "rmcp", - "rustc-hash 2.1.1", - "sacp-derive", - "schemars 1.2.1", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tracing", - "uuid", -] - -[[package]] -name = "sacp-derive" -version = "11.0.0" -source = "git+https://github.com/agentclientprotocol/symposium-acp?rev=14086c9#14086c93482ea0618ea6ca7a4d1288953569147e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "safetensors" version = "0.4.5" @@ -8454,6 +8452,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.2.1" @@ -8721,8 +8731,17 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", "serde_core", + "serde_json", "serde_with_macros", + "time", ] [[package]] @@ -9103,7 +9122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10248,7 +10267,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11916,7 +11935,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -12418,7 +12437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 757346bf..8998934f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ string_slice = "warn" [workspace.dependencies] rmcp = { version = "1.5.0", features = ["schemars", "auth"] } -agent-client-protocol-schema = { version = "0.11", features = ["unstable"] } -sacp = "11.0.0" +agent-client-protocol-schema = { version = "0.12", features = ["unstable"] } +agent-client-protocol = "0.11" arboard = "3" anyhow = "1.0" async-stream = "0.3" @@ -99,9 +99,6 @@ tree-sitter-typescript = "0.23" [patch.crates-io] v8 = { path = "vendor/v8" } cudaforge = { git = "https://github.com/jbg/cudaforge", rev = "e7c1967340e40673db98dc9e17da0f04834a456f" } -# TODO: switch to crates.io release once it includes the unstable feature and updates org from symposium-dev to agentclientprotocol -sacp = { git = "https://github.com/agentclientprotocol/symposium-acp", rev = "14086c9" } -sacp-derive = { git = "https://github.com/agentclientprotocol/symposium-acp", rev = "14086c9" } # TODO: switch to released version in opentelemetry 0.32.0 # https://github.com/open-telemetry/opentelemetry-rust/issues/3408 opentelemetry = { git = "https://github.com/open-telemetry/opentelemetry-rust", rev = "345cd74a" } diff --git a/crates/goose-acp-macros/src/lib.rs b/crates/goose-acp-macros/src/lib.rs index 532e1aa8..beaa3e62 100644 --- a/crates/goose-acp-macros/src/lib.rs +++ b/crates/goose-acp-macros/src/lib.rs @@ -7,14 +7,14 @@ use syn::{ /// Marks an impl block as containing `#[custom_method(RequestType)]`-annotated handlers. /// -/// The request type must derive `sacp::JsonRpcRequest` with a `#[request(method = "...")]` +/// The request type must derive `agent_client_protocol::JsonRpcRequest` with a `#[request(method = "...")]` /// attribute — the method name is extracted from that type at compile time, eliminating /// duplication between the request struct and the handler. /// /// Generates two methods on the impl: /// /// 1. `handle_custom_request` — a dispatcher that: -/// - Uses `::matches_method` to match incoming methods +/// - Uses `::matches_method` to match incoming methods /// - Parses JSON params into the handler's typed parameter (if any) /// - Serializes the handler's return value to JSON /// @@ -30,14 +30,14 @@ use syn::{ /// ```ignore /// // No params — called for requests with no/empty params /// #[custom_method(GetExtensionsRequest)] -/// async fn on_get_extensions(&self) -> Result { .. } +/// async fn on_get_extensions(&self) -> Result { .. } /// /// // Typed params — JSON params auto-deserialized /// #[custom_method(GetSessionRequest)] -/// async fn on_get_session(&self, req: GetSessionRequest) -> Result { .. } +/// async fn on_get_session(&self, req: GetSessionRequest) -> Result { .. } /// ``` /// -/// The return type must be `Result` where `T: Serialize`. +/// The return type must be `Result` where `T: Serialize`. #[proc_macro_attribute] pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut impl_block = parse_macro_input!(item as ItemImpl); @@ -89,21 +89,21 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { match &route.param_type { Some(_) => { quote! { - if <#req_type as sacp::JsonRpcMessage>::matches_method(method) { + if <#req_type as agent_client_protocol::JsonRpcMessage>::matches_method(method) { let req = serde_json::from_value(params) - .map_err(|e| sacp::Error::invalid_params().data(e.to_string()))?; + .map_err(|e| agent_client_protocol::Error::invalid_params().data(e.to_string()))?; let result = self.#fn_ident(req).await?; return serde_json::to_value(&result) - .map_err(|e| sacp::Error::internal_error().data(e.to_string())); + .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string())); } } } None => { quote! { - if <#req_type as sacp::JsonRpcMessage>::matches_method(method) { + if <#req_type as agent_client_protocol::JsonRpcMessage>::matches_method(method) { let result = self.#fn_ident().await?; return serde_json::to_value(&result) - .map_err(|e| sacp::Error::internal_error().data(e.to_string())); + .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string())); } } } @@ -169,7 +169,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { { let dummy = <#req_type as Default>::default(); crate::custom_requests::CustomMethodSchema { - method: sacp::JsonRpcMessage::method(&dummy).to_string(), + method: agent_client_protocol::JsonRpcMessage::method(&dummy).to_string(), params_schema: #params_expr, params_type_name: #params_name_expr, response_schema: #response_expr, @@ -186,9 +186,9 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { &self, method: &str, params: serde_json::Value, - ) -> Result { + ) -> Result { #(#arms)* - Err(sacp::Error::method_not_found()) + Err(agent_client_protocol::Error::method_not_found()) } }; diff --git a/crates/goose-sdk/Cargo.toml b/crates/goose-sdk/Cargo.toml index 18fa86da..5cdfc6bb 100644 --- a/crates/goose-sdk/Cargo.toml +++ b/crates/goose-sdk/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true description = "Rust SDK for talking to Goose over the Agent Client Protocol (ACP)" [dependencies] -sacp = { workspace = true, features = ["unstable"] } +agent-client-protocol = { workspace = true, features = ["unstable"] } agent-client-protocol-schema = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -20,5 +20,5 @@ tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat", "rt"] } [package.metadata.cargo-machete] -# Used to provide extras imports for sacp +# Used to provide extras imports for agent-client-protocol ignored = ["agent-client-protocol-schema"] diff --git a/crates/goose-sdk/examples/acp_client.rs b/crates/goose-sdk/examples/acp_client.rs index 8ef5b29c..9ffcdc51 100644 --- a/crates/goose-sdk/examples/acp_client.rs +++ b/crates/goose-sdk/examples/acp_client.rs @@ -19,13 +19,13 @@ //! cargo run -p goose-sdk --example acp_client -- --goose-bin ./target/debug/goose "Explain Rust's ownership model in one sentence" //! ``` -use goose_sdk::custom_requests::GetExtensionsRequest; -use sacp::schema::{ +use agent_client_protocol::schema::{ ContentBlock, InitializeRequest, ProtocolVersion, RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome, SessionNotification, SessionUpdate, }; -use sacp::{Client, ConnectionTo}; +use agent_client_protocol::{Client, ConnectionTo}; +use goose_sdk::custom_requests::GetExtensionsRequest; use std::path::PathBuf; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; @@ -48,7 +48,8 @@ async fn main() -> Result<(), Box> { let child_stdin = child.stdin.take().expect("stdin should be piped"); let child_stdout = child.stdout.take().expect("stdout should be piped"); - let transport = sacp::ByteStreams::new(child_stdin.compat_write(), child_stdout.compat()); + let transport = + agent_client_protocol::ByteStreams::new(child_stdin.compat_write(), child_stdout.compat()); let prompt_clone = prompt.clone(); @@ -76,7 +77,7 @@ async fn main() -> Result<(), Box> { } Ok(()) }, - sacp::on_receive_notification!(), + agent_client_protocol::on_receive_notification!(), ) // Auto-approve all permission requests .on_receive_request( @@ -92,39 +93,42 @@ async fn main() -> Result<(), Box> { )), } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) - .connect_with(transport, async move |cx: ConnectionTo| { - // Step 1: Initialize - eprintln!("🤝 Initializing..."); - let init_response = cx - .send_request(InitializeRequest::new(ProtocolVersion::LATEST)) - .block_task() - .await?; - eprintln!("✓ Agent initialized: {:?}", init_response.agent_info); + .connect_with( + transport, + async move |cx: ConnectionTo| { + // Step 1: Initialize + eprintln!("🤝 Initializing..."); + let init_response = cx + .send_request(InitializeRequest::new(ProtocolVersion::LATEST)) + .block_task() + .await?; + eprintln!("✓ Agent initialized: {:?}", init_response.agent_info); - let response = cx - .send_request(GetExtensionsRequest {}) - .block_task() - .await?; - eprintln!("Extensions: {:?}", response.extensions); + let response = cx + .send_request(GetExtensionsRequest {}) + .block_task() + .await?; + eprintln!("Extensions: {:?}", response.extensions); - // Step 2: Create a session and send the prompt - eprintln!("💬 Sending prompt: \"{}\"", prompt_clone); - cx.build_session_cwd()? - .block_task() - .run_until(async |mut session| { - session.send_prompt(&prompt_clone)?; - let response = session.read_to_string().await?; + // Step 2: Create a session and send the prompt + eprintln!("💬 Sending prompt: \"{}\"", prompt_clone); + cx.build_session_cwd()? + .block_task() + .run_until(async |mut session| { + session.send_prompt(&prompt_clone)?; + let response = session.read_to_string().await?; - // read_to_string collects text; we already printed chunks above, - // so just print a newline to finish. - println!(); - eprintln!("✅ Done ({} chars)", response.len()); - Ok(()) - }) - .await - }) + // read_to_string collects text; we already printed chunks above, + // so just print a newline to finish. + println!(); + eprintln!("✅ Done ({} chars)", response.len()); + Ok(()) + }) + .await + }, + ) .await?; let _ = child.kill().await; diff --git a/crates/goose-sdk/src/custom_requests.rs b/crates/goose-sdk/src/custom_requests.rs index 9ac41aa7..8863e402 100644 --- a/crates/goose-sdk/src/custom_requests.rs +++ b/crates/goose-sdk/src/custom_requests.rs @@ -1,4 +1,4 @@ -use sacp::{JsonRpcRequest, JsonRpcResponse}; +use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 658626e6..7dabd0f8 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -147,7 +147,7 @@ dashmap = "6.1" ahash = "0.8" tokio-util = { workspace = true, features = ["compat"] } agent-client-protocol-schema = { workspace = true } -sacp = { workspace = true, features = ["unstable"] } +agent-client-protocol = { workspace = true, features = ["unstable"] } unicode-normalization = "0.1" # For local Whisper transcription (optional, behind "local-inference" feature) @@ -258,7 +258,7 @@ path = "src/bin/generate_acp_schema.rs" ignored = [ # Used only on windows "winapi", - # Used to provide extras imports for sacp + # Used to provide extras imports for agent-client-protocol "agent-client-protocol-schema", # Used via http transport "http-body-util", diff --git a/crates/goose/src/acp/common.rs b/crates/goose/src/acp/common.rs index 803dd718..62184170 100644 --- a/crates/goose/src/acp/common.rs +++ b/crates/goose/src/acp/common.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use crate::permission::Permission; -use sacp::schema::{ +use agent_client_protocol::schema::{ PermissionOption, PermissionOptionKind, RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome, }; @@ -110,7 +110,9 @@ fn find_option(options: &[PermissionOption], kind: PermissionOptionKind) -> Opti #[cfg(test)] mod tests { use super::*; - use sacp::schema::{PermissionOptionId, ToolCallId, ToolCallUpdate, ToolCallUpdateFields}; + use agent_client_protocol::schema::{ + PermissionOptionId, ToolCallId, ToolCallUpdate, ToolCallUpdateFields, + }; use test_case::test_case; fn make_request(options: Vec) -> RequestPermissionRequest { diff --git a/crates/goose/src/acp/fs.rs b/crates/goose/src/acp/fs.rs index 89381a25..eb3f3be9 100644 --- a/crates/goose/src/acp/fs.rs +++ b/crates/goose/src/acp/fs.rs @@ -5,17 +5,17 @@ use crate::agents::platform_extensions::developer::edit::{ }; use crate::agents::platform_extensions::developer::shell::{ShellParams, OUTPUT_LIMIT_BYTES}; use crate::agents::platform_extensions::developer::DeveloperClient; -use agent_client_protocol_schema::TerminalId; -use async_trait::async_trait; -use fs_err as fs; -use rmcp::model::{CallToolResult, Content as RmcpContent, Tool, ToolAnnotations}; -use sacp::schema::{ +use agent_client_protocol::schema::{ CreateTerminalRequest, Diff, KillTerminalRequest, ReadTextFileRequest, ReleaseTerminalRequest, SessionId, SessionNotification, SessionUpdate, Terminal, TerminalOutputRequest, ToolCallContent, ToolCallId, ToolCallLocation, ToolCallUpdate, ToolCallUpdateFields, ToolKind, WaitForTerminalExitRequest, WriteTextFileRequest, }; -use sacp::{Client, ConnectionTo}; +use agent_client_protocol::{Client, ConnectionTo}; +use agent_client_protocol_schema::TerminalId; +use async_trait::async_trait; +use fs_err as fs; +use rmcp::model::{CallToolResult, Content as RmcpContent, Tool, ToolAnnotations}; use schemars::schema_for; use std::path::Path; use std::sync::Arc; @@ -311,7 +311,7 @@ impl AcpTools { &self, terminal_id: &TerminalId, timeout_secs: Option, - ) -> Result { + ) -> Result { let wait_fut = self .cx .send_request(WaitForTerminalExitRequest::new( diff --git a/crates/goose/src/acp/provider.rs b/crates/goose/src/acp/provider.rs index 18f887a9..142a3195 100644 --- a/crates/goose/src/acp/provider.rs +++ b/crates/goose/src/acp/provider.rs @@ -1,10 +1,4 @@ -use agent_client_protocol_schema::Usage as AcpUsage; -use agent_client_protocol_schema::AGENT_METHOD_NAMES; -use anyhow::{Context, Result}; -use async_stream::try_stream; -use futures::future::BoxFuture; -use rmcp::model::{CallToolRequestParams, CallToolResult, Content as RmcpContent, Role, Tool}; -use sacp::schema::{ +use agent_client_protocol::schema::{ ClientCapabilities, CloseSessionRequest, ContentBlock, ContentChunk, EnvVariable, HttpHeader, ImageContent, InitializeRequest, InitializeResponse, McpCapabilities, McpServer, McpServerHttp, McpServerStdio, NewSessionRequest, NewSessionResponse, PromptRequest, PromptResponse, @@ -14,7 +8,13 @@ use sacp::schema::{ SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModeResponse, StopReason, TextContent, ToolCallContent, ToolCallStatus, ToolKind, }; -use sacp::{Agent, Client, ConnectionTo}; +use agent_client_protocol::{Agent, Client, ConnectionTo}; +use agent_client_protocol_schema::Usage as AcpUsage; +use agent_client_protocol_schema::AGENT_METHOD_NAMES; +use anyhow::{Context, Result}; +use async_stream::try_stream; +use futures::future::BoxFuture; +use rmcp::model::{CallToolRequestParams, CallToolResult, Content as RmcpContent, Role, Tool}; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::path::PathBuf; @@ -191,7 +191,7 @@ impl AcpProvider { model: ModelConfig, goose_mode: GooseMode, config: AcpProviderConfig, - transport: impl sacp::ConnectTo + 'static, + transport: impl agent_client_protocol::ConnectTo + 'static, ) -> Result { Self::start( name, @@ -651,7 +651,8 @@ impl AcpClientLoop { if let Some(stderr) = child.stderr.take() { tokio::spawn(forward_child_stderr(stderr)); } - let transport = sacp::ByteStreams::new(stdin.compat_write(), stdout.compat()); + let transport = + agent_client_protocol::ByteStreams::new(stdin.compat_write(), stdout.compat()); let result = self.run(transport, rx, init_tx).await; let _ = child.kill().await; let _ = child.wait().await; @@ -660,7 +661,7 @@ impl AcpClientLoop { async fn run( self, - transport: impl sacp::ConnectTo + 'static, + transport: impl agent_client_protocol::ConnectTo + 'static, rx: &mut mpsc::Receiver, init_tx: oneshot::Sender>, ) -> Result<()> { @@ -837,7 +838,7 @@ impl AcpClientLoop { Ok(()) } }, - sacp::on_receive_notification!(), + agent_client_protocol::on_receive_notification!(), ) .on_receive_request( { @@ -850,17 +851,18 @@ impl AcpClientLoop { .ok() .as_ref() .and_then(|g| g.as_ref().cloned()); - let tx = handler.ok_or_else(sacp::Error::internal_error)?; + let tx = + handler.ok_or_else(agent_client_protocol::Error::internal_error)?; if tx.is_closed() { - return Err(sacp::Error::internal_error()); + return Err(agent_client_protocol::Error::internal_error()); } tx.try_send(AcpUpdate::PermissionRequest { request: Box::new(request), response_tx, }) - .map_err(|_| sacp::Error::internal_error())?; + .map_err(|_| agent_client_protocol::Error::internal_error())?; let response = response_rx.await.unwrap_or_else(|_| { RequestPermissionResponse::new(RequestPermissionOutcome::Cancelled) @@ -868,7 +870,7 @@ impl AcpClientLoop { responder.respond(response) } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .connect_with(transport, async move |cx: ConnectionTo| { handle_requests(config, goose_mode, cx, rx, prompt_response_tx, init_tx).await @@ -956,7 +958,7 @@ async fn handle_requests( rx: &mut mpsc::Receiver, prompt_response_tx: Arc>>>, init_tx: oneshot::Sender>, -) -> Result<(), sacp::Error> { +) -> Result<(), agent_client_protocol::Error> { let mut init_tx = Some(init_tx); let client_capabilities = ClientCapabilities::new(); @@ -972,7 +974,7 @@ async fn handle_requests( if let Some(tx) = init_tx.take() { let _ = tx.send(Err(anyhow::anyhow!(message.clone()))); } - sacp::Error::internal_error().data(message) + agent_client_protocol::Error::internal_error().data(message) })?; let supports_close = init_response @@ -1031,7 +1033,7 @@ async fn handle_requests( value, response_tx, } => { - let value_id = sacp::schema::SessionConfigValueId::new(value); + let value_id = agent_client_protocol::schema::SessionConfigValueId::new(value); let req = SetSessionConfigOptionRequest::new(session_id, config_id, value_id); let result: Result<()> = cx .send_request(req) @@ -1472,7 +1474,7 @@ fn permission_decision_from_mode(goose_mode: GooseMode) -> Option &str { @@ -1812,12 +1814,12 @@ mod tests { #[test_case( NewSessionResponse::new("s1") - .models(sacp::schema::SessionModelState::new( + .models(agent_client_protocol::schema::SessionModelState::new( "default", vec![ - sacp::schema::ModelInfo::new("default", "Default (recommended)"), - sacp::schema::ModelInfo::new("sonnet", "Sonnet"), - sacp::schema::ModelInfo::new("haiku", "Haiku"), + agent_client_protocol::schema::ModelInfo::new("default", "Default (recommended)"), + agent_client_protocol::schema::ModelInfo::new("sonnet", "Sonnet"), + agent_client_protocol::schema::ModelInfo::new("haiku", "Haiku"), ], )) .config_options(vec![ @@ -1833,12 +1835,12 @@ mod tests { )] #[test_case( NewSessionResponse::new("s1") - .models(sacp::schema::SessionModelState::new( + .models(agent_client_protocol::schema::SessionModelState::new( "auto-gemini-3", vec![ - sacp::schema::ModelInfo::new("auto-gemini-3", "Auto (Gemini 3)"), - sacp::schema::ModelInfo::new("auto-gemini-2.5", "Auto (Gemini 2.5)"), - sacp::schema::ModelInfo::new("gemini-2.5-pro", "gemini-2.5-pro"), + agent_client_protocol::schema::ModelInfo::new("auto-gemini-3", "Auto (Gemini 3)"), + agent_client_protocol::schema::ModelInfo::new("auto-gemini-2.5", "Auto (Gemini 2.5)"), + agent_client_protocol::schema::ModelInfo::new("gemini-2.5-pro", "gemini-2.5-pro"), ], )) => Ok(("auto-gemini-3".to_string(), vec!["auto-gemini-3".to_string(), "auto-gemini-2.5".to_string(), "gemini-2.5-pro".to_string()])) @@ -1901,16 +1903,16 @@ mod tests { #[test] fn acp_tool_call_content_handles_text_diff_terminal_and_image() { - use sacp::schema::{Diff, Terminal, TerminalId, TextContent}; + use agent_client_protocol::schema::{Diff, Terminal, TerminalId, TextContent}; let diff_block = ToolCallContent::Diff( Diff::new(std::path::PathBuf::from("/tmp/file.txt"), "new\n").old_text("old\n"), ); let terminal_block = ToolCallContent::Terminal(Terminal::new(TerminalId::new("term-7"))); - let text_block = ToolCallContent::Content(sacp::schema::Content::new(ContentBlock::Text( - TextContent::new("hello"), - ))); - let image_block = ToolCallContent::Content(sacp::schema::Content::new( + let text_block = ToolCallContent::Content(agent_client_protocol::schema::Content::new( + ContentBlock::Text(TextContent::new("hello")), + )); + let image_block = ToolCallContent::Content(agent_client_protocol::schema::Content::new( ContentBlock::Image(ImageContent::new("base64data", "image/png")), )); diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index 09952435..60c5c70d 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -24,15 +24,7 @@ use crate::providers::inventory::{ use crate::session::session_manager::SessionType; use crate::session::{EnabledExtensionsState, Session, SessionManager}; use crate::utils::sanitize_unicode_tags; -use anyhow::Result; -use fs_err as fs; -use futures::future::{BoxFuture, Either}; -use futures::stream::{self, StreamExt}; -use futures::FutureExt; -use rmcp::model::{ - AnnotateAble, CallToolResult, RawContent, RawTextContent, ResourceContents, Role, -}; -use sacp::schema::{ +use agent_client_protocol::schema::{ AgentCapabilities, Annotations, AuthMethod, AuthMethodAgent, AuthenticateRequest, AuthenticateResponse, BlobResourceContents, CancelNotification, CloseSessionRequest, CloseSessionResponse, ConfigOptionUpdate, Content, ContentBlock, ContentChunk, @@ -51,11 +43,19 @@ use sacp::schema::{ ToolCallLocation, ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, Usage, UsageUpdate, }; -use sacp::util::MatchDispatchFrom; -use sacp::{ +use agent_client_protocol::util::MatchDispatchFrom; +use agent_client_protocol::{ Agent as SacpAgent, ByteStreams, Client, ConnectionTo, Dispatch, HandleDispatchFrom, Handled, Responder, }; +use anyhow::Result; +use fs_err as fs; +use futures::future::{BoxFuture, Either}; +use futures::stream::{self, StreamExt}; +use futures::FutureExt; +use rmcp::model::{ + AnnotateAble, CallToolResult, RawContent, RawTextContent, ResourceContents, Role, +}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; use std::panic::AssertUnwindSafe; @@ -89,31 +89,35 @@ pub type AcpProviderFactory = Arc< + Sync, >; -/// Convenience conversions from any `Display` error into an `sacp::Error`. +/// Convenience conversions from any `Display` error into an `agent_client_protocol::Error`. /// /// Replaces the repetitive `.internal_err()` /// pattern. Use `.internal_err()?` for server-side failures and `.invalid_params_err()?` /// for bad client input. For custom messages use `.internal_err_ctx("context")?`. #[allow(dead_code)] trait ResultExt { - fn internal_err(self) -> Result; - fn invalid_params_err(self) -> Result; - fn internal_err_ctx(self, context: &str) -> Result; - fn invalid_params_err_ctx(self, context: &str) -> Result; + fn internal_err(self) -> Result; + fn invalid_params_err(self) -> Result; + fn internal_err_ctx(self, context: &str) -> Result; + fn invalid_params_err_ctx(self, context: &str) -> Result; } impl ResultExt for Result { - fn internal_err(self) -> Result { - self.map_err(|e| sacp::Error::internal_error().data(e.to_string())) + fn internal_err(self) -> Result { + self.map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string())) } - fn invalid_params_err(self) -> Result { - self.map_err(|e| sacp::Error::invalid_params().data(e.to_string())) + fn invalid_params_err(self) -> Result { + self.map_err(|e| agent_client_protocol::Error::invalid_params().data(e.to_string())) } - fn internal_err_ctx(self, context: &str) -> Result { - self.map_err(|e| sacp::Error::internal_error().data(format!("{context}: {e}"))) + fn internal_err_ctx(self, context: &str) -> Result { + self.map_err(|e| { + agent_client_protocol::Error::internal_error().data(format!("{context}: {e}")) + }) } - fn invalid_params_err_ctx(self, context: &str) -> Result { - self.map_err(|e| sacp::Error::invalid_params().data(format!("{context}: {e}"))) + fn invalid_params_err_ctx(self, context: &str) -> Result { + self.map_err(|e| { + agent_client_protocol::Error::invalid_params().data(format!("{context}: {e}")) + }) } } @@ -937,11 +941,13 @@ async fn resolve_provider_and_model( resolve_provider_and_model_from_config(&config, goose_session).await } -fn build_mode_state(current_mode: GooseMode) -> Result { +fn build_mode_state( + current_mode: GooseMode, +) -> Result { let mut available = Vec::with_capacity(GooseMode::VARIANTS.len()); for &name in GooseMode::VARIANTS { let goose_mode: GooseMode = name.parse().map_err(|_| { - sacp::Error::internal_error() // impossible but satisfy linters + agent_client_protocol::Error::internal_error() // impossible but satisfy linters .data(format!("Failed to parse GooseMode variant: {}", name)) })?; let mut mode = SessionMode::new(SessionModeId::new(name), name); @@ -1085,7 +1091,7 @@ impl GooseAcpAgent { Config::new(self.config_dir.join(CONFIG_YAML_NAME), "goose").map_err(Into::into) } - fn config(&self) -> Result { + fn config(&self) -> Result { self.load_config().internal_err_ctx("Failed to read config") } @@ -1525,8 +1531,12 @@ impl GooseAcpAgent { roles .iter() .filter_map(|r| match r { - sacp::schema::Role::Assistant => Some(Role::Assistant), - sacp::schema::Role::User => Some(Role::User), + agent_client_protocol::schema::Role::Assistant => { + Some(Role::Assistant) + } + agent_client_protocol::schema::Role::User => { + Some(Role::User) + } _ => None, }) .collect() @@ -1585,7 +1595,7 @@ impl GooseAcpAgent { agent: &Arc, session: &mut GooseAcpSession, cx: &ConnectionTo, - ) -> Result<(), sacp::Error> { + ) -> Result<(), agent_client_protocol::Error> { match content_item { MessageContent::Text(text) => { cx.send_notification(SessionNotification::new( @@ -1657,7 +1667,7 @@ impl GooseAcpAgent { message_id: Option<&str>, session: &mut GooseAcpSession, cx: &ConnectionTo, - ) -> Result<(), sacp::Error> { + ) -> Result<(), agent_client_protocol::Error> { session .tool_requests .insert(tool_request.id.clone(), tool_request.clone()); @@ -1825,7 +1835,7 @@ impl GooseAcpAgent { message_id: Option<&str>, session: &mut GooseAcpSession, cx: &ConnectionTo, - ) -> Result<(), sacp::Error> { + ) -> Result<(), agent_client_protocol::Error> { let status = match &tool_response.tool_result { Ok(result) if result.is_error == Some(true) => ToolCallStatus::Failed, Ok(_) => ToolCallStatus::Completed, @@ -2091,7 +2101,7 @@ impl GooseAcpAgent { tool_name: String, arguments: serde_json::Map, prompt: Option, - ) -> Result<(), sacp::Error> { + ) -> Result<(), agent_client_protocol::Error> { let cx = cx.clone(); let agent = agent.clone(); let session_id = session_id.clone(); @@ -2273,7 +2283,7 @@ impl GooseAcpAgent { async fn on_initialize( &self, args: InitializeRequest, - ) -> Result { + ) -> Result { debug!(?args, "initialize request"); let _ = self @@ -2310,7 +2320,7 @@ impl GooseAcpAgent { &self, cx: &ConnectionTo, args: NewSessionRequest, - ) -> Result { + ) -> Result { debug!(?args, "new session request"); let t_start = std::time::Instant::now(); @@ -2444,7 +2454,7 @@ impl GooseAcpAgent { cwd: std::path::PathBuf, provider_name: Option<&str>, model_name: Option<&str>, - ) -> Result { + ) -> Result { let goose_session = self .session_manager .create_session( @@ -2484,11 +2494,13 @@ impl GooseAcpAgent { &self, thread_id: &str, cancel_token: Option, - ) -> Result, tokio::sync::watch::Receiver>, sacp::Error> - { + ) -> Result< + Either, tokio::sync::watch::Receiver>, + agent_client_protocol::Error, + > { let mut sessions = self.sessions.lock().await; let session = sessions.get_mut(thread_id).ok_or_else(|| { - sacp::Error::resource_not_found(Some(thread_id.to_string())) + agent_client_protocol::Error::resource_not_found(Some(thread_id.to_string())) .data(format!("Session not found: {}", thread_id)) })?; if let Some(token) = cancel_token { @@ -2506,7 +2518,7 @@ impl GooseAcpAgent { &self, thread_id: &str, cancel_token: Option, - ) -> Result, sacp::Error> { + ) -> Result, agent_client_protocol::Error> { let mut rx = match self.get_agent_or_receiver(thread_id, cancel_token).await? { Either::Left(agent) => return Ok(agent), Either::Right(rx) => rx, @@ -2521,11 +2533,12 @@ impl GooseAcpAgent { }) .await .map_err(|_| { - sacp::Error::internal_error().data("Agent setup task was dropped".to_string()) + agent_client_protocol::Error::internal_error() + .data("Agent setup task was dropped".to_string()) })?; match guard.as_ref().unwrap() { Ok(AgentSetupProgress::FullyReady(agent)) => Ok(agent.clone()), - Err(e) => Err(sacp::Error::internal_error().data(e.clone())), + Err(e) => Err(agent_client_protocol::Error::internal_error().data(e.clone())), // wait_for predicate excludes ProviderReady _ => unreachable!(), } @@ -2537,21 +2550,22 @@ impl GooseAcpAgent { async fn get_session_agent_provider_ready( &self, thread_id: &str, - ) -> Result, sacp::Error> { + ) -> Result, agent_client_protocol::Error> { let mut rx = match self.get_agent_or_receiver(thread_id, None).await? { Either::Left(agent) => return Ok(agent), Either::Right(rx) => rx, }; // Any signal (ProviderReady, FullyReady, or Err) unblocks us. let guard = rx.wait_for(|v| v.is_some()).await.map_err(|_| { - sacp::Error::internal_error().data("Agent setup task was dropped".to_string()) + agent_client_protocol::Error::internal_error() + .data("Agent setup task was dropped".to_string()) })?; match guard.as_ref().unwrap() { Ok(progress) => match progress { AgentSetupProgress::ProviderReady(agent) | AgentSetupProgress::FullyReady(agent) => Ok(agent.clone()), }, - Err(e) => Err(sacp::Error::internal_error().data(e.clone())), + Err(e) => Err(agent_client_protocol::Error::internal_error().data(e.clone())), } } @@ -2559,13 +2573,13 @@ impl GooseAcpAgent { agent: &Arc, mcp_servers: Vec, internal_session_id: &str, - ) -> Result<(), sacp::Error> { + ) -> Result<(), agent_client_protocol::Error> { let mut configs = Vec::with_capacity(mcp_servers.len()); for mcp_server in mcp_servers { let config = match mcp_server_to_extension_config(mcp_server) { Ok(c) => c, Err(msg) => { - return Err(sacp::Error::invalid_params().data(msg)); + return Err(agent_client_protocol::Error::invalid_params().data(msg)); } }; configs.push(config); @@ -2582,7 +2596,7 @@ impl GooseAcpAgent { for result in &results { if !result.success { let error_msg = result.error.as_deref().unwrap_or("unknown error"); - return Err(sacp::Error::internal_error().data(format!( + return Err(agent_client_protocol::Error::internal_error().data(format!( "Failed to add MCP server '{}': {}", result.name, error_msg ))); @@ -2595,7 +2609,7 @@ impl GooseAcpAgent { &self, cx: &ConnectionTo, args: LoadSessionRequest, - ) -> Result { + ) -> Result { debug!(?args, "load session request"); // The ACP session_id IS the thread ID. @@ -2609,7 +2623,7 @@ impl GooseAcpAgent { .get_thread(&thread_id) .await .map_err(|_| { - sacp::Error::resource_not_found(Some(thread_id.clone())) + agent_client_protocol::Error::resource_not_found(Some(thread_id.clone())) .data(format!("Session not found: {}", thread_id)) })?; debug!(target: "perf", sid = %sid, ms = t0.elapsed().as_millis() as u64, "perf: load_session get_thread"); @@ -2618,7 +2632,7 @@ impl GooseAcpAgent { // conversation context (compaction state, full message history, etc.). // The internal session is the source of truth for provider/mode. let internal_session_id = thread.current_session_id.clone().ok_or_else(|| { - sacp::Error::internal_error() + agent_client_protocol::Error::internal_error() .data(format!("Thread {} has no internal session", thread_id)) })?; let t1 = std::time::Instant::now(); @@ -2670,8 +2684,10 @@ impl GooseAcpAgent { audience .iter() .map(|r| match r { - Role::Assistant => sacp::schema::Role::Assistant, - Role::User => sacp::schema::Role::User, + Role::Assistant => { + agent_client_protocol::schema::Role::Assistant + } + Role::User => agent_client_protocol::schema::Role::User, }) .collect::>(), ), @@ -2868,7 +2884,7 @@ impl GooseAcpAgent { &self, cx: &ConnectionTo, args: PromptRequest, - ) -> Result { + ) -> Result { // The ACP session_id IS the thread ID. let thread_id = args.session_id.0.to_string(); let sid = sid_short(&thread_id); @@ -2958,7 +2974,7 @@ impl GooseAcpAgent { let mut sessions = self.sessions.lock().await; let session = sessions.get_mut(&thread_id).ok_or_else(|| { - sacp::Error::invalid_params() + agent_client_protocol::Error::invalid_params() .data(format!("Session not found: {}", thread_id)) })?; @@ -3004,7 +3020,7 @@ impl GooseAcpAgent { } Ok(_) => {} Err(e) => { - return Err(sacp::Error::internal_error() + return Err(agent_client_protocol::Error::internal_error() .data(format!("Error in agent response stream: {}", e))); } } @@ -3059,7 +3075,10 @@ impl GooseAcpAgent { Ok(response) } - async fn on_cancel(&self, args: CancelNotification) -> Result<(), sacp::Error> { + async fn on_cancel( + &self, + args: CancelNotification, + ) -> Result<(), agent_client_protocol::Error> { debug!(?args, "cancel request"); let thread_id = args.session_id.0.to_string(); @@ -3081,7 +3100,7 @@ impl GooseAcpAgent { &self, thread_id: &str, model_id: &str, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(thread_id).await?; let config = self.config()?; let agent = self.get_session_agent_provider_ready(thread_id).await?; @@ -3116,14 +3135,17 @@ impl GooseAcpAgent { Ok(SetSessionModelResponse::new()) } - async fn internal_session_id(&self, thread_id: &str) -> Result { + async fn internal_session_id( + &self, + thread_id: &str, + ) -> Result { self.sessions .lock() .await .get(thread_id) .map(|s| s.internal_session_id.clone()) .ok_or_else(|| { - sacp::Error::resource_not_found(Some(thread_id.to_string())) + agent_client_protocol::Error::resource_not_found(Some(thread_id.to_string())) .data(format!("Session not found: {}", thread_id)) }) } @@ -3132,7 +3154,7 @@ impl GooseAcpAgent { &self, thread_id: &str, f: impl FnOnce(&mut crate::session::ThreadMetadata), - ) -> Result<(), sacp::Error> { + ) -> Result<(), agent_client_protocol::Error> { self.thread_manager .update_metadata(thread_id, f) .await @@ -3143,7 +3165,7 @@ impl GooseAcpAgent { async fn build_config_update( &self, thread_id: &SessionId, - ) -> Result<(SessionNotification, Vec), sacp::Error> { + ) -> Result<(SessionNotification, Vec), agent_client_protocol::Error> { let internal_id = self.internal_session_id(&thread_id.0).await?; let session = self .session_manager @@ -3164,7 +3186,7 @@ impl GooseAcpAgent { .await .internal_err()?; let Some(inventory) = inventory else { - return Err(sacp::Error::internal_error() + return Err(agent_client_protocol::Error::internal_error() .data(format!("Unknown provider inventory: {}", provider_name))); }; let model_state = build_model_state(current_model.as_str(), &inventory); @@ -3187,10 +3209,11 @@ impl GooseAcpAgent { &self, thread_id: &str, mode_id: &str, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(thread_id).await?; let mode = mode_id.parse::().map_err(|_| { - sacp::Error::invalid_params().data(format!("Invalid mode: {}", mode_id)) + agent_client_protocol::Error::invalid_params() + .data(format!("Invalid mode: {}", mode_id)) })?; let agent = self.get_session_agent_provider_ready(thread_id).await?; @@ -3215,7 +3238,7 @@ impl GooseAcpAgent { model_name: Option<&str>, context_limit: Option, request_params: Option>, - ) -> Result<(), sacp::Error> { + ) -> Result<(), agent_client_protocol::Error> { let internal_id = self.internal_session_id(thread_id).await?; let config = self.config()?; let agent = self.get_session_agent_provider_ready(thread_id).await?; @@ -3303,7 +3326,7 @@ impl GooseAcpAgent { Ok(()) } - async fn on_list_sessions(&self) -> Result { + async fn on_list_sessions(&self) -> Result { // Return threads (= ACP sessions), not internal goose sessions. let threads = self .thread_manager @@ -3332,7 +3355,7 @@ impl GooseAcpAgent { &self, cx: &ConnectionTo, args: ForkSessionRequest, - ) -> Result { + ) -> Result { let source_thread_id = &*args.session_id.0; // Fork the thread (copies metadata + messages). @@ -3399,7 +3422,10 @@ impl GooseAcpAgent { Ok(response) } - async fn on_close_session(&self, thread_id: &str) -> Result { + async fn on_close_session( + &self, + thread_id: &str, + ) -> Result { // Tear down the in-memory agent. The thread persists for later session/load. let mut sessions = self.sessions.lock().await; if let Some(session) = sessions.get(thread_id) { @@ -3462,12 +3488,12 @@ pub async fn run(builtins: Vec) -> Result<()> { mod tests { use super::*; use crate::conversation::message::{ToolRequest, ToolResponse}; - use rmcp::model::{CallToolRequestParams, Content as RmcpContent}; - use sacp::schema::{ + use agent_client_protocol::schema::{ EnvVariable, HttpHeader, McpServer, McpServerHttp, McpServerSse, McpServerStdio, PermissionOptionId, ResourceLink, SelectedPermissionOutcome, SessionConfigSelectOption, SessionMode, SessionModeId, SessionModeState, }; + use rmcp::model::{CallToolRequestParams, Content as RmcpContent}; use std::io::Write; use std::path::PathBuf; use tempfile::NamedTempFile; @@ -4367,7 +4393,9 @@ print(\"hello, world\") )) ; "approve mode" )] - fn test_build_mode_state(current_mode: GooseMode) -> Result { + fn test_build_mode_state( + current_mode: GooseMode, + ) -> Result { build_mode_state(current_mode) } diff --git a/crates/goose/src/acp/server/config.rs b/crates/goose/src/acp/server/config.rs index 2dacf9ec..15cdffc7 100644 --- a/crates/goose/src/acp/server/config.rs +++ b/crates/goose/src/acp/server/config.rs @@ -4,7 +4,7 @@ impl GooseAcpAgent { pub(super) async fn on_preferences_read( &self, req: PreferencesReadRequest, - ) -> Result { + ) -> Result { let config = self.config()?; let keys = if req.keys.is_empty() { PREFERENCE_DEFS.iter().map(|def| def.key).collect() @@ -18,7 +18,9 @@ impl GooseAcpAgent { let value = match config.get_param::(def.config_key) { Ok(value) => value, Err(crate::config::ConfigError::NotFound(_)) => serde_json::Value::Null, - Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())), + Err(e) => { + return Err(agent_client_protocol::Error::internal_error().data(e.to_string())) + } }; values.push(PreferenceValue { key, value }); } @@ -29,7 +31,7 @@ impl GooseAcpAgent { pub(super) async fn on_preferences_save( &self, req: PreferencesSaveRequest, - ) -> Result { + ) -> Result { let config = self.config()?; let mut updates = Vec::with_capacity(req.values.len()); @@ -46,7 +48,7 @@ impl GooseAcpAgent { pub(super) async fn on_preferences_remove( &self, req: PreferencesRemoveRequest, - ) -> Result { + ) -> Result { let config = self.config()?; for key in req.keys { let def = preference_def(key)?; @@ -58,7 +60,7 @@ impl GooseAcpAgent { pub(super) async fn on_defaults_read( &self, _req: DefaultsReadRequest, - ) -> Result { + ) -> Result { let config = self.config()?; Ok(DefaultsReadResponse { provider_id: optional_config_string(&config, "GOOSE_PROVIDER")?, @@ -69,10 +71,12 @@ impl GooseAcpAgent { pub(super) async fn on_defaults_save( &self, req: DefaultsSaveRequest, - ) -> Result { + ) -> Result { let provider_id = req.provider_id.trim().to_string(); if provider_id.is_empty() { - return Err(sacp::Error::invalid_params().data("providerId cannot be empty")); + return Err( + agent_client_protocol::Error::invalid_params().data("providerId cannot be empty") + ); } let model_id = req.model_id.and_then(|model| { @@ -89,13 +93,12 @@ impl GooseAcpAgent { .into_iter() .find(|entry| entry.provider_id == provider_id) else { - return Err( - sacp::Error::invalid_params().data(format!("Unknown provider: {provider_id}")) - ); + return Err(agent_client_protocol::Error::invalid_params() + .data(format!("Unknown provider: {provider_id}"))); }; if !entry.configured { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data(format!("Provider is not configured: {provider_id}"))); } @@ -103,7 +106,7 @@ impl GooseAcpAgent { let model_exists = entry.default_model == model_id || entry.models.iter().any(|model| model.id == model_id); if !model_exists { - return Err(sacp::Error::invalid_params().data(format!( + return Err(agent_client_protocol::Error::invalid_params().data(format!( "Model '{model_id}' is not available for provider '{provider_id}'" ))); } @@ -136,7 +139,7 @@ impl GooseAcpAgent { struct PreferenceDef { key: PreferenceKey, config_key: &'static str, - validate: fn(&serde_json::Value) -> Result<(), sacp::Error>, + validate: fn(&serde_json::Value) -> Result<(), agent_client_protocol::Error>, } const PREFERENCE_DEFS: &[PreferenceDef] = &[ @@ -162,56 +165,69 @@ const PREFERENCE_DEFS: &[PreferenceDef] = &[ }, ]; -fn preference_def(key: PreferenceKey) -> Result<&'static PreferenceDef, sacp::Error> { +fn preference_def( + key: PreferenceKey, +) -> Result<&'static PreferenceDef, agent_client_protocol::Error> { PREFERENCE_DEFS .iter() .find(|def| def.key == key) .ok_or_else(|| { - sacp::Error::internal_error().data(format!("Missing preference definition for {key:?}")) + agent_client_protocol::Error::internal_error() + .data(format!("Missing preference definition for {key:?}")) }) } -fn validate_auto_compact_threshold(value: &serde_json::Value) -> Result<(), sacp::Error> { +fn validate_auto_compact_threshold( + value: &serde_json::Value, +) -> Result<(), agent_client_protocol::Error> { let Some(value) = value.as_f64() else { - return Err(sacp::Error::invalid_params().data("autoCompactThreshold must be a number")); + return Err(agent_client_protocol::Error::invalid_params() + .data("autoCompactThreshold must be a number")); }; if !value.is_finite() || value <= 0.0 || value > 1.0 { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data("autoCompactThreshold must be greater than 0 and at most 1")); } Ok(()) } -fn validate_voice_auto_submit_phrases(value: &serde_json::Value) -> Result<(), sacp::Error> { +fn validate_voice_auto_submit_phrases( + value: &serde_json::Value, +) -> Result<(), agent_client_protocol::Error> { if !value.is_string() { - return Err(sacp::Error::invalid_params().data("voiceAutoSubmitPhrases must be a string")); + return Err(agent_client_protocol::Error::invalid_params() + .data("voiceAutoSubmitPhrases must be a string")); } Ok(()) } -fn validate_voice_dictation_provider(value: &serde_json::Value) -> Result<(), sacp::Error> { +fn validate_voice_dictation_provider( + value: &serde_json::Value, +) -> Result<(), agent_client_protocol::Error> { let Some(value) = value.as_str() else { - return Err(sacp::Error::invalid_params().data("voiceDictationProvider must be a string")); + return Err(agent_client_protocol::Error::invalid_params() + .data("voiceDictationProvider must be a string")); }; if !is_supported_voice_dictation_provider(value) { - return Err(sacp::Error::invalid_params().data("voiceDictationProvider is not supported")); + return Err(agent_client_protocol::Error::invalid_params() + .data("voiceDictationProvider is not supported")); } Ok(()) } -fn validate_voice_dictation_preferred_mic(value: &serde_json::Value) -> Result<(), sacp::Error> { +fn validate_voice_dictation_preferred_mic( + value: &serde_json::Value, +) -> Result<(), agent_client_protocol::Error> { let Some(value) = value.as_str() else { - return Err( - sacp::Error::invalid_params().data("voiceDictationPreferredMic must be a string") - ); + return Err(agent_client_protocol::Error::invalid_params() + .data("voiceDictationPreferredMic must be a string")); }; if value.is_empty() { - return Err( - sacp::Error::invalid_params().data("voiceDictationPreferredMic must be non-empty") - ); + return Err(agent_client_protocol::Error::invalid_params() + .data("voiceDictationPreferredMic must be non-empty")); } Ok(()) @@ -230,10 +246,13 @@ fn is_supported_voice_dictation_provider(value: &str) -> bool { } } -fn optional_config_string(config: &Config, key: &str) -> Result, sacp::Error> { +fn optional_config_string( + config: &Config, + key: &str, +) -> Result, agent_client_protocol::Error> { match config.get_param::(key) { Ok(value) => Ok(Some(value)), Err(crate::config::ConfigError::NotFound(_)) => Ok(None), - Err(e) => Err(sacp::Error::internal_error().data(e.to_string())), + Err(e) => Err(agent_client_protocol::Error::internal_error().data(e.to_string())), } } diff --git a/crates/goose/src/acp/server/custom_dispatch.rs b/crates/goose/src/acp/server/custom_dispatch.rs index 1a75e480..9415aba2 100644 --- a/crates/goose/src/acp/server/custom_dispatch.rs +++ b/crates/goose/src/acp/server/custom_dispatch.rs @@ -7,7 +7,7 @@ impl GooseAcpAgent { &self, method: &str, params: serde_json::Value, - ) -> Result { + ) -> Result { self.handle_custom_request(method, params).await } @@ -15,7 +15,7 @@ impl GooseAcpAgent { async fn dispatch_add_extension( &self, req: AddExtensionRequest, - ) -> Result { + ) -> Result { self.on_add_extension(req).await } @@ -23,7 +23,7 @@ impl GooseAcpAgent { async fn dispatch_remove_extension( &self, req: RemoveExtensionRequest, - ) -> Result { + ) -> Result { self.on_remove_extension(req).await } @@ -31,7 +31,7 @@ impl GooseAcpAgent { async fn dispatch_get_tools( &self, req: GetToolsRequest, - ) -> Result { + ) -> Result { self.on_get_tools(req).await } @@ -39,7 +39,7 @@ impl GooseAcpAgent { async fn dispatch_call_tool( &self, req: GooseToolCallRequest, - ) -> Result { + ) -> Result { self.on_call_tool(req).await } @@ -47,7 +47,7 @@ impl GooseAcpAgent { async fn dispatch_read_resource( &self, req: ReadResourceRequest, - ) -> Result { + ) -> Result { self.on_read_resource(req).await } @@ -55,7 +55,7 @@ impl GooseAcpAgent { async fn dispatch_update_working_dir( &self, req: UpdateWorkingDirRequest, - ) -> Result { + ) -> Result { self.on_update_working_dir(req).await } @@ -63,12 +63,14 @@ impl GooseAcpAgent { async fn dispatch_delete_session( &self, req: DeleteSessionRequest, - ) -> Result { + ) -> Result { self.on_delete_session(req).await } #[custom_method(GetExtensionsRequest)] - async fn dispatch_get_extensions(&self) -> Result { + async fn dispatch_get_extensions( + &self, + ) -> Result { self.on_get_extensions().await } @@ -76,7 +78,7 @@ impl GooseAcpAgent { async fn dispatch_add_config_extension( &self, req: AddConfigExtensionRequest, - ) -> Result { + ) -> Result { self.on_add_config_extension(req).await } @@ -84,7 +86,7 @@ impl GooseAcpAgent { async fn dispatch_remove_config_extension( &self, req: RemoveConfigExtensionRequest, - ) -> Result { + ) -> Result { self.on_remove_config_extension(req).await } @@ -92,7 +94,7 @@ impl GooseAcpAgent { async fn dispatch_toggle_config_extension( &self, req: ToggleConfigExtensionRequest, - ) -> Result { + ) -> Result { self.on_toggle_config_extension(req).await } @@ -100,7 +102,7 @@ impl GooseAcpAgent { async fn dispatch_get_session_extensions( &self, req: GetSessionExtensionsRequest, - ) -> Result { + ) -> Result { self.on_get_session_extensions(req).await } @@ -108,7 +110,7 @@ impl GooseAcpAgent { async fn dispatch_list_providers( &self, req: ListProvidersRequest, - ) -> Result { + ) -> Result { self.on_list_providers(req).await } @@ -116,7 +118,7 @@ impl GooseAcpAgent { async fn dispatch_list_provider_catalog( &self, req: ProviderCatalogListRequest, - ) -> Result { + ) -> Result { self.on_list_provider_catalog(req).await } @@ -124,7 +126,7 @@ impl GooseAcpAgent { async fn dispatch_list_provider_setup_catalog( &self, req: ProviderSetupCatalogListRequest, - ) -> Result { + ) -> Result { self.on_list_provider_setup_catalog(req).await } @@ -132,7 +134,7 @@ impl GooseAcpAgent { async fn dispatch_get_provider_catalog_template( &self, req: ProviderCatalogTemplateRequest, - ) -> Result { + ) -> Result { self.on_get_provider_catalog_template(req).await } @@ -140,7 +142,7 @@ impl GooseAcpAgent { async fn dispatch_create_custom_provider( &self, req: CustomProviderCreateRequest, - ) -> Result { + ) -> Result { self.on_create_custom_provider(req).await } @@ -148,7 +150,7 @@ impl GooseAcpAgent { async fn dispatch_read_custom_provider( &self, req: CustomProviderReadRequest, - ) -> Result { + ) -> Result { self.on_read_custom_provider(req).await } @@ -156,7 +158,7 @@ impl GooseAcpAgent { async fn dispatch_update_custom_provider( &self, req: CustomProviderUpdateRequest, - ) -> Result { + ) -> Result { self.on_update_custom_provider(req).await } @@ -164,7 +166,7 @@ impl GooseAcpAgent { async fn dispatch_delete_custom_provider( &self, req: CustomProviderDeleteRequest, - ) -> Result { + ) -> Result { self.on_delete_custom_provider(req).await } @@ -172,7 +174,7 @@ impl GooseAcpAgent { async fn dispatch_refresh_provider_inventory( &self, req: RefreshProviderInventoryRequest, - ) -> Result { + ) -> Result { self.on_refresh_provider_inventory(req).await } @@ -180,7 +182,7 @@ impl GooseAcpAgent { async fn dispatch_read_provider_config( &self, req: ProviderConfigReadRequest, - ) -> Result { + ) -> Result { self.on_read_provider_config(req).await } @@ -188,7 +190,7 @@ impl GooseAcpAgent { async fn dispatch_provider_config_status( &self, req: ProviderConfigStatusRequest, - ) -> Result { + ) -> Result { self.on_provider_config_status(req).await } @@ -196,7 +198,7 @@ impl GooseAcpAgent { async fn dispatch_save_provider_config( &self, req: ProviderConfigSaveRequest, - ) -> Result { + ) -> Result { self.on_save_provider_config(req).await } @@ -204,7 +206,7 @@ impl GooseAcpAgent { async fn dispatch_delete_provider_config( &self, req: ProviderConfigDeleteRequest, - ) -> Result { + ) -> Result { self.on_delete_provider_config(req).await } @@ -212,7 +214,7 @@ impl GooseAcpAgent { async fn dispatch_authenticate_provider_config( &self, req: ProviderConfigAuthenticateRequest, - ) -> Result { + ) -> Result { self.on_authenticate_provider_config(req).await } @@ -220,7 +222,7 @@ impl GooseAcpAgent { async fn dispatch_preferences_read( &self, req: PreferencesReadRequest, - ) -> Result { + ) -> Result { self.on_preferences_read(req).await } @@ -228,7 +230,7 @@ impl GooseAcpAgent { async fn dispatch_preferences_save( &self, req: PreferencesSaveRequest, - ) -> Result { + ) -> Result { self.on_preferences_save(req).await } @@ -236,7 +238,7 @@ impl GooseAcpAgent { async fn dispatch_preferences_remove( &self, req: PreferencesRemoveRequest, - ) -> Result { + ) -> Result { self.on_preferences_remove(req).await } @@ -244,7 +246,7 @@ impl GooseAcpAgent { async fn dispatch_defaults_read( &self, req: DefaultsReadRequest, - ) -> Result { + ) -> Result { self.on_defaults_read(req).await } @@ -252,7 +254,7 @@ impl GooseAcpAgent { async fn dispatch_defaults_save( &self, req: DefaultsSaveRequest, - ) -> Result { + ) -> Result { self.on_defaults_save(req).await } @@ -260,7 +262,7 @@ impl GooseAcpAgent { async fn dispatch_onboarding_import_scan( &self, req: OnboardingImportScanRequest, - ) -> Result { + ) -> Result { self.on_onboarding_import_scan(req).await } @@ -268,7 +270,7 @@ impl GooseAcpAgent { async fn dispatch_onboarding_import_apply( &self, req: OnboardingImportApplyRequest, - ) -> Result { + ) -> Result { self.on_onboarding_import_apply(req).await } @@ -276,7 +278,7 @@ impl GooseAcpAgent { async fn dispatch_export_session( &self, req: ExportSessionRequest, - ) -> Result { + ) -> Result { self.on_export_session(req).await } @@ -284,7 +286,7 @@ impl GooseAcpAgent { async fn dispatch_import_session( &self, req: ImportSessionRequest, - ) -> Result { + ) -> Result { self.on_import_session(req).await } @@ -292,7 +294,7 @@ impl GooseAcpAgent { async fn dispatch_update_session_project( &self, req: UpdateSessionProjectRequest, - ) -> Result { + ) -> Result { self.on_update_session_project(req).await } @@ -300,7 +302,7 @@ impl GooseAcpAgent { async fn dispatch_rename_session( &self, req: RenameSessionRequest, - ) -> Result { + ) -> Result { self.on_rename_session(req).await } @@ -308,7 +310,7 @@ impl GooseAcpAgent { async fn dispatch_archive_session( &self, req: ArchiveSessionRequest, - ) -> Result { + ) -> Result { self.on_archive_session(req).await } @@ -316,7 +318,7 @@ impl GooseAcpAgent { async fn dispatch_unarchive_session( &self, req: UnarchiveSessionRequest, - ) -> Result { + ) -> Result { self.on_unarchive_session(req).await } @@ -324,7 +326,7 @@ impl GooseAcpAgent { async fn dispatch_create_source( &self, req: CreateSourceRequest, - ) -> Result { + ) -> Result { self.on_create_source(req).await } @@ -332,7 +334,7 @@ impl GooseAcpAgent { async fn dispatch_list_sources( &self, req: ListSourcesRequest, - ) -> Result { + ) -> Result { self.on_list_sources(req).await } @@ -340,7 +342,7 @@ impl GooseAcpAgent { async fn dispatch_update_source( &self, req: UpdateSourceRequest, - ) -> Result { + ) -> Result { self.on_update_source(req).await } @@ -348,7 +350,7 @@ impl GooseAcpAgent { async fn dispatch_delete_source( &self, req: DeleteSourceRequest, - ) -> Result { + ) -> Result { self.on_delete_source(req).await } @@ -356,7 +358,7 @@ impl GooseAcpAgent { async fn dispatch_export_source( &self, req: ExportSourceRequest, - ) -> Result { + ) -> Result { self.on_export_source(req).await } @@ -364,7 +366,7 @@ impl GooseAcpAgent { async fn dispatch_import_sources( &self, req: ImportSourcesRequest, - ) -> Result { + ) -> Result { self.on_import_sources(req).await } @@ -372,7 +374,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_transcribe( &self, req: DictationTranscribeRequest, - ) -> Result { + ) -> Result { self.on_dictation_transcribe(req).await } @@ -380,7 +382,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_config( &self, _req: DictationConfigRequest, - ) -> Result { + ) -> Result { self.on_dictation_config(_req).await } @@ -388,7 +390,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_secret_save( &self, req: DictationSecretSaveRequest, - ) -> Result { + ) -> Result { self.on_dictation_secret_save(req).await } @@ -396,7 +398,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_secret_delete( &self, req: DictationSecretDeleteRequest, - ) -> Result { + ) -> Result { self.on_dictation_secret_delete(req).await } @@ -404,7 +406,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_models_list( &self, _req: DictationModelsListRequest, - ) -> Result { + ) -> Result { self.on_dictation_models_list(_req).await } @@ -412,7 +414,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_model_download( &self, _req: DictationModelDownloadRequest, - ) -> Result { + ) -> Result { self.on_dictation_model_download(_req).await } @@ -420,7 +422,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_model_download_progress( &self, _req: DictationModelDownloadProgressRequest, - ) -> Result { + ) -> Result { self.on_dictation_model_download_progress(_req).await } @@ -428,7 +430,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_model_cancel( &self, _req: DictationModelCancelRequest, - ) -> Result { + ) -> Result { self.on_dictation_model_cancel(_req).await } @@ -436,7 +438,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_model_delete( &self, _req: DictationModelDeleteRequest, - ) -> Result { + ) -> Result { self.on_dictation_model_delete(_req).await } @@ -444,7 +446,7 @@ impl GooseAcpAgent { async fn dispatch_dictation_model_select( &self, req: DictationModelSelectRequest, - ) -> Result { + ) -> Result { self.on_dictation_model_select(req).await } } diff --git a/crates/goose/src/acp/server/dictation.rs b/crates/goose/src/acp/server/dictation.rs index e4e05476..ccafdc4e 100644 --- a/crates/goose/src/acp/server/dictation.rs +++ b/crates/goose/src/acp/server/dictation.rs @@ -18,13 +18,13 @@ impl GooseAcpAgent { pub(super) async fn on_dictation_transcribe( &self, req: DictationTranscribeRequest, - ) -> Result { + ) -> Result { use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; let config = crate::config::Config::global(); #[cfg(not(feature = "local-inference"))] if req.provider == "local" { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data("Local inference is not available in this build")); } @@ -32,15 +32,18 @@ impl GooseAcpAgent { req.provider.clone(), )) .map_err(|_| { - sacp::Error::invalid_params().data(format!("Unknown provider: {}", req.provider)) + agent_client_protocol::Error::invalid_params() + .data(format!("Unknown provider: {}", req.provider)) })?; - let audio_bytes = BASE64 - .decode(&req.audio) - .map_err(|_| sacp::Error::invalid_params().data("Invalid base64 audio data"))?; + let audio_bytes = BASE64.decode(&req.audio).map_err(|_| { + agent_client_protocol::Error::invalid_params().data("Invalid base64 audio data") + })?; if audio_bytes.len() > 50 * 1024 * 1024 { - return Err(sacp::Error::invalid_params().data("Audio too large (max 50MB)")); + return Err( + agent_client_protocol::Error::invalid_params().data("Audio too large (max 50MB)") + ); } let extension = match req.mime_type.as_str() { @@ -50,9 +53,8 @@ impl GooseAcpAgent { "audio/m4a" => "m4a", "audio/wav" | "audio/x-wav" => "wav", other => { - return Err( - sacp::Error::invalid_params().data(format!("Unsupported format: {other}")) - ); + return Err(agent_client_protocol::Error::invalid_params() + .data(format!("Unsupported format: {other}"))); } }; @@ -82,7 +84,7 @@ impl GooseAcpAgent { pub(super) async fn on_dictation_config( &self, _req: DictationConfigRequest, - ) -> Result { + ) -> Result { let config = crate::config::Config::global(); let mut providers = std::collections::HashMap::new(); @@ -128,7 +130,7 @@ impl GooseAcpAgent { pub(super) async fn on_dictation_secret_save( &self, req: DictationSecretSaveRequest, - ) -> Result { + ) -> Result { let provider = parse_dictation_provider(&req.provider)?; let key = dictation_secret_config_key(provider)?; let config = self.config()?; @@ -140,7 +142,7 @@ impl GooseAcpAgent { pub(super) async fn on_dictation_secret_delete( &self, req: DictationSecretDeleteRequest, - ) -> Result { + ) -> Result { let provider = parse_dictation_provider(&req.provider)?; let key = dictation_secret_config_key(provider)?; let config = self.config()?; @@ -152,7 +154,7 @@ impl GooseAcpAgent { pub(super) async fn on_dictation_models_list( &self, _req: DictationModelsListRequest, - ) -> Result { + ) -> Result { #[cfg(feature = "local-inference")] { use crate::download_manager::{get_download_manager, DownloadStatus}; @@ -183,13 +185,14 @@ impl GooseAcpAgent { pub(super) async fn on_dictation_model_download( &self, _req: DictationModelDownloadRequest, - ) -> Result { + ) -> Result { #[cfg(feature = "local-inference")] { use crate::download_manager::get_download_manager; - let model = whisper::get_model(&_req.model_id) - .ok_or_else(|| sacp::Error::invalid_params().data("Unknown model id"))?; + let model = whisper::get_model(&_req.model_id).ok_or_else(|| { + agent_client_protocol::Error::invalid_params().data("Unknown model id") + })?; let manager = get_download_manager(); let model_id_for_config = model.id.to_string(); @@ -231,13 +234,13 @@ impl GooseAcpAgent { } #[cfg(not(feature = "local-inference"))] - Err(sacp::Error::invalid_params().data("Local inference not enabled")) + Err(agent_client_protocol::Error::invalid_params().data("Local inference not enabled")) } pub(super) async fn on_dictation_model_download_progress( &self, _req: DictationModelDownloadProgressRequest, - ) -> Result { + ) -> Result { #[cfg(feature = "local-inference")] { use crate::download_manager::get_download_manager; @@ -267,7 +270,7 @@ impl GooseAcpAgent { pub(super) async fn on_dictation_model_cancel( &self, _req: DictationModelCancelRequest, - ) -> Result { + ) -> Result { #[cfg(feature = "local-inference")] { use crate::download_manager::get_download_manager; @@ -279,21 +282,24 @@ impl GooseAcpAgent { } #[cfg(not(feature = "local-inference"))] - Err(sacp::Error::invalid_params().data("Local inference not enabled")) + Err(agent_client_protocol::Error::invalid_params().data("Local inference not enabled")) } pub(super) async fn on_dictation_model_delete( &self, _req: DictationModelDeleteRequest, - ) -> Result { + ) -> Result { #[cfg(feature = "local-inference")] { - let model = whisper::get_model(&_req.model_id) - .ok_or_else(|| sacp::Error::invalid_params().data("Unknown model id"))?; + let model = whisper::get_model(&_req.model_id).ok_or_else(|| { + agent_client_protocol::Error::invalid_params().data("Unknown model id") + })?; let path = model.local_path(); if !path.exists() { - return Err(sacp::Error::invalid_params().data("Model not downloaded")); + return Err( + agent_client_protocol::Error::invalid_params().data("Model not downloaded") + ); } std::fs::remove_file(path).internal_err()?; @@ -302,23 +308,26 @@ impl GooseAcpAgent { } #[cfg(not(feature = "local-inference"))] - Err(sacp::Error::invalid_params().data("Local inference not enabled")) + Err(agent_client_protocol::Error::invalid_params().data("Local inference not enabled")) } pub(super) async fn on_dictation_model_select( &self, req: DictationModelSelectRequest, - ) -> Result { + ) -> Result { #[cfg(not(feature = "local-inference"))] if req.provider == "local" { - return Err(sacp::Error::invalid_params().data("Local inference not enabled")); + return Err( + agent_client_protocol::Error::invalid_params().data("Local inference not enabled") + ); } let provider: DictationProvider = serde_json::from_value(serde_json::Value::String( req.provider.clone(), )) .map_err(|_| { - sacp::Error::invalid_params().data(format!("Unknown provider: {}", req.provider)) + agent_client_protocol::Error::invalid_params() + .data(format!("Unknown provider: {}", req.provider)) })?; let key = match provider { @@ -327,12 +336,12 @@ impl GooseAcpAgent { DictationProvider::ElevenLabs => ELEVENLABS_TRANSCRIPTION_MODEL_CONFIG_KEY, #[cfg(feature = "local-inference")] DictationProvider::Local => { - let model = whisper::get_model(&req.model_id) - .ok_or_else(|| sacp::Error::invalid_params().data("Unknown model id"))?; + let model = whisper::get_model(&req.model_id).ok_or_else(|| { + agent_client_protocol::Error::invalid_params().data("Unknown model id") + })?; if !model.is_downloaded() { - return Err( - sacp::Error::invalid_params().data("Local Whisper model is not downloaded") - ); + return Err(agent_client_protocol::Error::invalid_params() + .data("Local Whisper model is not downloaded")); } whisper::LOCAL_WHISPER_MODEL_CONFIG_KEY } @@ -346,22 +355,27 @@ impl GooseAcpAgent { } } -fn parse_dictation_provider(provider: &str) -> Result { - serde_json::from_value(serde_json::Value::String(provider.to_string())) - .map_err(|_| sacp::Error::invalid_params().data(format!("Unknown provider: {provider}"))) +fn parse_dictation_provider( + provider: &str, +) -> Result { + serde_json::from_value(serde_json::Value::String(provider.to_string())).map_err(|_| { + agent_client_protocol::Error::invalid_params().data(format!("Unknown provider: {provider}")) + }) } -fn dictation_secret_config_key(provider: DictationProvider) -> Result<&'static str, sacp::Error> { +fn dictation_secret_config_key( + provider: DictationProvider, +) -> Result<&'static str, agent_client_protocol::Error> { let def = get_provider_def(provider); if def.uses_provider_config { - return Err(sacp::Error::invalid_params().data( + return Err(agent_client_protocol::Error::invalid_params().data( "Dictation provider uses the main provider configuration. Configure its credentials in provider settings instead.", )); } #[cfg(feature = "local-inference")] if provider == DictationProvider::Local { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data("Dictation provider does not use an API key or secret.")); } diff --git a/crates/goose/src/acp/server/dispatch.rs b/crates/goose/src/acp/server/dispatch.rs index e00bec39..3a3694c4 100644 --- a/crates/goose/src/acp/server/dispatch.rs +++ b/crates/goose/src/acp/server/dispatch.rs @@ -9,7 +9,8 @@ impl HandleDispatchFrom for GooseAcpHandler { &mut self, message: Dispatch, cx: ConnectionTo, - ) -> impl std::future::Future, sacp::Error>> + Send { + ) -> impl std::future::Future, agent_client_protocol::Error>> + Send + { let agent = self.agent.clone(); // The MatchDispatchFrom chain produces an ~85KB async state machine. @@ -96,7 +97,7 @@ impl HandleDispatchFrom for GooseAcpHandler { cx.spawn(async move { let cx = cx_spawn; let value_id = req.value.as_value_id() - .ok_or_else(|| sacp::Error::invalid_params().data("Expected a value ID"))? + .ok_or_else(|| agent_client_protocol::Error::invalid_params().data("Expected a value ID"))? .clone(); let session_id = req.session_id.clone(); let sid = sid_short(session_id.0.as_ref()); @@ -124,7 +125,7 @@ impl HandleDispatchFrom for GooseAcpHandler { } other => { responder.respond_with_error( - sacp::Error::invalid_params().data(format!("Unsupported config option: {}", other)) + agent_client_protocol::Error::invalid_params().data(format!("Unsupported config option: {}", other)) )?; return Ok(()); } diff --git a/crates/goose/src/acp/server/extensions.rs b/crates/goose/src/acp/server/extensions.rs index f78777c4..ff8ca308 100644 --- a/crates/goose/src/acp/server/extensions.rs +++ b/crates/goose/src/acp/server/extensions.rs @@ -4,10 +4,11 @@ impl GooseAcpAgent { pub(super) async fn on_add_extension( &self, req: AddExtensionRequest, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(&req.session_id).await?; - let config: ExtensionConfig = serde_json::from_value(req.config) - .map_err(|e| sacp::Error::invalid_params().data(format!("bad config: {e}")))?; + let config: ExtensionConfig = serde_json::from_value(req.config).map_err(|e| { + agent_client_protocol::Error::invalid_params().data(format!("bad config: {e}")) + })?; let agent = self.get_session_agent(&req.session_id, None).await?; agent .add_extension(config, &internal_id) @@ -19,7 +20,7 @@ impl GooseAcpAgent { pub(super) async fn on_remove_extension( &self, req: RemoveExtensionRequest, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(&req.session_id).await?; let agent = self.get_session_agent(&req.session_id, None).await?; agent @@ -29,7 +30,9 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - pub(super) async fn on_get_extensions(&self) -> Result { + pub(super) async fn on_get_extensions( + &self, + ) -> Result { let extensions = crate::config::extensions::get_all_extensions(); let warnings = crate::config::extensions::get_warnings(); let extensions_json = extensions @@ -56,13 +59,12 @@ impl GooseAcpAgent { pub(super) async fn on_add_config_extension( &self, req: AddConfigExtensionRequest, - ) -> Result { + ) -> Result { let mut obj = match req.extension_config { serde_json::Value::Object(obj) => obj, _ => { - return Err( - sacp::Error::invalid_params().data("extensionConfig must be a JSON object") - ); + return Err(agent_client_protocol::Error::invalid_params() + .data("extensionConfig must be a JSON object")); } }; obj.insert( @@ -71,8 +73,9 @@ impl GooseAcpAgent { ); let config: crate::agents::ExtensionConfig = - serde_json::from_value(serde_json::Value::Object(obj)) - .map_err(|e| sacp::Error::invalid_params().data(format!("bad config: {e}")))?; + serde_json::from_value(serde_json::Value::Object(obj)).map_err(|e| { + agent_client_protocol::Error::invalid_params().data(format!("bad config: {e}")) + })?; crate::config::extensions::set_extension(crate::config::extensions::ExtensionEntry { enabled: req.enabled, @@ -84,10 +87,10 @@ impl GooseAcpAgent { pub(super) async fn on_remove_config_extension( &self, req: RemoveConfigExtensionRequest, - ) -> Result { + ) -> Result { let keys = crate::config::extensions::get_all_extension_names(); if !keys.iter().any(|k| k == &req.config_key) { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data(format!("Extension '{}' not found", req.config_key))); } crate::config::extensions::remove_extension(&req.config_key); @@ -97,10 +100,10 @@ impl GooseAcpAgent { pub(super) async fn on_toggle_config_extension( &self, req: ToggleConfigExtensionRequest, - ) -> Result { + ) -> Result { let keys = crate::config::extensions::get_all_extension_names(); if !keys.iter().any(|k| k == &req.config_key) { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data(format!("Extension '{}' not found", req.config_key))); } crate::config::extensions::set_extension_enabled(&req.config_key, req.enabled); @@ -110,7 +113,7 @@ impl GooseAcpAgent { pub(super) async fn on_get_session_extensions( &self, req: GetSessionExtensionsRequest, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(&req.session_id).await?; let session = self .session_manager diff --git a/crates/goose/src/acp/server/onboarding.rs b/crates/goose/src/acp/server/onboarding.rs index 1729fa63..8e175f7d 100644 --- a/crates/goose/src/acp/server/onboarding.rs +++ b/crates/goose/src/acp/server/onboarding.rs @@ -29,7 +29,7 @@ impl GooseAcpAgent { pub(super) async fn on_onboarding_import_scan( &self, req: OnboardingImportScanRequest, - ) -> Result { + ) -> Result { let source_filter = source_filter(&req.sources); let mut candidates = Vec::new(); @@ -56,7 +56,7 @@ impl GooseAcpAgent { pub(super) async fn on_onboarding_import_apply( &self, req: OnboardingImportApplyRequest, - ) -> Result { + ) -> Result { let config = self.config()?; Ok(apply_onboarding_import_candidates( &config, diff --git a/crates/goose/src/acp/server/providers.rs b/crates/goose/src/acp/server/providers.rs index 4fd4ad91..e902626a 100644 --- a/crates/goose/src/acp/server/providers.rs +++ b/crates/goose/src/acp/server/providers.rs @@ -246,10 +246,10 @@ fn custom_provider_engine_to_dto(engine: &declarative_providers::ProviderEngine) } } -fn normalize_custom_provider_engine(engine: &str) -> Result { +fn normalize_custom_provider_engine(engine: &str) -> Result { let engine = engine.trim().to_lowercase(); if declarative_providers::ProviderEngine::from_str(&engine).is_err() { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data(format!("Unsupported custom provider engine: {engine}"))); } @@ -261,10 +261,12 @@ fn normalize_custom_provider_engine(engine: &str) -> Result } } -fn non_empty_trimmed(value: String, field: &str) -> Result { +fn non_empty_trimmed(value: String, field: &str) -> Result { let value = value.trim().to_string(); if value.is_empty() { - return Err(sacp::Error::invalid_params().data(format!("{field} cannot be empty"))); + return Err( + agent_client_protocol::Error::invalid_params().data(format!("{field} cannot be empty")) + ); } Ok(value) } @@ -279,14 +281,17 @@ fn normalize_optional_string(value: Option) -> Option { fn normalize_custom_provider_upsert( mut provider: CustomProviderUpsertDto, require_api_key: bool, -) -> Result { +) -> Result { provider.engine = normalize_custom_provider_engine(&provider.engine)?; provider.display_name = non_empty_trimmed(provider.display_name, "displayName")?; provider.api_url = non_empty_trimmed(provider.api_url, "apiUrl")?; - let url = url::Url::parse(&provider.api_url) - .map_err(|_| sacp::Error::invalid_params().data("apiUrl must be a valid URL"))?; + let url = url::Url::parse(&provider.api_url).map_err(|_| { + agent_client_protocol::Error::invalid_params().data("apiUrl must be a valid URL") + })?; if !matches!(url.scheme(), "http" | "https") { - return Err(sacp::Error::invalid_params().data("apiUrl must use HTTP or HTTPS")); + return Err( + agent_client_protocol::Error::invalid_params().data("apiUrl must use HTTP or HTTPS") + ); } provider.api_key = provider.api_key.and_then(|api_key| { @@ -294,7 +299,7 @@ fn normalize_custom_provider_upsert( (!api_key.is_empty()).then_some(api_key) }); if require_api_key && provider.requires_auth && provider.api_key.is_none() { - return Err(sacp::Error::invalid_params().data("apiKey cannot be empty")); + return Err(agent_client_protocol::Error::invalid_params().data("apiKey cannot be empty")); } provider.models = provider .models @@ -305,7 +310,7 @@ fn normalize_custom_provider_upsert( }) .collect(); if provider.models.is_empty() { - return Err(sacp::Error::invalid_params().data("models cannot be empty")); + return Err(agent_client_protocol::Error::invalid_params().data("models cannot be empty")); } provider.headers = provider @@ -318,14 +323,16 @@ fn normalize_custom_provider_upsert( return Ok(None); } reqwest::header::HeaderName::from_bytes(key.as_bytes()).map_err(|_| { - sacp::Error::invalid_params().data(format!("Invalid header name: {key}")) + agent_client_protocol::Error::invalid_params() + .data(format!("Invalid header name: {key}")) })?; reqwest::header::HeaderValue::from_str(&value).map_err(|_| { - sacp::Error::invalid_params().data(format!("Invalid header value for: {key}")) + agent_client_protocol::Error::invalid_params() + .data(format!("Invalid header value for: {key}")) })?; Ok(Some((key, value))) }) - .collect::, sacp::Error>>()? + .collect::, agent_client_protocol::Error>>()? .into_iter() .flatten() .collect(); @@ -340,14 +347,15 @@ fn custom_provider_headers(headers: HashMap) -> Option Result { +) -> Result { declarative_providers::load_provider(provider_id).map_err(|error| { if error.to_string().contains("Provider not found") { - sacp::Error::invalid_params().data(format!("Unknown provider: {provider_id}")) + agent_client_protocol::Error::invalid_params() + .data(format!("Unknown provider: {provider_id}")) } else if error.to_string().contains("Invalid provider id") { - sacp::Error::invalid_params().data(error.to_string()) + agent_client_protocol::Error::invalid_params().data(error.to_string()) } else { - sacp::Error::internal_error().data(error.to_string()) + agent_client_protocol::Error::internal_error().data(error.to_string()) } }) } @@ -418,7 +426,7 @@ impl GooseAcpAgent { pub(super) async fn on_list_providers( &self, req: ListProvidersRequest, - ) -> Result { + ) -> Result { let entries = self .provider_inventory .entries(&req.provider_ids) @@ -432,11 +440,11 @@ impl GooseAcpAgent { pub(super) async fn on_list_provider_catalog( &self, req: ProviderCatalogListRequest, - ) -> Result { + ) -> Result { let formats = match req.format { Some(format) => vec![format .parse::() - .map_err(|error| sacp::Error::invalid_params().data(error))?], + .map_err(|error| agent_client_protocol::Error::invalid_params().data(error))?], None => vec![ crate::providers::catalog::ProviderFormat::OpenAI, crate::providers::catalog::ProviderFormat::Anthropic, @@ -465,7 +473,7 @@ impl GooseAcpAgent { pub(super) async fn on_list_provider_setup_catalog( &self, _req: ProviderSetupCatalogListRequest, - ) -> Result { + ) -> Result { let providers = crate::providers::catalog::get_setup_catalog_entries() .await .into_iter() @@ -477,10 +485,10 @@ impl GooseAcpAgent { pub(super) async fn on_get_provider_catalog_template( &self, req: ProviderCatalogTemplateRequest, - ) -> Result { + ) -> Result { let template = crate::providers::catalog::get_provider_template(&req.provider_id) .ok_or_else(|| { - sacp::Error::invalid_params() + agent_client_protocol::Error::invalid_params() .data(format!("Unknown catalog provider: {}", req.provider_id)) })?; Ok(ProviderCatalogTemplateResponse { @@ -491,7 +499,7 @@ impl GooseAcpAgent { pub(super) async fn on_create_custom_provider( &self, req: CustomProviderCreateRequest, - ) -> Result { + ) -> Result { let provider = normalize_custom_provider_upsert(req.provider, true)?; let config = declarative_providers::create_custom_provider( declarative_providers::CreateCustomProviderParams { @@ -528,7 +536,7 @@ impl GooseAcpAgent { pub(super) async fn on_read_custom_provider( &self, req: CustomProviderReadRequest, - ) -> Result { + ) -> Result { let loaded = load_declarative_provider_for_client(&req.provider_id)?; let status = Self::provider_config_status(req.provider_id).await; Ok(CustomProviderReadResponse { @@ -541,10 +549,10 @@ impl GooseAcpAgent { pub(super) async fn on_update_custom_provider( &self, req: CustomProviderUpdateRequest, - ) -> Result { + ) -> Result { let loaded = load_declarative_provider_for_client(&req.provider_id)?; if !loaded.is_editable { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data(format!("Provider is not editable: {}", req.provider_id))); } @@ -556,7 +564,7 @@ impl GooseAcpAgent { loaded.config.api_key_env.clone() }; if Config::global().get_secret::(&api_key_env).is_err() { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data("apiKey is required when auth is enabled and no secret is stored")); } } @@ -595,10 +603,10 @@ impl GooseAcpAgent { pub(super) async fn on_delete_custom_provider( &self, req: CustomProviderDeleteRequest, - ) -> Result { + ) -> Result { let loaded = load_declarative_provider_for_client(&req.provider_id)?; if !loaded.is_editable { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data(format!("Provider is not editable: {}", req.provider_id))); } @@ -608,7 +616,7 @@ impl GooseAcpAgent { .as_deref() == Some(req.provider_id.as_str()) { - return Err(sacp::Error::invalid_params().data(format!( + return Err(agent_client_protocol::Error::invalid_params().data(format!( "Cannot delete active provider: {}", req.provider_id ))); @@ -753,7 +761,7 @@ impl GooseAcpAgent { pub(super) async fn start_provider_inventory_refresh( &self, provider_ids: &[String], - ) -> Result { + ) -> Result { let refresh_job_plan = self .provider_inventory .plan_refresh_jobs(provider_ids) @@ -768,7 +776,7 @@ impl GooseAcpAgent { pub(super) async fn on_refresh_provider_inventory( &self, req: RefreshProviderInventoryRequest, - ) -> Result { + ) -> Result { Config::global().invalidate_secrets_cache(); self.start_provider_inventory_refresh(&req.provider_ids) .await @@ -777,7 +785,7 @@ impl GooseAcpAgent { pub(super) async fn on_read_provider_config( &self, req: ProviderConfigReadRequest, - ) -> Result { + ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; @@ -800,7 +808,7 @@ impl GooseAcpAgent { pub(super) async fn on_provider_config_status( &self, req: ProviderConfigStatusRequest, - ) -> Result { + ) -> Result { Ok(ProviderConfigStatusResponse { statuses: Self::provider_config_statuses(&req.provider_ids).await, }) @@ -809,7 +817,7 @@ impl GooseAcpAgent { pub(super) async fn on_save_provider_config( &self, req: ProviderConfigSaveRequest, - ) -> Result { + ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; @@ -824,13 +832,13 @@ impl GooseAcpAgent { .iter() .find(|config_key| config_key.name == field.key) else { - return Err(sacp::Error::invalid_params() + return Err(agent_client_protocol::Error::invalid_params() .data(format!("Unsupported provider config field: {}", field.key))); }; let value = field.value.trim(); if value.is_empty() { - return Err(sacp::Error::invalid_params().data(format!( + return Err(agent_client_protocol::Error::invalid_params().data(format!( "Provider config field cannot be empty: {}", field.key ))); @@ -864,7 +872,7 @@ impl GooseAcpAgent { pub(super) async fn on_delete_provider_config( &self, req: ProviderConfigDeleteRequest, - ) -> Result { + ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; @@ -898,13 +906,13 @@ impl GooseAcpAgent { pub(super) async fn on_authenticate_provider_config( &self, req: ProviderConfigAuthenticateRequest, - ) -> Result { + ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; let metadata = entry.metadata().clone(); if !metadata.config_keys.iter().any(|key| key.oauth_flow) { - return Err(sacp::Error::invalid_params().data(format!( + return Err(agent_client_protocol::Error::invalid_params().data(format!( "Provider does not support native authentication: {}", req.provider_id ))); diff --git a/crates/goose/src/acp/server/resources.rs b/crates/goose/src/acp/server/resources.rs index f50a0d96..859024c4 100644 --- a/crates/goose/src/acp/server/resources.rs +++ b/crates/goose/src/acp/server/resources.rs @@ -4,7 +4,7 @@ impl GooseAcpAgent { pub(super) async fn on_read_resource( &self, req: ReadResourceRequest, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(&req.session_id).await?; let agent = self.get_session_agent(&req.session_id, None).await?; let cancel_token = CancellationToken::new(); diff --git a/crates/goose/src/acp/server/sessions.rs b/crates/goose/src/acp/server/sessions.rs index f6e41e68..f1d21baf 100644 --- a/crates/goose/src/acp/server/sessions.rs +++ b/crates/goose/src/acp/server/sessions.rs @@ -4,14 +4,17 @@ impl GooseAcpAgent { pub(super) async fn on_update_working_dir( &self, req: UpdateWorkingDirRequest, - ) -> Result { + ) -> Result { let working_dir = req.working_dir.trim().to_string(); if working_dir.is_empty() { - return Err(sacp::Error::invalid_params().data("working directory cannot be empty")); + return Err(agent_client_protocol::Error::invalid_params() + .data("working directory cannot be empty")); } let path = std::path::PathBuf::from(&working_dir); if !path.exists() || !path.is_dir() { - return Err(sacp::Error::invalid_params().data("invalid directory path")); + return Err( + agent_client_protocol::Error::invalid_params().data("invalid directory path") + ); } let internal_id = self.internal_session_id(&req.session_id).await?; self.session_manager @@ -43,7 +46,7 @@ impl GooseAcpAgent { pub(super) async fn on_delete_session( &self, req: DeleteSessionRequest, - ) -> Result { + ) -> Result { // Delete the thread and all its internal sessions + messages. self.thread_manager .delete_thread(&req.session_id) @@ -56,15 +59,15 @@ impl GooseAcpAgent { pub(super) async fn on_export_session( &self, req: ExportSessionRequest, - ) -> Result { + ) -> Result { let thread = self .thread_manager .get_thread(&req.session_id) .await .internal_err()?; - let internal_id = thread - .current_session_id - .ok_or_else(|| sacp::Error::internal_error().data("Thread has no internal session"))?; + let internal_id = thread.current_session_id.ok_or_else(|| { + agent_client_protocol::Error::internal_error().data("Thread has no internal session") + })?; let data = self .session_manager .export_session(&internal_id) @@ -76,7 +79,7 @@ impl GooseAcpAgent { pub(super) async fn on_import_session( &self, req: ImportSessionRequest, - ) -> Result { + ) -> Result { let session = self .session_manager .import_session(&req.data, Some(SessionType::Acp)) @@ -130,7 +133,7 @@ impl GooseAcpAgent { pub(super) async fn on_update_session_project( &self, req: UpdateSessionProjectRequest, - ) -> Result { + ) -> Result { let project_id = req.project_id; self.update_thread_metadata(&req.session_id, move |meta| { meta.project_id = project_id; @@ -142,20 +145,20 @@ impl GooseAcpAgent { pub(super) async fn on_rename_session( &self, req: RenameSessionRequest, - ) -> Result { + ) -> Result { let title = req.title; let thread = self .thread_manager .update_thread(&req.session_id, Some(title.clone()), Some(true), None) .await - .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string()))?; if let Some(internal_session_id) = thread.current_session_id { self.session_manager .update(&internal_session_id) .user_provided_name(title) .apply() .await - .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string()))?; } Ok(EmptyResponse {}) } @@ -163,7 +166,7 @@ impl GooseAcpAgent { pub(super) async fn on_archive_session( &self, req: ArchiveSessionRequest, - ) -> Result { + ) -> Result { self.thread_manager .archive_thread(&req.session_id) .await @@ -175,7 +178,7 @@ impl GooseAcpAgent { pub(super) async fn on_unarchive_session( &self, req: UnarchiveSessionRequest, - ) -> Result { + ) -> Result { self.thread_manager .unarchive_thread(&req.session_id) .await diff --git a/crates/goose/src/acp/server/sources.rs b/crates/goose/src/acp/server/sources.rs index 9dbfd738..d86be102 100644 --- a/crates/goose/src/acp/server/sources.rs +++ b/crates/goose/src/acp/server/sources.rs @@ -4,7 +4,7 @@ impl GooseAcpAgent { pub(super) async fn on_create_source( &self, req: CreateSourceRequest, - ) -> Result { + ) -> Result { let source = crate::sources::create_source( req.source_type, &req.name, @@ -19,7 +19,7 @@ impl GooseAcpAgent { pub(super) async fn on_list_sources( &self, req: ListSourcesRequest, - ) -> Result { + ) -> Result { let sources = crate::sources::list_sources(req.source_type, req.project_dir.as_deref())?; Ok(ListSourcesResponse { sources }) } @@ -27,7 +27,7 @@ impl GooseAcpAgent { pub(super) async fn on_update_source( &self, req: UpdateSourceRequest, - ) -> Result { + ) -> Result { let source = crate::sources::update_source( req.source_type, &req.path, @@ -41,7 +41,7 @@ impl GooseAcpAgent { pub(super) async fn on_delete_source( &self, req: DeleteSourceRequest, - ) -> Result { + ) -> Result { crate::sources::delete_source(req.source_type, &req.path)?; Ok(EmptyResponse {}) } @@ -49,7 +49,7 @@ impl GooseAcpAgent { pub(super) async fn on_export_source( &self, req: ExportSourceRequest, - ) -> Result { + ) -> Result { let (json, filename) = crate::sources::export_source(req.source_type, &req.path)?; Ok(ExportSourceResponse { json, filename }) } @@ -57,7 +57,7 @@ impl GooseAcpAgent { pub(super) async fn on_import_sources( &self, req: ImportSourcesRequest, - ) -> Result { + ) -> Result { let sources = crate::sources::import_sources(&req.data, req.global, req.project_dir.as_deref())?; Ok(ImportSourcesResponse { sources }) diff --git a/crates/goose/src/acp/server/tools.rs b/crates/goose/src/acp/server/tools.rs index ae6ca23e..a148a585 100644 --- a/crates/goose/src/acp/server/tools.rs +++ b/crates/goose/src/acp/server/tools.rs @@ -6,7 +6,7 @@ impl GooseAcpAgent { pub(super) async fn on_get_tools( &self, req: GetToolsRequest, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(&req.session_id).await?; let agent = self.get_session_agent(&req.session_id, None).await?; let tools = agent.list_tools(&internal_id, None).await; @@ -21,24 +21,26 @@ impl GooseAcpAgent { pub(super) async fn on_call_tool( &self, req: GooseToolCallRequest, - ) -> Result { + ) -> Result { let internal_id = self.internal_session_id(&req.session_id).await?; let agent = self.get_session_agent(&req.session_id, None).await?; let tools = agent.list_tools(&internal_id, None).await; let Some(tool) = tools.iter().find(|t| *t.name == req.name) else { - return Err(sacp::Error::invalid_params().data("tool not found")); + return Err(agent_client_protocol::Error::invalid_params().data("tool not found")); }; if !is_tool_visible_to_app(tool) { - return Err(sacp::Error::invalid_params().data("tool is not visible to app clients")); + return Err(agent_client_protocol::Error::invalid_params() + .data("tool is not visible to app clients")); } let arguments = match req.arguments { serde_json::Value::Object(map) => Some(map), serde_json::Value::Null => None, _ => { - return Err(sacp::Error::invalid_params().data("tool arguments must be an object")); + return Err(agent_client_protocol::Error::invalid_params() + .data("tool arguments must be an object")); } }; @@ -55,19 +57,19 @@ impl GooseAcpAgent { .extension_manager .dispatch_tool_call(&ctx, tool_call, CancellationToken::new()) .await - .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string()))?; let result = tool_result .result .await - .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string()))?; let content = result .content .into_iter() .map(serde_json::to_value) .collect::, _>>() - .map_err(|e| sacp::Error::internal_error().data(e.to_string()))?; + .map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string()))?; Ok(GooseToolCallResponse { content, diff --git a/crates/goose/src/providers/codex_acp.rs b/crates/goose/src/providers/codex_acp.rs index 21f0a328..4ef6ffef 100644 --- a/crates/goose/src/providers/codex_acp.rs +++ b/crates/goose/src/providers/codex_acp.rs @@ -67,7 +67,7 @@ impl ProviderDef for CodexAcpProvider { // servers are configured so codex-acp can connect to them. let has_http_mcp = mcp_servers .iter() - .any(|s| matches!(s, sacp::schema::McpServer::Http(_))); + .any(|s| matches!(s, agent_client_protocol::schema::McpServer::Http(_))); if has_http_mcp { args.extend([ "-c".to_string(), diff --git a/crates/goose/src/skills/mod.rs b/crates/goose/src/skills/mod.rs index 1d1d6ce6..cac8f884 100644 --- a/crates/goose/src/skills/mod.rs +++ b/crates/goose/src/skills/mod.rs @@ -10,8 +10,8 @@ pub use client::{SkillsClient, EXTENSION_NAME}; use crate::config::paths::Paths; use crate::plugins::installed_plugin_skill_dirs; use crate::sources::parse_frontmatter; +use agent_client_protocol::Error; use goose_sdk::custom_requests::{SourceEntry, SourceType}; -use sacp::Error; use serde::Deserialize; use std::collections::HashSet; use std::path::{Path, PathBuf}; diff --git a/crates/goose/src/sources.rs b/crates/goose/src/sources.rs index 8b7b21e0..5baa5e8a 100644 --- a/crates/goose/src/sources.rs +++ b/crates/goose/src/sources.rs @@ -5,9 +5,9 @@ use crate::skills::{ parse_skill_frontmatter, resolve_discoverable_skill_dir, resolve_skill_dir, skill_base_dir, validate_skill_name, }; +use agent_client_protocol::Error; use fs_err as fs; use goose_sdk::custom_requests::{SourceEntry, SourceType}; -use sacp::Error; use serde::Deserialize; use std::path::PathBuf; diff --git a/crates/goose/tests/acp_common_tests/mod.rs b/crates/goose/tests/acp_common_tests/mod.rs index 15946dea..88a5c904 100644 --- a/crates/goose/tests/acp_common_tests/mod.rs +++ b/crates/goose/tests/acp_common_tests/mod.rs @@ -4,6 +4,10 @@ #[path = "../acp_fixtures/mod.rs"] pub mod fixtures; +use agent_client_protocol::schema::{ + ListSessionsResponse, McpServer, McpServerHttp, ModelId, SessionInfo, SessionModeId, + ToolCallStatus, ToolKind, +}; use fixtures::{ assert_notifications, Connection, FsFixture, Notification, OpenAiFixture, PermissionDecision, Session, SessionData, TerminalCall, TerminalFixture, TestConnectionConfig, @@ -19,10 +23,6 @@ use goose::providers::base::{ }; use goose::providers::errors::ProviderError; use goose_test_support::{McpFixture, FAKE_CODE, TEST_IMAGE_B64, TEST_MODEL}; -use sacp::schema::{ - ListSessionsResponse, McpServer, McpServerHttp, ModelId, SessionInfo, SessionModeId, - ToolCallStatus, ToolKind, -}; use sqlx::sqlite::SqlitePoolOptions; use std::sync::Arc; use std::time::Duration; @@ -230,8 +230,11 @@ pub async fn run_delete_session() { assert!(!after.contains(session.session_id())); let err = conn.load_session(&sid, vec![]).await.unwrap_err(); - let sacp_err = err.downcast::().unwrap(); - assert_eq!(sacp_err.code, sacp::ErrorCode::ResourceNotFound); + let acp_err = err.downcast::().unwrap(); + assert_eq!( + acp_err.code, + agent_client_protocol::ErrorCode::ResourceNotFound + ); } pub async fn run_config_mcp() { @@ -625,11 +628,13 @@ pub async fn run_load_session_error() { .await .unwrap_err(); - let sacp_err = err.downcast::().unwrap(); + let acp_err = err.downcast::().unwrap(); assert_eq!( - sacp_err, - sacp::Error::resource_not_found(Some("nonexistent-session-id".to_string())) - .data("Session not found: nonexistent-session-id") + acp_err, + agent_client_protocol::Error::resource_not_found(Some( + "nonexistent-session-id".to_string() + )) + .data("Session not found: nonexistent-session-id") ); } @@ -641,7 +646,7 @@ pub async fn run_config_option_set_error( config_id: &str, value: &str, session_id_override: Option<&str>, - expected: sacp::Error, + expected: agent_client_protocol::Error, ) { let openai = OpenAiFixture::new(vec![], C::expected_session_id()).await; let mut conn = C::new(TestConnectionConfig::default(), openai).await; @@ -656,21 +661,21 @@ pub async fn run_config_option_set_error( .await .unwrap_err(); - let sacp_err = err.downcast::().unwrap(); - assert_eq!(sacp_err, expected); + let acp_err = err.downcast::().unwrap(); + assert_eq!(acp_err, expected); } #[macro_export] macro_rules! tests_config_option_set_error { ($conn:ty) => { - #[test_case::test_case("mode", "not_a_mode", None, sacp::Error::invalid_params().data("Invalid mode: not_a_mode") ; "invalid mode via config option")] - #[test_case::test_case("mode", "auto", Some("nonexistent-session-id"), sacp::Error::resource_not_found(Some("nonexistent-session-id".to_string())).data("Session not found: nonexistent-session-id") ; "session not found via config option")] - #[test_case::test_case("thought_level", "high", None, sacp::Error::invalid_params().data("Unsupported config option: thought_level") ; "unsupported config option")] + #[test_case::test_case("mode", "not_a_mode", None, agent_client_protocol::Error::invalid_params().data("Invalid mode: not_a_mode") ; "invalid mode via config option")] + #[test_case::test_case("mode", "auto", Some("nonexistent-session-id"), agent_client_protocol::Error::resource_not_found(Some("nonexistent-session-id".to_string())).data("Session not found: nonexistent-session-id") ; "session not found via config option")] + #[test_case::test_case("thought_level", "high", None, agent_client_protocol::Error::invalid_params().data("Unsupported config option: thought_level") ; "unsupported config option")] fn test_config_option_set_error( config_id: &'static str, value: &'static str, session_id: Option<&'static str>, - expected: sacp::Error, + expected: agent_client_protocol::Error, ) { common_tests::fixtures::run_test(async move { common_tests::run_config_option_set_error::<$conn>( @@ -792,7 +797,7 @@ async fn run_mode_set_impl(via: SetModeVia) { pub async fn run_mode_set_error( mode_id: &str, session_id_override: Option<&str>, - expected: sacp::Error, + expected: agent_client_protocol::Error, ) { let openai = OpenAiFixture::new(vec![], C::expected_session_id()).await; let mut conn = C::new(TestConnectionConfig::default(), openai).await; @@ -807,19 +812,19 @@ pub async fn run_mode_set_error( .await .unwrap_err(); - let sacp_err = err.downcast::().unwrap(); - assert_eq!(sacp_err, expected); + let acp_err = err.downcast::().unwrap(); + assert_eq!(acp_err, expected); } #[macro_export] macro_rules! tests_mode_set_error { ($conn:ty) => { - #[test_case::test_case("not_a_mode", None, sacp::Error::invalid_params().data("Invalid mode: not_a_mode") ; "invalid mode")] - #[test_case::test_case("auto", Some("nonexistent-session-id"), sacp::Error::resource_not_found(Some("nonexistent-session-id".to_string())).data("Session not found: nonexistent-session-id") ; "session not found")] + #[test_case::test_case("not_a_mode", None, agent_client_protocol::Error::invalid_params().data("Invalid mode: not_a_mode") ; "invalid mode")] + #[test_case::test_case("auto", Some("nonexistent-session-id"), agent_client_protocol::Error::resource_not_found(Some("nonexistent-session-id".to_string())).data("Session not found: nonexistent-session-id") ; "session not found")] fn test_mode_set_error( mode_id: &'static str, session_id: Option<&'static str>, - expected: sacp::Error, + expected: agent_client_protocol::Error, ) { common_tests::fixtures::run_test(async move { common_tests::run_mode_set_error::<$conn>( @@ -966,19 +971,21 @@ pub async fn run_model_set_error_session_not_found() { .await .unwrap_err(); - let sacp_err = err.downcast::().unwrap(); + let acp_err = err.downcast::().unwrap(); assert_eq!( - sacp_err, - sacp::Error::resource_not_found(Some("nonexistent-session-id".to_string())) - .data("Session not found: nonexistent-session-id") + acp_err, + agent_client_protocol::Error::resource_not_found(Some( + "nonexistent-session-id".to_string() + )) + .data("Session not found: nonexistent-session-id") ); } #[allow(dead_code)] pub async fn run_new_session_error( - cx: &sacp::ConnectionTo, + cx: &agent_client_protocol::ConnectionTo, params: serde_json::Value, - expected: sacp::Error, + expected: agent_client_protocol::Error, ) { let err = fixtures::send_custom(cx, "session/new", params) .await @@ -997,8 +1004,11 @@ pub async fn run_prompt_error() { .prompt("test", PermissionDecision::Cancel) .await .unwrap_err(); - let sacp_err = err.downcast::().unwrap(); - assert_eq!(sacp_err.code, sacp::ErrorCode::ResourceNotFound); + let acp_err = err.downcast::().unwrap(); + assert_eq!( + acp_err.code, + agent_client_protocol::ErrorCode::ResourceNotFound + ); } pub async fn run_permission_persistence() { diff --git a/crates/goose/tests/acp_custom_requests_test.rs b/crates/goose/tests/acp_custom_requests_test.rs index 31b65d4f..adb23ca6 100644 --- a/crates/goose/tests/acp_custom_requests_test.rs +++ b/crates/goose/tests/acp_custom_requests_test.rs @@ -526,7 +526,7 @@ fn test_developer_fs_requests_use_acp_session_id() { builtins: vec!["developer".to_string()], read_text_file: Some(Arc::new(move |req| { *seen_session_id_clone.lock().unwrap() = Some(req.session_id.0.to_string()); - Ok(sacp::schema::ReadTextFileResponse::new( + Ok(agent_client_protocol::schema::ReadTextFileResponse::new( "test-read-content-12345", )) })), diff --git a/crates/goose/tests/acp_fixtures/mod.rs b/crates/goose/tests/acp_fixtures/mod.rs index 30a5561d..ae8a112d 100644 --- a/crates/goose/tests/acp_fixtures/mod.rs +++ b/crates/goose/tests/acp_fixtures/mod.rs @@ -1,6 +1,13 @@ #![recursion_limit = "256"] #![allow(unused_attributes)] +use agent_client_protocol::schema::{ + CreateTerminalResponse, KillTerminalResponse, ListSessionsResponse, McpServer, + ReadTextFileRequest, ReadTextFileResponse, ReleaseTerminalResponse, SessionModeState, + SessionModelState, SessionUpdate, TerminalExitStatus, TerminalId, TerminalOutputResponse, + ToolCallContent, ToolCallStatus, ToolKind, WaitForTerminalExitResponse, WriteTextFileRequest, + WriteTextFileResponse, +}; use async_trait::async_trait; use fs_err as fs; use goose::acp::server::{serve, AcpProviderFactory, GooseAcpAgent}; @@ -14,13 +21,6 @@ use goose::providers::base::Provider; use goose::providers::openai::OpenAiProvider; use goose::session_context::SESSION_ID_HEADER; use goose_test_support::{ExpectedSessionId, TEST_MODEL}; -use sacp::schema::{ - CreateTerminalResponse, KillTerminalResponse, ListSessionsResponse, McpServer, - ReadTextFileRequest, ReadTextFileResponse, ReleaseTerminalResponse, SessionModeState, - SessionModelState, SessionUpdate, TerminalExitStatus, TerminalId, TerminalOutputResponse, - ToolCallContent, ToolCallStatus, ToolKind, WaitForTerminalExitResponse, WriteTextFileRequest, - WriteTextFileResponse, -}; use std::collections::VecDeque; use std::future::Future; use std::path::PathBuf; @@ -125,13 +125,13 @@ impl OpenAiFixture { } } -pub type DuplexTransport = sacp::ByteStreams< +pub type DuplexTransport = agent_client_protocol::ByteStreams< tokio_util::compat::Compat, tokio_util::compat::Compat, >; /// Wires up duplex streams, spawns `serve` for the given agent, and returns -/// a ready-to-use sacp transport plus the server handle. +/// a ready-to-use agent_client_protocol transport plus the server handle. #[allow(dead_code)] pub async fn serve_agent_in_process( agent: Arc, @@ -145,7 +145,8 @@ pub async fn serve_agent_in_process( } }); - let transport = sacp::ByteStreams::new(client_write.compat_write(), client_read.compat()); + let transport = + agent_client_protocol::ByteStreams::new(client_write.compat_write(), client_read.compat()); (transport, handle) } @@ -554,7 +555,7 @@ pub trait Connection: Sized { #[async_trait] pub trait Session: std::fmt::Debug { - fn session_id(&self) -> &sacp::schema::SessionId; + fn session_id(&self) -> &agent_client_protocol::schema::SessionId; fn work_dir(&self) -> std::path::PathBuf; fn notifications(&self) -> Vec; async fn prompt( @@ -598,11 +599,11 @@ where } pub async fn send_custom( - cx: &sacp::ConnectionTo, + cx: &agent_client_protocol::ConnectionTo, method: &str, params: serde_json::Value, -) -> Result { - let msg = sacp::UntypedMessage::new(method, params).unwrap(); +) -> Result { + let msg = agent_client_protocol::UntypedMessage::new(method, params).unwrap(); cx.send_request(msg).block_task().await } diff --git a/crates/goose/tests/acp_fixtures/provider.rs b/crates/goose/tests/acp_fixtures/provider.rs index 2b04efef..ed4f9a3a 100644 --- a/crates/goose/tests/acp_fixtures/provider.rs +++ b/crates/goose/tests/acp_fixtures/provider.rs @@ -2,6 +2,11 @@ use super::{ spawn_acp_server_in_process, Connection, DuplexTransport, OpenAiFixture, PermissionDecision, Session, SessionData, TestConnectionConfig, TestOutput, }; +use agent_client_protocol::schema::{ + ListSessionsResponse, McpServer, ModelId, ModelInfo, SessionModelState, SessionUpdate, + ToolCallStatus, +}; +use agent_client_protocol::{Channel, Client, ConnectTo, DynConnectTo}; use async_trait::async_trait; use futures::StreamExt; use goose::acp::{AcpProvider, AcpProviderConfig}; @@ -12,11 +17,6 @@ use goose::permission::permission_confirmation::PrincipalType; use goose::permission::{Permission, PermissionConfirmation}; use goose::providers::base::Provider; use goose_test_support::{ExpectedSessionId, IgnoreSessionId, TEST_MODEL}; -use sacp::schema::{ - ListSessionsResponse, McpServer, ModelId, ModelInfo, SessionModelState, SessionUpdate, - ToolCallStatus, -}; -use sacp::{Channel, Client, ConnectTo, DynConnectTo}; use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::sync::Arc; @@ -45,7 +45,7 @@ pub struct AcpProviderConnection { #[allow(dead_code)] pub struct AcpProviderSession { provider: Arc>>, - session_id: sacp::schema::SessionId, + session_id: agent_client_protocol::schema::SessionId, notification_sink: NotificationSink, session_models: SessionModels, work_dir: std::path::PathBuf, @@ -247,7 +247,7 @@ impl Connection for AcpProviderConnection { let session = AcpProviderSession { provider: Arc::clone(&self.provider), - session_id: sacp::schema::SessionId::new(goose_id), + session_id: agent_client_protocol::schema::SessionId::new(goose_id), notification_sink: self.notification_sink.clone(), session_models: self.session_models.clone(), work_dir: self.work_dir.clone(), @@ -265,7 +265,7 @@ impl Connection for AcpProviderConnection { _session_id: &str, _mcp_servers: Vec, ) -> anyhow::Result> { - Err(sacp::Error::internal_error() + Err(agent_client_protocol::Error::internal_error() .data("load_session not implemented for ACP provider") .into()) } @@ -317,7 +317,7 @@ impl Connection for AcpProviderConnection { #[async_trait] impl Session for AcpProviderSession { - fn session_id(&self) -> &sacp::schema::SessionId { + fn session_id(&self) -> &agent_client_protocol::schema::SessionId { &self.session_id } @@ -382,22 +382,26 @@ fn strip_config_options(transport: DuplexTransport) -> Channel { while let Some(msg) = from_server.next().await { let msg = match msg { Ok(m) => match m { - sacp::jsonrpcmsg::Message::Response(mut resp) => { + agent_client_protocol::jsonrpcmsg::Message::Response(mut resp) => { if let Some(ref mut result) = resp.result { if let Some(obj) = result.as_object_mut() { obj.remove("configOptions"); } } - Ok(Some(sacp::jsonrpcmsg::Message::Response(resp))) + Ok(Some(agent_client_protocol::jsonrpcmsg::Message::Response( + resp, + ))) } - sacp::jsonrpcmsg::Message::Request(req) + agent_client_protocol::jsonrpcmsg::Message::Request(req) if req.id.is_none() && req.method == "session/update" && req .params .as_ref() .and_then(|params| match params { - sacp::jsonrpcmsg::Params::Object(obj) => Some(obj), + agent_client_protocol::jsonrpcmsg::Params::Object(obj) => { + Some(obj) + } _ => None, }) .and_then(|obj| obj.get("update")) @@ -409,7 +413,9 @@ fn strip_config_options(transport: DuplexTransport) -> Channel { .params .as_ref() .and_then(|params| match params { - sacp::jsonrpcmsg::Params::Object(obj) => Some(obj), + agent_client_protocol::jsonrpcmsg::Params::Object(obj) => { + Some(obj) + } _ => None, }) .and_then(|obj| obj.get("sessionId")) @@ -419,10 +425,14 @@ fn strip_config_options(transport: DuplexTransport) -> Channel { if stripped_initial_config.insert(session_id) { Ok(None) } else { - Ok(Some(sacp::jsonrpcmsg::Message::Request(req))) + Ok(Some(agent_client_protocol::jsonrpcmsg::Message::Request( + req, + ))) } } else { - Ok(Some(sacp::jsonrpcmsg::Message::Request(req))) + Ok(Some(agent_client_protocol::jsonrpcmsg::Message::Request( + req, + ))) } } other => Ok(Some(other)), diff --git a/crates/goose/tests/acp_fixtures/server.rs b/crates/goose/tests/acp_fixtures/server.rs index 1ddd6027..5050dafb 100644 --- a/crates/goose/tests/acp_fixtures/server.rs +++ b/crates/goose/tests/acp_fixtures/server.rs @@ -2,10 +2,7 @@ use super::{ map_permission_response, spawn_acp_server_in_process, Connection, PermissionDecision, Session, SessionData, TestConnectionConfig, TestOutput, }; -use async_trait::async_trait; -use goose::config::PermissionManager; -use goose_test_support::{ExpectedSessionId, IgnoreSessionId}; -use sacp::schema::{ +use agent_client_protocol::schema::{ ClientCapabilities, CloseSessionRequest, ContentBlock, CreateTerminalRequest, FileSystemCapabilities, ImageContent, InitializeRequest, KillTerminalRequest, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, McpServer, ModelId, ModelInfo, @@ -16,7 +13,10 @@ use sacp::schema::{ StopReason, TerminalOutputRequest, TextContent, ToolCallStatus, WaitForTerminalExitRequest, WriteTextFileRequest, }; -use sacp::{Agent, Client, ConnectionTo}; +use agent_client_protocol::{Agent, Client, ConnectionTo}; +use async_trait::async_trait; +use goose::config::PermissionManager; +use goose_test_support::{ExpectedSessionId, IgnoreSessionId}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::sync::Notify; @@ -37,7 +37,7 @@ pub struct AcpServerConnection { pub struct AcpServerSession { cx: ConnectionTo, - session_id: sacp::schema::SessionId, + session_id: agent_client_protocol::schema::SessionId, updates: Arc>>, permission: Arc>, notify: Arc, @@ -163,7 +163,7 @@ impl Connection for AcpServerConnection { Ok(()) } }, - sacp::on_receive_notification!(), + agent_client_protocol::on_receive_notification!(), ) .on_receive_request( { @@ -173,7 +173,7 @@ impl Connection for AcpServerConnection { responder.respond(map_permission_response(&req, decision)) } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .on_receive_request( async move |req: ReadTextFileRequest, responder, _cx| match read_handler { @@ -181,9 +181,11 @@ impl Connection for AcpServerConnection { Ok(resp) => responder.respond(resp), Err(msg) => responder.respond_with_internal_error(msg), }, - None => responder.respond_with_error(sacp::Error::method_not_found()), + None => responder.respond_with_error( + agent_client_protocol::Error::method_not_found(), + ), }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .on_receive_request( async move |req: WriteTextFileRequest, responder, _cx| match write_handler { @@ -191,21 +193,23 @@ impl Connection for AcpServerConnection { Ok(resp) => responder.respond(resp), Err(msg) => responder.respond_with_internal_error(msg), }, - None => responder.respond_with_error(sacp::Error::method_not_found()), + None => responder.respond_with_error( + agent_client_protocol::Error::method_not_found(), + ), }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .on_receive_request( { let t = terminal.clone(); async move |req: CreateTerminalRequest, responder, _cx| match t { Some(ref f) => responder.respond(f.on_create(&req.command)), - None => { - responder.respond_with_error(sacp::Error::method_not_found()) - } + None => responder.respond_with_error( + agent_client_protocol::Error::method_not_found(), + ), } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .on_receive_request( { @@ -214,48 +218,48 @@ impl Connection for AcpServerConnection { Some(ref f) => { responder.respond(f.on_wait_for_exit(&req.terminal_id)) } - None => { - responder.respond_with_error(sacp::Error::method_not_found()) - } + None => responder.respond_with_error( + agent_client_protocol::Error::method_not_found(), + ), } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .on_receive_request( { let t = terminal.clone(); async move |req: TerminalOutputRequest, responder, _cx| match t { Some(ref f) => responder.respond(f.on_output(&req.terminal_id)), - None => { - responder.respond_with_error(sacp::Error::method_not_found()) - } + None => responder.respond_with_error( + agent_client_protocol::Error::method_not_found(), + ), } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .on_receive_request( { let t = terminal.clone(); async move |req: ReleaseTerminalRequest, responder, _cx| match t { Some(ref f) => responder.respond(f.on_release(&req.terminal_id)), - None => { - responder.respond_with_error(sacp::Error::method_not_found()) - } + None => responder.respond_with_error( + agent_client_protocol::Error::method_not_found(), + ), } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .on_receive_request( { let t = terminal.clone(); async move |req: KillTerminalRequest, responder, _cx| match t { Some(ref f) => responder.respond(f.on_kill(&req.terminal_id)), - None => { - responder.respond_with_error(sacp::Error::method_not_found()) - } + None => responder.respond_with_error( + agent_client_protocol::Error::method_not_found(), + ), } }, - sacp::on_receive_request!(), + agent_client_protocol::on_receive_request!(), ) .connect_with(transport, { let cx_holder = cx_holder_clone; @@ -276,7 +280,7 @@ impl Connection for AcpServerConnection { *cx_holder.lock().unwrap() = Some(cx.clone()); let _ = ready_tx.send(()); - std::future::pending::>().await + std::future::pending::>().await } }) .await; @@ -342,7 +346,7 @@ impl Connection for AcpServerConnection { ) -> anyhow::Result> { self.updates.lock().unwrap().clear(); let work_dir = tempfile::tempdir().unwrap(); - let session_id = sacp::schema::SessionId::new(session_id.to_string()); + let session_id = agent_client_protocol::schema::SessionId::new(session_id.to_string()); let response = self .cx .send_request( @@ -452,7 +456,7 @@ impl Connection for AcpServerConnection { #[async_trait] impl Session for AcpServerSession { - fn session_id(&self) -> &sacp::schema::SessionId { + fn session_id(&self) -> &agent_client_protocol::schema::SessionId { &self.session_id } @@ -499,7 +503,7 @@ impl Session for AcpServerSession { } fn extract_model_state_from_config_options( - config_options: Option<&[sacp::schema::SessionConfigOption]>, + config_options: Option<&[agent_client_protocol::schema::SessionConfigOption]>, ) -> Option { let option = config_options? .iter() @@ -509,7 +513,7 @@ fn extract_model_state_from_config_options( }; let available_models = match &select.options { - sacp::schema::SessionConfigSelectOptions::Ungrouped(options) => options + agent_client_protocol::schema::SessionConfigSelectOptions::Ungrouped(options) => options .iter() .map(|option| { ModelInfo::new( @@ -518,7 +522,7 @@ fn extract_model_state_from_config_options( ) }) .collect(), - sacp::schema::SessionConfigSelectOptions::Grouped(groups) => groups + agent_client_protocol::schema::SessionConfigSelectOptions::Grouped(groups) => groups .iter() .flat_map(|group| { group.options.iter().map(|option| { diff --git a/ui/goose2/AGENTS.md b/ui/goose2/AGENTS.md index c0f4df6f..b26f3e04 100644 --- a/ui/goose2/AGENTS.md +++ b/ui/goose2/AGENTS.md @@ -171,7 +171,7 @@ ThemeProvider manages three axes: The skills → sources migration in [#8675](https://github.com/block/goose/pull/8675) is the clearest illustration of the rule. **It deleted 319 lines of Tauri-command code in `src-tauri/src/commands/skills.rs` and replaced them with ACP custom methods.** If you find yourself wanting to add an `invoke()` command that proxies to `goose`, that PR is what "doing it the other way" looks like. Copy this shape when adding new endpoints: 1. **Define the request/response in `crates/goose-sdk/src/custom_requests.rs`.** Use the `JsonRpcRequest` / `JsonRpcResponse` derives and the `#[request(method = "_goose//", response = ...)]` attribute. Sources uses namespaced methods like `_goose/sources/create`, `_goose/sources/list`, `_goose/sources/update`, `_goose/sources/delete`, `_goose/sources/export`, `_goose/sources/import` with paired request/response structs (`CreateSourceRequest` / `CreateSourceResponse`, etc.). Keep the docs on those structs aligned with the implementation: today `_goose/sources/list` is still skill-only; create/import take an explicit target scope (`global`, plus `projectDir` for project sources), while update/delete/export operate on an existing skill by absolute `path`. -2. **Implement the handler in `crates/goose-acp/src/server.rs`** with `#[custom_method(YourRequest)]`. Keep it thin: unpack the request, call into the `goose` crate, wrap the result. The sources handlers are ~5 lines each — e.g. `on_list_sources` just calls `goose::sources::list_sources(...)` and returns the typed response. Errors map to `sacp::Error::invalid_params()` / `internal_error()`. +2. **Implement the handler in `crates/goose-acp/src/server.rs`** with `#[custom_method(YourRequest)]`. Keep it thin: unpack the request, call into the `goose` crate, wrap the result. The sources handlers are ~5 lines each — e.g. `on_list_sources` just calls `goose::sources::list_sources(...)` and returns the typed response. Errors map to `agent_client_protocol::Error::invalid_params()` / `internal_error()`. 3. **Put the real logic in the `goose` crate.** Sources lives in `crates/goose/src/sources.rs` — filesystem CRUD, frontmatter parsing, scope resolution, all of it. `goose-acp` knows nothing about where skills are stored on disk; it just forwards typed arguments. This separation is the point. 4. **Regenerate the SDK.** The TS methods on `GooseClient` are generated into `ui/sdk/src/generated/`. Do not hand-edit generated files. 5. **Call it from the frontend via a feature `api/` module.** See `ui/goose2/src/features/skills/api/skills.ts`. It calls `getClient()` from `acpConnection.ts` and invokes the SDK, then adapts the generic `SourceEntry` shape into a feature-friendly `SkillInfo`: