1use crate::caps::Capabilities;
20use crate::handles::Handles;
21use crate::infer::{InferBackend, InferResult};
22use cuttlefish_abi::{error_codes, Command, Envelope, Event, JobError, JobStatus, Usage};
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::Arc;
25use std::time::Instant;
26use tokio::sync::mpsc;
27use tokio_util::sync::CancellationToken;
28use wasmtime::{Engine, Instance, Linker, Memory, Module, Store, TypedFunc};
29
30#[derive(Debug, Clone)]
32pub enum JobEvent {
33 Token(String),
35 Progress(serde_json::Value),
37}
38
39pub struct JobSpec {
41 pub module_bytes: Vec<u8>,
43 pub input: serde_json::Value,
45 pub caps: Capabilities,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Abi {
57 W32,
59 W64,
61}
62
63impl Abi {
64 fn ptr_size(self) -> usize {
66 match self {
67 Abi::W32 => 4,
68 Abi::W64 => 8,
69 }
70 }
71}
72
73struct Guest {
74 store: Store<()>,
75 memory: Memory,
76 abi: Abi,
77 alloc: TypedFunc<u32, u32>,
78 init: TypedFunc<(u32, u32), u32>,
79 step: TypedFunc<(u32, u32), u32>,
80 on_token: Option<TypedFunc<(u32, u32), i32>>,
81}
82
83impl Guest {
84 fn new(engine: &Engine, module_bytes: &[u8]) -> anyhow::Result<Self> {
85 let module = Module::new(engine, module_bytes)?;
86
87 let linker: Linker<()> = Linker::new(engine);
92 let mut store = Store::new(engine, ());
93 let instance: Instance = linker.instantiate(&mut store, &module)?;
94
95 let memory = instance
96 .get_memory(&mut store, "memory")
97 .ok_or_else(|| anyhow::anyhow!("guest exports no memory"))?;
98
99 let abi = if memory.ty(&store).is_64() {
103 Abi::W64
104 } else {
105 Abi::W32
106 };
107 if abi == Abi::W64 {
108 anyhow::bail!("guest uses 64-bit memory; only 32-bit guests are supported");
109 }
110
111 Ok(Self {
112 alloc: instance.get_typed_func(&mut store, "cf_alloc")?,
113 init: instance.get_typed_func(&mut store, "cf_init")?,
114 step: instance.get_typed_func(&mut store, "cf_step")?,
115 on_token: instance.get_typed_func(&mut store, "cf_on_token").ok(),
117 memory,
118 abi,
119 store,
120 })
121 }
122
123 fn write(&mut self, bytes: &[u8]) -> anyhow::Result<(u32, u32)> {
124 let len = bytes.len() as u32;
125 let ptr = self.alloc.call(&mut self.store, len)?;
126 self.memory.write(&mut self.store, ptr as usize, bytes)?;
127 Ok((ptr, len))
128 }
129
130 fn read_desc(&mut self, desc_ptr: u32) -> anyhow::Result<Vec<u8>> {
135 let w = self.abi.ptr_size();
136 let mut desc = vec![0u8; 2 * w];
137 self.memory
138 .read(&mut self.store, desc_ptr as usize, &mut desc)?;
139
140 let field = |bytes: &[u8]| -> u64 {
141 match w {
142 4 => u32::from_le_bytes(bytes.try_into().expect("4 bytes")) as u64,
143 _ => u64::from_le_bytes(bytes.try_into().expect("8 bytes")),
144 }
145 };
146 let ptr = field(&desc[..w]) as usize;
147 let len = field(&desc[w..]) as usize;
148
149 let mut buf = vec![0u8; len];
150 self.memory.read(&mut self.store, ptr, &mut buf)?;
151 Ok(buf)
152 }
153
154 fn call_init(&mut self, input: &serde_json::Value) -> anyhow::Result<Command> {
155 let bytes = serde_json::to_vec(input)?;
156 let (ptr, len) = self.write(&bytes)?;
157 let desc = self.init.call(&mut self.store, (ptr, len))?;
158 Ok(serde_json::from_slice(&self.read_desc(desc)?)?)
159 }
160
161 fn call_step(&mut self, event: &Event) -> anyhow::Result<Command> {
162 let bytes = serde_json::to_vec(event)?;
163 let (ptr, len) = self.write(&bytes)?;
164 let desc = self.step.call(&mut self.store, (ptr, len))?;
165 Ok(serde_json::from_slice(&self.read_desc(desc)?)?)
166 }
167
168 fn call_on_token(&mut self, token: &str) -> anyhow::Result<bool> {
170 let Some(f) = self.on_token.clone() else {
174 return Ok(true);
175 };
176 let (ptr, len) = self.write(token.as_bytes())?;
177 Ok(f.call(&mut self.store, (ptr, len))? == 0)
178 }
179}
180
181fn fail(code: &str, message: impl Into<String>, usage: Usage) -> Envelope {
182 Envelope {
183 status: JobStatus::Failed,
184 result: None,
185 error: Some(JobError {
186 code: code.into(),
187 message: message.into(),
188 }),
189 usage,
190 }
191}
192
193fn cancelled(usage: Usage, message: &str) -> Envelope {
194 Envelope {
195 status: JobStatus::Cancelled,
196 result: None,
197 error: Some(JobError {
198 code: error_codes::CANCELLED.into(),
199 message: message.into(),
200 }),
201 usage,
202 }
203}
204
205pub async fn run_job(
210 engine: Arc<Engine>,
211 backend: Arc<dyn InferBackend>,
212 job: JobSpec,
213 events: mpsc::Sender<JobEvent>,
214 cancel: CancellationToken,
215) -> Envelope {
216 let started = Instant::now();
217 let mut usage = Usage {
218 model: backend.model_name(),
219 ..Usage::default()
220 };
221
222 let mut handles = Handles::default();
225
226 let mut guest = match Guest::new(&engine, &job.module_bytes) {
227 Ok(g) => g,
228 Err(e) => return fail(error_codes::WASM_TRAP, e.to_string(), usage),
229 };
230
231 let mut command = match guest.call_init(&job.input) {
232 Ok(c) => c,
233 Err(e) => return fail(error_codes::WASM_TRAP, e.to_string(), usage),
234 };
235
236 loop {
237 if cancel.is_cancelled() {
238 usage.duration_ms = started.elapsed().as_millis() as u64;
239 return cancelled(usage, "job cancelled");
240 }
241
242 let event = match command {
243 Command::Done { result } => {
244 usage.duration_ms = started.elapsed().as_millis() as u64;
245 return Envelope {
246 status: JobStatus::Completed,
247 result: Some(result),
248 error: None,
249 usage,
250 };
251 }
252 Command::Fail { code, message } => {
253 usage.duration_ms = started.elapsed().as_millis() as u64;
254 return fail(&code, message, usage);
255 }
256 Command::Emit { progress } => {
257 let _ = events.send(JobEvent::Progress(progress)).await;
258 Event::Emitted
259 }
260
261 Command::Open { path } => {
265 let p = std::path::PathBuf::from(&path);
266 if !job.caps.allows_read(&p) {
267 usage.duration_ms = started.elapsed().as_millis() as u64;
268 return fail(
269 error_codes::CAPABILITY_DENIED,
270 format!("read not permitted: {path}"),
271 usage,
272 );
273 }
274 match handles.open(&p) {
275 Ok((handle, len)) => Event::Opened { handle, len },
276 Err(e) => {
277 usage.duration_ms = started.elapsed().as_millis() as u64;
278 return fail(error_codes::CAPABILITY_DENIED, e.to_string(), usage);
279 }
280 }
281 }
282
283 Command::Slice {
284 handle,
285 offset,
286 len,
287 } => match handles.slice(handle, offset, len) {
288 Ok(w) => Event::Sliced {
289 text: w.text,
290 next_offset: w.next_offset,
291 },
292 Err(e) => {
293 usage.duration_ms = started.elapsed().as_millis() as u64;
294 return fail(error_codes::CAPABILITY_DENIED, e.to_string(), usage);
295 }
296 },
297
298 Command::Infer { prompt, max_tokens } => {
299 let (tx, mut rx) = mpsc::unbounded_channel::<String>();
305 let stop = Arc::new(AtomicBool::new(false));
306 let sink_stop = stop.clone();
307 let mut sink = move |t: &str| {
308 tx.send(t.to_string()).is_ok() && !sink_stop.load(Ordering::Relaxed)
309 };
310
311 let mut trap: Option<String> = None;
312 let outcome: Option<anyhow::Result<InferResult>> = {
313 let infer = backend.infer(&prompt, max_tokens, &mut sink);
314 tokio::pin!(infer);
315 loop {
316 tokio::select! {
317 biased;
318 _ = cancel.cancelled() => break None,
319 Some(tok) = rx.recv() => {
320 let _ = events.send(JobEvent::Token(tok.clone())).await;
321 match guest.call_on_token(&tok) {
322 Ok(true) => {}
323 Ok(false) => stop.store(true, Ordering::Relaxed),
324 Err(e) => {
325 trap = Some(e.to_string());
326 break None;
327 }
328 }
329 }
330 r = &mut infer => break Some(r),
331 }
332 }
333 };
334
335 if let Some(message) = trap {
336 usage.duration_ms = started.elapsed().as_millis() as u64;
337 return fail(error_codes::WASM_TRAP, message, usage);
338 }
339
340 while let Ok(tok) = rx.try_recv() {
343 let _ = events.send(JobEvent::Token(tok)).await;
344 }
345
346 match outcome {
347 None => {
348 usage.duration_ms = started.elapsed().as_millis() as u64;
349 return cancelled(usage, "cancelled during inference");
350 }
351 Some(Err(e)) => {
352 usage.duration_ms = started.elapsed().as_millis() as u64;
353 return fail(error_codes::MODEL_LOAD_FAILED, e.to_string(), usage);
354 }
355 Some(Ok(r)) => {
356 usage.tokens_in += r.tokens_in;
357 usage.tokens_out += r.tokens_out;
358 Event::InferDone {
359 text: r.text,
360 tokens_out: r.tokens_out,
361 }
362 }
363 }
364 }
365 };
366
367 command = match guest.call_step(&event) {
368 Ok(c) => c,
369 Err(e) => {
370 usage.duration_ms = started.elapsed().as_millis() as u64;
371 return fail(error_codes::WASM_TRAP, e.to_string(), usage);
372 }
373 };
374 }
375}