1use crate::ipc_binary::{BinaryFrame, ExecutionErrorBin, ExecutionLimit};
2use agentos_runtime::readiness::ReadyFlags;
3use std::io;
4use std::sync::Arc;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct WarmSessionHint {
8 pub bridge_code: String,
9 pub userland_code: String,
10 pub heap_limit_mb: Option<u32>,
11}
12
13#[derive(Debug, Clone, PartialEq)]
14pub enum RuntimeCommand {
15 CreateSession {
16 session_id: String,
17 heap_limit_mb: Option<u32>,
18 cpu_time_limit_ms: Option<u32>,
19 wall_clock_limit_ms: Option<u32>,
20 warm_hint: Option<WarmSessionHint>,
21 },
22 DestroySession {
23 session_id: String,
24 },
25 PauseSession {
26 session_id: String,
27 },
28 ResumeSession {
29 session_id: String,
30 },
31 WarmSnapshot {
32 bridge_code: String,
33 userland_code: String,
34 },
35 SendToSession {
36 session_id: String,
37 message: SessionMessage,
38 },
39 PublishReadiness {
42 session_id: String,
43 capability_id: u64,
44 capability_generation: u64,
45 flags: ReadyFlags,
46 },
47 RemoveReadiness {
48 session_id: String,
49 capability_id: u64,
50 capability_generation: u64,
51 },
52 PublishSignal {
53 session_id: String,
54 signal: i32,
55 },
56 PublishTimer {
57 session_id: String,
58 timer_id: u64,
59 },
60 SetSessionModuleReader {
65 session_id: String,
66 reader: ModuleReaderHandle,
67 },
68}
69
70#[derive(Clone)]
74pub struct ModuleReaderHandle(
75 std::sync::Arc<std::sync::Mutex<Option<Box<dyn crate::execution::GuestModuleReader>>>>,
76);
77
78impl ModuleReaderHandle {
79 pub fn new(reader: Box<dyn crate::execution::GuestModuleReader>) -> Self {
80 ModuleReaderHandle(std::sync::Arc::new(std::sync::Mutex::new(Some(reader))))
81 }
82
83 pub fn take(&self) -> Option<Box<dyn crate::execution::GuestModuleReader>> {
85 self.0.lock().ok().and_then(|mut guard| guard.take())
86 }
87}
88
89impl std::fmt::Debug for ModuleReaderHandle {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str("ModuleReaderHandle(..)")
92 }
93}
94
95impl PartialEq for ModuleReaderHandle {
96 fn eq(&self, other: &Self) -> bool {
97 std::sync::Arc::ptr_eq(&self.0, &other.0)
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
102pub enum SessionMessage {
103 InjectGlobals {
104 payload: Vec<u8>,
105 },
106 Execute {
107 mode: u8,
108 file_path: String,
109 bridge_code: String,
110 post_restore_script: String,
111 userland_code: String,
112 high_resolution_time: bool,
113 user_code: String,
114 wasm_module_bytes: Option<Arc<Vec<u8>>>,
115 },
116 BridgeResponse(BridgeResponse),
117 StreamEvent(StreamEvent),
118 TerminateExecution,
119}
120
121#[derive(Debug, Clone, PartialEq)]
122pub struct BridgeResponse {
123 pub call_id: u64,
124 pub status: u8,
125 pub payload: Vec<u8>,
126 pub reservation: Option<agentos_runtime::accounting::SharedReservation>,
129}
130
131#[derive(Debug, Clone, PartialEq)]
132pub struct StreamEvent {
133 pub event_type: String,
134 pub payload: Vec<u8>,
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub enum RuntimeEvent {
139 BridgeCall {
140 session_id: String,
141 call_id: u64,
142 method: String,
143 payload: Vec<u8>,
144 },
145 ExecutionResult {
146 session_id: String,
147 exit_code: i32,
148 exports: Option<Vec<u8>>,
149 error: Option<ExecutionErrorBin>,
150 limit_exceeded: Option<ExecutionLimit>,
151 },
152 Log {
153 session_id: String,
154 channel: u8,
155 message: String,
156 },
157 StreamCallback {
158 session_id: String,
159 callback_type: String,
160 payload: Vec<u8>,
161 },
162}
163
164impl RuntimeEvent {
165 pub fn session_id(&self) -> &str {
166 match self {
167 RuntimeEvent::BridgeCall { session_id, .. }
168 | RuntimeEvent::ExecutionResult { session_id, .. }
169 | RuntimeEvent::Log { session_id, .. }
170 | RuntimeEvent::StreamCallback { session_id, .. } => session_id,
171 }
172 }
173}
174
175impl TryFrom<BinaryFrame> for RuntimeCommand {
176 type Error = io::Error;
177
178 fn try_from(frame: BinaryFrame) -> Result<Self, Self::Error> {
179 match frame {
180 BinaryFrame::CreateSession {
181 session_id,
182 heap_limit_mb,
183 cpu_time_limit_ms,
184 wall_clock_limit_ms,
185 } => Ok(RuntimeCommand::CreateSession {
186 session_id,
187 heap_limit_mb: non_zero_option(heap_limit_mb),
188 cpu_time_limit_ms: non_zero_option(cpu_time_limit_ms),
189 wall_clock_limit_ms: non_zero_option(wall_clock_limit_ms),
190 warm_hint: None,
191 }),
192 BinaryFrame::DestroySession { session_id } => {
193 Ok(RuntimeCommand::DestroySession { session_id })
194 }
195 BinaryFrame::InjectGlobals {
196 session_id,
197 payload,
198 } => Ok(RuntimeCommand::SendToSession {
199 session_id,
200 message: SessionMessage::InjectGlobals { payload },
201 }),
202 BinaryFrame::Execute {
203 session_id,
204 mode,
205 file_path,
206 bridge_code,
207 post_restore_script,
208 userland_code,
209 high_resolution_time,
210 user_code,
211 } => {
212 if mode > 3 {
213 return Err(io::Error::new(
214 io::ErrorKind::InvalidInput,
215 format!("unknown Execute mode: {mode}"),
216 ));
217 }
218 Ok(RuntimeCommand::SendToSession {
219 session_id,
220 message: SessionMessage::Execute {
221 mode,
222 file_path,
223 bridge_code,
224 post_restore_script,
225 userland_code,
226 high_resolution_time,
227 user_code,
228 wasm_module_bytes: None,
229 },
230 })
231 }
232 BinaryFrame::BridgeResponse {
233 session_id,
234 call_id,
235 status,
236 payload,
237 } => {
238 validate_bridge_response_status(status)?;
239 Ok(RuntimeCommand::SendToSession {
240 session_id,
241 message: SessionMessage::BridgeResponse(BridgeResponse {
242 call_id,
243 status,
244 payload,
245 reservation: None,
246 }),
247 })
248 }
249 BinaryFrame::StreamEvent {
250 session_id,
251 event_type,
252 payload,
253 } => Ok(RuntimeCommand::SendToSession {
254 session_id,
255 message: SessionMessage::StreamEvent(StreamEvent {
256 event_type,
257 payload,
258 }),
259 }),
260 BinaryFrame::TerminateExecution { session_id } => Ok(RuntimeCommand::SendToSession {
261 session_id,
262 message: SessionMessage::TerminateExecution,
263 }),
264 BinaryFrame::WarmSnapshot {
265 bridge_code,
266 userland_code,
267 } => Ok(RuntimeCommand::WarmSnapshot {
268 bridge_code,
269 userland_code,
270 }),
271 BinaryFrame::Authenticate { .. } => Err(io::Error::new(
272 io::ErrorKind::InvalidInput,
273 "Authenticate is not supported by the embedded runtime",
274 )),
275 _ => Err(io::Error::new(
276 io::ErrorKind::InvalidInput,
277 "host-output frames cannot be sent into the embedded runtime",
278 )),
279 }
280 }
281}
282
283impl From<RuntimeEvent> for BinaryFrame {
284 fn from(event: RuntimeEvent) -> Self {
285 match event {
286 RuntimeEvent::BridgeCall {
287 session_id,
288 call_id,
289 method,
290 payload,
291 } => BinaryFrame::BridgeCall {
292 session_id,
293 call_id,
294 method,
295 payload,
296 },
297 RuntimeEvent::ExecutionResult {
298 session_id,
299 exit_code,
300 exports,
301 error,
302 limit_exceeded,
303 } => BinaryFrame::ExecutionResult {
304 session_id,
305 exit_code,
306 exports,
307 error,
308 limit_exceeded,
309 },
310 RuntimeEvent::Log {
311 session_id,
312 channel,
313 message,
314 } => BinaryFrame::Log {
315 session_id,
316 channel,
317 message,
318 },
319 RuntimeEvent::StreamCallback {
320 session_id,
321 callback_type,
322 payload,
323 } => BinaryFrame::StreamCallback {
324 session_id,
325 callback_type,
326 payload,
327 },
328 }
329 }
330}
331
332fn non_zero_option(value: u32) -> Option<u32> {
333 if value == 0 {
334 None
335 } else {
336 Some(value)
337 }
338}
339
340pub fn validate_bridge_response_status(status: u8) -> io::Result<()> {
341 if status <= 2 {
342 return Ok(());
343 }
344 Err(io::Error::new(
345 io::ErrorKind::InvalidInput,
346 format!("unknown BridgeResponse status: {status}"),
347 ))
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn rejects_unknown_execute_mode() {
356 let err = RuntimeCommand::try_from(BinaryFrame::Execute {
357 session_id: "s".into(),
358 mode: 4,
359 file_path: "/app/main.mjs".into(),
360 bridge_code: String::new(),
361 post_restore_script: String::new(),
362 userland_code: String::new(),
363 high_resolution_time: false,
364 user_code: String::new(),
365 })
366 .expect_err("unknown execute mode should be rejected");
367
368 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
369 assert!(err.to_string().contains("unknown Execute mode"));
370 }
371
372 #[test]
373 fn rejects_unknown_bridge_response_status() {
374 let err = RuntimeCommand::try_from(BinaryFrame::BridgeResponse {
375 session_id: "s".into(),
376 call_id: 1,
377 status: 3,
378 payload: Vec::new(),
379 })
380 .expect_err("unknown bridge response status should be rejected");
381
382 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
383 assert!(err.to_string().contains("unknown BridgeResponse status"));
384 }
385
386 #[test]
387 fn accepts_known_bridge_response_statuses() {
388 for status in 0..=2 {
389 let command = RuntimeCommand::try_from(BinaryFrame::BridgeResponse {
390 session_id: "s".into(),
391 call_id: 1,
392 status,
393 payload: Vec::new(),
394 })
395 .expect("known bridge response status should be accepted");
396
397 assert!(matches!(
398 command,
399 RuntimeCommand::SendToSession {
400 message: SessionMessage::BridgeResponse(BridgeResponse { status: s, .. }),
401 ..
402 } if s == status
403 ));
404 }
405 }
406}