1use std::collections::{HashMap, HashSet};
4use std::io::{Read, Write};
5use std::os::unix::net::{UnixListener, UnixStream};
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8
9use serde::{Deserialize, Serialize};
10
11use crate::bus::{BusMsg, EndReason};
12use crate::store::CommittedEvent;
13use crate::wal::{EventBody, GenParams};
14use crate::{Daemon, DaemonError};
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct EventMsg {
18 pub event_id: u64,
19 pub epoch: u64,
20 pub ts_unix_ms: u64,
21 pub body: EventBody,
22}
23
24impl From<CommittedEvent> for EventMsg {
25 fn from(e: CommittedEvent) -> EventMsg {
26 EventMsg {
27 event_id: e.event_id,
28 epoch: e.epoch,
29 ts_unix_ms: e.ts_unix_ms,
30 body: e.body,
31 }
32 }
33}
34
35#[derive(Debug, Serialize, Deserialize)]
38pub enum Request {
39 Create {
40 parent: Option<u64>,
41 params: GenParams,
42 },
43 Append {
44 session: u64,
45 text: Option<String>,
46 span: Vec<u32>,
47 },
48 AppendMessage {
49 session: u64,
50 role: u32,
51 text: String,
52 },
53 Generate {
54 session: u64,
55 max_tokens: u32,
56 },
57 Read {
58 session: u64,
59 cursor: u64,
60 },
61 AppendToolResult {
62 session: u64,
63 call_id: u64,
64 content: String,
65 },
66 OpenToolCalls {
67 session: u64,
68 },
69 List,
70 Cancel {
71 session: u64,
72 },
73 Subscribe {
74 session: u64,
75 cursor: u64,
76 provisional: bool,
77 },
78 Unsubscribe {
79 sub: u64,
80 },
81 Fork {
82 session: u64,
83 at_event: u64,
84 params: Option<GenParams>,
85 },
86 Tree,
87 Sessions {
88 include_archived: bool,
89 },
90 SetMeta {
91 session: u64,
92 expected_version: u64,
93 title: Option<String>,
94 archived: Option<bool>,
95 },
96 Rebase {
97 session: u64,
98 edits: Vec<crate::wal::RebaseEdit>,
99 params: Option<GenParams>,
100 },
101 Purge {
102 session: u64,
103 expected_generation: u64,
104 mode: crate::wal::PurgeMode,
105 },
106 SetQos {
107 session: u64,
108 class: u8,
109 batch_invariant: bool,
110 },
111 SetBackgroundRate {
112 divisor: u32,
113 },
114 Inspect {
115 session: u64,
116 },
117 SetLogLevel {
118 directive: String,
119 },
120 Metrics,
121 AppendBlock {
122 session: u64,
123 role: u32,
124 kind: u32,
125 payload: String,
126 },
127 RequestPermission {
128 session: u64,
129 call_id: u64,
130 text: String,
131 },
132 RespondPermission {
133 session: u64,
134 request_id: u64,
135 granted: bool,
136 },
137 RequestToolCancel {
138 session: u64,
139 call_id: u64,
140 },
141 AppendToolOutcome {
142 session: u64,
143 call_id: u64,
144 outcome: u8,
145 note: String,
146 },
147 Ledger {
148 session: u64,
149 },
150 PutMedia {
151 bytes: Vec<u8>,
152 mime: String,
153 },
154 AppendImage {
155 session: u64,
156 role: u32,
157 blob: String,
158 pre_text: String,
159 post_text: String,
160 },
161 GcMedia,
162 Complete {
163 prefix: String,
164 suffix: String,
165 mode: u8,
166 max_tokens: u32,
167 },
168 Trim {
169 session: u64,
170 to_event: u64,
171 },
172 Pin {
173 session: u64,
174 ttl_ms: u64,
175 },
176 AppendSystem {
177 session: u64,
178 text: Option<String>,
179 tools: Vec<String>,
180 },
181}
182
183#[derive(Debug, Serialize, Deserialize)]
184pub enum Response {
185 Created {
186 session: u64,
187 },
188 Delta {
189 event: EventMsg,
190 },
191 Cancelling,
192 Committed {
193 event: EventMsg,
194 },
195 Generated {
196 events: Vec<EventMsg>,
197 tokens_generated: u32,
198 finish: u32,
199 warm_prefix: u64,
200 },
201 Events {
202 events: Vec<EventMsg>,
203 },
204 Sessions {
205 ids: Vec<u64>,
206 },
207 ToolCalls {
208 calls: Vec<(u64, String, String)>,
209 },
210 Subscribed {
211 sub: u64,
212 },
213 SubEvent {
214 sub: u64,
215 event: EventMsg,
216 },
217 SubProvisional {
218 sub: u64,
219 channel: u32,
220 text: String,
221 },
222 SubEnded {
223 sub: u64,
224 reason: String,
225 },
226 Unsubscribed,
227 Err {
228 message: String,
229 },
230 Tree {
231 nodes: Vec<(u64, Option<u64>, u64)>,
232 },
233 SessionList {
234 sessions: Vec<crate::SessionSummary>,
235 },
236 Meta {
237 version: u64,
238 },
239 Purged {
240 sessions: Vec<u64>,
241 },
242 Ack,
243 Inspection {
244 inspection: crate::Inspection,
245 },
246 Text {
247 text: String,
248 },
249 Ledger {
250 entries: Vec<(u64, crate::store::LedgerEntry)>,
251 },
252 Media {
253 hash: String,
254 },
255 Gc {
256 kept: u64,
257 removed: u64,
258 },
259 Completion {
260 text: String,
261 tokens: Vec<u32>,
262 cached: bool,
263 expired: bool,
264 },
265 Pinned {
266 deadline_unix_ms: u64,
267 },
268 Throttled {
269 retry_after_ms: u64,
270 },
271}
272
273fn write_frame<T: Serialize>(w: &mut impl Write, msg: &T) -> Result<(), DaemonError> {
274 let payload = postcard::to_stdvec(msg)?;
275 w.write_all(&(payload.len() as u32).to_le_bytes())?;
276 w.write_all(&payload)?;
277 Ok(())
278}
279
280fn read_frame<T: for<'de> Deserialize<'de>>(r: &mut impl Read) -> Result<Option<T>, DaemonError> {
281 let mut len_buf = [0u8; 4];
282 match r.read_exact(&mut len_buf) {
283 Ok(()) => {}
284 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
285 Err(e) => return Err(e.into()),
286 }
287 let len = u32::from_le_bytes(len_buf) as usize;
288 const MAX_FRAME: usize = 64 << 20;
289 if len > MAX_FRAME {
290 return Err(DaemonError::Protocol("api frame exceeds limit"));
291 }
292 let mut payload = vec![0u8; len];
293 r.read_exact(&mut payload)?;
294 Ok(Some(postcard::from_bytes(&payload)?))
295}
296
297pub fn handle_web(d: &Daemon, req: Request) -> Response {
298 match req {
299 Request::Cancel { session } => cancel(&d.cancel_registry(), &d.active_registry(), session),
300 Request::Generate { .. }
301 | Request::Subscribe { .. }
302 | Request::Unsubscribe { .. } => Response::Err {
303 message: "streaming verbs use the WebSocket surface (/web/ws) or SSE (/web/stream), \
304 not /web/rpc"
305 .into(),
306 },
307 other => handle(d, other),
308 }
309}
310
311fn cancel(cancels: &crate::CancelSet, active: &Mutex<HashSet<u64>>, session: u64) -> Response {
312 let mut c = cancels.lock().expect("cancel registry");
313 if active.lock().expect("active set").contains(&session) {
314 c.insert(session);
315 cancels.wake();
317 Response::Cancelling
318 } else {
319 Response::Err {
320 message: format!("session {session} has no generation in flight"),
321 }
322 }
323}
324
325fn handle(d: &Daemon, req: Request) -> Response {
326 let result: Result<Response, DaemonError> = (|| match req {
327 Request::Create { parent, params } => {
328 let session = d.create(parent, params)?;
329 Ok(Response::Created { session })
330 }
331 Request::Append {
332 session,
333 text,
334 span,
335 } => {
336 let event = d.append(session, text, span)?;
337 Ok(Response::Committed {
338 event: event.into(),
339 })
340 }
341 Request::AppendMessage {
342 session,
343 role,
344 text,
345 } => {
346 let event = d.append_message(session, role, text)?;
347 Ok(Response::Committed {
348 event: event.into(),
349 })
350 }
351 Request::Read { session, cursor } => {
352 let events = d.read(session, cursor)?;
353 Ok(Response::Events {
354 events: events.into_iter().map(Into::into).collect(),
355 })
356 }
357 Request::AppendToolResult {
358 session,
359 call_id,
360 content,
361 } => {
362 let event = d.append_tool_result(session, call_id, content)?;
363 Ok(Response::Committed {
364 event: event.into(),
365 })
366 }
367 Request::OpenToolCalls { session } => Ok(Response::ToolCalls {
368 calls: d.open_tool_calls(session)?,
369 }),
370 Request::List => Ok(Response::Sessions {
371 ids: d.session_ids(),
372 }),
373 Request::Fork {
374 session,
375 at_event,
376 params,
377 } => {
378 let child = d.fork(session, at_event, params)?;
379 Ok(Response::Created { session: child })
380 }
381 Request::Tree => Ok(Response::Tree { nodes: d.tree() }),
382 Request::Sessions { include_archived } => Ok(Response::SessionList {
383 sessions: d.sessions(include_archived),
384 }),
385 Request::SetMeta {
386 session,
387 expected_version,
388 title,
389 archived,
390 } => Ok(Response::Meta {
391 version: d.set_meta(session, expected_version, title, archived)?,
392 }),
393 Request::Rebase {
394 session,
395 edits,
396 params,
397 } => {
398 let child = d.rebase(session, edits, params)?;
399 Ok(Response::Created { session: child })
400 }
401 Request::Purge {
402 session,
403 expected_generation,
404 mode,
405 } => Ok(Response::Purged {
406 sessions: d.purge(session, expected_generation, mode)?,
407 }),
408 Request::SetQos {
409 session,
410 class,
411 batch_invariant,
412 } => Ok(Response::Committed {
413 event: d.set_qos(session, class, batch_invariant)?.into(),
414 }),
415 Request::SetBackgroundRate { divisor } => {
416 d.set_background_tick_divisor(divisor);
417 Ok(Response::Ack)
418 }
419 Request::Inspect { session } => Ok(Response::Inspection {
420 inspection: d.inspect(session)?,
421 }),
422 Request::SetLogLevel { directive } => {
423 crate::telemetry::set_filter(&directive).map_err(DaemonError::Generation)?;
424 Ok(Response::Ack)
425 }
426 Request::Metrics => Ok(Response::Text {
427 text: d.metrics_text(),
428 }),
429 Request::AppendBlock {
430 session,
431 role,
432 kind,
433 payload,
434 } => Ok(Response::Committed {
435 event: d.append_block(session, role, kind, payload)?.into(),
436 }),
437 Request::RequestPermission {
438 session,
439 call_id,
440 text,
441 } => Ok(Response::Committed {
442 event: d.request_permission(session, call_id, text)?.into(),
443 }),
444 Request::RespondPermission {
445 session,
446 request_id,
447 granted,
448 } => Ok(Response::Committed {
449 event: d.respond_permission(session, request_id, granted)?.into(),
450 }),
451 Request::RequestToolCancel { session, call_id } => Ok(Response::Committed {
452 event: d.request_tool_cancel(session, call_id)?.into(),
453 }),
454 Request::AppendToolOutcome {
455 session,
456 call_id,
457 outcome,
458 note,
459 } => Ok(Response::Committed {
460 event: d.append_tool_outcome(session, call_id, outcome, note)?.into(),
461 }),
462 Request::Ledger { session } => Ok(Response::Ledger {
463 entries: d.ledger(session)?,
464 }),
465 Request::PutMedia { bytes, mime: _ } => Ok(Response::Media {
466 hash: d.put_media(&bytes)?,
467 }),
468 Request::AppendImage {
469 session,
470 role,
471 blob,
472 pre_text,
473 post_text,
474 } => Ok(Response::Committed {
475 event: d.append_image(session, role, &blob, &pre_text, &post_text)?.into(),
476 }),
477 Request::GcMedia => {
478 let (kept, removed) = d.gc_media()?;
479 Ok(Response::Gc {
480 kept: kept as u64,
481 removed: removed as u64,
482 })
483 }
484 Request::Trim { session, to_event } => Ok(Response::Created {
485 session: d.trim(session, to_event)?,
486 }),
487 Request::Pin { session, ttl_ms } => Ok(Response::Pinned {
488 deadline_unix_ms: d.pin(session, ttl_ms)?,
489 }),
490 Request::AppendSystem {
491 session,
492 text,
493 tools,
494 } => Ok(Response::Committed {
495 event: d.append_system(session, text, tools)?.into(),
496 }),
497 Request::Complete {
498 prefix,
499 suffix,
500 mode,
501 max_tokens,
502 } => {
503 let c = d.complete(&prefix, &suffix, mode, max_tokens)?;
504 Ok(Response::Completion {
505 text: c.text,
506 tokens: c.tokens,
507 cached: c.cached,
508 expired: c.expired,
509 })
510 }
511 Request::Generate { .. }
512 | Request::Cancel { .. }
513 | Request::Subscribe { .. }
514 | Request::Unsubscribe { .. } => {
515 Err(DaemonError::Protocol("request routed to the wrong handler"))
516 }
517 })();
518 result.unwrap_or_else(|e| Response::Err {
519 message: e.to_string(),
520 })
521}
522
523pub fn serve(listener: UnixListener, daemon: Arc<Daemon>) -> Result<(), DaemonError> {
524 let cancels = daemon.cancel_registry();
525 let active = daemon.active_registry();
526 for conn in listener.incoming() {
527 let stream = conn?;
528 let daemon = Arc::clone(&daemon);
529 let cancels = Arc::clone(&cancels);
530 let active = Arc::clone(&active);
531 std::thread::spawn(move || {
532 let _ = serve_conn(stream, daemon, &cancels, &active);
533 });
534 }
535 Ok(())
536}
537
538pub(crate) type Sink = Arc<dyn Fn(&Response) -> Result<(), DaemonError> + Send + Sync>;
539
540fn send(w: &Sink, msg: &Response) -> Result<(), DaemonError> {
541 w(msg)
542}
543
544struct ConnSub {
545 session: u64,
546 bus_id: u64,
547 detached: Arc<AtomicBool>,
548}
549
550fn serve_conn(
551 mut stream: UnixStream,
552 daemon: Arc<Daemon>,
553 cancels: &crate::CancelSet,
554 active: &Mutex<HashSet<u64>>,
555) -> Result<(), DaemonError> {
556 let out = Mutex::new(stream.try_clone()?);
557 let writer: Sink = Arc::new(move |msg: &Response| {
558 write_frame(&mut *out.lock().expect("api writer"), msg)
559 });
560 let requests = std::iter::from_fn(move || read_frame::<Request>(&mut stream).transpose());
561 serve_requests(requests, writer, daemon, cancels, active)
562}
563
564pub(crate) fn serve_requests(
565 requests: impl Iterator<Item = Result<Request, DaemonError>>,
566 writer: Sink,
567 daemon: Arc<Daemon>,
568 cancels: &crate::CancelSet,
569 active: &Mutex<HashSet<u64>>,
570) -> Result<(), DaemonError> {
571 let mut subs: HashMap<u64, ConnSub> = HashMap::new();
572 let bucket_cfg = daemon.completion_bucket();
573 let mut fim_bucket =
574 crate::completion_bucket::TokenBucket::new(bucket_cfg, std::time::Instant::now());
575 let result = (|| -> Result<(), DaemonError> {
576 for req in requests {
577 match req? {
578 Request::Generate {
579 session,
580 max_tokens,
581 } => {
582 let w = Arc::clone(&writer);
583 let result = daemon.generate_streaming(session, max_tokens, |ev| {
584 send(
585 &w,
586 &Response::Delta {
587 event: ev.clone().into(),
588 },
589 )
590 });
591 let res = match result {
592 Ok(out) => Response::Generated {
593 events: out.events.into_iter().map(Into::into).collect(),
594 tokens_generated: out.tokens_generated,
595 finish: out.finish,
596 warm_prefix: out.warm_prefix,
597 },
598 Err(e) => Response::Err {
599 message: e.to_string(),
600 },
601 };
602 send(&writer, &res)?;
603 }
604 Request::Cancel { session } => {
605 send(&writer, &cancel(cancels, active, session))?;
606 }
607 Request::Subscribe {
608 session,
609 cursor,
610 provisional,
611 } => {
612 let res = subscribe(&daemon, &writer, &mut subs, session, cursor, provisional);
613 if let Err(e) = res {
614 send(
615 &writer,
616 &Response::Err {
617 message: e.to_string(),
618 },
619 )?;
620 }
621 }
622 Request::Unsubscribe { sub } => {
623 let res = match subs.remove(&sub) {
624 Some(cs) => {
625 cs.detached.store(true, Ordering::Release);
626 daemon.bus().unsubscribe(cs.session, cs.bus_id);
627 Response::Unsubscribed
628 }
629 None => Response::Err {
630 message: format!("no subscription {sub} on this connection"),
631 },
632 };
633 send(&writer, &res)?;
634 }
635 req @ Request::Complete { .. } => {
636 let res = match fim_bucket.try_take(bucket_cfg, std::time::Instant::now()) {
637 Ok(()) => handle(&daemon, req),
638 Err(wait) => {
639 daemon.note_completion_throttled();
640 Response::Throttled {
641 retry_after_ms: crate::completion_bucket::retry_after_ms(wait),
642 }
643 }
644 };
645 send(&writer, &res)?;
646 }
647 other => {
648 let res = handle(&daemon, other);
649 send(&writer, &res)?;
650 }
651 }
652 }
653 Ok(())
654 })();
655 for (_, cs) in subs.drain() {
656 cs.detached.store(true, Ordering::Release);
657 daemon.bus().unsubscribe(cs.session, cs.bus_id);
658 }
659 result
660}
661
662fn subscribe(
663 daemon: &Arc<Daemon>,
664 writer: &Sink,
665 subs: &mut HashMap<u64, ConnSub>,
666 session: u64,
667 cursor: u64,
668 provisional: bool,
669) -> Result<(), DaemonError> {
670 let sub = daemon.bus().subscribe(session, provisional);
671 let backlog = match daemon.read(session, cursor) {
672 Ok(evs) => evs,
673 Err(e) => {
674 daemon.bus().unsubscribe(session, sub.id);
675 return Err(e);
676 }
677 };
678 let sub_id = sub.id;
679 let detached = Arc::new(AtomicBool::new(false));
680 subs.insert(
681 sub_id,
682 ConnSub {
683 session,
684 bus_id: sub.id,
685 detached: Arc::clone(&detached),
686 },
687 );
688 send(writer, &Response::Subscribed { sub: sub_id })?;
689 let w = Arc::clone(writer);
690 std::thread::Builder::new()
691 .name(format!("superfluid-sub-{sub_id}"))
692 .spawn(move || {
693 let mut next_id = cursor;
694 for ev in backlog {
695 next_id = ev.event_id + 1;
696 if send(
697 &w,
698 &Response::SubEvent {
699 sub: sub_id,
700 event: ev.into(),
701 },
702 )
703 .is_err()
704 {
705 return;
706 }
707 }
708 loop {
709 if detached.load(Ordering::Acquire) {
710 let _ = send(
711 &w,
712 &Response::SubEnded {
713 sub: sub_id,
714 reason: "unsubscribed".into(),
715 },
716 );
717 return;
718 }
719 match sub.rx.recv() {
720 Ok(msg) if detached.load(Ordering::Acquire) => {
721 let _ = msg;
722 let _ = send(
723 &w,
724 &Response::SubEnded {
725 sub: sub_id,
726 reason: "unsubscribed".into(),
727 },
728 );
729 return;
730 }
731 Ok(BusMsg::Committed(ev)) => {
732 if ev.event_id < next_id {
733 continue;
734 }
735 next_id = ev.event_id + 1;
736 if send(
737 &w,
738 &Response::SubEvent {
739 sub: sub_id,
740 event: ev.into(),
741 },
742 )
743 .is_err()
744 {
745 return;
746 }
747 }
748 Ok(BusMsg::Provisional { channel, text }) => {
749 if send(
750 &w,
751 &Response::SubProvisional {
752 sub: sub_id,
753 channel,
754 text,
755 },
756 )
757 .is_err()
758 {
759 return;
760 }
761 }
762 Err(_) => {
763 let reason = if detached.load(Ordering::Acquire) {
764 "unsubscribed"
765 } else {
766 match sub.end_reason() {
767 EndReason::Lagged => "lagged",
768 EndReason::PublisherClosed => "publisher closed",
769 EndReason::Purged => "purged",
770 }
771 };
772 let _ = send(
773 &w,
774 &Response::SubEnded {
775 sub: sub_id,
776 reason: reason.into(),
777 },
778 );
779 return;
780 }
781 }
782 }
783 })
784 .expect("spawn subscription delivery");
785 Ok(())
786}
787
788pub struct NativeClient {
789 stream: UnixStream,
790}
791
792impl NativeClient {
793 pub fn connect(path: &std::path::Path) -> Result<NativeClient, DaemonError> {
794 Ok(NativeClient {
795 stream: UnixStream::connect(path)?,
796 })
797 }
798
799 pub fn request(&mut self, req: &Request) -> Result<Response, DaemonError> {
800 write_frame(&mut self.stream, req)?;
801 read_frame(&mut self.stream)?.ok_or(DaemonError::Protocol("daemon closed the connection"))
802 }
803
804 pub fn generate_stream(
805 &mut self,
806 session: u64,
807 max_tokens: u32,
808 mut on_delta: impl FnMut(EventMsg),
809 ) -> Result<Response, DaemonError> {
810 write_frame(
811 &mut self.stream,
812 &Request::Generate {
813 session,
814 max_tokens,
815 },
816 )?;
817 loop {
818 let res: Response = read_frame(&mut self.stream)?
819 .ok_or(DaemonError::Protocol("daemon closed mid-stream"))?;
820 match res {
821 Response::Delta { event } => on_delta(event),
822 terminal => return Ok(terminal),
823 }
824 }
825 }
826
827 pub fn cancel(&mut self, session: u64) -> Result<(), DaemonError> {
828 match self.request(&Request::Cancel { session })? {
829 Response::Cancelling => Ok(()),
830 _ => Err(DaemonError::Protocol("unexpected cancel response")),
831 }
832 }
833
834 pub fn subscribe(
835 &mut self,
836 session: u64,
837 cursor: u64,
838 provisional: bool,
839 ) -> Result<u64, DaemonError> {
840 match self.request(&Request::Subscribe {
841 session,
842 cursor,
843 provisional,
844 })? {
845 Response::Subscribed { sub } => Ok(sub),
846 Response::Err { message } => Err(DaemonError::Generation(message)),
847 _ => Err(DaemonError::Protocol("unexpected subscribe response")),
848 }
849 }
850
851 pub fn next_frame(&mut self) -> Result<Response, DaemonError> {
852 read_frame(&mut self.stream)?.ok_or(DaemonError::Protocol("daemon closed the connection"))
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859
860 #[test]
861 fn socket_variant_indices_are_stable() {
862 let enc = |r: &Request| postcard::to_stdvec(r).unwrap();
863 assert_eq!(enc(&Request::List), [7]);
864 assert_eq!(
865 enc(&Request::AppendMessage { session: 1, role: 1, text: "hi".into() }),
866 [2, 1, 1, 2, b'h', b'i']
867 );
868 assert_eq!(
869 enc(&Request::AppendToolResult { session: 1, call_id: 4, content: "x".into() }),
870 [5, 1, 4, 1, b'x']
871 );
872 assert_eq!(enc(&Request::Pin { session: 1, ttl_ms: 5 }), [33, 1, 5]);
873 assert_eq!(
874 enc(&Request::AppendSystem { session: 1, text: None, tools: vec!["{}".into()] }),
875 [34, 1, 0, 1, 2, b'{', b'}']
876 );
877 let res = |r: &Response| postcard::to_stdvec(r).unwrap();
878 assert_eq!(res(&Response::Err { message: "no".into() }), [13, 2, b'n', b'o']);
879 assert_eq!(res(&Response::Throttled { retry_after_ms: 9 }), [26, 9]);
880 let back: Request = postcard::from_bytes(&[3, 1, 8]).unwrap();
881 assert!(matches!(back, Request::Generate { session: 1, max_tokens: 8 }));
882 }
883}