Skip to main content

superfluid_daemon/
fleet.rs

1//! The head daemon's fleet edge.
2
3use 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
67/// Told each capacity a node reports, as the call that reads it, a generation included, reads it.
68pub type StatusHook = std::sync::Arc<dyn Fn(&superfluid_proto::handshake::CapacitySummary) + Send + Sync>;
69
70/// Epochs and command ids are numbered across every connection this head makes: a node keeps
71/// a session's across connections, and refuses an epoch or command it has already seen.
72static 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
87/// Dials `addr` and opens the link: encrypted and keyed by the token when there is one.
88fn 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
99/// The head's side of the handshake on a stream just opened to `peer`, dialed or accepted,
100/// with each way it can fail named for the operator; `mismatch` says what differs when the
101/// token is refused.
102fn 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    /// A node that dialed in; the head still opens the handshake, encrypted when it has a token.
142    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    /// How long the head waits on a peer that says nothing where its silence does not end the
169    /// wait (a version 2 node): the silence limit, or the default one when that is off.
170    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    /// When the head stops waiting on a generation: [`Self::grace`] past its own wall-clock
178    /// budget, as the node ends it at the budget, so one that has not by then never will.
179    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    /// The next frames the node sends, or `TimedOut` when none come `within`.
185    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    /// Keeps this connection, and the ones that replace it, alive as `liveness` says.
199    pub fn keep_alive(&mut self, liveness: Liveness) {
200        self.liveness = liveness;
201        self.endpoint.keep_alive(liveness);
202    }
203
204    /// Tells `hook` every status the node sends from now on, over this connection and the ones
205    /// that replace it.
206    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    /// Reads what the node sent while the connection sat idle, which also sends it a heartbeat
221    /// when one is due; an error means it is gone.
222    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    /// Takes what any read of the link can carry, a status or a lease renewal, and hands back
230    /// the frames left for the caller.
231    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        // A session the node still holds may have missed the last append, written into the
334        // connection that dropped; resend each one's latest content.
335        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(&params);
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    /// Ends the session on the node, which frees the context it kept. `epoch` is the one it was
411    /// last assigned, so this also reaches a session placed over another connection.
412    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            // A connection the node dropped while idle fails before anything streams; nothing
492            // reached the caller, so the generation can start over on a fresh one.
493            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            // One read at a time, not a wait for frames: a version 2 node is never given up for
538            // silence, so the deadline above is what ends a wait on one that went quiet.
539            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            // One read at a time, not a wait for frames: a version 2 node is never given up for
703            // silence, so the deadline above is what ends a wait on one that went quiet.
704            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}