1use anyhow::{Context, Result, bail};
19use bytemuck::cast_slice;
20use dynamo_tokens::compute_hash_v2;
21use rustc_hash::FxHashMap;
22use serde::{Deserialize, Serialize};
23use std::fs::File;
24use std::io::{BufWriter, Write};
25use std::path::Path;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct MooncakeRow {
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)]
47 pub hash_ids: Option<Vec<u64>>,
48 #[serde(
49 default,
50 skip_serializing_if = "Option::is_none",
51 alias = "created_time"
52 )]
53 pub timestamp: Option<f64>,
54 #[serde(default, skip_serializing_if = "Option::is_none", alias = "delay_ms")]
55 pub delay: Option<f64>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AgenticMooncakeRow {
68 pub request_id: String,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub session_id: Option<String>,
71 #[serde(default, alias = "input_tokens")]
72 pub input_length: Option<usize>,
73 #[serde(default, alias = "output_tokens")]
74 pub output_length: Option<usize>,
75 #[serde(default)]
76 pub hash_ids: Option<Vec<u64>>,
77 #[serde(
78 default,
79 skip_serializing_if = "Option::is_none",
80 alias = "created_time"
81 )]
82 pub timestamp: Option<f64>,
83 #[serde(default, skip_serializing_if = "Option::is_none", alias = "delay_ms")]
84 pub delay: Option<f64>,
85 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub wait_for: Vec<String>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 pub branches: Vec<String>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub prefix_reset: Option<bool>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub tool_wait_ms: Option<f64>,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub tool_events: Vec<AgenticToolEvent>,
95}
96
97impl AgenticMooncakeRow {
98 pub fn dependency_delay_ms(&self) -> f64 {
100 self.delay.unwrap_or(0.0) + self.tool_wait_ms.unwrap_or(0.0)
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107pub struct AgenticToolEvent {
108 pub tool_call_id: String,
109 pub tool_class: String,
110 pub started_at_unix_ms: u64,
111 pub ended_at_unix_ms: u64,
112 pub duration_ms: f64,
113 pub status: String,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub output_bytes: Option<u64>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub output_tokens: Option<u64>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub error_type: Option<String>,
120}
121
122pub struct RollingHashIdMapper {
133 block_size: usize,
134 hash_to_id: FxHashMap<u64, u64>,
135 next_id: u64,
136}
137
138impl RollingHashIdMapper {
139 pub fn new(block_size: usize) -> Self {
141 Self {
142 block_size,
143 hash_to_id: FxHashMap::default(),
144 next_id: 0,
145 }
146 }
147
148 pub fn block_size(&self) -> usize {
150 self.block_size
151 }
152
153 pub fn hash_token_blocks(&mut self, tokens: &[u32]) -> Vec<u64> {
160 hash_token_blocks(self, tokens)
161 }
162
163 pub fn ids_for_sequence_hashes(&mut self, sequence_hashes: &[u64]) -> Vec<u64> {
168 ids_for_sequence_hashes(self, sequence_hashes)
169 }
170}
171
172pub fn hash_token_blocks(mapper: &mut RollingHashIdMapper, tokens: &[u32]) -> Vec<u64> {
180 let block_size = mapper.block_size;
181 let mut hash_ids = Vec::with_capacity(tokens.len().div_ceil(block_size));
182 let mut parent_hash = 0_u64;
183 for block in tokens.chunks(block_size) {
184 let block_hash = compute_hash_v2(cast_slice(block), 0);
185 let combined_hash = compute_hash_v2(&block_hash.to_be_bytes(), parent_hash);
186 let id = *mapper.hash_to_id.entry(combined_hash).or_insert_with(|| {
187 let next_id = mapper.next_id;
188 mapper.next_id += 1;
189 next_id
190 });
191 hash_ids.push(id);
192 parent_hash = combined_hash;
193 }
194 hash_ids
195}
196
197pub fn ids_for_sequence_hashes(
199 mapper: &mut RollingHashIdMapper,
200 sequence_hashes: &[u64],
201) -> Vec<u64> {
202 sequence_hashes
203 .iter()
204 .map(|sequence_hash| {
205 *mapper.hash_to_id.entry(*sequence_hash).or_insert_with(|| {
206 let next_id = mapper.next_id;
207 mapper.next_id += 1;
208 next_id
209 })
210 })
211 .collect()
212}
213
214#[derive(Debug, Clone, Copy, Default)]
216pub struct WriterStats {
217 pub row_count: usize,
218 pub sidecar_count: usize,
219}
220
221pub struct MooncakeJsonlWriter {
229 output: BufWriter<File>,
230 sidecar: Option<BufWriter<File>>,
231 stats: WriterStats,
232}
233
234impl MooncakeJsonlWriter {
235 pub fn create(output_path: &Path, sidecar_path: Option<&Path>) -> Result<Self> {
238 if let Some(parent) = output_path.parent() {
239 std::fs::create_dir_all(parent)?;
240 }
241 let output = BufWriter::new(
242 File::create(output_path)
243 .with_context(|| format!("failed to create {}", output_path.display()))?,
244 );
245 let sidecar = if let Some(path) = sidecar_path {
246 if let Some(parent) = path.parent() {
247 std::fs::create_dir_all(parent)?;
248 }
249 Some(BufWriter::new(File::create(path).with_context(|| {
250 format!("failed to create {}", path.display())
251 })?))
252 } else {
253 None
254 };
255 Ok(Self {
256 output,
257 sidecar,
258 stats: WriterStats::default(),
259 })
260 }
261
262 pub fn write_row(&mut self, row: &MooncakeRow) -> Result<()> {
264 serde_json::to_writer(&mut self.output, row)?;
265 self.output.write_all(b"\n")?;
266 self.stats.row_count += 1;
267 Ok(())
268 }
269
270 pub fn write_agentic_row(&mut self, row: &AgenticMooncakeRow) -> Result<()> {
272 serde_json::to_writer(&mut self.output, row)?;
273 self.output.write_all(b"\n")?;
274 self.stats.row_count += 1;
275 Ok(())
276 }
277
278 pub fn write_sidecar<S: Serialize>(&mut self, sidecar: &S) -> Result<()> {
280 let writer = self
281 .sidecar
282 .as_mut()
283 .ok_or_else(|| anyhow::anyhow!("sidecar was not configured for this writer"))?;
284 serde_json::to_writer(writer, sidecar)?;
285 let writer = self.sidecar.as_mut().unwrap();
286 writer.write_all(b"\n")?;
287 self.stats.sidecar_count += 1;
288 Ok(())
289 }
290
291 pub fn has_sidecar(&self) -> bool {
293 self.sidecar.is_some()
294 }
295
296 pub fn stats(&self) -> WriterStats {
298 self.stats
299 }
300
301 pub fn finish(mut self) -> Result<WriterStats> {
303 self.output.flush()?;
304 if let Some(sidecar) = self.sidecar.as_mut() {
305 sidecar.flush()?;
306 }
307 Ok(self.stats)
308 }
309}
310
311pub fn write_empty_files(output_path: &Path, sidecar_path: Option<&Path>) -> Result<()> {
315 if let Some(parent) = output_path.parent() {
316 std::fs::create_dir_all(parent)?;
317 }
318 File::create(output_path)
319 .with_context(|| format!("failed to create {}", output_path.display()))?;
320 if let Some(path) = sidecar_path {
321 if let Some(parent) = path.parent() {
322 std::fs::create_dir_all(parent)?;
323 }
324 File::create(path).with_context(|| format!("failed to create {}", path.display()))?;
325 }
326 Ok(())
327}
328
329pub fn require_positive(name: &str, value: usize) -> Result<()> {
333 if value == 0 {
334 bail!("{name} must be greater than 0");
335 }
336 Ok(())
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use serde_json::{Value, json};
343 use tempfile::TempDir;
344
345 #[test]
346 fn shared_prefix_yields_shared_leading_hash_ids() {
347 let mut mapper = RollingHashIdMapper::new(2);
348 let prefix = vec![1u32, 2, 3, 4];
349 let extended = vec![1u32, 2, 3, 4, 5, 6];
350
351 let prefix_ids = mapper.hash_token_blocks(&prefix);
352 let extended_ids = mapper.hash_token_blocks(&extended);
353
354 assert_eq!(prefix_ids.len(), 2);
355 assert_eq!(extended_ids.len(), 3);
356 assert_eq!(extended_ids[..2], prefix_ids[..]);
357 }
358
359 #[test]
360 fn mapper_state_is_reused_across_requests() {
361 let mut mapper = RollingHashIdMapper::new(4);
362 let request_a = vec![10u32, 20, 30, 40, 50, 60, 70, 80];
363 let request_b = vec![10u32, 20, 30, 40, 50, 60, 70, 80];
364 let request_c = vec![10u32, 20, 30, 40, 99, 99, 99, 99];
365
366 let ids_a = mapper.hash_token_blocks(&request_a);
367 let ids_b = mapper.hash_token_blocks(&request_b);
368 let ids_c = mapper.hash_token_blocks(&request_c);
369
370 assert_eq!(ids_a, ids_b);
371 assert_eq!(ids_c[0], ids_a[0], "shared first block should keep its id");
372 assert_ne!(
373 ids_c[1], ids_a[1],
374 "diverging tail block must get a fresh id"
375 );
376 }
377
378 #[test]
379 fn free_function_and_method_agree() {
380 let mut mapper_a = RollingHashIdMapper::new(2);
381 let mut mapper_b = RollingHashIdMapper::new(2);
382 let tokens = vec![7u32, 8, 9, 10, 11];
383
384 let via_method = mapper_a.hash_token_blocks(&tokens);
385 let via_function = hash_token_blocks(&mut mapper_b, &tokens);
386
387 assert_eq!(via_method, via_function);
388 }
389
390 #[test]
391 fn empty_token_input_yields_empty_hash_ids() {
392 let mut mapper = RollingHashIdMapper::new(4);
393 assert!(mapper.hash_token_blocks(&[]).is_empty());
394 }
395
396 #[test]
397 fn precomputed_sequence_hashes_map_to_stable_ids() {
398 let mut mapper = RollingHashIdMapper::new(64);
399
400 let first = mapper.ids_for_sequence_hashes(&[101, 202, 303]);
401 let second = mapper.ids_for_sequence_hashes(&[101, 202, 404]);
402
403 assert_eq!(first[..2], second[..2]);
404 assert_ne!(first[2], second[2]);
405 }
406
407 #[test]
408 fn row_omits_timestamp_and_delay_when_absent() {
409 let row = MooncakeRow {
410 session_id: Some("s".to_string()),
411 input_length: Some(4),
412 output_length: Some(1),
413 hash_ids: Some(vec![0, 1]),
414 timestamp: None,
415 delay: None,
416 };
417 let rendered: Value = serde_json::to_value(&row).unwrap();
418 assert!(rendered.get("timestamp").is_none());
419 assert!(rendered.get("delay").is_none());
420 assert_eq!(rendered["hash_ids"], json!([0, 1]));
421 }
422
423 #[test]
424 fn row_serializes_optional_fields_when_set() {
425 let with_timestamp = MooncakeRow {
426 session_id: Some("s".to_string()),
427 input_length: Some(4),
428 output_length: Some(1),
429 hash_ids: Some(vec![]),
430 timestamp: Some(0.0),
431 delay: None,
432 };
433 let with_delay = MooncakeRow {
434 session_id: Some("s".to_string()),
435 input_length: Some(4),
436 output_length: Some(1),
437 hash_ids: Some(vec![]),
438 timestamp: None,
439 delay: Some(123.0),
440 };
441 let v_ts: Value = serde_json::to_value(&with_timestamp).unwrap();
442 let v_dl: Value = serde_json::to_value(&with_delay).unwrap();
443 assert_eq!(v_ts["timestamp"], json!(0.0));
444 assert!(v_ts.get("delay").is_none());
445 assert_eq!(v_dl["delay"], json!(123.0));
446 assert!(v_dl.get("timestamp").is_none());
447 }
448
449 #[test]
450 fn row_deserializes_canonical_field_names() {
451 let raw = r#"{"session_id":"s","input_length":4,"output_length":1,"hash_ids":[0,1],"timestamp":12.5,"delay":3.0}"#;
452 let row: MooncakeRow = serde_json::from_str(raw).unwrap();
453 assert_eq!(row.session_id.as_deref(), Some("s"));
454 assert_eq!(row.input_length, Some(4));
455 assert_eq!(row.output_length, Some(1));
456 assert_eq!(row.hash_ids, Some(vec![0, 1]));
457 assert_eq!(row.timestamp, Some(12.5));
458 assert_eq!(row.delay, Some(3.0));
459 }
460
461 #[test]
462 fn row_deserializes_upstream_mooncake_aliases() {
463 let raw = r#"{"input_tokens":4,"output_tokens":1,"hash_ids":[0,1],"created_time":12.5,"delay_ms":3.0}"#;
464 let row: MooncakeRow = serde_json::from_str(raw).unwrap();
465 assert_eq!(row.input_length, Some(4));
466 assert_eq!(row.output_length, Some(1));
467 assert_eq!(row.timestamp, Some(12.5));
468 assert_eq!(row.delay, Some(3.0));
469 }
470
471 #[test]
472 fn row_deserializes_with_missing_optional_fields() {
473 let raw = r#"{"output_length":2}"#;
474 let row: MooncakeRow = serde_json::from_str(raw).unwrap();
475 assert_eq!(row.session_id, None);
476 assert_eq!(row.input_length, None);
477 assert_eq!(row.output_length, Some(2));
478 assert_eq!(row.hash_ids, None);
479 assert_eq!(row.timestamp, None);
480 assert_eq!(row.delay, None);
481 }
482
483 #[test]
484 fn agentic_row_defaults_workflow_fields() {
485 let raw = r#"{"request_id":"r1","input_length":4,"output_length":1,"hash_ids":[0,1],"timestamp":10.0}"#;
486 let row: AgenticMooncakeRow = serde_json::from_str(raw).unwrap();
487
488 assert_eq!(row.request_id, "r1");
489 assert!(row.wait_for.is_empty());
490 assert!(row.branches.is_empty());
491 assert_eq!(row.prefix_reset, None);
492 assert_eq!(row.dependency_delay_ms(), 0.0);
493 }
494
495 #[test]
496 fn agentic_row_delay_includes_tool_wait() {
497 let row = AgenticMooncakeRow {
498 request_id: "r2".to_string(),
499 session_id: Some("trajectory-a".to_string()),
500 input_length: Some(4),
501 output_length: Some(1),
502 hash_ids: Some(vec![0, 1]),
503 timestamp: Some(20.0),
504 delay: Some(3.0),
505 wait_for: vec!["r1".to_string()],
506 branches: vec!["r3".to_string()],
507 prefix_reset: Some(false),
508 tool_wait_ms: Some(7.0),
509 tool_events: Vec::new(),
510 };
511
512 assert_eq!(row.dependency_delay_ms(), 10.0);
513 let rendered: Value = serde_json::to_value(&row).unwrap();
514 assert_eq!(rendered["request_id"], json!("r2"));
515 assert_eq!(rendered["wait_for"], json!(["r1"]));
516 assert_eq!(rendered["branches"], json!(["r3"]));
517 assert_eq!(rendered["tool_wait_ms"], json!(7.0));
518 assert!(rendered.get("tool_events").is_none());
519 }
520
521 #[test]
522 fn agentic_row_round_trips_tool_events() {
523 let row = AgenticMooncakeRow {
524 request_id: "r1".to_string(),
525 session_id: Some("trajectory-a".to_string()),
526 input_length: Some(4),
527 output_length: Some(1),
528 hash_ids: Some(vec![0, 1]),
529 timestamp: Some(0.0),
530 delay: Some(0.0),
531 wait_for: Vec::new(),
532 branches: Vec::new(),
533 prefix_reset: Some(true),
534 tool_wait_ms: Some(8.0),
535 tool_events: vec![AgenticToolEvent {
536 tool_call_id: "call-1".to_string(),
537 tool_class: "web_search".to_string(),
538 started_at_unix_ms: 1_000,
539 ended_at_unix_ms: 1_008,
540 duration_ms: 8.0,
541 status: "succeeded".to_string(),
542 output_bytes: Some(512),
543 output_tokens: None,
544 error_type: None,
545 }],
546 };
547
548 let rendered = serde_json::to_string(&row).unwrap();
549 let decoded: AgenticMooncakeRow = serde_json::from_str(&rendered).unwrap();
550 assert_eq!(decoded.tool_events.len(), 1);
551 assert_eq!(decoded.tool_events[0].tool_class, "web_search");
552 assert_eq!(decoded.tool_events[0].output_bytes, Some(512));
553 }
554
555 #[test]
556 fn writer_writes_rows_and_sidecar_jsonl() {
557 let temp = TempDir::new().unwrap();
558 let output = temp.path().join("trace.jsonl");
559 let sidecar = temp.path().join("trace.sidecar.jsonl");
560
561 let mut writer = MooncakeJsonlWriter::create(&output, Some(&sidecar)).unwrap();
562 writer
563 .write_row(&MooncakeRow {
564 session_id: Some("s".to_string()),
565 input_length: Some(2),
566 output_length: Some(1),
567 hash_ids: Some(vec![0]),
568 timestamp: Some(0.0),
569 delay: None,
570 })
571 .unwrap();
572 writer.write_sidecar(&json!({"k": "v"})).unwrap();
573 let stats = writer.finish().unwrap();
574
575 assert_eq!(stats.row_count, 1);
576 assert_eq!(stats.sidecar_count, 1);
577
578 let row_lines: Vec<Value> = std::fs::read_to_string(&output)
579 .unwrap()
580 .lines()
581 .map(|line| serde_json::from_str(line).unwrap())
582 .collect();
583 let sidecar_lines: Vec<Value> = std::fs::read_to_string(&sidecar)
584 .unwrap()
585 .lines()
586 .map(|line| serde_json::from_str(line).unwrap())
587 .collect();
588 assert_eq!(row_lines.len(), 1);
589 assert_eq!(sidecar_lines, vec![json!({"k": "v"})]);
590 assert_eq!(row_lines[0]["session_id"], json!("s"));
591 assert!(row_lines[0].get("delay").is_none());
592 }
593
594 #[test]
595 fn writer_writes_agentic_rows() {
596 let temp = TempDir::new().unwrap();
597 let output = temp.path().join("agentic.jsonl");
598 let mut writer = MooncakeJsonlWriter::create(&output, None).unwrap();
599 writer
600 .write_agentic_row(&AgenticMooncakeRow {
601 request_id: "r1".to_string(),
602 session_id: None,
603 input_length: Some(2),
604 output_length: Some(1),
605 hash_ids: Some(vec![0]),
606 timestamp: Some(0.0),
607 delay: None,
608 wait_for: Vec::new(),
609 branches: Vec::new(),
610 prefix_reset: Some(true),
611 tool_wait_ms: None,
612 tool_events: Vec::new(),
613 })
614 .unwrap();
615 let stats = writer.finish().unwrap();
616
617 assert_eq!(stats.row_count, 1);
618 let row_lines: Vec<Value> = std::fs::read_to_string(&output)
619 .unwrap()
620 .lines()
621 .map(|line| serde_json::from_str(line).unwrap())
622 .collect();
623 assert_eq!(row_lines[0]["request_id"], json!("r1"));
624 assert_eq!(row_lines[0]["prefix_reset"], json!(true));
625 }
626
627 #[test]
628 fn writer_without_sidecar_rejects_sidecar_writes() {
629 let temp = TempDir::new().unwrap();
630 let output = temp.path().join("trace.jsonl");
631 let mut writer = MooncakeJsonlWriter::create(&output, None).unwrap();
632 assert!(!writer.has_sidecar());
633 let err = writer.write_sidecar(&json!({})).unwrap_err();
634 assert!(err.to_string().contains("sidecar was not configured"));
635 }
636}