1use anyhow::{Context, Result, bail};
18use dynamo_kv_hashing::{Request, compute_hash_v2, compute_next_sequence_hash};
19use rustc_hash::FxHashMap;
20use serde::{Deserialize, Serialize};
21use std::fs::File;
22use std::io::{BufWriter, Write};
23use std::path::Path;
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct MooncakeRow {
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub request_id: Option<String>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub session_id: Option<String>,
42 #[serde(default, alias = "input_tokens")]
43 pub input_length: Option<usize>,
44 #[serde(default, alias = "output_tokens")]
45 pub output_length: Option<usize>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub output_token_ids: Option<Vec<u32>>,
48 #[serde(default)]
49 pub hash_ids: Option<Vec<u64>>,
50 #[serde(
51 default,
52 skip_serializing_if = "Option::is_none",
53 alias = "created_time"
54 )]
55 pub timestamp: Option<f64>,
56 #[serde(default, skip_serializing_if = "Option::is_none", alias = "delay_ms")]
57 pub delay: Option<f64>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub priority: Option<i32>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub strict_priority: Option<u32>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub policy_class: Option<String>,
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct AgenticMooncakeRow {
76 pub request_id: String,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub session_id: Option<String>,
79 #[serde(default, alias = "input_tokens")]
80 pub input_length: Option<usize>,
81 #[serde(default, alias = "output_tokens")]
82 pub output_length: Option<usize>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub output_token_ids: Option<Vec<u32>>,
85 #[serde(default)]
86 pub hash_ids: Option<Vec<u64>>,
87 #[serde(
88 default,
89 skip_serializing_if = "Option::is_none",
90 alias = "created_time"
91 )]
92 pub timestamp: Option<f64>,
93 #[serde(default, skip_serializing_if = "Option::is_none", alias = "delay_ms")]
94 pub delay: Option<f64>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub priority: Option<i32>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub strict_priority: Option<u32>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub policy_class: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub request_kind: Option<String>,
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub wait_for: Vec<String>,
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
106 pub branches: Vec<String>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub prefix_reset: Option<bool>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub tool_wait_ms: Option<f64>,
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub tool_events: Vec<AgenticToolEvent>,
113}
114
115impl AgenticMooncakeRow {
116 pub fn dependency_delay_ms(&self) -> f64 {
118 self.delay.unwrap_or(0.0) + self.tool_wait_ms.unwrap_or(0.0)
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct AgenticToolEvent {
125 pub tool_call_id: String,
126 pub tool_class: String,
127 pub started_at_unix_ms: u64,
128 pub ended_at_unix_ms: u64,
129 pub duration_ms: f64,
130 pub status: String,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub output_bytes: Option<u64>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub output_tokens: Option<u64>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub error_type: Option<String>,
137}
138
139pub struct RollingHashIdMapper {
150 block_size: usize,
151 hash_to_id: FxHashMap<u64, u64>,
152 next_id: u64,
153}
154
155impl RollingHashIdMapper {
156 pub fn new(block_size: usize) -> Self {
158 Self {
159 block_size,
160 hash_to_id: FxHashMap::default(),
161 next_id: 0,
162 }
163 }
164
165 pub fn block_size(&self) -> usize {
167 self.block_size
168 }
169
170 pub fn hash_token_blocks(&mut self, tokens: &[u32]) -> Vec<u64> {
178 hash_token_blocks(self, tokens)
179 }
180
181 pub fn try_hash_token_blocks(&mut self, tokens: &[u32]) -> Result<Vec<u64>> {
183 try_hash_token_blocks(self, tokens)
184 }
185
186 pub fn ids_for_sequence_hashes(&mut self, sequence_hashes: &[u64]) -> Vec<u64> {
191 ids_for_sequence_hashes(self, sequence_hashes)
192 }
193}
194
195pub fn hash_token_blocks(mapper: &mut RollingHashIdMapper, tokens: &[u32]) -> Vec<u64> {
205 try_hash_token_blocks(mapper, tokens).expect("Mooncake token-block hashing failed")
206}
207
208pub fn try_hash_token_blocks(mapper: &mut RollingHashIdMapper, tokens: &[u32]) -> Result<Vec<u64>> {
211 let sequence_hashes = sequence_hashes_for_tokens(tokens, mapper.block_size)?;
212 Ok(ids_for_sequence_hashes(mapper, &sequence_hashes))
213}
214
215pub fn sequence_hashes_for_tokens(tokens: &[u32], block_size: usize) -> Result<Vec<u64>> {
217 require_positive("block size", block_size)?;
218 let block_size_u32: u32 = block_size
219 .try_into()
220 .context("block_size does not fit u32")?;
221 let request = Request::builder().tokens(tokens.to_vec()).build()?;
222 let salt_hash = request.salt_hash()?;
223 let mut sequence_hashes = request.into_sequence_hashes(block_size_u32)?;
224 if let Some(partial_hash) =
225 trailing_partial_sequence_hash(salt_hash, block_size, tokens, &sequence_hashes)
226 {
227 sequence_hashes.push(partial_hash);
228 }
229 Ok(sequence_hashes)
230}
231
232fn trailing_partial_sequence_hash(
233 salt_hash: u64,
234 block_size: usize,
235 tokens: &[u32],
236 complete_sequence_hashes: &[u64],
237) -> Option<u64> {
238 let tail_len = tokens.len() % block_size;
239 if tail_len == 0 {
240 return None;
241 }
242
243 let tail = &tokens[tokens.len() - tail_len..];
244 let mut tail_bytes = Vec::with_capacity(std::mem::size_of_val(tail));
245 for token in tail {
246 tail_bytes.extend_from_slice(&token.to_ne_bytes());
247 }
248 let tail_block_hash = compute_hash_v2(&tail_bytes, salt_hash);
249 Some(match complete_sequence_hashes.last().copied() {
250 Some(parent) => compute_next_sequence_hash(parent, tail_block_hash),
251 None => tail_block_hash,
252 })
253}
254
255pub fn ids_for_sequence_hashes(
257 mapper: &mut RollingHashIdMapper,
258 sequence_hashes: &[u64],
259) -> Vec<u64> {
260 sequence_hashes
261 .iter()
262 .map(|sequence_hash| {
263 *mapper.hash_to_id.entry(*sequence_hash).or_insert_with(|| {
264 let next_id = mapper.next_id;
265 mapper.next_id += 1;
266 next_id
267 })
268 })
269 .collect()
270}
271
272#[derive(Debug, Clone, Copy, Default)]
274pub struct WriterStats {
275 pub row_count: usize,
276 pub sidecar_count: usize,
277}
278
279pub struct MooncakeJsonlWriter {
287 output: BufWriter<File>,
288 sidecar: Option<BufWriter<File>>,
289 stats: WriterStats,
290}
291
292impl MooncakeJsonlWriter {
293 pub fn create(output_path: &Path, sidecar_path: Option<&Path>) -> Result<Self> {
296 if let Some(parent) = output_path.parent() {
297 std::fs::create_dir_all(parent)?;
298 }
299 let output = BufWriter::new(
300 File::create(output_path)
301 .with_context(|| format!("failed to create {}", output_path.display()))?,
302 );
303 let sidecar = if let Some(path) = sidecar_path {
304 if let Some(parent) = path.parent() {
305 std::fs::create_dir_all(parent)?;
306 }
307 Some(BufWriter::new(File::create(path).with_context(|| {
308 format!("failed to create {}", path.display())
309 })?))
310 } else {
311 None
312 };
313 Ok(Self {
314 output,
315 sidecar,
316 stats: WriterStats::default(),
317 })
318 }
319
320 pub fn write_row(&mut self, row: &MooncakeRow) -> Result<()> {
322 serde_json::to_writer(&mut self.output, row)?;
323 self.output.write_all(b"\n")?;
324 self.stats.row_count += 1;
325 Ok(())
326 }
327
328 pub fn write_agentic_row(&mut self, row: &AgenticMooncakeRow) -> Result<()> {
330 serde_json::to_writer(&mut self.output, row)?;
331 self.output.write_all(b"\n")?;
332 self.stats.row_count += 1;
333 Ok(())
334 }
335
336 pub fn write_sidecar<S: Serialize>(&mut self, sidecar: &S) -> Result<()> {
338 let writer = self
339 .sidecar
340 .as_mut()
341 .ok_or_else(|| anyhow::anyhow!("sidecar was not configured for this writer"))?;
342 serde_json::to_writer(writer, sidecar)?;
343 let writer = self.sidecar.as_mut().unwrap();
344 writer.write_all(b"\n")?;
345 self.stats.sidecar_count += 1;
346 Ok(())
347 }
348
349 pub fn has_sidecar(&self) -> bool {
351 self.sidecar.is_some()
352 }
353
354 pub fn stats(&self) -> WriterStats {
356 self.stats
357 }
358
359 pub fn finish(mut self) -> Result<WriterStats> {
361 self.output.flush()?;
362 if let Some(sidecar) = self.sidecar.as_mut() {
363 sidecar.flush()?;
364 }
365 Ok(self.stats)
366 }
367}
368
369pub fn write_empty_files(output_path: &Path, sidecar_path: Option<&Path>) -> Result<()> {
373 if let Some(parent) = output_path.parent() {
374 std::fs::create_dir_all(parent)?;
375 }
376 File::create(output_path)
377 .with_context(|| format!("failed to create {}", output_path.display()))?;
378 if let Some(path) = sidecar_path {
379 if let Some(parent) = path.parent() {
380 std::fs::create_dir_all(parent)?;
381 }
382 File::create(path).with_context(|| format!("failed to create {}", path.display()))?;
383 }
384 Ok(())
385}
386
387pub fn require_positive(name: &str, value: usize) -> Result<()> {
391 if value == 0 {
392 bail!("{name} must be greater than 0");
393 }
394 Ok(())
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use serde_json::{Value, json};
401 use tempfile::TempDir;
402
403 #[test]
404 fn shared_prefix_yields_shared_leading_hash_ids() {
405 let mut mapper = RollingHashIdMapper::new(2);
406 let prefix = vec![1u32, 2, 3, 4];
407 let extended = vec![1u32, 2, 3, 4, 5, 6];
408
409 let prefix_ids = mapper.hash_token_blocks(&prefix);
410 let extended_ids = mapper.hash_token_blocks(&extended);
411
412 assert_eq!(prefix_ids.len(), 2);
413 assert_eq!(extended_ids.len(), 3);
414 assert_eq!(extended_ids[..2], prefix_ids[..]);
415 }
416
417 #[test]
418 fn mapper_state_is_reused_across_requests() {
419 let mut mapper = RollingHashIdMapper::new(4);
420 let request_a = vec![10u32, 20, 30, 40, 50, 60, 70, 80];
421 let request_b = vec![10u32, 20, 30, 40, 50, 60, 70, 80];
422 let request_c = vec![10u32, 20, 30, 40, 99, 99, 99, 99];
423
424 let ids_a = mapper.hash_token_blocks(&request_a);
425 let ids_b = mapper.hash_token_blocks(&request_b);
426 let ids_c = mapper.hash_token_blocks(&request_c);
427
428 assert_eq!(ids_a, ids_b);
429 assert_eq!(ids_c[0], ids_a[0], "shared first block should keep its id");
430 assert_ne!(
431 ids_c[1], ids_a[1],
432 "diverging tail block must get a fresh id"
433 );
434 }
435
436 #[test]
437 fn free_function_and_method_agree() {
438 let mut mapper_a = RollingHashIdMapper::new(2);
439 let mut mapper_b = RollingHashIdMapper::new(2);
440 let tokens = vec![7u32, 8, 9, 10, 11];
441
442 let via_method = mapper_a.hash_token_blocks(&tokens);
443 let via_function = hash_token_blocks(&mut mapper_b, &tokens);
444
445 assert_eq!(via_method, via_function);
446 }
447
448 #[test]
449 fn exact_token_blocks_match_shared_kv_hashing_contract() {
450 let tokens = vec![7u32, 8, 9, 10, 11, 12, 13, 14];
451 let request = Request::builder().tokens(tokens.clone()).build().unwrap();
452 let expected = request.into_sequence_hashes(4).unwrap();
453
454 assert_eq!(sequence_hashes_for_tokens(&tokens, 4).unwrap(), expected);
455 }
456
457 #[test]
458 fn empty_token_input_yields_empty_hash_ids() {
459 let mut mapper = RollingHashIdMapper::new(4);
460 assert!(mapper.hash_token_blocks(&[]).is_empty());
461 }
462
463 #[test]
464 fn trailing_partial_block_preserves_replay_capacity() {
465 let mut mapper = RollingHashIdMapper::new(4);
466
467 assert_eq!(mapper.hash_token_blocks(&[1, 2, 3]), vec![0]);
468 assert_eq!(mapper.hash_token_blocks(&[1, 2, 3, 4, 5, 6]), vec![1, 2]);
469 }
470
471 #[test]
472 fn trailing_partial_block_uses_shared_chain_contract() {
473 let tokens = vec![1u32, 2, 3, 4, 5, 6];
474 let request = Request::builder().tokens(tokens.clone()).build().unwrap();
475 let salt_hash = request.salt_hash().unwrap();
476 let mut expected = request.into_sequence_hashes(4).unwrap();
477 let mut tail_bytes = Vec::new();
478 for token in &tokens[4..] {
479 tail_bytes.extend_from_slice(&token.to_ne_bytes());
480 }
481 let tail_block_hash = compute_hash_v2(&tail_bytes, salt_hash);
482 expected.push(compute_next_sequence_hash(expected[0], tail_block_hash));
483
484 assert_eq!(sequence_hashes_for_tokens(&tokens, 4).unwrap(), expected);
485 }
486
487 #[test]
488 fn exact_block_boundary_does_not_add_partial_hash_id() {
489 let mut mapper = RollingHashIdMapper::new(4);
490
491 assert_eq!(mapper.hash_token_blocks(&[1, 2, 3, 4]), vec![0]);
492 assert_eq!(
493 mapper.hash_token_blocks(&[1, 2, 3, 4, 5, 6, 7, 8]),
494 vec![0, 1]
495 );
496 }
497
498 #[test]
499 fn try_hash_token_blocks_rejects_zero_block_size() {
500 let mut mapper = RollingHashIdMapper::new(0);
501 let err = mapper.try_hash_token_blocks(&[1, 2, 3]).unwrap_err();
502
503 assert!(err.to_string().contains("block size"));
504 }
505
506 #[test]
507 fn precomputed_sequence_hashes_map_to_stable_ids() {
508 let mut mapper = RollingHashIdMapper::new(64);
509
510 let first = mapper.ids_for_sequence_hashes(&[101, 202, 303]);
511 let second = mapper.ids_for_sequence_hashes(&[101, 202, 404]);
512
513 assert_eq!(first[..2], second[..2]);
514 assert_ne!(first[2], second[2]);
515 }
516
517 #[test]
518 fn row_omits_timestamp_and_delay_when_absent() {
519 let row = MooncakeRow {
520 session_id: Some("s".to_string()),
521 input_length: Some(4),
522 output_length: Some(1),
523 hash_ids: Some(vec![0, 1]),
524 timestamp: None,
525 delay: None,
526 ..Default::default()
527 };
528 let rendered: Value = serde_json::to_value(&row).unwrap();
529 assert!(rendered.get("timestamp").is_none());
530 assert!(rendered.get("delay").is_none());
531 assert_eq!(rendered["hash_ids"], json!([0, 1]));
532 }
533
534 #[test]
535 fn row_serializes_optional_fields_when_set() {
536 let with_timestamp = MooncakeRow {
537 session_id: Some("s".to_string()),
538 input_length: Some(4),
539 output_length: Some(1),
540 hash_ids: Some(vec![]),
541 timestamp: Some(0.0),
542 delay: None,
543 ..Default::default()
544 };
545 let with_delay = MooncakeRow {
546 session_id: Some("s".to_string()),
547 input_length: Some(4),
548 output_length: Some(1),
549 hash_ids: Some(vec![]),
550 timestamp: None,
551 delay: Some(123.0),
552 ..Default::default()
553 };
554 let v_ts: Value = serde_json::to_value(&with_timestamp).unwrap();
555 let v_dl: Value = serde_json::to_value(&with_delay).unwrap();
556 assert_eq!(v_ts["timestamp"], json!(0.0));
557 assert!(v_ts.get("delay").is_none());
558 assert_eq!(v_dl["delay"], json!(123.0));
559 assert!(v_dl.get("timestamp").is_none());
560 }
561
562 #[test]
563 fn row_deserializes_canonical_field_names() {
564 let raw = r#"{"session_id":"s","input_length":4,"output_length":1,"hash_ids":[0,1],"timestamp":12.5,"delay":3.0}"#;
565 let row: MooncakeRow = serde_json::from_str(raw).unwrap();
566 assert_eq!(row.session_id.as_deref(), Some("s"));
567 assert_eq!(row.input_length, Some(4));
568 assert_eq!(row.output_length, Some(1));
569 assert_eq!(row.hash_ids, Some(vec![0, 1]));
570 assert_eq!(row.timestamp, Some(12.5));
571 assert_eq!(row.delay, Some(3.0));
572 }
573
574 #[test]
575 fn row_deserializes_upstream_mooncake_aliases() {
576 let raw = r#"{"input_tokens":4,"output_tokens":1,"hash_ids":[0,1],"created_time":12.5,"delay_ms":3.0}"#;
577 let row: MooncakeRow = serde_json::from_str(raw).unwrap();
578 assert_eq!(row.input_length, Some(4));
579 assert_eq!(row.output_length, Some(1));
580 assert_eq!(row.timestamp, Some(12.5));
581 assert_eq!(row.delay, Some(3.0));
582 }
583
584 #[test]
585 fn row_alias_input_round_trips_to_canonical_fields() {
586 let raw = r#"{"input_tokens":8,"output_tokens":2,"created_time":12.5,"delay_ms":3.0}"#;
587 let mut row: MooncakeRow = serde_json::from_str(raw).unwrap();
588
589 let tokens: Vec<u32> = (0..row.input_length.unwrap() as u32).collect();
590 let mut mapper = RollingHashIdMapper::new(4);
591 row.hash_ids = Some(mapper.hash_token_blocks(&tokens));
592
593 let rendered: Value = serde_json::to_value(&row).unwrap();
594 assert_eq!(rendered["input_length"], json!(8));
595 assert_eq!(rendered["output_length"], json!(2));
596 assert_eq!(rendered["timestamp"], json!(12.5));
597 assert_eq!(rendered["delay"], json!(3.0));
598 assert_eq!(rendered["hash_ids"], json!([0, 1]));
599 assert!(rendered.get("input_tokens").is_none());
600 assert!(rendered.get("output_tokens").is_none());
601 assert!(rendered.get("created_time").is_none());
602 assert!(rendered.get("delay_ms").is_none());
603 }
604
605 #[test]
606 fn row_replay_fields_round_trip_canonical_and_alias_inputs() {
607 let canonical = r#"{"request_id":"r1","session_id":"s","input_length":8,"output_length":3,"output_token_ids":[101,102,103],"hash_ids":[0,1],"timestamp":12.5,"delay":3.0}"#;
608 let row: MooncakeRow = serde_json::from_str(canonical).unwrap();
609 assert_eq!(row.request_id.as_deref(), Some("r1"));
610 assert_eq!(row.output_length, Some(3));
611 assert_eq!(row.output_token_ids, Some(vec![101, 102, 103]));
612
613 let rendered: Value = serde_json::to_value(&row).unwrap();
614 assert_eq!(rendered["request_id"], json!("r1"));
615 assert_eq!(rendered["output_token_ids"], json!([101, 102, 103]));
616 let decoded: MooncakeRow = serde_json::from_value(rendered).unwrap();
617 assert_eq!(decoded.request_id.as_deref(), Some("r1"));
618 assert_eq!(decoded.output_token_ids, Some(vec![101, 102, 103]));
619
620 let aliased = r#"{"request_id":"r2","input_tokens":4,"output_tokens":2,"output_token_ids":[201,202],"hash_ids":[7],"created_time":1.5,"delay_ms":0.5}"#;
621 let row: MooncakeRow = serde_json::from_str(aliased).unwrap();
622 assert_eq!(row.request_id.as_deref(), Some("r2"));
623 assert_eq!(row.input_length, Some(4));
624 assert_eq!(row.output_length, Some(2));
625 assert_eq!(row.output_token_ids, Some(vec![201, 202]));
626
627 let rendered: Value = serde_json::to_value(&row).unwrap();
628 assert_eq!(rendered["input_length"], json!(4));
629 assert_eq!(rendered["output_length"], json!(2));
630 assert_eq!(rendered["output_token_ids"], json!([201, 202]));
631 assert!(rendered.get("input_tokens").is_none());
632 assert!(rendered.get("output_tokens").is_none());
633 }
634
635 #[test]
636 fn row_canonical_input_round_trips_without_renaming() {
637 let raw = r#"{"input_length":8,"output_length":2,"timestamp":12.5,"delay":3.0}"#;
638 let mut row: MooncakeRow = serde_json::from_str(raw).unwrap();
639
640 let tokens: Vec<u32> = (0..row.input_length.unwrap() as u32).collect();
641 let mut mapper = RollingHashIdMapper::new(4);
642 row.hash_ids = Some(mapper.hash_token_blocks(&tokens));
643
644 let rendered: Value = serde_json::to_value(&row).unwrap();
645 assert_eq!(rendered["input_length"], json!(8));
646 assert_eq!(rendered["output_length"], json!(2));
647 assert_eq!(rendered["timestamp"], json!(12.5));
648 assert_eq!(rendered["delay"], json!(3.0));
649 assert_eq!(rendered["hash_ids"], json!([0, 1]));
650 assert!(rendered.get("input_tokens").is_none());
651 assert!(rendered.get("output_tokens").is_none());
652 assert!(rendered.get("created_time").is_none());
653 assert!(rendered.get("delay_ms").is_none());
654 }
655
656 #[test]
657 fn row_deserializes_with_missing_optional_fields() {
658 let raw = r#"{"output_length":2}"#;
659 let row: MooncakeRow = serde_json::from_str(raw).unwrap();
660 assert_eq!(row.session_id, None);
661 assert_eq!(row.input_length, None);
662 assert_eq!(row.output_length, Some(2));
663 assert_eq!(row.hash_ids, None);
664 assert_eq!(row.timestamp, None);
665 assert_eq!(row.delay, None);
666 assert_eq!(row.priority, None);
667 assert_eq!(row.strict_priority, None);
668 assert_eq!(row.policy_class, None);
669 let rendered: Value = serde_json::to_value(&row).unwrap();
670 assert!(rendered.get("priority").is_none());
671 assert!(rendered.get("strict_priority").is_none());
672 assert!(rendered.get("policy_class").is_none());
673 assert!(rendered.get("request_id").is_none());
674 assert!(rendered.get("output_token_ids").is_none());
675 }
676
677 #[test]
678 fn row_round_trips_priorities() {
679 for priority in [Some(7), Some(0), Some(-3)] {
680 let raw = json!({
681 "output_length": 2,
682 "priority": priority,
683 "strict_priority": 9,
684 "policy_class": "latency"
685 });
686 let row: MooncakeRow = serde_json::from_value(raw).unwrap();
687 assert_eq!(row.priority, priority);
688 assert_eq!(row.strict_priority, Some(9));
689 assert_eq!(row.policy_class.as_deref(), Some("latency"));
690
691 let rendered: Value = serde_json::to_value(&row).unwrap();
692 assert_eq!(rendered["priority"], json!(priority.unwrap()));
693 assert_eq!(rendered["strict_priority"], json!(9));
694 assert_eq!(rendered["policy_class"], json!("latency"));
695 }
696 }
697
698 #[test]
699 fn agentic_row_defaults_workflow_fields() {
700 let raw = r#"{"request_id":"r1","input_length":4,"output_length":1,"hash_ids":[0,1],"timestamp":10.0}"#;
701 let row: AgenticMooncakeRow = serde_json::from_str(raw).unwrap();
702
703 assert_eq!(row.request_id, "r1");
704 assert!(row.wait_for.is_empty());
705 assert!(row.branches.is_empty());
706 assert_eq!(row.prefix_reset, None);
707 assert_eq!(row.dependency_delay_ms(), 0.0);
708 assert_eq!(row.priority, None);
709 assert_eq!(row.strict_priority, None);
710 assert_eq!(row.policy_class, None);
711 let rendered: Value = serde_json::to_value(&row).unwrap();
712 assert!(rendered.get("priority").is_none());
713 assert!(rendered.get("strict_priority").is_none());
714 assert!(rendered.get("policy_class").is_none());
715 assert!(rendered.get("output_token_ids").is_none());
716 }
717
718 #[test]
719 fn agentic_row_replay_fields_round_trip_canonical_and_alias_inputs() {
720 let canonical = r#"{"request_id":"r1","session_id":"s","input_length":8,"output_length":3,"output_token_ids":[101,102,103],"hash_ids":[0,1],"timestamp":12.5,"delay":3.0}"#;
721 let row: AgenticMooncakeRow = serde_json::from_str(canonical).unwrap();
722 assert_eq!(row.request_id, "r1");
723 assert_eq!(row.output_length, Some(3));
724 assert_eq!(row.output_token_ids, Some(vec![101, 102, 103]));
725
726 let rendered: Value = serde_json::to_value(&row).unwrap();
727 assert_eq!(rendered["request_id"], json!("r1"));
728 assert_eq!(rendered["output_token_ids"], json!([101, 102, 103]));
729 let decoded: AgenticMooncakeRow = serde_json::from_value(rendered).unwrap();
730 assert_eq!(decoded.request_id, "r1");
731 assert_eq!(decoded.output_token_ids, Some(vec![101, 102, 103]));
732
733 let aliased = r#"{"request_id":"r2","input_tokens":4,"output_tokens":2,"output_token_ids":[201,202],"hash_ids":[7],"created_time":1.5,"delay_ms":0.5}"#;
734 let row: AgenticMooncakeRow = serde_json::from_str(aliased).unwrap();
735 assert_eq!(row.request_id, "r2");
736 assert_eq!(row.input_length, Some(4));
737 assert_eq!(row.output_length, Some(2));
738 assert_eq!(row.output_token_ids, Some(vec![201, 202]));
739
740 let rendered: Value = serde_json::to_value(&row).unwrap();
741 assert_eq!(rendered["input_length"], json!(4));
742 assert_eq!(rendered["output_length"], json!(2));
743 assert_eq!(rendered["output_token_ids"], json!([201, 202]));
744 assert!(rendered.get("input_tokens").is_none());
745 assert!(rendered.get("output_tokens").is_none());
746 }
747
748 #[test]
749 fn agentic_row_round_trips_priorities() {
750 let raw = r#"{"request_id":"r1","priority":-2,"strict_priority":4,"policy_class":"batch"}"#;
751 let row: AgenticMooncakeRow = serde_json::from_str(raw).unwrap();
752 assert_eq!(row.priority, Some(-2));
753 assert_eq!(row.strict_priority, Some(4));
754 assert_eq!(row.policy_class.as_deref(), Some("batch"));
755
756 let rendered: Value = serde_json::to_value(&row).unwrap();
757 assert_eq!(rendered["priority"], json!(-2));
758 assert_eq!(rendered["strict_priority"], json!(4));
759 assert_eq!(rendered["policy_class"], json!("batch"));
760 }
761
762 #[test]
763 fn agentic_row_delay_includes_tool_wait() {
764 let row = AgenticMooncakeRow {
765 request_id: "r2".to_string(),
766 session_id: Some("trajectory-a".to_string()),
767 input_length: Some(4),
768 output_length: Some(1),
769 hash_ids: Some(vec![0, 1]),
770 timestamp: Some(20.0),
771 delay: Some(3.0),
772 wait_for: vec!["r1".to_string()],
773 branches: vec!["r3".to_string()],
774 prefix_reset: Some(false),
775 tool_wait_ms: Some(7.0),
776 ..Default::default()
777 };
778
779 assert_eq!(row.dependency_delay_ms(), 10.0);
780 let rendered: Value = serde_json::to_value(&row).unwrap();
781 assert_eq!(rendered["request_id"], json!("r2"));
782 assert_eq!(rendered["wait_for"], json!(["r1"]));
783 assert_eq!(rendered["branches"], json!(["r3"]));
784 assert_eq!(rendered["tool_wait_ms"], json!(7.0));
785 assert!(rendered.get("tool_events").is_none());
786 }
787
788 #[test]
789 fn agentic_row_round_trips_tool_events() {
790 let row = AgenticMooncakeRow {
791 request_id: "r1".to_string(),
792 session_id: Some("trajectory-a".to_string()),
793 input_length: Some(4),
794 output_length: Some(1),
795 hash_ids: Some(vec![0, 1]),
796 timestamp: Some(0.0),
797 delay: Some(0.0),
798 priority: Some(5),
799 strict_priority: Some(6),
800 prefix_reset: Some(true),
801 tool_wait_ms: Some(8.0),
802 tool_events: vec![AgenticToolEvent {
803 tool_call_id: "call-1".to_string(),
804 tool_class: "web_search".to_string(),
805 started_at_unix_ms: 1_000,
806 ended_at_unix_ms: 1_008,
807 duration_ms: 8.0,
808 status: "succeeded".to_string(),
809 output_bytes: Some(512),
810 output_tokens: None,
811 error_type: None,
812 }],
813 ..Default::default()
814 };
815
816 let rendered = serde_json::to_string(&row).unwrap();
817 let decoded: AgenticMooncakeRow = serde_json::from_str(&rendered).unwrap();
818 assert_eq!(decoded.tool_events.len(), 1);
819 assert_eq!(decoded.tool_events[0].tool_class, "web_search");
820 assert_eq!(decoded.tool_events[0].output_bytes, Some(512));
821 assert_eq!(decoded.priority, Some(5));
822 assert_eq!(decoded.strict_priority, Some(6));
823 }
824
825 #[test]
826 fn writer_writes_rows_and_sidecar_jsonl() {
827 let temp = TempDir::new().unwrap();
828 let output = temp.path().join("trace.jsonl");
829 let sidecar = temp.path().join("trace.sidecar.jsonl");
830
831 let mut writer = MooncakeJsonlWriter::create(&output, Some(&sidecar)).unwrap();
832 writer
833 .write_row(&MooncakeRow {
834 session_id: Some("s".to_string()),
835 input_length: Some(2),
836 output_length: Some(1),
837 hash_ids: Some(vec![0]),
838 timestamp: Some(0.0),
839 delay: None,
840 ..Default::default()
841 })
842 .unwrap();
843 writer.write_sidecar(&json!({"k": "v"})).unwrap();
844 let stats = writer.finish().unwrap();
845
846 assert_eq!(stats.row_count, 1);
847 assert_eq!(stats.sidecar_count, 1);
848
849 let row_lines: Vec<Value> = std::fs::read_to_string(&output)
850 .unwrap()
851 .lines()
852 .map(|line| serde_json::from_str(line).unwrap())
853 .collect();
854 let sidecar_lines: Vec<Value> = std::fs::read_to_string(&sidecar)
855 .unwrap()
856 .lines()
857 .map(|line| serde_json::from_str(line).unwrap())
858 .collect();
859 assert_eq!(row_lines.len(), 1);
860 assert_eq!(sidecar_lines, vec![json!({"k": "v"})]);
861 assert_eq!(row_lines[0]["session_id"], json!("s"));
862 assert!(row_lines[0].get("delay").is_none());
863 }
864
865 #[test]
866 fn writer_writes_agentic_rows() {
867 let temp = TempDir::new().unwrap();
868 let output = temp.path().join("agentic.jsonl");
869 let mut writer = MooncakeJsonlWriter::create(&output, None).unwrap();
870 writer
871 .write_agentic_row(&AgenticMooncakeRow {
872 request_id: "r1".to_string(),
873 session_id: None,
874 input_length: Some(2),
875 output_length: Some(1),
876 hash_ids: Some(vec![0]),
877 timestamp: Some(0.0),
878 delay: None,
879 prefix_reset: Some(true),
880 ..Default::default()
881 })
882 .unwrap();
883 let stats = writer.finish().unwrap();
884
885 assert_eq!(stats.row_count, 1);
886 let row_lines: Vec<Value> = std::fs::read_to_string(&output)
887 .unwrap()
888 .lines()
889 .map(|line| serde_json::from_str(line).unwrap())
890 .collect();
891 assert_eq!(row_lines[0]["request_id"], json!("r1"));
892 assert_eq!(row_lines[0]["prefix_reset"], json!(true));
893 }
894
895 #[test]
896 fn writer_without_sidecar_rejects_sidecar_writes() {
897 let temp = TempDir::new().unwrap();
898 let output = temp.path().join("trace.jsonl");
899 let mut writer = MooncakeJsonlWriter::create(&output, None).unwrap();
900 assert!(!writer.has_sidecar());
901 let err = writer.write_sidecar(&json!({})).unwrap_err();
902 assert!(err.to_string().contains("sidecar was not configured"));
903 }
904}