1use crate::ports::{InferenceEngine, Ports};
4use el_core::{
5 DegradeReason, DomainEvent, EdgeError, EventEnvelope, Phase, Result, SessionConfig, SessionId,
6 StopReason, Token,
7};
8use el_memory::KvRegion;
9use el_provenance::LoadPermit;
10use el_safety::{
11 Checkpoint, CheckpointManager, LogitAdjustment, RollbackPolicy, SafetyModeSelector,
12};
13
14const MIN_CHECKPOINT_BUDGET_BYTES: u64 = 64 * 1024 * 1024;
18
19pub struct InferenceSession<E: InferenceEngine> {
23 id: SessionId,
24 config: SessionConfig,
25 phase: Phase,
26 engine: E,
27 kv: KvRegion,
28 permit: LoadPermit,
29 prompt: Vec<Token>,
32 output: Vec<Token>,
33 step: u32,
34 events: Vec<EventEnvelope>,
35}
36
37enum GuardVerdict {
39 Pass,
41 RolledBack,
43 FailClosed,
45}
46
47struct GuardState {
50 checkpoints: CheckpointManager,
51 rollback_count: u8,
52 banned: Vec<Token>,
53 start_out: u32,
56 start_kv: u32,
57}
58
59impl<E: InferenceEngine> InferenceSession<E> {
60 pub fn new(id: SessionId, config: SessionConfig, engine: E, permit: LoadPermit) -> Self {
61 let mut s = Self {
62 id,
63 config,
64 phase: Phase::Initialized,
65 engine,
66 kv: KvRegion::new(),
67 permit,
68 prompt: Vec::new(),
69 output: Vec::new(),
70 step: 0,
71 events: Vec::new(),
72 };
73 s.emit(DomainEvent::SessionInitialized {
74 runtime: config.format.runtime(),
75 device: config.device,
76 safety: config.safety,
77 speculation: config.speculation,
78 });
79 s.emit(DomainEvent::ModelLoaded {
80 model: permit.model,
81 version: permit.version,
82 format: permit.format,
83 });
84 s
85 }
86
87 pub fn phase(&self) -> Phase {
88 self.phase
89 }
90 pub fn output(&self) -> &[Token] {
91 &self.output
92 }
93 pub fn kv_len(&self) -> u32 {
94 self.kv.len()
95 }
96 pub fn config(&self) -> &SessionConfig {
97 &self.config
98 }
99 pub fn permit(&self) -> LoadPermit {
102 self.permit
103 }
104 pub fn drain_events(&mut self) -> Vec<EventEnvelope> {
107 std::mem::take(&mut self.events)
108 }
109
110 fn emit(&mut self, event: DomainEvent) {
111 self.events
112 .push(EventEnvelope::new(self.id, self.step, event));
113 }
114
115 pub fn load_prompt(&mut self, ports: &Ports, prompt: &[Token]) -> Result<()> {
117 if self.phase != Phase::Initialized {
118 return Err(EdgeError::InvalidPhase {
119 expected: "Initialized",
120 found: self.phase.as_str(),
121 });
122 }
123
124 self.prompt = prompt.to_vec();
127
128 let compressed = if self.config.compress {
129 ports.compressor.compress(prompt)
130 } else {
131 prompt.to_vec()
132 };
133 if compressed.len() < prompt.len() {
134 let ratio_milli =
135 ((compressed.len() as u64 * 1000) / (prompt.len().max(1) as u64)) as u32;
136 self.emit(DomainEvent::PromptCompressed {
137 input_tokens: prompt.len() as u32,
138 output_tokens: compressed.len() as u32,
139 ratio_milli,
140 });
141 }
142
143 self.phase = Phase::Prefilling;
144 let kv_len = self.engine.prefill(&compressed)?;
145 for _ in 0..kv_len {
146 let off = self.kv.len() as u64;
147 self.kv.push(off);
148 }
149 self.emit(DomainEvent::PrefillCompleted {
150 prompt_tokens: compressed.len() as u32,
151 kv_len,
152 prefill_tps: 0,
153 });
154 self.phase = Phase::Decoding;
155 Ok(())
156 }
157
158 pub fn continue_prompt(&mut self, _ports: &Ports, full_context: &[Token]) -> Result<()> {
190 if self.phase != Phase::Completed {
191 return Err(EdgeError::InvalidPhase {
192 expected: "Completed",
193 found: self.phase.as_str(),
194 });
195 }
196 self.prompt = full_context.to_vec();
199
200 self.phase = Phase::Prefilling;
201 let kv_len = self.engine.prefill_reuse(full_context)?;
202 self.kv = KvRegion::new();
204 for _ in 0..kv_len {
205 let off = self.kv.len() as u64;
206 self.kv.push(off);
207 }
208 self.output.clear();
211 self.emit(DomainEvent::PrefillCompleted {
212 prompt_tokens: full_context.len() as u32,
213 kv_len,
214 prefill_tps: 0,
215 });
216 self.phase = Phase::Decoding;
217 Ok(())
218 }
219
220 pub fn generate(&mut self, ports: &Ports, max_tokens: u32) -> Result<StopReason> {
223 let effective = SafetyModeSelector::resolve(self.config.safety, self.config.device);
228 self.emit(DomainEvent::SafetyModeSelected { mode: effective });
229 let policy = RollbackPolicy::for_device(self.config.device, effective);
230 self.generate_with_policy(ports, max_tokens, policy)
231 }
232
233 pub fn generate_with_policy(
253 &mut self,
254 ports: &Ports,
255 max_tokens: u32,
256 policy: RollbackPolicy,
257 ) -> Result<StopReason> {
258 if self.phase != Phase::Decoding {
259 return Err(EdgeError::InvalidPhase {
260 expected: "Decoding",
261 found: self.phase.as_str(),
262 });
263 }
264
265 if policy.active() {
271 if let Some(ingress) = ports.ingress.as_deref() {
272 let score = ingress.score(&self.prompt);
273 if score >= policy.hard_threshold {
274 self.emit(DomainEvent::SafetyViolationDetected {
275 score_milli: score.milli(),
276 threshold_milli: policy.hard_threshold.milli(),
277 });
278 self.phase = Phase::Completed;
279 self.emit(DomainEvent::GenerationCompleted {
280 total_tokens: self.output.len() as u32,
281 stop: StopReason::Stopped,
282 });
283 return Ok(StopReason::Stopped);
284 }
285 }
286 }
287
288 let eos = self.engine.eos_token();
289 let guarding = policy.guards() && ports.guard.is_some();
290
291 let checkpoints = if guarding {
294 if self.config.memory_budget_bytes < MIN_CHECKPOINT_BUDGET_BYTES {
295 self.emit(DomainEvent::SafetyDisabled {
296 reason: DegradeReason::MemoryPressure,
297 });
298 CheckpointManager::new(0)
299 } else {
300 CheckpointManager::new(policy.max_checkpoints)
301 }
302 } else {
303 CheckpointManager::new(0)
304 };
305 let mut state = GuardState {
306 checkpoints,
307 rollback_count: 0,
308 banned: Vec::new(),
309 start_out: self.output.len() as u32,
314 start_kv: self.kv.len(),
315 };
316 if state.checkpoints.enabled() {
318 state.checkpoints.push(Checkpoint {
319 output_len: state.start_out,
320 kv_len: state.start_kv,
321 });
322 }
323
324 let stop = loop {
325 let mut terminating: Option<StopReason> = None;
331
332 if self.output.len() as u32 >= max_tokens {
333 terminating = Some(StopReason::MaxTokens);
334 } else {
335 let logits = self.engine.next_logits(&self.output);
337 let vocab = logits.len();
338
339 let mut mask = ports.grammar.mask(&self.output, vocab);
342 for &t in &state.banned {
343 if let Some(slot) = mask.get_mut(t as usize) {
344 *slot = false;
345 }
346 }
347 let allowed = mask.iter().filter(|b| **b).count() as u32;
348 self.emit(DomainEvent::TokenMaskApplied { allowed });
349
350 let adj = if (self.output.len() as u32) < policy.steer_window {
357 if mask.iter().any(|&legal| !legal) {
362 let legal_logits: Vec<i32> = logits
363 .iter()
364 .zip(mask.iter())
365 .map(|(&l, &legal)| if legal { l } else { i32::MIN })
366 .collect();
367 ports.safety.adjust_with_logits(&self.output, &legal_logits)
368 } else {
369 ports.safety.adjust_with_logits(&self.output, &logits)
370 }
371 } else {
372 ports.safety.adjust(&self.output)
373 };
374 if !adj.is_empty() {
375 self.emit(DomainEvent::LogitsSteered {
376 adjustment_norm_milli: adj.l1_norm_milli(),
377 });
378 }
379
380 let token = match pick(&logits, &mask, &adj) {
384 Some(t) => t,
385 None => {
386 self.emit(DomainEvent::GrammarViolationBlocked);
387 break StopReason::Stopped;
388 }
389 };
390 self.emit(DomainEvent::TokenGenerated { sampled: false });
391
392 self.output.push(token);
394 self.kv.push(self.output.len() as u64);
395 self.step += 1;
396 self.emit(DomainEvent::TokenCommitted {
397 kv_len: self.kv.len(),
398 });
399
400 if token == eos {
401 terminating = Some(StopReason::Eos);
402 }
403 }
404
405 if guarding {
411 let guard = ports
412 .guard
413 .as_deref()
414 .expect("guarding implies a guard is wired");
415 let at_boundary = (self.output.len() as u32).is_multiple_of(policy.guard_every);
416 if terminating.is_some() || at_boundary {
417 match self.guard_chunk(guard, &policy, &mut state) {
418 GuardVerdict::FailClosed => break StopReason::Stopped,
420 GuardVerdict::RolledBack => continue,
423 GuardVerdict::Pass => {}
424 }
425 }
426 }
427
428 if let Some(reason) = terminating {
429 break reason;
430 }
431 };
432
433 self.phase = Phase::Completed;
434 self.emit(DomainEvent::GenerationCompleted {
435 total_tokens: self.output.len() as u32,
436 stop,
437 });
438 Ok(stop)
439 }
440
441 fn guard_chunk(
455 &mut self,
456 guard: &dyn el_safety::ChunkGuard,
457 policy: &RollbackPolicy,
458 state: &mut GuardState,
459 ) -> GuardVerdict {
460 let score = guard.score(&self.output);
461 if score >= policy.hard_threshold {
462 self.emit(DomainEvent::SafetyViolationDetected {
463 score_milli: score.milli(),
464 threshold_milli: policy.hard_threshold.milli(),
465 });
466 match state.checkpoints.last() {
467 Some(cp) if state.rollback_count < policy.max_rollbacks => {
468 if self.engine.rollback(cp.output_len).is_err() {
472 self.output.truncate(cp.output_len as usize);
473 self.kv.truncate(cp.kv_len);
474 return GuardVerdict::FailClosed;
475 }
476 if let Some(&bad) = self.output.get(cp.output_len as usize) {
478 state.banned.push(bad);
479 }
480 self.output.truncate(cp.output_len as usize);
481 self.kv.truncate(cp.kv_len);
482 state.rollback_count += 1;
483 self.emit(DomainEvent::ClaimBacktracked {
484 claim_index: cp.output_len,
485 });
486 GuardVerdict::RolledBack
487 }
488 _ => {
489 let (safe_out, safe_kv) = state
490 .checkpoints
491 .last()
492 .map_or((state.start_out, state.start_kv), |c| {
493 (c.output_len, c.kv_len)
494 });
495 self.output.truncate(safe_out as usize);
496 self.kv.truncate(safe_kv);
497 GuardVerdict::FailClosed
498 }
499 }
500 } else if score < policy.soft_threshold {
501 if state.checkpoints.enabled() {
503 state.checkpoints.push(Checkpoint {
504 output_len: self.output.len() as u32,
505 kv_len: self.kv.len(),
506 });
507 }
508 state.banned.clear();
509 GuardVerdict::Pass
510 } else {
511 GuardVerdict::Pass
513 }
514 }
515
516 pub fn reset(&mut self) -> Result<()> {
538 self.engine.reset_cache()?;
539 self.kv = KvRegion::new();
540 self.prompt.clear();
541 self.output.clear();
542 self.step = 0;
543 self.phase = Phase::Initialized;
544 self.emit(DomainEvent::SessionReset);
545 Ok(())
546 }
547
548 pub fn close(&mut self) -> Result<()> {
564 self.engine.reset_cache()?;
565 self.kv = KvRegion::new();
566 self.prompt = Vec::new();
567 self.output = Vec::new();
568 self.events = Vec::new();
569 self.step = 0;
570 self.phase = Phase::Initialized;
571 Ok(())
572 }
573
574 pub fn consult_relay(&mut self, ports: &Ports, query: &[Token]) -> Result<Vec<Token>> {
577 if !self.config.hybrid_mode {
578 return Err(EdgeError::AirGapViolation);
579 }
580 match &ports.relay {
581 Some(relay) => {
582 let out = relay.consult(query);
583 self.emit(DomainEvent::HybridRelayConsulted);
584 Ok(out)
585 }
586 None => Err(EdgeError::AirGapViolation),
587 }
588 }
589}
590
591fn pick(logits: &[i32], mask: &[bool], adj: &LogitAdjustment) -> Option<Token> {
596 let mut best: Option<Token> = None;
597 let mut best_val = i32::MIN;
598 for (i, &l) in logits.iter().enumerate() {
599 if mask.get(i).copied() == Some(false) {
600 continue;
601 }
602 let v = l.saturating_add(adj.delta_for(i as Token));
603 if v > best_val {
604 best_val = v;
605 best = Some(i as Token);
606 }
607 }
608 best
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::defaults::NullEngine;
615 use crate::ports::{GrammarMasker, Ports};
616 use el_core::{ModelFormat, ModelId, ModelVersion};
617 use el_provenance::{ModelArtifact, SignatureVerifier};
618 use el_safety::LightweightFilter;
619
620 struct OkVerifier;
621 impl SignatureVerifier for OkVerifier {
622 fn verify(&self, _b: &[u8], _s: &[u8], _k: u32) -> bool {
623 true
624 }
625 }
626
627 fn permit() -> LoadPermit {
628 let mut a = ModelArtifact::new(ModelId(1), ModelVersion::new(0, 1, 0), ModelFormat::Gguf);
629 a.verify(&OkVerifier, b"weights", b"sig", 1);
630 a.ensure_loadable().expect("verified artifact loads")
631 }
632
633 struct FixedEngine {
636 logits: Vec<i32>,
637 }
638 impl InferenceEngine for FixedEngine {
639 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
640 Ok(t.len() as u32)
641 }
642 fn next_logits(&mut self, _c: &[Token]) -> Vec<i32> {
643 self.logits.clone()
644 }
645 fn eos_token(&self) -> Token {
646 9999
647 }
648 fn rollback(&mut self, _keep: u32) -> Result<()> {
649 Ok(()) }
651 fn reset_cache(&mut self) -> Result<()> {
652 Ok(()) }
654 }
655
656 struct DisallowMasker(Vec<Token>);
658 impl GrammarMasker for DisallowMasker {
659 fn mask(&self, _recent: &[Token], vocab: usize) -> Vec<bool> {
660 (0..vocab as Token).map(|t| !self.0.contains(&t)).collect()
661 }
662 }
663
664 #[test]
665 fn full_lifecycle_init_prefill_decode_complete_reset() {
666 let mut s = InferenceSession::new(
667 SessionId(1),
668 SessionConfig::default(),
669 NullEngine::new(3, 8),
670 permit(),
671 );
672 assert_eq!(s.phase(), Phase::Initialized);
673
674 let ports = Ports::permissive();
675 s.load_prompt(&ports, &[10, 11, 12]).unwrap();
676 assert_eq!(s.phase(), Phase::Decoding);
677
678 let stop = s.generate(&ports, 16).unwrap();
679 assert_eq!(stop, StopReason::Eos); assert_eq!(s.output(), &[3]);
681 assert_eq!(s.phase(), Phase::Completed);
682
683 s.reset().unwrap();
684 assert_eq!(s.phase(), Phase::Initialized);
685 assert!(s.output().is_empty());
686 }
687
688 #[test]
689 fn decode_applies_grammar_before_safety_before_sampling() {
690 let engine = FixedEngine {
692 logits: vec![10, 9, 8, 7],
693 };
694 let mut s = InferenceSession::new(SessionId(2), SessionConfig::default(), engine, permit());
695
696 let ports = Ports {
697 compressor: Box::new(crate::defaults::IdentityCompressor),
698 grammar: Box::new(DisallowMasker(vec![0])), safety: Box::new(LightweightFilter::new(vec![1])), guard: None,
701 ingress: None,
702 relay: None,
703 };
704 s.load_prompt(&ports, &[1]).unwrap();
705 let stop = s.generate(&ports, 1).unwrap();
706
707 assert_eq!(stop, StopReason::MaxTokens);
708 assert_eq!(s.output(), &[2]);
711 }
712
713 #[test]
714 fn generate_before_load_prompt_is_invalid_phase() {
715 let mut s = InferenceSession::new(
716 SessionId(3),
717 SessionConfig::default(),
718 NullEngine::new(0, 4),
719 permit(),
720 );
721 let ports = Ports::permissive();
722 let err = s.generate(&ports, 4).unwrap_err();
723 assert!(matches!(err, EdgeError::InvalidPhase { .. }));
724 }
725
726 #[test]
727 fn relay_is_blocked_unless_hybrid_mode_opted_in() {
728 struct EchoRelay;
729 impl crate::ports::HybridRelay for EchoRelay {
730 fn consult(&self, q: &[Token]) -> Vec<Token> {
731 q.to_vec()
732 }
733 }
734
735 let mut s = InferenceSession::new(
737 SessionId(4),
738 SessionConfig::default(),
739 NullEngine::new(0, 4),
740 permit(),
741 );
742 let ports = Ports {
743 relay: Some(Box::new(EchoRelay)),
744 ..Ports::permissive()
745 };
746 assert_eq!(
747 s.consult_relay(&ports, &[1, 2]).unwrap_err(),
748 EdgeError::AirGapViolation
749 );
750
751 let cfg = SessionConfig {
753 hybrid_mode: true,
754 ..SessionConfig::default()
755 };
756 let mut s2 = InferenceSession::new(SessionId(5), cfg, NullEngine::new(0, 4), permit());
757 assert_eq!(s2.consult_relay(&ports, &[1, 2]).unwrap(), vec![1, 2]);
758
759 let no_relay = Ports::permissive();
761 assert_eq!(
762 s2.consult_relay(&no_relay, &[1]).unwrap_err(),
763 EdgeError::AirGapViolation
764 );
765 }
766
767 #[test]
768 fn first_events_are_init_then_model_loaded() {
769 let mut s = InferenceSession::new(
770 SessionId(6),
771 SessionConfig::default(),
772 NullEngine::new(0, 4),
773 permit(),
774 );
775 let evs = s.drain_events();
776 assert!(matches!(
777 evs[0].event,
778 DomainEvent::SessionInitialized { .. }
779 ));
780 assert!(matches!(evs[1].event, DomainEvent::ModelLoaded { .. }));
781 }
782
783 use el_safety::{ChunkGuard, SafetyScore};
786
787 struct BanToken(Token);
789 impl ChunkGuard for BanToken {
790 fn score(&self, recent: &[Token]) -> SafetyScore {
791 if recent.contains(&self.0) {
792 SafetyScore::MAX
793 } else {
794 SafetyScore::SAFE
795 }
796 }
797 }
798
799 struct AlwaysHot;
801 impl ChunkGuard for AlwaysHot {
802 fn score(&self, _recent: &[Token]) -> SafetyScore {
803 SafetyScore::MAX
804 }
805 }
806
807 fn tiny_policy(max_rollbacks: u8) -> RollbackPolicy {
808 RollbackPolicy {
809 guard_every: 1,
810 steer_window: 0,
811 soft_threshold: SafetyScore::from_milli(500),
812 hard_threshold: SafetyScore::from_milli(800),
813 max_rollbacks,
814 max_checkpoints: 8,
815 }
816 }
817
818 struct DenyAllMasker;
821 impl GrammarMasker for DenyAllMasker {
822 fn mask(&self, _recent: &[Token], vocab: usize) -> Vec<bool> {
823 vec![false; vocab]
824 }
825 }
826
827 fn descending_engine() -> FixedEngine {
830 FixedEngine {
831 logits: vec![5, 4, 3, 2],
832 }
833 }
834
835 struct UnsafeThenEos {
839 eos: Token,
840 vocab: usize,
841 }
842 impl InferenceEngine for UnsafeThenEos {
843 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
844 Ok(t.len() as u32)
845 }
846 fn next_logits(&mut self, ctx: &[Token]) -> Vec<i32> {
847 let mut v = vec![0i32; self.vocab];
848 if ctx.is_empty() {
849 v[0] = 10; } else {
851 v[self.eos as usize] = 10; }
853 v
854 }
855 fn eos_token(&self) -> Token {
856 self.eos
857 }
858 fn rollback(&mut self, _keep: u32) -> Result<()> {
859 Ok(()) }
861 fn reset_cache(&mut self) -> Result<()> {
862 Ok(()) }
864 }
865
866 fn coarse_policy(max_rollbacks: u8) -> RollbackPolicy {
869 RollbackPolicy {
870 guard_every: 16,
871 steer_window: 0,
872 soft_threshold: SafetyScore::from_milli(500),
873 hard_threshold: SafetyScore::from_milli(800),
874 max_rollbacks,
875 max_checkpoints: 8,
876 }
877 }
878
879 #[test]
880 fn eos_terminated_short_completion_is_scored_not_bypassed() {
881 let mut s = InferenceSession::new(
884 SessionId(26),
885 SessionConfig::default(),
886 UnsafeThenEos { eos: 5, vocab: 8 },
887 permit(),
888 );
889 let ports = guarded_ports(Box::new(BanToken(0)));
890 s.load_prompt(&ports, &[]).unwrap();
891
892 let stop = s.generate_with_policy(&ports, 8, coarse_policy(0)).unwrap();
894
895 assert_eq!(stop, StopReason::Stopped);
896 assert!(s.output().is_empty()); let evs = s.drain_events();
898 assert!(evs
899 .iter()
900 .any(|e| matches!(e.event, DomainEvent::SafetyViolationDetected { .. })));
901 }
902
903 #[test]
904 fn max_tokens_partial_chunk_is_scored_not_bypassed() {
905 let mut s = InferenceSession::new(
908 SessionId(27),
909 SessionConfig::default(),
910 descending_engine(), permit(),
912 );
913 let ports = guarded_ports(Box::new(BanToken(0)));
914 s.load_prompt(&ports, &[]).unwrap();
915
916 let stop = s.generate_with_policy(&ports, 2, coarse_policy(0)).unwrap();
918
919 assert_eq!(stop, StopReason::Stopped);
920 assert!(s.output().is_empty());
921 let evs = s.drain_events();
922 assert!(evs
923 .iter()
924 .any(|e| matches!(e.event, DomainEvent::SafetyViolationDetected { .. })));
925 }
926
927 #[test]
928 fn eos_unsafe_tail_rolls_back_and_recovers() {
929 let mut s = InferenceSession::new(
933 SessionId(28),
934 SessionConfig::default(),
935 UnsafeThenEos { eos: 5, vocab: 8 },
936 permit(),
937 );
938 let ports = guarded_ports(Box::new(BanToken(0)));
939 s.load_prompt(&ports, &[]).unwrap();
940
941 let stop = s.generate_with_policy(&ports, 8, coarse_policy(1)).unwrap();
942
943 assert_eq!(stop, StopReason::Eos);
944 assert!(!s.output().contains(&0)); assert_eq!(s.output().last(), Some(&5)); let evs = s.drain_events();
947 assert!(evs
948 .iter()
949 .any(|e| matches!(e.event, DomainEvent::ClaimBacktracked { .. })));
950 }
951
952 struct StatefulEngine {
959 fed: usize,
960 logits: Vec<i32>,
961 eos: Token,
962 rollbacks: std::rc::Rc<std::cell::Cell<u32>>,
963 last_keep: std::rc::Rc<std::cell::Cell<u32>>,
964 desynced: std::rc::Rc<std::cell::Cell<bool>>,
965 }
966 impl InferenceEngine for StatefulEngine {
967 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
968 self.fed = 0;
969 Ok(t.len() as u32)
970 }
971 fn next_logits(&mut self, committed: &[Token]) -> Vec<i32> {
972 if self.fed > committed.len() {
975 self.desynced.set(true);
976 }
977 while self.fed < committed.len() {
978 self.fed += 1;
979 }
980 self.logits.clone()
981 }
982 fn eos_token(&self) -> Token {
983 self.eos
984 }
985 fn rollback(&mut self, keep_committed: u32) -> Result<()> {
986 self.fed = keep_committed as usize; self.rollbacks.set(self.rollbacks.get() + 1);
988 self.last_keep.set(keep_committed);
989 Ok(())
990 }
991 fn reset_cache(&mut self) -> Result<()> {
992 self.fed = 0; Ok(())
994 }
995 }
996
997 #[test]
998 fn rollback_restores_engine_state_not_just_session_metadata() {
999 let rollbacks = std::rc::Rc::new(std::cell::Cell::new(0u32));
1003 let last_keep = std::rc::Rc::new(std::cell::Cell::new(u32::MAX));
1004 let desynced = std::rc::Rc::new(std::cell::Cell::new(false));
1005 let engine = StatefulEngine {
1006 fed: 0,
1007 logits: vec![5, 4, 3, 2], eos: 9999,
1009 rollbacks: rollbacks.clone(),
1010 last_keep: last_keep.clone(),
1011 desynced: desynced.clone(),
1012 };
1013 let mut s =
1014 InferenceSession::new(SessionId(29), SessionConfig::default(), engine, permit());
1015 let ports = guarded_ports(Box::new(BanToken(0)));
1016 s.load_prompt(&ports, &[7, 8]).unwrap(); let stop = s.generate_with_policy(&ports, 3, tiny_policy(3)).unwrap();
1019
1020 assert_eq!(stop, StopReason::MaxTokens);
1021 assert_eq!(s.output(), &[1, 1, 1]); assert!(
1024 rollbacks.get() >= 1,
1025 "session must propagate the backtrack to the engine"
1026 );
1027 assert!(last_keep.get() < 3);
1029 assert!(
1030 !desynced.get(),
1031 "engine cache must track the session rollback"
1032 );
1033 }
1034
1035 fn guarded_ports(guard: Box<dyn ChunkGuard>) -> Ports {
1036 Ports {
1037 compressor: Box::new(crate::defaults::IdentityCompressor),
1038 grammar: Box::new(crate::defaults::AllowAllMasker),
1039 safety: Box::new(el_safety::NoSafety),
1040 guard: Some(guard),
1041 ingress: None,
1042 relay: None,
1043 }
1044 }
1045
1046 #[test]
1047 fn hard_breach_rolls_back_kv_and_recovers() {
1048 let mut s = InferenceSession::new(
1049 SessionId(20),
1050 SessionConfig::default(),
1051 descending_engine(),
1052 permit(),
1053 );
1054 let ports = guarded_ports(Box::new(BanToken(0)));
1055 s.load_prompt(&ports, &[]).unwrap();
1056
1057 let stop = s.generate_with_policy(&ports, 3, tiny_policy(3)).unwrap();
1058
1059 assert_eq!(stop, StopReason::MaxTokens);
1060 assert_eq!(s.output(), &[1, 1, 1]);
1063 assert!(!s.output().contains(&0));
1064 assert_eq!(s.kv_len(), s.output().len() as u32);
1066
1067 let evs = s.drain_events();
1068 assert!(evs
1069 .iter()
1070 .any(|e| matches!(e.event, DomainEvent::ClaimBacktracked { .. })));
1071 assert!(evs
1072 .iter()
1073 .any(|e| matches!(e.event, DomainEvent::SafetyViolationDetected { .. })));
1074 }
1075
1076 #[test]
1077 fn fail_closed_refusal_when_no_rollback_budget() {
1078 let mut s = InferenceSession::new(
1079 SessionId(21),
1080 SessionConfig::default(),
1081 descending_engine(),
1082 permit(),
1083 );
1084 let ports = guarded_ports(Box::new(BanToken(0)));
1085 s.load_prompt(&ports, &[]).unwrap();
1086
1087 let stop = s.generate_with_policy(&ports, 5, tiny_policy(0)).unwrap();
1089
1090 assert_eq!(stop, StopReason::Stopped);
1091 assert!(s.output().is_empty());
1092 let evs = s.drain_events();
1093 assert!(evs
1094 .iter()
1095 .any(|e| matches!(e.event, DomainEvent::SafetyViolationDetected { .. })));
1096 assert!(!evs
1097 .iter()
1098 .any(|e| matches!(e.event, DomainEvent::ClaimBacktracked { .. })));
1099 }
1100
1101 #[test]
1102 fn rollbacks_are_bounded_then_fail_closed() {
1103 let mut s = InferenceSession::new(
1104 SessionId(22),
1105 SessionConfig::default(),
1106 descending_engine(),
1107 permit(),
1108 );
1109 let ports = guarded_ports(Box::new(AlwaysHot));
1110 s.load_prompt(&ports, &[]).unwrap();
1111
1112 let stop = s.generate_with_policy(&ports, 8, tiny_policy(2)).unwrap();
1113
1114 assert_eq!(stop, StopReason::Stopped);
1115 let evs = s.drain_events();
1116 let rollbacks = evs
1117 .iter()
1118 .filter(|e| matches!(e.event, DomainEvent::ClaimBacktracked { .. }))
1119 .count();
1120 assert_eq!(rollbacks, 2);
1122 }
1123
1124 #[test]
1125 fn memory_pressure_disables_checkpoints_and_fails_closed() {
1126 let cfg = SessionConfig {
1127 memory_budget_bytes: 1024, ..SessionConfig::default()
1129 };
1130 let mut s = InferenceSession::new(SessionId(23), cfg, descending_engine(), permit());
1131 let ports = guarded_ports(Box::new(BanToken(0)));
1132 s.load_prompt(&ports, &[]).unwrap();
1133
1134 let stop = s.generate_with_policy(&ports, 5, tiny_policy(3)).unwrap();
1135
1136 assert_eq!(stop, StopReason::Stopped); let evs = s.drain_events();
1138 assert!(evs.iter().any(|e| matches!(
1139 e.event,
1140 DomainEvent::SafetyDisabled {
1141 reason: DegradeReason::MemoryPressure
1142 }
1143 )));
1144 }
1145
1146 #[test]
1147 fn no_legal_token_fails_closed() {
1148 let mut s = InferenceSession::new(
1151 SessionId(24),
1152 SessionConfig::default(),
1153 descending_engine(),
1154 permit(),
1155 );
1156 let ports = Ports {
1157 compressor: Box::new(crate::defaults::IdentityCompressor),
1158 grammar: Box::new(DenyAllMasker),
1159 safety: Box::new(el_safety::NoSafety),
1160 guard: None,
1161 ingress: None,
1162 relay: None,
1163 };
1164 s.load_prompt(&ports, &[]).unwrap();
1165
1166 let stop = s.generate(&ports, 4).unwrap();
1167
1168 assert_eq!(stop, StopReason::Stopped);
1169 assert!(s.output().is_empty()); let evs = s.drain_events();
1171 assert!(evs
1172 .iter()
1173 .any(|e| matches!(e.event, DomainEvent::GrammarViolationBlocked)));
1174 }
1175
1176 #[test]
1177 fn fail_closed_preserves_prefill_kv() {
1178 let mut s = InferenceSession::new(
1182 SessionId(25),
1183 SessionConfig::default(),
1184 descending_engine(),
1185 permit(),
1186 );
1187 let ports = guarded_ports(Box::new(AlwaysHot));
1188 s.load_prompt(&ports, &[7, 8, 9]).unwrap(); assert_eq!(s.kv_len(), 3);
1190
1191 let stop = s.generate_with_policy(&ports, 8, tiny_policy(1)).unwrap();
1192
1193 assert_eq!(stop, StopReason::Stopped);
1194 assert!(s.output().is_empty()); assert_eq!(s.kv_len(), 3); }
1197
1198 use el_core::SafetyMode;
1201 use el_safety::SafetySteerer;
1202
1203 struct RecordingSteerer {
1206 log: std::rc::Rc<std::cell::RefCell<Vec<(bool, usize)>>>,
1207 }
1208 impl SafetySteerer for RecordingSteerer {
1209 fn adjust(&self, recent: &[Token]) -> LogitAdjustment {
1210 self.log.borrow_mut().push((false, recent.len()));
1211 LogitAdjustment::none()
1212 }
1213 fn adjust_with_logits(&self, recent: &[Token], _base: &[i32]) -> LogitAdjustment {
1214 self.log.borrow_mut().push((true, recent.len()));
1215 LogitAdjustment::none()
1216 }
1217 fn mode(&self) -> SafetyMode {
1218 SafetyMode::SecDecoding
1219 }
1220 }
1221
1222 #[test]
1223 fn soft_steer_applies_only_inside_the_early_token_window() {
1224 let log = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
1227 let mut s = InferenceSession::new(
1228 SessionId(30),
1229 SessionConfig::default(),
1230 descending_engine(), permit(),
1232 );
1233 let ports = Ports {
1234 compressor: Box::new(crate::defaults::IdentityCompressor),
1235 grammar: Box::new(crate::defaults::AllowAllMasker),
1236 safety: Box::new(RecordingSteerer { log: log.clone() }),
1237 guard: None,
1238 ingress: None,
1239 relay: None,
1240 };
1241 s.load_prompt(&ports, &[]).unwrap();
1242 let policy = RollbackPolicy {
1243 guard_every: 0,
1244 steer_window: 2,
1245 soft_threshold: SafetyScore::MAX,
1246 hard_threshold: SafetyScore::MAX,
1247 max_rollbacks: 0,
1248 max_checkpoints: 0,
1249 };
1250 s.generate_with_policy(&ports, 4, policy).unwrap();
1251
1252 let calls = log.borrow();
1253 assert_eq!(calls.len(), 4);
1254 for &(with_logits, len) in calls.iter() {
1255 assert_eq!(
1256 with_logits,
1257 len < 2,
1258 "window gate wrong at output len {len}"
1259 );
1260 }
1261 }
1262
1263 #[test]
1264 fn ingress_triage_fails_closed_before_generation() {
1265 let mut s = InferenceSession::new(
1267 SessionId(31),
1268 SessionConfig::default(),
1269 descending_engine(),
1270 permit(),
1271 );
1272 let ports = Ports {
1273 compressor: Box::new(crate::defaults::IdentityCompressor),
1274 grammar: Box::new(crate::defaults::AllowAllMasker),
1275 safety: Box::new(el_safety::NoSafety),
1276 guard: None,
1277 ingress: Some(Box::new(AlwaysHot)), relay: None,
1279 };
1280 s.load_prompt(&ports, &[1, 2, 3]).unwrap();
1281
1282 let stop = s.generate_with_policy(&ports, 8, coarse_policy(0)).unwrap();
1283
1284 assert_eq!(stop, StopReason::Stopped);
1285 assert!(s.output().is_empty());
1286 let evs = s.drain_events();
1287 assert!(evs
1288 .iter()
1289 .any(|e| matches!(e.event, DomainEvent::SafetyViolationDetected { .. })));
1290 assert!(!evs
1292 .iter()
1293 .any(|e| matches!(e.event, DomainEvent::TokenCommitted { .. })));
1294 }
1295
1296 #[test]
1297 fn generate_applies_safety_mode_selector_and_records_effective_mode() {
1298 let cfg = SessionConfig {
1301 device: el_core::DeviceTarget::MidRange,
1302 safety: SafetyMode::SecDecoding,
1303 ..SessionConfig::default()
1304 };
1305 let mut s = InferenceSession::new(SessionId(32), cfg, NullEngine::new(0, 4), permit());
1306 let ports = Ports::permissive();
1307 s.load_prompt(&ports, &[1]).unwrap();
1308 s.generate(&ports, 4).unwrap();
1309
1310 let evs = s.drain_events();
1311 assert!(evs.iter().any(|e| matches!(
1312 e.event,
1313 DomainEvent::SafetyModeSelected {
1314 mode: SafetyMode::Lightweight
1315 }
1316 )));
1317 }
1318
1319 struct CountingEngine {
1325 eos: Token,
1326 vocab: usize,
1327 prefills: std::rc::Rc<std::cell::Cell<u32>>,
1328 resets: std::rc::Rc<std::cell::Cell<u32>>,
1329 }
1330 impl InferenceEngine for CountingEngine {
1331 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
1332 self.prefills.set(self.prefills.get() + 1);
1333 Ok(t.len() as u32)
1334 }
1335 fn next_logits(&mut self, _committed: &[Token]) -> Vec<i32> {
1336 let mut v = vec![0i32; self.vocab];
1337 if let Some(s) = v.get_mut(self.eos as usize) {
1338 *s = 1;
1339 }
1340 v
1341 }
1342 fn eos_token(&self) -> Token {
1343 self.eos
1344 }
1345 fn rollback(&mut self, _keep: u32) -> Result<()> {
1346 Ok(())
1347 }
1348 fn reset_cache(&mut self) -> Result<()> {
1349 self.resets.set(self.resets.get() + 1);
1350 Ok(())
1351 }
1352 }
1353
1354 fn counting_engine() -> (
1355 CountingEngine,
1356 std::rc::Rc<std::cell::Cell<u32>>,
1357 std::rc::Rc<std::cell::Cell<u32>>,
1358 ) {
1359 let prefills = std::rc::Rc::new(std::cell::Cell::new(0u32));
1360 let resets = std::rc::Rc::new(std::cell::Cell::new(0u32));
1361 let engine = CountingEngine {
1362 eos: 1,
1363 vocab: 4,
1364 prefills: prefills.clone(),
1365 resets: resets.clone(),
1366 };
1367 (engine, prefills, resets)
1368 }
1369
1370 #[test]
1371 fn reset_resets_the_engine_cache() {
1372 let (engine, _prefills, resets) = counting_engine();
1376 let mut s =
1377 InferenceSession::new(SessionId(40), SessionConfig::default(), engine, permit());
1378 assert_eq!(resets.get(), 0);
1379 s.reset().unwrap();
1380 assert_eq!(
1381 resets.get(),
1382 1,
1383 "reset() must reset the engine cache (ADR-018)"
1384 );
1385 }
1386
1387 #[test]
1388 fn one_engine_serves_multiple_conversations_via_reset() {
1389 let (engine, prefills, resets) = counting_engine();
1393 let mut s =
1394 InferenceSession::new(SessionId(41), SessionConfig::default(), engine, permit());
1395 let ports = Ports::permissive();
1396
1397 s.load_prompt(&ports, &[1, 2]).unwrap();
1398 s.generate(&ports, 8).unwrap();
1399 assert_eq!(prefills.get(), 1);
1400
1401 s.reset().unwrap();
1403 s.load_prompt(&ports, &[3, 4, 5]).unwrap();
1404 s.generate(&ports, 8).unwrap();
1405
1406 assert_eq!(
1407 prefills.get(),
1408 2,
1409 "the one resident engine was prefilled again, not rebuilt"
1410 );
1411 assert!(
1412 resets.get() >= 1,
1413 "the engine cache was reset between conversations"
1414 );
1415 }
1416
1417 struct ResidentEngine {
1420 eos: Token,
1421 vocab: usize,
1422 resets: std::rc::Rc<std::cell::Cell<u32>>,
1423 dropped: std::rc::Rc<std::cell::Cell<u32>>,
1424 }
1425 impl Drop for ResidentEngine {
1426 fn drop(&mut self) {
1427 self.dropped.set(self.dropped.get() + 1);
1428 }
1429 }
1430 impl InferenceEngine for ResidentEngine {
1431 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
1432 Ok(t.len() as u32)
1433 }
1434 fn next_logits(&mut self, _committed: &[Token]) -> Vec<i32> {
1435 let mut v = vec![0i32; self.vocab];
1436 if let Some(s) = v.get_mut(self.eos as usize) {
1437 *s = 1;
1438 }
1439 v
1440 }
1441 fn eos_token(&self) -> Token {
1442 self.eos
1443 }
1444 fn rollback(&mut self, _keep: u32) -> Result<()> {
1445 Ok(())
1446 }
1447 fn reset_cache(&mut self) -> Result<()> {
1448 self.resets.set(self.resets.get() + 1);
1449 Ok(())
1450 }
1451 }
1452
1453 #[test]
1454 fn close_releases_conversation_but_keeps_engine_resident() {
1455 let resets = std::rc::Rc::new(std::cell::Cell::new(0u32));
1460 let dropped = std::rc::Rc::new(std::cell::Cell::new(0u32));
1461 let engine = ResidentEngine {
1462 eos: 1,
1463 vocab: 4,
1464 resets: resets.clone(),
1465 dropped: dropped.clone(),
1466 };
1467 let mut s =
1468 InferenceSession::new(SessionId(42), SessionConfig::default(), engine, permit());
1469 let ports = Ports::permissive();
1470 s.load_prompt(&ports, &[1, 2, 3]).unwrap();
1471 s.generate(&ports, 8).unwrap();
1472 assert!(s.kv_len() > 0, "conversation memory is measurable (AC-4)");
1473
1474 s.close().unwrap();
1475
1476 assert_eq!(s.kv_len(), 0, "close frees the session's KV descriptors");
1477 assert!(s.output().is_empty(), "close frees committed output");
1478 assert!(
1479 resets.get() >= 1,
1480 "close releases the engine's conversation KV (keeps weights)"
1481 );
1482 assert_eq!(
1483 dropped.get(),
1484 0,
1485 "the resident engine/weights are NOT dropped by close"
1486 );
1487
1488 s.load_prompt(&ports, &[4, 5]).unwrap();
1490 s.generate(&ports, 8).unwrap();
1491 assert!(
1492 s.kv_len() > 0,
1493 "same engine serves a new conversation after close"
1494 );
1495 assert_eq!(dropped.get(), 0);
1496 }
1497
1498 #[test]
1499 fn reset_preserves_undrained_events() {
1500 let (engine, _prefills, _resets) = counting_engine();
1504 let mut s =
1505 InferenceSession::new(SessionId(44), SessionConfig::default(), engine, permit());
1506 let ports = Ports::permissive();
1507 s.load_prompt(&ports, &[1, 2]).unwrap();
1508 s.generate(&ports, 8).unwrap(); s.reset().unwrap();
1511
1512 let evs = s.drain_events();
1513 assert!(
1514 evs.iter()
1515 .any(|e| matches!(e.event, DomainEvent::TokenCommitted { .. })),
1516 "pre-reset events are preserved for the consumer"
1517 );
1518 assert!(
1519 evs.iter()
1520 .any(|e| matches!(e.event, DomainEvent::SessionReset)),
1521 "and the reset itself is recorded"
1522 );
1523 }
1524
1525 struct FailResetEngine {
1527 eos: Token,
1528 vocab: usize,
1529 }
1530 impl InferenceEngine for FailResetEngine {
1531 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
1532 Ok(t.len() as u32)
1533 }
1534 fn next_logits(&mut self, _committed: &[Token]) -> Vec<i32> {
1535 let mut v = vec![0i32; self.vocab];
1536 if let Some(s) = v.get_mut(self.eos as usize) {
1537 *s = 1;
1538 }
1539 v
1540 }
1541 fn eos_token(&self) -> Token {
1542 self.eos
1543 }
1544 fn rollback(&mut self, _keep: u32) -> Result<()> {
1545 Ok(())
1546 }
1547 fn reset_cache(&mut self) -> Result<()> {
1548 Err(EdgeError::Engine("reset_cache refused"))
1549 }
1550 }
1551
1552 #[test]
1553 fn reset_propagates_engine_cache_failure_and_leaves_state() {
1554 let mut s = InferenceSession::new(
1558 SessionId(45),
1559 SessionConfig::default(),
1560 FailResetEngine { eos: 1, vocab: 4 },
1561 permit(),
1562 );
1563 let ports = Ports::permissive();
1564 s.load_prompt(&ports, &[1, 2, 3]).unwrap();
1565 s.generate(&ports, 8).unwrap();
1566 let kv_before = s.kv_len();
1567 assert!(kv_before > 0);
1568
1569 let err = s.reset().unwrap_err();
1570 assert!(matches!(err, EdgeError::Engine(_)));
1571 assert_eq!(
1572 s.kv_len(),
1573 kv_before,
1574 "descriptors must be left intact when the engine reset fails"
1575 );
1576 assert_eq!(
1577 s.phase(),
1578 Phase::Completed,
1579 "phase is unchanged on a failed reset"
1580 );
1581 }
1582
1583 struct RetryClearEngine {
1587 eos: Token,
1588 vocab: usize,
1589 dirty: bool,
1590 fails_left: std::rc::Rc<std::cell::Cell<u32>>,
1591 cleared: std::rc::Rc<std::cell::Cell<bool>>,
1592 }
1593 impl InferenceEngine for RetryClearEngine {
1594 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
1595 self.dirty = true;
1596 Ok(t.len() as u32)
1597 }
1598 fn next_logits(&mut self, _committed: &[Token]) -> Vec<i32> {
1599 self.dirty = true;
1600 let mut v = vec![0i32; self.vocab];
1601 if let Some(s) = v.get_mut(self.eos as usize) {
1602 *s = 1;
1603 }
1604 v
1605 }
1606 fn eos_token(&self) -> Token {
1607 self.eos
1608 }
1609 fn rollback(&mut self, _keep: u32) -> Result<()> {
1610 Ok(())
1611 }
1612 fn reset_cache(&mut self) -> Result<()> {
1613 if self.dirty {
1614 if self.fails_left.get() > 0 {
1615 self.fails_left.set(self.fails_left.get() - 1);
1616 return Err(EdgeError::Engine("partial eviction")); }
1618 self.dirty = false;
1619 self.cleared.set(true);
1620 }
1621 Ok(())
1622 }
1623 }
1624
1625 #[test]
1626 fn reset_retries_clearing_after_a_failed_attempt() {
1627 let fails_left = std::rc::Rc::new(std::cell::Cell::new(1u32));
1632 let cleared = std::rc::Rc::new(std::cell::Cell::new(false));
1633 let engine = RetryClearEngine {
1634 eos: 1,
1635 vocab: 4,
1636 dirty: false,
1637 fails_left: fails_left.clone(),
1638 cleared: cleared.clone(),
1639 };
1640 let mut s =
1641 InferenceSession::new(SessionId(47), SessionConfig::default(), engine, permit());
1642 let ports = Ports::permissive();
1643 s.load_prompt(&ports, &[1, 2]).unwrap();
1644 s.generate(&ports, 8).unwrap();
1645
1646 assert!(s.reset().is_err());
1648 assert!(
1649 !cleared.get(),
1650 "a failed reset must not report the cache cleared"
1651 );
1652
1653 s.reset().unwrap();
1655 assert!(
1656 cleared.get(),
1657 "the next reset re-clears after a failed attempt"
1658 );
1659 }
1660
1661 struct ReuseEngine {
1671 eos: Token,
1672 vocab: usize,
1673 index_pos: usize,
1674 fed: usize,
1675 cached: Vec<Token>,
1676 prompt: Vec<Token>,
1677 last_logits: Vec<i32>,
1680 forwards: std::rc::Rc<std::cell::Cell<u32>>,
1681 eos_immediately: bool,
1684 }
1685 impl ReuseEngine {
1686 fn new(eos: Token, vocab: usize, eos_immediately: bool) -> Self {
1687 Self {
1688 eos,
1689 vocab,
1690 index_pos: 0,
1691 fed: 0,
1692 cached: Vec::new(),
1693 prompt: Vec::new(),
1694 last_logits: Vec::new(),
1695 forwards: std::rc::Rc::new(std::cell::Cell::new(0)),
1696 eos_immediately,
1697 }
1698 }
1699 fn forward(&mut self, t: Token) {
1700 self.forwards.set(self.forwards.get() + 1);
1701 self.cached.push(t);
1702 self.index_pos += 1;
1703 self.last_logits = self.logits_for_cache();
1704 }
1705 fn logits_for_cache(&self) -> Vec<i32> {
1706 let mut v = vec![0i32; self.vocab];
1707 if self.eos_immediately {
1708 if let Some(s) = v.get_mut(self.eos as usize) {
1709 *s = 1_000_000;
1710 }
1711 return v;
1712 }
1713 let h = self
1716 .cached
1717 .iter()
1718 .fold(0i32, |a, &t| a.wrapping_mul(31).wrapping_add(t as i32 + 1));
1719 for (i, slot) in v.iter_mut().enumerate() {
1720 *slot = h.wrapping_add(i as i32);
1721 }
1722 v
1723 }
1724 }
1725 impl InferenceEngine for ReuseEngine {
1726 fn prefill(&mut self, tokens: &[Token]) -> Result<u32> {
1727 self.index_pos = 0;
1728 self.fed = 0;
1729 self.cached = Vec::new();
1730 self.last_logits = Vec::new();
1731 self.prompt = tokens.to_vec();
1732 for &t in tokens {
1733 self.forward(t);
1734 }
1735 Ok(tokens.len() as u32)
1736 }
1737 fn next_logits(&mut self, committed: &[Token]) -> Vec<i32> {
1738 while self.fed < committed.len() {
1739 self.forward(committed[self.fed]);
1740 self.fed += 1;
1741 }
1742 self.last_logits.clone()
1743 }
1744 fn eos_token(&self) -> Token {
1745 self.eos
1746 }
1747 fn rollback(&mut self, _keep: u32) -> Result<()> {
1748 self.index_pos = 0;
1749 self.fed = 0;
1750 self.cached = Vec::new();
1751 self.last_logits = Vec::new();
1752 let prompt = self.prompt.clone();
1753 for &t in &prompt {
1754 self.forward(t);
1755 }
1756 Ok(())
1757 }
1758 fn reset_cache(&mut self) -> Result<()> {
1759 self.index_pos = 0;
1760 self.fed = 0;
1761 self.cached = Vec::new();
1762 self.last_logits = Vec::new();
1763 self.prompt = Vec::new();
1764 Ok(())
1765 }
1766 fn prefill_reuse(&mut self, full_context: &[Token]) -> Result<u32> {
1767 let reuse = self
1768 .cached
1769 .iter()
1770 .zip(full_context)
1771 .take_while(|(a, b)| a == b)
1772 .count();
1773 if reuse == self.cached.len() && reuse == self.index_pos {
1774 for &t in &full_context[reuse..] {
1775 self.forward(t);
1776 }
1777 } else {
1778 self.index_pos = 0;
1779 self.cached = Vec::new();
1780 self.last_logits = Vec::new(); for &t in full_context {
1782 self.forward(t);
1783 }
1784 }
1785 self.fed = 0;
1786 self.prompt = full_context.to_vec();
1787 Ok(self.index_pos as u32)
1788 }
1789 }
1790
1791 struct FailReuseEngine {
1794 eos: Token,
1795 vocab: usize,
1796 }
1797 impl InferenceEngine for FailReuseEngine {
1798 fn prefill(&mut self, t: &[Token]) -> Result<u32> {
1799 Ok(t.len() as u32)
1800 }
1801 fn next_logits(&mut self, _c: &[Token]) -> Vec<i32> {
1802 let mut v = vec![0i32; self.vocab];
1803 if let Some(s) = v.get_mut(self.eos as usize) {
1804 *s = 1;
1805 }
1806 v
1807 }
1808 fn eos_token(&self) -> Token {
1809 self.eos
1810 }
1811 fn rollback(&mut self, _keep: u32) -> Result<()> {
1812 Ok(())
1813 }
1814 fn reset_cache(&mut self) -> Result<()> {
1815 Ok(())
1816 }
1817 fn prefill_reuse(&mut self, _full_context: &[Token]) -> Result<u32> {
1818 Err(EdgeError::Engine("prefill_reuse failed"))
1819 }
1820 }
1821
1822 #[test]
1823 fn continue_prompt_reuses_cached_prefix_and_feeds_only_suffix() {
1824 let engine = ReuseEngine::new(1, 4, true);
1827 let forwards = engine.forwards.clone();
1828 let mut s =
1829 InferenceSession::new(SessionId(50), SessionConfig::default(), engine, permit());
1830 let ports = Ports::permissive();
1831
1832 s.load_prompt(&ports, &[10, 11]).unwrap();
1833 s.generate(&ports, 8).unwrap(); assert_eq!(forwards.get(), 2, "turn 1 prefilled the 2 prompt tokens");
1835
1836 s.continue_prompt(&ports, &[10, 11, 20, 21, 22]).unwrap();
1838 assert_eq!(
1839 forwards.get() - 2,
1840 3,
1841 "only the 3-token suffix was prefilled, not the whole 5-token context"
1842 );
1843 assert_eq!(s.phase(), Phase::Decoding);
1844 assert_eq!(s.kv_len(), 5);
1846
1847 s.generate(&ports, 8).unwrap();
1848 assert_eq!(s.output(), &[1]);
1849 assert_eq!(s.phase(), Phase::Completed);
1850 }
1851
1852 #[test]
1853 fn continue_prompt_requires_a_completed_prior_turn() {
1854 let engine = ReuseEngine::new(1, 4, true);
1857 let mut s =
1858 InferenceSession::new(SessionId(51), SessionConfig::default(), engine, permit());
1859 let ports = Ports::permissive();
1860 let err = s.continue_prompt(&ports, &[1, 2]).unwrap_err();
1861 assert!(matches!(err, EdgeError::InvalidPhase { .. }));
1862 }
1863
1864 #[test]
1865 fn prefill_reuse_leaves_same_state_as_a_fresh_prefill() {
1866 let mut fresh = ReuseEngine::new(99, 6, false);
1871 fresh.prefill(&[1, 2, 3, 4]).unwrap();
1872 let want = fresh.next_logits(&[]);
1873
1874 let mut extend = ReuseEngine::new(99, 6, false);
1876 extend.prefill(&[1, 2]).unwrap();
1877 extend.prefill_reuse(&[1, 2, 3, 4]).unwrap();
1878 assert_eq!(
1879 extend.next_logits(&[]),
1880 want,
1881 "extend-reuse == fresh prefill"
1882 );
1883
1884 let mut rebuild = ReuseEngine::new(99, 6, false);
1886 rebuild.prefill(&[1, 9, 3]).unwrap();
1887 rebuild.prefill_reuse(&[1, 2, 3, 4]).unwrap();
1888 assert_eq!(
1889 rebuild.next_logits(&[]),
1890 want,
1891 "rebuild-on-divergence == fresh prefill"
1892 );
1893 }
1894
1895 #[test]
1896 fn default_prefill_reuse_does_a_full_recompute() {
1897 let mut e = NullEngine::new(1, 4);
1901 assert_eq!(e.prefill(&[1, 2]).unwrap(), 2);
1902 assert_eq!(
1903 e.prefill_reuse(&[1, 2, 3, 4, 5]).unwrap(),
1904 5,
1905 "default reuse re-prefills the full context length"
1906 );
1907 }
1908
1909 #[test]
1910 fn continued_turn_still_runs_the_safety_control_loop() {
1911 let mut s = InferenceSession::new(
1915 SessionId(52),
1916 SessionConfig::default(),
1917 descending_engine(), permit(),
1919 );
1920 let ports = guarded_ports(Box::new(BanToken(0)));
1921
1922 s.load_prompt(&ports, &[7]).unwrap();
1923 s.generate_with_policy(&ports, 2, tiny_policy(3)).unwrap();
1924 assert_eq!(s.phase(), Phase::Completed);
1925
1926 s.continue_prompt(&ports, &[7, 1, 1, 8]).unwrap();
1928 let stop = s.generate_with_policy(&ports, 3, tiny_policy(3)).unwrap();
1929
1930 assert_eq!(stop, StopReason::MaxTokens);
1931 assert!(
1932 !s.output().contains(&0),
1933 "the guard still bans the unsafe token on a continued turn"
1934 );
1935 assert_eq!(s.output(), &[1, 1, 1]);
1936 let evs = s.drain_events();
1937 assert!(evs
1938 .iter()
1939 .any(|e| matches!(e.event, DomainEvent::ClaimBacktracked { .. })));
1940 }
1941
1942 #[test]
1943 fn prefill_reuse_into_empty_context_drops_stale_logits() {
1944 let mut reused = ReuseEngine::new(9, 4, false);
1949 reused.prefill(&[1, 2, 3]).unwrap();
1950 assert!(!reused.next_logits(&[]).is_empty());
1951 reused.prefill_reuse(&[]).unwrap();
1952
1953 let mut fresh = ReuseEngine::new(9, 4, false);
1954 fresh.reset_cache().unwrap();
1955 fresh.prefill(&[]).unwrap();
1956
1957 assert_eq!(
1958 reused.next_logits(&[]),
1959 fresh.next_logits(&[]),
1960 "rebuild into an empty context == a fresh empty prefill"
1961 );
1962 assert!(
1963 reused.next_logits(&[]).is_empty(),
1964 "no stale distribution may survive the rebuild"
1965 );
1966 }
1967
1968 #[test]
1969 fn failed_continue_prompt_leaves_a_recoverable_session() {
1970 let mut s = InferenceSession::new(
1975 SessionId(54),
1976 SessionConfig::default(),
1977 FailReuseEngine { eos: 1, vocab: 4 },
1978 permit(),
1979 );
1980 let ports = Ports::permissive();
1981 s.load_prompt(&ports, &[10, 11]).unwrap();
1982 s.generate(&ports, 8).unwrap();
1983 assert_eq!(s.phase(), Phase::Completed);
1984
1985 let err = s.continue_prompt(&ports, &[10, 11, 12]).unwrap_err();
1986 assert!(matches!(err, EdgeError::Engine(_)));
1987 assert_ne!(
1988 s.phase(),
1989 Phase::Completed,
1990 "a failed reuse must not masquerade as a finished turn"
1991 );
1992
1993 s.reset().unwrap();
1995 s.load_prompt(&ports, &[20, 21]).unwrap();
1996 assert_eq!(s.phase(), Phase::Decoding);
1997 assert_eq!(s.generate(&ports, 8).unwrap(), StopReason::Eos);
1998 }
1999}