el_runtime/ports.rs
1//! Port traits the Inference Runtime depends on (the collaborator contexts).
2
3use el_core::{Result, Token};
4use el_safety::{ChunkGuard, SafetySteerer};
5
6/// The inference engine adapter (`RuntimeAcl`). Implemented for real by Candle
7/// in the excluded adapter `el-engine-candle` (ADR-002).
8///
9/// Logits are integer milli-logits to keep the orchestrator deterministic and
10/// float-free; a real engine quantises its float logits at the ACL boundary.
11pub trait InferenceEngine {
12 /// Encode the (compressed) prompt; returns the resulting KV length.
13 fn prefill(&mut self, tokens: &[Token]) -> Result<u32>;
14 /// Produce next-token logits given the committed context.
15 fn next_logits(&mut self, committed: &[Token]) -> Vec<i32>;
16 /// The end-of-sequence token id.
17 fn eos_token(&self) -> Token;
18
19 /// Roll the engine's internal state back so its context is exactly the
20 /// prompt plus `keep_committed` generated tokens.
21 ///
22 /// The ADR-012 control loop truncates the session's committed output and KV
23 /// descriptors on a safety backtrack. A **stateful** engine (one holding a
24 /// real KV cache and position counters, e.g. a transformer) must mirror that
25 /// truncation here — otherwise it keeps serving logits from the abandoned
26 /// (unsafe) branch and never re-feeds the replacement tokens, so the rollback
27 /// is silently a no-op at the engine level.
28 ///
29 /// After `Ok(())`, the next [`next_logits`](Self::next_logits) call — passed a
30 /// `committed` slice of length `keep_committed` — must produce logits
31 /// consistent with that prefix. Returning `Err` makes the loop fail closed
32 /// rather than resume on an inconsistent cache.
33 ///
34 /// This method is **required, with no default**, deliberately: a default
35 /// no-op would let a stateful adapter that forgot to override silently resume
36 /// on a stale KV cache — a safety bug that fails *open*. Every engine must
37 /// make the choice explicit. A stateless engine whose `next_logits`
38 /// recomputes purely from `committed` implements it as `Ok(())`.
39 fn rollback(&mut self, keep_committed: u32) -> Result<()>;
40}
41
42/// Prompt Compression port (LLMLingua-2 — context 2).
43pub trait PromptCompressor {
44 fn compress(&self, tokens: &[Token]) -> Vec<Token>;
45}
46
47/// Grammar Constraint port (llguidance — context 4). Returns a per-token allow
48/// mask of length `vocab`; `true` = legal this step.
49pub trait GrammarMasker {
50 fn mask(&self, recent: &[Token], vocab: usize) -> Vec<bool>;
51}
52
53/// Opt-in LAN relay (ADR-004 HybridMode). Implementations MUST stay on the local
54/// network — there is no cloud variant.
55pub trait HybridRelay {
56 fn consult(&self, query_tokens: &[Token]) -> Vec<Token>;
57}
58
59/// The collaborator ports bound to a session. `relay` is `None` by default —
60/// air-gapped.
61pub struct Ports {
62 pub compressor: Box<dyn PromptCompressor>,
63 pub grammar: Box<dyn GrammarMasker>,
64 pub safety: Box<dyn SafetySteerer>,
65 /// Optional chunk guard for the checkpointed-rollback control loop
66 /// (ADR-012). `None` runs the plain single-pass decode.
67 pub guard: Option<Box<dyn ChunkGuard>>,
68 /// Optional **ingress / prompt-risk triage** (ADR-013): scores the prompt
69 /// before generation. Reuses the [`ChunkGuard`] contract — it scores a token
70 /// window for risk — applied to the prompt rather than the output. `None`
71 /// runs no ingress check.
72 pub ingress: Option<Box<dyn ChunkGuard>>,
73 pub relay: Option<Box<dyn HybridRelay>>,
74}
75
76impl Ports {
77 /// Defaults: identity compression, all-tokens-allowed grammar, no safety
78 /// steering, no guard, no ingress, no relay (air-gapped).
79 pub fn permissive() -> Self {
80 Self {
81 compressor: Box::new(super::defaults::IdentityCompressor),
82 grammar: Box::new(super::defaults::AllowAllMasker),
83 safety: Box::new(el_safety::NoSafety),
84 guard: None,
85 ingress: None,
86 relay: None,
87 }
88 }
89}