From 7992804f32d694d80cdfe0a65dd040b3962c7560 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 4 Sep 2025 15:42:20 +1000 Subject: [PATCH 1/7] basics --- Cargo.lock | 66 +++++++++++++ crates/goose-cli/Cargo.toml | 3 +- crates/goose-cli/src/cli.rs | 10 ++ crates/goose-cli/src/commands/acp.rs | 136 +++++++++++++++++++++++++++ crates/goose-cli/src/commands/mod.rs | 1 + 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 crates/goose-cli/src/commands/acp.rs diff --git a/Cargo.lock b/Cargo.lock index 84d7681a165e..73eda921ec8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,22 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "agent-client-protocol" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b91e5ec3ce05e8effb2a7a3b7b1a587daa6699b9f98bbde6a35e44b8c6c773a" +dependencies = [ + "anyhow", + "async-broadcast", + "futures", + "log", + "parking_lot", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "ahash" version = "0.8.11" @@ -427,6 +443,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" version = "0.4.20" @@ -1525,6 +1553,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "config" version = "0.14.1" @@ -2146,6 +2183,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "eventsource-client" version = "0.12.2" @@ -2644,6 +2702,7 @@ dependencies = [ name = "goose-cli" version = "1.7.0" dependencies = [ + "agent-client-protocol", "anstream", "anyhow", "async-trait", @@ -4573,6 +4632,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -6598,6 +6663,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 0986ec3219e0..d45d8135b0f9 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -22,6 +22,7 @@ mcp-client = { path = "../mcp-client" } mcp-server = { path = "../mcp-server" } mcp-core = { path = "../mcp-core" } rmcp = { workspace = true } +agent-client-protocol = "0.1.1" clap = { version = "4.4", features = ["derive"] } cliclack = "0.3.5" console = "0.15.8" @@ -56,7 +57,7 @@ tower-http = { version = "0.5", features = ["cors", "fs"] } http = "1.0" webbrowser = "1.0" indicatif = "0.17.11" -tokio-util = "0.7.15" +tokio-util = { version = "0.7.15", features = ["compat"] } is-terminal = "0.4.16" anstream = "0.6.18" diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index f3ed00c657b7..1c59cc346226 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -3,6 +3,7 @@ use clap::{Args, Parser, Subcommand}; use goose::config::{Config, ExtensionConfig}; +use crate::commands::acp::run_acp_agent; use crate::commands::bench::agent_generator; use crate::commands::configure::handle_configure; use crate::commands::info::handle_info; @@ -291,6 +292,10 @@ enum Command { #[command(about = "Run one of the mcp servers bundled with goose")] Mcp { name: String }, + /// Run Goose as an ACP (Agent Client Protocol) agent + #[command(about = "Run Goose as an ACP agent server on stdio")] + Acp {}, + /// Start or resume interactive chat sessions #[command( about = "Start or resume interactive chat sessions", @@ -709,6 +714,7 @@ pub async fn cli() -> Result<()> { Some(Command::Configure {}) => "configure", Some(Command::Info { .. }) => "info", Some(Command::Mcp { .. }) => "mcp", + Some(Command::Acp {}) => "acp", Some(Command::Session { .. }) => "session", Some(Command::Project {}) => "project", Some(Command::Projects) => "projects", @@ -739,6 +745,10 @@ pub async fn cli() -> Result<()> { Some(Command::Mcp { name }) => { let _ = run_server(&name).await; } + Some(Command::Acp {}) => { + let _ = run_acp_agent().await; + return Ok(()); + } Some(Command::Session { command, identifier, diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs new file mode 100644 index 000000000000..bc5e0cdeaf39 --- /dev/null +++ b/crates/goose-cli/src/commands/acp.rs @@ -0,0 +1,136 @@ +use agent_client_protocol::{self as acp, Client, SessionNotification}; +use anyhow::Result; +use std::cell::Cell; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; +use tracing::{error, info}; + +/// Simple Goose ACP Agent implementation +struct GooseAcpAgent { + session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>, + next_session_id: Cell, +} + +impl GooseAcpAgent { + fn new( + session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>, + ) -> Self { + Self { + session_update_tx, + next_session_id: Cell::new(0), + } + } +} + +impl acp::Agent for GooseAcpAgent { + async fn initialize( + &self, + arguments: acp::InitializeRequest, + ) -> Result { + info!("ACP: Received initialize request {:?}", arguments); + Ok(acp::InitializeResponse { + protocol_version: acp::V1, + agent_capabilities: acp::AgentCapabilities::default(), + auth_methods: Vec::new(), + }) + } + + async fn authenticate(&self, arguments: acp::AuthenticateRequest) -> Result<(), acp::Error> { + info!("ACP: Received authenticate request {:?}", arguments); + Ok(()) + } + + async fn new_session( + &self, + arguments: acp::NewSessionRequest, + ) -> Result { + info!("ACP: Received new session request {:?}", arguments); + let session_id = self.next_session_id.get(); + self.next_session_id.set(session_id + 1); + Ok(acp::NewSessionResponse { + session_id: acp::SessionId(session_id.to_string().into()), + }) + } + + async fn load_session(&self, arguments: acp::LoadSessionRequest) -> Result<(), acp::Error> { + info!("ACP: Received load session request {:?}", arguments); + // For now, we don't support loading previous sessions + Err(acp::Error::method_not_found()) + } + + async fn prompt( + &self, + arguments: acp::PromptRequest, + ) -> Result { + info!("ACP: Received prompt request {:?}", arguments); + + // Echo back the prompt with a prefix (simple example behavior) + for content in ["Goose ACP Agent received: ".into()] + .into_iter() + .chain(arguments.prompt) + { + let (tx, rx) = oneshot::channel(); + self.session_update_tx + .send(( + SessionNotification { + session_id: arguments.session_id.clone(), + update: acp::SessionUpdate::AgentMessageChunk { content }, + }, + tx, + )) + .map_err(|_| acp::Error::internal_error())?; + rx.await.map_err(|_| acp::Error::internal_error())?; + } + + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + } + + async fn cancel(&self, args: acp::CancelNotification) -> Result<(), acp::Error> { + info!("ACP: Received cancel request {:?}", args); + Ok(()) + } +} + +/// Run the ACP agent server +pub async fn run_acp_agent() -> Result<()> { + info!("Starting Goose ACP agent server on stdio"); + println!("Goose ACP agent started. Listening on stdio..."); + + let outgoing = tokio::io::stdout().compat_write(); + let incoming = tokio::io::stdin().compat(); + + // The AgentSideConnection will spawn futures onto our Tokio runtime. + // LocalSet and spawn_local are used because the futures from the + // agent-client-protocol crate are not Send. + let local_set = tokio::task::LocalSet::new(); + local_set + .run_until(async move { + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + + // Start up the GooseAcpAgent connected to stdio. + let (conn, handle_io) = + acp::AgentSideConnection::new(GooseAcpAgent::new(tx), outgoing, incoming, |fut| { + tokio::task::spawn_local(fut); + }); + + // Kick off a background task to send the agent's session notifications to the client. + tokio::task::spawn_local(async move { + while let Some((session_notification, tx)) = rx.recv().await { + let result = conn.session_notification(session_notification).await; + if let Err(e) = result { + error!("ACP session notification error: {}", e); + break; + } + tx.send(()).ok(); + } + }); + + // Run until stdin/stdout are closed. + handle_io.await + }) + .await?; + + Ok(()) +} diff --git a/crates/goose-cli/src/commands/mod.rs b/crates/goose-cli/src/commands/mod.rs index 72ce9be243ed..43b666de7d2d 100644 --- a/crates/goose-cli/src/commands/mod.rs +++ b/crates/goose-cli/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod acp; pub mod bench; pub mod configure; pub mod info; From 0201f492056199d0174a55f062c8f2902c510f39 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 4 Sep 2025 16:06:13 +1000 Subject: [PATCH 2/7] basics of session --- crates/goose-cli/Cargo.toml | 2 +- crates/goose-cli/src/commands/acp.rs | 168 ++++++++++++++++++++++----- test_acp_client.py | 117 +++++++++++++++++++ 3 files changed, 257 insertions(+), 30 deletions(-) create mode 100755 test_acp_client.py diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index d45d8135b0f9..185f59c0cfbe 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -26,6 +26,7 @@ agent-client-protocol = "0.1.1" clap = { version = "4.4", features = ["derive"] } cliclack = "0.3.5" console = "0.15.8" +uuid = { version = "1.11", features = ["v4"] } dotenvy = "0.15.7" bat = "0.24.0" anyhow = "1.0" @@ -48,7 +49,6 @@ shlex = "1.3.0" async-trait = "0.1.86" base64 = "0.22.1" regex = "1.11.1" -uuid = { version = "1.11", features = ["v4"] } nix = { version = "0.30.1", features = ["process", "signal"] } tar = "0.4" # Web server dependencies diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index bc5e0cdeaf39..ee9db45ec744 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -1,24 +1,61 @@ use agent_client_protocol::{self as acp, Client, SessionNotification}; use anyhow::Result; -use std::cell::Cell; -use tokio::sync::{mpsc, oneshot}; +use goose::agents::Agent; +use goose::conversation::Conversation; +use goose::conversation::message::{Message, MessageContent}; +use goose::providers::create; +use goose::config::Config; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::{mpsc, oneshot, Mutex}; use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; +use tokio_util::sync::CancellationToken; use tracing::{error, info}; -/// Simple Goose ACP Agent implementation +/// Represents a single Goose session for ACP +struct GooseSession { + agent: Agent, + messages: Conversation, +} + +/// Goose ACP Agent implementation that connects to real Goose agents struct GooseAcpAgent { session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>, - next_session_id: Cell, + sessions: Arc>>, + provider: Arc, } impl GooseAcpAgent { - fn new( + async fn new( session_update_tx: mpsc::UnboundedSender<(acp::SessionNotification, oneshot::Sender<()>)>, - ) -> Self { - Self { + ) -> Result { + // Load config and create provider + let config = Config::global(); + + let provider_name: String = config + .get_param("GOOSE_PROVIDER") + .map_err(|e| anyhow::anyhow!("No provider configured: {}", e))?; + + let model_name: String = config + .get_param("GOOSE_MODEL") + .map_err(|e| anyhow::anyhow!("No model configured: {}", e))?; + + let model_config = goose::model::ModelConfig { + model_name: model_name.clone(), + context_limit: None, + temperature: None, + max_tokens: None, + toolshim: false, + toolshim_model: None, + fast_model: None, + }; + let provider = create(&provider_name, model_config)?; + + Ok(Self { session_update_tx, - next_session_id: Cell::new(0), - } + sessions: Arc::new(Mutex::new(HashMap::new())), + provider, + }) } } @@ -45,10 +82,26 @@ impl acp::Agent for GooseAcpAgent { arguments: acp::NewSessionRequest, ) -> Result { info!("ACP: Received new session request {:?}", arguments); - let session_id = self.next_session_id.get(); - self.next_session_id.set(session_id + 1); + + // Generate a unique session ID + let session_id = uuid::Uuid::new_v4().to_string(); + + // Create a new Agent and session for this ACP session + let mut agent = Agent::new(); + agent.update_provider(self.provider.clone()).await + .map_err(|_| acp::Error::internal_error())?; + + let session = GooseSession { + agent, + messages: Conversation::new_unvalidated(Vec::new()), + }; + + // Store the session + let mut sessions = self.sessions.lock().await; + sessions.insert(session_id.clone(), session); + Ok(acp::NewSessionResponse { - session_id: acp::SessionId(session_id.to_string().into()), + session_id: acp::SessionId(session_id.into()), }) } @@ -64,22 +117,77 @@ impl acp::Agent for GooseAcpAgent { ) -> Result { info!("ACP: Received prompt request {:?}", arguments); - // Echo back the prompt with a prefix (simple example behavior) - for content in ["Goose ACP Agent received: ".into()] + // Get the session + let session_id = arguments.session_id.0.to_string(); + let mut sessions = self.sessions.lock().await; + let session = sessions.get_mut(&session_id) + .ok_or_else(|| acp::Error::invalid_params())?; + + // Convert ACP prompt to Goose message + // Extract text from ContentBlocks + let prompt_text = arguments.prompt .into_iter() - .chain(arguments.prompt) - { - let (tx, rx) = oneshot::channel(); - self.session_update_tx - .send(( - SessionNotification { - session_id: arguments.session_id.clone(), - update: acp::SessionUpdate::AgentMessageChunk { content }, - }, - tx, - )) - .map_err(|_| acp::Error::internal_error())?; - rx.await.map_err(|_| acp::Error::internal_error())?; + .filter_map(|block| { + if let acp::ContentBlock::Text(text) = block { + Some(text.text.clone()) + } else { + None + } + }) + .collect::>() + .join(" "); + + let user_message = Message::user().with_text(&prompt_text); + + // Add message to conversation + session.messages.push(user_message); + + // Get agent's reply through the Goose agent + let cancel_token = CancellationToken::new(); + let mut stream = session.agent + .reply(session.messages.clone(), None, Some(cancel_token.clone())) + .await + .map_err(|e| { + error!("Error getting agent reply: {}", e); + acp::Error::internal_error() + })?; + + use futures::StreamExt; + + // Process the agent's response stream + while let Some(event) = stream.next().await { + match event { + Ok(goose::agents::AgentEvent::Message(message)) => { + // Add to conversation + session.messages.push(message.clone()); + + // Stream the response text to the client + for content_item in &message.content { + if let MessageContent::Text(text) = content_item { + let (tx, rx) = oneshot::channel(); + self.session_update_tx + .send(( + SessionNotification { + session_id: arguments.session_id.clone(), + update: acp::SessionUpdate::AgentMessageChunk { + content: text.text.clone().into() + }, + }, + tx, + )) + .map_err(|_| acp::Error::internal_error())?; + rx.await.map_err(|_| acp::Error::internal_error())?; + } + } + } + Ok(_) => { + // Ignore other events for now + } + Err(e) => { + error!("Error in agent response stream: {}", e); + return Err(acp::Error::internal_error()); + } + } } Ok(acp::PromptResponse { @@ -96,7 +204,7 @@ impl acp::Agent for GooseAcpAgent { /// Run the ACP agent server pub async fn run_acp_agent() -> Result<()> { info!("Starting Goose ACP agent server on stdio"); - println!("Goose ACP agent started. Listening on stdio..."); + eprintln!("Goose ACP agent started. Listening on stdio..."); let outgoing = tokio::io::stdout().compat_write(); let incoming = tokio::io::stdin().compat(); @@ -110,8 +218,10 @@ pub async fn run_acp_agent() -> Result<()> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); // Start up the GooseAcpAgent connected to stdio. + let agent = GooseAcpAgent::new(tx).await + .map_err(|e| anyhow::anyhow!("Failed to create ACP agent: {}", e))?; let (conn, handle_io) = - acp::AgentSideConnection::new(GooseAcpAgent::new(tx), outgoing, incoming, |fut| { + acp::AgentSideConnection::new(agent, outgoing, incoming, |fut| { tokio::task::spawn_local(fut); }); diff --git a/test_acp_client.py b/test_acp_client.py new file mode 100755 index 000000000000..0213df2b2c52 --- /dev/null +++ b/test_acp_client.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +Simple ACP client to test the Goose ACP agent. +Connects to goose acp running on stdio. +""" + +import subprocess +import json +import sys +import uuid + +class AcpClient: + def __init__(self): + # Start the goose acp process + self.process = subprocess.Popen( + ['cargo', 'run', '-p', 'goose-cli', '--', 'acp'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=0 + ) + self.request_id = 0 + + def send_request(self, method, params=None): + self.request_id += 1 + request = { + "jsonrpc": "2.0", + "method": method, + "id": self.request_id, + } + if params: + request["params"] = params + + # Send the request + request_str = json.dumps(request) + print(f">>> Sending: {request_str}") + self.process.stdin.write(request_str + '\n') + self.process.stdin.flush() + + # Read response + response_line = self.process.stdout.readline() + if not response_line: + return None + + print(f"<<< Response: {response_line}") + return json.loads(response_line) + + def initialize(self): + return self.send_request("initialize", { + "protocolVersion": "v1", + "clientCapabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + }) + + def new_session(self): + return self.send_request("newSession", { + "context": {} + }) + + def prompt(self, session_id, text): + return self.send_request("prompt", { + "sessionId": session_id, + "prompt": [ + { + "type": "text", + "text": text + } + ] + }) + + def close(self): + if self.process: + self.process.terminate() + self.process.wait() + +def main(): + print("Starting ACP client test...") + client = AcpClient() + + try: + # Initialize the agent + print("\n1. Initializing agent...") + init_response = client.initialize() + if init_response and 'result' in init_response: + print(f" Initialized successfully: {init_response['result']}") + else: + print(f" Failed to initialize: {init_response}") + return + + # Create a new session + print("\n2. Creating new session...") + session_response = client.new_session() + if session_response and 'result' in session_response: + session_id = session_response['result']['sessionId'] + print(f" Created session: {session_id}") + else: + print(f" Failed to create session: {session_response}") + return + + # Send a prompt + print("\n3. Sending prompt...") + prompt_response = client.prompt(session_id, "Hello! What is 2 + 2?") + if prompt_response: + print(f" Got response: {prompt_response}") + else: + print(" Failed to get prompt response") + + finally: + client.close() + print("\nTest complete.") + +if __name__ == "__main__": + main() From b6e8b658b5db9688e4e6bda6f7df3d25051743a6 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 4 Sep 2025 16:13:12 +1000 Subject: [PATCH 3/7] basics now working --- crates/goose-cli/src/commands/acp.rs | 63 ++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index ee9db45ec744..7a5e5c41770a 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -1,16 +1,17 @@ use agent_client_protocol::{self as acp, Client, SessionNotification}; use anyhow::Result; use goose::agents::Agent; +use goose::config::{Config, ExtensionConfigManager}; use goose::conversation::Conversation; use goose::conversation::message::{Message, MessageContent}; use goose::providers::create; -use goose::config::Config; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use tokio::sync::{mpsc, oneshot, Mutex}; +use tokio::task::JoinSet; use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; use tokio_util::sync::CancellationToken; -use tracing::{error, info}; +use tracing::{error, info, warn}; /// Represents a single Goose session for ACP struct GooseSession { @@ -87,10 +88,62 @@ impl acp::Agent for GooseAcpAgent { let session_id = uuid::Uuid::new_v4().to_string(); // Create a new Agent and session for this ACP session - let mut agent = Agent::new(); + let agent = Agent::new(); agent.update_provider(self.provider.clone()).await .map_err(|_| acp::Error::internal_error())?; + // Load and add extensions just like the normal CLI + // Get all enabled extensions from configuration + let extensions_to_run: Vec<_> = ExtensionConfigManager::get_all() + .map_err(|e| { + error!("Failed to load extensions: {}", e); + acp::Error::internal_error() + })? + .into_iter() + .filter(|ext| ext.enabled) + .map(|ext| ext.config) + .collect(); + + // Add extensions to the agent in parallel + let agent_ptr = Arc::new(agent); + let mut set = JoinSet::new(); + let mut waiting_on = HashSet::new(); + + for extension in extensions_to_run { + waiting_on.insert(extension.name()); + let agent_ptr_clone = agent_ptr.clone(); + set.spawn(async move { + ( + extension.name(), + agent_ptr_clone.add_extension(extension.clone()).await, + ) + }); + } + + // Wait for all extensions to load + while let Some(result) = set.join_next().await { + match result { + Ok((name, Ok(_))) => { + waiting_on.remove(&name); + info!("Loaded extension: {}", name); + } + Ok((name, Err(e))) => { + warn!("Failed to load extension '{}': {}", name, e); + waiting_on.remove(&name); + } + Err(e) => { + error!("Task error while loading extension: {}", e); + } + } + } + + // Unwrap the Arc to get the agent back + let agent = Arc::try_unwrap(agent_ptr) + .map_err(|_| { + error!("Failed to unwrap agent Arc"); + acp::Error::internal_error() + })?; + let session = GooseSession { agent, messages: Conversation::new_unvalidated(Vec::new()), @@ -100,6 +153,8 @@ impl acp::Agent for GooseAcpAgent { let mut sessions = self.sessions.lock().await; sessions.insert(session_id.clone(), session); + info!("Created new session with ID: {}", session_id); + Ok(acp::NewSessionResponse { session_id: acp::SessionId(session_id.into()), }) From 7e32b7c8afb1cbb2b9505814e3f69c995f40c6e5 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 4 Sep 2025 16:55:13 +1000 Subject: [PATCH 4/7] tidy --- crates/goose-cli/src/commands/acp.rs | 73 +++++++++++++++------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index 7a5e5c41770a..42f2f7de1f8c 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -2,8 +2,8 @@ use agent_client_protocol::{self as acp, Client, SessionNotification}; use anyhow::Result; use goose::agents::Agent; use goose::config::{Config, ExtensionConfigManager}; -use goose::conversation::Conversation; use goose::conversation::message::{Message, MessageContent}; +use goose::conversation::Conversation; use goose::providers::create; use std::collections::{HashMap, HashSet}; use std::sync::Arc; @@ -32,11 +32,11 @@ impl GooseAcpAgent { ) -> Result { // Load config and create provider let config = Config::global(); - + let provider_name: String = config .get_param("GOOSE_PROVIDER") .map_err(|e| anyhow::anyhow!("No provider configured: {}", e))?; - + let model_name: String = config .get_param("GOOSE_MODEL") .map_err(|e| anyhow::anyhow!("No model configured: {}", e))?; @@ -83,15 +83,17 @@ impl acp::Agent for GooseAcpAgent { arguments: acp::NewSessionRequest, ) -> Result { info!("ACP: Received new session request {:?}", arguments); - + // Generate a unique session ID let session_id = uuid::Uuid::new_v4().to_string(); - + // Create a new Agent and session for this ACP session let agent = Agent::new(); - agent.update_provider(self.provider.clone()).await + agent + .update_provider(self.provider.clone()) + .await .map_err(|_| acp::Error::internal_error())?; - + // Load and add extensions just like the normal CLI // Get all enabled extensions from configuration let extensions_to_run: Vec<_> = ExtensionConfigManager::get_all() @@ -103,12 +105,12 @@ impl acp::Agent for GooseAcpAgent { .filter(|ext| ext.enabled) .map(|ext| ext.config) .collect(); - + // Add extensions to the agent in parallel let agent_ptr = Arc::new(agent); let mut set = JoinSet::new(); let mut waiting_on = HashSet::new(); - + for extension in extensions_to_run { waiting_on.insert(extension.name()); let agent_ptr_clone = agent_ptr.clone(); @@ -119,7 +121,7 @@ impl acp::Agent for GooseAcpAgent { ) }); } - + // Wait for all extensions to load while let Some(result) = set.join_next().await { match result { @@ -136,25 +138,24 @@ impl acp::Agent for GooseAcpAgent { } } } - + // Unwrap the Arc to get the agent back - let agent = Arc::try_unwrap(agent_ptr) - .map_err(|_| { - error!("Failed to unwrap agent Arc"); - acp::Error::internal_error() - })?; - + let agent = Arc::try_unwrap(agent_ptr).map_err(|_| { + error!("Failed to unwrap agent Arc"); + acp::Error::internal_error() + })?; + let session = GooseSession { agent, messages: Conversation::new_unvalidated(Vec::new()), }; - + // Store the session let mut sessions = self.sessions.lock().await; sessions.insert(session_id.clone(), session); - + info!("Created new session with ID: {}", session_id); - + Ok(acp::NewSessionResponse { session_id: acp::SessionId(session_id.into()), }) @@ -175,12 +176,14 @@ impl acp::Agent for GooseAcpAgent { // Get the session let session_id = arguments.session_id.0.to_string(); let mut sessions = self.sessions.lock().await; - let session = sessions.get_mut(&session_id) - .ok_or_else(|| acp::Error::invalid_params())?; - + let session = sessions + .get_mut(&session_id) + .ok_or_else(acp::Error::invalid_params)?; + // Convert ACP prompt to Goose message // Extract text from ContentBlocks - let prompt_text = arguments.prompt + let prompt_text = arguments + .prompt .into_iter() .filter_map(|block| { if let acp::ContentBlock::Text(text) = block { @@ -191,31 +194,32 @@ impl acp::Agent for GooseAcpAgent { }) .collect::>() .join(" "); - + let user_message = Message::user().with_text(&prompt_text); - + // Add message to conversation session.messages.push(user_message); - + // Get agent's reply through the Goose agent let cancel_token = CancellationToken::new(); - let mut stream = session.agent + let mut stream = session + .agent .reply(session.messages.clone(), None, Some(cancel_token.clone())) .await .map_err(|e| { error!("Error getting agent reply: {}", e); acp::Error::internal_error() })?; - + use futures::StreamExt; - + // Process the agent's response stream while let Some(event) = stream.next().await { match event { Ok(goose::agents::AgentEvent::Message(message)) => { // Add to conversation session.messages.push(message.clone()); - + // Stream the response text to the client for content_item in &message.content { if let MessageContent::Text(text) = content_item { @@ -224,8 +228,8 @@ impl acp::Agent for GooseAcpAgent { .send(( SessionNotification { session_id: arguments.session_id.clone(), - update: acp::SessionUpdate::AgentMessageChunk { - content: text.text.clone().into() + update: acp::SessionUpdate::AgentMessageChunk { + content: text.text.clone().into(), }, }, tx, @@ -273,7 +277,8 @@ pub async fn run_acp_agent() -> Result<()> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); // Start up the GooseAcpAgent connected to stdio. - let agent = GooseAcpAgent::new(tx).await + let agent = GooseAcpAgent::new(tx) + .await .map_err(|e| anyhow::anyhow!("Failed to create ACP agent: {}", e))?; let (conn, handle_io) = acp::AgentSideConnection::new(agent, outgoing, incoming, |fut| { From 52a6fd1ce6b657db35bb2e5e7086605ab72e83ca Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 4 Sep 2025 19:23:10 +1000 Subject: [PATCH 5/7] record tool calls --- crates/goose-cli/src/commands/acp.rs | 105 +++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 14 deletions(-) diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index 42f2f7de1f8c..188b3071eb94 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -17,6 +17,7 @@ use tracing::{error, info, warn}; struct GooseSession { agent: Agent, messages: Conversation, + tool_call_ids: HashMap, // Maps internal tool IDs to ACP tool call IDs } /// Goose ACP Agent implementation that connects to real Goose agents @@ -148,6 +149,7 @@ impl acp::Agent for GooseAcpAgent { let session = GooseSession { agent, messages: Conversation::new_unvalidated(Vec::new()), + tool_call_ids: HashMap::new(), }; // Store the session @@ -167,6 +169,7 @@ impl acp::Agent for GooseAcpAgent { Err(acp::Error::method_not_found()) } + #[allow(clippy::too_many_lines)] async fn prompt( &self, arguments: acp::PromptRequest, @@ -220,22 +223,96 @@ impl acp::Agent for GooseAcpAgent { // Add to conversation session.messages.push(message.clone()); - // Stream the response text to the client + // Process message content, including tool calls for content_item in &message.content { - if let MessageContent::Text(text) = content_item { - let (tx, rx) = oneshot::channel(); - self.session_update_tx - .send(( - SessionNotification { - session_id: arguments.session_id.clone(), - update: acp::SessionUpdate::AgentMessageChunk { - content: text.text.clone().into(), + match content_item { + MessageContent::Text(text) => { + // Stream text to the client + let (tx, rx) = oneshot::channel(); + self.session_update_tx + .send(( + SessionNotification { + session_id: arguments.session_id.clone(), + update: acp::SessionUpdate::AgentMessageChunk { + content: text.text.clone().into(), + }, }, - }, - tx, - )) - .map_err(|_| acp::Error::internal_error())?; - rx.await.map_err(|_| acp::Error::internal_error())?; + tx, + )) + .map_err(|_| acp::Error::internal_error())?; + rx.await.map_err(|_| acp::Error::internal_error())?; + } + MessageContent::ToolRequest(tool_request) => { + // Generate ACP tool call ID and track mapping + let acp_tool_id = format!("tool_{}", uuid::Uuid::new_v4()); + session + .tool_call_ids + .insert(tool_request.id.clone(), acp_tool_id.clone()); + + // Extract tool name from the ToolCall if successful + let tool_name = match &tool_request.tool_call { + Ok(tool_call) => tool_call.name.clone(), + Err(_) => "unknown".to_string(), + }; + + // Send tool call notification + let (tx, rx) = oneshot::channel(); + self.session_update_tx + .send(( + SessionNotification { + session_id: arguments.session_id.clone(), + update: acp::SessionUpdate::ToolCall(acp::ToolCall { + id: acp::ToolCallId(acp_tool_id.clone().into()), + title: format!("Calling tool: {}", tool_name), + kind: acp::ToolKind::default(), + status: acp::ToolCallStatus::Pending, + content: Vec::new(), + locations: Vec::new(), + raw_input: None, + raw_output: None, + }), + }, + tx, + )) + .map_err(|_| acp::Error::internal_error())?; + rx.await.map_err(|_| acp::Error::internal_error())?; + + // No need for separate update - status is already set to Pending + } + MessageContent::ToolResponse(tool_response) => { + // Look up the ACP tool call ID + if let Some(acp_tool_id) = + session.tool_call_ids.get(&tool_response.id) + { + // Send completed status update + let (tx, rx) = oneshot::channel(); + self.session_update_tx + .send(( + SessionNotification { + session_id: arguments.session_id.clone(), + update: acp::SessionUpdate::ToolCallUpdate( + acp::ToolCallUpdate { + id: acp::ToolCallId( + acp_tool_id.clone().into(), + ), + fields: acp::ToolCallUpdateFields { + status: Some( + acp::ToolCallStatus::Completed, + ), + ..Default::default() + }, + }, + ), + }, + tx, + )) + .map_err(|_| acp::Error::internal_error())?; + rx.await.map_err(|_| acp::Error::internal_error())?; + } + } + _ => { + // Ignore other content types for now + } } } } From 38333a96ccb1402499004a94e27ea11b0345aa5a Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 5 Sep 2025 09:15:21 +1000 Subject: [PATCH 6/7] checkpoint --- crates/goose-cli/src/commands/acp.rs | 180 +++++++++++++++++++++++---- 1 file changed, 154 insertions(+), 26 deletions(-) diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index 188b3071eb94..7c58ba64931e 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -18,6 +18,7 @@ struct GooseSession { agent: Agent, messages: Conversation, tool_call_ids: HashMap, // Maps internal tool IDs to ACP tool call IDs + cancel_token: Option, // Active cancellation token for prompt processing } /// Goose ACP Agent implementation that connects to real Goose agents @@ -67,9 +68,20 @@ impl acp::Agent for GooseAcpAgent { arguments: acp::InitializeRequest, ) -> Result { info!("ACP: Received initialize request {:?}", arguments); + + // Advertise Goose's capabilities + let agent_capabilities = acp::AgentCapabilities { + load_session: false, // TODO: Implement session persistence + prompt_capabilities: acp::PromptCapabilities { + image: true, // Goose supports image inputs via providers + audio: false, // TODO: Add audio support when providers support it + embedded_context: true, // Goose can handle embedded context resources + }, + }; + Ok(acp::InitializeResponse { protocol_version: acp::V1, - agent_capabilities: acp::AgentCapabilities::default(), + agent_capabilities, auth_methods: Vec::new(), }) } @@ -150,6 +162,7 @@ impl acp::Agent for GooseAcpAgent { agent, messages: Conversation::new_unvalidated(Vec::new()), tool_call_ids: HashMap::new(), + cancel_token: None, }; // Store the session @@ -165,6 +178,24 @@ impl acp::Agent for GooseAcpAgent { async fn load_session(&self, arguments: acp::LoadSessionRequest) -> Result<(), acp::Error> { info!("ACP: Received load session request {:?}", arguments); + + // TODO: Implement session loading using Goose's existing session storage + // 1. Load session from disk using session::read_messages or similar + // 2. Stream all messages back as session/update notifications + // 3. Store the loaded session in self.sessions + // + // The ACP spec requires streaming the entire conversation history back to the client + // as session/update notifications (both user_message_chunk and agent_message_chunk types) + // + // Example flow: + // - Load session file by session_id (might need to map ACP session IDs to Goose session paths) + // - For each message in history: + // - If user message: send user_message_chunk notification + // - If assistant message: send agent_message_chunk notification + // - If tool calls/responses: send appropriate notifications + // - Create GooseSession with loaded agent and messages + // - Store in self.sessions HashMap + // For now, we don't support loading previous sessions Err(acp::Error::method_not_found()) } @@ -184,27 +215,46 @@ impl acp::Agent for GooseAcpAgent { .ok_or_else(acp::Error::invalid_params)?; // Convert ACP prompt to Goose message - // Extract text from ContentBlocks - let prompt_text = arguments - .prompt - .into_iter() - .filter_map(|block| { - if let acp::ContentBlock::Text(text) = block { - Some(text.text.clone()) - } else { - None + let mut user_message = Message::user(); + + // Process all content blocks from the prompt + for block in arguments.prompt { + match block { + acp::ContentBlock::Text(text) => { + user_message = user_message.with_text(&text.text); } - }) - .collect::>() - .join(" "); - - let user_message = Message::user().with_text(&prompt_text); + acp::ContentBlock::Image(image) => { + // Goose supports images via base64 encoded data + // The ACP ImageContent has data as a String directly + user_message = user_message.with_image(&image.data, &image.mime_type); + } + acp::ContentBlock::Resource(resource) => { + // Embed resource content as text with context + match &resource.resource { + acp::EmbeddedResourceResource::TextResourceContents(text_resource) => { + let header = format!("--- Resource: {} ---\n", text_resource.uri); + let content = format!("{}{}\n---\n", header, text_resource.text); + user_message = user_message.with_text(&content); + } + _ => { + // Ignore non-text resources for now + } + } + } + _ => { + // Ignore unsupported content types for now + } + } + } // Add message to conversation session.messages.push(user_message); - // Get agent's reply through the Goose agent + // Create and store cancellation token for this prompt let cancel_token = CancellationToken::new(); + session.cancel_token = Some(cancel_token.clone()); + + // Get agent's reply through the Goose agent let mut stream = session .agent .reply(session.messages.clone(), None, Some(cancel_token.clone())) @@ -216,8 +266,17 @@ impl acp::Agent for GooseAcpAgent { use futures::StreamExt; + // Track if we were cancelled + let mut was_cancelled = false; + // Process the agent's response stream while let Some(event) = stream.next().await { + // Check if we've been cancelled + if cancel_token.is_cancelled() { + was_cancelled = true; + break; + } + match event { Ok(goose::agents::AgentEvent::Message(message)) => { // Add to conversation @@ -249,10 +308,37 @@ impl acp::Agent for GooseAcpAgent { .tool_call_ids .insert(tool_request.id.clone(), acp_tool_id.clone()); - // Extract tool name from the ToolCall if successful - let tool_name = match &tool_request.tool_call { - Ok(tool_call) => tool_call.name.clone(), - Err(_) => "unknown".to_string(), + // Extract tool name and parameters from the ToolCall if successful + let (tool_name, locations) = match &tool_request.tool_call { + Ok(tool_call) => { + let name = tool_call.name.clone(); + + // Extract file locations from certain tools for client tracking + let mut locs = Vec::new(); + if name == "developer__text_editor" { + // Try to extract the path from the arguments + let args = &tool_call.arguments; + if let Some(path_str) = args.get("path").and_then(|p| p.as_str()) { + locs.push(acp::ToolCallLocation { + path: path_str.into(), + line: None, // Line info can be added if available + /* + line: args.get("insert_line") + .or(args.get("view_range")) + .and_then(|v| { + // For view_range, use the first line + if let Some(arr) = v.as_array() { + arr.get(0).and_then(|n| n.as_u64().map(|n| n as u32)) + } else { + v.as_u64().map(|n| n as u32) + } + }),*/ + }); + } + } + (name, locs) + } + Err(_) => ("unknown".to_string(), Vec::new()), }; // Send tool call notification @@ -267,7 +353,7 @@ impl acp::Agent for GooseAcpAgent { kind: acp::ToolKind::default(), status: acp::ToolCallStatus::Pending, content: Vec::new(), - locations: Vec::new(), + locations, raw_input: None, raw_output: None, }), @@ -284,7 +370,14 @@ impl acp::Agent for GooseAcpAgent { if let Some(acp_tool_id) = session.tool_call_ids.get(&tool_response.id) { - // Send completed status update + // Determine if the tool call succeeded or failed + let status = if tool_response.tool_result.is_ok() { + acp::ToolCallStatus::Completed + } else { + acp::ToolCallStatus::Failed + }; + + // Send status update (completed or failed) let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( @@ -296,9 +389,7 @@ impl acp::Agent for GooseAcpAgent { acp_tool_id.clone().into(), ), fields: acp::ToolCallUpdateFields { - status: Some( - acp::ToolCallStatus::Completed, - ), + status: Some(status), ..Default::default() }, }, @@ -310,6 +401,22 @@ impl acp::Agent for GooseAcpAgent { rx.await.map_err(|_| acp::Error::internal_error())?; } } + MessageContent::Thinking(thinking) => { + // Stream thinking/reasoning content as thought chunks + let (tx, rx) = oneshot::channel(); + self.session_update_tx + .send(( + SessionNotification { + session_id: arguments.session_id.clone(), + update: acp::SessionUpdate::AgentThoughtChunk { + content: thinking.thinking.clone().into(), + }, + }, + tx, + )) + .map_err(|_| acp::Error::internal_error())?; + rx.await.map_err(|_| acp::Error::internal_error())?; + } _ => { // Ignore other content types for now } @@ -326,13 +433,34 @@ impl acp::Agent for GooseAcpAgent { } } + // Clear the cancel token since we're done + session.cancel_token = None; + Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, + stop_reason: if was_cancelled { + acp::StopReason::Cancelled + } else { + acp::StopReason::EndTurn + }, }) } async fn cancel(&self, args: acp::CancelNotification) -> Result<(), acp::Error> { info!("ACP: Received cancel request {:?}", args); + + // Get the session and cancel its active operation + let session_id = args.session_id.0.to_string(); + let mut sessions = self.sessions.lock().await; + + if let Some(session) = sessions.get_mut(&session_id) { + if let Some(ref token) = session.cancel_token { + info!("Cancelling active prompt for session {}", session_id); + token.cancel(); + } + } else { + warn!("Cancel request for non-existent session: {}", session_id); + } + Ok(()) } } From e964da1d5f65654cd14d772b78b6138a34040f19 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 5 Sep 2025 11:21:25 +1000 Subject: [PATCH 7/7] tidy up --- JOKES.md | 9 +++++ README.md | 6 +++ crates/goose-cli/src/commands/acp.rs | 58 ++++++++++------------------ 3 files changed, 36 insertions(+), 37 deletions(-) create mode 100644 JOKES.md diff --git a/JOKES.md b/JOKES.md new file mode 100644 index 000000000000..6e4959bebdd8 --- /dev/null +++ b/JOKES.md @@ -0,0 +1,9 @@ +# Goose Jokes 🦢 + +## Why did the Goose become a developer? + +Because it wanted to help debug all those "fowl" errors in the code! + +--- + +*Feel free to add more goose-related programming humor below!* diff --git a/README.md b/README.md index b95c8d03ae25..3476958bb94e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ Designed for maximum flexibility, goose works with any LLM and supports multi-mo - [Documentation](https://block.github.io/goose/docs/category/getting-started) +# A Little Goose Humor 🦢 + +> Why did the developer choose goose as their AI agent? +> +> Because it always helps them "migrate" their code to production! 🚀 + # Goose Around with Us - [Discord](https://discord.gg/block-opensource) - [YouTube](https://www.youtube.com/@blockopensource) diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index 7c58ba64931e..089cc82ea459 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -68,17 +68,17 @@ impl acp::Agent for GooseAcpAgent { arguments: acp::InitializeRequest, ) -> Result { info!("ACP: Received initialize request {:?}", arguments); - + // Advertise Goose's capabilities let agent_capabilities = acp::AgentCapabilities { load_session: false, // TODO: Implement session persistence prompt_capabilities: acp::PromptCapabilities { - image: true, // Goose supports image inputs via providers - audio: false, // TODO: Add audio support when providers support it + image: true, // Goose supports image inputs via providers + audio: false, // TODO: Add audio support when providers support it embedded_context: true, // Goose can handle embedded context resources }, }; - + Ok(acp::InitializeResponse { protocol_version: acp::V1, agent_capabilities, @@ -178,24 +178,17 @@ impl acp::Agent for GooseAcpAgent { async fn load_session(&self, arguments: acp::LoadSessionRequest) -> Result<(), acp::Error> { info!("ACP: Received load session request {:?}", arguments); - - // TODO: Implement session loading using Goose's existing session storage - // 1. Load session from disk using session::read_messages or similar - // 2. Stream all messages back as session/update notifications - // 3. Store the loaded session in self.sessions - // - // The ACP spec requires streaming the entire conversation history back to the client - // as session/update notifications (both user_message_chunk and agent_message_chunk types) + // For now, will start a new session. We could use goose session storage as an enhancement + // we would need to map ACP session IDs to goose session ids (which by default are auto generated) + // normal goose session restore in CLI doesn't load conversation visually. // // Example flow: // - Load session file by session_id (might need to map ACP session IDs to Goose session paths) // - For each message in history: // - If user message: send user_message_chunk notification - // - If assistant message: send agent_message_chunk notification + // - If assistant message: send agent_message_chunk notification // - If tool calls/responses: send appropriate notifications - // - Create GooseSession with loaded agent and messages - // - Store in self.sessions HashMap - + // For now, we don't support loading previous sessions Err(acp::Error::method_not_found()) } @@ -216,7 +209,7 @@ impl acp::Agent for GooseAcpAgent { // Convert ACP prompt to Goose message let mut user_message = Message::user(); - + // Process all content blocks from the prompt for block in arguments.prompt { match block { @@ -253,7 +246,7 @@ impl acp::Agent for GooseAcpAgent { // Create and store cancellation token for this prompt let cancel_token = CancellationToken::new(); session.cancel_token = Some(cancel_token.clone()); - + // Get agent's reply through the Goose agent let mut stream = session .agent @@ -276,7 +269,7 @@ impl acp::Agent for GooseAcpAgent { was_cancelled = true; break; } - + match event { Ok(goose::agents::AgentEvent::Message(message)) => { // Add to conversation @@ -312,27 +305,18 @@ impl acp::Agent for GooseAcpAgent { let (tool_name, locations) = match &tool_request.tool_call { Ok(tool_call) => { let name = tool_call.name.clone(); - + // Extract file locations from certain tools for client tracking let mut locs = Vec::new(); if name == "developer__text_editor" { // Try to extract the path from the arguments let args = &tool_call.arguments; - if let Some(path_str) = args.get("path").and_then(|p| p.as_str()) { + if let Some(path_str) = + args.get("path").and_then(|p| p.as_str()) + { locs.push(acp::ToolCallLocation { path: path_str.into(), - line: None, // Line info can be added if available - /* - line: args.get("insert_line") - .or(args.get("view_range")) - .and_then(|v| { - // For view_range, use the first line - if let Some(arr) = v.as_array() { - arr.get(0).and_then(|n| n.as_u64().map(|n| n as u32)) - } else { - v.as_u64().map(|n| n as u32) - } - }),*/ + line: Some(1), }); } } @@ -376,7 +360,7 @@ impl acp::Agent for GooseAcpAgent { } else { acp::ToolCallStatus::Failed }; - + // Send status update (completed or failed) let (tx, rx) = oneshot::channel(); self.session_update_tx @@ -447,11 +431,11 @@ impl acp::Agent for GooseAcpAgent { async fn cancel(&self, args: acp::CancelNotification) -> Result<(), acp::Error> { info!("ACP: Received cancel request {:?}", args); - + // Get the session and cancel its active operation let session_id = args.session_id.0.to_string(); let mut sessions = self.sessions.lock().await; - + if let Some(session) = sessions.get_mut(&session_id) { if let Some(ref token) = session.cancel_token { info!("Cancelling active prompt for session {}", session_id); @@ -460,7 +444,7 @@ impl acp::Agent for GooseAcpAgent { } else { warn!("Cancel request for non-existent session: {}", session_id); } - + Ok(()) } }