Skip to main content

pi/modes/rpc/
mode_host.rs

1//! Concrete `RpcSessionHost` implementation for `Arc<AgentSessionRuntime>`.
2//!
3//! Adapts the product session/runtime surface to the trait defined in
4//! [`super::server`]. Each async method clones the needed `Arc` so the
5//! returned future is `'static` and can be `tokio::spawn`'d.
6
7use std::sync::Arc;
8
9use futures::Future;
10use futures::future::BoxFuture;
11use pi_agent::{AgentMessage, QueueMode};
12use pi_ai::{ImageContent, Model, ModelThinkingLevel};
13use std::pin::Pin;
14
15use crate::core::agent_session::AgentSession;
16use crate::core::agent_session::events::AgentSessionEvent;
17use crate::core::agent_session::extension::ExtensionBindings;
18use crate::core::agent_session::model::CycleDirection;
19use crate::core::agent_session::prompt::{
20    PreflightCallback, PromptOptions, StreamingBehavior as PromptStreamingBehavior,
21};
22use crate::core::agent_session_runtime::{
23    AgentSessionRuntime, ForkOutcome, ForkPosition, NewSessionOptions, RebindSessionCallback,
24    SwitchSessionOptions,
25};
26use crate::core::compaction::CompactionResult;
27use crate::core::extension_host::HostExtensionRunner;
28use crate::core::resources::{SlashCommandInfo, SlashCommandSource};
29use crate::core::sessions::{SessionEntry, SessionTreeNode};
30
31use super::server::{ModelCycleResult, RebindCallback, RpcSessionHost};
32use super::types::{
33    BashResult, ForkMessage, RpcSessionState, RpcSessionTreeNode, RpcSlashCommand,
34    RpcSlashCommandSource, SessionStats, StreamingBehavior,
35};
36
37// ---------------------------------------------------------------------------
38// Conversion helpers
39// ---------------------------------------------------------------------------
40
41fn convert_tree(nodes: Vec<SessionTreeNode>) -> Vec<RpcSessionTreeNode> {
42    nodes
43        .into_iter()
44        .map(|n| RpcSessionTreeNode {
45            entry: n.entry,
46            children: convert_tree(n.children),
47            label: n.label,
48            label_timestamp: n.label_timestamp,
49        })
50        .collect()
51}
52
53fn convert_bash_result(r: crate::core::agent_session::bash::BashResult) -> BashResult {
54    BashResult {
55        output: r.output,
56        exit_code: r.exit_code,
57        cancelled: r.cancelled,
58        truncated: r.truncated,
59        full_output_path: r.full_output_path,
60    }
61}
62
63fn convert_stats(s: crate::core::agent_session::stats::SessionStats) -> SessionStats {
64    use crate::modes::rpc::types::{ContextUsage, SessionStatsTokens};
65    SessionStats {
66        session_file: s.session_file,
67        session_id: s.session_id,
68        user_messages: s.user_messages,
69        assistant_messages: s.assistant_messages,
70        tool_calls: s.tool_calls,
71        tool_results: s.tool_results,
72        total_messages: s.total_messages,
73        tokens: SessionStatsTokens {
74            input: s.tokens.input,
75            output: s.tokens.output,
76            cache_read: s.tokens.cache_read,
77            cache_write: s.tokens.cache_write,
78            total: s.tokens.total,
79        },
80        cost: s.cost,
81        context_usage: s.context_usage.map(|c| ContextUsage {
82            tokens: c.tokens,
83            context_window: c.context_window,
84            percent: c.percent,
85        }),
86    }
87}
88fn convert_slash_command(command: SlashCommandInfo) -> RpcSlashCommand {
89    RpcSlashCommand {
90        name: command.name,
91        description: command.description,
92        source: match command.source {
93            SlashCommandSource::Extension => RpcSlashCommandSource::Extension,
94            SlashCommandSource::Prompt => RpcSlashCommandSource::Prompt,
95            SlashCommandSource::Skill => RpcSlashCommandSource::Skill,
96        },
97        source_info: command.source_info.into(),
98    }
99}
100
101// ---------------------------------------------------------------------------
102// RpcSessionHost for Arc<AgentSessionRuntime>
103// ---------------------------------------------------------------------------
104
105impl RpcSessionHost for Arc<AgentSessionRuntime> {
106    fn prompt(
107        &self,
108        message: String,
109        images: Vec<ImageContent>,
110        streaming_behavior: Option<StreamingBehavior>,
111        preflight: PreflightCallback,
112    ) -> BoxFuture<'static, Result<(), String>> {
113        let session = self.session();
114        Box::pin(async move {
115            let opts = PromptOptions {
116                images,
117                streaming_behavior: streaming_behavior.map(|sb| match sb {
118                    StreamingBehavior::Steer => PromptStreamingBehavior::Steer,
119                    StreamingBehavior::FollowUp => PromptStreamingBehavior::FollowUp,
120                }),
121                source: Some("rpc".into()),
122                preflight_result: Some(preflight),
123                ..Default::default()
124            };
125            session
126                .prompt(&message, opts)
127                .await
128                .map_err(|e| e.to_string())
129        })
130    }
131
132    fn steer(
133        &self,
134        message: String,
135        images: Vec<ImageContent>,
136    ) -> BoxFuture<'static, Result<(), String>> {
137        let session = self.session();
138        Box::pin(async move { session.steer(&message, images).map_err(|e| e.to_string()) })
139    }
140
141    fn follow_up(
142        &self,
143        message: String,
144        images: Vec<ImageContent>,
145    ) -> BoxFuture<'static, Result<(), String>> {
146        let session = self.session();
147        Box::pin(async move {
148            session
149                .follow_up(&message, images)
150                .map_err(|e| e.to_string())
151        })
152    }
153
154    fn abort(&self) -> BoxFuture<'static, ()> {
155        let session = self.session();
156        Box::pin(async move {
157            session.abort().await;
158        })
159    }
160
161    fn get_state(&self) -> BoxFuture<'static, RpcSessionState> {
162        let session = self.session();
163        Box::pin(async move {
164            let m = session.model();
165            RpcSessionState {
166                model: if m.id.is_empty() { None } else { Some(m) },
167                thinking_level: session.thinking_level(),
168                is_streaming: session.is_streaming(),
169                is_compacting: session.is_compacting(),
170                steering_mode: session.steering_mode(),
171                follow_up_mode: session.follow_up_mode(),
172                session_file: session.session_file().await,
173                session_id: session.session_id().await,
174                session_name: session.session_name().await,
175                auto_compaction_enabled: session.auto_compaction_enabled(),
176                message_count: session.message_count() as u64,
177                pending_message_count: session.pending_message_count() as u64,
178            }
179        })
180    }
181
182    fn get_available_models(&self) -> BoxFuture<'static, Vec<Model>> {
183        let session = self.session();
184        Box::pin(async move {
185            if let Some(mr) = session.model_runtime_handle() {
186                return mr.get_available(None).await.unwrap_or_default();
187            }
188            Vec::new()
189        })
190    }
191
192    fn set_model(&self, model: Model) -> BoxFuture<'static, Result<(), String>> {
193        let session = self.session();
194        Box::pin(async move { session.set_model(model).await.map_err(|e| e.to_string()) })
195    }
196
197    fn cycle_model(&self) -> BoxFuture<'static, Option<ModelCycleResult>> {
198        let session = self.session();
199        Box::pin(async move {
200            session
201                .cycle_model(CycleDirection::Forward)
202                .await
203                .map(|r| ModelCycleResult {
204                    model: r.model,
205                    thinking_level: r.thinking_level,
206                    is_scoped: r.is_scoped,
207                })
208        })
209    }
210
211    fn set_thinking_level(&self, level: ModelThinkingLevel) -> BoxFuture<'static, bool> {
212        let session = self.session();
213        Box::pin(async move { session.set_thinking_level(level).await })
214    }
215
216    fn cycle_thinking_level(&self) -> BoxFuture<'static, Option<ModelThinkingLevel>> {
217        let session = self.session();
218        Box::pin(async move { session.cycle_thinking_level().await })
219    }
220
221    fn set_steering_mode(&self, mode: QueueMode) -> BoxFuture<'static, ()> {
222        let session = self.session();
223        Box::pin(async move {
224            session.set_steering_mode(mode);
225        })
226    }
227
228    fn set_follow_up_mode(&self, mode: QueueMode) -> BoxFuture<'static, ()> {
229        let session = self.session();
230        Box::pin(async move {
231            session.set_follow_up_mode(mode);
232        })
233    }
234
235    fn compact(
236        &self,
237        custom_instructions: Option<String>,
238    ) -> BoxFuture<'static, Result<CompactionResult, String>> {
239        let session = self.session();
240        Box::pin(async move {
241            session
242                .compact(custom_instructions.as_deref())
243                .await
244                .map_err(|e| e.to_string())
245        })
246    }
247
248    fn set_auto_compaction(&self, enabled: bool) -> BoxFuture<'static, ()> {
249        let session = self.session();
250        Box::pin(async move {
251            session.set_auto_compaction_enabled(enabled);
252        })
253    }
254
255    fn set_auto_retry(&self, enabled: bool) -> BoxFuture<'static, ()> {
256        let session = self.session();
257        Box::pin(async move {
258            session.set_auto_retry_enabled(enabled);
259        })
260    }
261
262    fn abort_retry(&self) -> BoxFuture<'static, ()> {
263        let session = self.session();
264        Box::pin(async move {
265            session.abort_retry();
266        })
267    }
268
269    fn execute_bash(
270        &self,
271        command: String,
272        exclude_from_context: Option<bool>,
273    ) -> BoxFuture<'static, Result<BashResult, String>> {
274        let session = self.session();
275        Box::pin(async move {
276            let opts = crate::core::agent_session::bash::ExecuteBashOptions {
277                exclude_from_context: exclude_from_context.unwrap_or(false),
278                ..Default::default()
279            };
280            session
281                .execute_bash(&command, None::<fn(&str)>, opts)
282                .await
283                .map(convert_bash_result)
284                .map_err(|e| e.to_string())
285        })
286    }
287
288    fn abort_bash(&self) -> BoxFuture<'static, ()> {
289        let session = self.session();
290        Box::pin(async move {
291            session.abort_bash();
292        })
293    }
294
295    fn get_session_stats(&self) -> BoxFuture<'static, SessionStats> {
296        let session = self.session();
297        Box::pin(async move { convert_stats(session.get_session_stats().await) })
298    }
299
300    fn export_to_html(
301        &self,
302        output_path: Option<String>,
303    ) -> BoxFuture<'static, Result<String, String>> {
304        let session = self.session();
305        Box::pin(async move {
306            session
307                .export_to_html(output_path.as_deref(), None)
308                .await
309                .map_err(|e| e.to_string())
310        })
311    }
312
313    fn set_session_name(&self, name: String) -> BoxFuture<'static, Result<(), String>> {
314        let session = self.session();
315        Box::pin(async move {
316            session
317                .set_session_name(&name)
318                .await
319                .map_err(|e| e.to_string())
320        })
321    }
322
323    fn new_session(
324        &self,
325        parent_session: Option<String>,
326    ) -> BoxFuture<'static, Result<bool, String>> {
327        let this = Arc::clone(self);
328        Box::pin(async move {
329            this.as_ref()
330                .new_session(NewSessionOptions { parent_session })
331                .await
332                .map(|o| o.cancelled)
333                .map_err(|e| e.to_string())
334        })
335    }
336
337    fn switch_session(&self, session_path: String) -> BoxFuture<'static, Result<bool, String>> {
338        let this = Arc::clone(self);
339        Box::pin(async move {
340            this.as_ref()
341                .switch_session(&session_path, SwitchSessionOptions::default())
342                .await
343                .map(|o| o.cancelled)
344                .map_err(|e| e.to_string())
345        })
346    }
347
348    fn fork(
349        &self,
350        entry_id: String,
351        position: ForkPosition,
352    ) -> BoxFuture<'static, Result<ForkOutcome, String>> {
353        let this = Arc::clone(self);
354        Box::pin(async move {
355            this.as_ref()
356                .fork(&entry_id, position)
357                .await
358                .map_err(|e| e.to_string())
359        })
360    }
361
362    fn get_entries(&self) -> BoxFuture<'static, Vec<SessionEntry>> {
363        let sm = self.session().session_manager();
364        Box::pin(async move { sm.lock().await.get_entries().into_iter().cloned().collect() })
365    }
366
367    fn get_leaf_id(&self) -> BoxFuture<'static, Option<String>> {
368        let sm = self.session().session_manager();
369        Box::pin(async move { sm.lock().await.get_leaf_id().map(str::to_owned) })
370    }
371
372    fn get_tree(&self) -> BoxFuture<'static, Vec<RpcSessionTreeNode>> {
373        let sm = self.session().session_manager();
374        Box::pin(async move { convert_tree(sm.lock().await.get_tree()) })
375    }
376
377    fn get_fork_messages(&self) -> BoxFuture<'static, Vec<ForkMessage>> {
378        let session = self.session();
379        Box::pin(async move {
380            session
381                .get_user_messages_for_forking()
382                .await
383                .into_iter()
384                .map(|m| ForkMessage {
385                    entry_id: m.entry_id,
386                    text: m.text,
387                })
388                .collect()
389        })
390    }
391
392    fn get_last_assistant_text(&self) -> BoxFuture<'static, Option<String>> {
393        let session = self.session();
394        Box::pin(async move { session.get_last_assistant_text() })
395    }
396
397    fn get_messages(&self) -> BoxFuture<'static, Vec<AgentMessage>> {
398        let session = self.session();
399        Box::pin(async move { session.messages() })
400    }
401
402    fn get_commands(&self) -> BoxFuture<'static, Vec<RpcSlashCommand>> {
403        let session = self.session();
404        Box::pin(async move {
405            session
406                .slash_commands()
407                .into_iter()
408                .map(convert_slash_command)
409                .collect()
410        })
411    }
412
413    fn host_extension_runner(&self) -> Option<Arc<HostExtensionRunner>> {
414        self.session().host_extension_runner()
415    }
416
417    fn subscribe(
418        &self,
419        listener: Arc<dyn Fn(&AgentSessionEvent) + Send + Sync>,
420    ) -> Box<dyn Fn() + Send + Sync> {
421        let session = self.session();
422        let unsub = session.subscribe(move |event: &AgentSessionEvent| {
423            listener(event);
424        });
425        Box::new(unsub)
426    }
427
428    fn register_backpressure_hook(
429        &self,
430        hook: Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>,
431    ) -> Box<dyn Fn() + Send + Sync> {
432        self.session().register_event_backpressure_hook(hook)
433    }
434
435    fn bind_extensions_rpc(
436        &self,
437        bindings: ExtensionBindings,
438    ) -> BoxFuture<'static, Result<(), String>> {
439        let session = self.session();
440        Box::pin(async move {
441            session
442                .bind_extensions(bindings)
443                .await
444                .map_err(|e| e.to_string())
445        })
446    }
447
448    fn dispose(&self) -> BoxFuture<'static, ()> {
449        let this = Arc::clone(self);
450        Box::pin(async move {
451            this.as_ref().dispose().await;
452        })
453    }
454
455    fn set_rebind(&self, callback: Option<RebindCallback>) {
456        let adapted = callback.map(|cb| {
457            Arc::new(move |_session: Arc<AgentSession>| {
458                let cb = Arc::clone(&cb);
459                let fut: Pin<Box<dyn Future<Output = ()> + Send + 'static>> =
460                    Box::pin(async move { cb().await });
461                fut
462            }) as RebindSessionCallback
463        });
464        self.as_ref().set_rebind_session(adapted);
465    }
466}