From 5d621bef789a7e534d1abb8ac9e4653955040d24 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 2 Aug 2025 00:15:22 -0600 Subject: [PATCH] WIP --- Cargo.lock | 1 + crates/agent2/Cargo.toml | 1 + crates/agent2/src/agent.rs | 218 +++++++++++++++---------------------- 3 files changed, 87 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8aaaff3947..705ed44d38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,7 @@ dependencies = [ name = "agent2" version = "0.1.0" dependencies = [ + "acp_thread", "agent-client-protocol", "anyhow", "assistant_tool", diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml index 30e0f36c0d..72f5f14008 100644 --- a/crates/agent2/Cargo.toml +++ b/crates/agent2/Cargo.toml @@ -12,6 +12,7 @@ path = "src/agent2.rs" workspace = true [dependencies] +acp_thread.workspace = true agent-client-protocol.workspace = true anyhow.workspace = true assistant_tool.workspace = true diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index c608069586..c1c28ad41b 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -1,173 +1,125 @@ -//! 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 anyhow::Result; +use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use project::Project; use std::collections::HashMap; +use std::path::Path; +use std::rc::Rc; use std::sync::Arc; use crate::{templates::Templates, Thread}; pub struct Agent { /// Session ID -> Thread entity mapping - sessions: RefCell>>, + sessions: HashMap>, /// 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()), + sessions: 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(); +/// Wrapper struct that implements the AgentConnection trait +pub struct AgentConnection(pub Entity); - 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()), - }, - ], +impl acp_thread::AgentConnection for AgentConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>> { + let _cwd = cwd.to_owned(); + let agent = self.0.clone(); + + cx.spawn(async move |cx| { + // Create Thread and store in Agent + let (session_id, _thread) = + agent.update(cx, |agent, cx: &mut gpui::Context| { + let thread = cx.new(|_| Thread::new(agent.templates.clone())); + let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into()); + agent.sessions.insert(session_id.clone(), thread.clone()); + (session_id, thread) + })?; + + // Create AcpThread + let acp_thread = cx.update(|cx| { + cx.new(|cx| acp_thread::AcpThread::new("agent2", self, project, session_id, cx)) + })?; + + Ok(acp_thread) }) } - /// 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(()) + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] // No auth for in-process } - /// 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), - }) + fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task> { + Task::ready(Ok(())) } - /// 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()), - }] - }, - }) - } + fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task> { + let session_id = params.session_id.clone(); + let agent = self.0.clone(); - /// 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 + cx.spawn(|cx| async move { + // Get thread + let thread: Entity = agent + .read_with(cx, |agent, _| agent.sessions.get(&session_id).cloned())? + .ok_or_else(|| anyhow::anyhow!("Session not found"))?; - let _session_id = arguments.session_id; - let _prompt = arguments.prompt; + // Convert prompt to message + let message = convert_prompt_to_message(params.prompt); - // STUB: Just acknowledge receipt for now - log::info!("Received prompt for session: {}", _session_id.0); + // TODO: Get model from somewhere - for now use a placeholder + log::warn!("Model selection not implemented - need to get from UI context"); - Err(acp::Error::internal_error().with_data("Prompt handling not yet implemented")) - } + // Send to thread + // thread.update(&mut cx, |thread, cx| { + // thread.send(model, message, cx) + // })?; - /// 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))) - } + }) + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + self.0.update(cx, |agent, _cx| { + agent.sessions.remove(session_id); + }); } } -// Helper functions for type conversions between acp and agent2 types +/// Convert ACP content blocks to a message string +fn convert_prompt_to_message(blocks: Vec) -> String { + let mut message = String::new(); -/// 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() -} + for block in blocks { + match block { + acp::ContentBlock::Text(text) => { + message.push_str(&text.text); + } + acp::ContentBlock::ResourceLink(link) => { + message.push_str(&format!(" @{} ", link.uri)); + } + acp::ContentBlock::Image(_) => { + message.push_str(" [image] "); + } + acp::ContentBlock::Audio(_) => { + message.push_str(" [audio] "); + } + acp::ContentBlock::Resource(resource) => { + message.push_str(&format!(" [resource: {:?}] ", resource.resource)); + } + } + } -/// Convert agent2 messages to acp format for responses -/// STUB: Needs implementation -fn convert_to_acp_content(_content: &str) -> Vec { - // TODO: Implement proper conversion - vec![] + message }