agent.rs

  1//! Agent implementation for the agent-client-protocol
  2//!
  3//! Implementation Status:
  4//! - [x] initialize: Complete - Basic protocol handshake
  5//! - [x] authenticate: Complete - Accepts any auth (stub)
  6//! - [~] new_session: Partial - Creates session ID but Thread creation needs GPUI context
  7//! - [~] load_session: Stub - Returns not implemented
  8//! - [ ] prompt: Stub - Needs GPUI context and type conversions
  9//! - [~] cancelled: Partial - Removes session from map but needs GPUI cleanup
 10
 11use agent_client_protocol as acp;
 12use gpui::Entity;
 13use std::cell::{Cell, RefCell};
 14use std::collections::HashMap;
 15use std::sync::Arc;
 16
 17use crate::{templates::Templates, Thread};
 18
 19pub struct Agent {
 20    /// Session ID -> Thread entity mapping
 21    sessions: RefCell<HashMap<acp::SessionId, Entity<Thread>>>,
 22    /// Shared templates for all threads
 23    templates: Arc<Templates>,
 24    /// Current protocol version we support
 25    protocol_version: acp::ProtocolVersion,
 26    /// Authentication state
 27    authenticated: Cell<bool>,
 28}
 29
 30impl Agent {
 31    pub fn new(templates: Arc<Templates>) -> Self {
 32        Self {
 33            sessions: RefCell::new(HashMap::new()),
 34            templates,
 35            protocol_version: acp::VERSION,
 36            authenticated: Cell::new(false),
 37        }
 38    }
 39}
 40
 41impl acp::Agent for Agent {
 42    /// COMPLETE: Initialize handshake with client
 43    async fn initialize(
 44        &self,
 45        arguments: acp::InitializeRequest,
 46    ) -> Result<acp::InitializeResponse, acp::Error> {
 47        // For now, we just use the client's requested version
 48        let response_version = arguments.protocol_version.clone();
 49
 50        Ok(acp::InitializeResponse {
 51            protocol_version: response_version,
 52            agent_capabilities: acp::AgentCapabilities::default(),
 53            auth_methods: vec![
 54                // STUB: No authentication required for now
 55                acp::AuthMethod {
 56                    id: acp::AuthMethodId("none".into()),
 57                    label: "No Authentication".to_string(),
 58                    description: Some("No authentication required".to_string()),
 59                },
 60            ],
 61        })
 62    }
 63
 64    /// COMPLETE: Handle authentication (currently just accepts any auth)
 65    async fn authenticate(&self, _arguments: acp::AuthenticateRequest) -> Result<(), acp::Error> {
 66        // STUB: Accept any authentication method for now
 67        self.authenticated.set(true);
 68        Ok(())
 69    }
 70
 71    /// PARTIAL: Create a new session
 72    async fn new_session(
 73        &self,
 74        arguments: acp::NewSessionRequest,
 75    ) -> Result<acp::NewSessionResponse, acp::Error> {
 76        // Check if authenticated
 77        if !self.authenticated.get() {
 78            return Ok(acp::NewSessionResponse { session_id: None });
 79        }
 80
 81        // STUB: Generate a simple session ID
 82        let session_id = acp::SessionId(format!("session-{}", uuid::Uuid::new_v4()).into());
 83
 84        // Create a new Thread for this session
 85        // TODO: This needs to be done on the main thread with proper GPUI context
 86        // For now, we'll return the session ID and expect the actual Thread creation
 87        // to happen when we have access to a GPUI context
 88
 89        // STUB: MCP server support not implemented
 90        if !arguments.mcp_servers.is_empty() {
 91            log::warn!("MCP servers requested but not yet supported");
 92        }
 93
 94        Ok(acp::NewSessionResponse {
 95            session_id: Some(session_id),
 96        })
 97    }
 98
 99    /// STUB: Load existing session
100    async fn load_session(
101        &self,
102        _arguments: acp::LoadSessionRequest,
103    ) -> Result<acp::LoadSessionResponse, acp::Error> {
104        // STUB: Session persistence not implemented
105        Ok(acp::LoadSessionResponse {
106            auth_required: !self.authenticated.get(),
107            auth_methods: if self.authenticated.get() {
108                vec![]
109            } else {
110                vec![acp::AuthMethod {
111                    id: acp::AuthMethodId("none".into()),
112                    label: "No Authentication".to_string(),
113                    description: Some("No authentication required".to_string()),
114                }]
115            },
116        })
117    }
118
119    /// STUB: Handle prompts
120    async fn prompt(&self, arguments: acp::PromptRequest) -> Result<(), acp::Error> {
121        // TODO: This needs to be implemented with proper GPUI context access
122        // The implementation would:
123        // 1. Look up the Thread for this session
124        // 2. Convert acp::ContentBlock to agent2 message format
125        // 3. Call thread.send() with the converted message
126        // 4. Stream responses back to the client
127
128        let _session_id = arguments.session_id;
129        let _prompt = arguments.prompt;
130
131        // STUB: Just acknowledge receipt for now
132        log::info!("Received prompt for session: {}", _session_id.0);
133
134        Err(acp::Error::internal_error().with_data("Prompt handling not yet implemented"))
135    }
136
137    /// PARTIAL: Handle cancellation
138    async fn cancelled(&self, args: acp::CancelledNotification) -> Result<(), acp::Error> {
139        // Remove the session from our map
140        let removed = self.sessions.borrow_mut().remove(&args.session_id);
141
142        if removed.is_some() {
143            // TODO: Properly clean up the Thread entity when we have GPUI context
144            log::info!("Session {} cancelled and removed", args.session_id.0);
145            Ok(())
146        } else {
147            Err(acp::Error::invalid_request()
148                .with_data(format!("Session {} not found", args.session_id.0)))
149        }
150    }
151}
152
153// Helper functions for type conversions between acp and agent2 types
154
155/// Convert acp::ContentBlock to agent2 message format
156/// STUB: Needs implementation
157fn convert_content_block(_block: acp::ContentBlock) -> String {
158    // TODO: Implement proper conversion
159    // This would handle:
160    // - Text content
161    // - Resource links
162    // - Images
163    // - Audio
164    // - Other content types
165    "".to_string()
166}
167
168/// Convert agent2 messages to acp format for responses
169/// STUB: Needs implementation
170fn convert_to_acp_content(_content: &str) -> Vec<acp::ContentBlock> {
171    // TODO: Implement proper conversion
172    vec![]
173}