Skip to main content

ai_agent/bridge/
repl_bridge.rs

1//! REPL Bridge - Main entry point for Remote Control functionality.
2//!
3//! Translated from openclaudecode/src/bridge/replBridge.ts
4//!
5//! This module provides the main bridge interface for the REPL.
6
7use crate::bridge::poll_config_defaults::PollIntervalConfig;
8use crate::bridge::repl_bridge_handle::{BridgeControlRequest, BridgeControlResponse, BridgeState};
9use crate::bridge::repl_bridge_transport::ReplBridgeTransport;
10use crate::bridge::SDKMessage;
11use crate::error::AgentError;
12use std::pin::Pin;
13use std::sync::Arc;
14use tokio::sync::RwLock;
15
16/// Boxed future type for async operations.
17type BoxFuture<T> = Pin<Box<dyn std::future::Future<Output = T> + Send>>;
18
19// =============================================================================
20// TYPES
21// =============================================================================
22
23/// Options for initializing the REPL bridge.
24#[derive(Clone)]
25pub struct ReplBridgeOptions {
26    /// Current working directory.
27    pub dir: String,
28    /// Machine name.
29    pub machine_name: String,
30    /// Current git branch.
31    pub branch: String,
32    /// Git repo URL.
33    pub git_repo_url: Option<String>,
34    /// Session title.
35    pub title: String,
36    /// Base API URL.
37    pub base_url: String,
38    /// Session ingress URL.
39    pub session_ingress_url: String,
40    /// Worker type (e.g., "repl", "daemon").
41    pub worker_type: String,
42    /// Get the current OAuth access token.
43    pub get_access_token: Arc<dyn Fn() -> Option<String> + Send + Sync>,
44    /// Create a new session.
45    pub create_session: Arc<
46        dyn Fn(
47                String,
48                String,
49                Option<String>,
50                String,
51            ) -> BoxFuture<Result<Option<String>, AgentError>>
52            + Send
53            + Sync,
54    >,
55    /// Archive a session.
56    pub archive_session: Arc<dyn Fn(String) -> BoxFuture<Result<(), AgentError>> + Send + Sync>,
57    /// Get current session title (for reconnection).
58    pub get_current_title: Option<Arc<dyn Fn() -> String + Send + Sync>>,
59    /// Convert internal messages to SDK format.
60    pub to_sdk_messages:
61        Option<Arc<dyn Fn(Vec<crate::types::Message>) -> Vec<SDKMessage> + Send + Sync>>,
62    /// Handle OAuth 401 refresh.
63    pub on_auth_401:
64        Option<Arc<dyn Fn(String) -> BoxFuture<Result<bool, AgentError>> + Send + Sync>>,
65    /// Get poll interval config.
66    pub get_poll_interval_config: Option<Arc<dyn Fn() -> PollIntervalConfig + Send + Sync>>,
67    /// Max initial messages to replay on connect.
68    pub initial_history_cap: Option<u32>,
69    /// Initial messages to flush on connect.
70    pub initial_messages: Option<Vec<crate::types::Message>>,
71    /// Previously flushed UUIDs (for dedup).
72    pub previously_flushed_uuids:
73        Option<Arc<dyn Fn() -> std::collections::HashSet<String> + Send + Sync>>,
74    /// Callback for inbound messages.
75    pub on_inbound_message: Option<Arc<dyn Fn(SDKMessage) + Send + Sync>>,
76    /// Callback for permission responses.
77    pub on_permission_response: Option<Arc<dyn Fn(BridgeControlResponse) + Send + Sync>>,
78    /// Callback for interrupt.
79    pub on_interrupt: Option<Arc<dyn Fn() + Send + Sync>>,
80    /// Callback for model change.
81    pub on_set_model: Option<Arc<dyn Fn(Option<String>) + Send + Sync>>,
82    /// Callback for max thinking tokens change.
83    pub on_set_max_thinking_tokens: Option<Arc<dyn Fn(Option<u32>) + Send + Sync>>,
84    /// Callback for permission mode change.
85    pub on_set_permission_mode:
86        Option<Arc<dyn Fn(crate::permission::PermissionMode) -> Result<(), String> + Send + Sync>>,
87    /// Callback for state changes.
88    pub on_state_change: Option<Arc<dyn Fn(BridgeState, Option<String>) + Send + Sync>>,
89    /// Callback for user messages (title derivation).
90    pub on_user_message: Option<Arc<dyn Fn(String, String) -> bool + Send + Sync>>,
91    /// Whether this is a perpetual (persistent) bridge.
92    pub perpetual: Option<bool>,
93    /// Initial SSE sequence number (for daemon persistence).
94    pub initial_sse_sequence_num: Option<u64>,
95}
96
97impl Default for ReplBridgeOptions {
98    fn default() -> Self {
99        Self {
100            dir: std::env::current_dir()
101                .map(|p| p.to_string_lossy().to_string())
102                .unwrap_or_default(),
103            machine_name: "unknown".to_string(),
104            branch: String::new(),
105            git_repo_url: None,
106            title: String::new(),
107            base_url: String::new(),
108            session_ingress_url: String::new(),
109            worker_type: "repl".to_string(),
110            get_access_token: Arc::new(|| None),
111            create_session: Arc::new(|_, _, _, _| Box::pin(async { Ok(None) })),
112            archive_session: Arc::new(|_| Box::pin(async { Ok(()) })),
113            get_current_title: None,
114            to_sdk_messages: None,
115            on_auth_401: None,
116            get_poll_interval_config: None,
117            initial_history_cap: None,
118            initial_messages: None,
119            previously_flushed_uuids: None,
120            on_inbound_message: None,
121            on_permission_response: None,
122            on_interrupt: None,
123            on_set_model: None,
124            on_set_max_thinking_tokens: None,
125            on_set_permission_mode: None,
126            on_state_change: None,
127            on_user_message: None,
128            perpetual: None,
129            initial_sse_sequence_num: None,
130        }
131    }
132}
133
134/// Main handle for interacting with the REPL bridge.
135pub struct ReplBridge {
136    /// The bridge session ID.
137    pub session_id: RwLock<String>,
138    /// The environment ID.
139    pub environment_id: RwLock<String>,
140    /// The session ingress URL.
141    pub session_ingress_url: String,
142    /// Internal handle (if using env-less v2).
143    pub inner: RwLock<Option<Arc<dyn ReplBridgeHandleInner>>>,
144}
145
146/// Internal bridge handle trait.
147pub trait ReplBridgeHandleInner: Send + Sync {
148    /// Write messages to the bridge.
149    fn write_messages(&self, messages: Vec<SDKMessage>);
150    /// Write SDK messages directly.
151    fn write_sdk_messages(&self, messages: Vec<SDKMessage>);
152    /// Send a control request.
153    fn send_control_request(&self, request: BridgeControlRequest);
154    /// Send a control response.
155    fn send_control_response(&self, response: BridgeControlResponse);
156    /// Send a control cancel request.
157    fn send_control_cancel_request(&self, request_id: &str);
158    /// Send a result message.
159    fn send_result(&self);
160    /// Tear down the bridge.
161    fn teardown(&self) -> BoxFuture<()>;
162}
163
164impl ReplBridge {
165    /// Create a new REPL bridge.
166    pub fn new(session_id: String, environment_id: String, session_ingress_url: String) -> Self {
167        Self {
168            session_id: RwLock::new(session_id),
169            environment_id: RwLock::new(environment_id),
170            session_ingress_url,
171            inner: RwLock::new(None),
172        }
173    }
174
175    /// Get the bridge session ID.
176    pub async fn bridge_session_id(&self) -> String {
177        self.session_id.read().await.clone()
178    }
179
180    /// Get the environment ID.
181    pub async fn environment_id(&self) -> String {
182        self.environment_id.read().await.clone()
183    }
184
185    /// Get the session ingress URL.
186    pub fn session_ingress_url(&self) -> &str {
187        &self.session_ingress_url
188    }
189
190    /// Write messages to the bridge.
191    pub fn write_messages(&self, messages: Vec<SDKMessage>) {
192        if let Some(inner) = self.inner.blocking_read().as_ref() {
193            inner.write_messages(messages);
194        }
195    }
196
197    /// Write SDK messages directly to the bridge.
198    pub fn write_sdk_messages(&self, messages: Vec<SDKMessage>) {
199        if let Some(inner) = self.inner.blocking_read().as_ref() {
200            inner.write_sdk_messages(messages);
201        }
202    }
203
204    /// Send a control request.
205    pub fn send_control_request(&self, request: BridgeControlRequest) {
206        if let Some(inner) = self.inner.blocking_read().as_ref() {
207            inner.send_control_request(request);
208        }
209    }
210
211    /// Send a control response.
212    pub fn send_control_response(&self, response: BridgeControlResponse) {
213        if let Some(inner) = self.inner.blocking_read().as_ref() {
214            inner.send_control_response(response);
215        }
216    }
217
218    /// Send a control cancel request.
219    pub fn send_control_cancel_request(&self, request_id: &str) {
220        if let Some(inner) = self.inner.blocking_read().as_ref() {
221            inner.send_control_cancel_request(request_id);
222        }
223    }
224
225    /// Send a result message.
226    pub fn send_result(&self) {
227        if let Some(inner) = self.inner.blocking_read().as_ref() {
228            inner.send_result();
229        }
230    }
231
232    /// Tear down the bridge.
233    pub async fn teardown(&self) {
234        if let Some(inner) = self.inner.write().await.take() {
235            inner.teardown().await;
236        }
237    }
238}
239
240// =============================================================================
241// INITIALIZATION
242// =============================================================================
243
244/// Initialize the REPL bridge with the given options.
245///
246/// Returns None if initialization fails.
247pub async fn init_repl_bridge(
248    _options: ReplBridgeOptions,
249) -> Result<Option<ReplBridge>, AgentError> {
250    // Note: The actual implementation would:
251    // 1. Check if v2 (env-less) bridge is enabled via GrowthBook
252    // 2. If v2: call init_env_less_bridge_core
253    // 3. If v1: call init_bridge_core (env-based)
254    // 4. Return the appropriate handle
255
256    // For SDK, we return None as actual bridge initialization
257    // requires CLI-specific infrastructure
258    Ok(None)
259}
260
261// =============================================================================
262// BRIDGE STATE MANAGEMENT
263// =============================================================================
264
265/// Global pointer to the active REPL bridge handle, so callers outside
266/// useReplBridge's React tree (tools, slash commands) can invoke handle methods.
267static REPL_BRIDGE_HANDLE: std::sync::OnceLock<ReplBridge> = std::sync::OnceLock::new();
268
269/// Set the global REPL bridge handle.
270pub fn set_repl_bridge_handle(bridge: Option<ReplBridge>) {
271    let _ = REPL_BRIDGE_HANDLE.set(
272        bridge.unwrap_or_else(|| ReplBridge::new(String::new(), String::new(), String::new())),
273    );
274}
275
276/// Get the global REPL bridge handle.
277pub fn get_repl_bridge_handle() -> Option<&'static ReplBridge> {
278    REPL_BRIDGE_HANDLE.get()
279}