1use 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
37fn 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
101impl 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}