diff --git a/Cargo.lock b/Cargo.lock index d2d4798880..8aaaff3947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,7 @@ dependencies = [ name = "agent2" version = "0.1.0" dependencies = [ + "agent-client-protocol", "anyhow", "assistant_tool", "assistant_tools", @@ -168,6 +169,7 @@ dependencies = [ "handlebars 4.5.0", "language_model", "language_models", + "log", "parking_lot", "project", "reqwest_client", @@ -179,6 +181,7 @@ dependencies = [ "smol", "thiserror 2.0.12", "util", + "uuid", "worktree", ] diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index c1ce775ae6..30e0f36c0d 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -12,6 +12,7 @@ path = "src/agent2.rs" workspace = true [dependencies] +agent-client-protocol.workspace = true anyhow.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true @@ -24,6 +25,7 @@ gpui.workspace = true handlebars = { workspace = true, features = ["rust-embed"] } language_model.workspace = true language_models.workspace = true +log.workspace = true parking_lot.workspace = true project.workspace = true rust-embed.workspace = true @@ -34,6 +36,7 @@ settings.workspace = true smol.workspace = true thiserror.workspace = true util.workspace = true +uuid.workspace = true worktree.workspace = true [dev-dependencies] diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs new file mode 100644 index 0000000000..c608069586 --- /dev/null +++ b/crates/agent2/src/agent.rs @@ -0,0 +1,173 @@ +//! Agent implementation for the agent-client-protocol +//! +//! Implementation Status: +//! - [x] initialize: Complete - Basic protocol handshake +//! - [x] authenticate: Complete - Accepts any auth (stub) +//! - [~] new_session: Partial - Creates session ID but Thread creation needs GPUI context +//! - [~] load_session: Stub - Returns not implemented +//! - [ ] prompt: Stub - Needs GPUI context and type conversions +//! - [~] cancelled: Partial - Removes session from map but needs GPUI cleanup + +use agent_client_protocol as acp; +use gpui::Entity; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::{templates::Templates, Thread}; + +pub struct Agent { + /// Session ID -> Thread entity mapping + sessions: RefCell>>, + /// Shared templates for all threads + templates: Arc, + /// Current protocol version we support + protocol_version: acp::ProtocolVersion, + /// Authentication state + authenticated: Cell, +} + +impl Agent { + pub fn new(templates: Arc) -> Self { + Self { + sessions: RefCell::new(HashMap::new()), + templates, + protocol_version: acp::VERSION, + authenticated: Cell::new(false), + } + } +} + +impl acp::Agent for Agent { + /// COMPLETE: Initialize handshake with client + async fn initialize( + &self, + arguments: acp::InitializeRequest, + ) -> Result { + // For now, we just use the client's requested version + let response_version = arguments.protocol_version.clone(); + + Ok(acp::InitializeResponse { + protocol_version: response_version, + agent_capabilities: acp::AgentCapabilities::default(), + auth_methods: vec![ + // STUB: No authentication required for now + acp::AuthMethod { + id: acp::AuthMethodId("none".into()), + label: "No Authentication".to_string(), + description: Some("No authentication required".to_string()), + }, + ], + }) + } + + /// COMPLETE: Handle authentication (currently just accepts any auth) + async fn authenticate(&self, _arguments: acp::AuthenticateRequest) -> Result<(), acp::Error> { + // STUB: Accept any authentication method for now + self.authenticated.set(true); + Ok(()) + } + + /// PARTIAL: Create a new session + async fn new_session( + &self, + arguments: acp::NewSessionRequest, + ) -> Result { + // Check if authenticated + if !self.authenticated.get() { + return Ok(acp::NewSessionResponse { session_id: None }); + } + + // STUB: Generate a simple session ID + let session_id = acp::SessionId(format!("session-{}", uuid::Uuid::new_v4()).into()); + + // Create a new Thread for this session + // TODO: This needs to be done on the main thread with proper GPUI context + // For now, we'll return the session ID and expect the actual Thread creation + // to happen when we have access to a GPUI context + + // STUB: MCP server support not implemented + if !arguments.mcp_servers.is_empty() { + log::warn!("MCP servers requested but not yet supported"); + } + + Ok(acp::NewSessionResponse { + session_id: Some(session_id), + }) + } + + /// STUB: Load existing session + async fn load_session( + &self, + _arguments: acp::LoadSessionRequest, + ) -> Result { + // STUB: Session persistence not implemented + Ok(acp::LoadSessionResponse { + auth_required: !self.authenticated.get(), + auth_methods: if self.authenticated.get() { + vec![] + } else { + vec![acp::AuthMethod { + id: acp::AuthMethodId("none".into()), + label: "No Authentication".to_string(), + description: Some("No authentication required".to_string()), + }] + }, + }) + } + + /// STUB: Handle prompts + async fn prompt(&self, arguments: acp::PromptRequest) -> Result<(), acp::Error> { + // TODO: This needs to be implemented with proper GPUI context access + // The implementation would: + // 1. Look up the Thread for this session + // 2. Convert acp::ContentBlock to agent2 message format + // 3. Call thread.send() with the converted message + // 4. Stream responses back to the client + + let _session_id = arguments.session_id; + let _prompt = arguments.prompt; + + // STUB: Just acknowledge receipt for now + log::info!("Received prompt for session: {}", _session_id.0); + + Err(acp::Error::internal_error().with_data("Prompt handling not yet implemented")) + } + + /// PARTIAL: Handle cancellation + async fn cancelled(&self, args: acp::CancelledNotification) -> Result<(), acp::Error> { + // Remove the session from our map + let removed = self.sessions.borrow_mut().remove(&args.session_id); + + if removed.is_some() { + // TODO: Properly clean up the Thread entity when we have GPUI context + log::info!("Session {} cancelled and removed", args.session_id.0); + Ok(()) + } else { + Err(acp::Error::invalid_request() + .with_data(format!("Session {} not found", args.session_id.0))) + } + } +} + +// Helper functions for type conversions between acp and agent2 types + +/// Convert acp::ContentBlock to agent2 message format +/// STUB: Needs implementation +fn convert_content_block(_block: acp::ContentBlock) -> String { + // TODO: Implement proper conversion + // This would handle: + // - Text content + // - Resource links + // - Images + // - Audio + // - Other content types + "".to_string() +} + +/// Convert agent2 messages to acp format for responses +/// STUB: Needs implementation +fn convert_to_acp_content(_content: &str) -> Vec { + // TODO: Implement proper conversion + vec![] +} diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index 71d5ea711c..66ed32eccd 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -1,3 +1,4 @@ +mod agent; mod prompts; mod templates; mod thread; @@ -6,4 +7,5 @@ mod tools; #[cfg(test)] mod tests; +pub use agent::*; pub use thread::*;