1use std::fmt;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use bytes::Bytes;
9use serde::{Deserialize, Serialize};
10use tokio::sync::oneshot;
11
12use crate::tensor::HostTensor;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16pub struct NeuronId(pub(crate) u32);
17
18impl NeuronId {
19 pub(crate) const NONE: Self = Self(u32::MAX);
21
22 pub fn index(self) -> usize {
24 self.0 as usize
25 }
26}
27
28impl fmt::Display for NeuronId {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 write!(f, "n{}", self.0)
31 }
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub(crate) struct PathwayId(pub(crate) u32);
37
38impl PathwayId {
39 pub(crate) fn index(self) -> usize {
41 self.0 as usize
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
47pub struct SignalId(pub u64);
48
49static NEXT_SIGNAL: AtomicU64 = AtomicU64::new(1);
50
51impl SignalId {
52 fn next() -> Self {
53 Self(NEXT_SIGNAL.fetch_add(1, Ordering::Relaxed))
54 }
55}
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub struct Feature {
66 pub kind: u16,
68 pub sketch: u64,
70}
71
72impl Feature {
73 pub const NONE: Self = Self { kind: 0, sketch: 0 };
76
77 pub const fn new(kind: u16, sketch: u64) -> Self {
79 Self { kind, sketch }
80 }
81
82 pub fn sketch(bytes: &[u8]) -> Self {
85 Self::new(1, sketch64(bytes))
86 }
87
88 pub fn sketch_as(kind: u16, bytes: &[u8]) -> Self {
90 Self::new(kind, sketch64(bytes))
91 }
92
93 pub fn with_kind(self, kind: u16) -> Self {
95 Self { kind, ..self }
96 }
97
98 pub fn is_none(self) -> bool {
100 self == Self::NONE
101 }
102}
103
104pub(crate) fn sketch64(bytes: &[u8]) -> u64 {
108 const K: u64 = 0x9E37_79B9_7F4A_7C15;
109 let mut h = (bytes.len() as u64).wrapping_mul(K) ^ 0x2545_F491_4F6C_DD1D;
110 let (chunks, rest) = bytes.as_chunks::<8>();
111 for chunk in chunks {
112 h = (h ^ u64::from_le_bytes(*chunk)).wrapping_mul(K);
113 h ^= h >> 29;
114 }
115 if !rest.is_empty() {
116 let mut word = [0u8; 8];
117 word[..rest.len()].copy_from_slice(rest);
118 h = (h ^ u64::from_le_bytes(word)).wrapping_mul(K);
119 h ^= h >> 29;
120 }
121 h = (h ^ (h >> 32)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
122 h ^ (h >> 31)
123}
124
125#[derive(Clone, Copy, Debug, Default, PartialEq)]
128pub struct Urgency {
129 pub deadline: Option<Instant>,
131 pub rate: f32,
134}
135
136impl Urgency {
137 pub const NONE: Self = Self {
139 deadline: None,
140 rate: 0.0,
141 };
142
143 pub fn within(d: Duration) -> Self {
145 Self {
146 deadline: Some(Instant::now() + d),
147 rate: 0.0,
148 }
149 }
150
151 pub fn at(deadline: Instant) -> Self {
153 Self {
154 deadline: Some(deadline),
155 rate: 0.0,
156 }
157 }
158
159 pub fn with_rate(self, rate: f32) -> Self {
161 Self { rate, ..self }
162 }
163
164 pub fn remaining(&self, now: Instant) -> Option<Duration> {
166 self.deadline.map(|d| d.saturating_duration_since(now))
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
172pub struct Command {
173 pub effector: String,
175 #[serde(default)]
178 pub args: serde_json::Value,
179}
180
181impl Command {
182 pub fn new(effector: &str, args: serde_json::Value) -> Self {
184 Self {
185 effector: effector.to_string(),
186 args,
187 }
188 }
189}
190
191#[derive(Clone, Debug, Default)]
194pub enum Payload {
195 #[default]
197 None,
198 Bytes(Bytes),
200 Text(Arc<str>),
202 Json(Arc<serde_json::Value>),
204 Tensors(Arc<Vec<HostTensor>>),
206 Command(Arc<Command>),
208 #[cfg(feature = "llm")]
211 Prompt(Arc<crate::llm::GenerateRequest>),
212}
213
214impl Payload {
215 pub fn text(s: &str) -> Self {
217 Self::Text(Arc::from(s))
218 }
219
220 pub fn bytes(b: impl Into<Bytes>) -> Self {
222 Self::Bytes(b.into())
223 }
224
225 pub fn json(v: serde_json::Value) -> Self {
227 Self::Json(Arc::new(v))
228 }
229
230 pub fn command(c: Command) -> Self {
232 Self::Command(Arc::new(c))
233 }
234
235 pub fn tensors(t: Vec<HostTensor>) -> Self {
237 Self::Tensors(Arc::new(t))
238 }
239
240 pub fn as_text(&self) -> Option<&str> {
242 match self {
243 Self::Text(s) => Some(s),
244 Self::Bytes(b) => std::str::from_utf8(b).ok(),
245 _ => None,
246 }
247 }
248
249 pub fn as_bytes(&self) -> Option<&[u8]> {
251 match self {
252 Self::Bytes(b) => Some(b),
253 Self::Text(s) => Some(s.as_bytes()),
254 _ => None,
255 }
256 }
257
258 pub fn as_command(&self) -> Option<&Command> {
260 match self {
261 Self::Command(c) => Some(c),
262 _ => None,
263 }
264 }
265
266 pub fn commands(&self, effector: &str) -> bool {
268 self.as_command().is_some_and(|c| c.effector == effector)
269 }
270
271 pub fn as_tensors(&self) -> Option<&[HostTensor]> {
273 match self {
274 Self::Tensors(t) => Some(t),
275 _ => None,
276 }
277 }
278
279 pub fn to_json(&self) -> Option<serde_json::Value> {
281 match self {
282 Self::Json(v) => Some((**v).clone()),
283 Self::Text(_) | Self::Bytes(_) => serde_json::from_slice(self.as_bytes()?).ok(),
284 Self::Command(c) => serde_json::to_value(&**c).ok(),
285 Self::None | Self::Tensors(_) => None,
286 #[cfg(feature = "llm")]
287 Self::Prompt(_) => None,
288 }
289 }
290
291 pub fn to_bytes(&self) -> Bytes {
294 match self {
295 Self::None => Bytes::new(),
296 Self::Bytes(b) => b.clone(),
297 Self::Text(s) => Bytes::copy_from_slice(s.as_bytes()),
298 Self::Json(v) => Bytes::from(v.to_string()),
299 Self::Command(c) => Bytes::from(serde_json::to_vec(&**c).unwrap_or_default()),
300 Self::Tensors(t) => Bytes::from(
301 t.iter()
302 .flat_map(|t| match &t.values {
303 crate::tensor::Values::F32(v) => {
304 v.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>()
305 }
306 crate::tensor::Values::I64(v) => {
307 v.iter().flat_map(|x| x.to_le_bytes()).collect()
308 }
309 })
310 .collect::<Vec<u8>>(),
311 ),
312 #[cfg(feature = "llm")]
313 Self::Prompt(_) => Bytes::new(),
314 }
315 }
316
317 pub fn kind(&self) -> &'static str {
319 match self {
320 Self::None => "none",
321 Self::Bytes(_) => "bytes",
322 Self::Text(_) => "text",
323 Self::Json(_) => "json",
324 Self::Tensors(_) => "tensors",
325 Self::Command(_) => "command",
326 #[cfg(feature = "llm")]
327 Self::Prompt(_) => "prompt",
328 }
329 }
330}
331
332pub(crate) const TRACE_HOPS: usize = 8;
334
335#[derive(Clone, Copy, Debug)]
337pub struct Trace {
338 hops: [NeuronId; TRACE_HOPS],
339 len: u8,
340 overflow: u16,
342 brain: bool,
343 queued: bool,
345}
346
347impl Default for Trace {
348 fn default() -> Self {
349 Self {
350 hops: [NeuronId::NONE; TRACE_HOPS],
351 len: 0,
352 overflow: 0,
353 brain: false,
354 queued: false,
355 }
356 }
357}
358
359impl Trace {
360 pub(crate) fn push(&mut self, id: NeuronId) {
361 if (self.len as usize) < TRACE_HOPS {
362 self.hops[self.len as usize] = id;
363 self.len += 1;
364 } else {
365 self.overflow = self.overflow.saturating_add(1);
366 }
367 }
368
369 pub(crate) fn mark_brain(&mut self) {
370 self.brain = true;
371 }
372
373 pub(crate) fn mark_queued(&mut self) {
374 self.queued = true;
375 }
376
377 pub fn hops(&self) -> &[NeuronId] {
379 &self.hops[..self.len as usize]
380 }
381
382 pub fn len(&self) -> usize {
384 self.len as usize + self.overflow as usize
385 }
386
387 pub fn is_empty(&self) -> bool {
389 self.len == 0 && self.overflow == 0
390 }
391
392 pub fn reached_brain(&self) -> bool {
394 self.brain
395 }
396
397 pub fn was_queued(&self) -> bool {
399 self.queued
400 }
401
402 pub fn last(&self) -> Option<NeuronId> {
404 self.hops().last().copied()
405 }
406}
407
408#[derive(Debug)]
410pub enum Answer {
411 Http {
414 status: u16,
416 content_type: String,
418 body: Bytes,
420 },
421 Tensors(Vec<HostTensor>),
424 Failed(Box<crate::error::FurnaceError>),
426 #[cfg(feature = "llm")]
429 Generation(
430 std::result::Result<
431 tokio::sync::mpsc::Receiver<crate::llm::Event>,
432 crate::llm::EngineError,
433 >,
434 ),
435}
436
437impl Answer {
438 pub fn text(status: u16, body: &str) -> Self {
440 Self::Http {
441 status,
442 content_type: "text/plain; charset=utf-8".to_string(),
443 body: Bytes::copy_from_slice(body.as_bytes()),
444 }
445 }
446
447 pub fn json(status: u16, value: &serde_json::Value) -> Self {
449 Self::Http {
450 status,
451 content_type: "application/json".to_string(),
452 body: Bytes::from(value.to_string()),
453 }
454 }
455
456 pub fn bytes(status: u16, content_type: &str, body: Bytes) -> Self {
458 Self::Http {
459 status,
460 content_type: content_type.to_string(),
461 body,
462 }
463 }
464
465 pub fn failed(error: crate::error::FurnaceError) -> Self {
467 Self::Failed(Box::new(error))
468 }
469}
470
471struct ReplyInner {
472 sender: Mutex<Option<oneshot::Sender<Answer>>>,
473 required: bool,
476}
477
478#[derive(Clone)]
482pub struct ReplyHandle(Arc<ReplyInner>);
483
484impl fmt::Debug for ReplyHandle {
485 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486 f.debug_struct("ReplyHandle")
487 .field("open", &self.is_open())
488 .field("required", &self.0.required)
489 .finish()
490 }
491}
492
493impl ReplyHandle {
494 pub fn new() -> (Self, oneshot::Receiver<Answer>) {
497 Self::make(false)
498 }
499
500 pub fn required() -> (Self, oneshot::Receiver<Answer>) {
503 Self::make(true)
504 }
505
506 fn make(required: bool) -> (Self, oneshot::Receiver<Answer>) {
507 let (sender, receiver) = oneshot::channel();
508 let inner = ReplyInner {
509 sender: Mutex::new(Some(sender)),
510 required,
511 };
512 (Self(Arc::new(inner)), receiver)
513 }
514
515 pub fn answer(&self, answer: Answer) -> bool {
517 let sender = self
518 .0
519 .sender
520 .lock()
521 .unwrap_or_else(|e| e.into_inner())
522 .take();
523 sender.is_some_and(|s| s.send(answer).is_ok())
524 }
525
526 pub fn is_open(&self) -> bool {
528 self.0
529 .sender
530 .lock()
531 .unwrap_or_else(|e| e.into_inner())
532 .as_ref()
533 .is_some_and(|s| !s.is_closed())
534 }
535
536 pub fn wanted(&self) -> bool {
539 !self.0.required || self.is_open()
540 }
541}
542
543#[derive(Clone, Debug)]
549pub struct Signal {
550 pub id: SignalId,
552 pub origin: NeuronId,
554 pub at: Instant,
556 pub urgency: Urgency,
558 pub feature: Feature,
560 pub intensity: f32,
562 pub payload: Payload,
564 pub trace: Trace,
566 pub reply: Option<ReplyHandle>,
568}
569
570impl Signal {
571 pub fn new(feature: Feature, payload: Payload) -> Self {
573 Self {
574 id: SignalId::next(),
575 origin: NeuronId::NONE,
576 at: Instant::now(),
577 urgency: Urgency::NONE,
578 feature,
579 intensity: 1.0,
580 payload,
581 trace: Trace::default(),
582 reply: None,
583 }
584 }
585
586 pub fn with_urgency(mut self, urgency: Urgency) -> Self {
588 self.urgency = urgency;
589 self
590 }
591
592 pub fn with_intensity(mut self, intensity: f32) -> Self {
594 self.intensity = intensity;
595 self
596 }
597
598 pub fn with_feature(mut self, feature: Feature) -> Self {
600 self.feature = feature;
601 self
602 }
603
604 pub fn with_reply(mut self, reply: ReplyHandle) -> Self {
606 self.reply = Some(reply);
607 self
608 }
609
610 pub fn derive(&self, payload: Payload) -> Self {
613 Self {
614 id: self.id,
615 origin: self.origin,
616 at: self.at,
617 urgency: self.urgency,
618 feature: self.feature,
619 intensity: self.intensity,
620 payload,
621 trace: self.trace,
622 reply: self.reply.clone(),
623 }
624 }
625
626 pub fn command(&self, effector: &str, args: serde_json::Value) -> Self {
628 self.derive(Payload::command(Command::new(effector, args)))
629 }
630
631 pub fn age(&self) -> Duration {
633 self.at.elapsed()
634 }
635
636 pub fn answer(&self, answer: Answer) -> bool {
638 self.reply.as_ref().is_some_and(|r| r.answer(answer))
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
647 fn sketches_tell_inputs_apart_and_repeat_for_equal_ones() {
648 assert_eq!(Feature::sketch(b"hello"), Feature::sketch(b"hello"));
649 assert_ne!(Feature::sketch(b"hello"), Feature::sketch(b"hellp"));
650 assert_ne!(Feature::sketch(b""), Feature::sketch(b"\0"));
651 assert_ne!(Feature::sketch(b"12345678"), Feature::sketch(b"123456789"));
652 assert_ne!(Feature::sketch(b"a"), Feature::sketch_as(2, b"a"));
653 assert!(!Feature::sketch(b"").is_none());
654 }
655
656 #[test]
657 fn trace_records_eight_hops_inline_and_counts_the_rest() {
658 let mut trace = Trace::default();
659 for i in 0..12 {
660 trace.push(NeuronId(i));
661 }
662 assert_eq!(trace.hops().len(), TRACE_HOPS);
663 assert_eq!(trace.len(), 12);
664 assert_eq!(trace.last(), Some(NeuronId(7)));
665 assert!(!trace.reached_brain());
666 }
667
668 #[test]
669 fn derived_signals_share_identity_and_reply() {
670 let (reply, mut receiver) = ReplyHandle::new();
671 let signal = Signal::new(Feature::sketch(b"x"), Payload::text("x"))
672 .with_reply(reply)
673 .with_urgency(Urgency::within(Duration::from_secs(1)));
674 let child = signal.command("log", serde_json::json!({"line": "x"}));
675 assert_eq!(child.id, signal.id);
676 assert_eq!(child.payload.as_command().unwrap().effector, "log");
677 assert!(child.urgency.remaining(Instant::now()).is_some());
678 assert!(child.answer(Answer::text(200, "ok")));
679 assert!(!signal.answer(Answer::text(200, "again")));
681 let Answer::Http { body, .. } = receiver.try_recv().unwrap() else {
682 panic!("an HTTP answer");
683 };
684 assert_eq!(body, Bytes::from_static(b"ok"));
685 }
686
687 #[test]
688 fn only_a_required_reply_makes_its_signal_unwanted_once_abandoned() {
689 let (optional, receiver) = ReplyHandle::new();
690 drop(receiver);
691 assert!(!optional.is_open());
692 assert!(optional.wanted());
693 let (required, receiver) = ReplyHandle::required();
694 assert!(required.wanted());
695 drop(receiver);
696 assert!(!required.wanted());
697 }
698
699 #[test]
700 fn payload_conversions() {
701 let json = Payload::text(r#"{"a":1}"#).to_json().unwrap();
702 assert_eq!(json["a"], 1);
703 assert_eq!(
704 Payload::json(json.clone()).to_bytes(),
705 Bytes::from(r#"{"a":1}"#)
706 );
707 let command = Payload::command(Command::new("w", serde_json::json!({"k": "v"})));
708 assert_eq!(command.to_json().unwrap()["effector"], "w");
709 assert!(Payload::None.as_text().is_none());
710 assert_eq!(Payload::bytes(&b"abc"[..]).as_text(), Some("abc"));
711 }
712}