1pub mod anthropic;
4pub mod api;
5pub mod apiweb;
6pub mod atem;
7pub mod bus;
8pub mod capabilities;
9pub mod cli;
10pub mod compat;
11pub mod codec;
12mod exec;
13pub mod completion_bucket;
14pub mod files;
15pub mod harmony;
16pub mod keypolicy;
17pub mod launch;
18pub mod batches;
19pub mod response_store;
20pub mod ratelimit;
21pub mod registry;
22pub mod template_codec;
23pub mod tool_fragment;
24pub mod fleet;
25pub mod fleet_manager;
26pub mod fleet_join;
27pub mod fleet_mdns;
28pub mod fleet_token;
29#[cfg(feature = "tui")]
30pub mod tui;
31#[cfg(feature = "tui")]
32pub mod tui_top;
33pub mod nodeagent;
34mod inference;
35mod block_inference;
36mod generation_output;
37mod openai_output;
38mod http_state;
39mod http_generation;
40mod keepalive;
41mod responses;
42pub mod openai;
43pub mod ollama;
44pub mod fleet_openai;
45pub mod park;
46pub mod pins;
47pub mod runtime;
48pub mod runtime_options;
49pub mod runtime_pick;
50pub mod installs;
51pub mod pulls;
52pub mod runtimes;
53pub mod runtimes_report;
54pub mod versions;
55pub mod selfupdate;
56pub mod linked;
57pub mod dylib_tokenizer;
58pub mod export;
59pub mod otlp;
60pub mod media;
61pub mod metrics;
62pub mod pressure;
63pub mod scheduler;
64pub mod serve;
65pub mod speculate_resolve;
66use superfluid_proto::linkw as proto_linkw;
67pub mod telemetry;
68pub mod store;
69pub mod store_id;
70pub mod wal;
71pub mod workerd;
72
73pub mod qos {
74 pub const INTERACTIVE_CHAT: u8 = 0;
75 pub const INLINE_COMPLETION: u8 = 1;
76 pub const FOREGROUND_AGENT: u8 = 2;
77 pub const BACKGROUND_AGENT: u8 = 3;
78
79 pub const NAMES: [&str; 4] = ["interactive", "completion", "agent", "background"];
80
81 pub fn parse(s: &str) -> Option<u8> {
82 match s.trim().to_ascii_lowercase().as_str() {
83 "interactive" | "interactive-chat" | "interactive_chat" | "0" => Some(INTERACTIVE_CHAT),
84 "completion" | "inline-completion" | "inline_completion" | "1" => Some(INLINE_COMPLETION),
85 "agent" | "foreground" | "foreground-agent" | "foreground_agent" | "2" => Some(FOREGROUND_AGENT),
86 "background" | "background-agent" | "background_agent" | "3" => Some(BACKGROUND_AGENT),
87 _ => None,
88 }
89 }
90
91 pub fn parse_class_lanes(spec: &str, max_lanes: usize) -> Result<[usize; 4], String> {
92 let mut caps = [0usize; 4];
93 let mut seen = [false; 4];
94 for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
95 let (name, n) = part
96 .split_once('=')
97 .ok_or_else(|| format!("expected class=N, got {part:?}"))?;
98 let class = parse(name).ok_or_else(|| {
99 format!("unknown class {:?} (expected one of {})", name.trim(), NAMES.join(", "))
100 })? as usize;
101 if seen[class] {
102 return Err(format!("class {} is capped twice", NAMES[class]));
103 }
104 seen[class] = true;
105 let n: usize = n
106 .trim()
107 .parse()
108 .map_err(|_| format!("cap for {} must be a whole number, got {:?}", NAMES[class], n.trim()))?;
109 if n == 0 {
110 return Err(format!(
111 "cap for {} must be at least 1 (omit the class to leave it uncapped)",
112 NAMES[class]
113 ));
114 }
115 if n > max_lanes {
116 return Err(format!(
117 "cap {n} for {} exceeds --max-batch {max_lanes}; it could never bind",
118 NAMES[class]
119 ));
120 }
121 caps[class] = n;
122 }
123 Ok(caps)
124 }
125
126 #[cfg(test)]
127 mod tests {
128 use super::*;
129
130 #[test]
131 fn names_round_trip_and_aliases_parse() {
132 for (c, name) in NAMES.iter().enumerate() {
133 assert_eq!(parse(name), Some(c as u8));
134 assert_eq!(parse(&c.to_string()), Some(c as u8));
135 }
136 assert_eq!(parse(" Interactive-Chat "), Some(INTERACTIVE_CHAT));
137 assert_eq!(parse("foreground_agent"), Some(FOREGROUND_AGENT));
138 assert_eq!(parse("urgent"), None);
139 assert_eq!(parse("4"), None);
140 }
141
142 #[test]
143 fn class_lanes_spec_parses_and_refuses_bad_caps() {
144 assert_eq!(parse_class_lanes("", 8), Ok([0; 4]));
145 assert_eq!(parse_class_lanes("background=2, agent=6", 8), Ok([0, 0, 6, 2]));
146 assert_eq!(parse_class_lanes("interactive=8", 8), Ok([8, 0, 0, 0]));
147 assert!(parse_class_lanes("background", 8).unwrap_err().contains("class=N"));
148 assert!(parse_class_lanes("urgent=1", 8).unwrap_err().contains("unknown class"));
149 assert!(parse_class_lanes("agent=-1", 8).unwrap_err().contains("whole number"));
150 assert!(parse_class_lanes("agent=9", 8).unwrap_err().contains("exceeds"));
151 assert!(parse_class_lanes("background=0", 8).unwrap_err().contains("at least 1"));
152 assert!(parse_class_lanes("agent=1,foreground=2", 8).unwrap_err().contains("twice"));
153 }
154 }
155}
156
157pub use codec::{MockCodec, NoCodec, TextCodec, Utf8Stream};
158pub use superfluid_engine::StageOutput;
159pub use runtime::{EngineHost, WorkerSpec};
160pub use store::{CommittedEvent, SessionStore};
161pub use wal::{EventBody, GenParams};
162
163pub use codec::BundleCodec;
164
165#[derive(Debug, thiserror::Error)]
166pub enum DaemonError {
167 #[error("io: {0}")]
168 Io(#[from] std::io::Error),
169 #[error("wal encode/decode: {0}")]
170 Codec(#[from] postcard::Error),
171 #[error("wal record failed checksum")]
172 WalCorrupt,
173 #[error("unknown session {0}")]
174 UnknownSession(u64),
175 #[error("session {0} has no content to generate from")]
176 EmptySession(u64),
177 #[error(
178 "this model's maximum context length is {max} tokens, but your request has {len} \
179 tokens; shorten the prompt or drop earlier turns"
180 )]
181 StreamTooLong { len: u64, max: u64 },
182 #[error("model load: {0}")]
183 EngineLoad(String),
184 #[error("engine link: {0}")]
185 Agent(#[from] superfluid_agent::AgentError),
186 #[error("fleet link (Link F): {0}")]
187 Fleet(#[from] superfluid_linkf::LinkFError),
188 #[error("fleet node {addr}: {why}")]
189 FleetNode { addr: String, why: String },
190 #[error("engine rejected the plan with status {0}")]
191 Rejected(i32),
192 #[error("generation failed: {0}")]
193 Generation(String),
194 #[error("session {0} is generating; append/generate after it finishes or cancel it")]
195 SessionBusy(u64),
196 #[error("tool call {0} is unknown or already closed")]
197 UnknownToolCall(u64),
198 #[error("unknown message role {0}")]
199 UnknownRole(u32),
200 #[error("tool {index} is not a function declaration: {why}")]
201 InvalidTool { index: usize, why: String },
202 #[error("session {0} already has a prompt; AppendSystem must come before any other turn")]
203 SystemNotFirst(u64),
204 #[error("fork point {fork_at} is outside session {session}'s committed events (1..={max})")]
205 ForkPoint { session: u64, fork_at: u64, max: u64 },
206 #[error("tool call {0} belongs to an ancestor's timeline; only that session can close it")]
207 InheritedToolCall(u64),
208 #[error("session {session} metadata version is {actual}, command expected {expected}")]
209 MetaConflict { session: u64, expected: u64, actual: u64 },
210 #[error("session {session} generation is {actual}, command expected {expected}")]
211 GenerationConflict { session: u64, expected: u64, actual: u64 },
212 #[error("session {0} was purged")]
213 Purged(u64),
214 #[error("files quota exceeded: {stored} bytes stored, {incoming} more would pass the {max} limit")]
215 FilesQuota { stored: u64, incoming: u64, max: u64 },
216 #[error("rebase edit spec invalid: {0}")]
217 RebaseSpec(&'static str),
218 #[error("session {0} did not quiesce for purge (lane still running)")]
219 PurgeTimeout(u64),
220 #[error("permission request {0} is unknown or already answered")]
221 UnknownPermission(u64),
222 #[error("session {0} has an open permission request; answer it before generating")]
223 PermissionPending(u64),
224 #[error("tool call {0}'s executor lease expired; the result was recorded as expired, not applied")]
225 ToolLeaseExpired(u64),
226 #[error("tool call {0} already reached a terminal outcome; the late result was recorded as a reconciliation")]
227 ToolLateEffect(u64),
228 #[error("unknown block kind {0}")]
229 UnknownBlockKind(u32),
230 #[error("media blob {0} is not in the pool (put it first)")]
231 UnknownMedia(String),
232 #[error("image expands to {0} placeholders, beyond one prefill chunk")]
233 MediaTooLarge(u32),
234 #[error("this codec has no FIM dialect")]
235 NoFimDialect,
236 #[error("completion rate limit exceeded; retry in {retry_after_ms} ms")]
237 CompletionThrottled { retry_after_ms: u64 },
238 #[error("protocol: {0}")]
239 Protocol(&'static str),
240 #[error("constraint unsatisfiable: {0}")]
241 Constraint(String),
242 #[error("the model's chat template refused this conversation: {0}")]
243 TemplateRefused(String),
244 #[error("{0}")]
245 Unsupported(Refusal),
246 #[error("configuration: {0}")]
247 Config(&'static str),
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct Refusal {
252 pub code: &'static str,
253 pub param: Option<String>,
254 pub message: String,
255}
256
257impl Refusal {
258 pub fn new(code: &'static str, param: Option<&str>, message: impl Into<String>) -> Refusal {
259 Refusal { code, param: param.map(str::to_string), message: message.into() }
260 }
261}
262
263impl std::fmt::Display for Refusal {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 f.write_str(&self.message)
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
270pub struct SessionSummary {
271 pub id: u64,
272 pub parent: Option<u64>,
273 pub fork_at: u64,
274 pub title: Option<String>,
275 pub archived: bool,
276 pub meta_version: u64,
277 pub generation: u64,
278 pub events: u64,
279 pub tokens: u64,
280 pub open_tool_calls: u64,
281}
282
283#[derive(Debug)]
284pub struct GenerateOutcome {
285 pub events: Vec<CommittedEvent>,
286 pub tokens_generated: u32,
287 pub finish: u32,
288 pub warm_prefix: u64,
289 pub logprobs: Vec<scheduler::TokenLogprob>,
290 pub spec: (u64, u64),
291}
292
293#[derive(Debug, Clone, Default, PartialEq)]
294pub struct ModelSamplingDefaults {
295 pub temperature: Option<f32>,
296 pub top_p: Option<f32>,
297 pub top_k: Option<u32>,
298 pub min_p: Option<f32>,
299 pub do_sample: Option<bool>,
300 pub repetition_penalty: Option<f32>,
301}
302
303impl ModelSamplingDefaults {
304 pub fn from_json(v: &serde_json::Value) -> Self {
305 let f = |k: &str| v.get(k).and_then(|x| x.as_f64()).map(|x| x as f32);
306 Self {
307 temperature: f("temperature"),
308 top_p: f("top_p"),
309 top_k: v.get("top_k").and_then(|x| x.as_u64()).map(|x| x as u32),
310 min_p: f("min_p"),
311 do_sample: v.get("do_sample").and_then(|x| x.as_bool()),
312 repetition_penalty: f("repetition_penalty"),
313 }
314 }
315}
316
317thread_local! {
318 static SYNC_CLASS: std::cell::Cell<Option<u8>> = const { std::cell::Cell::new(None) };
319}
320
321pub fn with_sync_class<R>(class: Option<u8>, f: impl FnOnce() -> R) -> R {
322 let _scope = SyncClassScope::enter(class);
323 f()
324}
325
326pub struct SyncClassScope {
327 prev: Option<Option<u8>>,
328 _not_send: std::marker::PhantomData<*const ()>,
329}
330
331impl SyncClassScope {
332 pub fn enter(class: Option<u8>) -> SyncClassScope {
333 let prev = class.map(|c| SYNC_CLASS.with(|cell| cell.replace(Some(c))));
334 SyncClassScope { prev, _not_send: std::marker::PhantomData }
335 }
336}
337
338impl Drop for SyncClassScope {
339 fn drop(&mut self) {
340 if let Some(prev) = self.prev {
341 SYNC_CLASS.with(|c| c.set(prev));
342 }
343 }
344}
345
346fn ambient_sync_class() -> Option<u8> {
347 SYNC_CLASS.with(|c| c.get())
348}
349
350struct Serving {
351 capabilities: runtime::SharedCapabilities,
352 retired: std::sync::Arc<std::sync::Mutex<Option<String>>>,
353 model_sampling: std::sync::OnceLock<ModelSamplingDefaults>,
354 codec: std::sync::Arc<dyn TextCodec + Send + Sync>,
355 sched: scheduler::SchedulerHandle,
356 speculation: std::sync::Arc<scheduler::Speculation>,
357 staged_tokens: u64,
358}
359
360#[derive(Default)]
364pub struct CancelSet {
365 set: std::sync::Mutex<std::collections::HashSet<u64>>,
366 wake: std::sync::RwLock<Option<runtime::YieldSlot>>,
367 latency_at_ms: std::sync::atomic::AtomicU64,
370}
371
372fn monotonic_ms() -> u64 {
375 static BASE: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
376 BASE.get_or_init(std::time::Instant::now).elapsed().as_millis() as u64 + 1
377}
378
379impl std::ops::Deref for CancelSet {
380 type Target = std::sync::Mutex<std::collections::HashSet<u64>>;
381
382 fn deref(&self) -> &Self::Target {
383 &self.set
384 }
385}
386
387impl CancelSet {
388 pub fn cancel(&self, session: u64) {
389 self.set.lock().expect("cancel registry").insert(session);
390 self.wake();
391 }
392
393 pub fn wake(&self) {
395 if let Some(slot) = self.wake.read().expect("wake slot").as_ref() {
396 runtime::raise_yield(slot);
397 }
398 }
399
400 pub(crate) fn wake_with(&self, slot: runtime::YieldSlot) {
401 *self.wake.write().expect("wake slot") = Some(slot);
402 }
403
404 pub fn latency_request(&self) {
407 self.latency_at_ms.store(monotonic_ms(), std::sync::atomic::Ordering::Release);
408 if let Some(slot) = self.wake.read().expect("wake slot").as_ref() {
409 runtime::set_latency(slot, true);
410 }
411 self.wake();
412 }
413
414 pub(crate) fn latency_recent(&self, hold: std::time::Duration) -> bool {
416 let at = self.latency_at_ms.load(std::sync::atomic::Ordering::Acquire);
417 at > 0 && monotonic_ms().saturating_sub(at) < hold.as_millis() as u64
418 }
419}
420
421struct Ground<'a> {
422 store: &'a std::sync::Arc<std::sync::Mutex<SessionStore>>,
423 cancels: &'a std::sync::Arc<CancelSet>,
424 active: &'a std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
425 stats: &'a std::sync::Arc<scheduler::SchedStats>,
426 background_tick_divisor: &'a std::sync::Arc<std::sync::atomic::AtomicU32>,
427}
428
429impl Serving {
430 fn start(
431 host: EngineHost,
432 codec: std::sync::Arc<dyn TextCodec + Send + Sync>,
433 ground: Ground<'_>,
434 opts: DaemonOptions,
435 ) -> Result<Serving, DaemonError> {
436 let mut opts = opts;
437 let mut speculation = std::sync::Arc::new(scheduler::Speculation::default());
438 let mut host = host;
439 ground.cancels.wake_with(host.yield_slot());
440 ground
441 .stats
442 .kv_bits
443 .store(host.kv_bits() as u64, std::sync::atomic::Ordering::Relaxed);
444 if let Some(first) = opts.speculate.clone() {
445 let mut candidates = vec![first];
446 if opts.speculate_auto {
447 candidates.extend(opts.speculate_fallbacks.iter().cloned());
448 }
449 let picked = first_registrable(
450 &candidates,
451 opts.speculate_auto,
452 |strategy| {
453 speculation = std::sync::Arc::new(scheduler::Speculation::default());
454 scheduler::Speculation::register(&speculation, &mut host, strategy, &opts.speculation)
455 },
456 |strategy, e, next| {
457 eprintln!(
458 "superfluid: speculation: auto picked {strategy}, but the engine refused it ({e}) — {}",
459 match next {
460 Some(n) => format!("trying {n}"),
461 None => "decoding plainly".to_string(),
462 }
463 );
464 },
465 )?;
466 if picked.is_none() {
467 speculation = std::sync::Arc::new(scheduler::Speculation::default());
468 } else if picked != opts.speculate {
469 eprintln!("superfluid: speculation: {}", picked.as_deref().unwrap_or_default());
470 }
471 opts.speculate = picked;
472 }
473 let staged_tokens = host.staged_tokens();
474 let capabilities = host.capabilities();
475 let retired = std::sync::Arc::new(std::sync::Mutex::new(None));
476 let sched = scheduler::Scheduler::spawn(
477 host,
478 std::sync::Arc::clone(ground.store),
479 std::sync::Arc::clone(&codec),
480 std::sync::Arc::clone(ground.cancels),
481 std::sync::Arc::clone(ground.active),
482 std::sync::Arc::clone(ground.stats),
483 std::sync::Arc::clone(ground.background_tick_divisor),
484 std::sync::Arc::clone(&speculation),
485 std::sync::Arc::clone(&retired),
486 opts,
487 );
488 {
489 let st = ground.store.lock().expect("store");
490 let now = wal::now_unix_ms();
491 for id in st.session_ids() {
492 let Ok(s) = st.session(id) else { continue };
493 if s.pin_deadline_unix_ms > now {
494 let (tokens, want) = pins::session_target(&s.tokens);
495 let _ = sched.tx.send(scheduler::SchedCmd::Pin(pins::PinReq {
496 key: pins::PinKey::Session(id),
497 tokens,
498 want,
499 deadline_unix_ms: s.pin_deadline_unix_ms,
500 anchor: Some(id),
501 reply: None,
502 }));
503 }
504 }
505 }
506 Ok(Serving { capabilities, retired, model_sampling: std::sync::OnceLock::new(), codec, sched, speculation, staged_tokens })
507 }
508}
509
510pub struct Daemon {
511 serving: std::sync::RwLock<std::sync::Arc<Serving>>,
512 reloading: std::sync::Mutex<()>,
513 reload_hook: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>>,
514 runtime_id: Option<&'static str>,
515 store: std::sync::Arc<std::sync::Mutex<SessionStore>>,
516 cancels: std::sync::Arc<CancelSet>,
517 active: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
518 purging: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
519 stats: std::sync::Arc<scheduler::SchedStats>,
520 trace_scope: otlp::TraceScope,
521 background_tick_divisor: std::sync::Arc<std::sync::atomic::AtomicU32>,
522 park_dir: Option<std::path::PathBuf>,
523 tool_lease_ms: u64,
524 media: media::MediaPool,
525 files: files::FileStore,
526 batches: std::sync::Arc<batches::BatchStore>,
527 responses: response_store::ResponseStore,
528 completion: std::sync::Arc<CompletionSatellite>,
529 fim_satellite: std::sync::OnceLock<FimSatellite>,
530 built_with: DaemonOptions,
531}
532
533impl CompletionSatellite {
534 fn cache_get(&self, key: u64) -> Option<(Vec<u32>, String, u32)> {
535 let mut c = self.cache.lock().expect("completion cache");
536 if let Some(v) = c.map.get(&key).cloned() {
537 c.lru.retain(|&k| k != key);
538 c.lru.push_back(key);
539 Some(v)
540 } else {
541 None
542 }
543 }
544 fn cache_put(&self, key: u64, tokens: Vec<u32>, text: String, finish: u32) {
545 let mut c = self.cache.lock().expect("completion cache");
546 if c.map.insert(key, (tokens, text, finish)).is_none() {
547 c.lru.push_back(key);
548 }
549 while c.lru.len() > self.cache_cap {
550 if let Some(old) = c.lru.pop_front() {
551 c.map.remove(&old);
552 }
553 }
554 }
555}
556
557struct FimSatellite {
558 name: String,
559 daemon: std::sync::Arc<Daemon>,
560}
561
562#[derive(Default)]
563struct CompletionCounters {
564 generated: std::sync::atomic::AtomicU64,
565 cached: std::sync::atomic::AtomicU64,
566 expired: std::sync::atomic::AtomicU64,
567 throttled: std::sync::atomic::AtomicU64,
568}
569
570struct CompletionSatellite {
571 store: std::sync::Mutex<SessionStore>,
572 cache: std::sync::Mutex<CompletionCache>,
573 deadline_ms: u64,
574 cache_cap: usize,
575 bucket: completion_bucket::BucketConfig,
576 counters: CompletionCounters,
577}
578
579#[derive(Default)]
580struct CompletionCache {
581 map: std::collections::HashMap<u64, (Vec<u32>, String, u32)>,
582 lru: std::collections::VecDeque<u64>,
583}
584
585#[derive(Debug, Clone, PartialEq)]
586pub struct Completion {
587 pub text: String,
588 pub tokens: Vec<u32>,
589 pub cached: bool,
590 pub expired: bool,
591 pub finish: u32,
592 pub prompt_tokens: u32,
593}
594
595#[derive(Debug, Clone)]
596pub struct FimRequest {
597 pub prefix: String,
598 pub suffix: String,
599 pub mode: u8,
600 pub max_tokens: u32,
601 pub params: GenParams,
602 pub extras: scheduler::GenExtras,
603}
604
605impl FimRequest {
606 pub fn greedy(prefix: &str, suffix: &str, mode: u8, max_tokens: u32) -> FimRequest {
607 FimRequest {
608 prefix: prefix.to_string(),
609 suffix: suffix.to_string(),
610 mode,
611 max_tokens,
612 params: GenParams::default(),
613 extras: scheduler::GenExtras::default(),
614 }
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
619pub enum Tier {
620 Resident,
621 Parked { covered: u64, encoding: u8 },
622 Cold,
623}
624
625#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
626pub struct Inspection {
627 pub summary: SessionSummary,
628 pub params: GenParams,
629 pub epoch: u64,
630 pub last_finish: u32,
631 pub qos_class: u8,
632 pub batch_invariant: bool,
633 pub pin_deadline_unix_ms: u64,
634 pub behavior_fingerprint: Option<u64>,
635 pub open_tool_calls: Vec<(u64, String, String)>,
636 pub tier: Tier,
637 pub tokens: Vec<u32>,
638 pub events: Vec<api::EventMsg>,
639 pub wal_version: u8,
640 #[serde(default, deserialize_with = "de_trailing_opt")]
641 pub store_id: Option<store::StoreId>,
642}
643
644fn de_trailing_opt<'de, D, T>(d: D) -> Result<Option<T>, D::Error>
645where
646 D: serde::Deserializer<'de>,
647 T: serde::Deserialize<'de>,
648{
649 use std::cell::Cell;
650 struct V<'a, T>(&'a Cell<bool>, std::marker::PhantomData<T>);
651 impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for V<'_, T> {
652 type Value = Option<T>;
653 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
654 f.write_str("an optional trailing field")
655 }
656 fn visit_none<E: serde::de::Error>(self) -> Result<Option<T>, E> {
657 self.0.set(true);
658 Ok(None)
659 }
660 fn visit_some<D2: serde::Deserializer<'de>>(self, d: D2) -> Result<Option<T>, D2::Error> {
661 self.0.set(true);
662 T::deserialize(d).map(Some)
663 }
664 }
665 let reached = Cell::new(false);
666 match d.deserialize_option(V(&reached, std::marker::PhantomData)) {
667 Ok(v) => Ok(v),
668 Err(e) if !reached.get() && e.to_string() == POSTCARD_UNEXPECTED_END => Ok(None),
669 Err(e) => Err(e),
670 }
671}
672
673const POSTCARD_UNEXPECTED_END: &str = "Hit the end of buffer, expected more data";
674
675#[derive(Debug, Clone, Default, PartialEq)]
676pub struct SpeculationOptions {
677 pub max_draft: Option<u32>,
678 pub adaptive: Option<bool>,
679 pub min_yield: Option<f64>,
680 pub yield_rounds: Option<u32>,
681 pub throughput_gate: Option<bool>,
682 pub gate_probe_tokens: Option<u32>,
683 pub min_speedup: Option<f64>,
684 pub gate_reprobe: Option<u32>,
685 pub gate_reprobe_max: Option<u32>,
686 pub dspark_confidence: Option<f32>,
687}
688
689#[derive(Debug, Clone)]
690pub struct DaemonOptions {
691 pub max_lanes: usize,
692 pub park_dir: Option<std::path::PathBuf>,
693 pub pressure_high_pct: u8,
694 pub pressure_low_pct: u8,
695 pub pin_budget_pct: u8,
696 pub park_lossy: bool,
697 pub park_budget_bytes: u64,
698 pub class_lanes: [usize; 4],
699 pub tick_decode_budget: u32,
700 pub tick_target_ms: u32,
701 pub runtime_id: Option<&'static str>,
702 pub prefill_budget: u32,
703 pub agent_starvation_ticks: u32,
704 pub pressure_source: pressure::PressureConfig,
705 pub completion_deadline_ms: u64,
706 pub completion_bucket: completion_bucket::BucketConfig,
707 pub media_dir: Option<std::path::PathBuf>,
708 pub files_max_bytes: Option<u64>,
709 pub files_expiry_secs: Option<u64>,
710 pub tool_lease_ms: u64,
711 pub speculate: Option<String>,
712 pub speculate_auto: bool,
713 pub speculate_fallbacks: Vec<String>,
714 pub speculation: SpeculationOptions,
715 pub trace_scope: otlp::TraceScope,
716}
717
718impl Default for DaemonOptions {
719 fn default() -> DaemonOptions {
720 DaemonOptions {
721 max_lanes: 8,
722 park_dir: None,
723 pressure_high_pct: 85,
724 pressure_low_pct: 70,
725 pin_budget_pct: 50,
726 park_lossy: false,
727 park_budget_bytes: 20 << 30,
728 class_lanes: [0; 4],
729 tick_decode_budget: 0,
730 tick_target_ms: 0,
731 runtime_id: None,
732 prefill_budget: scheduler::PREFILL_BUDGET,
733 agent_starvation_ticks: 8,
734 pressure_source: pressure::PressureConfig::None,
735 tool_lease_ms: 0,
736 speculate: None,
737 speculate_auto: false,
738 speculate_fallbacks: Vec::new(),
739 speculation: SpeculationOptions::default(),
740 files_max_bytes: None,
741 files_expiry_secs: None,
742 media_dir: None,
743 completion_deadline_ms: 0,
744 trace_scope: otlp::TraceScope::default(),
745 completion_bucket: completion_bucket::BucketConfig::default(),
746 }
747 }
748}
749
750struct PurgeFence {
751 set: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>>,
752 ids: Vec<u64>,
753}
754
755impl Drop for PurgeFence {
756 fn drop(&mut self) {
757 let mut p = self.set.lock().expect("purging");
758 for id in &self.ids {
759 p.remove(id);
760 }
761 }
762}
763
764pub(crate) fn render_refusal(
765 codec: &dyn TextCodec,
766 messages: &[codec::ChatMessage],
767 tools: &[String],
768 kwargs: &serde_json::Map<String, serde_json::Value>,
769 fallback: &'static str,
770) -> DaemonError {
771 match codec.template_refusal(messages, tools, kwargs) {
772 Some(why) => DaemonError::TemplateRefused(why),
773 None => DaemonError::Protocol(fallback),
774 }
775}
776
777impl Daemon {
778 pub fn new(
779 store: SessionStore,
780 host: EngineHost,
781 codec: Box<dyn TextCodec + Send + Sync>,
782 max_lanes: usize,
783 ) -> Daemon {
784 Daemon::with_park(store, host, codec, max_lanes, None)
785 }
786
787 pub fn with_park(
788 store: SessionStore,
789 host: EngineHost,
790 codec: Box<dyn TextCodec + Send + Sync>,
791 max_lanes: usize,
792 park_dir: Option<std::path::PathBuf>,
793 ) -> Daemon {
794 Daemon::with_options(
795 store,
796 host,
797 codec,
798 DaemonOptions {
799 max_lanes,
800 park_dir,
801 ..DaemonOptions::default()
802 },
803 )
804 .expect("default watermarks are valid")
805 }
806
807 pub fn with_options(
808 store: SessionStore,
809 host: EngineHost,
810 codec: Box<dyn TextCodec + Send + Sync>,
811 opts: DaemonOptions,
812 ) -> Result<Daemon, DaemonError> {
813 if opts.pressure_high_pct == 0 || opts.pressure_high_pct > 100 {
814 return Err(DaemonError::Config(
815 "pressure_high_pct must be in 1..=100",
816 ));
817 }
818 if opts.pressure_low_pct >= opts.pressure_high_pct {
819 return Err(DaemonError::Config(
820 "pressure_low_pct must be below pressure_high_pct",
821 ));
822 }
823 if opts.max_lanes == 0 {
824 return Err(DaemonError::Config("max_lanes must be at least 1"));
825 }
826 let mut opts = opts;
827 let runtime_id = opts.runtime_id;
828 opts.pin_budget_pct = opts.pin_budget_pct.min(opts.pressure_low_pct);
829 opts.completion_bucket.validate().map_err(DaemonError::Config)?;
830 if matches!(opts.speculation.max_draft, Some(0 | 16..)) {
831 return Err(DaemonError::Config("speculation max_draft must be in 1..=15"));
832 }
833 if opts.speculation.min_yield.is_some_and(|v| !v.is_finite() || v < 0.0) {
834 return Err(DaemonError::Config("speculation min_yield must be finite and non-negative"));
835 }
836 if opts.speculation.yield_rounds == Some(0) {
837 return Err(DaemonError::Config("speculation yield_rounds must be at least 1"));
838 }
839 if opts.speculation.min_speedup.is_some_and(|v| !v.is_finite() || v < 1.0) {
840 return Err(DaemonError::Config(
841 "speculation min_speedup must be finite and at least 1.0",
842 ));
843 }
844 if opts.speculation.dspark_confidence.is_some_and(|v| !v.is_finite() || v <= 0.0) {
845 return Err(DaemonError::Config(
846 "speculation dspark_confidence must be finite and positive",
847 ));
848 }
849 opts.trace_scope.ns = export::store_trace_ns(&store.store_id());
850 let store = std::sync::Arc::new(std::sync::Mutex::new(store));
851 let built_with = opts.clone();
852 let cancels = std::sync::Arc::new(CancelSet::default());
853 let active = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
854 let purging = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
855 let stats = std::sync::Arc::new(scheduler::SchedStats::default());
856 let background_tick_divisor = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(1));
857 let park_dir = opts.park_dir.clone();
858 let tool_lease_ms = opts.tool_lease_ms;
859 let files_max_bytes = opts.files_max_bytes;
860 let files_expiry_secs = opts.files_expiry_secs;
861 let completion_deadline_ms = opts.completion_deadline_ms;
862 let completion_bucket = opts.completion_bucket;
863 let media_dir = opts.media_dir.clone().unwrap_or_else(|| {
864 park_dir
865 .as_ref()
866 .and_then(|p| p.parent().map(|d| d.join("media")))
867 .unwrap_or_else(|| {
868 let uniq = std::time::SystemTime::now()
869 .duration_since(std::time::UNIX_EPOCH)
870 .map(|d| d.as_nanos())
871 .unwrap_or(0);
872 std::env::temp_dir().join(format!("superfluid-{}-{}", std::process::id(), uniq)).join("media")
873 })
874 });
875 let trace_scope = opts.trace_scope.clone();
876 let serving = Serving::start(
877 host,
878 std::sync::Arc::from(codec),
879 Ground {
880 store: &store,
881 cancels: &cancels,
882 active: &active,
883 stats: &stats,
884 background_tick_divisor: &background_tick_divisor,
885 },
886 opts,
887 )?;
888 Ok(Daemon {
889 trace_scope,
890 serving: std::sync::RwLock::new(std::sync::Arc::new(serving)),
891 reloading: std::sync::Mutex::new(()),
892 reload_hook: std::sync::OnceLock::new(),
893 runtime_id,
894 store,
895 cancels,
896 active,
897 purging,
898 stats,
899 background_tick_divisor,
900 park_dir,
901 tool_lease_ms,
902 files: files::FileStore::with_limits(
903 files::files_dir_for(media_dir.parent().unwrap_or(media_dir.as_path())),
904 files_max_bytes,
905 files_expiry_secs,
906 ),
907 batches: std::sync::Arc::new(batches::BatchStore::new(batches::batches_dir_for(
908 media_dir.parent().unwrap_or(media_dir.as_path()),
909 ))),
910 responses: response_store::ResponseStore::new(response_store::responses_dir_for(
911 media_dir.parent().unwrap_or(media_dir.as_path()),
912 )),
913 media: media::MediaPool::new(media_dir),
914 completion: std::sync::Arc::new(CompletionSatellite {
915 store: std::sync::Mutex::new(SessionStore::ephemeral()),
916 cache: std::sync::Mutex::new(CompletionCache::default()),
917 deadline_ms: completion_deadline_ms,
918 cache_cap: 256,
919 bucket: completion_bucket,
920 counters: CompletionCounters::default(),
921 }),
922 fim_satellite: std::sync::OnceLock::new(),
923 built_with,
924 })
925 }
926
927 fn serving(&self) -> std::sync::Arc<Serving> {
928 std::sync::Arc::clone(&self.serving.read().expect("serving"))
929 }
930
931 fn codec(&self) -> std::sync::Arc<dyn TextCodec + Send + Sync> {
932 std::sync::Arc::clone(&self.serving().codec)
933 }
934
935 fn sched_tx(&self) -> std::sync::mpsc::Sender<scheduler::SchedCmd> {
936 self.serving().sched.tx.clone()
937 }
938
939 pub fn reload_with(
940 &self,
941 build: impl FnOnce() -> Result<(EngineHost, Box<dyn TextCodec + Send + Sync>), DaemonError>,
942 ) -> Result<(), DaemonError> {
943 let _one_at_a_time = self.reloading.lock().expect("reloading");
944 self.shutdown();
945 {
946 let mut cancels = self.cancels.lock().expect("cancel registry");
947 let active = self.active.lock().expect("active set");
948 cancels.retain(|session| active.contains(session));
949 }
950 let started = build().and_then(|(host, codec)| {
951 Serving::start(
952 host,
953 std::sync::Arc::from(codec),
954 Ground {
955 store: &self.store,
956 cancels: &self.cancels,
957 active: &self.active,
958 stats: &self.stats,
959 background_tick_divisor: &self.background_tick_divisor,
960 },
961 self.built_with.clone(),
962 )
963 });
964 match started {
965 Ok(serving) => {
966 *self.serving.write().expect("serving") = std::sync::Arc::new(serving);
967 Ok(())
968 }
969 Err(e) => {
970 let stopped = self.serving();
971 let mut why = stopped.retired.lock().expect("retired");
972 if why.is_none() {
973 *why = Some(format!("the model's engine was stopped to load it again, which failed ({e}); it loads again on its next request, so retry"));
974 }
975 Err(e)
976 }
977 }
978 }
979
980 pub fn on_retired(&self, reload: Box<dyn Fn() + Send + Sync>) {
981 let _ = self.reload_hook.set(reload);
982 }
983
984 fn ensure_serving(&self) -> Result<(), DaemonError> {
985 if self.needs_reload().is_none() {
986 return Ok(());
987 }
988 if let Some(reload) = self.reload_hook.get() {
989 reload();
990 }
991 match self.needs_reload() {
992 None => Ok(()),
993 Some(why) => Err(DaemonError::Generation(why)),
994 }
995 }
996
997 pub fn max_stream_tokens(&self) -> u64 {
998 let serving = self.serving();
999 let record = serving.capabilities.read().expect("capability record");
1000 runtime::stream_ceiling(serving.staged_tokens, &record)
1001 }
1002
1003 fn check_stream_fits(&self, len: usize) -> Result<(), DaemonError> {
1004 let len = len as u64;
1005 let max = self.max_stream_tokens();
1006 if max > 0 && len > max {
1007 return Err(DaemonError::StreamTooLong { len, max });
1008 }
1009 Ok(())
1010 }
1011
1012 pub fn session_fits(&self, session: u64, thinking: Option<bool>) -> Result<(), DaemonError> {
1013 self.ensure_serving()?;
1014 let len = {
1015 let store = self.store.lock().expect("store");
1016 let s = store.session(session)?;
1017 s.tokens.len() + self.generation_opener(&s.events, thinking).map_or(0, |op| op.len())
1018 };
1019 self.check_stream_fits(len)
1020 }
1021
1022 pub fn fim_fits(&self, req: &FimRequest) -> Result<(), DaemonError> {
1023 let d = match self.fim_satellite.get() {
1024 Some(sat) => &sat.daemon,
1025 None => self,
1026 };
1027 d.ensure_serving()?;
1028 let fim = d
1029 .codec()
1030 .render_fim(&req.prefix, &req.suffix, req.mode)
1031 .ok_or(DaemonError::NoFimDialect)?;
1032 d.check_stream_fits(fim.len())
1033 }
1034
1035 pub fn complete(
1036 &self,
1037 prefix: &str,
1038 suffix: &str,
1039 mode: u8,
1040 max_tokens: u32,
1041 ) -> Result<Completion, DaemonError> {
1042 self.complete_streaming(&FimRequest::greedy(prefix, suffix, mode, max_tokens), |_, _| {
1043 std::ops::ControlFlow::Continue(())
1044 })
1045 }
1046
1047 pub fn complete_streaming(
1048 &self,
1049 req: &FimRequest,
1050 on_text: impl FnMut(&str, usize) -> std::ops::ControlFlow<()>,
1051 ) -> Result<Completion, DaemonError> {
1052 let result = match self.fim_satellite.get() {
1053 Some(sat) => sat.daemon.complete_here(req, on_text),
1054 None => self.complete_here(req, on_text),
1055 };
1056 if let Ok(c) = &result {
1057 use std::sync::atomic::Ordering::Relaxed;
1058 let n = &self.completion.counters;
1059 if c.expired {
1060 n.expired.fetch_add(1, Relaxed);
1061 } else if c.cached {
1062 n.cached.fetch_add(1, Relaxed);
1063 } else {
1064 n.generated.fetch_add(1, Relaxed);
1065 }
1066 }
1067 result
1068 }
1069
1070 pub fn attach_fim_satellite(
1071 &self,
1072 name: impl Into<String>,
1073 satellite: std::sync::Arc<Daemon>,
1074 ) -> Result<(), DaemonError> {
1075 if satellite.codec().render_fim("", "", codec::fim_mode::PSM).is_none() {
1076 return Err(DaemonError::NoFimDialect);
1077 }
1078 if satellite.fim_satellite.get().is_some() {
1079 return Err(DaemonError::Config("a FIM satellite cannot itself route to another"));
1080 }
1081 self.fim_satellite
1082 .set(FimSatellite {
1083 name: name.into(),
1084 daemon: satellite,
1085 })
1086 .map_err(|_| DaemonError::Config("a FIM satellite is already attached"))
1087 }
1088
1089 pub fn fim_satellite_name(&self) -> Option<String> {
1090 self.fim_satellite.get().map(|s| s.name.clone())
1091 }
1092
1093 pub fn fim_satellite(&self) -> Option<std::sync::Arc<Daemon>> {
1094 self.fim_satellite.get().map(|s| std::sync::Arc::clone(&s.daemon))
1095 }
1096
1097 pub fn fim_supported(&self) -> bool {
1098 self.fim_satellite.get().is_some()
1099 || self.codec().render_fim("", "", codec::fim_mode::PSM).is_some()
1100 }
1101
1102 pub fn completion_bucket(&self) -> completion_bucket::BucketConfig {
1103 self.completion.bucket
1104 }
1105
1106 pub fn note_completion_throttled(&self) {
1107 self.completion
1108 .counters
1109 .throttled
1110 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1111 }
1112
1113 fn complete_here(
1114 &self,
1115 req: &FimRequest,
1116 mut on_text: impl FnMut(&str, usize) -> std::ops::ControlFlow<()>,
1117 ) -> Result<Completion, DaemonError> {
1118 self.ensure_serving()?;
1119 let _span = self.raw_generate_span(req.max_tokens);
1120 let submitted = std::time::Instant::now();
1121 let fim = self
1122 .codec()
1123 .render_fim(&req.prefix, &req.suffix, req.mode)
1124 .ok_or(DaemonError::NoFimDialect)?;
1125 self.check_stream_fits(fim.len())?;
1126 let prompt_tokens = fim.len() as u32;
1127 let expired = || Completion {
1128 text: String::new(),
1129 tokens: Vec::new(),
1130 cached: false,
1131 expired: true,
1132 finish: superfluid_abi::finish::NONE,
1133 prompt_tokens,
1134 };
1135 let cacheable = req.params.temperature <= 0.0;
1136 let key = {
1137 let mut h = superfluid_fingerprint::RecordHasher::new();
1138 for t in &fim {
1139 h.field_u32(1, *t);
1140 }
1141 h.field_u32(2, req.mode as u32);
1142 h.field_u32(3, req.max_tokens);
1143 if let Some(fp) = self.codec().behavior_fingerprint() {
1144 h.field_u64(4, fp.digest());
1145 }
1146 let x = &req.extras;
1147 h.field_u32(5, x.presence_penalty.to_bits());
1148 h.field_u32(6, x.frequency_penalty.to_bits());
1149 h.field_u32(7, x.repeat_penalty.to_bits());
1150 h.field_u32(8, x.ignore_eos as u32);
1151 h.finish()
1152 };
1153 if cacheable {
1154 if let Some((tokens, text, finish)) = self.completion.cache_get(key) {
1155 let _ = on_text(&text, tokens.len());
1156 return Ok(Completion {
1157 text,
1158 tokens,
1159 cached: true,
1160 expired: false,
1161 finish,
1162 prompt_tokens,
1163 });
1164 }
1165 }
1166 if self.completion.deadline_ms > 0
1167 && submitted.elapsed().as_millis() as u64 > self.completion.deadline_ms
1168 {
1169 return Ok(expired());
1170 }
1171 let session = {
1172 let mut store = self.completion.store.lock().expect("completion store");
1173 let s = store.create(None, req.params)?;
1174 store.set_qos(s, qos::INLINE_COMPLETION, false)?;
1175 store.append(s, None, fim.clone())?;
1176 s
1177 };
1178 let (stream, params) = {
1179 let store = self.completion.store.lock().expect("completion store");
1180 let st = store.session(session)?;
1181 (st.tokens.clone(), st.params)
1182 };
1183 let mut produced: Vec<u32> = Vec::new();
1184 let mut emitted = 0usize;
1185 let result = scheduler::run_completion_streaming(
1186 &self.sched_tx(),
1187 stream,
1188 params,
1189 req.max_tokens,
1190 self.completion.deadline_ms,
1191 scheduler::GenExtras {
1192 grammar_handle: 0,
1193 logit_bias_handle: 0,
1194 want_logprobs: false,
1195 ..req.extras.clone()
1196 },
1197 |toks| {
1198 produced.extend_from_slice(toks);
1199 let text = self.codec().decode(&produced);
1200 let stable = text.trim_end_matches('\u{FFFD}');
1201 if stable.len() > emitted && stable.is_char_boundary(emitted) {
1202 let piece = &stable[emitted..];
1203 emitted = stable.len();
1204 return on_text(piece, produced.len());
1205 }
1206 std::ops::ControlFlow::Continue(())
1207 },
1208 );
1209 {
1210 let mut store = self.completion.store.lock().expect("completion store");
1211 let _ = store.drop_session(session);
1212 }
1213 let (tokens, finish, was_expired) = result?;
1214 if was_expired {
1215 return Ok(expired());
1216 }
1217 let text = self.codec().decode(&tokens);
1218 if text.len() > emitted && text.is_char_boundary(emitted) {
1219 let _ = on_text(&text[emitted..], tokens.len());
1220 }
1221 if cacheable && finish != superfluid_abi::finish::CANCELLED {
1222 self.completion.cache_put(key, tokens.clone(), text.clone(), finish);
1223 }
1224 Ok(Completion {
1225 text,
1226 tokens,
1227 cached: false,
1228 expired: false,
1229 finish,
1230 prompt_tokens,
1231 })
1232 }
1233
1234 pub fn generate_tokens(
1235 &self,
1236 stream: Vec<u32>,
1237 params: GenParams,
1238 max_tokens: u32,
1239 deadline_ms: u64,
1240 ) -> Result<(Vec<u32>, bool), DaemonError> {
1241 self.ensure_serving()?;
1242 let _span = self.raw_generate_span(max_tokens);
1243 self.check_stream_fits(stream.len())?;
1244 let deadline = self.effective_deadline(deadline_ms);
1245 let (tokens, _finish, expired) =
1246 scheduler::run_completion(&self.sched_tx(), stream, params, max_tokens, deadline, 0)?;
1247 Ok((tokens, expired))
1248 }
1249
1250 pub fn generate_tokens_streaming(
1251 &self,
1252 stream: Vec<u32>,
1253 params: GenParams,
1254 max_tokens: u32,
1255 deadline_ms: u64,
1256 extras: scheduler::GenExtras,
1257 on_tokens: impl FnMut(&[u32]) -> std::ops::ControlFlow<()>,
1258 ) -> Result<(Vec<u32>, u32, bool), DaemonError> {
1259 self.ensure_serving()?;
1260 let _span = self.raw_generate_span(max_tokens);
1261 self.check_stream_fits(stream.len())?;
1262 let deadline = self.effective_deadline(deadline_ms);
1263 scheduler::run_completion_streaming(
1264 &self.sched_tx(),
1265 stream,
1266 params,
1267 max_tokens,
1268 deadline,
1269 extras,
1270 on_tokens,
1271 )
1272 }
1273
1274 pub fn forward_stage(
1275 &self,
1276 tokens: Vec<u32>,
1277 start_layer: u32,
1278 end_layer: u32,
1279 hidden_in: Option<Vec<u8>>,
1280 ) -> Result<superfluid_engine::StageOutput, DaemonError> {
1281 self.ensure_serving()?;
1282 match self.engine_sync(proto_linkw::StateSyncReqMsg::ForwardStage {
1283 tokens,
1284 start_layer,
1285 end_layer,
1286 hidden_in,
1287 })? {
1288 proto_linkw::StateSyncOkMsg::ForwardStage {
1289 hidden: Some(h),
1290 token: None,
1291 } => Ok(superfluid_engine::StageOutput::Hidden(h)),
1292 proto_linkw::StateSyncOkMsg::ForwardStage {
1293 hidden: None,
1294 token: Some(t),
1295 } => Ok(superfluid_engine::StageOutput::Token(t)),
1296 _ => Err(DaemonError::Protocol("mismatched ForwardStage reply")),
1297 }
1298 }
1299
1300 fn effective_deadline(&self, deadline_ms: u64) -> u64 {
1301 if deadline_ms == 0 {
1302 self.completion.deadline_ms
1303 } else if self.completion.deadline_ms == 0 {
1304 deadline_ms
1305 } else {
1306 deadline_ms.min(self.completion.deadline_ms)
1307 }
1308 }
1309
1310 pub fn generate_tokens_constrained(
1311 &self,
1312 stream: Vec<u32>,
1313 params: GenParams,
1314 max_tokens: u32,
1315 deadline_ms: u64,
1316 json_schema: &str,
1317 ) -> Result<(Vec<u32>, bool), DaemonError> {
1318 self.ensure_serving()?;
1319 let _span = self.raw_generate_span(max_tokens);
1320 let handle = match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammar {
1321 json_schema: json_schema.to_string(),
1322 })? {
1323 proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => grammar_handle,
1324 _ => return Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
1325 };
1326 let deadline = self.effective_deadline(deadline_ms);
1327 let result =
1328 scheduler::run_completion(&self.sched_tx(), stream, params, max_tokens, deadline, handle);
1329 let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeGrammar {
1330 grammar_handle: handle,
1331 });
1332 let (tokens, finish, expired) = result?;
1333 if expired {
1334 return Err(DaemonError::Generation(format!(
1335 "constrained generation hit the wall deadline with the grammar incomplete ({} tokens)",
1336 tokens.len()
1337 )));
1338 }
1339 if finish != superfluid_abi::finish::GRAMMAR {
1340 return Err(DaemonError::Generation(format!(
1341 "constrained generation did not complete the grammar (finish={finish}, {} tokens); raise max_tokens",
1342 tokens.len()
1343 )));
1344 }
1345 Ok((tokens, false))
1346 }
1347
1348 pub fn put_media(&self, bytes: &[u8]) -> Result<String, DaemonError> {
1349 Ok(self.media.put(bytes)?)
1350 }
1351
1352 pub fn media_pool(&self) -> &media::MediaPool {
1353 &self.media
1354 }
1355
1356 fn engine_sync(&self, req: proto_linkw::StateSyncReqMsg) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1357 self.engine_sync_as(req, None)
1358 }
1359
1360 fn engine_sync_as(
1361 &self,
1362 req: proto_linkw::StateSyncReqMsg,
1363 class: Option<u8>,
1364 ) -> Result<proto_linkw::StateSyncOkMsg, DaemonError> {
1365 let class = class.or_else(ambient_sync_class);
1366 let (tx, rx) = std::sync::mpsc::channel();
1367 let cmd = scheduler::SchedCmd::Sync(req, tx);
1368 self.sched_tx()
1369 .send(match class {
1370 Some(c) => scheduler::SchedCmd::SyncAs(c, Box::new(cmd)),
1371 None => cmd,
1372 })
1373 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
1374 rx.recv()
1375 .map_err(|_| DaemonError::Protocol("scheduler dropped the query"))?
1376 .map_err(DaemonError::Generation)
1377 }
1378
1379 pub fn append_image(
1380 &self,
1381 session: u64,
1382 role: u32,
1383 blob: &str,
1384 pre_text: &str,
1385 post_text: &str,
1386 ) -> Result<CommittedEvent, DaemonError> {
1387 if self.active.lock().expect("active set").contains(&session) {
1388 return Err(DaemonError::SessionBusy(session));
1389 }
1390 if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
1391 return Err(DaemonError::UnknownRole(role));
1392 }
1393 if !self.media.exists(blob) {
1394 return Err(DaemonError::UnknownMedia(blob.to_string()));
1395 }
1396 let path = self.media.path_of(blob);
1397 let info = match self.engine_sync(proto_linkw::StateSyncReqMsg::MediaProbe {
1398 image_path: path.to_string_lossy().into_owned(),
1399 })? {
1400 proto_linkw::StateSyncOkMsg::MediaProbe { info } => info,
1401 _ => return Err(DaemonError::Protocol("mismatched MediaProbe reply")),
1402 };
1403 if info.n_tokens == 0 || info.n_tokens as u64 > scheduler::PREFILL_BUDGET as u64 {
1404 return Err(DaemonError::MediaTooLarge(info.n_tokens));
1405 }
1406 let mut run = Vec::with_capacity(info.n_tokens as usize + 2);
1407 if info.boi_token_id != 0 {
1408 run.push(info.boi_token_id);
1409 }
1410 run.extend(std::iter::repeat_n(info.image_token_id, info.n_tokens as usize));
1411 if info.eoi_token_id != 0 {
1412 run.push(info.eoi_token_id);
1413 }
1414 let mut span = self.separator_for(session);
1415 let turn = self
1416 .codec()
1417 .render_turn_with_tokens(role, pre_text, &run, post_text)
1418 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
1419 span.extend(turn);
1420 let first_in_span = span
1421 .iter()
1422 .position(|&t| t == info.image_token_id)
1423 .ok_or(DaemonError::Protocol("codec dropped the placeholder run"))?;
1424 let mut store = self.store.lock().expect("store");
1425 let stream_len = store.session(session)?.tokens.len() as u64;
1426 let payload = serde_json::json!({
1427 "blob": blob,
1428 "n_tokens": info.n_tokens,
1429 "image_token_id": info.image_token_id,
1430 "offset": stream_len + first_in_span as u64,
1431 "pre_text": pre_text,
1432 "post_text": post_text,
1433 "media_identity": {
1434 "preprocessing_fp": format!("{:016x}", info.preprocess_fp),
1435 "encoder": "bundle-weights-identity",
1436 },
1437 })
1438 .to_string();
1439 store.commit_block(
1440 session,
1441 role,
1442 wal::block_kind::IMAGE,
1443 payload,
1444 span,
1445 codec::MODEL_VISIBILITY_VERSION,
1446 )
1447 }
1448
1449 fn media_of(&self, s: &store::SessionState) -> Vec<(u64, std::path::PathBuf, u32)> {
1450 let mut v = Vec::new();
1451 for e in &s.events {
1452 if let EventBody::Block {
1453 kind, payload, ..
1454 } = &e.body
1455 {
1456 if *kind != wal::block_kind::IMAGE {
1457 continue;
1458 }
1459 let p: serde_json::Value = serde_json::from_str(payload).unwrap_or_default();
1460 let blob = p.get("blob").and_then(|b| b.as_str()).unwrap_or("");
1461 let off = p.get("offset").and_then(|o| o.as_u64()).unwrap_or(0);
1462 let n = p.get("n_tokens").and_then(|o| o.as_u64()).unwrap_or(0) as u32;
1463 if !blob.is_empty() && n > 0 {
1464 v.push((off, self.media.path_of(blob), n));
1465 }
1466 }
1467 }
1468 v
1469 }
1470
1471 pub fn gc_media(&self) -> Result<(usize, usize), DaemonError> {
1472 let store = self.store.lock().expect("store");
1473 let mut live = std::collections::HashSet::new();
1474 for id in store.session_ids() {
1475 let Ok(s) = store.session(id) else { continue };
1476 for e in &s.events {
1477 if let EventBody::Block { kind, payload, .. } = &e.body {
1478 if *kind == wal::block_kind::IMAGE {
1479 let p: serde_json::Value = serde_json::from_str(payload).unwrap_or_default();
1480 if let Some(b) = p.get("blob").and_then(|b| b.as_str()) {
1481 live.insert(b.to_string());
1482 }
1483 }
1484 }
1485 }
1486 }
1487 Ok(self.media.sweep(&live)?)
1488 }
1489
1490 pub fn speculation(&self) -> Option<scheduler::SpeculationGrant> {
1491 self.serving().speculation.grant()
1492 }
1493
1494 pub fn tool_lease_ms(&self) -> u64 {
1495 self.tool_lease_ms
1496 }
1497
1498 pub fn inspect(&self, session: u64) -> Result<Inspection, DaemonError> {
1499 let resident = self.active.lock().expect("active set").contains(&session);
1501 let store = self.store.lock().expect("store");
1502 let s = store.session(session)?;
1503 let tier = if resident {
1504 Tier::Resident
1505 } else {
1506 match self
1507 .park_dir
1508 .as_deref()
1509 .and_then(|d| park::read(d, session))
1510 {
1511 Some(p) => Tier::Parked {
1512 covered: p.covered,
1513 encoding: p.encoding,
1514 },
1515 None => Tier::Cold,
1516 }
1517 };
1518 Ok(Inspection {
1519 summary: SessionSummary {
1520 id: s.id,
1521 parent: if s.rerooted { None } else { s.parent },
1522 fork_at: s.base,
1523 title: s.title.clone(),
1524 archived: s.archived,
1525 meta_version: s.meta_version,
1526 generation: s.generation,
1527 events: s.events.len() as u64,
1528 tokens: s.tokens.len() as u64,
1529 open_tool_calls: s.open_tool_calls.len() as u64,
1530 },
1531 params: s.params,
1532 epoch: s.epoch,
1533 last_finish: s.last_finish,
1534 qos_class: s.qos_class,
1535 batch_invariant: s.batch_invariant,
1536 pin_deadline_unix_ms: s.pin_deadline_unix_ms,
1537 behavior_fingerprint: s.behavior_fp,
1538 open_tool_calls: s
1539 .open_tool_calls
1540 .iter()
1541 .map(|(id, (n, a))| (*id, n.clone(), a.clone()))
1542 .collect(),
1543 tier,
1544 tokens: s.tokens.clone(),
1545 events: s.events.iter().cloned().map(Into::into).collect(),
1546 wal_version: store.wal_version(),
1547 store_id: Some(store.store_id()),
1548 })
1549 }
1550
1551 pub fn files(&self) -> &files::FileStore {
1552 &self.files
1553 }
1554
1555 pub fn batches(&self) -> std::sync::Arc<batches::BatchStore> {
1556 std::sync::Arc::clone(&self.batches)
1557 }
1558
1559 pub fn responses(&self) -> &response_store::ResponseStore {
1560 &self.responses
1561 }
1562
1563 pub fn tokenize(&self, text: &str) -> Vec<u32> {
1564 self.codec().encode(text)
1565 }
1566
1567 pub fn grammar_create(&self, json_schema: &str) -> Result<u32, DaemonError> {
1568 match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammar {
1569 json_schema: json_schema.to_string(),
1570 })? {
1571 proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => Ok(grammar_handle),
1572 _ => Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
1573 }
1574 }
1575
1576 pub fn grammar_free(&self, grammar_handle: u32) {
1577 let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeGrammar { grammar_handle });
1578 }
1579
1580 pub fn logit_bias_create(&self, tokens: &[i32], values: &[f32]) -> Result<u32, DaemonError> {
1581 match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateLogitBias {
1582 tokens: tokens.to_vec(),
1583 values: values.iter().map(|v| v.to_bits()).collect(),
1584 })? {
1585 proto_linkw::StateSyncOkMsg::CreateLogitBias { handle } => Ok(handle),
1586 _ => Err(DaemonError::Protocol("mismatched CreateLogitBias reply")),
1587 }
1588 }
1589 pub fn logit_bias_free(&self, handle: u32) {
1590 let _ = self.engine_sync(proto_linkw::StateSyncReqMsg::FreeLogitBias { handle });
1591 }
1592
1593 pub fn lora_load(&self, adapter_path: &str) -> Result<Option<String>, DaemonError> {
1594 self.lora_reply(proto_linkw::StateSyncReqMsg::LoraLoad {
1595 adapter_path: adapter_path.to_string(),
1596 })
1597 }
1598
1599 pub fn lora_unload(&self) -> Result<Option<String>, DaemonError> {
1600 self.lora_reply(proto_linkw::StateSyncReqMsg::LoraUnload)
1601 }
1602
1603 pub fn lora_id(&self) -> Result<Option<String>, DaemonError> {
1604 self.lora_reply(proto_linkw::StateSyncReqMsg::LoraId)
1605 }
1606
1607 fn lora_reply(&self, req: proto_linkw::StateSyncReqMsg) -> Result<Option<String>, DaemonError> {
1608 match self.engine_sync(req)? {
1609 proto_linkw::StateSyncOkMsg::LoraId { id } => Ok(id),
1610 _ => Err(DaemonError::Protocol("mismatched LoraId reply")),
1611 }
1612 }
1613
1614 pub fn try_model_sampling_defaults(&self) -> Option<ModelSamplingDefaults> {
1615 let serving = self.serving();
1616 if let Some(cached) = serving.model_sampling.get() {
1617 return Some(cached.clone());
1618 }
1619 let descriptor = self.capability_descriptor()?;
1620 let resolved = descriptor
1621 .get("sampling_defaults")
1622 .map(ModelSamplingDefaults::from_json)
1623 .unwrap_or_default();
1624 let _ = serving.model_sampling.set(resolved.clone());
1625 Some(resolved)
1626 }
1627
1628 pub fn model_sampling_defaults(&self) -> ModelSamplingDefaults {
1629 self.try_model_sampling_defaults().unwrap_or_default()
1630 }
1631
1632 pub fn capability_descriptor(&self) -> Option<serde_json::Value> {
1633 self.serving().capabilities.read().expect("capability record").with_runtime(self.runtime_id)
1634 }
1635
1636 pub fn runtime_id(&self) -> Option<&'static str> {
1637 self.runtime_id
1638 }
1639
1640 pub fn needs_reload(&self) -> Option<String> {
1641 self.serving().retired.lock().expect("retired").clone()
1642 }
1643
1644 pub fn capabilities(&self) -> capabilities::Capabilities {
1645 self.serving().capabilities.read().expect("capability record").clone()
1646 }
1647
1648 pub fn require(&self, section: &str, key: &str, code: &'static str, param: Option<&str>, what: &str) -> Result<(), DaemonError> {
1649 let why = match self.serving().capabilities.read().expect("capability record").get(section, key) {
1650 capabilities::Cap::No(why) => why.to_string(),
1651 capabilities::Cap::Yes | capabilities::Cap::Unknown => return Ok(()),
1652 };
1653 Err(DaemonError::Unsupported(Refusal::new(code, param, format!("{} {what}: {why}", self.described()))))
1654 }
1655
1656 fn described(&self) -> String {
1657 let model = &self.trace_scope.model;
1658 let runtime = self.capability_descriptor().and_then(|r| r["runtime"]["id"].as_str().map(str::to_string));
1659 match (model.is_empty(), runtime) {
1660 (false, Some(r)) => format!("model '{model}' on runtime {r}"),
1661 (false, None) => format!("model '{model}'"),
1662 (true, Some(r)) => format!("this model (runtime {r})"),
1663 (true, None) => "this model".to_string(),
1664 }
1665 }
1666
1667 pub fn chat_template(&self) -> Option<String> {
1668 self.codec().chat_template_source()
1669 }
1670
1671 pub fn transcribe(
1672 &self,
1673 audio_path: &str,
1674 params: &superfluid_engine::TranscribeParams,
1675 ) -> Result<superfluid_engine::Transcription, DaemonError> {
1676 self.transcribe_as(audio_path, params, None)
1677 }
1678
1679 pub fn transcribe_as(
1680 &self,
1681 audio_path: &str,
1682 params: &superfluid_engine::TranscribeParams,
1683 class: Option<u8>,
1684 ) -> Result<superfluid_engine::Transcription, DaemonError> {
1685 self.ensure_serving()?;
1686 self.require_transcription(params)?;
1687 Self::decode_transcription(self.engine_sync_as(Self::transcribe_req(audio_path, params, false), class)?)
1688 }
1689
1690 fn require_transcription(&self, params: &superfluid_engine::TranscribeParams) -> Result<(), DaemonError> {
1691 if params.translate {
1692 self.require("modalities", "whisper_translate", "unsupported_model", Some("model"), "does not translate speech")
1693 } else {
1694 self.require("modalities", "whisper_transcribe", "unsupported_model", Some("model"), "does not transcribe speech")
1695 }
1696 }
1697
1698 #[allow(clippy::type_complexity)]
1699 pub fn transcribe_streaming(
1700 &self,
1701 audio_path: &str,
1702 params: &superfluid_engine::TranscribeParams,
1703 ) -> Result<
1704 (
1705 std::sync::mpsc::Receiver<proto_linkw::TranscribeSegmentMsg>,
1706 std::sync::mpsc::Receiver<Result<superfluid_engine::Transcription, DaemonError>>,
1707 ),
1708 DaemonError,
1709 > {
1710 self.transcribe_streaming_as(audio_path, params, None)
1711 }
1712
1713 #[allow(clippy::type_complexity)]
1714 pub fn transcribe_streaming_as(
1715 &self,
1716 audio_path: &str,
1717 params: &superfluid_engine::TranscribeParams,
1718 class: Option<u8>,
1719 ) -> Result<
1720 (
1721 std::sync::mpsc::Receiver<proto_linkw::TranscribeSegmentMsg>,
1722 std::sync::mpsc::Receiver<Result<superfluid_engine::Transcription, DaemonError>>,
1723 ),
1724 DaemonError,
1725 > {
1726 self.ensure_serving()?;
1727 self.require_transcription(params)?;
1728 let (seg_tx, seg_rx) = std::sync::mpsc::channel();
1729 let (res_tx, res_rx) = std::sync::mpsc::channel();
1730 let (raw_tx, raw_rx) = std::sync::mpsc::channel();
1731 self.sched_tx()
1732 .send({
1733 let cmd = scheduler::SchedCmd::SyncStream(Self::transcribe_req(audio_path, params, true), seg_tx, raw_tx);
1734 match class.or_else(ambient_sync_class) {
1735 Some(c) => scheduler::SchedCmd::SyncAs(c, Box::new(cmd)),
1736 None => cmd,
1737 }
1738 })
1739 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
1740 std::thread::spawn(move || {
1741 let out = match raw_rx.recv() {
1742 Ok(Ok(ok)) => Self::decode_transcription(ok),
1743 Ok(Err(e)) => Err(DaemonError::Generation(e)),
1744 Err(_) => Err(DaemonError::Protocol("scheduler dropped the query")),
1745 };
1746 let _ = res_tx.send(out);
1747 });
1748 Ok((seg_rx, res_rx))
1749 }
1750
1751 fn transcribe_req(
1752 audio_path: &str,
1753 params: &superfluid_engine::TranscribeParams,
1754 stream: bool,
1755 ) -> proto_linkw::StateSyncReqMsg {
1756 proto_linkw::StateSyncReqMsg::Transcribe {
1757 audio_path: audio_path.to_string(),
1758 language: params.language.clone(),
1759 translate: params.translate,
1760 timestamps: params.timestamps,
1761 prompt: params.prompt.clone(),
1762 stream,
1763 }
1764 }
1765
1766 fn decode_transcription(
1767 ok: proto_linkw::StateSyncOkMsg,
1768 ) -> Result<superfluid_engine::Transcription, DaemonError> {
1769 match ok {
1770 proto_linkw::StateSyncOkMsg::Transcribe { text, language, duration_ms, segments } => {
1771 Ok(superfluid_engine::Transcription {
1772 text,
1773 language,
1774 duration_ms,
1775 segments: segments
1776 .into_iter()
1777 .map(|s| superfluid_engine::TranscriptSegment {
1778 start_ms: s.start_ms,
1779 end_ms: s.end_ms,
1780 text: s.text,
1781 avg_logprob: f32::from_bits(s.avg_logprob_bits),
1782 no_speech_prob: f32::from_bits(s.no_speech_prob_bits),
1783 compression_ratio: f32::from_bits(s.compression_ratio_bits),
1784 temperature: f32::from_bits(s.temperature_bits),
1785 })
1786 .collect(),
1787 })
1788 }
1789 _ => Err(DaemonError::Protocol("engine returned a non-Transcribe reply")),
1790 }
1791 }
1792
1793 pub fn embed(&self, text: &str) -> Result<Vec<f32>, DaemonError> {
1794 self.embed_as(text, None)
1795 }
1796
1797 pub fn embed_as(&self, text: &str, class: Option<u8>) -> Result<Vec<f32>, DaemonError> {
1798 self.ensure_serving()?;
1799 self.require("workload", "embedding", "unsupported_model", Some("model"), "serves no embeddings")?;
1800 self.embed_tokens_as(self.codec().encode(text), class)
1801 }
1802
1803 pub(crate) fn embed_tokens_as(&self, tokens: Vec<u32>, class: Option<u8>) -> Result<Vec<f32>, DaemonError> {
1804 self.ensure_serving()?;
1805 self.require("workload", "embedding", "unsupported_model", Some("model"), "serves no embeddings")?;
1806 match self.engine_sync_as(proto_linkw::StateSyncReqMsg::Embed { tokens }, class)? {
1807 proto_linkw::StateSyncOkMsg::Embed { embedding } => Ok(embedding
1808 .as_chunks::<4>()
1809 .0
1810 .iter()
1811 .map(|b| f32::from_le_bytes(*b))
1812 .collect()),
1813 _ => Err(DaemonError::Protocol("engine returned a non-Embed reply")),
1814 }
1815 }
1816
1817 pub fn detokenize(&self, tokens: &[u32]) -> String {
1818 self.codec().decode(tokens)
1819 }
1820
1821 fn raw_generate_span(&self, max_tokens: u32) -> tracing::span::EnteredSpan {
1822 tracing::info_span!(
1823 parent: None,
1824 "generate",
1825 max_tokens,
1826 raw = true,
1827 model = self.trace_scope.model.as_str(),
1828 store_ns = self.trace_scope.ns
1829 )
1830 .entered()
1831 }
1832
1833 pub fn metrics_text(&self) -> String {
1834 let mut out = metrics::render_prometheus(&self.stats);
1835 out.push_str(&format!(
1836 "# HELP superfluid_telemetry_dropped_total Telemetry records dropped by the bounded sinks (log lines and OTLP spans).\n# TYPE superfluid_telemetry_dropped_total counter\nsuperfluid_telemetry_dropped_total {}\n",
1837 telemetry::dropped_total()
1838 ));
1839 let g = |a: &std::sync::atomic::AtomicU64| a.load(std::sync::atomic::Ordering::Relaxed);
1840 out.push_str(&format!(
1841 "# HELP superfluid_otlp_spans_exported_total Operational spans accepted by the OTLP collector.\n# TYPE superfluid_otlp_spans_exported_total counter\nsuperfluid_otlp_spans_exported_total {}\n",
1842 g(&otlp::EXPORTED)
1843 ));
1844 out.push_str(&format!(
1845 "# HELP superfluid_otlp_spans_dropped_total Operational spans dropped instead of delaying a tick.\n# TYPE superfluid_otlp_spans_dropped_total counter\nsuperfluid_otlp_spans_dropped_total{{reason=\"queue_full\"}} {}\nsuperfluid_otlp_spans_dropped_total{{reason=\"export_failed\"}} {}\nsuperfluid_otlp_spans_dropped_total{{reason=\"rejected\"}} {}\n",
1846 g(&otlp::DROPPED_QUEUE_FULL),
1847 g(&otlp::DROPPED_EXPORT_FAILED),
1848 g(&otlp::DROPPED_REJECTED)
1849 ));
1850 out.push_str(&self.completion_metrics_text());
1851 out
1852 }
1853
1854 fn completion_metrics_text(&self) -> String {
1855 use std::sync::atomic::Ordering::Relaxed;
1856 let n = &self.completion.counters;
1857 let mut out = String::from(
1858 "# HELP superfluid_fim_completions_total FIM completions by outcome.\n\
1859 # TYPE superfluid_fim_completions_total counter\n",
1860 );
1861 for (label, v) in [
1862 ("generated", &n.generated),
1863 ("cached", &n.cached),
1864 ("expired", &n.expired),
1865 ("throttled", &n.throttled),
1866 ] {
1867 out.push_str(&format!(
1868 "superfluid_fim_completions_total{{result=\"{label}\"}} {}\n",
1869 v.load(Relaxed)
1870 ));
1871 }
1872 if let Some(sat) = self.fim_satellite.get() {
1873 let s = &sat.daemon.stats;
1874 let model = sat.name.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
1875 out.push_str(&format!(
1876 "# HELP superfluid_fim_satellite_info The --fim-model satellite serving completions.\n\
1877 # TYPE superfluid_fim_satellite_info gauge\n\
1878 superfluid_fim_satellite_info{{model=\"{model}\"}} 1\n\
1879 # HELP superfluid_fim_satellite_decode_tokens_total Tokens the satellite decoded.\n\
1880 # TYPE superfluid_fim_satellite_decode_tokens_total counter\n\
1881 superfluid_fim_satellite_decode_tokens_total {}\n\
1882 # HELP superfluid_fim_satellite_lanes_active Satellite lanes resident after its last tick.\n\
1883 # TYPE superfluid_fim_satellite_lanes_active gauge\n\
1884 superfluid_fim_satellite_lanes_active {}\n\
1885 # HELP superfluid_fim_satellite_pool_blocks_used Satellite KV paged blocks in use.\n\
1886 # TYPE superfluid_fim_satellite_pool_blocks_used gauge\n\
1887 superfluid_fim_satellite_pool_blocks_used {}\n\
1888 # HELP superfluid_fim_satellite_pool_blocks_total Satellite KV paged blocks total.\n\
1889 # TYPE superfluid_fim_satellite_pool_blocks_total gauge\n\
1890 superfluid_fim_satellite_pool_blocks_total {}\n",
1891 s.decode_tokens.load(Relaxed),
1892 s.lanes_active.load(Relaxed),
1893 s.pool_blocks_used.load(Relaxed),
1894 s.pool_blocks_total.load(Relaxed),
1895 ));
1896 }
1897 out
1898 }
1899
1900 pub fn set_background_tick_divisor(&self, divisor: u32) {
1901 self.background_tick_divisor
1902 .store(divisor.max(1), std::sync::atomic::Ordering::Relaxed);
1903 }
1904
1905 pub fn set_qos(
1906 &self,
1907 session: u64,
1908 class: u8,
1909 batch_invariant: bool,
1910 ) -> Result<CommittedEvent, DaemonError> {
1911 self.store
1912 .lock()
1913 .expect("store")
1914 .set_qos(session, class, batch_invariant)
1915 }
1916
1917 pub fn sched_stats(&self) -> std::sync::Arc<scheduler::SchedStats> {
1918 std::sync::Arc::clone(&self.stats)
1919 }
1920
1921 pub fn cancel_registry(&self) -> std::sync::Arc<CancelSet> {
1922 std::sync::Arc::clone(&self.cancels)
1923 }
1924
1925 pub fn active_registry(
1926 &self,
1927 ) -> std::sync::Arc<std::sync::Mutex<std::collections::HashSet<u64>>> {
1928 std::sync::Arc::clone(&self.active)
1929 }
1930
1931 pub fn store(&self) -> std::sync::Arc<std::sync::Mutex<SessionStore>> {
1932 std::sync::Arc::clone(&self.store)
1933 }
1934
1935 pub fn bus(&self) -> std::sync::Arc<bus::SessionBus> {
1936 self.store.lock().expect("store").bus()
1937 }
1938
1939 pub fn shutdown(&self) {
1940 self.serving().sched.shutdown();
1941 }
1942
1943 pub fn signal_shutdown(&self) {
1944 self.serving().sched.signal_shutdown();
1945 }
1946
1947 pub fn create(&self, parent: Option<u64>, params: GenParams) -> Result<u64, DaemonError> {
1948 self.store.lock().expect("store").create(parent, params)
1949 }
1950
1951 pub fn fork(
1952 &self,
1953 parent: u64,
1954 at_event: u64,
1955 params: Option<GenParams>,
1956 ) -> Result<u64, DaemonError> {
1957 self.refuse_if_purging(parent)?;
1958 self.store.lock().expect("store").fork(parent, at_event, params)
1959 }
1960
1961 pub fn tree(&self) -> Vec<(u64, Option<u64>, u64)> {
1962 self.sessions(false)
1963 .into_iter()
1964 .map(|s| (s.id, s.parent, s.fork_at))
1965 .collect()
1966 }
1967
1968 pub fn sessions(&self, include_archived: bool) -> Vec<SessionSummary> {
1969 let store = self.store.lock().expect("store");
1970 store
1971 .session_ids()
1972 .into_iter()
1973 .filter_map(|id| store.session(id).ok())
1974 .filter(|s| include_archived || !s.archived)
1975 .map(|s| SessionSummary {
1976 id: s.id,
1977 parent: if s.rerooted { None } else { s.parent },
1978 fork_at: s.base,
1979 title: s.title.clone(),
1980 archived: s.archived,
1981 meta_version: s.meta_version,
1982 generation: s.generation,
1983 events: s.events.len() as u64,
1984 tokens: s.tokens.len() as u64,
1985 open_tool_calls: s.open_tool_calls.len() as u64,
1986 })
1987 .collect()
1988 }
1989
1990 pub fn set_meta(
1991 &self,
1992 session: u64,
1993 expected_version: u64,
1994 title: Option<String>,
1995 archived: Option<bool>,
1996 ) -> Result<u64, DaemonError> {
1997 self.store
1998 .lock()
1999 .expect("store")
2000 .set_meta(session, expected_version, title, archived)
2001 }
2002
2003 pub fn rebase(
2004 &self,
2005 parent: u64,
2006 edits: Vec<wal::RebaseEdit>,
2007 params: Option<GenParams>,
2008 ) -> Result<u64, DaemonError> {
2009 use wal::RebaseEdit;
2010 if edits.is_empty() {
2011 return Err(DaemonError::RebaseSpec("no edits"));
2012 }
2013 self.refuse_if_purging(parent)?;
2014 let range = |e: &RebaseEdit| match e {
2015 RebaseEdit::Drop { from, to } | RebaseEdit::Replace { from, to, .. } => (*from, *to),
2016 };
2017 let mut store = self.store.lock().expect("store");
2018 let (max, parent_events) = {
2019 let p = store.session(parent)?;
2020 (p.next_event_id(), p.events.clone())
2021 };
2022 let mut last_to = 0;
2023 for e in &edits {
2024 let (from, to) = range(e);
2025 if from == 0 {
2026 return Err(DaemonError::RebaseSpec("edits never include Created (id 0)"));
2027 }
2028 if from >= to {
2029 return Err(DaemonError::RebaseSpec("empty range"));
2030 }
2031 if to > max {
2032 return Err(DaemonError::RebaseSpec("range beyond the parent's committed events"));
2033 }
2034 if from < last_to {
2035 return Err(DaemonError::RebaseSpec("ranges must be sorted and non-overlapping"));
2036 }
2037 if let RebaseEdit::Replace { role, .. } = e {
2038 if !matches!(*role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
2039 return Err(DaemonError::UnknownRole(*role));
2040 }
2041 }
2042 last_to = to;
2043 }
2044 let first = range(&edits[0]).0;
2045 let child = store.rebase_branch(parent, first, params, edits.clone())?;
2046 let mut tool_ids: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
2047 let mut i = first as usize;
2048 while i < parent_events.len() {
2049 let id = i as u64;
2050 if let Some(e) = edits.iter().find(|e| {
2051 let (from, to) = range(e);
2052 id >= from && id < to
2053 }) {
2054 let (from, to) = range(e);
2055 if let RebaseEdit::Replace { role, text, .. } = e {
2056 if id == from {
2057 let span = self.render_for(&store, child, *role, text)?;
2058 store.append_message(child, *role, text.clone(), span)?;
2059 }
2060 }
2061 i = to as usize;
2062 continue;
2063 }
2064 match &parent_events[i].body {
2065 EventBody::Appended { text, span } => {
2066 store.append(child, text.clone(), span.clone())?;
2067 }
2068 EventBody::Message { role, text, span } => {
2069 store.append_message(child, *role, text.clone(), span.clone())?;
2070 }
2071 EventBody::GenerationPrompt { span } => {
2072 store.commit_generation_prompt(child, span.clone())?;
2073 }
2074 EventBody::Generated {
2075 span,
2076 text,
2077 channel,
2078 finish,
2079 } => {
2080 store.commit_generated(child, span.clone(), text.clone(), *channel, *finish)?;
2081 }
2082 EventBody::ToolUse { name, arguments } => {
2083 let e = store.commit_tool_use(child, name.clone(), arguments.clone())?;
2084 tool_ids.insert(id, e.event_id);
2085 }
2086 EventBody::ToolResult {
2087 call_id,
2088 content,
2089 span,
2090 } => match tool_ids.get(call_id) {
2091 Some(new_id) => {
2092 store.commit_tool_result(child, *new_id, content.clone(), span.clone())?;
2093 }
2094 None => {
2095 store.append_message(child, wal::role::TOOL, content.clone(), span.clone())?;
2096 }
2097 },
2098 EventBody::ToolParseFailure { raw } => {
2099 store.commit_tool_parse_failure(child, raw.clone())?;
2100 }
2101 EventBody::Created { .. }
2102 | EventBody::EpochBump
2103 | EventBody::GenerationFingerprint { .. }
2104 | EventBody::Forked { .. }
2105 | EventBody::Rebased { .. }
2106 | EventBody::MetaUpdated { .. }
2107 | EventBody::ToolExpired { .. }
2108 | EventBody::Purged { .. }
2109 | EventBody::Rerooted { .. }
2110 | EventBody::QosSet { .. } => {}
2111 EventBody::Block {
2112 role,
2113 kind,
2114 payload,
2115 span,
2116 visibility_version,
2117 } => {
2118 store.commit_block(child, *role, *kind, payload.clone(), span.clone(), *visibility_version)?;
2119 }
2120 EventBody::PermissionRequest { .. }
2121 | EventBody::PermissionResponse { .. }
2122 | EventBody::ToolCancelRequested { .. }
2123 | EventBody::ToolOutcome { .. }
2124 | EventBody::ToolReconciliation { .. }
2125 | EventBody::ToolLease { .. }
2126 | EventBody::Pinned { .. } => {}
2127 }
2128 i += 1;
2129 }
2130 Ok(child)
2131 }
2132
2133 pub fn trim(&self, session: u64, to_event: u64) -> Result<u64, DaemonError> {
2134 let max = {
2135 let store = self.store.lock().expect("store");
2136 store.session(session)?.next_event_id()
2137 };
2138 if to_event == 0 || to_event > max {
2139 return Err(DaemonError::ForkPoint {
2140 session,
2141 fork_at: to_event,
2142 max,
2143 });
2144 }
2145 if to_event == max {
2146 return self.fork(session, to_event, None);
2147 }
2148 self.rebase(
2149 session,
2150 vec![wal::RebaseEdit::Drop {
2151 from: to_event,
2152 to: max,
2153 }],
2154 None,
2155 )
2156 }
2157
2158 pub fn pin(&self, session: u64, ttl_ms: u64) -> Result<u64, DaemonError> {
2159 let deadline = if ttl_ms == 0 {
2160 0
2161 } else {
2162 wal::now_unix_ms().saturating_add(ttl_ms)
2163 };
2164 let (tokens, want) = {
2165 let mut store = self.store.lock().expect("store");
2166 store.set_pin(session, deadline)?;
2167 pins::session_target(&store.session(session)?.tokens)
2168 };
2169 self.send_pin(
2170 pins::PinKey::Session(session),
2171 tokens,
2172 want,
2173 deadline,
2174 Some(session),
2175 )?;
2176 Ok(deadline)
2177 }
2178
2179 pub fn pin_prefix(
2180 &self,
2181 anchor: Option<u64>,
2182 stream: &[u32],
2183 upto: usize,
2184 ttl_ms: u64,
2185 ) -> Result<u64, DaemonError> {
2186 let upto = upto.min(stream.len());
2187 let key = pins::PinKey::Prefix(pins::prefix_digest(&stream[..upto]));
2188 let tokens = if upto < stream.len() {
2189 stream[..=upto].to_vec()
2190 } else {
2191 pins::session_target(stream).0
2192 };
2193 let deadline = wal::now_unix_ms().saturating_add(ttl_ms.max(1));
2194 self.send_pin(key, tokens, upto as u64, deadline, anchor)
2195 }
2196
2197 pub fn pinned_prefix_tokens(
2198 &self,
2199 stream: &[u32],
2200 uptos: &[usize],
2201 ) -> Result<u64, DaemonError> {
2202 let keys = uptos
2203 .iter()
2204 .map(|&u| {
2205 let upto = u.min(stream.len());
2206 pins::PinKey::Prefix(pins::prefix_digest(&stream[..upto]))
2207 })
2208 .collect();
2209 let (tx, rx) = std::sync::mpsc::channel();
2210 self.sched_tx()
2211 .send(scheduler::SchedCmd::PinHeld(keys, tx))
2212 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
2213 rx.recv()
2214 .map_err(|_| DaemonError::Protocol("scheduler dropped the query"))
2215 }
2216
2217 pub fn unanchor_pins(&self, session: u64) {
2218 let _ = self.sched_tx().send(scheduler::SchedCmd::PinUnanchor(session));
2219 }
2220
2221 fn send_pin(
2222 &self,
2223 key: pins::PinKey,
2224 tokens: Vec<u32>,
2225 want: u64,
2226 deadline_unix_ms: u64,
2227 anchor: Option<u64>,
2228 ) -> Result<u64, DaemonError> {
2229 let (tx, rx) = std::sync::mpsc::channel();
2230 self.sched_tx()
2231 .send(scheduler::SchedCmd::Pin(pins::PinReq {
2232 key,
2233 tokens,
2234 want,
2235 deadline_unix_ms,
2236 anchor,
2237 reply: Some(tx),
2238 }))
2239 .map_err(|_| DaemonError::Protocol("scheduler is gone"))?;
2240 rx.recv()
2241 .map_err(|_| DaemonError::Protocol("scheduler dropped the pin"))
2242 }
2243
2244 fn render_for(
2245 &self,
2246 store: &SessionStore,
2247 session: u64,
2248 role: u32,
2249 text: &str,
2250 ) -> Result<Vec<u32>, DaemonError> {
2251 let s = store.session(session)?;
2252 let mut span = if s.tokens.is_empty() {
2253 self.codec().stream_prologue()
2254 } else {
2255 match Self::last_stream_record(&s.events) {
2256 Some(EventBody::Generated { finish, .. })
2257 if *finish == superfluid_abi::finish::EOS =>
2258 {
2259 self.codec().turn_separator().unwrap_or_default()
2260 }
2261 _ => Vec::new(),
2262 }
2263 };
2264 match self.codec().render_message(role, text) {
2265 Some(rendered) => span.extend(rendered),
2266 None => {
2267 let encoded = self.codec().encode(text);
2268 if encoded.is_empty() {
2269 return Err(DaemonError::Protocol("codec produced no span for the replacement"));
2270 }
2271 span.extend(encoded);
2272 }
2273 }
2274 Ok(span)
2275 }
2276
2277 pub fn purge(
2278 &self,
2279 session: u64,
2280 expected_generation: u64,
2281 mode: wal::PurgeMode,
2282 ) -> Result<Vec<u64>, DaemonError> {
2283 let targets: Vec<u64> = {
2284 let store = self.store.lock().expect("store");
2285 let s = store.session(session)?;
2286 if s.generation != expected_generation {
2287 return Err(DaemonError::GenerationConflict {
2288 session,
2289 expected: expected_generation,
2290 actual: s.generation,
2291 });
2292 }
2293 let mut t = vec![session];
2294 if mode == wal::PurgeMode::Cascade {
2295 t.extend(store.descendants_of(session));
2296 }
2297 t
2298 };
2299 {
2300 let _a = self.active.lock().expect("active set");
2301 let mut p = self.purging.lock().expect("purging");
2302 p.extend(targets.iter().copied());
2303 }
2304 let _fence = PurgeFence {
2305 set: std::sync::Arc::clone(&self.purging),
2306 ids: targets.clone(),
2307 };
2308 for &t in &targets {
2309 self.quiesce(t)?;
2310 }
2311 let purged = {
2312 let mut store = self.store.lock().expect("store");
2313 for &t in &targets {
2314 let open: Vec<u64> = store.session(t)?.open_tool_calls.keys().copied().collect();
2315 for call in open {
2316 store.expire_tool_call(t, call, "purged")?;
2317 }
2318 }
2319 store.purge(session, expected_generation, mode)?
2320 };
2321 let bus = self.bus();
2322 for &p in &purged {
2323 bus.end_session(p, bus::EndReason::Purged);
2324 }
2325 for &p in &purged {
2326 let _ = self.send_pin(pins::PinKey::Session(p), Vec::new(), 0, 0, None);
2327 }
2328 Ok(purged)
2329 }
2330
2331 fn refuse_if_purging(&self, session: u64) -> Result<(), DaemonError> {
2332 let _a = self.active.lock().expect("active set");
2333 if self.purging.lock().expect("purging").contains(&session) {
2334 return Err(DaemonError::Purged(session));
2335 }
2336 Ok(())
2337 }
2338
2339 fn quiesce(&self, session: u64) -> Result<(), DaemonError> {
2340 {
2341 let mut c = self.cancels.lock().expect("cancel registry");
2342 if self.active.lock().expect("active set").contains(&session) {
2343 c.insert(session);
2344 } else {
2345 return Ok(());
2346 }
2347 }
2348 self.cancels.wake();
2349 for _ in 0..2000 {
2350 if !self.active.lock().expect("active set").contains(&session) {
2351 return Ok(());
2352 }
2353 std::thread::sleep(std::time::Duration::from_millis(5));
2354 }
2355 Err(DaemonError::PurgeTimeout(session))
2356 }
2357
2358 pub fn append(
2359 &self,
2360 session: u64,
2361 text: Option<String>,
2362 span: Vec<u32>,
2363 ) -> Result<CommittedEvent, DaemonError> {
2364 {
2365 let active = self.active.lock().expect("active set");
2366 if active.contains(&session) {
2367 return Err(DaemonError::SessionBusy(session));
2368 }
2369 if self.purging.lock().expect("purging").contains(&session) {
2370 return Err(DaemonError::Purged(session));
2371 }
2372 }
2373 let span = if span.is_empty() {
2374 match &text {
2375 Some(t) => {
2376 let encoded = self.codec().encode(t);
2377 if encoded.is_empty() {
2378 return Err(DaemonError::Protocol(
2379 "append had no span and the codec produced none",
2380 ));
2381 }
2382 encoded
2383 }
2384 None => return Err(DaemonError::Protocol("append carried neither text nor span")),
2385 }
2386 } else {
2387 span
2388 };
2389 self.store.lock().expect("store").append(session, text, span)
2390 }
2391
2392 fn separator_for(&self, session: u64) -> Vec<u32> {
2393 let store = self.store.lock().expect("store");
2394 let Ok(s) = store.session(session) else {
2395 return Vec::new();
2396 };
2397 if s.tokens.is_empty() {
2398 return self.codec().stream_prologue();
2399 }
2400 match Self::last_stream_record(&s.events) {
2401 Some(EventBody::Generated { finish, .. })
2402 if *finish == superfluid_abi::finish::EOS =>
2403 {
2404 self.codec().turn_separator().unwrap_or_default()
2405 }
2406 _ => Vec::new(),
2407 }
2408 }
2409
2410 pub fn append_message(
2411 &self,
2412 session: u64,
2413 role: u32,
2414 text: String,
2415 ) -> Result<CommittedEvent, DaemonError> {
2416 if self.active.lock().expect("active set").contains(&session) {
2417 return Err(DaemonError::SessionBusy(session));
2418 }
2419 if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT) {
2420 return Err(DaemonError::UnknownRole(role));
2421 }
2422 let mut span = self.separator_for(session);
2423 span.extend(
2424 self.codec()
2425 .render_message(role, &text)
2426 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?,
2427 );
2428 self.store
2429 .lock()
2430 .expect("store")
2431 .append_message(session, role, text, span)
2432 }
2433
2434 pub fn renders_per_message(&self) -> bool {
2435 self.codec().renders_per_message()
2436 }
2437
2438 pub fn append_system_full(
2439 &self,
2440 session: u64,
2441 system: Option<String>,
2442 tool_jsons: Vec<String>,
2443 kwargs: &serde_json::Map<String, serde_json::Value>,
2444 ) -> Result<CommittedEvent, DaemonError> {
2445 if self.active.lock().expect("active set").contains(&session) {
2446 return Err(DaemonError::SessionBusy(session));
2447 }
2448 let span = self
2449 .codec()
2450 .render_system_full(system.as_deref(), &tool_jsons, kwargs)
2451 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
2452 self.store.lock().expect("store").append_message(
2453 session,
2454 wal::role::SYSTEM,
2455 system.unwrap_or_default(),
2456 span,
2457 )
2458 }
2459
2460 pub fn append_system(
2463 &self,
2464 session: u64,
2465 system: Option<String>,
2466 tools: Vec<String>,
2467 ) -> Result<CommittedEvent, DaemonError> {
2468 if system.is_none() && tools.is_empty() {
2469 return Err(DaemonError::Protocol("AppendSystem carried neither text nor tools"));
2470 }
2471 let tool_jsons = tools
2472 .iter()
2473 .enumerate()
2474 .map(|(index, t)| {
2475 codec::function_tool(t).map_err(|why| DaemonError::InvalidTool { index, why })
2476 })
2477 .collect::<Result<Vec<_>, _>>()?;
2478 if self.active.lock().expect("active set").contains(&session) {
2479 return Err(DaemonError::SessionBusy(session));
2480 }
2481 let codec = self.codec();
2482 let mut span = codec.stream_prologue();
2483 span.extend(
2484 codec
2485 .render_system_full(system.as_deref(), &tool_jsons, &serde_json::Map::new())
2486 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?,
2487 );
2488 let mut store = self.store.lock().expect("store");
2489 if !store.session(session)?.tokens.is_empty() {
2490 return Err(DaemonError::SystemNotFirst(session));
2491 }
2492 store.append_message(session, wal::role::SYSTEM, system.unwrap_or_default(), span)
2493 }
2494
2495 pub(crate) fn render_conversation(
2496 &self,
2497 messages: &[codec::ChatMessage],
2498 tools: &[String],
2499 ) -> Option<Vec<u32>> {
2500 self.codec()
2501 .render_prompt_structured_with(messages, tools, &serde_json::Map::new())
2502 }
2503
2504 pub fn supports_reasoning_effort(&self) -> bool {
2505 self.codec().supports_reasoning_effort()
2506 }
2507
2508 pub fn reasoning_effort_levels(&self) -> Vec<&'static str> {
2509 self.codec().reasoning_effort_levels()
2510 }
2511
2512 pub fn supports_enable_thinking(&self) -> bool {
2513 self.codec().supports_enable_thinking()
2514 }
2515
2516 pub fn validate_template_kwargs(
2517 &self,
2518 kwargs: &serde_json::Map<String, serde_json::Value>,
2519 ) -> Result<(), String> {
2520 self.codec().validate_template_kwargs(kwargs)
2521 }
2522
2523 fn commit_opener(&self, session: u64, opener: Vec<u32>) -> Result<(), DaemonError> {
2524 if opener.is_empty() {
2525 return Ok(());
2526 }
2527 self.store
2528 .lock()
2529 .expect("store")
2530 .commit_generation_prompt(session, opener)
2531 .map(|_| ())
2532 }
2533
2534 pub fn append_conversation(
2535 &self,
2536 session: u64,
2537 messages: &[crate::codec::ChatMessage],
2538 tools: &[String],
2539 kwargs: &serde_json::Map<String, serde_json::Value>,
2540 ) -> Result<CommittedEvent, DaemonError> {
2541 let codec = self.codec();
2542 let mut span = codec
2543 .render_prompt_structured_with(messages, tools, kwargs)
2544 .ok_or_else(|| render_refusal(&*codec, messages, tools, kwargs, "codec has no chat dialect"))?;
2545 let opener_len = codec.trailing_generation_prompt(messages, tools, kwargs, &span);
2546 let opener: Vec<u32> = if opener_len > 0 && opener_len < span.len() {
2547 span.split_off(span.len() - opener_len)
2548 } else {
2549 Vec::new()
2550 };
2551 let text = messages.last().map(|m| m.content.clone());
2552 let blobs: Vec<(u32, String)> = messages
2553 .iter()
2554 .flat_map(|m| {
2555 m.parts.iter().filter_map(move |p| match p {
2556 crate::codec::ContentPart::Image { blob } => Some((m.role, blob.clone())),
2557 crate::codec::ContentPart::Audio { blob } => Some((m.role, blob.clone())),
2558 crate::codec::ContentPart::Text(_) => None,
2559 })
2560 })
2561 .collect();
2562 if blobs.is_empty() {
2563 let ev = self.append(session, text, span)?;
2564 self.commit_opener(session, opener)?;
2565 return Ok(ev);
2566 }
2567 let mut probes = Vec::with_capacity(blobs.len());
2568 for (_, blob) in &blobs {
2569 if !self.media.exists(blob) {
2570 return Err(DaemonError::UnknownMedia(blob.clone()));
2571 }
2572 let path = self.media.path_of(blob);
2573 let info = match self.engine_sync(proto_linkw::StateSyncReqMsg::MediaProbe {
2574 image_path: path.to_string_lossy().into_owned(),
2575 })? {
2576 proto_linkw::StateSyncOkMsg::MediaProbe { info } => info,
2577 _ => return Err(DaemonError::Protocol("mismatched MediaProbe reply")),
2578 };
2579 if info.n_tokens == 0 || info.n_tokens as u64 > scheduler::PREFILL_BUDGET as u64 {
2580 return Err(DaemonError::MediaTooLarge(info.n_tokens));
2581 }
2582 probes.push(info);
2583 }
2584 let mut out = Vec::with_capacity(span.len() + 256);
2585 let mut offsets: Vec<usize> = Vec::with_capacity(probes.len());
2586 let mut img = 0usize;
2587 for (idx, &t) in span.iter().enumerate() {
2588 if img == probes.len() || t != probes[img].image_token_id {
2589 if probes.iter().any(|p| p.image_token_id == t) {
2590 return Err(DaemonError::Protocol(
2591 "rendered prompt contains a media marker with no matching pending part",
2592 ));
2593 }
2594 out.push(t);
2595 continue;
2596 }
2597 let info = &probes[img];
2598 if info.boi_token_id != 0 && out.last() != Some(&info.boi_token_id) {
2599 out.push(info.boi_token_id);
2600 }
2601 offsets.push(out.len());
2602 out.extend(std::iter::repeat_n(info.image_token_id, info.n_tokens as usize));
2603 if info.eoi_token_id != 0 && span.get(idx + 1) != Some(&info.eoi_token_id) {
2604 out.push(info.eoi_token_id);
2605 }
2606 img += 1;
2607 }
2608 if img != probes.len() {
2609 return Err(DaemonError::Protocol(
2610 "this model's chat template did not emit an image marker for every image part; it cannot place media",
2611 ));
2612 }
2613 let stream_len = {
2614 let store = self.store.lock().expect("store");
2615 store.session(session)?.tokens.len() as u64
2616 };
2617 let ev = self.append(session, text, out)?;
2618 for (((role, blob), info), off) in blobs.iter().zip(&probes).zip(&offsets) {
2619 let payload = serde_json::json!({
2620 "blob": blob,
2621 "n_tokens": info.n_tokens,
2622 "image_token_id": info.image_token_id,
2623 "offset": stream_len + *off as u64,
2624 "pre_text": "",
2625 "post_text": "",
2626 "media_identity": {
2627 "preprocessing_fp": format!("{:016x}", info.preprocess_fp),
2628 "encoder": "bundle-weights-identity",
2629 },
2630 })
2631 .to_string();
2632 let mut store = self.store.lock().expect("store");
2633 store.commit_block(
2634 session,
2635 *role,
2636 wal::block_kind::IMAGE,
2637 payload,
2638 Vec::new(),
2639 codec::MODEL_VISIBILITY_VERSION,
2640 )?;
2641 }
2642 self.commit_opener(session, opener)?;
2643 Ok(ev)
2644 }
2645
2646 pub fn response_format_tag(&self, json_schema: &str) -> Option<String> {
2647 self.codec().response_format_tag(json_schema)
2648 }
2649
2650 pub fn tool_structural_tag(&self, tool_jsons: &[String], at_least_one: bool) -> Option<String> {
2651 self.codec().structural_tag(tool_jsons, at_least_one)
2652 }
2653
2654 pub fn grammar_create_structural(&self, tag_json: &str) -> Result<u32, DaemonError> {
2655 match self.engine_sync(proto_linkw::StateSyncReqMsg::CreateGrammarStructural {
2656 tag_json: tag_json.to_string(),
2657 })? {
2658 proto_linkw::StateSyncOkMsg::CreateGrammar { grammar_handle } => Ok(grammar_handle),
2659 _ => Err(DaemonError::Protocol("mismatched CreateGrammar reply")),
2660 }
2661 }
2662
2663 pub fn parse_tool_call(&self, raw: &str) -> Option<(String, String)> {
2664 self.codec().parse_tool_call(raw)
2665 }
2666
2667 pub fn named_tool_call(&self, raw: &str) -> Option<(String, String)> {
2668 self.codec().named_tool_call(raw)
2669 }
2670
2671 pub fn parse_tool_call_with(
2672 &self,
2673 raw: &str,
2674 schemas: Option<&crate::codec::ToolSchemas>,
2675 ) -> Option<(String, String)> {
2676 self.codec().parse_tool_call_with(raw, schemas)
2677 }
2678
2679 pub fn tool_fragment_mode(&self) -> crate::tool_fragment::ToolFragmentMode {
2680 self.codec().tool_fragment_mode()
2681 }
2682
2683 pub fn tool_call_grammar(&self, tool_jsons: &[String]) -> Option<String> {
2684 self.codec().call_grammar(tool_jsons)
2685 }
2686
2687 pub fn append_system_with_tools(
2688 &self,
2689 session: u64,
2690 system: Option<String>,
2691 tool_jsons: Vec<String>,
2692 ) -> Result<CommittedEvent, DaemonError> {
2693 if self.active.lock().expect("active set").contains(&session) {
2694 return Err(DaemonError::SessionBusy(session));
2695 }
2696 let span = self
2697 .codec()
2698 .render_system_with_tools(system.as_deref(), &tool_jsons)
2699 .ok_or(DaemonError::Protocol("codec has no tool dialect"))?;
2700 self.store.lock().expect("store").append_message(
2701 session,
2702 wal::role::SYSTEM,
2703 system.unwrap_or_default(),
2704 span,
2705 )
2706 }
2707
2708 pub fn append_assistant_with_tool_calls(
2711 &self,
2712 session: u64,
2713 content: String,
2714 calls: Vec<(String, String)>,
2715 after_query: bool,
2716 ) -> Result<CommittedEvent, DaemonError> {
2717 if self.active.lock().expect("active set").contains(&session) {
2718 return Err(DaemonError::SessionBusy(session));
2719 }
2720 let mut span = self.separator_for(session);
2721 let codec = self.codec();
2722 let turn = if after_query {
2723 codec.render_assistant_with_tool_calls_after_query(&content, &calls)
2724 } else {
2725 codec.render_assistant_with_tool_calls(&content, &calls)
2726 };
2727 span.extend(turn.ok_or(DaemonError::Protocol("codec has no tool dialect"))?);
2728 self.store.lock().expect("store").append_message(
2729 session,
2730 wal::role::ASSISTANT,
2731 content,
2732 span,
2733 )
2734 }
2735
2736 pub fn append_tool_result(
2737 &self,
2738 session: u64,
2739 call_id: u64,
2740 content: String,
2741 ) -> Result<CommittedEvent, DaemonError> {
2742 if self.active.lock().expect("active set").contains(&session) {
2743 return Err(DaemonError::SessionBusy(session));
2744 }
2745 let name = {
2746 let mut store = self.store.lock().expect("store");
2747 let s = store.session(session)?;
2748 if call_id < s.base {
2749 return Err(DaemonError::InheritedToolCall(call_id));
2750 }
2751 match s.ledger.get(&call_id) {
2752 None => return Err(DaemonError::UnknownToolCall(call_id)),
2753 Some(e)
2754 if e.state == wal::ledger_state::OPEN
2755 || e.state == wal::ledger_state::CANCEL_REQUESTED =>
2756 {
2757 if let Some(deadline) = e.lease_deadline_unix_ms {
2758 if wal::now_unix_ms() > deadline {
2759 store.expire_tool_call(session, call_id, "lease expired")?;
2760 return Err(DaemonError::ToolLeaseExpired(call_id));
2761 }
2762 }
2763 e.name.clone()
2764 }
2765 Some(e) if e.state == wal::ledger_state::SUCCEEDED => {
2766 return Err(DaemonError::UnknownToolCall(call_id));
2767 }
2768 Some(_) => {
2769 store.commit_tool_reconciliation(
2770 session,
2771 call_id,
2772 format!("late result: {} bytes", content.len()),
2773 )?;
2774 return Err(DaemonError::ToolLateEffect(call_id));
2775 }
2776 }
2777 };
2778 let mut span = self.separator_for(session);
2779 span.extend(
2780 self.codec()
2781 .render_tool_result(&name, &content)
2782 .ok_or(DaemonError::Protocol("codec has no tool dialect"))?,
2783 );
2784 self.store
2785 .lock()
2786 .expect("store")
2787 .commit_tool_result(session, call_id, content, span)
2788 }
2789
2790 pub fn append_block(
2791 &self,
2792 session: u64,
2793 role: u32,
2794 kind: u32,
2795 payload: String,
2796 ) -> Result<CommittedEvent, DaemonError> {
2797 if self.active.lock().expect("active set").contains(&session) {
2798 return Err(DaemonError::SessionBusy(session));
2799 }
2800 if !matches!(role, wal::role::SYSTEM | wal::role::USER | wal::role::ASSISTANT | wal::role::TOOL) {
2801 return Err(DaemonError::UnknownRole(role));
2802 }
2803 if codec::project_block_text(kind, &payload).is_none() {
2804 return Err(DaemonError::UnknownBlockKind(kind));
2805 }
2806 let projected = self
2807 .codec()
2808 .render_block(role, kind, &payload)
2809 .ok_or(DaemonError::Protocol("codec has no chat dialect"))?;
2810 let span = if projected.is_empty() {
2811 Vec::new()
2812 } else {
2813 let mut span = self.separator_for(session);
2814 span.extend(projected);
2815 span
2816 };
2817 self.store.lock().expect("store").commit_block(
2818 session,
2819 role,
2820 kind,
2821 payload,
2822 span,
2823 codec::MODEL_VISIBILITY_VERSION,
2824 )
2825 }
2826
2827 pub fn request_permission(
2828 &self,
2829 session: u64,
2830 call_id: u64,
2831 text: String,
2832 ) -> Result<CommittedEvent, DaemonError> {
2833 let active = self.active.lock().expect("active set");
2834 if active.contains(&session) {
2835 return Err(DaemonError::SessionBusy(session));
2836 }
2837 if self.purging.lock().expect("purging").contains(&session) {
2838 return Err(DaemonError::Purged(session));
2839 }
2840 self.store
2841 .lock()
2842 .expect("store")
2843 .commit_permission_request(session, call_id, text)
2844 }
2845
2846 pub fn respond_permission(
2847 &self,
2848 session: u64,
2849 request_id: u64,
2850 granted: bool,
2851 ) -> Result<CommittedEvent, DaemonError> {
2852 self.store
2853 .lock()
2854 .expect("store")
2855 .commit_permission_response(session, request_id, granted)
2856 }
2857
2858 pub fn request_tool_cancel(&self, session: u64, call_id: u64) -> Result<CommittedEvent, DaemonError> {
2859 self.store
2860 .lock()
2861 .expect("store")
2862 .commit_tool_cancel_request(session, call_id)
2863 }
2864
2865 pub fn append_tool_outcome(
2866 &self,
2867 session: u64,
2868 call_id: u64,
2869 outcome: u8,
2870 note: String,
2871 ) -> Result<CommittedEvent, DaemonError> {
2872 self.store
2873 .lock()
2874 .expect("store")
2875 .commit_tool_outcome(session, call_id, outcome, note)
2876 }
2877
2878 pub fn ledger(&self, session: u64) -> Result<Vec<(u64, store::LedgerEntry)>, DaemonError> {
2879 Ok(self
2880 .store
2881 .lock()
2882 .expect("store")
2883 .session(session)?
2884 .ledger
2885 .iter()
2886 .map(|(id, e)| (*id, e.clone()))
2887 .collect())
2888 }
2889
2890 pub fn append_tool_history(
2891 &self,
2892 session: u64,
2893 content: String,
2894 ) -> Result<CommittedEvent, DaemonError> {
2895 if self.active.lock().expect("active set").contains(&session) {
2896 return Err(DaemonError::SessionBusy(session));
2897 }
2898 let mut span = self.separator_for(session);
2899 span.extend(
2900 self.codec()
2901 .render_tool_result("", &content)
2902 .ok_or(DaemonError::Protocol("codec has no tool dialect"))?,
2903 );
2904 self.store.lock().expect("store").append_message(
2905 session,
2906 wal::role::TOOL,
2907 content,
2908 span,
2909 )
2910 }
2911
2912 pub fn open_tool_calls(
2913 &self,
2914 session: u64,
2915 ) -> Result<Vec<(u64, String, String)>, DaemonError> {
2916 Ok(self
2917 .store
2918 .lock()
2919 .expect("store")
2920 .session(session)?
2921 .open_tool_calls
2922 .iter()
2923 .map(|(id, (n, a))| (*id, n.clone(), a.clone()))
2924 .collect())
2925 }
2926
2927 pub fn generate(&self, session: u64, max_tokens: u32) -> Result<GenerateOutcome, DaemonError> {
2928 self.generate_streaming(session, max_tokens, |_| Ok(()))
2929 }
2930
2931 pub fn generate_streaming(
2932 &self,
2933 session: u64,
2934 max_tokens: u32,
2935 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2936 ) -> Result<GenerateOutcome, DaemonError> {
2937 self.generate_streaming_ex(session, max_tokens, scheduler::GenExtras::default(), on_event)
2938 }
2939
2940 pub fn generate_streaming_ex(
2941 &self,
2942 session: u64,
2943 max_tokens: u32,
2944 extras: scheduler::GenExtras,
2945 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2946 ) -> Result<GenerateOutcome, DaemonError> {
2947 self.generate_streaming_ex_lp(session, max_tokens, extras, on_event, |_| {})
2948 }
2949
2950 pub fn generate_streaming_ex_lp(
2951 &self,
2952 session: u64,
2953 max_tokens: u32,
2954 extras: scheduler::GenExtras,
2955 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2956 on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
2957 ) -> Result<GenerateOutcome, DaemonError> {
2958 self.generate_streaming_inner(session, max_tokens, extras, on_event, on_logprobs, |_, _, _| {})
2959 }
2960
2961 pub fn generate_streaming_delta(
2962 &self,
2963 session: u64,
2964 max_tokens: u32,
2965 mut extras: scheduler::GenExtras,
2966 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
2967 on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
2968 on_delta: impl FnMut(u32, &str, u32),
2969 ) -> Result<GenerateOutcome, DaemonError> {
2970 extras.wants_deltas = true;
2971 self.generate_streaming_inner(session, max_tokens, extras, on_event, on_logprobs, on_delta)
2972 }
2973
2974 fn generation_opener(&self, events: &[store::CommittedEvent], thinking: Option<bool>) -> Option<Vec<u32>> {
2975 match Self::last_stream_record(events) {
2976 Some(EventBody::Message { .. }) | Some(EventBody::ToolResult { .. }) => self
2977 .codec()
2978 .generation_prefix_with(codec::TurnState::AfterInput, thinking),
2979 Some(EventBody::Generated { finish, .. }) if *finish == superfluid_abi::finish::EOS => self
2980 .codec()
2981 .generation_prefix_with(codec::TurnState::AfterFinishedTurn, thinking),
2982 _ => None,
2983 }
2984 }
2985
2986 pub(crate) fn session_end(&self, session: u64) -> Option<(u64, bool)> {
2990 let store = self.store.lock().expect("store");
2991 let s = store.session(session).ok()?;
2992 let ended = matches!(
2993 Self::last_stream_record(&s.events),
2994 Some(EventBody::Generated { finish, .. }) if *finish == superfluid_abi::finish::EOS
2995 );
2996 Some((s.next_event_id(), ended))
2997 }
2998
2999 fn last_stream_record(events: &[store::CommittedEvent]) -> Option<&EventBody> {
3000 events
3001 .iter()
3002 .rev()
3003 .map(|e| &e.body)
3004 .find(|b| b.span().is_some_and(|s| !s.is_empty()))
3005 }
3006
3007 fn open_turn_tokens(events: &[store::CommittedEvent]) -> Vec<u32> {
3008 let mut spans: Vec<&[u32]> = Vec::new();
3009 for e in events.iter().rev() {
3010 match &e.body {
3011 EventBody::Generated { span, finish, .. } => {
3012 if spans.is_empty() && *finish == superfluid_abi::finish::EOS {
3013 return Vec::new();
3014 }
3015 spans.push(span);
3016 }
3017 EventBody::GenerationPrompt { span } => {
3018 spans.push(span);
3019 break;
3020 }
3021 b => match b.span() {
3022 None | Some([]) => continue,
3023 Some(_) => break,
3024 },
3025 }
3026 }
3027 spans.into_iter().rev().flatten().copied().collect()
3028 }
3029
3030 fn generate_streaming_inner(
3031 &self,
3032 session: u64,
3033 max_tokens: u32,
3034 extras: scheduler::GenExtras,
3035 on_event: impl FnMut(&CommittedEvent) -> Result<(), DaemonError>,
3036 on_logprobs: impl FnMut(&[scheduler::TokenLogprob]),
3037 on_delta: impl FnMut(u32, &str, u32),
3038 ) -> Result<GenerateOutcome, DaemonError> {
3039 self.ensure_serving()?;
3040 let _span = tracing::info_span!(
3041 parent: None,
3042 "generate",
3043 session,
3044 max_tokens,
3045 model = self.trace_scope.model.as_str(),
3046 store_ns = self.trace_scope.ns
3047 )
3048 .entered();
3049 let (stream, params, class, batch_invariant, media) = {
3050 let mut cancels = self.cancels.lock().expect("cancel registry");
3051 let mut active = self.active.lock().expect("active set");
3052 if self.purging.lock().expect("purging").contains(&session) {
3053 return Err(DaemonError::Purged(session));
3054 }
3055 if self.tool_lease_ms > 0 {
3056 let now = crate::wal::now_unix_ms();
3057 let mut store = self.store.lock().expect("store");
3058 let elapsed: Vec<u64> = {
3059 let s = store.session(session)?;
3060 s.open_tool_calls
3061 .keys()
3062 .filter(|id| {
3063 s.ledger
3064 .get(id)
3065 .and_then(|e| e.lease_deadline_unix_ms)
3066 .is_some_and(|d| d <= now)
3067 })
3068 .copied()
3069 .collect()
3070 };
3071 for call_id in elapsed {
3072 let _ = store.expire_tool_call(session, call_id, "lease elapsed");
3073 }
3074 }
3075 let snap = {
3076 let store = self.store.lock().expect("store");
3077 let s = store.session(session)?;
3078 if !s.open_permissions.is_empty() {
3079 return Err(DaemonError::PermissionPending(session));
3080 }
3081 if !s.open_tool_calls.is_empty() {
3082 tracing::warn!(
3083 session,
3084 open_tool_calls = s.open_tool_calls.len(),
3085 "generating with unanswered tool calls: the model cannot see its own \
3086 tool output and will re-issue the call. Return a result for each open \
3087 call id (OpenAI: a `role: \"tool\"` message carrying its `tool_call_id`), \
3088 or run with --tool-lease-ms so unanswered calls expire instead of \
3089 accumulating."
3090 );
3091 }
3092 (
3093 s.tokens.clone(),
3094 s.params,
3095 s.qos_class,
3096 s.batch_invariant,
3097 self.media_of(s),
3098 )
3099 };
3100 if snap.0.is_empty() {
3101 return Err(DaemonError::EmptySession(session));
3102 }
3103 self.check_stream_fits(snap.0.len())?;
3104 if max_tokens == 0 {
3105 return Ok(GenerateOutcome {
3106 events: Vec::new(),
3107 tokens_generated: 0,
3108 finish: superfluid_abi::finish::NONE,
3109 warm_prefix: 0,
3110 logprobs: Vec::new(),
3111 spec: (0, 0),
3112 });
3113 }
3114 if !active.insert(session) {
3115 return Err(DaemonError::SessionBusy(session));
3116 }
3117 cancels.remove(&session);
3121 snap
3122 };
3123 if let Some(mut fp) = self.codec().behavior_fingerprint() {
3124 fp.speculation = self.serving().speculation.identity_for_fingerprint();
3125 let commit = self
3126 .store
3127 .lock()
3128 .expect("store")
3129 .commit_generation_fingerprint(session, fp.digest());
3130 if let Err(e) = commit {
3131 self.active.lock().expect("active set").remove(&session);
3132 return Err(e);
3133 }
3134 }
3135 let mut stream = stream;
3136 let (opener, open_turn) = {
3137 let store = self.store.lock().expect("store");
3138 let events = &store.session(session)?.events;
3139 let opener = self.generation_opener(events, extras.thinking);
3140 let open_turn: Vec<u32> = if opener.is_some() {
3141 Vec::new()
3142 } else {
3143 Self::open_turn_tokens(events)
3144 };
3145 (opener, open_turn)
3146 };
3147 let opener_len = opener.as_ref().map(Vec::len);
3148 let input_end = if opener.is_some() { stream.len() as u64 } else { 0 };
3149 if let Some(op) = opener {
3150 if let Err(e) = self.check_stream_fits(stream.len() + op.len()) {
3151 self.active.lock().expect("active set").remove(&session);
3152 return Err(e);
3153 }
3154 let commit = self
3155 .store
3156 .lock()
3157 .expect("store")
3158 .commit_generation_prompt(session, op.clone());
3159 if let Err(e) = commit {
3160 self.active.lock().expect("active set").remove(&session);
3161 return Err(e);
3162 }
3163 stream.extend_from_slice(&op);
3164 }
3165 let mut extras = extras;
3166 if extras.open_channel == 0 {
3167 let scope: &[u32] = match opener_len {
3168 Some(n) => &stream[stream.len() - n..],
3169 None => &open_turn,
3170 };
3171 let open = codec::trailing_open_channel(scope, &self.codec().channel_markers());
3172 if open != 0 {
3173 extras.open_channel = open;
3174 }
3175 }
3176 let result = scheduler::run_job(
3177 &self.sched_tx(),
3178 session,
3179 stream,
3180 input_end,
3181 params,
3182 max_tokens,
3183 class,
3184 batch_invariant,
3185 media,
3186 extras,
3187 &self.cancels,
3188 on_event,
3189 on_logprobs,
3190 on_delta,
3191 );
3192 match result {
3193 Ok((events, tokens_generated, finish, warm_prefix, logprobs, spec)) => Ok(GenerateOutcome {
3194 events,
3195 tokens_generated,
3196 finish,
3197 warm_prefix,
3198 logprobs,
3199 spec,
3200 }),
3201 Err(e) => {
3202 self.active.lock().expect("active set").remove(&session);
3203 Err(e)
3204 }
3205 }
3206 }
3207
3208 pub fn read(&self, session: u64, cursor: u64) -> Result<Vec<CommittedEvent>, DaemonError> {
3209 Ok(self.store.lock().expect("store").read(session, cursor)?.to_vec())
3210 }
3211
3212 pub fn session_ids(&self) -> Vec<u64> {
3213 self.store.lock().expect("store").session_ids()
3214 }
3215}
3216
3217#[cfg(test)]
3218mod open_turn_tests {
3219 use super::*;
3220 use superfluid_abi::finish;
3221
3222 fn ev(event_id: u64, body: EventBody) -> store::CommittedEvent {
3223 store::CommittedEvent { event_id, epoch: 0, ts_unix_ms: 0, body }
3224 }
3225
3226 fn generated(span: &[u32], finish: u32) -> EventBody {
3227 EventBody::Generated { span: span.to_vec(), text: String::new(), channel: 0, finish }
3228 }
3229
3230 #[test]
3231 fn open_turn_skips_every_token_less_record() {
3232 let events = vec![
3233 ev(0, EventBody::Message { role: 1, text: "hi".into(), span: vec![900, 901] }),
3234 ev(1, EventBody::GenerationPrompt { span: vec![1, 2] }),
3235 ev(2, generated(&[3, 4], finish::NONE)),
3236 ev(3, EventBody::ToolUse { name: "x".into(), arguments: "{".into() }),
3237 ev(4, EventBody::ToolLease { call_id: 3, deadline_unix_ms: 1 }),
3238 ev(5, EventBody::ToolReconciliation { call_id: 3, note: String::new() }),
3239 ev(6, EventBody::EpochBump),
3240 ev(7, EventBody::Pinned { deadline_unix_ms: 1 }),
3241 ev(8, EventBody::Block { role: 1, kind: 7, payload: "{}".into(), span: vec![], visibility_version: 1 }),
3242 ev(9, generated(&[5], finish::LENGTH)),
3243 ];
3244 assert_eq!(Daemon::open_turn_tokens(&events), vec![1, 2, 3, 4, 5]);
3245 }
3246
3247 #[test]
3248 fn token_less_records_do_not_reopen_a_closed_turn() {
3249 let closed = vec![
3250 ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3251 ev(1, generated(&[2], finish::EOS)),
3252 ev(2, EventBody::ToolLease { call_id: 9, deadline_unix_ms: 1 }),
3253 ev(3, EventBody::QosSet { class: 0, batch_invariant: false }),
3254 ];
3255 assert!(Daemon::open_turn_tokens(&closed).is_empty());
3256 let after_result = vec![
3257 ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3258 ev(1, generated(&[2], finish::NONE)),
3259 ev(2, EventBody::ToolUse { name: "x".into(), arguments: "{}".into() }),
3260 ev(3, EventBody::ToolResult { call_id: 2, content: "ok".into(), span: vec![7, 8] }),
3261 ev(4, EventBody::ToolLease { call_id: 2, deadline_unix_ms: 1 }),
3262 ];
3263 assert!(Daemon::open_turn_tokens(&after_result).is_empty());
3264 }
3265
3266 #[test]
3267 fn the_last_stream_record_is_the_last_one_with_tokens() {
3268 let events = vec![
3269 ev(0, EventBody::GenerationPrompt { span: vec![1] }),
3270 ev(1, generated(&[2], finish::EOS)),
3271 ev(2, EventBody::ToolUse { name: "x".into(), arguments: "{}".into() }),
3272 ev(3, EventBody::ToolLease { call_id: 2, deadline_unix_ms: 1 }),
3273 ev(4, EventBody::Block { role: 1, kind: 7, payload: "{}".into(), span: vec![], visibility_version: 1 }),
3274 ev(5, EventBody::GenerationFingerprint { digest: 9 }),
3275 ];
3276 assert!(matches!(
3277 Daemon::last_stream_record(&events),
3278 Some(EventBody::Generated { finish: finish::EOS, .. })
3279 ));
3280 assert!(Daemon::last_stream_record(&events[2..]).is_none());
3281 }
3282}
3283
3284fn first_registrable<E>(
3285 candidates: &[String],
3286 auto: bool,
3287 mut register: impl FnMut(&str) -> Result<(), E>,
3288 mut refused: impl FnMut(&str, &E, Option<&str>),
3289) -> Result<Option<String>, E> {
3290 for (i, c) in candidates.iter().enumerate() {
3291 match register(c) {
3292 Ok(()) => return Ok(Some(c.clone())),
3293 Err(e) if auto => refused(c, &e, candidates.get(i + 1).map(String::as_str)),
3294 Err(e) => return Err(e),
3295 }
3296 }
3297 Ok(None)
3298}
3299
3300#[cfg(test)]
3301mod speculate_fallback_tests {
3302 use super::first_registrable;
3303
3304 fn strs(v: &[&str]) -> Vec<String> {
3305 v.iter().map(|s| s.to_string()).collect()
3306 }
3307
3308 #[test]
3309 fn auto_tries_each_candidate_in_order_until_one_registers() {
3310 let mut tried = Vec::new();
3311 let mut reported = Vec::new();
3312 let picked = first_registrable(
3313 &strs(&["dspark:a", "dflash:b", "eagle3:c"]),
3314 true,
3315 |s| {
3316 tried.push(s.to_string());
3317 if s == "dflash:b" { Ok(()) } else { Err(format!("no {s}")) }
3318 },
3319 |s, e, next| reported.push((s.to_string(), e.clone(), next.map(str::to_string))),
3320 )
3321 .unwrap();
3322 assert_eq!(picked.as_deref(), Some("dflash:b"));
3323 assert_eq!(tried, ["dspark:a", "dflash:b"]);
3324 assert_eq!(reported, [("dspark:a".to_string(), "no dspark:a".to_string(), Some("dflash:b".to_string()))]);
3325 }
3326
3327 #[test]
3328 fn auto_with_every_candidate_refused_serves_plainly() {
3329 let mut last_next = Some("x".to_string());
3330 let picked = first_registrable(
3331 &strs(&["dspark:a", "dflash:b"]),
3332 true,
3333 |_| Err::<(), _>("refused"),
3334 |_, _, next| last_next = next.map(str::to_string),
3335 )
3336 .unwrap();
3337 assert_eq!(picked, None);
3338 assert_eq!(last_next, None, "the last refusal says it is decoding plainly");
3339 }
3340
3341 #[test]
3342 fn an_explicit_strategy_fails_on_the_first_refusal() {
3343 let mut reported = 0;
3344 let r = first_registrable(
3345 &strs(&["dspark:a", "dflash:b"]),
3346 false,
3347 |s| if s == "dspark:a" { Err("refused") } else { Ok(()) },
3348 |_, _, _| reported += 1,
3349 );
3350 assert_eq!(r, Err("refused"));
3351 assert_eq!(reported, 0);
3352 }
3353}
3354
3355#[cfg(test)]
3356mod latency_tests {
3357 use super::CancelSet;
3358 use std::time::Duration;
3359
3360 #[test]
3361 fn a_latency_request_raises_the_word_and_holds_it_for_a_while() {
3362 let ctl = superfluid_shm::ControlSegment::create().unwrap();
3363 let worker = superfluid_shm::ControlSegment::attach(
3364 superfluid_shm::ShmSegment::from_fd(
3365 {
3366 use std::os::fd::FromRawFd;
3367 unsafe { std::os::fd::OwnedFd::from_raw_fd(libc::dup(ctl.raw_fd())) }
3369 },
3370 superfluid_shm::CONTROL_BYTES,
3371 )
3372 .unwrap(),
3373 )
3374 .unwrap();
3375 let cancels = CancelSet::default();
3376 cancels.wake_with(std::sync::Arc::new(std::sync::RwLock::new(Some(ctl.signal().unwrap()))));
3377 assert!(!cancels.latency_recent(Duration::from_secs(300)), "nothing has come");
3378 cancels.latency_request();
3379 assert!(worker.latency_wanted() && worker.yield_requested(), "short steps, and the running tick ends at its next");
3380 assert!(cancels.latency_recent(Duration::from_secs(300)));
3381 assert!(!cancels.latency_recent(Duration::ZERO), "past the hold");
3382 }
3383}