rust acp client for extension methods (#8227)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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 `<RequestType as sacp::JsonRpcMessage>::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<ListSessionsResponse, sacp::Error> { .. }
|
||||
/// #[custom_method(GetExtensionsRequest)]
|
||||
/// async fn on_get_extensions(&self) -> Result<GetExtensionsResponse, sacp::Error> { .. }
|
||||
///
|
||||
/// // Typed params — JSON params auto-deserialized
|
||||
/// #[custom_method("session/get")]
|
||||
/// #[custom_method(GetSessionRequest)]
|
||||
/// async fn on_get_session(&self, req: GetSessionRequest) -> Result<GetSessionResponse, sacp::Error> { .. }
|
||||
/// ```
|
||||
///
|
||||
@@ -40,15 +44,15 @@ pub fn custom_methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
|
||||
let mut routes: Vec<Route> = 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::<Lit>() {
|
||||
route_name = Some(s.value());
|
||||
if let Ok(ty) = meta_list.parse_args::<Type>() {
|
||||
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<serde_json::Value, sacp::Error> {
|
||||
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<Type>,
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/extensions",
|
||||
"requestType": null,
|
||||
"requestType": "GetExtensionsRequest",
|
||||
"responseType": "GetExtensionsResponse"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<GetToolsResponse, sacp::Error> {
|
||||
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<GetExtensionsResponse, sacp::Error> {
|
||||
let extensions = goose::config::extensions::get_all_extensions();
|
||||
let warnings = goose::config::extensions::get_warnings();
|
||||
|
||||
23
crates/goose-sdk/Cargo.toml
Normal file
23
crates/goose-sdk/Cargo.toml
Normal file
@@ -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"]
|
||||
156
crates/goose-sdk/examples/acp_client.rs
Normal file
156
crates/goose-sdk/examples/acp_client.rs
Normal file
@@ -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<dyn std::error::Error>> {
|
||||
// Parse args: [--goose-bin PATH] PROMPT
|
||||
let args: Vec<String> = 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<sacp::Agent>| {
|
||||
// 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))
|
||||
}
|
||||
@@ -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<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// 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<serde_json::Value>,
|
||||
@@ -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 {}
|
||||
1
crates/goose-sdk/src/lib.rs
Normal file
1
crates/goose-sdk/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod custom_requests;
|
||||
@@ -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<GetExtensionsResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/config/extensions", {});
|
||||
async GooseConfigExtensions(
|
||||
params: GetExtensionsRequest,
|
||||
): Promise<GetExtensionsResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/config/extensions", params);
|
||||
return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user