ts_wire/frames.rs
1// Copyright (c) 2026 LightSeek Foundation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! ZMQ message framing on the frontend–engine sockets.
22//!
23//! * Input (frontend ROUTER to engine DEALER): `[type, payload, aux...]`.
24//! `type` is one byte so the engine dispatches without decoding: ADD
25//! carries a [`TokenizedGenerateReqInput`] whose tensors may reference the
26//! `aux` frames; ABORT carries a msgpack list of request ids; CONTROL
27//! carries a [`ControlRequest`].
28//! * Output (engine PUSH to frontend PULL): a [`BatchTokenIDOutSlim`]
29//! message, a [`ControlOutput`] (told apart by its tag), or the single
30//! frame [`ENGINE_CORE_DEAD`] when the engine exits.
31
32use bytes::Bytes;
33
34use crate::{
35 codec::{decode_frames, encode_frames},
36 control::{ControlOutput, ControlRequest},
37 error::{Error, Result},
38 msgpack::Decoder,
39 output::BatchTokenIDOutSlim,
40 request::TokenizedGenerateReqInput,
41};
42
43/// Type byte of an ADD message.
44pub const REQ_TYPE_ADD: u8 = 0x00;
45/// Type byte of an ABORT message.
46pub const REQ_TYPE_ABORT: u8 = 0x01;
47/// Type byte of a CONTROL message.
48pub const REQ_TYPE_CONTROL: u8 = 0x02;
49/// The frame an engine sends on its output socket when it shuts down; the
50/// frontend then treats the engine as dead.
51pub const ENGINE_CORE_DEAD: &[u8] = b"ENGINE_CORE_DEAD";
52
53/// The ZMQ routing identity of engine `engine_index`: its index as two
54/// little-endian bytes.
55pub fn engine_identity(engine_index: u16) -> [u8; 2] {
56 engine_index.to_le_bytes()
57}
58
59/// A message on the engine's input socket.
60#[derive(Debug, Clone, PartialEq)]
61pub enum EngineRequest {
62 /// Start a request.
63 Add(Box<TokenizedGenerateReqInput>),
64 /// Abort requests by id.
65 Abort(Vec<String>),
66 /// A control operation.
67 Control(Box<ControlRequest>),
68}
69
70impl EngineRequest {
71 /// The message's frames.
72 pub fn to_frames(&self) -> Vec<Bytes> {
73 let (type_frame, payload): (&'static [u8], _) = match self {
74 EngineRequest::Add(request) => (&[REQ_TYPE_ADD], encode_frames(request.as_ref())),
75 EngineRequest::Abort(rids) => (&[REQ_TYPE_ABORT], encode_frames(rids)),
76 EngineRequest::Control(request) => {
77 (&[REQ_TYPE_CONTROL], encode_frames(request.as_ref()))
78 }
79 };
80 let mut frames = Vec::with_capacity(1 + payload.len());
81 frames.push(Bytes::from_static(type_frame));
82 frames.extend(payload);
83 frames
84 }
85
86 /// Decode a message from its frames.
87 pub fn from_frames(frames: &[Bytes]) -> Result<Self> {
88 let [type_frame, payload @ ..] = frames else {
89 return Err(Error::Invalid("an input message has no frames".to_owned()));
90 };
91 if payload.is_empty() {
92 return Err(Error::Invalid(
93 "an input message needs a type frame and a payload".to_owned(),
94 ));
95 }
96 match type_frame.as_ref() {
97 [REQ_TYPE_ADD] => Ok(EngineRequest::Add(Box::new(decode_frames(payload)?))),
98 [REQ_TYPE_ABORT] => Ok(EngineRequest::Abort(decode_frames(payload)?)),
99 [REQ_TYPE_CONTROL] => Ok(EngineRequest::Control(Box::new(decode_frames(payload)?))),
100 other => Err(Error::Invalid(format!(
101 "unknown request type frame {other:02x?}"
102 ))),
103 }
104 }
105}
106
107/// A message on the engine's output socket.
108#[derive(Debug, Clone, PartialEq)]
109pub enum EngineOutput {
110 /// One step's outputs.
111 Batch(Box<BatchTokenIDOutSlim>),
112 /// A control operation's reply.
113 Control(Box<ControlOutput>),
114 /// The engine shut down.
115 Dead,
116}
117
118impl EngineOutput {
119 /// The message's frames.
120 pub fn to_frames(&self) -> Vec<Bytes> {
121 match self {
122 EngineOutput::Batch(batch) => encode_frames(batch.as_ref()),
123 EngineOutput::Control(output) => encode_frames(output.as_ref()),
124 EngineOutput::Dead => vec![Bytes::from_static(ENGINE_CORE_DEAD)],
125 }
126 }
127
128 /// Decode a message from its frames.
129 pub fn from_frames(frames: &[Bytes]) -> Result<Self> {
130 match frames {
131 [] => Err(Error::Invalid("an output message has no frames".to_owned())),
132 [only] if only.as_ref() == ENGINE_CORE_DEAD => Ok(EngineOutput::Dead),
133 _ if ControlOutput::knows(Decoder::new(frames).peek_tag()?) => {
134 Ok(EngineOutput::Control(Box::new(decode_frames(frames)?)))
135 }
136 _ => Ok(EngineOutput::Batch(Box::new(decode_frames(frames)?))),
137 }
138 }
139}