1use std::collections::HashMap;
14use std::process::Stdio;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::{Arc, Mutex};
17
18use crate::{AttachInfo, ResponseError, RuntimeEvent, SessionEventFrame, SessionLifecycleFrame, SessionRunState};
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
22use tokio::process::{Child, Command};
23use tokio::sync::{mpsc, oneshot};
24use tokio::task::JoinHandle;
25
26#[derive(Clone, Debug)]
38pub enum ClientEvent {
39 Runtime(RuntimeEvent),
40 Gap {
43 session_id: String,
44 from: u64,
45 to: u64,
46 },
47 Other(Value),
48 Closed {
51 reason: String,
52 },
53}
54
55#[derive(Debug, Serialize)]
56struct RequestEnvelope<'a> {
57 id: u64,
58 method: &'a str,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 params: Option<Value>,
61}
62
63#[derive(Debug, Deserialize)]
64struct ResponseEnvelope {
65 id: u64,
66 #[serde(default)]
67 result: Value,
68 #[serde(default)]
69 error: Option<ResponseError>,
70}
71
72pub type RequestHandler = Arc<
76 dyn Fn(String, Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value, ResponseError>> + Send>>
77 + Send
78 + Sync,
79>;
80
81const REFILL_PAGE: usize = 2_000;
83const REFILL_ROUNDS: u32 = 2;
85
86struct SessionCursor {
88 agent_id: String,
89 last: Option<u64>,
91 refilling: bool,
92 parked: Vec<(u64, RuntimeEvent)>,
94}
95
96enum Verdict {
97 Deliver,
98 Duplicate,
99 Gap,
100}
101
102fn verdict(last: Option<u64>, sequence: u64) -> Verdict {
107 match last {
108 None => Verdict::Deliver,
109 Some(last) if sequence == last + 1 => Verdict::Deliver,
110 Some(last) if sequence == 0 && last > 0 => Verdict::Deliver,
111 Some(last) if sequence <= last => Verdict::Duplicate,
112 Some(_) => Verdict::Gap,
113 }
114}
115
116fn journal_position(event: &RuntimeEvent) -> Option<(String, u64, String)> {
118 match event {
119 RuntimeEvent::Outbound { agent_id, session_id: Some(session_id), sequence: Some(sequence), .. }
120 | RuntimeEvent::AcpNotification { agent_id, session_id: Some(session_id), sequence: Some(sequence), .. }
121 | RuntimeEvent::AcpAgentRequest { agent_id, session_id: Some(session_id), sequence: Some(sequence), .. } => {
122 Some((session_id.clone(), *sequence, agent_id.clone()))
123 }
124 _ => None,
125 }
126}
127
128fn journaled_event(agent_id: &str, session_id: &str, frame: &SessionEventFrame) -> Option<RuntimeEvent> {
131 let position = (Some(session_id.to_string()), Some(frame.sequence));
132 if frame.direction == crate::FrameDirection::Out {
133 return Some(RuntimeEvent::Outbound {
134 agent_id: agent_id.to_string(),
135 line: frame.line.clone(),
136 session_id: position.0,
137 sequence: position.1,
138 });
139 }
140 let value: Value = serde_json::from_str(&frame.line).ok()?;
141 let method = value.get("method").and_then(Value::as_str)?.to_string();
142 let params = value.get("params").cloned().unwrap_or(Value::Null);
143 Some(match value.get("id") {
144 Some(id) if !id.is_null() => RuntimeEvent::AcpAgentRequest {
145 agent_id: agent_id.to_string(),
146 id: id.clone(),
147 method,
148 params,
149 session_id: position.0,
150 sequence: position.1,
151 },
152 _ => RuntimeEvent::AcpNotification {
153 agent_id: agent_id.to_string(),
154 method,
155 params,
156 session_id: position.0,
157 sequence: position.1,
158 },
159 })
160}
161
162struct Shared {
163 pending: Mutex<HashMap<u64, oneshot::Sender<Result<Value, ResponseError>>>>,
164 subscribers: Mutex<Vec<mpsc::UnboundedSender<ClientEvent>>>,
167 cursors: Mutex<HashMap<String, SessionCursor>>,
168 next_id: AtomicU64,
169 closed: Mutex<Option<String>>,
170 closed_notify: tokio::sync::Notify,
171 handler: Option<RequestHandler>,
172 outbound: mpsc::UnboundedSender<String>,
175}
176
177impl Shared {
178 fn emit(&self, event: ClientEvent) {
179 self.subscribers.lock().unwrap().retain(|subscriber| subscriber.send(event.clone()).is_ok());
180 }
181
182 async fn request(self: &Arc<Self>, method: &str, params: Value) -> Result<Value, ResponseError> {
185 if let Some(reason) = self.closed.lock().unwrap().clone() {
186 return Err(ResponseError::connection_lost(format!("{method}: {reason}")));
187 }
188 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
189 let (sender, receiver) = oneshot::channel();
190 self.pending.lock().unwrap().insert(id, sender);
191 let mut line =
192 serde_json::to_string(&RequestEnvelope { id, method, params: Some(params) }).expect("serialize request");
193 line.push('\n');
194 if self.outbound.send(line).is_err() {
195 self.pending.lock().unwrap().remove(&id);
196 return Err(ResponseError::connection_lost(format!("{method}: connection closed")));
197 }
198 match receiver.await {
199 Ok(result) => result,
200 Err(_) => Err(ResponseError::connection_lost(format!("{method}: connection closed"))),
201 }
202 }
203
204 fn route(self: &Arc<Self>, event: RuntimeEvent) {
207 let Some((session_id, sequence, agent_id)) = journal_position(&event) else {
208 self.emit(ClientEvent::Runtime(event));
209 return;
210 };
211 let mut cursors = self.cursors.lock().unwrap();
212 let cursor = cursors.entry(session_id.clone()).or_insert_with(|| SessionCursor {
213 agent_id: agent_id.clone(),
214 last: None,
215 refilling: false,
216 parked: Vec::new(),
217 });
218 cursor.agent_id = agent_id;
219 if cursor.refilling {
220 cursor.parked.push((sequence, event));
221 return;
222 }
223 match verdict(cursor.last, sequence) {
224 Verdict::Deliver => {
225 cursor.last = Some(sequence);
226 drop(cursors);
227 self.emit(ClientEvent::Runtime(event));
228 }
229 Verdict::Duplicate => {}
230 Verdict::Gap => {
231 cursor.refilling = true;
232 cursor.parked.push((sequence, event));
233 drop(cursors);
234 tokio::spawn(Arc::clone(self).refill(session_id, 0));
235 }
236 }
237 }
238
239 fn refill_all(self: &Arc<Self>) {
242 let mut cursors = self.cursors.lock().unwrap();
243 for (session_id, cursor) in cursors.iter_mut() {
244 if cursor.last.is_some() && !cursor.refilling {
245 cursor.refilling = true;
246 tokio::spawn(Arc::clone(self).refill(session_id.clone(), 0));
247 }
248 }
249 }
250
251 async fn refill(self: Arc<Self>, session_id: String, round: u32) {
255 let (mut after, agent_id) = {
256 let cursors = self.cursors.lock().unwrap();
257 let cursor = &cursors[&session_id];
258 (cursor.last.unwrap_or(0), cursor.agent_id.clone())
259 };
260 let mut first = true;
261 loop {
262 let params = serde_json::json!({ "sessionId": session_id, "afterSequence": after, "limit": REFILL_PAGE });
263 let frames: Vec<SessionEventFrame> = match self.request("sessionEvents", params).await {
264 Ok(value) => match serde_json::from_value(value) {
265 Ok(frames) => frames,
266 Err(error) => {
267 log::error!("[alwith-api] sessionEvents for {session_id}: unreadable page: {error}");
268 break;
269 }
270 },
271 Err(_) => break,
274 };
275 if frames.is_empty() {
276 break;
277 }
278 if first && frames[0].sequence > after + 1 {
279 self.emit(ClientEvent::Gap {
280 session_id: session_id.clone(),
281 from: after + 1,
282 to: frames[0].sequence - 1,
283 });
284 }
285 first = false;
286 let page = frames.len();
287 for frame in frames {
288 if frame.sequence <= after {
289 continue;
290 }
291 if let Some(event) = journaled_event(&agent_id, &session_id, &frame) {
292 self.emit(ClientEvent::Runtime(event));
293 }
294 after = frame.sequence;
295 self.cursors.lock().unwrap().get_mut(&session_id).expect("cursor").last = Some(after);
296 }
297 if page < REFILL_PAGE {
298 break;
299 }
300 }
301 let another_round = {
307 let mut cursors = self.cursors.lock().unwrap();
308 let cursor = cursors.get_mut(&session_id).expect("cursor");
309 let mut parked = std::mem::take(&mut cursor.parked);
310 parked.sort_by_key(|(sequence, _)| *sequence);
311 let mut still_missing = Vec::new();
312 for (sequence, event) in parked {
313 match verdict(cursor.last, sequence) {
314 Verdict::Duplicate => continue,
315 Verdict::Gap if round < REFILL_ROUNDS => {
316 still_missing.push((sequence, event));
317 continue;
318 }
319 Verdict::Gap => self.emit(ClientEvent::Gap {
320 session_id: session_id.clone(),
321 from: cursor.last.map_or(0, |last| last + 1),
322 to: sequence - 1,
323 }),
324 Verdict::Deliver => {}
325 }
326 cursor.last = Some(sequence);
327 self.emit(ClientEvent::Runtime(event));
328 }
329 if still_missing.is_empty() {
330 cursor.refilling = false;
331 false
332 } else {
333 cursor.parked = still_missing;
334 true
335 }
336 };
337 if another_round {
338 Box::pin(self.refill(session_id, round + 1)).await;
339 }
340 }
341}
342
343pub struct RuntimeClient {
344 writer: Arc<tokio::sync::Mutex<Box<dyn AsyncWrite + Send + Unpin>>>,
345 shared: Arc<Shared>,
346 reader_task: JoinHandle<()>,
347 child: Mutex<Option<Child>>,
348}
349
350impl RuntimeClient {
351 pub async fn spawn(mut command: Command) -> std::io::Result<Self> {
354 command.stdin(Stdio::piped()).stdout(Stdio::piped()).kill_on_drop(true);
355 let mut child = command.spawn()?;
356 let stdin = child.stdin.take().expect("piped stdin");
357 let stdout = child.stdout.take().expect("piped stdout");
358 let mut reader = BufReader::new(stdout);
359 let mut first = String::new();
360 loop {
361 first.clear();
362 if reader.read_line(&mut first).await? == 0 {
363 return Err(std::io::Error::other("alwith-runtime closed stdout before ready"));
364 }
365 let Ok(value) = serde_json::from_str::<Value>(first.trim()) else { continue };
366 if value.get("type").and_then(Value::as_str) == Some("ready") {
367 break;
368 }
369 }
370 let client = Self::over(reader, stdin);
371 *client.child.lock().unwrap() = Some(child);
372 Ok(client)
373 }
374
375 pub fn over<R, W>(reader: R, writer: W) -> Self
378 where
379 R: AsyncRead + Send + Unpin + 'static,
380 W: AsyncWrite + Send + Unpin + 'static,
381 {
382 Self::over_with_handler(reader, writer, None)
383 }
384
385 pub fn over_with_handler<R, W>(reader: R, writer: W, handler: Option<RequestHandler>) -> Self
388 where
389 R: AsyncRead + Send + Unpin + 'static,
390 W: AsyncWrite + Send + Unpin + 'static,
391 {
392 let (outbound, outbound_receiver) = mpsc::unbounded_channel::<String>();
393 let shared = Arc::new(Shared {
394 pending: Mutex::new(HashMap::new()),
395 subscribers: Mutex::new(Vec::new()),
396 cursors: Mutex::new(HashMap::new()),
397 next_id: AtomicU64::new(1),
398 closed: Mutex::new(None),
399 closed_notify: tokio::sync::Notify::new(),
400 handler,
401 outbound,
402 });
403 let writer: Arc<tokio::sync::Mutex<Box<dyn AsyncWrite + Send + Unpin>>> =
404 Arc::new(tokio::sync::Mutex::new(Box::new(writer)));
405 tokio::spawn(write_loop(Arc::clone(&writer), outbound_receiver));
406 let reader_task = tokio::spawn(read_loop(BufReader::new(reader), Arc::clone(&shared)));
407 Self { writer, shared, reader_task, child: Mutex::new(None) }
408 }
409
410 pub fn emit(&self, event: Value) -> Result<(), ResponseError> {
413 let mut line = event.to_string();
414 line.push('\n');
415 self.shared.outbound.send(line).map_err(|_| ResponseError::connection_lost("connection closed"))
416 }
417
418 pub fn events(&self) -> mpsc::UnboundedReceiver<ClientEvent> {
423 let (sender, receiver) = mpsc::unbounded_channel();
424 self.shared.subscribers.lock().unwrap().push(sender);
425 receiver
426 }
427
428 pub async fn request(&self, method: &str, params: Option<Value>) -> Result<Value, ResponseError> {
432 if let Some(reason) = self.shared.closed.lock().unwrap().clone() {
433 return Err(ResponseError::connection_lost(format!("{method}: {reason}")));
434 }
435 let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
436 let (sender, receiver) = oneshot::channel();
437 self.shared.pending.lock().unwrap().insert(id, sender);
438 let mut line = serde_json::to_string(&RequestEnvelope { id, method, params }).expect("serialize request");
439 line.push('\n');
440 {
441 let mut writer = self.writer.lock().await;
442 if let Err(error) = writer.write_all(line.as_bytes()).await.and(writer.flush().await) {
443 self.shared.pending.lock().unwrap().remove(&id);
444 return Err(ResponseError::connection_lost(format!("{method}: write failed: {error}")));
445 }
446 }
447 match receiver.await {
448 Ok(result) => result,
449 Err(_) => Err(ResponseError::connection_lost(format!("{method}: connection closed"))),
450 }
451 }
452
453 async fn typed<T: serde::de::DeserializeOwned>(
454 &self, method: &str, params: Option<Value>,
455 ) -> Result<T, ResponseError> {
456 let value = self.request(method, params).await?;
457 serde_json::from_value(value).map_err(|error| ResponseError::runtime(format!("{method}: bad result: {error}")))
458 }
459
460 fn with_owner(mut params: Value, owner: Option<&str>) -> Value {
461 if let Some(owner) = owner {
462 params["owner"] = Value::String(owner.to_string());
463 }
464 params
465 }
466
467 pub async fn start(&self, agent_id: &str, launch: Value, owner: Option<&str>) -> Result<(), ResponseError> {
474 let params = Self::with_owner(serde_json::json!({ "agentId": agent_id, "launch": launch }), owner);
475 self.request("start", Some(params)).await.map(|_| ())
476 }
477
478 pub async fn send(&self, agent_id: &str, line: &str, owner: Option<&str>) -> Result<(), ResponseError> {
479 let params = Self::with_owner(serde_json::json!({ "agentId": agent_id, "line": line }), owner);
480 self.request("send", Some(params)).await.map(|_| ())
481 }
482
483 pub async fn stop(&self, agent_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
484 let params = Self::with_owner(serde_json::json!({ "agentId": agent_id }), owner);
485 self.request("stop", Some(params)).await.map(|_| ())
486 }
487
488 pub async fn attach_session(
489 &self, session_id: &str, owner: Option<&str>,
490 ) -> Result<Option<AttachInfo>, ResponseError> {
491 let params = Self::with_owner(serde_json::json!({ "sessionId": session_id }), owner);
492 self.typed("attachSession", Some(params)).await
493 }
494
495 pub async fn release_session(&self, session_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
496 let params = Self::with_owner(serde_json::json!({ "sessionId": session_id }), owner);
497 self.request("releaseSession", Some(params)).await.map(|_| ())
498 }
499
500 pub async fn stop_session(&self, session_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
501 let params = Self::with_owner(serde_json::json!({ "sessionId": session_id }), owner);
502 self.request("stopSession", Some(params)).await.map(|_| ())
503 }
504
505 pub async fn detach_owner(&self, owner: &str) -> Result<(), ResponseError> {
506 self.request("detachOwner", Some(serde_json::json!({ "owner": owner }))).await.map(|_| ())
507 }
508
509 pub async fn release_execution(&self, execution_id: &str) -> Result<bool, ResponseError> {
510 self.typed("releaseExecution", Some(serde_json::json!({ "executionId": execution_id }))).await
511 }
512
513 pub async fn agent_for_session(&self, session_id: &str) -> Result<Option<String>, ResponseError> {
514 self.typed("agentForSession", Some(serde_json::json!({ "sessionId": session_id }))).await
515 }
516
517 pub async fn session_replay(&self, session_id: &str) -> Result<Vec<String>, ResponseError> {
518 self.typed("sessionReplay", Some(serde_json::json!({ "sessionId": session_id }))).await
519 }
520
521 pub async fn set_slow_mode(&self, delay_ms: u64) -> Result<(), ResponseError> {
522 self.request("setSlowMode", Some(serde_json::json!({ "delayMs": delay_ms }))).await.map(|_| ())
523 }
524
525 pub async fn mark_read(&self, session_id: &str, version: Option<u64>) -> Result<(), ResponseError> {
528 self.request("markRead", Some(serde_json::json!({ "sessionId": session_id, "version": version })))
529 .await
530 .map(|_| ())
531 }
532
533 pub async fn set_session_title(&self, session_id: &str, title: &str) -> Result<(), ResponseError> {
534 self.request("setSessionTitle", Some(serde_json::json!({ "sessionId": session_id, "title": title })))
535 .await
536 .map(|_| ())
537 }
538
539 pub async fn session_lifecycle(&self, session_id: &str) -> Result<Vec<SessionLifecycleFrame>, ResponseError> {
540 self.typed("sessionLifecycle", Some(serde_json::json!({ "sessionId": session_id }))).await
541 }
542
543 pub async fn session_events(
544 &self, session_id: &str, after_sequence: Option<u64>, limit: Option<usize>,
545 ) -> Result<Vec<SessionEventFrame>, ResponseError> {
546 self.typed(
547 "sessionEvents",
548 Some(serde_json::json!({ "sessionId": session_id, "afterSequence": after_sequence, "limit": limit })),
549 )
550 .await
551 }
552
553 pub async fn run_states(&self) -> Result<Vec<SessionRunState>, ResponseError> {
554 self.typed("runStates", None).await
555 }
556
557 pub async fn version(&self) -> Result<String, ResponseError> {
559 self.typed("version", None).await
560 }
561
562 pub async fn agents(&self) -> Result<Vec<Value>, ResponseError> {
563 self.typed("agents", None).await
564 }
565
566 pub async fn ping(&self) -> Result<(), ResponseError> {
567 self.request("ping", None).await.map(|_| ())
568 }
569
570 pub async fn acp_request(
575 &self, agent_id: &str, method: &str, params: Value, request_id: Option<Value>, owner: Option<&str>,
576 ) -> Result<Value, ResponseError> {
577 let mut params = serde_json::json!({ "agentId": agent_id, "method": method, "params": params });
578 if let Some(id) = request_id {
579 params["id"] = id;
580 }
581 self.request("acpRequest", Some(Self::with_owner(params, owner))).await
582 }
583
584 pub async fn acp_notify(
585 &self, agent_id: &str, method: &str, params: Value, owner: Option<&str>,
586 ) -> Result<(), ResponseError> {
587 let params =
588 Self::with_owner(serde_json::json!({ "agentId": agent_id, "method": method, "params": params }), owner);
589 self.request("acpNotify", Some(params)).await.map(|_| ())
590 }
591
592 pub async fn acp_respond(
594 &self, agent_id: &str, id: Value, result: Option<Value>, error: Option<Value>, owner: Option<&str>,
595 ) -> Result<(), ResponseError> {
596 let params = Self::with_owner(
597 serde_json::json!({ "agentId": agent_id, "id": id, "result": result, "error": error }),
598 owner,
599 );
600 self.request("acpRespond", Some(params)).await.map(|_| ())
601 }
602
603 pub async fn initialize(
607 &self, agent_id: &str, info: Option<Value>, capabilities: Option<Value>, owner: Option<&str>,
608 ) -> Result<Value, ResponseError> {
609 let params = Self::with_owner(
610 serde_json::json!({ "agentId": agent_id, "info": info, "capabilities": capabilities }),
611 owner,
612 );
613 self.request("initialize", Some(params)).await
614 }
615
616 pub async fn session_new(
619 &self, agent_id: &str, cwd: &str, mcp_servers: Vec<Value>, owner: Option<&str>,
620 ) -> Result<Value, ResponseError> {
621 let params =
622 Self::with_owner(serde_json::json!({ "agentId": agent_id, "cwd": cwd, "mcpServers": mcp_servers }), owner);
623 self.request("sessionNew", Some(params)).await
624 }
625
626 pub async fn session_load(
627 &self, agent_id: &str, session_id: &str, cwd: &str, mcp_servers: Vec<Value>, owner: Option<&str>,
628 ) -> Result<Value, ResponseError> {
629 let params = Self::with_owner(
630 serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "cwd": cwd, "mcpServers": mcp_servers }),
631 owner,
632 );
633 self.request("sessionLoad", Some(params)).await
634 }
635
636 pub async fn session_resume(
637 &self, agent_id: &str, session_id: &str, cwd: &str, replay_from: Option<Value>, owner: Option<&str>,
638 ) -> Result<Value, ResponseError> {
639 let params = Self::with_owner(
640 serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "cwd": cwd, "replayFrom": replay_from }),
641 owner,
642 );
643 self.request("sessionResume", Some(params)).await
644 }
645
646 pub async fn session_fork(
647 &self, agent_id: &str, session_id: &str, cwd: &str, mcp_servers: Vec<Value>, owner: Option<&str>,
648 ) -> Result<Value, ResponseError> {
649 let params = Self::with_owner(
650 serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "cwd": cwd, "mcpServers": mcp_servers }),
651 owner,
652 );
653 self.request("sessionFork", Some(params)).await
654 }
655
656 pub async fn prompt(
659 &self, agent_id: &str, session_id: &str, prompt: Value, owner: Option<&str>,
660 ) -> Result<Value, ResponseError> {
661 let params = Self::with_owner(
662 serde_json::json!({ "agentId": agent_id, "sessionId": session_id, "prompt": prompt }),
663 owner,
664 );
665 self.request("prompt", Some(params)).await
666 }
667
668 pub async fn cancel(&self, agent_id: &str, session_id: &str, owner: Option<&str>) -> Result<(), ResponseError> {
669 let params = Self::with_owner(serde_json::json!({ "agentId": agent_id, "sessionId": session_id }), owner);
670 self.request("cancel", Some(params)).await.map(|_| ())
671 }
672
673 pub fn kill(&self) {
676 if let Some(child) = self.child.lock().unwrap().as_mut() {
677 let _ = child.start_kill();
678 }
679 }
680
681 pub fn closed(&self) -> Option<String> {
683 self.shared.closed.lock().unwrap().clone()
684 }
685
686 pub async fn close(&self, grace: std::time::Duration) {
693 let deadline = tokio::time::Instant::now() + grace;
694 let closed = self.shared.closed_notify.notified();
697 tokio::pin!(closed);
698 closed.as_mut().enable();
699 if self.closed().is_none() {
700 let _ = tokio::time::timeout_at(deadline, self.request("shutdown", None)).await;
701 if self.closed().is_none() {
702 let _ = tokio::time::timeout_at(deadline, closed).await;
703 }
704 }
705 if let Some(child) = self.child.lock().unwrap().as_mut() {
706 let _ = child.start_kill();
707 }
708 }
709
710 pub async fn shutdown(mut self) {
713 {
714 let mut writer = self.writer.lock().await;
715 let _ = writer.shutdown().await;
716 }
717 let _ = tokio::time::timeout(std::time::Duration::from_secs(5), &mut self.reader_task).await;
718 if let Some(mut child) = self.child.lock().unwrap().take() {
719 let _ = child.start_kill();
720 }
721 }
722}
723
724impl Drop for RuntimeClient {
725 fn drop(&mut self) {
726 self.reader_task.abort();
727 }
728}
729
730async fn write_loop(
731 writer: Arc<tokio::sync::Mutex<Box<dyn AsyncWrite + Send + Unpin>>>, mut receiver: mpsc::UnboundedReceiver<String>,
732) {
733 while let Some(line) = receiver.recv().await {
734 let mut writer = writer.lock().await;
735 if writer.write_all(line.as_bytes()).await.is_err() || writer.flush().await.is_err() {
736 break;
737 }
738 }
739}
740
741async fn read_loop<R: AsyncRead + Unpin>(mut reader: BufReader<R>, shared: Arc<Shared>) {
742 let mut line = String::new();
743 let mut handlers = tokio::task::JoinSet::<String>::new();
744 let mut active = std::collections::HashMap::<String, tokio::task::AbortHandle>::new();
745 let reason = loop {
746 while let Some(result) = handlers.try_join_next() {
747 if let Ok(id) = result {
748 active.remove(&id);
749 }
750 }
751 line.clear();
752 match reader.read_line(&mut line).await {
753 Ok(0) => break "connection closed".to_string(),
754 Ok(_) => {}
755 Err(error) => break format!("read failed: {error}"),
756 }
757 let text = line.trim();
758 if text.is_empty() {
759 continue;
760 }
761 let value: Value = match serde_json::from_str(text) {
762 Ok(value) => value,
763 Err(error) => {
764 log::warn!("[alwith-api] unparseable frame: {error}");
765 continue;
766 }
767 };
768 if value.get("type").is_some() {
771 match value.get("type").and_then(Value::as_str) {
772 Some("lagged") => shared.refill_all(),
774 _ => match serde_json::from_value::<RuntimeEvent>(value.clone()) {
775 Ok(event) => shared.route(event),
776 Err(_) => shared.emit(ClientEvent::Other(value)),
777 },
778 }
779 continue;
780 }
781 if value.get("method").is_some() {
782 let id = value.get("id").cloned().unwrap_or(Value::Null);
783 let method = value.get("method").and_then(Value::as_str).unwrap_or_default().to_string();
784 let params = value.get("params").cloned().unwrap_or(Value::Null);
785 if method == "_runtime/cancel" {
786 if let Some(handle) = params.get("id").and_then(|id| active.remove(&id.to_string())) {
787 handle.abort();
788 }
789 continue;
790 }
791 let key = id.to_string();
792 if !id.is_null() && active.get(&key).is_some_and(|handle| !handle.is_finished()) {
793 continue; }
795 let shared = Arc::clone(&shared);
796 let task_key = key.clone();
797 let task = handlers.spawn(async move {
798 let response = match &shared.handler {
799 Some(handler) => match handler(method.clone(), params).await {
800 Ok(result) => serde_json::json!({ "id": id, "result": result, "error": null }),
801 Err(error) => serde_json::json!({ "id": id, "result": null, "error": error }),
802 },
803 None => serde_json::json!({ "id": id, "result": null, "error": ResponseError::method_not_found(&method) }),
804 };
805 if !id.is_null() {
806 let mut line = response.to_string();
807 line.push('\n');
808 let _ = shared.outbound.send(line);
809 }
810 task_key
811 });
812 active.insert(key, task);
813 continue;
814 }
815 match serde_json::from_value::<ResponseEnvelope>(value) {
816 Ok(response) => {
817 if let Some(sender) = shared.pending.lock().unwrap().remove(&response.id) {
818 let _ = sender.send(match response.error {
819 Some(error) => Err(error),
820 None => Ok(response.result),
821 });
822 }
823 }
824 Err(error) => log::warn!("[alwith-api] frame is neither event nor response: {error}"),
825 }
826 };
827 handlers.abort_all();
828 while handlers.join_next().await.is_some() {}
829 *shared.closed.lock().unwrap() = Some(reason.clone());
830 let pending: Vec<_> = shared.pending.lock().unwrap().drain().collect();
831 for (_, sender) in pending {
832 let _ = sender.send(Err(ResponseError::connection_lost(reason.clone())));
833 }
834 shared.emit(ClientEvent::Closed { reason });
835 shared.closed_notify.notify_waiters();
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
843 use tokio::io::{AsyncWriteExt, DuplexStream};
844
845 fn fake_peer() -> (RuntimeClient, JoinHandle<Vec<Value>>) {
848 let (client_reader, peer_writer) = tokio::io::duplex(64 * 1024);
849 let (peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
850 let task = tokio::spawn(run_fake_peer(peer_reader, peer_writer));
851 (RuntimeClient::over(client_reader, client_writer), task)
852 }
853
854 async fn run_fake_peer(reader: DuplexStream, mut writer: DuplexStream) -> Vec<Value> {
855 let mut lines = BufReader::new(reader).lines();
856 let mut seen_events = Vec::new();
857 while let Ok(Some(line)) = lines.next_line().await {
858 let value: Value = serde_json::from_str(&line).unwrap();
859 let reply = match value.get("method").and_then(Value::as_str) {
860 Some("ping") => serde_json::json!({ "id": value["id"], "result": "pong", "error": null }),
861 Some("nope") => {
862 serde_json::json!({ "id": value["id"], "result": null, "error": { "code": -32601, "message": "unknown method nope" } })
863 }
864 Some("lines") => {
865 let event = serde_json::json!({ "type": "inbound", "agentId": "a", "lines": ["x", "y"] });
866 writer.write_all(format!("{event}\n").as_bytes()).await.unwrap();
867 serde_json::json!({ "id": value["id"], "result": null, "error": null })
868 }
869 Some("hang") => continue,
870 Some(_) => unreachable!("unexpected request {line}"),
871 None => {
872 seen_events.push(value);
873 continue;
874 }
875 };
876 writer.write_all(format!("{reply}\n").as_bytes()).await.unwrap();
877 }
878 seen_events
879 }
880
881 fn fake_peer_with_journal(journal: Vec<Value>) -> (RuntimeClient, mpsc::UnboundedSender<Value>) {
885 let (client_reader, mut peer_writer) = tokio::io::duplex(64 * 1024);
886 let (peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
887 let (push, mut pushed) = mpsc::unbounded_channel::<Value>();
888 tokio::spawn(async move {
889 let mut lines = BufReader::new(peer_reader).lines();
890 loop {
891 let reply = tokio::select! {
892 Some(event) = pushed.recv() => event,
893 line = lines.next_line() => {
894 let Ok(Some(line)) = line else { break };
895 let value: Value = serde_json::from_str(&line).unwrap();
896 match value.get("method").and_then(Value::as_str) {
897 Some("sessionEvents") => {
898 let after = value["params"]["afterSequence"].as_u64();
899 let frames: Vec<&Value> = journal
900 .iter()
901 .filter(|frame| after.is_none_or(|after| frame["sequence"].as_u64().unwrap() > after))
902 .collect();
903 serde_json::json!({ "id": value["id"], "result": frames, "error": null })
904 }
905 Some("shutdown") => {
906 let receipt = serde_json::json!({ "id": value["id"], "result": null, "error": null });
907 peer_writer.write_all(format!("{receipt}\n").as_bytes()).await.unwrap();
908 break;
909 }
910 _ => serde_json::json!({ "id": value["id"], "result": null, "error": null }),
911 }
912 }
913 };
914 peer_writer.write_all(format!("{reply}\n").as_bytes()).await.unwrap();
915 }
916 });
917 (RuntimeClient::over(client_reader, client_writer), push)
918 }
919
920 fn update(sequence: u64) -> Value {
921 serde_json::json!({
922 "type": "acpNotification", "agentId": "a", "method": "session/update",
923 "params": { "sessionId": "s-1", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": format!("t{sequence}") } } },
924 "sessionId": "s-1", "sequence": sequence
925 })
926 }
927
928 fn journaled(sequence: u64) -> Value {
929 let line = serde_json::json!({
930 "jsonrpc": "2.0", "method": "session/update",
931 "params": { "sessionId": "s-1", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": format!("t{sequence}") } } }
932 });
933 serde_json::json!({ "sequence": sequence, "recordedAt": 1, "direction": "in", "line": line.to_string() })
934 }
935
936 async fn collect(events: &mut mpsc::UnboundedReceiver<ClientEvent>, count: usize) -> Vec<Option<u64>> {
939 let mut seen = Vec::new();
940 while seen.len() < count {
941 let event = tokio::time::timeout(std::time::Duration::from_secs(2), events.recv())
942 .await
943 .expect("event before deadline")
944 .expect("client alive");
945 match event {
946 ClientEvent::Runtime(RuntimeEvent::AcpNotification { sequence, .. }) => seen.push(sequence),
947 ClientEvent::Gap { from, to, .. } => {
948 assert_eq!((from, to), (1, 3));
949 seen.push(None);
950 }
951 _ => {}
952 }
953 }
954 seen
955 }
956
957 #[tokio::test]
958 async fn a_skipped_sequence_is_refilled_from_the_journal_before_later_frames() {
959 let (client, push) = fake_peer_with_journal(vec![journaled(0), journaled(1), journaled(2), journaled(3)]);
960 let mut events = client.events();
961 push.send(update(0)).unwrap();
962 push.send(update(1)).unwrap();
963 push.send(update(3)).unwrap();
964 push.send(update(4)).unwrap();
965 assert_eq!(collect(&mut events, 5).await, vec![Some(0), Some(1), Some(2), Some(3), Some(4)]);
966 }
967
968 #[tokio::test]
969 async fn what_the_journal_lost_is_reported_as_a_gap_not_hidden() {
970 let (client, push) = fake_peer_with_journal(vec![journaled(4), journaled(5)]);
971 let mut events = client.events();
972 push.send(update(0)).unwrap();
973 push.send(update(5)).unwrap();
974 assert_eq!(collect(&mut events, 4).await, vec![Some(0), None, Some(4), Some(5)]);
975 }
976
977 #[tokio::test]
978 async fn lagged_refills_every_followed_session_and_duplicates_are_dropped() {
979 let (client, push) = fake_peer_with_journal(vec![journaled(0), journaled(1), journaled(2)]);
980 let mut events = client.events();
981 push.send(update(0)).unwrap();
982 push.send(serde_json::json!({ "type": "lagged", "dropped": 2 })).unwrap();
983 push.send(update(2)).unwrap();
984 push.send(update(3)).unwrap();
985 assert_eq!(collect(&mut events, 4).await, vec![Some(0), Some(1), Some(2), Some(3)]);
987 }
988
989 #[tokio::test]
990 async fn close_asks_the_runtime_to_shut_down_and_waits_for_the_connection_to_end() {
991 let (client, _push) = fake_peer_with_journal(Vec::new());
992 let mut events = client.events();
993 tokio::time::timeout(std::time::Duration::from_secs(2), client.close(std::time::Duration::from_secs(5)))
994 .await
995 .expect("close returns once the peer hangs up, not after the grace");
996 assert!(client.closed().is_some());
997 assert!(matches!(events.recv().await.unwrap(), ClientEvent::Closed { .. }));
998 }
999
1000 #[tokio::test]
1001 async fn requests_round_trip_and_errors_arrive_as_sent() {
1002 let (client, _peer) = fake_peer();
1003 assert_eq!(client.request("ping", None).await.unwrap(), Value::String("pong".into()));
1004 let error = client.request("nope", None).await.unwrap_err();
1005 assert_eq!(error, ResponseError::new(ResponseError::METHOD_NOT_FOUND, "unknown method nope"));
1006 }
1007
1008 #[tokio::test]
1009 async fn protocol_events_are_typed_and_others_pass_through() {
1010 let (client, _peer) = fake_peer();
1011 let mut events = client.events();
1012 client.request("lines", None).await.unwrap();
1013 match events.recv().await.unwrap() {
1014 ClientEvent::Runtime(RuntimeEvent::Inbound { agent_id, lines }) => {
1015 assert_eq!(agent_id, "a");
1016 assert_eq!(lines, vec!["x".to_string(), "y".to_string()]);
1017 }
1018 other => panic!("unexpected {other:?}"),
1019 }
1020 }
1021
1022 #[tokio::test]
1023 async fn peer_requests_reach_the_handler_and_emitted_events_reach_the_peer() {
1024 let (client_reader, mut peer_writer) = tokio::io::duplex(64 * 1024);
1025 let (peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
1026 let handler: RequestHandler = Arc::new(|method, params| {
1027 Box::pin(async move {
1028 if method == "echo" { Ok(params) } else { Err(ResponseError::runtime(format!("unknown {method}"))) }
1029 })
1030 });
1031 let client = RuntimeClient::over_with_handler(client_reader, client_writer, Some(handler));
1032 let mut peer_lines = BufReader::new(peer_reader).lines();
1033
1034 peer_writer.write_all(b"{\"id\":5,\"method\":\"echo\",\"params\":{\"x\":1}}\n").await.unwrap();
1035 let reply: Value = serde_json::from_str(&peer_lines.next_line().await.unwrap().unwrap()).unwrap();
1036 assert_eq!(reply, serde_json::json!({ "id": 5, "result": { "x": 1 }, "error": null }));
1037
1038 peer_writer.write_all(b"{\"id\":6,\"method\":\"nope\"}\n").await.unwrap();
1039 let reply: Value = serde_json::from_str(&peer_lines.next_line().await.unwrap().unwrap()).unwrap();
1040 assert_eq!(
1041 reply["error"],
1042 serde_json::json!({ "code": ResponseError::RUNTIME, "message": "unknown nope", "data": null })
1043 );
1044
1045 client.emit(serde_json::json!({ "type": "statusChanged", "statuses": [] })).unwrap();
1046 let event: Value = serde_json::from_str(&peer_lines.next_line().await.unwrap().unwrap()).unwrap();
1047 assert_eq!(event["type"], "statusChanged");
1048 }
1049
1050 #[tokio::test]
1051 async fn closing_the_stream_fails_pending_requests_and_broadcasts_closed() {
1052 let (client_reader, peer_writer) = tokio::io::duplex(64 * 1024);
1053 let (_peer_reader, client_writer) = tokio::io::duplex(64 * 1024);
1054 let client = RuntimeClient::over(client_reader, client_writer);
1055 let mut events = client.events();
1056 let pending = tokio::spawn({
1057 let client = Arc::new(client);
1058 let client_for_task = Arc::clone(&client);
1059 async move { (client, client_for_task.request("hang", None).await) }
1060 });
1061 drop(peer_writer);
1062 let (client, result) = pending.await.unwrap();
1063 assert_eq!(result.unwrap_err(), ResponseError::connection_lost("hang: connection closed"));
1064 assert_eq!(client.closed().as_deref(), Some("connection closed"));
1065 assert!(matches!(events.recv().await.unwrap(), ClientEvent::Closed { .. }));
1066 assert_eq!(client.request("ping", None).await.unwrap_err().code, ResponseError::CONNECTION_LOST);
1067 }
1068 #[tokio::test]
1069 async fn peer_handler_can_request_on_same_connection_and_eof_cancels_it() {
1070 let (client_stream, peer_stream) = tokio::io::duplex(8192);
1071 let (reader, writer) = tokio::io::split(client_stream);
1072 let (peer_reader, mut peer_writer) = tokio::io::split(peer_stream);
1073 let slot = Arc::new(std::sync::OnceLock::<std::sync::Weak<RuntimeClient>>::new());
1074 let handler_slot = Arc::clone(&slot);
1075 let entered = Arc::new(tokio::sync::Notify::new());
1076 let dropped = Arc::new(tokio::sync::Notify::new());
1077 struct Signal(Arc<tokio::sync::Notify>);
1078 impl Drop for Signal {
1079 fn drop(&mut self) {
1080 self.0.notify_one();
1081 }
1082 }
1083 let handler_entered = Arc::clone(&entered);
1084 let handler_dropped = Arc::clone(&dropped);
1085 let handler: RequestHandler = Arc::new(move |method, _| {
1086 let client = handler_slot.get().unwrap().upgrade().unwrap();
1087 let entered = Arc::clone(&handler_entered);
1088 let dropped = Arc::clone(&handler_dropped);
1089 Box::pin(async move {
1090 if method == "nested" {
1091 return client.request("ping", None).await;
1092 }
1093 let _signal = Signal(dropped);
1094 entered.notify_one();
1095 std::future::pending().await
1096 })
1097 });
1098 let client = Arc::new(RuntimeClient::over_with_handler(reader, writer, Some(handler)));
1099 assert!(slot.set(Arc::downgrade(&client)).is_ok());
1100 let mut lines = BufReader::new(peer_reader).lines();
1101 peer_writer.write_all(b"{\"id\":101,\"method\":\"nested\"}\n").await.unwrap();
1102 let nested: Value = serde_json::from_str(
1103 &tokio::time::timeout(std::time::Duration::from_secs(2), lines.next_line())
1104 .await
1105 .unwrap()
1106 .unwrap()
1107 .unwrap(),
1108 )
1109 .unwrap();
1110 assert_eq!(nested["method"], "ping");
1111 peer_writer
1112 .write_all(format!("{}\n", serde_json::json!({"id":nested["id"],"result":"pong"})).as_bytes())
1113 .await
1114 .unwrap();
1115 let response: Value = serde_json::from_str(
1116 &tokio::time::timeout(std::time::Duration::from_secs(2), lines.next_line())
1117 .await
1118 .unwrap()
1119 .unwrap()
1120 .unwrap(),
1121 )
1122 .unwrap();
1123 assert_eq!(response["id"], 101);
1124 assert_eq!(response["result"], "pong");
1125 peer_writer.write_all(b"{\"id\":102,\"method\":\"wait\"}\n").await.unwrap();
1126 tokio::time::timeout(std::time::Duration::from_secs(2), entered.notified()).await.unwrap();
1127 peer_writer.write_all(b"{\"method\":\"_runtime/cancel\",\"params\":{\"id\":102}}\n").await.unwrap();
1128 tokio::time::timeout(std::time::Duration::from_secs(2), dropped.notified()).await.unwrap();
1129 peer_writer.write_all(b"{\"id\":103,\"method\":\"wait\"}\n").await.unwrap();
1130 tokio::time::timeout(std::time::Duration::from_secs(2), entered.notified()).await.unwrap();
1131 peer_writer.shutdown().await.unwrap();
1132 tokio::time::timeout(std::time::Duration::from_secs(2), dropped.notified()).await.unwrap();
1133 }
1134}