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}