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