//! 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 agent_client_protocol::schema::{ ContentBlock, InitializeRequest, ProtocolVersion, RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome, SessionNotification, SessionUpdate, }; use agent_client_protocol::{Client, ConnectionTo}; use goose_sdk::custom_requests::GetExtensionsRequest; 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 = agent_client_protocol::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(()) }, agent_client_protocol::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, )), } }, 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); 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)) }