1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::process::Stdio;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
8
9use futures::StreamExt;
10use futures::stream::BoxStream;
11use oino_ai::{AgentMessage, ImageContent, JsonValue, Model, ThinkingLevel};
12use oino_client::{
13 AgentCommand, ClientError, RpcCommand, RpcResponse, RpcResponseData, RpcSessionState,
14};
15use oino_core::compaction::CompactionResult;
16use oino_core::session::{
17 BashResult, CycleModelResult, ForkMessage, PromptDisposition, SessionStats, SlashCommandInfo,
18 StreamingBehavior,
19};
20use oino_core::ui::UiResponse;
21use serde::Deserialize;
22use serde::de::DeserializeOwned;
23use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
24use tokio::process::Child;
25use tokio::sync::{broadcast, oneshot};
26
27use crate::jsonl::{LineReader, serialize_line};
28
29const EVENT_BUFFER: usize = 4096;
31
32fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
33 mutex.lock().unwrap_or_else(PoisonError::into_inner)
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
38pub struct ChildCommand {
39 pub program: PathBuf,
41 pub args: Vec<String>,
43 pub cwd: Option<PathBuf>,
45 pub env: Vec<(String, String)>,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51#[non_exhaustive]
52pub enum Transport {
53 StdioChild(ChildCommand),
55}
56
57type Writer = Box<dyn AsyncWrite + Send + Unpin>;
58
59struct Inner {
60 writer: tokio::sync::Mutex<Option<Writer>>,
62 pending: Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>,
63 next_id: AtomicU64,
64 events: Mutex<broadcast::Receiver<JsonValue>>,
65 child: Mutex<Option<Child>>,
66}
67
68#[derive(Clone)]
70pub struct RpcClient {
71 inner: Arc<Inner>,
72}
73
74impl std::fmt::Debug for RpcClient {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("RpcClient").finish_non_exhaustive()
77 }
78}
79
80#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
82pub struct ForkOutcome {
83 #[serde(default)]
85 pub text: Option<String>,
86 pub cancelled: bool,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ClearedQueues {
94 pub steering: Vec<String>,
96 pub follow_up: Vec<String>,
98}
99
100#[derive(Clone, Debug, PartialEq, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct EntriesOutcome {
104 #[serde(alias = "tree")]
106 pub entries: Vec<JsonValue>,
107 pub leaf_id: Option<String>,
109}
110
111#[derive(Deserialize)]
112struct Disposition {
113 disposition: PromptDisposition,
114}
115
116#[derive(Deserialize)]
117struct Cancelled {
118 cancelled: bool,
119}
120
121impl RpcClient {
122 pub fn connect(transport: Transport) -> Result<Self, ClientError> {
128 match transport {
129 Transport::StdioChild(command) => {
130 let mut builder = tokio::process::Command::new(&command.program);
131 builder
132 .args(&command.args)
133 .envs(command.env.iter().map(|(k, v)| (k, v)))
134 .stdin(Stdio::piped())
135 .stdout(Stdio::piped())
136 .stderr(Stdio::inherit())
137 .kill_on_drop(true);
138 if let Some(cwd) = &command.cwd {
139 builder.current_dir(cwd);
140 }
141 let mut child = builder
142 .spawn()
143 .map_err(|e| ClientError::Transport(e.to_string()))?;
144 let (Some(stdin), Some(stdout)) = (child.stdin.take(), child.stdout.take()) else {
145 return Err(ClientError::Transport("child stdio unavailable".to_owned()));
146 };
147 let client = Self::from_io(stdout, stdin);
148 *lock(&client.inner.child) = Some(child);
149 Ok(client)
150 }
151 }
152 }
153
154 pub fn from_io<R, W>(reader: R, writer: W) -> Self
156 where
157 R: AsyncRead + Unpin + Send + 'static,
158 W: AsyncWrite + Unpin + Send + 'static,
159 {
160 let (sender, receiver) = broadcast::channel(EVENT_BUFFER);
161 let inner = Arc::new(Inner {
162 writer: tokio::sync::Mutex::new(Some(Box::new(writer))),
163 pending: Mutex::new(HashMap::new()),
164 next_id: AtomicU64::new(1),
165 events: Mutex::new(receiver),
166 child: Mutex::new(None),
167 });
168 let weak = Arc::downgrade(&inner);
169 tokio::spawn(async move {
170 let mut lines = LineReader::new(reader);
171 while let Ok(Some(line)) = lines.next_line().await {
172 let Ok(value) = serde_json::from_str::<JsonValue>(&line) else {
173 continue;
174 };
175 if let Some(response) = RpcResponse::from_value(&value) {
176 let waiter = response
177 .id
178 .as_ref()
179 .and_then(JsonValue::as_str)
180 .and_then(|id| weak.upgrade().and_then(|i| lock(&i.pending).remove(id)));
181 if let Some(waiter) = waiter {
182 let _ = waiter.send(response);
183 continue;
184 }
185 }
186 let _ = sender.send(value);
187 }
188 if let Some(inner) = weak.upgrade() {
189 lock(&inner.pending).clear();
190 }
191 });
192 Self { inner }
193 }
194
195 pub fn events(&self) -> BoxStream<'static, JsonValue> {
197 let receiver = lock(&self.inner.events).resubscribe();
198 futures::stream::unfold(receiver, |mut receiver| async move {
199 loop {
200 match receiver.recv().await {
201 Ok(event) => return Some((event, receiver)),
202 Err(broadcast::error::RecvError::Lagged(skipped)) => {
203 tracing::warn!(skipped, "rpc client lagged");
204 }
205 Err(broadcast::error::RecvError::Closed) => return None,
206 }
207 }
208 })
209 .boxed()
210 }
211
212 async fn write(&self, value: &JsonValue) -> Result<(), ClientError> {
213 let line = serialize_line(value);
214 let mut guard = self.inner.writer.lock().await;
215 let writer = guard.as_mut().ok_or(ClientError::Closed)?;
216 writer
217 .write_all(line.as_bytes())
218 .await
219 .map_err(|e| ClientError::Transport(e.to_string()))?;
220 writer
221 .flush()
222 .await
223 .map_err(|e| ClientError::Transport(e.to_string()))
224 }
225
226 pub async fn request(&self, command: RpcCommand) -> Result<RpcResponseData, ClientError> {
233 let id = format!("req_{}", self.inner.next_id.fetch_add(1, Ordering::Relaxed));
234 let (tx, rx) = oneshot::channel();
235 lock(&self.inner.pending).insert(id.clone(), tx);
236 let request = oino_client::RpcRequest {
237 id: Some(JsonValue::String(id.clone())),
238 command,
239 };
240 if let Err(error) = self.write(&request.to_value()).await {
241 lock(&self.inner.pending).remove(&id);
242 return Err(error);
243 }
244 let response = rx.await.map_err(|_| ClientError::Closed)?;
245 response.result.map_err(ClientError::Command)
246 }
247
248 async fn typed<T: DeserializeOwned>(&self, command: AgentCommand) -> Result<T, ClientError> {
249 let data = self.request(command.into()).await?;
250 serde_json::from_value(data.unwrap_or(JsonValue::Null))
251 .map_err(|e| ClientError::Protocol(e.to_string()))
252 }
253
254 async fn unit(&self, command: AgentCommand) -> Result<(), ClientError> {
255 self.request(command.into()).await.map(|_| ())
256 }
257
258 pub async fn ui_respond(&self, response: &UiResponse) -> Result<(), ClientError> {
264 let value =
265 serde_json::to_value(response).map_err(|e| ClientError::Protocol(e.to_string()))?;
266 self.write(&value).await
267 }
268
269 pub async fn close(&self) -> Result<Option<i32>, ClientError> {
275 if let Some(mut writer) = self.inner.writer.lock().await.take() {
276 let _ = writer.shutdown().await;
277 }
278 let child = lock(&self.inner.child).take();
279 match child {
280 Some(mut child) => child
281 .wait()
282 .await
283 .map(|status| status.code())
284 .map_err(|e| ClientError::Transport(e.to_string())),
285 None => Ok(None),
286 }
287 }
288
289 pub async fn prompt(
295 &self,
296 message: impl Into<String>,
297 images: Vec<ImageContent>,
298 streaming_behavior: Option<StreamingBehavior>,
299 ) -> Result<PromptDisposition, ClientError> {
300 let reply: Disposition = self
301 .typed(AgentCommand::Prompt {
302 message: message.into(),
303 images,
304 streaming_behavior,
305 })
306 .await?;
307 Ok(reply.disposition)
308 }
309
310 pub async fn steer(
316 &self,
317 message: impl Into<String>,
318 images: Vec<ImageContent>,
319 ) -> Result<PromptDisposition, ClientError> {
320 let reply: Disposition = self
321 .typed(AgentCommand::Steer {
322 message: message.into(),
323 images,
324 })
325 .await?;
326 Ok(reply.disposition)
327 }
328
329 pub async fn follow_up(
335 &self,
336 message: impl Into<String>,
337 images: Vec<ImageContent>,
338 ) -> Result<PromptDisposition, ClientError> {
339 let reply: Disposition = self
340 .typed(AgentCommand::FollowUp {
341 message: message.into(),
342 images,
343 })
344 .await?;
345 Ok(reply.disposition)
346 }
347
348 pub async fn abort(&self) -> Result<(), ClientError> {
354 self.unit(AgentCommand::Abort).await
355 }
356
357 pub async fn clear_queue(&self) -> Result<ClearedQueues, ClientError> {
363 self.typed(AgentCommand::ClearQueue).await
364 }
365
366 pub async fn new_session(&self, parent_session: Option<String>) -> Result<bool, ClientError> {
372 let reply: Cancelled = self
373 .typed(AgentCommand::NewSession { parent_session })
374 .await?;
375 Ok(reply.cancelled)
376 }
377
378 pub async fn get_state(&self) -> Result<RpcSessionState, ClientError> {
384 self.typed(AgentCommand::GetState).await
385 }
386
387 pub async fn set_model(
393 &self,
394 provider: impl Into<String>,
395 model_id: impl Into<String>,
396 ) -> Result<Model, ClientError> {
397 self.typed(AgentCommand::SetModel {
398 provider: provider.into(),
399 model_id: model_id.into(),
400 })
401 .await
402 }
403
404 pub async fn cycle_model(&self) -> Result<Option<CycleModelResult>, ClientError> {
410 self.typed(AgentCommand::CycleModel).await
411 }
412
413 pub async fn get_available_models(&self) -> Result<Vec<Model>, ClientError> {
419 #[derive(Deserialize)]
420 struct Models {
421 models: Vec<Model>,
422 }
423 let reply: Models = self.typed(AgentCommand::GetAvailableModels).await?;
424 Ok(reply.models)
425 }
426
427 pub async fn set_thinking_level(&self, level: ThinkingLevel) -> Result<(), ClientError> {
433 self.unit(AgentCommand::SetThinkingLevel { level }).await
434 }
435
436 pub async fn cycle_thinking_level(&self) -> Result<Option<ThinkingLevel>, ClientError> {
442 #[derive(Deserialize)]
443 struct Level {
444 level: ThinkingLevel,
445 }
446 let reply: Option<Level> = self.typed(AgentCommand::CycleThinkingLevel).await?;
447 Ok(reply.map(|l| l.level))
448 }
449
450 pub async fn get_available_thinking_levels(&self) -> Result<Vec<ThinkingLevel>, ClientError> {
456 #[derive(Deserialize)]
457 struct Levels {
458 levels: Vec<ThinkingLevel>,
459 }
460 let reply: Levels = self.typed(AgentCommand::GetAvailableThinkingLevels).await?;
461 Ok(reply.levels)
462 }
463
464 pub async fn set_steering_mode(&self, mode: oino_agent::QueueMode) -> Result<(), ClientError> {
470 self.unit(AgentCommand::SetSteeringMode { mode }).await
471 }
472
473 pub async fn set_follow_up_mode(&self, mode: oino_agent::QueueMode) -> Result<(), ClientError> {
479 self.unit(AgentCommand::SetFollowUpMode { mode }).await
480 }
481
482 pub async fn compact(
488 &self,
489 custom_instructions: Option<String>,
490 ) -> Result<CompactionResult, ClientError> {
491 self.typed(AgentCommand::Compact {
492 custom_instructions,
493 })
494 .await
495 }
496
497 pub async fn set_auto_compaction(&self, enabled: bool) -> Result<(), ClientError> {
503 self.unit(AgentCommand::SetAutoCompaction { enabled }).await
504 }
505
506 pub async fn set_auto_retry(&self, enabled: bool) -> Result<(), ClientError> {
512 self.unit(AgentCommand::SetAutoRetry { enabled }).await
513 }
514
515 pub async fn abort_retry(&self) -> Result<(), ClientError> {
521 self.unit(AgentCommand::AbortRetry).await
522 }
523
524 pub async fn bash(
530 &self,
531 command: impl Into<String>,
532 exclude_from_context: bool,
533 ) -> Result<BashResult, ClientError> {
534 self.typed(AgentCommand::Bash {
535 command: command.into(),
536 exclude_from_context: exclude_from_context.then_some(true),
537 })
538 .await
539 }
540
541 pub async fn abort_bash(&self) -> Result<(), ClientError> {
547 self.unit(AgentCommand::AbortBash).await
548 }
549
550 pub async fn get_session_stats(&self) -> Result<SessionStats, ClientError> {
556 self.typed(AgentCommand::GetSessionStats).await
557 }
558
559 pub async fn export_html(&self, output_path: Option<String>) -> Result<String, ClientError> {
565 #[derive(Deserialize)]
566 struct Path {
567 path: String,
568 }
569 let reply: Path = self.typed(AgentCommand::ExportHtml { output_path }).await?;
570 Ok(reply.path)
571 }
572
573 pub async fn switch_session(
579 &self,
580 session_path: impl Into<String>,
581 ) -> Result<bool, ClientError> {
582 let reply: Cancelled = self
583 .typed(AgentCommand::SwitchSession {
584 session_path: session_path.into(),
585 })
586 .await?;
587 Ok(reply.cancelled)
588 }
589
590 pub async fn fork(&self, entry_id: impl Into<String>) -> Result<ForkOutcome, ClientError> {
596 self.typed(AgentCommand::Fork {
597 entry_id: entry_id.into(),
598 })
599 .await
600 }
601
602 pub async fn clone_session(&self) -> Result<bool, ClientError> {
608 let reply: Cancelled = self.typed(AgentCommand::Clone).await?;
609 Ok(reply.cancelled)
610 }
611
612 pub async fn get_fork_messages(&self) -> Result<Vec<ForkMessage>, ClientError> {
618 #[derive(Deserialize)]
619 struct Messages {
620 messages: Vec<ForkMessage>,
621 }
622 let reply: Messages = self.typed(AgentCommand::GetForkMessages).await?;
623 Ok(reply.messages)
624 }
625
626 pub async fn get_entries(&self, since: Option<String>) -> Result<EntriesOutcome, ClientError> {
632 self.typed(AgentCommand::GetEntries { since }).await
633 }
634
635 pub async fn get_tree(&self) -> Result<EntriesOutcome, ClientError> {
641 self.typed(AgentCommand::GetTree).await
642 }
643
644 pub async fn get_last_assistant_text(&self) -> Result<Option<String>, ClientError> {
650 #[derive(Deserialize)]
651 struct Text {
652 text: Option<String>,
653 }
654 let reply: Text = self.typed(AgentCommand::GetLastAssistantText).await?;
655 Ok(reply.text)
656 }
657
658 pub async fn set_session_name(&self, name: impl Into<String>) -> Result<(), ClientError> {
664 self.unit(AgentCommand::SetSessionName { name: name.into() })
665 .await
666 }
667
668 pub async fn get_messages(&self) -> Result<Vec<AgentMessage>, ClientError> {
674 #[derive(Deserialize)]
675 struct Messages {
676 messages: Vec<AgentMessage>,
677 }
678 let reply: Messages = self.typed(AgentCommand::GetMessages).await?;
679 Ok(reply.messages)
680 }
681
682 pub async fn get_commands(&self) -> Result<Vec<SlashCommandInfo>, ClientError> {
688 #[derive(Deserialize)]
689 struct Commands {
690 commands: Vec<SlashCommandInfo>,
691 }
692 let reply: Commands = self.typed(AgentCommand::GetCommands).await?;
693 Ok(reply.commands)
694 }
695}