1use std::collections::BTreeMap;
4
5use superfluid_linkf::{Endpoint, HeadSession, Liveness};
6use superfluid_proto::handshake::HelloAckF;
7use superfluid_proto::linkf::{
8 self, msg_type, AppendContentMsg, GenerateCancelMsg, GenerationBudgetsMsg, Lease, TokenEventPayload,
9};
10
11use crate::codec::TextCodec;
12use superfluid_engine::StageOutput;
13use crate::wal::GenParams;
14use crate::DaemonError;
15
16pub fn encode_params(params: &GenParams) -> Vec<u8> {
17 postcard::to_stdvec(params).expect("GenParams postcard")
18}
19
20pub fn decode_params(bytes: &[u8]) -> GenParams {
21 postcard::from_bytes(bytes).unwrap_or_default()
22}
23
24pub fn encode_tokens(tokens: &[u32]) -> Vec<u8> {
25 postcard::to_stdvec(&tokens.to_vec()).expect("tokens postcard")
26}
27
28pub fn decode_tokens(bytes: &[u8]) -> Result<Vec<u32>, DaemonError> {
29 postcard::from_bytes(bytes).map_err(DaemonError::Codec)
30}
31
32#[derive(Debug, Clone)]
33pub struct FleetGeneration {
34 pub tokens: Vec<u32>,
35 pub finish: u32,
36 pub expired: bool,
37}
38
39#[derive(Debug, Clone)]
40pub struct FleetSegment {
41 pub channel: u32,
42 pub text: String,
43 pub tool: Option<(String, String)>,
44}
45
46#[derive(Debug, Clone)]
47struct Placement {
48 params: GenParams,
49 qos_class: u8,
50 codec: String,
51 lease: Lease,
52 tokens: Vec<u32>,
53}
54
55pub struct FleetHead {
56 head: HeadSession,
57 endpoint: Endpoint,
58 node: HelloAckF,
59 next_generation: u64,
60 placements: BTreeMap<u64, Placement>,
61 addr: String,
62 auth: Vec<u8>,
63 on_status: Option<StatusHook>,
64 liveness: Liveness,
65}
66
67pub type StatusHook = std::sync::Arc<dyn Fn(&superfluid_proto::handshake::CapacitySummary) + Send + Sync>;
69
70static NEXT_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
73static NEXT_COMMANDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
74
75fn fresh_session_state() -> HeadSession {
76 HeadSession::starting_at(NEXT_COMMANDS.fetch_add(1 << 32, std::sync::atomic::Ordering::Relaxed))
77}
78
79pub const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
80
81pub const HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
82
83fn closed_at_handshake() -> String {
84 "closed the connection at the handshake; check that --fleet-auth holds the token in the node's --auth-file".into()
85}
86
87fn handshake(addr: &str, auth: Vec<u8>) -> Result<(Endpoint, HelloAckF), DaemonError> {
89 let node = |why: String| DaemonError::FleetNode { addr: addr.to_string(), why };
90 let stream = Endpoint::tcp_connect_within(addr, CONNECT_TIMEOUT).map_err(|e| node(format!("unreachable ({e})")))?;
91 let opened = if auth.is_empty() {
92 Endpoint::from_tcp(stream)
93 } else {
94 Endpoint::from_tcp_secure(stream, &auth, superfluid_linkf::Role::Initiator, HANDSHAKE_TIMEOUT)
95 };
96 hello(opened, addr, auth, "--fleet-auth and the node's --auth-file differ")
97}
98
99fn hello(opened: Result<Endpoint, superfluid_linkf::LinkFError>, peer: &str, auth: Vec<u8>, mismatch: &str) -> Result<(Endpoint, HelloAckF), DaemonError> {
103 use superfluid_linkf::LinkFError;
104 let node = |why: String| DaemonError::FleetNode { addr: peer.to_string(), why };
105 let mut endpoint = match opened {
106 Ok(ep) => ep,
107 Err(LinkFError::Unauthorized) => return Err(node(format!("refused the fleet token: {mismatch}"))),
108 Err(LinkFError::TimedOut) => {
109 return Err(node(format!("did not finish the handshake within {} s", HANDSHAKE_TIMEOUT.as_secs())))
110 }
111 Err(LinkFError::Closed | LinkFError::Io(_)) if !auth.is_empty() => return Err(node(closed_at_handshake())),
112 Err(e) => return Err(node(e.to_string())),
113 };
114 match endpoint.hello_as_head_within(auth, HANDSHAKE_TIMEOUT) {
115 Ok(ack) => Ok((endpoint, ack)),
116 Err(LinkFError::Unauthorized) => Err(node(format!("refused the fleet token: {mismatch}"))),
117 Err(LinkFError::Closed | LinkFError::Io(_)) => Err(node(closed_at_handshake())),
118 Err(LinkFError::TimedOut) => Err(node(format!(
119 "did not answer the handshake within {} s; is superfluid-noded listening there?",
120 HANDSHAKE_TIMEOUT.as_secs()
121 ))),
122 Err(e) => Err(node(e.to_string())),
123 }
124}
125
126fn is_transient(e: &DaemonError) -> bool {
127 matches!(
128 e,
129 DaemonError::Fleet(superfluid_linkf::LinkFError::Closed)
130 | DaemonError::Fleet(superfluid_linkf::LinkFError::TimedOut)
131 | DaemonError::Fleet(superfluid_linkf::LinkFError::Io(_))
132 )
133}
134
135impl FleetHead {
136 pub fn connect(addr: &str, auth: Vec<u8>) -> Result<FleetHead, DaemonError> {
137 let (endpoint, node) = handshake(addr, auth.clone())?;
138 Ok(FleetHead::from_parts(endpoint, node, addr.to_string(), auth))
139 }
140
141 pub fn accept(sock: std::net::TcpStream, auth: Vec<u8>) -> Result<FleetHead, DaemonError> {
143 let peer = sock.peer_addr().map(|a| a.to_string()).unwrap_or_default();
144 let opened = if auth.is_empty() {
145 Endpoint::from_tcp(sock)
146 } else {
147 Endpoint::from_tcp_secure(sock, &auth, superfluid_linkf::Role::Initiator, HANDSHAKE_TIMEOUT)
148 };
149 let (endpoint, node) = hello(opened, &peer, auth.clone(), "its token differs from this head's")?;
150 Ok(FleetHead::from_parts(endpoint, node, String::new(), auth))
151 }
152
153 fn from_parts(mut endpoint: Endpoint, node: HelloAckF, addr: String, auth: Vec<u8>) -> FleetHead {
154 endpoint.keep_alive(Liveness::DEFAULT);
155 FleetHead {
156 head: fresh_session_state(),
157 endpoint,
158 node,
159 next_generation: 1,
160 placements: BTreeMap::new(),
161 addr,
162 auth,
163 on_status: None,
164 liveness: Liveness::DEFAULT,
165 }
166 }
167
168 fn grace(&self) -> std::time::Duration {
171 match self.liveness.silence {
172 t if t.is_zero() => Liveness::DEFAULT.silence,
173 t => t,
174 }
175 }
176
177 fn wall_deadline(&self, max_wall_ms: u64) -> Option<std::time::Instant> {
180 (max_wall_ms > 0)
181 .then(|| std::time::Instant::now() + std::time::Duration::from_millis(max_wall_ms) + self.grace())
182 }
183
184 fn frames_within(&mut self, within: std::time::Duration) -> Result<Vec<superfluid_proto::Frame>, DaemonError> {
186 let by = std::time::Instant::now() + within;
187 loop {
188 let frames = self.endpoint.pump()?;
189 if !frames.is_empty() {
190 return Ok(frames);
191 }
192 if std::time::Instant::now() >= by {
193 return Err(superfluid_linkf::LinkFError::TimedOut.into());
194 }
195 }
196 }
197
198 pub fn keep_alive(&mut self, liveness: Liveness) {
200 self.liveness = liveness;
201 self.endpoint.keep_alive(liveness);
202 }
203
204 pub fn on_status(&mut self, hook: StatusHook) {
207 self.on_status = Some(hook);
208 }
209
210 fn reconnect_self(&mut self) -> Result<(), DaemonError> {
211 let (addr, auth) = (self.addr.clone(), self.auth.clone());
212 self.reconnect(&addr, auth)?;
213 Ok(())
214 }
215
216 pub fn node(&self) -> &HelloAckF {
217 &self.node
218 }
219
220 pub fn drain_status(&mut self) -> Result<(), DaemonError> {
223 for frame in self.endpoint.pump()? {
224 self.absorb(frame)?;
225 }
226 Ok(())
227 }
228
229 fn absorb(&mut self, frame: superfluid_proto::Frame) -> Result<Option<superfluid_proto::Frame>, DaemonError> {
232 match frame.msg_type {
233 msg_type::NODE_STATUS => {
234 if let Ok(m) = postcard::from_bytes::<linkf::NodeStatusMsg>(&frame.payload) {
235 self.node.capacity = m.capacity;
236 if let Some(hook) = &self.on_status {
237 hook(&self.node.capacity);
238 }
239 if !m.loadable_models.is_empty() {
240 self.node.loadable_models = m.loadable_models;
241 }
242 }
243 Ok(None)
244 }
245 msg_type::LEASE_RENEW => {
246 let m: linkf::LeaseRenewMsg = postcard::from_bytes(&frame.payload)?;
247 if let Some(grant) = self.head.on_lease_renew(&m) {
248 self.endpoint.send(msg_type::LEASE_GRANT, &grant)?;
249 }
250 Ok(None)
251 }
252 _ => Ok(Some(frame)),
253 }
254 }
255
256 pub fn knows(&self, session_id: u64) -> bool {
257 self.head.current_epoch(session_id).is_some()
258 }
259
260
261 pub fn reconnect(&mut self, addr: &str, auth: Vec<u8>) -> Result<Vec<u64>, DaemonError> {
262 let _ = self.endpoint.shutdown();
263 if addr.is_empty() {
264 return Err(DaemonError::FleetNode {
265 addr: self.node.node_identity.clone(),
266 why: "dialed in on its own; waiting for it to join again".into(),
267 });
268 }
269 let (mut endpoint, node) = handshake(addr, auth)?;
270 endpoint.keep_alive(self.liveness);
271 self.node = node;
272 self.endpoint = endpoint;
273 let rec = self.head.reconcile();
274 self.endpoint.send(msg_type::RECONCILE, &rec)?;
275
276 let mut received: std::collections::BTreeSet<(u64, u64, u64)> =
277 std::collections::BTreeSet::new();
278 let ack = loop {
279 let mut got = None;
280 for frame in self.frames_within(self.grace())? {
281 let Some(frame) = self.absorb(frame)? else { continue };
282 match frame.msg_type {
283 msg_type::RECONCILE_ACK => {
284 got = Some(postcard::from_bytes::<linkf::ReconcileAckMsg>(&frame.payload)?);
285 }
286 msg_type::TOKEN_EVENTS => {
287 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
288 for ev in &m.events {
289 received.insert((ev.session_id, ev.generation_id, ev.event_seq));
290 }
291 for a in self.head.on_token_events(&m) {
292 self.endpoint.send(msg_type::WATERMARK_ACK, &a)?;
293 }
294 }
295 _ => {}
296 }
297 }
298 if let Some(a) = got {
299 break a;
300 }
301 };
302
303 let mut expected: std::collections::BTreeSet<(u64, u64, u64)> =
304 std::collections::BTreeSet::new();
305 for s in &ack.sessions {
306 if self.head.current_epoch(s.session_id).is_some() {
307 for r in &s.retry_ranges {
308 for seq in r.from_seq..=r.to_seq {
309 let id = (s.session_id, r.generation_id, seq);
310 if !received.contains(&id) {
311 expected.insert(id);
312 }
313 }
314 }
315 }
316 }
317 while !expected.is_empty() {
318 for frame in self.frames_within(self.grace())? {
319 let Some(frame) = self.absorb(frame)? else { continue };
320 if frame.msg_type != msg_type::TOKEN_EVENTS {
321 continue;
322 }
323 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
324 for ev in &m.events {
325 expected.remove(&(ev.session_id, ev.generation_id, ev.event_seq));
326 }
327 for a in self.head.on_token_events(&m) {
328 self.endpoint.send(msg_type::WATERMARK_ACK, &a)?;
329 }
330 }
331 }
332
333 let reassigned = self.head.on_reconcile_ack(&ack);
336 for s in ack.sessions.iter().filter(|s| s.held && !reassigned.contains(&s.session_id)) {
337 if let (Some(p), Some(epoch)) =
338 (self.placements.get(&s.session_id).cloned(), self.head.current_epoch(s.session_id))
339 {
340 if !p.tokens.is_empty() {
341 self.send_append(s.session_id, epoch, &p.tokens)?;
342 }
343 }
344 }
345 let mut replaced = Vec::new();
346 for session_id in reassigned {
347 if let Some(p) = self.placements.get(&session_id).cloned() {
348 if let Some(epoch) = self.head.current_epoch(session_id) {
349 let mut msg = self.head.assign_session(
350 session_id,
351 epoch,
352 p.lease,
353 p.qos_class,
354 &p.codec,
355 );
356 msg.params = encode_params(&p.params);
357 self.endpoint.send(msg_type::SESSION_ASSIGN, &msg)?;
358 if !p.tokens.is_empty() {
359 self.send_append(session_id, epoch, &p.tokens)?;
360 }
361 replaced.push(session_id);
362 }
363 }
364 }
365 Ok(replaced)
366 }
367
368 pub fn assign(
369 &mut self,
370 session_id: u64,
371 params: GenParams,
372 qos_class: u8,
373 codec: &str,
374 lease: Lease,
375 ) -> Result<u64, DaemonError> {
376 let entry = self.placements.entry(session_id).or_insert(Placement {
377 params,
378 qos_class,
379 codec: codec.to_string(),
380 lease,
381 tokens: Vec::new(),
382 });
383 entry.params = params;
384 entry.qos_class = qos_class;
385 entry.codec = codec.to_string();
386 entry.lease = lease;
387
388 let epoch = NEXT_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
389 let mut msg = self.head.assign_session(session_id, epoch, lease, qos_class, codec);
390 msg.params = encode_params(¶ms);
391 match self.endpoint.send(msg_type::SESSION_ASSIGN, &msg) {
392 Ok(()) => Ok(epoch),
393 Err(e) => {
394 let e: DaemonError = e.into();
395 if is_transient(&e) {
396 self.reconnect_self()?;
397 Ok(self.head.current_epoch(session_id).unwrap_or(epoch))
398 } else {
399 Err(e)
400 }
401 }
402 }
403 }
404
405 pub fn park(&mut self, session_id: u64) -> Result<(), DaemonError> {
406 let epoch = self.head.current_epoch(session_id).unwrap_or(0);
407 self.revoke(session_id, epoch)
408 }
409
410 pub fn revoke(&mut self, session_id: u64, epoch: u64) -> Result<(), DaemonError> {
413 self.placements.remove(&session_id);
414 let msg = match self.head.revoke(session_id) {
415 Some(msg) => msg,
416 None => linkf::SessionRevokeMsg { command_id: self.head.next_command(session_id), session_id, epoch },
417 };
418 self.endpoint.send(msg_type::SESSION_REVOKE, &msg)?;
419 Ok(())
420 }
421
422 pub fn append(&mut self, session_id: u64, tokens: &[u32]) -> Result<(), DaemonError> {
423 let epoch = self
424 .head
425 .current_epoch(session_id)
426 .ok_or(DaemonError::Protocol("append before assign"))?;
427 if let Some(p) = self.placements.get_mut(&session_id) {
428 p.tokens = tokens.to_vec();
429 }
430 match self.send_append(session_id, epoch, tokens) {
431 Ok(()) => Ok(()),
432 Err(e) if is_transient(&e) => {
433 self.reconnect_self()
434 }
435 Err(e) => Err(e),
436 }
437 }
438
439 fn send_append(&mut self, session_id: u64, epoch: u64, tokens: &[u32]) -> Result<(), DaemonError> {
440 let msg = AppendContentMsg {
441 command_id: self.head.next_command(session_id),
442 session_id,
443 epoch,
444 blocks: encode_tokens(tokens),
445 spans: Vec::new(),
446 media_refs: Vec::new(),
447 };
448 self.endpoint.send(msg_type::APPEND_CONTENT, &msg)?;
449 Ok(())
450 }
451
452 pub fn generate(
453 &mut self,
454 session_id: u64,
455 max_new_tokens: u64,
456 max_wall_ms: u64,
457 ) -> Result<FleetGeneration, DaemonError> {
458 let generation_id = self.next_generation;
459 self.next_generation += 1;
460 for attempt in 0..2 {
461 match self.generate_once(session_id, generation_id, max_new_tokens, max_wall_ms) {
462 Ok(g) => return Ok(g),
463 Err(e) if attempt == 0 && is_transient(&e) => {
464 self.reconnect_self()?;
465 if let Some(g) = self.recovered_generation(session_id, generation_id) {
466 self.head.retire_generation(session_id, generation_id);
467 return Ok(g);
468 }
469 continue;
470 }
471 Err(e) => return Err(e),
472 }
473 }
474 unreachable!("generate retried at most once")
475 }
476
477 pub fn generate_streaming(
478 &mut self,
479 session_id: u64,
480 max_new_tokens: u64,
481 max_wall_ms: u64,
482 cancel: &std::sync::atomic::AtomicBool,
483 mut on_tokens: impl FnMut(&[u32]),
484 ) -> Result<FleetGeneration, DaemonError> {
485 let mut delivered = false;
486 let first = self.generate_streaming_once(session_id, max_new_tokens, max_wall_ms, cancel, |t| {
487 delivered = true;
488 on_tokens(t)
489 });
490 match first {
491 Err(e) if !delivered && is_transient(&e) => {
494 self.reconnect_self()?;
495 self.generate_streaming_once(session_id, max_new_tokens, max_wall_ms, cancel, on_tokens)
496 }
497 r => r,
498 }
499 }
500
501 fn generate_streaming_once(
502 &mut self,
503 session_id: u64,
504 max_new_tokens: u64,
505 max_wall_ms: u64,
506 cancel: &std::sync::atomic::AtomicBool,
507 mut on_tokens: impl FnMut(&[u32]),
508 ) -> Result<FleetGeneration, DaemonError> {
509 let generation_id = self.next_generation;
510 self.next_generation += 1;
511 let req = self
512 .head
513 .generate(
514 session_id,
515 generation_id,
516 GenerationBudgetsMsg { max_new_tokens, max_wall_ms },
517 )
518 .ok_or(DaemonError::Protocol("generate before assign"))?;
519 self.endpoint.send(msg_type::GENERATE_REQ, &req)?;
520
521 let mut tokens = Vec::new();
522 let mut finish = 0u32;
523 let mut done = false;
524 let mut cancel_sent = false;
525 let deadline = self.wall_deadline(max_wall_ms);
526 while !done {
527 if deadline.is_some_and(|d| std::time::Instant::now() >= d) {
528 return Err(superfluid_linkf::LinkFError::TimedOut.into());
529 }
530 if !cancel_sent && cancel.load(std::sync::atomic::Ordering::Relaxed) {
531 self.endpoint.send(
532 msg_type::GENERATE_CANCEL,
533 &GenerateCancelMsg { session_id, generation_id },
534 )?;
535 cancel_sent = true;
536 }
537 for frame in self.endpoint.pump()? {
540 let Some(frame) = self.absorb(frame)?.filter(|f| f.msg_type == msg_type::TOKEN_EVENTS) else {
541 continue;
542 };
543 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
544 for ev in &m.events {
545 if ev.session_id == session_id && ev.generation_id == generation_id {
546 match &ev.payload {
547 TokenEventPayload::Tokens(t) => {
548 tokens.extend_from_slice(t);
549 on_tokens(t);
550 }
551 TokenEventPayload::Finish { reason } => {
552 finish = *reason;
553 done = true;
554 }
555 _ => {}
556 }
557 }
558 }
559 for ack in self.head.on_token_events(&m) {
560 self.endpoint.send(msg_type::WATERMARK_ACK, &ack)?;
561 }
562 }
563 }
564 let expired = finish == u32::MAX;
565 self.head.retire_generation(session_id, generation_id);
566 Ok(FleetGeneration { tokens, finish, expired })
567 }
568
569 pub fn forward_stage(
570 &mut self,
571 tokens: &[u32],
572 start_layer: u32,
573 end_layer: u32,
574 hidden_in: Option<Vec<u8>>,
575 ) -> Result<superfluid_engine::StageOutput, DaemonError> {
576 let request_id = self.next_generation;
577 self.next_generation += 1;
578 let req = linkf::StageForwardReqMsg {
579 request_id,
580 tokens: tokens.to_vec(),
581 start_layer,
582 end_layer,
583 hidden_in,
584 };
585 self.endpoint.send(msg_type::STAGE_FORWARD_REQ, &req)?;
586 loop {
587 for frame in self.endpoint.wait_frames()? {
588 let Some(frame) = self.absorb(frame)? else { continue };
589 if frame.msg_type == msg_type::STAGE_FORWARD_RESP {
590 let m: linkf::StageForwardRespMsg = postcard::from_bytes(&frame.payload)?;
591 if m.request_id != request_id {
592 continue;
593 }
594 return match (m.hidden, m.token) {
595 (Some(h), None) => Ok(superfluid_engine::StageOutput::Hidden(h)),
596 (None, Some(t)) => Ok(superfluid_engine::StageOutput::Token(t)),
597 _ => Err(DaemonError::Generation(
598 "stage node could not run the forward (engine lacks staged forward)".into(),
599 )),
600 };
601 }
602 }
603 }
604 }
605
606 pub fn generate_chat(
607 &mut self,
608 session_id: u64,
609 codec: &dyn TextCodec,
610 messages: &[crate::codec::ChatMessage],
611 tools: &[String],
612 max_new_tokens: u64,
613 max_wall_ms: u64,
614 ) -> Result<Vec<FleetSegment>, DaemonError> {
615 let prompt = codec
616 .render_prompt_structured(messages, tools)
617 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
618 self.append(session_id, &prompt)?;
619 let gen = self.generate(session_id, max_new_tokens, max_wall_ms)?;
620
621 let mut chan = crate::codec::primed_channelizer(codec, &prompt);
622 let schemas = crate::codec::ToolSchemas::from_jsons(tools);
623 let mut segments = Vec::new();
624 for run in chan.split(&gen.tokens) {
625 let text = codec.decode(&run.text);
626 let tool = if run.channel == crate::wal::channel::TOOL_CALL && run.closes {
627 codec.parse_tool_call_with(&text, Some(&schemas))
628 } else {
629 None
630 };
631 if !text.is_empty() || tool.is_some() {
632 segments.push(FleetSegment {
633 channel: run.channel,
634 text,
635 tool,
636 });
637 }
638 }
639 Ok(segments)
640 }
641
642 fn recovered_generation(
643 &self,
644 session_id: u64,
645 generation_id: u64,
646 ) -> Option<FleetGeneration> {
647 let epoch = self.head.current_epoch(session_id)?;
648 let mut events: Vec<_> = self
649 .head
650 .wal()
651 .session_events(session_id)
652 .iter()
653 .filter(|c| c.event.epoch == epoch && c.event.generation_id == generation_id)
654 .collect();
655 events.sort_by_key(|c| c.event.event_seq);
656 let mut tokens = Vec::new();
657 let mut finish = None;
658 for c in events {
659 match &c.event.payload {
660 TokenEventPayload::Tokens(t) => tokens.extend_from_slice(t),
661 TokenEventPayload::Finish { reason } => finish = Some(*reason),
662 _ => {}
663 }
664 }
665 let finish = finish?;
666 Some(FleetGeneration {
667 tokens,
668 finish,
669 expired: finish == crate::nodeagent::FINISH_EXPIRED,
670 })
671 }
672
673 fn generate_once(
674 &mut self,
675 session_id: u64,
676 generation_id: u64,
677 max_new_tokens: u64,
678 max_wall_ms: u64,
679 ) -> Result<FleetGeneration, DaemonError> {
680 let req = self
681 .head
682 .generate(
683 session_id,
684 generation_id,
685 GenerationBudgetsMsg {
686 max_new_tokens,
687 max_wall_ms,
688 },
689 )
690 .ok_or(DaemonError::Protocol("generate before assign"))?;
691 self.endpoint.send(msg_type::GENERATE_REQ, &req)?;
692
693 let mut tokens = Vec::new();
694 let mut finish = 0u32;
695 let mut expired = false;
696 let mut done = false;
697 let deadline = self.wall_deadline(max_wall_ms);
698 while !done {
699 if deadline.is_some_and(|d| std::time::Instant::now() >= d) {
700 return Err(superfluid_linkf::LinkFError::TimedOut.into());
701 }
702 for frame in self.endpoint.pump()? {
705 let Some(frame) = self.absorb(frame)?.filter(|f| f.msg_type == msg_type::TOKEN_EVENTS) else {
706 continue;
707 };
708 let m: linkf::TokenEventsMsg = postcard::from_bytes(&frame.payload)?;
709 for ev in &m.events {
710 if ev.session_id == session_id && ev.generation_id == generation_id {
711 match &ev.payload {
712 TokenEventPayload::Tokens(t) => tokens.extend_from_slice(t),
713 TokenEventPayload::Finish { reason } => {
714 finish = *reason;
715 done = true;
716 }
717 _ => {}
718 }
719 }
720 }
721 for ack in self.head.on_token_events(&m) {
722 self.endpoint.send(msg_type::WATERMARK_ACK, &ack)?;
723 }
724 }
725 }
726 if finish == u32::MAX {
727 expired = true;
728 }
729 self.head.retire_generation(session_id, generation_id);
730 Ok(FleetGeneration {
731 tokens,
732 finish,
733 expired,
734 })
735 }
736
737 pub fn wal(&self) -> &superfluid_linkf::WalStub {
738 self.head.wal()
739 }
740}
741
742pub struct PipelineFleet {
743 stages: Vec<FleetHead>,
744 boundaries: Vec<u32>,
745}
746
747impl PipelineFleet {
748 pub fn connect(nodes: Vec<(String, Vec<u8>)>, n_layers: u32) -> Result<PipelineFleet, DaemonError> {
749 if nodes.is_empty() {
750 return Err(DaemonError::Config("a pipeline needs at least one stage node"));
751 }
752 let p = nodes.len() as u32;
753 let boundaries: Vec<u32> = (0..p).map(|i| ((i + 1) * n_layers) / p).collect();
754 let mut stages = Vec::with_capacity(nodes.len());
755 for (addr, auth) in nodes {
756 stages.push(FleetHead::connect(&addr, auth)?);
757 }
758 Ok(PipelineFleet { stages, boundaries })
759 }
760
761 pub fn boundaries(&self) -> &[u32] {
762 &self.boundaries
763 }
764
765 fn forward_one(&mut self, context: &[u32]) -> Result<u32, DaemonError> {
766 let mut hidden: Option<Vec<u8>> = None;
767 let mut start = 0u32;
768 let n = self.stages.len();
769 for i in 0..n {
770 let end = self.boundaries[i];
771 let empty: Vec<u32> = Vec::new();
772 let tokens: &[u32] = if i == 0 { context } else { &empty };
773 match self.stages[i].forward_stage(tokens, start, end, hidden.take())? {
774 StageOutput::Hidden(h) => hidden = Some(h),
775 StageOutput::Token(t) => return Ok(t),
776 }
777 start = end;
778 }
779 Err(DaemonError::Protocol(
780 "pipeline finished every stage without a token — last stage must reach n_layers",
781 ))
782 }
783
784 pub fn generate(&mut self, context: &[u32], max_tokens: u32) -> Result<Vec<u32>, DaemonError> {
785 let mut ctx = context.to_vec();
786 let mut out = Vec::with_capacity(max_tokens as usize);
787 for _ in 0..max_tokens {
788 let tok = self.forward_one(&ctx)?;
789 out.push(tok);
790 ctx.push(tok);
791 }
792 Ok(out)
793 }
794}