Skip to main content

mesh_native_serving_plugin_api/
lib.rs

1//! Stable native plugin boundary for Mesh's local Skippy serving path.
2//!
3//! The ABI is deliberately smaller than Mesh's Rust crate graph. Plugins are
4//! independently compiled dynamic libraries, so only fixed-layout values,
5//! opaque handles, borrowed byte/token slices, and host-owned output buffers
6//! cross the boundary. Rust collections, trait objects, futures, and allocator
7//! ownership never do.
8//!
9//! Every function in the table must be thread-safe and return promptly. In
10//! particular, proposal submission and polling must never wait for proposer
11//! work. Mesh invokes proposal functions on an isolated host worker and owns
12//! the absolute decode deadline; a plugin that violates this contract can
13//! strand only that worker, never the model decode thread.
14
15use std::ffi::{c_char, c_void};
16
17pub const NATIVE_SERVING_PLUGIN_ABI_V1: u32 = 1;
18pub const NATIVE_SERVING_PLUGIN_ENTRY_V1: &[u8] = b"mesh_native_serving_plugin_v1\0";
19pub const MAX_DECISION_ID_BYTES: usize = 64;
20pub const TOKENIZER_INVENTORY_SCHEMA: u32 = 1;
21
22/// Host-owned typed inventory. This Rust value never crosses the ABI directly.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct TokenizerInventory {
25    pub schema_version: u32,
26    pub model_id: String,
27    pub source_model_sha256: String,
28    pub tokenizer_id: String,
29    pub tokens: Vec<TokenizerInventoryToken>,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct TokenizerInventoryToken {
34    pub id: u32,
35    pub piece: TokenizerInventoryPiece,
36}
37
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub enum TokenizerInventoryPiece {
40    Bytes { bytes: Vec<u8> },
41    Control { identity: String },
42}
43
44pub type PluginInstance = *mut c_void;
45pub type ProposalOperation = u64;
46
47#[repr(C)]
48#[derive(Clone, Copy, Debug, Default)]
49pub struct ByteSlice {
50    pub pointer: *const u8,
51    pub length: usize,
52}
53
54impl ByteSlice {
55    #[must_use]
56    pub fn from_bytes(bytes: &[u8]) -> Self {
57        Self {
58            pointer: bytes.as_ptr(),
59            length: bytes.len(),
60        }
61    }
62}
63
64#[repr(transparent)]
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub struct TokenizerPieceKind(pub u32);
67
68impl TokenizerPieceKind {
69    pub const BYTES: Self = Self(0);
70    pub const CONTROL: Self = Self(1);
71}
72
73/// Borrowed ABI view of one immutable native token. The referenced bytes are
74/// valid only for the duration of `activate`; a plugin must copy or transform
75/// them before it returns.
76#[repr(C)]
77#[derive(Clone, Copy, Debug)]
78pub struct TokenizerInventoryEntry {
79    pub id: u32,
80    pub piece_kind: TokenizerPieceKind,
81    pub bytes: ByteSlice,
82}
83
84/// Borrowed ABI view of the complete vocabulary. The host owns the entries and
85/// their bytes and passes them only while activating the plugin.
86#[repr(C)]
87#[derive(Clone, Copy, Debug)]
88pub struct TokenizerInventoryView {
89    pub struct_size: usize,
90    pub schema_version: u32,
91    pub entries: *const TokenizerInventoryEntry,
92    pub entry_count: usize,
93}
94
95#[repr(C)]
96#[derive(Clone, Copy, Debug, Default)]
97pub struct TokenSlice {
98    pub pointer: *const i32,
99    pub length: usize,
100}
101
102#[repr(C)]
103#[derive(Clone, Copy, Debug, Default)]
104pub struct U64Slice {
105    pub pointer: *const u64,
106    pub length: usize,
107}
108
109impl U64Slice {
110    #[must_use]
111    pub fn from_values(values: &[u64]) -> Self {
112        Self {
113            pointer: values.as_ptr(),
114            length: values.len(),
115        }
116    }
117}
118
119impl TokenSlice {
120    #[must_use]
121    pub fn from_tokens(tokens: &[i32]) -> Self {
122        Self {
123            pointer: tokens.as_ptr(),
124            length: tokens.len(),
125        }
126    }
127}
128
129#[repr(transparent)]
130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
131pub struct PluginStatus(pub u32);
132
133impl PluginStatus {
134    pub const OK: Self = Self(0);
135    pub const INVALID_ARGUMENT: Self = Self(1);
136    pub const INCOMPATIBLE: Self = Self(2);
137    pub const UNAVAILABLE: Self = Self(3);
138    pub const INTERNAL_ERROR: Self = Self(4);
139}
140
141#[repr(transparent)]
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub struct ProposalPollStatus(pub u32);
144
145impl ProposalPollStatus {
146    pub const PENDING: Self = Self(0);
147    pub const READY: Self = Self(1);
148    pub const ABSTAIN: Self = Self(2);
149    pub const FAILED: Self = Self(3);
150}
151
152#[repr(transparent)]
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub struct GenerationTermination(pub u32);
155
156impl GenerationTermination {
157    pub const CALLBACK_STOP: Self = Self(0);
158    pub const MAX_TOKENS: Self = Self(1);
159    pub const CANCELLED: Self = Self(2);
160}
161
162#[repr(transparent)]
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub struct ProposalDisposition(pub u32);
165
166impl ProposalDisposition {
167    pub const FULL_ACCEPT: Self = Self(0);
168    pub const FIRST_MISMATCH: Self = Self(1);
169    pub const STOPPED: Self = Self(2);
170}
171
172#[repr(transparent)]
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub struct ProposalDiscardReason(pub u32);
175
176impl ProposalDiscardReason {
177    pub const DEADLINE_EXCEEDED: Self = Self(0);
178    pub const INVALID_TOKEN_COUNT: Self = Self(1);
179    pub const INVALID_TOKEN_ID: Self = Self(2);
180    pub const POSITION_MISMATCH: Self = Self(3);
181    pub const EXECUTION_FAILED: Self = Self(4);
182}
183
184pub type MonotonicNowNs = unsafe extern "C" fn(context: *mut c_void) -> u64;
185
186#[repr(C)]
187#[derive(Clone, Copy)]
188pub struct ActivationContext {
189    pub struct_size: usize,
190    pub model_id: ByteSlice,
191    pub source_model_sha256: ByteSlice,
192    pub tokenizer_id: ByteSlice,
193    pub tokenizer_inventory: *const TokenizerInventoryView,
194    pub config_path: ByteSlice,
195    pub state_directory: ByteSlice,
196    pub proposal_deadline_ns: u64,
197    pub host_clock_context: *mut c_void,
198    pub monotonic_now_ns: MonotonicNowNs,
199}
200
201#[repr(C)]
202#[derive(Clone, Copy, Debug)]
203pub struct PluginActivation {
204    pub instance: PluginInstance,
205}
206
207#[repr(C)]
208#[derive(Clone, Copy, Debug, Default)]
209pub struct GenerationStart {
210    pub struct_size: usize,
211    pub request_id: u64,
212    pub session_id: u64,
213    pub agent_session_id: ByteSlice,
214    pub prompt_token_ids: TokenSlice,
215}
216
217#[repr(C)]
218#[derive(Clone, Copy, Debug, Default)]
219pub struct GenerationCommit {
220    pub struct_size: usize,
221    pub request_id: u64,
222    pub session_id: u64,
223    pub generated_token_count: u64,
224    pub token_ids: TokenSlice,
225}
226
227#[repr(C)]
228#[derive(Clone, Copy, Debug, Default)]
229pub struct GenerationAbort {
230    pub struct_size: usize,
231    pub request_id: u64,
232    pub session_id: u64,
233}
234
235#[repr(C)]
236#[derive(Clone, Copy, Debug)]
237pub struct GenerationFinish {
238    pub struct_size: usize,
239    pub request_id: u64,
240    pub session_id: u64,
241    pub prompt_token_count: u64,
242    pub prompt_token_digest: [u8; 32],
243    pub prompt_token_ids: TokenSlice,
244    pub generated_token_ids: TokenSlice,
245    pub final_session_position: u64,
246    pub termination: GenerationTermination,
247    pub model_generation_elapsed_us: u64,
248    pub has_request_to_first_token: bool,
249    pub request_to_first_token_us: u64,
250    pub request_to_token_emission_us: U64Slice,
251}
252
253#[repr(C)]
254#[derive(Clone, Copy, Debug, Default)]
255pub struct ProposalQuery {
256    pub struct_size: usize,
257    pub request_id: u64,
258    pub session_id: u64,
259    pub prompt_token_count: u64,
260    pub committed_token_count: u64,
261    pub decode_step: u64,
262    pub max_proposal_tokens: u64,
263    pub absolute_deadline_ns: u64,
264}
265
266#[repr(C)]
267#[derive(Debug)]
268pub struct ProposalOutput {
269    pub struct_size: usize,
270    pub decision_id: *mut u8,
271    pub decision_id_capacity: usize,
272    pub decision_id_length: usize,
273    pub token_ids: *mut i32,
274    pub token_capacity: usize,
275    pub token_length: usize,
276}
277
278#[repr(C)]
279#[derive(Clone, Copy, Debug)]
280pub struct ProposalOutcome {
281    pub struct_size: usize,
282    pub decision_id: ByteSlice,
283    pub disposition: ProposalDisposition,
284    pub proposal_token_count: u64,
285    pub verification_rows: u64,
286    pub accepted_proposal_tokens: u64,
287    pub committed_tokens: TokenSlice,
288    pub verification_row_predictions: TokenSlice,
289    pub canonical_prediction_count: u64,
290    pub has_correction_or_boundary_token: bool,
291    pub correction_or_boundary_token: i32,
292    pub base_position: u64,
293    pub position_after_verification: u64,
294    pub canonical_position: u64,
295    pub trimmed_rows: u64,
296}
297
298#[repr(C)]
299#[derive(Clone, Copy, Debug)]
300pub struct ProposalDiscard {
301    pub struct_size: usize,
302    pub decision_id: ByteSlice,
303    pub reason: ProposalDiscardReason,
304}
305
306pub type ActivatePlugin = unsafe extern "C" fn(
307    context: *const ActivationContext,
308    activation: *mut PluginActivation,
309) -> PluginStatus;
310pub type ShutdownPlugin = unsafe extern "C" fn(instance: PluginInstance) -> PluginStatus;
311pub type BeginGeneration =
312    unsafe extern "C" fn(instance: PluginInstance, event: *const GenerationStart) -> PluginStatus;
313pub type CommitGeneration =
314    unsafe extern "C" fn(instance: PluginInstance, event: *const GenerationCommit) -> PluginStatus;
315pub type AbortGeneration =
316    unsafe extern "C" fn(instance: PluginInstance, event: *const GenerationAbort) -> PluginStatus;
317pub type FinishGeneration =
318    unsafe extern "C" fn(instance: PluginInstance, event: *const GenerationFinish) -> PluginStatus;
319pub type StartProposal = unsafe extern "C" fn(
320    instance: PluginInstance,
321    query: *const ProposalQuery,
322    operation: *mut ProposalOperation,
323) -> PluginStatus;
324pub type PollProposal = unsafe extern "C" fn(
325    instance: PluginInstance,
326    operation: ProposalOperation,
327    output: *mut ProposalOutput,
328) -> ProposalPollStatus;
329pub type CancelProposal =
330    unsafe extern "C" fn(instance: PluginInstance, operation: ProposalOperation);
331pub type ReportProposal =
332    unsafe extern "C" fn(instance: PluginInstance, outcome: *const ProposalOutcome) -> PluginStatus;
333pub type DiscardProposal =
334    unsafe extern "C" fn(instance: PluginInstance, discard: *const ProposalDiscard) -> PluginStatus;
335pub type LastError =
336    unsafe extern "C" fn(instance: PluginInstance, output: *mut c_char, capacity: usize) -> usize;
337
338#[repr(C)]
339pub struct NativeServingPluginV1 {
340    pub abi_version: u32,
341    pub struct_size: usize,
342    pub plugin_name: ByteSlice,
343    pub activate: ActivatePlugin,
344    pub shutdown: ShutdownPlugin,
345    pub begin_generation: BeginGeneration,
346    pub commit_generation: CommitGeneration,
347    pub abort_generation: AbortGeneration,
348    pub finish_generation: FinishGeneration,
349    pub start_proposal: StartProposal,
350    pub poll_proposal: PollProposal,
351    pub cancel_proposal: CancelProposal,
352    pub report_proposal: ReportProposal,
353    pub discard_proposal: DiscardProposal,
354    pub last_error: LastError,
355}
356
357// SAFETY: the ABI requires this table and the bytes referenced by
358// `plugin_name` to remain immutable and valid for the loaded library's entire
359// lifetime. The host copies the name during load and only calls function
360// pointers afterward.
361unsafe impl Sync for NativeServingPluginV1 {}
362
363pub type NativeServingPluginEntryV1 = unsafe extern "C" fn() -> *const NativeServingPluginV1;
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    #[test]
370    fn borrowed_slices_preserve_exact_addresses_and_lengths() {
371        let bytes = b"cacheline";
372        let tokens = [-1, 0, 42];
373        let byte_slice = ByteSlice::from_bytes(bytes);
374        let token_slice = TokenSlice::from_tokens(&tokens);
375        assert_eq!(byte_slice.pointer, bytes.as_ptr());
376        assert_eq!(byte_slice.length, bytes.len());
377        assert_eq!(token_slice.pointer, tokens.as_ptr());
378        assert_eq!(token_slice.length, tokens.len());
379    }
380
381    #[test]
382    fn initial_contract_is_v1() {
383        assert_eq!(MAX_DECISION_ID_BYTES, 64);
384        assert_eq!(NATIVE_SERVING_PLUGIN_ABI_V1, 1);
385    }
386}