diff --git a/Cargo.lock b/Cargo.lock index 27b71f86..76d9aefd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4459,6 +4459,7 @@ dependencies = [ "goose", "goose-acp-macros", "goose-mcp", + "goose-sdk", "goose-test-support", "http-body-util", "regex", @@ -4570,6 +4571,19 @@ dependencies = [ "url", ] +[[package]] +name = "goose-sdk" +version = "1.29.0" +dependencies = [ + "agent-client-protocol-schema", + "sacp", + "schemars 1.2.1", + "serde", + "serde_json", + "tokio", + "tokio-util", +] + [[package]] name = "goose-server" version = "1.30.0" diff --git a/crates/goose-acp-macros/src/lib.rs b/crates/goose-acp-macros/src/lib.rs index a15213f6..532e1aa8 100644 --- a/crates/goose-acp-macros/src/lib.rs +++ b/crates/goose-acp-macros/src/lib.rs @@ -1,16 +1,20 @@ use proc_macro::TokenStream; use quote::quote; use syn::{ - parse_macro_input, FnArg, GenericArgument, ImplItem, ItemImpl, Lit, Pat, PathArguments, - ReturnType, Type, + parse_macro_input, FnArg, GenericArgument, ImplItem, ItemImpl, Pat, PathArguments, ReturnType, + Type, }; -/// Marks an impl block as containing `#[custom_method("...")]`-annotated handlers. +/// Marks an impl block as containing `#[custom_method(RequestType)]`-annotated handlers. +/// +/// The request type must derive `sacp::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 each annotation string as the method name (include `_goose/` for goose-only 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 /// @@ -25,11 +29,11 @@ use syn::{ /// /// ```ignore /// // No params — called for requests with no/empty params -/// #[custom_method("session/list")] -/// async fn on_list_sessions(&self) -> Result { .. } +/// #[custom_method(GetExtensionsRequest)] +/// async fn on_get_extensions(&self) -> Result { .. } /// /// // Typed params — JSON params auto-deserialized -/// #[custom_method("session/get")] +/// #[custom_method(GetSessionRequest)] /// async fn on_get_session(&self, req: GetSessionRequest) -> Result { .. } /// ``` /// @@ -40,15 +44,15 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut routes: Vec = Vec::new(); - // Collect all #[custom_method("...")] annotations and strip them. + // Collect all #[custom_method(RequestType)] annotations and strip them. for item in &mut impl_block.items { if let ImplItem::Fn(method) = item { - let mut route_name = None; + let mut request_type = None; method.attrs.retain(|attr| { if attr.path().is_ident("custom_method") { if let Ok(meta_list) = attr.meta.require_list() { - if let Ok(Lit::Str(s)) = meta_list.parse_args::() { - route_name = Some(s.value()); + if let Ok(ty) = meta_list.parse_args::() { + request_type = Some(ty); } } false // strip the attribute @@ -57,7 +61,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } }); - if let Some(name) = route_name { + if let Some(req_type) = request_type { let fn_ident = method.sig.ident.clone(); let param_type = extract_param_type(&method.sig); @@ -65,7 +69,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { let ok_type = extract_result_ok_type(&method.sig); routes.push(Route { - method_name: name, + request_type: req_type, fn_ident, param_type, return_type, @@ -75,31 +79,31 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } } - // Generate the dispatch arms. + // Generate the dispatch arms using matches_method for routing. let arms: Vec<_> = routes .iter() .map(|route| { - let method = &route.method_name; + let req_type = &route.request_type; let fn_ident = &route.fn_ident; match &route.param_type { Some(_) => { quote! { - #method => { + if <#req_type as sacp::JsonRpcMessage>::matches_method(method) { let req = serde_json::from_value(params) .map_err(|e| sacp::Error::invalid_params().data(e.to_string()))?; let result = self.#fn_ident(req).await?; - serde_json::to_value(&result) - .map_err(|e| sacp::Error::internal_error().data(e.to_string())) + return serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string())); } } } None => { quote! { - #method => { + if <#req_type as sacp::JsonRpcMessage>::matches_method(method) { let result = self.#fn_ident().await?; - serde_json::to_value(&result) - .map_err(|e| sacp::Error::internal_error().data(e.to_string())) + return serde_json::to_value(&result) + .map_err(|e| sacp::Error::internal_error().data(e.to_string())); } } } @@ -111,7 +115,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { let schema_entries: Vec<_> = routes .iter() .map(|route| { - let method = &route.method_name; + let req_type = &route.request_type; let params_expr = if let Some(pt) = &route.param_type { if is_json_value(pt) { @@ -120,7 +124,12 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { quote! { Some(generator.subschema_for::<#pt>()) } } } else { - quote! { None } + // Even with no handler param, generate schema from the request type + if is_json_value(req_type) { + quote! { None } + } else { + quote! { Some(generator.subschema_for::<#req_type>()) } + } }; let response_expr = if let Some(ok_ty) = &route.ok_type { @@ -141,7 +150,8 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { quote! { Some(#name.to_string()) } } } else { - quote! { None } + let name = type_name(req_type); + quote! { Some(#name.to_string()) } }; let response_name_expr = if let Some(ok_ty) = &route.ok_type { @@ -156,12 +166,15 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { }; quote! { - crate::custom_requests::CustomMethodSchema { - method: #method.to_string(), - params_schema: #params_expr, - params_type_name: #params_name_expr, - response_schema: #response_expr, - response_type_name: #response_name_expr, + { + let dummy = <#req_type as Default>::default(); + crate::custom_requests::CustomMethodSchema { + method: sacp::JsonRpcMessage::method(&dummy).to_string(), + params_schema: #params_expr, + params_type_name: #params_name_expr, + response_schema: #response_expr, + response_type_name: #response_name_expr, + } } } }) @@ -174,10 +187,8 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { method: &str, params: serde_json::Value, ) -> Result { - match method { - #(#arms)* - _ => Err(sacp::Error::method_not_found()), - } + #(#arms)* + Err(sacp::Error::method_not_found()) } }; @@ -202,7 +213,7 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream { } struct Route { - method_name: String, + request_type: Type, fn_ident: syn::Ident, param_type: Option, #[allow(dead_code)] diff --git a/crates/goose-acp/Cargo.toml b/crates/goose-acp/Cargo.toml index a271d741..8bc2b1e7 100644 --- a/crates/goose-acp/Cargo.toml +++ b/crates/goose-acp/Cargo.toml @@ -47,6 +47,7 @@ http-body-util = "0.1.3" uuid = { workspace = true, features = ["v7"] } schemars = { workspace = true, features = ["derive"] } goose-acp-macros = { path = "../goose-acp-macros" } +goose-sdk = { path = "../goose-sdk" } [dev-dependencies] async-trait = { workspace = true } diff --git a/crates/goose-acp/acp-meta.json b/crates/goose-acp/acp-meta.json index 5050c86e..53de01bf 100644 --- a/crates/goose-acp/acp-meta.json +++ b/crates/goose-acp/acp-meta.json @@ -47,7 +47,7 @@ }, { "method": "_goose/config/extensions", - "requestType": null, + "requestType": "GetExtensionsRequest", "responseType": "GetExtensionsResponse" } ] diff --git a/crates/goose-acp/acp-schema.json b/crates/goose-acp/acp-schema.json index f6db67e3..a9e50a0f 100644 --- a/crates/goose-acp/acp-schema.json +++ b/crates/goose-acp/acp-schema.json @@ -9,12 +9,12 @@ "type": "string" }, "config": { - "description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform)." + "description": "Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform).", + "default": null } }, "required": [ - "sessionId", - "config" + "sessionId" ], "description": "Add an extension to an active session.", "x-side": "agent", @@ -69,6 +69,7 @@ "required": [ "tools" ], + "description": "Tools response.", "x-side": "agent", "x-method": "_goose/tools" }, @@ -98,12 +99,11 @@ "type": "object", "properties": { "result": { - "description": "The resource result from the extension (MCP ReadResourceResult)." + "description": "The resource result from the extension (MCP ReadResourceResult).", + "default": null } }, - "required": [ - "result" - ], + "description": "Resource read response.", "x-side": "agent", "x-method": "_goose/resource/read" }, @@ -147,12 +147,10 @@ "type": "object", "properties": { "session": { - "description": "The session object with id, name, working_dir, timestamps, tokens, etc." + "description": "The session object with id, name, working_dir, timestamps, tokens, etc.", + "default": null } }, - "required": [ - "session" - ], "description": "Get a session response.", "x-side": "agent", "x-method": "session/get" @@ -195,6 +193,7 @@ "required": [ "data" ], + "description": "Export session response.", "x-side": "agent", "x-method": "_goose/session/export" }, @@ -216,15 +215,20 @@ "type": "object", "properties": { "session": { - "description": "The imported session object." + "description": "The imported session object.", + "default": null } }, - "required": [ - "session" - ], + "description": "Import session response.", "x-side": "agent", "x-method": "_goose/session/import" }, + "GetExtensionsRequest": { + "type": "object", + "description": "List configured extensions and any warnings.", + "x-side": "agent", + "x-method": "_goose/config/extensions" + }, "GetExtensionsResponse": { "type": "object", "properties": { @@ -340,6 +344,15 @@ ], "description": "Params for _goose/session/import", "title": "ImportSessionRequest" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetExtensionsRequest" + } + ], + "description": "Params for _goose/config/extensions", + "title": "GetExtensionsRequest" } ] }, diff --git a/crates/goose-acp/src/lib.rs b/crates/goose-acp/src/lib.rs index 97c405e2..d1bddef8 100644 --- a/crates/goose-acp/src/lib.rs +++ b/crates/goose-acp/src/lib.rs @@ -1,7 +1,7 @@ #![recursion_limit = "256"] mod adapters; -pub mod custom_requests; +pub use goose_sdk::custom_requests; mod fs; pub mod server; pub mod server_factory; diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs index 0b776364..9751fdc7 100644 --- a/crates/goose-acp/src/server.rs +++ b/crates/goose-acp/src/server.rs @@ -1303,7 +1303,7 @@ impl GooseAcpAgent { #[custom_methods] impl GooseAcpAgent { - #[custom_method("_goose/extensions/add")] + #[custom_method(AddExtensionRequest)] async fn on_add_extension( &self, req: AddExtensionRequest, @@ -1318,7 +1318,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - #[custom_method("_goose/extensions/remove")] + #[custom_method(RemoveExtensionRequest)] async fn on_remove_extension( &self, req: RemoveExtensionRequest, @@ -1331,7 +1331,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - #[custom_method("_goose/tools")] + #[custom_method(GetToolsRequest)] async fn on_get_tools(&self, req: GetToolsRequest) -> Result { let agent = self.get_session_agent(&req.session_id, None).await?; let tools = agent.list_tools(&req.session_id, None).await; @@ -1343,7 +1343,7 @@ impl GooseAcpAgent { Ok(GetToolsResponse { tools: tools_json }) } - #[custom_method("_goose/resource/read")] + #[custom_method(ReadResourceRequest)] async fn on_read_resource( &self, req: ReadResourceRequest, @@ -1362,7 +1362,7 @@ impl GooseAcpAgent { }) } - #[custom_method("_goose/working_dir/update")] + #[custom_method(UpdateWorkingDirRequest)] async fn on_update_working_dir( &self, req: UpdateWorkingDirRequest, @@ -1394,8 +1394,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - // TODO: use typed GetSessionRequest when agent-client-protocol-schema adds it (Discussion #60) - #[custom_method("session/get")] + #[custom_method(GetSessionRequest)] async fn on_get_session( &self, req: GetSessionRequest, @@ -1412,8 +1411,7 @@ impl GooseAcpAgent { }) } - // TODO: use typed DeleteSessionRequest when agent-client-protocol-schema adds it (RFD #395) - #[custom_method("session/delete")] + #[custom_method(DeleteSessionRequest)] async fn on_delete_session( &self, req: DeleteSessionRequest, @@ -1426,7 +1424,7 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - #[custom_method("_goose/session/export")] + #[custom_method(ExportSessionRequest)] async fn on_export_session( &self, req: ExportSessionRequest, @@ -1439,7 +1437,7 @@ impl GooseAcpAgent { Ok(ExportSessionResponse { data }) } - #[custom_method("_goose/session/import")] + #[custom_method(ImportSessionRequest)] async fn on_import_session( &self, req: ImportSessionRequest, @@ -1456,7 +1454,7 @@ impl GooseAcpAgent { }) } - #[custom_method("_goose/config/extensions")] + #[custom_method(GetExtensionsRequest)] async fn on_get_extensions(&self) -> Result { let extensions = goose::config::extensions::get_all_extensions(); let warnings = goose::config::extensions::get_warnings(); diff --git a/crates/goose-sdk/Cargo.toml b/crates/goose-sdk/Cargo.toml new file mode 100644 index 00000000..6eef747a --- /dev/null +++ b/crates/goose-sdk/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "goose-sdk" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +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-schema = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +schemars = { workspace = true, features = ["derive"] } + +[dev-dependencies] +tokio = { workspace = true } +tokio-util = { workspace = true, features = ["compat", "rt"] } + +[package.metadata.cargo-machete] +# Used to provide extras imports for sacp +ignored = ["agent-client-protocol-schema"] diff --git a/crates/goose-sdk/examples/acp_client.rs b/crates/goose-sdk/examples/acp_client.rs new file mode 100644 index 00000000..8ef5b29c --- /dev/null +++ b/crates/goose-sdk/examples/acp_client.rs @@ -0,0 +1,156 @@ +//! ACP Client Example +//! +//! Spawns `goose acp` as a child process and sends it a completion request +//! using the Agent Client Protocol over stdio. +//! +//! # Prerequisites +//! +//! You must have goose built and a provider configured (`goose configure`). +//! +//! # Usage +//! +//! ```bash +//! cargo run -p goose-sdk --example acp_client -- "What is 2 + 2?" +//! ``` +//! +//! Or with a custom goose binary path: +//! +//! ```bash +//! 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::{ + ContentBlock, InitializeRequest, ProtocolVersion, RequestPermissionOutcome, + RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome, + SessionNotification, SessionUpdate, +}; +use sacp::{Client, ConnectionTo}; +use std::path::PathBuf; +use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Parse args: [--goose-bin PATH] PROMPT + let args: Vec = std::env::args().skip(1).collect(); + let (goose_bin, prompt) = parse_args(&args)?; + + eprintln!("🚀 Spawning: {} acp", goose_bin.display()); + + let mut child = tokio::process::Command::new(&goose_bin) + .arg("acp") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .spawn() + .map_err(|e| format!("Failed to spawn '{}': {e}", goose_bin.display()))?; + + 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 prompt_clone = prompt.clone(); + + Client + .builder() + .name("acp-client-example") + // Print session notifications (agent text, tool calls, etc.) + .on_receive_notification( + async move |notification: SessionNotification, _cx| { + match ¬ification.update { + SessionUpdate::AgentMessageChunk(chunk) => { + if let ContentBlock::Text(text) = &chunk.content { + print!("{}", text.text); + } + } + SessionUpdate::ToolCall(tool_call) => { + eprintln!("🔧 Tool call: {}", tool_call.title); + } + SessionUpdate::ToolCallUpdate(update) => { + if let Some(status) = &update.fields.status { + eprintln!(" Tool status: {:?}", status); + } + } + _ => {} + } + Ok(()) + }, + sacp::on_receive_notification!(), + ) + // Auto-approve all permission requests + .on_receive_request( + async move |request: RequestPermissionRequest, responder, _cx| { + eprintln!("✅ Auto-approving permission request"); + let option_id = request.options.first().map(|opt| opt.option_id.clone()); + match option_id { + Some(id) => responder.respond(RequestPermissionResponse::new( + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(id)), + )), + None => responder.respond(RequestPermissionResponse::new( + RequestPermissionOutcome::Cancelled, + )), + } + }, + sacp::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); + + 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?; + + // 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; + Ok(()) +} + +fn parse_args(args: &[String]) -> Result<(PathBuf, String), String> { + let mut goose_bin = PathBuf::from("goose"); + let mut i = 0; + + while i < args.len() { + match args[i].as_str() { + "--goose-bin" => { + i += 1; + goose_bin = PathBuf::from(args.get(i).ok_or("--goose-bin requires a value")?); + } + _ => break, + } + i += 1; + } + + let prompt = args[i..].join(" "); + + if prompt.is_empty() { + return Err("Usage: acp_client [--goose-bin PATH] PROMPT".into()); + } + + Ok((goose_bin, prompt)) +} diff --git a/crates/goose-acp/src/custom_requests.rs b/crates/goose-sdk/src/custom_requests.rs similarity index 57% rename from crates/goose-acp/src/custom_requests.rs rename to crates/goose-sdk/src/custom_requests.rs index 2782e25d..1f199aea 100644 --- a/crates/goose-acp/src/custom_requests.rs +++ b/crates/goose-sdk/src/custom_requests.rs @@ -1,3 +1,4 @@ +use sacp::{JsonRpcRequest, JsonRpcResponse}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -20,16 +21,19 @@ pub struct CustomMethodSchema { } /// Add an extension to an active session. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/extensions/add", response = EmptyResponse)] #[serde(rename_all = "camelCase")] pub struct AddExtensionRequest { pub session_id: String, /// Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). + #[serde(default)] pub config: serde_json::Value, } /// Remove an extension from an active session. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/extensions/remove", response = EmptyResponse)] #[serde(rename_all = "camelCase")] pub struct RemoveExtensionRequest { pub session_id: String, @@ -37,20 +41,23 @@ pub struct RemoveExtensionRequest { } /// List all tools available in a session. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/tools", response = GetToolsResponse)] #[serde(rename_all = "camelCase")] pub struct GetToolsRequest { pub session_id: String, } -#[derive(Debug, Serialize, JsonSchema)] +/// Tools response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct GetToolsResponse { /// Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. pub tools: Vec, } /// Read a resource from an extension. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/resource/read", response = ReadResourceResponse)] #[serde(rename_all = "camelCase")] pub struct ReadResourceRequest { pub session_id: String, @@ -58,14 +65,17 @@ pub struct ReadResourceRequest { pub extension_name: String, } -#[derive(Debug, Serialize, JsonSchema)] +/// Resource read response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct ReadResourceResponse { /// The resource result from the extension (MCP ReadResourceResult). + #[serde(default)] pub result: serde_json::Value, } /// Update the working directory for a session. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/working_dir/update", response = EmptyResponse)] #[serde(rename_all = "camelCase")] pub struct UpdateWorkingDirRequest { pub session_id: String, @@ -73,7 +83,8 @@ pub struct UpdateWorkingDirRequest { } /// Get a session by ID. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "session/get", response = GetSessionResponse)] #[serde(rename_all = "camelCase")] pub struct GetSessionRequest { pub session_id: String, @@ -82,45 +93,57 @@ pub struct GetSessionRequest { } /// Get a session response. -#[derive(Debug, Serialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct GetSessionResponse { /// The session object with id, name, working_dir, timestamps, tokens, etc. + #[serde(default)] pub session: serde_json::Value, } /// Delete a session. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "session/delete", response = EmptyResponse)] #[serde(rename_all = "camelCase")] pub struct DeleteSessionRequest { pub session_id: String, } /// Export a session as a JSON string. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/session/export", response = ExportSessionResponse)] #[serde(rename_all = "camelCase")] pub struct ExportSessionRequest { pub session_id: String, } -#[derive(Debug, Serialize, JsonSchema)] +/// Export session response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct ExportSessionResponse { pub data: String, } /// Import a session from a JSON string. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/session/import", response = ImportSessionResponse)] pub struct ImportSessionRequest { pub data: String, } -#[derive(Debug, Serialize, JsonSchema)] +/// Import session response. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct ImportSessionResponse { /// The imported session object. + #[serde(default)] pub session: serde_json::Value, } /// List configured extensions and any warnings. -#[derive(Debug, Serialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request(method = "_goose/config/extensions", response = GetExtensionsResponse)] +pub struct GetExtensionsRequest {} + +/// List configured extensions and any warnings. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct GetExtensionsResponse { /// Array of ExtensionEntry objects with `enabled` flag and config details. pub extensions: Vec, @@ -128,5 +151,5 @@ pub struct GetExtensionsResponse { } /// Empty success response for operations that return no data. -#[derive(Debug, Serialize, JsonSchema)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] pub struct EmptyResponse {} diff --git a/crates/goose-sdk/src/lib.rs b/crates/goose-sdk/src/lib.rs new file mode 100644 index 00000000..6c1c1bf5 --- /dev/null +++ b/crates/goose-sdk/src/lib.rs @@ -0,0 +1 @@ +pub mod custom_requests; diff --git a/ui/acp/src/generated/client.gen.ts b/ui/acp/src/generated/client.gen.ts index 748c3fdc..dd46316c 100644 --- a/ui/acp/src/generated/client.gen.ts +++ b/ui/acp/src/generated/client.gen.ts @@ -12,6 +12,7 @@ import type { DeleteSessionRequest, ExportSessionRequest, ExportSessionResponse, + GetExtensionsRequest, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, @@ -83,8 +84,10 @@ export class GooseExtClient { return zImportSessionResponse.parse(raw) as ImportSessionResponse; } - async GooseConfigExtensions(): Promise { - const raw = await this.conn.extMethod("_goose/config/extensions", {}); + async GooseConfigExtensions( + params: GetExtensionsRequest, + ): Promise { + const raw = await this.conn.extMethod("_goose/config/extensions", params); return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse; } } diff --git a/ui/acp/src/generated/index.ts b/ui/acp/src/generated/index.ts index a51e05ee..f82eb9b9 100644 --- a/ui/acp/src/generated/index.ts +++ b/ui/acp/src/generated/index.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen.js'; +export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { @@ -50,7 +50,7 @@ export const GOOSE_EXT_METHODS = [ }, { method: "_goose/config/extensions", - requestType: null, + requestType: "GetExtensionsRequest", responseType: "GetExtensionsResponse", }, ] as const; diff --git a/ui/acp/src/generated/types.gen.ts b/ui/acp/src/generated/types.gen.ts index a787eb1a..4ce7b461 100644 --- a/ui/acp/src/generated/types.gen.ts +++ b/ui/acp/src/generated/types.gen.ts @@ -9,7 +9,7 @@ export type AddExtensionRequest = { /** * Extension configuration (see ExtensionConfig variants: Stdio, StreamableHttp, Builtin, Platform). */ - config: unknown; + config?: unknown; }; /** @@ -34,6 +34,9 @@ export type GetToolsRequest = { sessionId: string; }; +/** + * Tools response. + */ export type GetToolsResponse = { /** * Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`. @@ -50,11 +53,14 @@ export type ReadResourceRequest = { extensionName: string; }; +/** + * Resource read response. + */ export type ReadResourceResponse = { /** * The resource result from the extension (MCP ReadResourceResult). */ - result: unknown; + result?: unknown; }; /** @@ -80,7 +86,7 @@ export type GetSessionResponse = { /** * The session object with id, name, working_dir, timestamps, tokens, etc. */ - session: unknown; + session?: unknown; }; /** @@ -97,6 +103,9 @@ export type ExportSessionRequest = { sessionId: string; }; +/** + * Export session response. + */ export type ExportSessionResponse = { data: string; }; @@ -108,11 +117,21 @@ export type ImportSessionRequest = { data: string; }; +/** + * Import session response. + */ export type ImportSessionResponse = { /** * The imported session object. */ - session: unknown; + session?: unknown; +}; + +/** + * List configured extensions and any warnings. + */ +export type GetExtensionsRequest = { + [key: string]: unknown; }; /** @@ -129,7 +148,7 @@ export type GetExtensionsResponse = { export type ExtRequest = { id: string; method: string; - params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | GetSessionRequest | DeleteSessionRequest | ExportSessionRequest | ImportSessionRequest | { + params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | GetSessionRequest | DeleteSessionRequest | ExportSessionRequest | ImportSessionRequest | GetExtensionsRequest | { [key: string]: unknown; } | null; }; diff --git a/ui/acp/src/generated/zod.gen.ts b/ui/acp/src/generated/zod.gen.ts index fe4fd531..834d26e1 100644 --- a/ui/acp/src/generated/zod.gen.ts +++ b/ui/acp/src/generated/zod.gen.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; */ export const zAddExtensionRequest = z.object({ sessionId: z.string(), - config: z.unknown() + config: z.unknown().optional().default(null) }); /** @@ -30,6 +30,9 @@ export const zGetToolsRequest = z.object({ sessionId: z.string() }); +/** + * Tools response. + */ export const zGetToolsResponse = z.object({ tools: z.array(z.unknown()) }); @@ -43,8 +46,11 @@ export const zReadResourceRequest = z.object({ extensionName: z.string() }); +/** + * Resource read response. + */ export const zReadResourceResponse = z.object({ - result: z.unknown() + result: z.unknown().optional().default(null) }); /** @@ -67,7 +73,7 @@ export const zGetSessionRequest = z.object({ * Get a session response. */ export const zGetSessionResponse = z.object({ - session: z.unknown() + session: z.unknown().optional().default(null) }); /** @@ -84,6 +90,9 @@ export const zExportSessionRequest = z.object({ sessionId: z.string() }); +/** + * Export session response. + */ export const zExportSessionResponse = z.object({ data: z.string() }); @@ -95,10 +104,18 @@ export const zImportSessionRequest = z.object({ data: z.string() }); +/** + * Import session response. + */ export const zImportSessionResponse = z.object({ - session: z.unknown() + session: z.unknown().optional().default(null) }); +/** + * List configured extensions and any warnings. + */ +export const zGetExtensionsRequest = z.record(z.unknown()); + /** * List configured extensions and any warnings. */ @@ -120,7 +137,8 @@ export const zExtRequest = z.object({ zGetSessionRequest, zDeleteSessionRequest, zExportSessionRequest, - zImportSessionRequest + zImportSessionRequest, + zGetExtensionsRequest ]), z.union([ z.record(z.unknown()),