1use std::collections::HashMap;
5use std::fs::File;
6use std::io::{BufRead, BufReader};
7use std::path::{Path, PathBuf};
8
9use anyhow::{Context, Result, anyhow, bail, ensure};
10use rand::rngs::StdRng;
11use rand::{Rng, SeedableRng};
12use rustc_hash::FxHashMap;
13use serde::{Deserialize, Serialize};
14use uuid::Uuid;
15
16use super::driver::WorkloadDriver;
17use super::types::{
18 AGENTIC_MOONCAKE_SCHEMA, AGENTIC_MOONCAKE_VERSION, AgenticDependency,
19 AgenticDependencyRelation, AgenticDependencyTrigger, AgenticHashIdScope, AgenticMooncakeHeader,
20 AgenticMooncakeRow, AgenticNode, AgenticPlay, AgenticSourceProvenance, AgenticTrace, DelaySpec,
21 LengthSpec, MooncakeRow, ReplayRequestHashes, SessionPartitionSpec, SessionTrace,
22 SyntheticTraceSpec, Trace, TraceFileFormat, TurnTrace, effective_replay_key,
23};
24use super::{SYNTHETIC_OUTPUT_SEED, planned_output_token_ids};
25use crate::replay::protocol::DirectRequest;
26
27#[derive(Debug, Deserialize)]
28struct RawAppliedComputeAgenticRecord {
29 num_turns: usize,
30 input_prompt_length: usize,
31 assistant_response_length: Vec<usize>,
32 tool_call_output_length: Vec<usize>,
33 tool_call_latency: Vec<f64>,
34 final_assistant_response_length: usize,
35}
36
37#[derive(Debug, Deserialize)]
38struct LegacyAgenticMooncakeRow {
39 request_id: String,
40 #[serde(default)]
41 session_id: Option<String>,
42 #[serde(default, alias = "input_tokens")]
43 input_length: Option<usize>,
44 #[serde(alias = "output_tokens")]
45 output_length: usize,
46 #[serde(default)]
47 output_token_ids: Option<Vec<u32>>,
48 hash_ids: Vec<u64>,
49 #[serde(default, alias = "created_time")]
50 timestamp: Option<f64>,
51 #[serde(default)]
52 delay: Option<f64>,
53 #[serde(default)]
54 delay_ms: Option<f64>,
55 #[serde(default)]
56 tool_wait_ms: f64,
57 #[serde(default)]
58 wait_for: Vec<String>,
59 #[serde(default)]
60 priority: Option<i32>,
61 #[serde(default)]
62 strict_priority: Option<u32>,
63 #[serde(default)]
64 policy_class: Option<String>,
65}
66
67pub fn load_agentic_mooncake(path: &Path, legacy_trace_block_size: usize) -> Result<AgenticTrace> {
72 let file = File::open(path)
73 .with_context(|| format!("failed to open trace file {}", path.display()))?;
74 let mut lines = BufReader::new(file).lines();
75 let first = loop {
76 let line = lines
77 .next()
78 .transpose()
79 .with_context(|| format!("failed to read trace file {}", path.display()))?
80 .context("agentic trace file is empty")?;
81 if !line.trim().is_empty() {
82 break line;
83 }
84 };
85 let first_json: serde_json::Value =
86 serde_json::from_str(&first).context("failed to parse first agentic trace row")?;
87 if first_json.get("schema").and_then(serde_json::Value::as_str) == Some(AGENTIC_MOONCAKE_SCHEMA)
88 {
89 return AgenticTrace::from_agentic_mooncake(path);
90 }
91
92 let mut raw_rows = vec![
93 serde_json::from_value::<LegacyAgenticMooncakeRow>(first_json)
94 .context("failed to parse first legacy agentic Mooncake row")?,
95 ];
96 for (line_index, line) in lines.enumerate() {
97 let line = line.with_context(|| {
98 format!(
99 "failed to read legacy agentic trace line {}",
100 line_index + 2
101 )
102 })?;
103 if line.trim().is_empty() {
104 continue;
105 }
106 raw_rows.push(serde_json::from_str(&line).with_context(|| {
107 format!(
108 "failed to parse legacy agentic trace line {}",
109 line_index + 2
110 )
111 })?);
112 }
113 let mut rows = raw_rows
114 .into_iter()
115 .map(|raw| -> Result<AgenticMooncakeRow> {
116 if raw.request_id.trim().is_empty() {
117 bail!("request_id must be nonempty");
118 }
119 if raw.hash_ids.is_empty() {
120 bail!("hash_ids must be nonempty");
121 }
122 if !raw.tool_wait_ms.is_finite() || raw.tool_wait_ms < 0.0 {
123 bail!("tool_wait_ms must be finite and nonnegative");
124 }
125 if raw.delay.is_some() && raw.delay_ms.is_some() {
126 bail!("delay and delay_ms cannot both be set");
127 }
128 let delay = raw.delay.or(raw.delay_ms).unwrap_or(0.0) + raw.tool_wait_ms;
129 if !delay.is_finite() || delay < 0.0 {
130 bail!("dependency delay must be finite and nonnegative");
131 }
132 let relation = if raw.wait_for.len() > 1 {
133 AgenticDependencyRelation::Join
134 } else {
135 AgenticDependencyRelation::Sequence
136 };
137 let dependencies = raw
138 .wait_for
139 .into_iter()
140 .map(|request_id| AgenticDependency {
141 request_id,
142 trigger: AgenticDependencyTrigger::Completion,
143 delay_ms: delay,
144 relation,
145 })
146 .collect::<Vec<_>>();
147 Ok(AgenticMooncakeRow {
148 request_id: raw.request_id,
149 play_id: "agentic-play".to_string(),
150 session_id: raw
151 .session_id
152 .unwrap_or_else(|| "agentic-session".to_string()),
153 model: "unknown".to_string(),
154 input_length: raw.input_length,
155 output_length: Some(raw.output_length),
156 output_token_ids: raw.output_token_ids,
157 hash_ids: Some(raw.hash_ids),
158 not_before_ms: if dependencies.is_empty() {
159 raw.timestamp.unwrap_or(0.0)
160 } else {
161 0.0
162 },
163 priority: raw.priority,
164 strict_priority: raw.strict_priority,
165 policy_class: raw.policy_class,
166 dependencies,
167 })
168 })
169 .collect::<Result<Vec<_>>>()?;
170 assign_dependency_component_play_ids(&mut rows, "legacy-play");
171 AgenticTrace::from_agentic_mooncake_rows(
172 AgenticMooncakeHeader {
173 schema: AGENTIC_MOONCAKE_SCHEMA.to_string(),
174 version: AGENTIC_MOONCAKE_VERSION,
175 block_size: legacy_trace_block_size,
176 hash_id_scope: AgenticHashIdScope::Local,
177 source: AgenticSourceProvenance {
178 format: "legacy_agentic_mooncake".to_string(),
179 digest: format!("{}:{}", path.display(), rows.len()),
180 },
181 },
182 rows,
183 )
184}
185
186pub(super) fn assign_dependency_component_play_ids(rows: &mut [AgenticMooncakeRow], prefix: &str) {
187 fn find(parent: &mut [usize], value: usize) -> usize {
188 if parent[value] != value {
189 parent[value] = find(parent, parent[value]);
190 }
191 parent[value]
192 }
193
194 let by_id = rows
195 .iter()
196 .enumerate()
197 .map(|(index, row)| (row.request_id.clone(), index))
198 .collect::<HashMap<_, _>>();
199 let mut parent = (0..rows.len()).collect::<Vec<_>>();
200 for (index, row) in rows.iter().enumerate() {
201 for dependency in &row.dependencies {
202 let Some(&source) = by_id.get(&dependency.request_id) else {
203 continue;
204 };
205 let left = find(&mut parent, index);
206 let right = find(&mut parent, source);
207 if left != right {
208 parent[left] = right;
209 }
210 }
211 }
212 let mut roots_by_component: HashMap<usize, Vec<usize>> = HashMap::new();
213 for (index, row) in rows.iter().enumerate() {
214 if row.dependencies.is_empty() {
215 let component = find(&mut parent, index);
216 roots_by_component.entry(component).or_default().push(index);
217 }
218 }
219 let mut labels = HashMap::new();
220 for (component, roots) in roots_by_component {
221 let canonical = roots
222 .iter()
223 .copied()
224 .min_by(|left, right| {
225 rows[*left]
226 .not_before_ms
227 .total_cmp(&rows[*right].not_before_ms)
228 .then_with(|| rows[*left].request_id.cmp(&rows[*right].request_id))
229 })
230 .expect("a root component is nonempty");
231 labels.insert(component, rows[canonical].request_id.clone());
232 }
233 for (index, row) in rows.iter_mut().enumerate() {
234 let component = find(&mut parent, index);
235 let label = labels
236 .entry(component)
237 .or_insert_with(|| row.request_id.clone());
238 row.play_id = format!("{prefix}:{label}");
239 }
240}
241
242#[derive(Debug, Default)]
243struct HashIdInterner {
244 canonical_ids: FxHashMap<u64, u32>,
245}
246
247impl HashIdInterner {
248 fn intern_all(&mut self, hash_ids: Vec<u64>) -> Result<Vec<u32>> {
249 hash_ids
250 .into_iter()
251 .map(|hash_id| self.intern(hash_id))
252 .collect()
253 }
254
255 fn intern(&mut self, hash_id: u64) -> Result<u32> {
256 let next_id = self.canonical_ids.len();
257 match self.canonical_ids.entry(hash_id) {
258 std::collections::hash_map::Entry::Occupied(entry) => Ok(*entry.get()),
259 std::collections::hash_map::Entry::Vacant(entry) => {
260 let canonical_id = u32::try_from(next_id)
261 .context("trace contains more unique hash IDs than u32 can represent")?;
262 entry.insert(canonical_id);
263 Ok(canonical_id)
264 }
265 }
266 }
267}
268
269pub fn validate_trace_files(format: TraceFileFormat, paths: &[PathBuf]) -> Result<()> {
270 if paths.is_empty() {
271 bail!("trace replay requires at least one trace file");
272 }
273 if format != TraceFileFormat::Dynamo && paths.len() != 1 {
274 bail!(
275 "trace_format='{}' requires exactly one trace file, got {}",
276 format.as_str(),
277 paths.len()
278 );
279 }
280 Ok(())
281}
282
283fn single_turn_request_uuid(_request_ordinal: usize) -> Uuid {
284 Uuid::new_v4()
285}
286
287pub(super) fn validate_synthesizable_prompt(
288 input_length: usize,
289 hash_ids: &[u32],
290 trace_block_size: usize,
291) -> Result<()> {
292 if trace_block_size == 0 {
293 bail!("trace_block_size must be greater than 0");
294 }
295 let synthesizable_capacity = hash_ids
296 .len()
297 .checked_mul(trace_block_size)
298 .context("synthesized prompt capacity overflow")?;
299 let required_hash_ids = input_length.div_ceil(trace_block_size);
300 if hash_ids.len() < required_hash_ids {
301 bail!(
302 "input_length {} exceeds synthesized capacity {}",
303 input_length,
304 synthesizable_capacity
305 );
306 }
307
308 Ok(())
309}
310
311pub(super) fn synthesize_trace_tokens(
312 input_length: usize,
313 hash_ids: &[u32],
314 trace_block_size: usize,
315) -> Result<Vec<u32>> {
316 validate_synthesizable_prompt(input_length, hash_ids, trace_block_size)?;
317
318 Ok(synthesize_validated_trace_tokens(
319 input_length,
320 hash_ids,
321 trace_block_size,
322 ))
323}
324
325pub(super) fn synthesize_validated_trace_tokens(
326 input_length: usize,
327 hash_ids: &[u32],
328 trace_block_size: usize,
329) -> Vec<u32> {
330 let mut tokens = Vec::with_capacity(input_length);
331 for &hash_id in hash_ids {
332 let remaining = input_length - tokens.len();
333 tokens.extend(std::iter::repeat_n(
334 hash_id,
335 remaining.min(trace_block_size),
336 ));
337 if tokens.len() == input_length {
338 break;
339 }
340 }
341
342 debug_assert_eq!(tokens.len(), input_length);
343 tokens
344}
345
346fn trace_to_replay_hashes(
347 input_length: usize,
348 hash_ids: &[u32],
349 trace_block_size: usize,
350 engine_block_size: usize,
351) -> Result<ReplayRequestHashes> {
352 if engine_block_size == 0 {
353 bail!("engine_block_size must be greater than 0");
354 }
355
356 let tokens = synthesize_trace_tokens(input_length, hash_ids, trace_block_size)?;
357 let engine_block_size =
358 u32::try_from(engine_block_size).context("engine_block_size does not fit in u32")?;
359 Ok(ReplayRequestHashes::from_tokens(&tokens, engine_block_size))
360}
361
362impl TurnTrace {
363 pub fn synthesize_tokens(&self, trace_block_size: usize) -> Result<Vec<u32>> {
364 synthesize_trace_tokens(self.input_length, &self.hash_ids, trace_block_size)
365 }
366
367 pub fn to_direct_request(
368 &self,
369 trace_block_size: usize,
370 request_uuid: Uuid,
371 arrival_timestamp_ms: Option<f64>,
372 ) -> Result<DirectRequest> {
373 let tokens = self.synthesize_tokens(trace_block_size)?;
374 Ok(DirectRequest {
375 tokens,
376 max_output_tokens: self.max_output_tokens,
377 output_token_ids: self.output_token_ids.clone(),
378 uuid: Some(request_uuid),
379 dp_rank: 0,
380 preferred_dp_rank: None,
381 arrival_timestamp_ms,
382 priority: self.priority,
383 strict_priority: self.strict_priority,
384 policy_class: self.policy_class.clone(),
385 replay_context: None,
386 })
387 }
388
389 pub fn to_replay_hashes(
390 &self,
391 trace_block_size: usize,
392 engine_block_size: usize,
393 ) -> Result<ReplayRequestHashes> {
394 trace_to_replay_hashes(
395 self.input_length,
396 &self.hash_ids,
397 trace_block_size,
398 engine_block_size,
399 )
400 }
401}
402
403struct MooncakeTraceBuilder {
404 trace_block_size: usize,
405 hash_id_interner: HashIdInterner,
406 sessions: Vec<SessionTrace>,
407 session_indices: HashMap<String, usize>,
408 last_timestamps: Vec<Option<f64>>,
409}
410
411impl MooncakeTraceBuilder {
412 fn new(trace_block_size: usize) -> Self {
413 Self {
414 trace_block_size,
415 hash_id_interner: HashIdInterner::default(),
416 sessions: Vec::new(),
417 session_indices: HashMap::new(),
418 last_timestamps: Vec::new(),
419 }
420 }
421
422 fn is_empty(&self) -> bool {
423 self.sessions.is_empty()
424 }
425
426 fn push(&mut self, line_idx: usize, raw: MooncakeRow) -> Result<()> {
427 let request_id = raw.request_id;
428 let raw_session_id = raw.session_id;
429 let session_id = raw_session_id
430 .clone()
431 .unwrap_or_else(|| format!("request_{}", line_idx + 1));
432 let hash_ids = raw
433 .hash_ids
434 .ok_or_else(|| anyhow!("trace line {} is missing hash_ids", line_idx + 1))?;
435 let synthesizable_capacity = hash_ids
436 .len()
437 .checked_mul(self.trace_block_size)
438 .ok_or_else(|| anyhow!("trace line {} synthesized capacity overflow", line_idx + 1))?;
439 let input_length = raw.input_length.unwrap_or(synthesizable_capacity);
440 ensure!(
441 input_length <= synthesizable_capacity,
442 "trace line {} input_length {} exceeds hash_ids capacity {}",
443 line_idx + 1,
444 input_length,
445 synthesizable_capacity
446 );
447 let output_length = raw
448 .output_length
449 .ok_or_else(|| anyhow!("trace line {} is missing output_length", line_idx + 1))?;
450 let output_token_ids = raw.output_token_ids;
451 if let Some(output_token_ids) = output_token_ids.as_ref()
452 && output_token_ids.len() != output_length
453 {
454 bail!(
455 "trace line {} output_length {} does not match output_token_ids length {}",
456 line_idx + 1,
457 output_length,
458 output_token_ids.len()
459 );
460 }
461 let timestamp_ms = raw.timestamp;
462 let explicit_delay_ms = raw.delay;
463 let priority = raw.priority.unwrap_or(0);
464 let strict_priority = raw.strict_priority.unwrap_or(0);
465 let policy_class = raw.policy_class.clone();
466
467 let session_index = *self
468 .session_indices
469 .entry(session_id.clone())
470 .or_insert_with(|| {
471 let idx = self.sessions.len();
472 self.sessions.push(SessionTrace {
473 session_id: session_id.clone(),
474 first_arrival_timestamp_ms: timestamp_ms,
475 turns: Vec::new(),
476 });
477 self.last_timestamps.push(timestamp_ms);
478 idx
479 });
480
481 let session = self
482 .sessions
483 .get_mut(session_index)
484 .expect("newly inserted session must exist");
485 let turn_idx = session.turns.len();
486 let replay_key = output_token_ids.as_ref().map(|_| {
487 effective_replay_key(
488 request_id.as_deref(),
489 raw_session_id.as_deref(),
490 turn_idx,
491 line_idx,
492 )
493 });
494 let delay_after_previous_ms = if turn_idx == 0 {
495 let delay = explicit_delay_ms.unwrap_or(0.0);
496 if delay != 0.0 {
497 bail!(
498 "trace line {} sets delay on the first turn of session {}",
499 line_idx + 1,
500 session.session_id
501 );
502 }
503 0.0
504 } else if let Some(delay_ms) = explicit_delay_ms {
505 delay_ms
506 } else if let Some(timestamp_ms) = timestamp_ms {
507 let previous_timestamp_ms = self.last_timestamps[session_index].ok_or_else(|| {
508 anyhow!(
509 "trace line {} for session {} cannot infer delay without a previous timestamp",
510 line_idx + 1,
511 session.session_id
512 )
513 })?;
514 timestamp_ms - previous_timestamp_ms
515 } else {
516 0.0
517 };
518
519 if !delay_after_previous_ms.is_finite() || delay_after_previous_ms < 0.0 {
520 bail!(
521 "trace line {} has invalid delay {}",
522 line_idx + 1,
523 delay_after_previous_ms
524 );
525 }
526
527 if hash_ids.len() * self.trace_block_size < input_length {
528 bail!(
529 "trace line {} input_length {} exceeds synthesized capacity {}",
530 line_idx + 1,
531 input_length,
532 hash_ids.len() * self.trace_block_size
533 );
534 }
535
536 let hash_ids = self.hash_id_interner.intern_all(hash_ids)?;
537
538 session.turns.push(TurnTrace {
539 input_length,
540 max_output_tokens: output_length,
541 output_token_ids,
542 replay_key,
543 hash_ids,
544 delay_after_previous_ms,
545 priority,
546 strict_priority,
547 policy_class,
548 });
549 if let Some(timestamp_ms) = timestamp_ms {
550 self.last_timestamps[session_index] = Some(timestamp_ms);
551 }
552 Ok(())
553 }
554
555 fn finish(self) -> Trace {
556 Trace {
557 block_size: self.trace_block_size,
558 sessions: self.sessions,
559 }
560 }
561}
562
563impl Trace {
564 pub fn from_mooncake(path: &Path, trace_block_size: usize) -> Result<Self> {
565 if trace_block_size == 0 {
566 bail!("trace_block_size must be greater than 0");
567 }
568
569 let file = File::open(path)
570 .with_context(|| format!("failed to open trace file {}", path.display()))?;
571 let reader = BufReader::new(file);
572 let mut builder = MooncakeTraceBuilder::new(trace_block_size);
573
574 for (line_idx, line) in reader.lines().enumerate() {
575 let line = line.with_context(|| {
576 format!(
577 "failed to read line {} from {}",
578 line_idx + 1,
579 path.display()
580 )
581 })?;
582 if line.trim().is_empty() {
583 continue;
584 }
585
586 let row = serde_json::from_str(&line).with_context(|| {
587 format!(
588 "failed to parse line {} from {} as JSON",
589 line_idx + 1,
590 path.display()
591 )
592 })?;
593 builder.push(line_idx, row)?;
594 }
595
596 if builder.is_empty() {
597 bail!("trace file {} did not contain any requests", path.display());
598 }
599
600 Ok(builder.finish())
601 }
602
603 pub fn from_mooncake_rows(rows: Vec<MooncakeRow>, trace_block_size: usize) -> Result<Self> {
604 if trace_block_size == 0 {
605 bail!("trace_block_size must be greater than 0");
606 }
607 let mut builder = MooncakeTraceBuilder::new(trace_block_size);
608 for (line_idx, row) in rows.into_iter().enumerate() {
609 builder.push(line_idx, row)?;
610 }
611 if builder.is_empty() {
612 bail!("Mooncake rows did not contain any requests");
613 }
614 Ok(builder.finish())
615 }
616
617 pub fn from_applied_compute_agentic(
618 path: &Path,
619 trace_block_size: usize,
620 shared_prefix_ratio: f64,
621 num_prefix_groups: usize,
622 ) -> Result<Self> {
623 if trace_block_size == 0 {
624 bail!("trace_block_size must be greater than 0");
625 }
626 if !(0.0..=1.0).contains(&shared_prefix_ratio) {
627 bail!(
628 "shared_prefix_ratio must be between 0.0 and 1.0, got {}",
629 shared_prefix_ratio
630 );
631 }
632
633 let file = File::open(path)
634 .with_context(|| format!("failed to open trace file {}", path.display()))?;
635 let reader = BufReader::new(file);
636 let mut sessions = Vec::new();
637 let mut hash_id_interner = HashIdInterner::default();
638 let mut next_unique_hash = 1_u64;
639
640 for (line_idx, line) in reader.lines().enumerate() {
641 let line = line.with_context(|| {
642 format!(
643 "failed to read line {} from {}",
644 line_idx + 1,
645 path.display()
646 )
647 })?;
648 if line.trim().is_empty() {
649 continue;
650 }
651
652 let raw: RawAppliedComputeAgenticRecord =
653 serde_json::from_str(&line).with_context(|| {
654 format!(
655 "failed to parse line {} from {} as JSON",
656 line_idx + 1,
657 path.display()
658 )
659 })?;
660
661 for (name, values) in [
662 (
663 "assistant_response_length",
664 raw.assistant_response_length.len(),
665 ),
666 ("tool_call_output_length", raw.tool_call_output_length.len()),
667 ("tool_call_latency", raw.tool_call_latency.len()),
668 ] {
669 if values != raw.num_turns {
670 bail!(
671 "trace line {} field {} length {} does not match num_turns {}",
672 line_idx + 1,
673 name,
674 values,
675 raw.num_turns
676 );
677 }
678 }
679
680 if raw.input_prompt_length == 0 {
681 bail!(
682 "trace line {} input_prompt_length must be positive",
683 line_idx + 1
684 );
685 }
686
687 let group_id = if shared_prefix_ratio > 0.0 && num_prefix_groups > 0 {
688 Some(line_idx % num_prefix_groups)
689 } else {
690 None
691 };
692 let mut current_input_length = raw.input_prompt_length;
693 let mut hash_ids = Vec::new();
694 let shared_initial_blocks = ((current_input_length.div_ceil(trace_block_size) as f64)
695 * shared_prefix_ratio)
696 .round() as usize;
697 extend_applied_compute_agentic_hash_ids(
698 &mut hash_ids,
699 current_input_length,
700 trace_block_size,
701 shared_initial_blocks,
702 group_id,
703 &mut next_unique_hash,
704 )?;
705
706 let mut turns = Vec::with_capacity(raw.num_turns + 1);
707 let mut next_turn_delay_ms = 0.0;
708 for turn_idx in 0..raw.num_turns {
709 let tool_call_latency = raw.tool_call_latency[turn_idx];
710 if !tool_call_latency.is_finite() || tool_call_latency < 0.0 {
711 bail!(
712 "trace line {} tool_call_latency[{}] must be a finite non-negative number",
713 line_idx + 1,
714 turn_idx
715 );
716 }
717
718 turns.push(TurnTrace {
719 input_length: current_input_length,
720 max_output_tokens: raw.assistant_response_length[turn_idx],
721 hash_ids: hash_id_interner.intern_all(hash_ids.clone())?,
722 delay_after_previous_ms: next_turn_delay_ms,
723 ..Default::default()
724 });
725
726 current_input_length = current_input_length
727 .checked_add(raw.assistant_response_length[turn_idx])
728 .and_then(|value| value.checked_add(raw.tool_call_output_length[turn_idx]))
729 .ok_or_else(|| {
730 anyhow!(
731 "trace line {} cumulative input length overflow",
732 line_idx + 1
733 )
734 })?;
735 extend_applied_compute_agentic_hash_ids(
736 &mut hash_ids,
737 current_input_length,
738 trace_block_size,
739 shared_initial_blocks,
740 group_id,
741 &mut next_unique_hash,
742 )?;
743 next_turn_delay_ms = tool_call_latency * 1000.0;
744 }
745
746 turns.push(TurnTrace {
747 input_length: current_input_length,
748 max_output_tokens: raw.final_assistant_response_length,
749 hash_ids: hash_id_interner.intern_all(hash_ids)?,
750 delay_after_previous_ms: next_turn_delay_ms,
751 ..Default::default()
752 });
753
754 sessions.push(SessionTrace {
755 session_id: format!("applied_compute_agentic_session_{}", line_idx + 1),
756 first_arrival_timestamp_ms: None,
757 turns,
758 });
759 }
760
761 if sessions.is_empty() {
762 bail!("trace file {} did not contain any requests", path.display());
763 }
764
765 Ok(Self {
766 block_size: trace_block_size,
767 sessions,
768 })
769 }
770
771 pub fn synthetic(spec: SyntheticTraceSpec) -> Result<Self> {
772 if spec.block_size == 0 {
773 bail!("block_size must be greater than 0");
774 }
775 if spec.num_sessions == 0 {
776 bail!("num_sessions must be greater than 0");
777 }
778 if spec.turns_per_session == 0 {
779 bail!("turns_per_session must be greater than 0");
780 }
781 if !(0.0..=1.0).contains(&spec.shared_prefix_ratio) {
782 bail!(
783 "shared_prefix_ratio must be between 0.0 and 1.0, got {}",
784 spec.shared_prefix_ratio
785 );
786 }
787
788 let mut rng = StdRng::seed_from_u64(spec.seed);
789 let mut sessions = Vec::with_capacity(spec.num_sessions);
790 let first_arrivals = spec
791 .first_turn_arrivals
792 .timestamps(spec.num_sessions, spec.arrival_seed)?;
793
794 let mut next_unique_hash = 1_u64;
795 let mut hash_id_interner = HashIdInterner::default();
796 for (session_idx, first_arrival_timestamp_ms) in first_arrivals.into_iter().enumerate() {
797 let group_id = if spec.num_prefix_groups > 0 && spec.shared_prefix_ratio > 0.0 {
798 Some(rng.random_range(0..spec.num_prefix_groups) as u64)
799 } else {
800 None
801 };
802 let mut turns = Vec::with_capacity(spec.turns_per_session);
803 for turn_idx in 0..spec.turns_per_session {
804 let input_length = sample_length(&spec.input_tokens, 1, &mut rng);
805 let max_output_tokens = sample_length(&spec.output_tokens, 1, &mut rng);
806 let num_blocks = input_length.div_ceil(spec.block_size);
807 let prefix_blocks =
808 ((num_blocks as f64) * spec.shared_prefix_ratio).round() as usize;
809 let prefix_blocks = prefix_blocks.min(num_blocks);
810 let mut hash_ids = Vec::with_capacity(num_blocks);
811
812 for block_idx in 0..prefix_blocks {
813 if let Some(group_id) = group_id {
814 hash_ids.push(0xD00D_0000_0000_0000 | (group_id << 32) | block_idx as u64);
815 }
816 }
817
818 while hash_ids.len() < num_blocks {
819 hash_ids.push(next_unique_hash);
820 next_unique_hash = next_unique_hash
821 .checked_add(1)
822 .expect("synthetic hash id overflow");
823 }
824
825 turns.push(TurnTrace {
826 input_length,
827 max_output_tokens,
828 hash_ids: hash_id_interner.intern_all(hash_ids)?,
829 delay_after_previous_ms: if turn_idx == 0 {
830 0.0
831 } else {
832 sample_delay_ms(&spec.inter_turn_delays, &mut rng)?
833 },
834 ..Default::default()
835 });
836 }
837
838 sessions.push(SessionTrace {
839 session_id: format!("session_{session_idx}"),
840 first_arrival_timestamp_ms: Some(first_arrival_timestamp_ms),
841 turns,
842 });
843 }
844
845 Ok(Self {
846 block_size: spec.block_size,
847 sessions,
848 })
849 }
850
851 pub fn validate_for_trace_mode(&self) -> Result<()> {
852 self.validate(false)
853 }
854
855 pub fn validate_for_concurrency_mode(&self) -> Result<()> {
856 self.validate(true)
857 }
858
859 pub fn normalize_session_starts(mut self) -> Result<Self> {
860 let Some(min_timestamp_ms) = self
861 .sessions
862 .iter()
863 .filter_map(|session| session.first_arrival_timestamp_ms)
864 .min_by(|left, right| left.total_cmp(right))
865 else {
866 return Ok(self);
867 };
868
869 for session in &mut self.sessions {
870 if let Some(timestamp_ms) = session.first_arrival_timestamp_ms.as_mut() {
871 *timestamp_ms -= min_timestamp_ms;
872 }
873 }
874 Ok(self)
875 }
876
877 pub fn speed_up_timing(mut self, ratio: f64) -> Result<Self> {
878 if !ratio.is_finite() || ratio <= 0.0 {
879 bail!("ratio must be a finite positive number, got {ratio}");
880 }
881
882 for session in &mut self.sessions {
883 if let Some(timestamp_ms) = session.first_arrival_timestamp_ms.as_mut() {
884 *timestamp_ms /= ratio;
885 }
886 for turn in &mut session.turns {
887 turn.delay_after_previous_ms /= ratio;
888 }
889 }
890 Ok(self)
891 }
892
893 pub fn rescale_session_start_span(mut self, duration_ms: u64) -> Result<Self> {
894 let Some(min_timestamp_ms) = self
895 .sessions
896 .iter()
897 .filter_map(|session| session.first_arrival_timestamp_ms)
898 .min_by(|left, right| left.total_cmp(right))
899 else {
900 return Ok(self);
901 };
902 let Some(max_timestamp_ms) = self
903 .sessions
904 .iter()
905 .filter_map(|session| session.first_arrival_timestamp_ms)
906 .max_by(|left, right| left.total_cmp(right))
907 else {
908 return Ok(self);
909 };
910
911 let target_span_ms = duration_ms as f64;
912 let source_span_ms = max_timestamp_ms - min_timestamp_ms;
913 for session in &mut self.sessions {
914 if let Some(timestamp_ms) = session.first_arrival_timestamp_ms.as_mut() {
915 *timestamp_ms = if source_span_ms == 0.0 {
916 0.0
917 } else {
918 (*timestamp_ms - min_timestamp_ms) * target_span_ms / source_span_ms
919 };
920 }
921 }
922 Ok(self)
923 }
924
925 pub fn rescale_ready_span(mut self, duration_ms: u64) -> Result<Self> {
926 let Some(min_start_ms) = self
927 .sessions
928 .iter()
929 .map(|session| session.first_arrival_timestamp_ms.unwrap_or(0.0))
930 .min_by(|left, right| left.total_cmp(right))
931 else {
932 return Ok(self);
933 };
934
935 let Some(max_ready_ms) = self
936 .sessions
937 .iter()
938 .map(|session| {
939 session.first_arrival_timestamp_ms.unwrap_or(0.0)
940 + session
941 .turns
942 .iter()
943 .enumerate()
944 .filter(|(turn_idx, _)| *turn_idx > 0)
945 .map(|(_, turn)| turn.delay_after_previous_ms)
946 .sum::<f64>()
947 })
948 .max_by(|left, right| left.total_cmp(right))
949 else {
950 return Ok(self);
951 };
952
953 let ratio = duration_ms as f64 / (max_ready_ms - min_start_ms).max(1.0);
954 for session in &mut self.sessions {
955 if let Some(start_ms) = session.first_arrival_timestamp_ms.as_mut() {
956 *start_ms = (*start_ms - min_start_ms) * ratio;
957 }
958 for (turn_idx, turn) in session.turns.iter_mut().enumerate() {
959 if turn_idx > 0 {
960 turn.delay_after_previous_ms *= ratio;
961 }
962 }
963 }
964 Ok(self)
965 }
966
967 pub fn expand_hash_prefix_depth(mut self, factor: usize) -> Self {
968 if factor <= 1 {
969 return self;
970 }
971 let factor = u32::try_from(factor).expect("hash prefix expansion factor exceeds u32");
972 for session in &mut self.sessions {
973 for turn in &mut session.turns {
974 turn.input_length = turn
975 .input_length
976 .checked_mul(factor as usize)
977 .expect("input_length expansion overflow");
978 turn.hash_ids = turn
979 .hash_ids
980 .iter()
981 .flat_map(|&hash_id| {
982 let base = hash_id
983 .checked_mul(factor)
984 .expect("hash prefix expansion overflow");
985 (0..factor).map(move |offset| {
986 base.checked_add(offset)
987 .expect("hash prefix expansion overflow")
988 })
989 })
990 .collect();
991 }
992 }
993 self
994 }
995
996 pub fn duplicate_hash_space(mut self, copies: usize) -> Self {
997 if copies <= 1 {
998 return self;
999 }
1000
1001 let max_hash_id = self
1002 .sessions
1003 .iter()
1004 .flat_map(|session| session.turns.iter())
1005 .flat_map(|turn| turn.hash_ids.iter().copied())
1006 .max()
1007 .unwrap_or(0);
1008 let offset_base = max_hash_id
1009 .checked_add(1)
1010 .expect("hash duplication offset overflow");
1011 let original_sessions = self.sessions.clone();
1012 self.sessions.clear();
1013
1014 for copy_idx in 0..copies {
1015 let copy_idx = u32::try_from(copy_idx).expect("hash copy index exceeds u32");
1016 let offset = offset_base
1017 .checked_mul(copy_idx)
1018 .expect("hash duplication offset overflow");
1019 for session in &original_sessions {
1020 let mut duplicated = session.clone();
1021 duplicated.session_id = format!("{}:copy_{copy_idx}", session.session_id);
1022 for turn in &mut duplicated.turns {
1023 turn.hash_ids = turn
1024 .hash_ids
1025 .iter()
1026 .map(|&hash_id| {
1027 hash_id
1028 .checked_add(offset)
1029 .expect("hash duplication overflow")
1030 })
1031 .collect();
1032 }
1033 self.sessions.push(duplicated);
1034 }
1035 }
1036 self
1037 }
1038
1039 pub fn partition_by_session(&self, spec: SessionPartitionSpec) -> Vec<Self> {
1040 let num_partitions = match spec {
1041 SessionPartitionSpec::Random { num_partitions, .. } => num_partitions,
1042 SessionPartitionSpec::RoundRobin { num_partitions } => num_partitions,
1043 }
1044 .max(1);
1045 let mut partitions = vec![
1046 Self {
1047 block_size: self.block_size,
1048 sessions: Vec::new(),
1049 };
1050 num_partitions
1051 ];
1052
1053 let mut rng = match spec {
1054 SessionPartitionSpec::Random { seed, .. } => Some(StdRng::seed_from_u64(seed)),
1055 SessionPartitionSpec::RoundRobin { .. } => None,
1056 };
1057
1058 for (session_idx, session) in self.sessions.iter().cloned().enumerate() {
1059 let partition_idx = match spec {
1060 SessionPartitionSpec::Random { .. } => rng
1061 .as_mut()
1062 .expect("random partitioner must exist")
1063 .random_range(0..num_partitions),
1064 SessionPartitionSpec::RoundRobin { .. } => session_idx % num_partitions,
1065 };
1066 partitions[partition_idx].sessions.push(session);
1067 }
1068
1069 partitions
1070 }
1071
1072 pub fn to_single_turn_requests(&self) -> Result<Vec<DirectRequest>> {
1073 let mut requests = Vec::with_capacity(self.sessions.len());
1074 let mut output_rng = StdRng::seed_from_u64(SYNTHETIC_OUTPUT_SEED);
1075 for (request_ordinal, session) in self.sessions.iter().enumerate() {
1076 if session.turns.len() != 1 {
1077 bail!(
1078 "to_single_turn_requests requires exactly one turn per session, but session {} has {} turns",
1079 session.session_id,
1080 session.turns.len()
1081 );
1082 }
1083 let request_uuid = single_turn_request_uuid(request_ordinal);
1084 let mut request = session.turns[0].to_direct_request(
1085 self.block_size,
1086 request_uuid,
1087 session.first_arrival_timestamp_ms,
1088 )?;
1089 request.output_token_ids = Some(planned_output_token_ids(
1090 request.output_token_ids,
1091 request.max_output_tokens,
1092 &mut output_rng,
1093 ));
1094 requests.push(request);
1095 }
1096 Ok(requests)
1097 }
1098
1099 pub fn is_single_turn(&self) -> bool {
1100 self.sessions.iter().all(|session| session.turns.len() == 1)
1101 }
1102
1103 pub fn into_trace_driver(self) -> Result<WorkloadDriver> {
1104 self.validate_for_trace_mode()?;
1105 let engine_block_size = self.block_size;
1106 WorkloadDriver::new_trace(self, engine_block_size)
1107 }
1108
1109 pub fn into_concurrency_driver(self, max_in_flight: usize) -> Result<WorkloadDriver> {
1110 self.validate_for_concurrency_mode()?;
1111 let engine_block_size = self.block_size;
1112 WorkloadDriver::new_concurrency(self, engine_block_size, max_in_flight)
1113 }
1114
1115 pub fn into_trace_driver_with_block_size(
1116 self,
1117 engine_block_size: usize,
1118 ) -> Result<WorkloadDriver> {
1119 self.validate_for_trace_mode()?;
1120 WorkloadDriver::new_trace(self, engine_block_size)
1121 }
1122
1123 pub fn into_delta_accumulating_trace_driver_with_block_size(
1124 self,
1125 engine_block_size: usize,
1126 ) -> Result<WorkloadDriver> {
1127 self.validate_for_trace_mode()?;
1128 WorkloadDriver::new_trace_accumulating_deltas(self, engine_block_size)
1129 }
1130
1131 pub fn into_concurrency_driver_with_block_size(
1132 self,
1133 engine_block_size: usize,
1134 max_in_flight: usize,
1135 ) -> Result<WorkloadDriver> {
1136 self.validate_for_concurrency_mode()?;
1137 WorkloadDriver::new_concurrency(self, engine_block_size, max_in_flight)
1138 }
1139
1140 pub fn into_delta_accumulating_concurrency_driver_with_block_size(
1141 self,
1142 engine_block_size: usize,
1143 max_in_flight: usize,
1144 ) -> Result<WorkloadDriver> {
1145 self.validate_for_concurrency_mode()?;
1146 WorkloadDriver::new_concurrency_accumulating_deltas(self, engine_block_size, max_in_flight)
1147 }
1148
1149 fn validate(&self, allow_missing_first_timestamp: bool) -> Result<()> {
1150 if self.block_size == 0 {
1151 bail!("block_size must be greater than 0");
1152 }
1153 if self.sessions.is_empty() {
1154 bail!("trace must contain at least one session");
1155 }
1156
1157 for session in &self.sessions {
1158 if session.turns.is_empty() {
1159 bail!(
1160 "session {} must contain at least one turn",
1161 session.session_id
1162 );
1163 }
1164 if !allow_missing_first_timestamp {
1165 let timestamp_ms = session.first_arrival_timestamp_ms.ok_or_else(|| {
1166 anyhow!(
1167 "trace mode requires first_arrival_timestamp_ms for session {}",
1168 session.session_id
1169 )
1170 })?;
1171 if !timestamp_ms.is_finite() || timestamp_ms < 0.0 {
1172 bail!(
1173 "session {} has invalid first_arrival_timestamp_ms {}",
1174 session.session_id,
1175 timestamp_ms
1176 );
1177 }
1178 } else if let Some(timestamp_ms) = session.first_arrival_timestamp_ms
1179 && (!timestamp_ms.is_finite() || timestamp_ms < 0.0)
1180 {
1181 bail!(
1182 "session {} has invalid first_arrival_timestamp_ms {}",
1183 session.session_id,
1184 timestamp_ms
1185 );
1186 }
1187
1188 for (turn_idx, turn) in session.turns.iter().enumerate() {
1189 if let Some(output_token_ids) = turn.output_token_ids.as_ref()
1190 && output_token_ids.len() != turn.max_output_tokens
1191 {
1192 bail!(
1193 "session {} turn {} max_output_tokens {} does not match output_token_ids length {}",
1194 session.session_id,
1195 turn_idx,
1196 turn.max_output_tokens,
1197 output_token_ids.len()
1198 );
1199 }
1200 if turn.input_length == 0 {
1201 bail!(
1202 "session {} turn {} must have a positive input_length",
1203 session.session_id,
1204 turn_idx
1205 );
1206 }
1207 if turn.hash_ids.is_empty() {
1208 bail!(
1209 "session {} turn {} must contain at least one hash id",
1210 session.session_id,
1211 turn_idx
1212 );
1213 }
1214 validate_synthesizable_prompt(turn.input_length, &turn.hash_ids, self.block_size)
1215 .with_context(|| {
1216 format!(
1217 "session {} turn {} has invalid prompt",
1218 session.session_id, turn_idx
1219 )
1220 })?;
1221 if !turn.delay_after_previous_ms.is_finite() || turn.delay_after_previous_ms < 0.0 {
1222 bail!(
1223 "session {} turn {} has invalid delay {}",
1224 session.session_id,
1225 turn_idx,
1226 turn.delay_after_previous_ms
1227 );
1228 }
1229 if turn_idx == 0 && turn.delay_after_previous_ms != 0.0 {
1230 bail!(
1231 "session {} first turn must have delay_after_previous_ms == 0.0",
1232 session.session_id
1233 );
1234 }
1235 }
1236 }
1237
1238 Ok(())
1239 }
1240}
1241
1242struct AgenticTraceBuilder {
1243 header: AgenticMooncakeHeader,
1244 nodes: Vec<AgenticNode>,
1245 request_ids: std::collections::HashSet<String>,
1246}
1247
1248pub struct AgenticGraphBuilder {
1253 inner: AgenticTraceBuilder,
1254 row_count: usize,
1255}
1256
1257impl AgenticGraphBuilder {
1258 pub fn new(header: AgenticMooncakeHeader) -> Result<Self> {
1259 Ok(Self {
1260 inner: AgenticTraceBuilder::new(header)?,
1261 row_count: 0,
1262 })
1263 }
1264
1265 pub fn push(&mut self, row: AgenticMooncakeRow) -> Result<()> {
1266 self.inner.push(self.row_count + 1, row)?;
1267 self.row_count += 1;
1268 Ok(())
1269 }
1270
1271 pub fn finish(self) -> Result<AgenticTrace> {
1272 if self.inner.is_empty() {
1273 bail!("agentic Mooncake rows did not contain any requests");
1274 }
1275 self.inner.finish()
1276 }
1277}
1278
1279impl AgenticTraceBuilder {
1280 fn new(header: AgenticMooncakeHeader) -> Result<Self> {
1281 if header.schema != AGENTIC_MOONCAKE_SCHEMA {
1282 bail!(
1283 "unsupported agentic Mooncake schema {:?}; expected {:?}",
1284 header.schema,
1285 AGENTIC_MOONCAKE_SCHEMA
1286 );
1287 }
1288 if header.version != AGENTIC_MOONCAKE_VERSION {
1289 bail!(
1290 "unsupported agentic Mooncake version {}; expected {}",
1291 header.version,
1292 AGENTIC_MOONCAKE_VERSION
1293 );
1294 }
1295 if header.block_size == 0 {
1296 bail!("agentic Mooncake block_size must be greater than 0");
1297 }
1298 if header.source.format.trim().is_empty() || header.source.digest.trim().is_empty() {
1299 bail!("agentic Mooncake source provenance requires nonempty format and digest");
1300 }
1301
1302 Ok(Self {
1303 header,
1304 nodes: Vec::new(),
1305 request_ids: std::collections::HashSet::new(),
1306 })
1307 }
1308
1309 fn is_empty(&self) -> bool {
1310 self.nodes.is_empty()
1311 }
1312
1313 fn push(&mut self, line_idx: usize, raw: AgenticMooncakeRow) -> Result<()> {
1314 if raw.request_id.trim().is_empty() {
1315 bail!("trace line {} has empty request_id", line_idx + 1);
1316 }
1317 if raw.play_id.trim().is_empty() {
1318 bail!("trace line {} has empty play_id", line_idx + 1);
1319 }
1320 if raw.session_id.trim().is_empty() {
1321 bail!("trace line {} has empty session_id", line_idx + 1);
1322 }
1323 if !self.request_ids.insert(raw.request_id.clone()) {
1324 bail!(
1325 "trace line {} duplicates request_id {}",
1326 line_idx + 1,
1327 raw.request_id
1328 );
1329 }
1330
1331 let hash_ids = raw
1332 .hash_ids
1333 .ok_or_else(|| anyhow!("trace line {} is missing hash_ids", line_idx + 1))?;
1334 if hash_ids.is_empty() {
1335 bail!("trace line {} has empty hash_ids", line_idx + 1);
1336 }
1337 let input_length = raw
1338 .input_length
1339 .ok_or_else(|| anyhow!("trace line {} is missing input_length", line_idx + 1))?;
1340 if input_length == 0 {
1341 bail!("trace line {} has zero input_length", line_idx + 1);
1342 }
1343 let expected_hashes = input_length.div_ceil(self.header.block_size);
1344 if hash_ids.len() != expected_hashes {
1345 bail!(
1346 "trace line {} has input_length {} at block_size {} and requires exactly {} hash_ids, got {}",
1347 line_idx + 1,
1348 input_length,
1349 self.header.block_size,
1350 expected_hashes,
1351 hash_ids.len()
1352 );
1353 }
1354 if raw.model.trim().is_empty() {
1355 bail!("trace line {} has an empty model", line_idx + 1);
1356 }
1357 let output_length = raw
1358 .output_length
1359 .ok_or_else(|| anyhow!("trace line {} is missing output_length", line_idx + 1))?;
1360 let output_token_ids = raw.output_token_ids;
1361 if let Some(output_token_ids) = output_token_ids.as_ref()
1362 && output_token_ids.len() != output_length
1363 {
1364 bail!(
1365 "trace line {} output_length {} does not match output_token_ids length {}",
1366 line_idx + 1,
1367 output_length,
1368 output_token_ids.len()
1369 );
1370 }
1371 if !raw.not_before_ms.is_finite() || raw.not_before_ms < 0.0 {
1372 bail!(
1373 "trace line {} has invalid not_before_ms {}",
1374 line_idx + 1,
1375 raw.not_before_ms
1376 );
1377 }
1378 for dependency in &raw.dependencies {
1379 if dependency.request_id.trim().is_empty() {
1380 bail!(
1381 "trace line {} has a dependency with an empty request_id",
1382 line_idx + 1
1383 );
1384 }
1385 if !dependency.delay_ms.is_finite() || dependency.delay_ms < 0.0 {
1386 bail!(
1387 "trace line {} has invalid dependency delay {}",
1388 line_idx + 1,
1389 dependency.delay_ms
1390 );
1391 }
1392 match (dependency.relation, dependency.trigger) {
1393 (AgenticDependencyRelation::Sequence, AgenticDependencyTrigger::Completion)
1394 | (AgenticDependencyRelation::Spawn, _)
1395 | (AgenticDependencyRelation::Join, AgenticDependencyTrigger::Completion)
1396 | (
1397 AgenticDependencyRelation::ReplayBarrier,
1398 AgenticDependencyTrigger::Completion,
1399 ) => {}
1400 (relation, trigger) => bail!(
1401 "trace line {} has invalid {:?} dependency with {:?} trigger",
1402 line_idx + 1,
1403 relation,
1404 trigger
1405 ),
1406 }
1407 }
1408
1409 let replay_key = output_token_ids.as_ref().map(|_| {
1410 effective_replay_key(
1411 Some(raw.request_id.as_str()),
1412 Some(raw.session_id.as_str()),
1413 0,
1414 line_idx,
1415 )
1416 });
1417 self.nodes.push(AgenticNode {
1418 replay_key,
1419 request_id: raw.request_id,
1420 play_id: raw.play_id,
1421 session_id: raw.session_id,
1422 model: raw.model,
1423 input_length,
1424 max_output_tokens: output_length,
1425 output_token_ids,
1426 hash_ids,
1427 not_before_ms: raw.not_before_ms,
1428 priority: raw.priority.unwrap_or(0),
1429 strict_priority: raw.strict_priority.unwrap_or(0),
1430 policy_class: raw.policy_class,
1431 dependencies: raw.dependencies,
1432 });
1433 Ok(())
1434 }
1435
1436 fn finish(mut self) -> Result<AgenticTrace> {
1437 self.nodes
1438 .sort_by(|left, right| left.request_id.cmp(&right.request_id));
1439 let index_by_id: HashMap<&str, usize> = self
1440 .nodes
1441 .iter()
1442 .enumerate()
1443 .map(|(index, node)| (node.request_id.as_str(), index))
1444 .collect();
1445 let mut nodes_by_play: HashMap<String, Vec<usize>> = HashMap::new();
1446
1447 for (node_index, node) in self.nodes.iter().enumerate() {
1448 nodes_by_play
1449 .entry(node.play_id.clone())
1450 .or_default()
1451 .push(node_index);
1452 for dependency in &node.dependencies {
1453 let Some(&dependency_index) = index_by_id.get(dependency.request_id.as_str())
1454 else {
1455 bail!(
1456 "request {} references unknown request_id {}",
1457 node.request_id,
1458 dependency.request_id
1459 );
1460 };
1461 let dependency_node = &self.nodes[dependency_index];
1462 if dependency_index == node_index {
1463 bail!("request {} cannot depend on itself", node.request_id);
1464 }
1465 if dependency_node.play_id != node.play_id {
1466 bail!(
1467 "request {} in play {} depends on request {} in play {}",
1468 node.request_id,
1469 node.play_id,
1470 dependency.request_id,
1471 dependency_node.play_id
1472 );
1473 }
1474 }
1475 }
1476 validate_agentic_trace_is_acyclic(&self.nodes, &index_by_id)?;
1477
1478 let mut plays = Vec::with_capacity(nodes_by_play.len());
1479 for (play_id, mut node_indices) in nodes_by_play {
1480 node_indices.sort_unstable();
1481 let roots: Vec<_> = node_indices
1482 .iter()
1483 .copied()
1484 .filter(|node_index| self.nodes[*node_index].dependencies.is_empty())
1485 .collect();
1486 if roots.is_empty() {
1487 bail!(
1488 "play {} must have at least one root request, found {}",
1489 play_id,
1490 roots.len()
1491 );
1492 }
1493 plays.push(AgenticPlay {
1494 play_id,
1495 root_nodes: roots,
1496 nodes: node_indices,
1497 });
1498 }
1499 plays.sort_by(|left, right| left.play_id.cmp(&right.play_id));
1500 let graph_digest = canonical_agentic_graph_digest(self.header.block_size, &mut self.nodes)?;
1501
1502 Ok(AgenticTrace {
1503 block_size: self.header.block_size,
1504 source: self.header.source,
1505 graph_digest,
1506 nodes: self.nodes,
1507 plays,
1508 })
1509 }
1510}
1511
1512impl AgenticTrace {
1513 pub fn from_agentic_mooncake(path: &Path) -> Result<Self> {
1514 let file = File::open(path)
1515 .with_context(|| format!("failed to open trace file {}", path.display()))?;
1516 let reader = BufReader::new(file);
1517 let mut builder = None;
1518
1519 for (line_idx, line) in reader.lines().enumerate() {
1520 let line = line.with_context(|| {
1521 format!(
1522 "failed to read line {} from {}",
1523 line_idx + 1,
1524 path.display()
1525 )
1526 })?;
1527 if line.trim().is_empty() {
1528 continue;
1529 }
1530
1531 if builder.is_none() {
1532 let header = serde_json::from_str(&line).with_context(|| {
1533 format!(
1534 "failed to parse line {} from {} as the agentic Mooncake v2 header",
1535 line_idx + 1,
1536 path.display()
1537 )
1538 })?;
1539 builder = Some(AgenticTraceBuilder::new(header)?);
1540 continue;
1541 }
1542 let row = serde_json::from_str(&line).with_context(|| {
1543 format!(
1544 "failed to parse line {} from {} as an agentic Mooncake v2 request",
1545 line_idx + 1,
1546 path.display()
1547 )
1548 })?;
1549 builder
1550 .as_mut()
1551 .expect("builder was initialized from the v2 header")
1552 .push(line_idx, row)?;
1553 }
1554
1555 let Some(builder) = builder else {
1556 bail!(
1557 "agentic trace file {} is missing its v2 header",
1558 path.display()
1559 );
1560 };
1561 if builder.is_empty() {
1562 bail!(
1563 "agentic trace file {} did not contain any requests",
1564 path.display()
1565 );
1566 }
1567
1568 builder.finish()
1569 }
1570
1571 pub fn from_agentic_mooncake_rows(
1572 header: AgenticMooncakeHeader,
1573 rows: Vec<AgenticMooncakeRow>,
1574 ) -> Result<Self> {
1575 let mut builder = AgenticTraceBuilder::new(header)?;
1576 for (line_idx, row) in rows.into_iter().enumerate() {
1577 builder.push(line_idx + 1, row)?;
1578 }
1579 if builder.is_empty() {
1580 bail!("agentic Mooncake rows did not contain any requests");
1581 }
1582 builder.finish()
1583 }
1584
1585 pub fn normalize_starts(mut self) -> Self {
1586 let min_timestamp_ms = self
1587 .nodes
1588 .iter()
1589 .map(|node| node.not_before_ms)
1590 .min_by(|left, right| left.total_cmp(right))
1591 .unwrap_or(0.0);
1592
1593 for node in &mut self.nodes {
1594 node.not_before_ms -= min_timestamp_ms;
1595 }
1596 self.graph_digest = canonical_agentic_graph_digest(self.block_size, &mut self.nodes)
1597 .expect("validated agentic graph remains serializable after normalization");
1598 self
1599 }
1600
1601 pub fn speed_up_timing(mut self, ratio: f64) -> Result<Self> {
1602 if !ratio.is_finite() || ratio <= 0.0 {
1603 bail!("ratio must be a finite positive number, got {ratio}");
1604 }
1605
1606 for node in &mut self.nodes {
1607 node.not_before_ms /= ratio;
1608 for dependency in &mut node.dependencies {
1609 dependency.delay_ms /= ratio;
1610 }
1611 }
1612 self.graph_digest = canonical_agentic_graph_digest(self.block_size, &mut self.nodes)?;
1613 Ok(self)
1614 }
1615
1616 pub fn into_trace_driver_with_block_size(
1617 self,
1618 engine_block_size: usize,
1619 ) -> Result<WorkloadDriver> {
1620 WorkloadDriver::new_agentic_trace(self, engine_block_size)
1621 }
1622
1623 pub fn into_trace_driver_with_options(
1624 self,
1625 engine_block_size: usize,
1626 include_replay_hashes: bool,
1627 agentic_lanes: Option<usize>,
1628 ) -> Result<WorkloadDriver> {
1629 WorkloadDriver::new_agentic_trace_with_options(
1630 self,
1631 engine_block_size,
1632 include_replay_hashes,
1633 agentic_lanes,
1634 )
1635 }
1636}
1637
1638fn validate_agentic_trace_is_acyclic(
1639 nodes: &[AgenticNode],
1640 index_by_id: &HashMap<&str, usize>,
1641) -> Result<()> {
1642 let mut indegree = nodes
1643 .iter()
1644 .map(|node| node.dependencies.len())
1645 .collect::<Vec<_>>();
1646 let mut dependents = vec![Vec::new(); nodes.len()];
1647 for (node_index, node) in nodes.iter().enumerate() {
1648 for dependency in &node.dependencies {
1649 let dependency_index = *index_by_id
1650 .get(dependency.request_id.as_str())
1651 .expect("dependencies were prevalidated");
1652 dependents[dependency_index].push(node_index);
1653 }
1654 }
1655 let mut ready = std::collections::VecDeque::from_iter(
1656 indegree
1657 .iter()
1658 .enumerate()
1659 .filter_map(|(index, degree)| (*degree == 0).then_some(index)),
1660 );
1661 let mut visited = 0;
1662 while let Some(node_index) = ready.pop_front() {
1663 visited += 1;
1664 for dependent in &dependents[node_index] {
1665 indegree[*dependent] -= 1;
1666 if indegree[*dependent] == 0 {
1667 ready.push_back(*dependent);
1668 }
1669 }
1670 }
1671 if visited == nodes.len() {
1672 return Ok(());
1673 }
1674 let blocked = indegree
1675 .iter()
1676 .enumerate()
1677 .filter_map(|(index, degree)| (*degree > 0).then_some(nodes[index].request_id.as_str()))
1678 .take(8)
1679 .collect::<Vec<_>>();
1680 bail!(
1681 "cycle detected among {} agentic requests; blocked request IDs: {}",
1682 nodes.len() - visited,
1683 blocked.join(", ")
1684 )
1685}
1686
1687fn canonical_agentic_graph_digest(block_size: usize, nodes: &mut [AgenticNode]) -> Result<String> {
1688 struct Blake3Writer<'a>(&'a mut blake3::Hasher);
1689
1690 impl std::io::Write for Blake3Writer<'_> {
1691 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
1692 self.0.update(bytes);
1693 Ok(bytes.len())
1694 }
1695
1696 fn flush(&mut self) -> std::io::Result<()> {
1697 Ok(())
1698 }
1699 }
1700
1701 #[derive(Serialize)]
1702 struct CanonicalGraph<'a> {
1703 block_size: usize,
1704 nodes: &'a [AgenticNode],
1705 }
1706
1707 for node in nodes.iter_mut() {
1708 node.dependencies.sort_by(|left, right| {
1709 left.request_id
1710 .cmp(&right.request_id)
1711 .then_with(|| trigger_rank(left.trigger).cmp(&trigger_rank(right.trigger)))
1712 .then_with(|| relation_rank(left.relation).cmp(&relation_rank(right.relation)))
1713 .then_with(|| left.delay_ms.total_cmp(&right.delay_ms))
1714 });
1715 }
1716 let mut hasher = blake3::Hasher::new();
1717 serde_json::to_writer(
1718 Blake3Writer(&mut hasher),
1719 &CanonicalGraph { block_size, nodes },
1720 )?;
1721 Ok(hasher.finalize().to_hex().to_string())
1722}
1723
1724fn trigger_rank(trigger: AgenticDependencyTrigger) -> u8 {
1725 match trigger {
1726 AgenticDependencyTrigger::Dispatch => 0,
1727 AgenticDependencyTrigger::Completion => 1,
1728 }
1729}
1730
1731fn relation_rank(relation: AgenticDependencyRelation) -> u8 {
1732 match relation {
1733 AgenticDependencyRelation::Sequence => 0,
1734 AgenticDependencyRelation::Spawn => 1,
1735 AgenticDependencyRelation::Join => 2,
1736 AgenticDependencyRelation::ReplayBarrier => 3,
1737 }
1738}
1739
1740fn extend_applied_compute_agentic_hash_ids(
1741 hash_ids: &mut Vec<u64>,
1742 input_length: usize,
1743 trace_block_size: usize,
1744 shared_initial_blocks: usize,
1745 group_id: Option<usize>,
1746 next_unique_hash: &mut u64,
1747) -> Result<()> {
1748 let target_blocks = input_length.div_ceil(trace_block_size);
1749 while hash_ids.len() < target_blocks {
1750 let block_idx = hash_ids.len();
1751 if block_idx < shared_initial_blocks
1752 && let Some(group_id) = group_id
1753 {
1754 hash_ids.push(0xA63E_0000_0000_0000 | ((group_id as u64) << 32) | block_idx as u64);
1755 continue;
1756 }
1757 hash_ids.push(*next_unique_hash);
1758 *next_unique_hash = next_unique_hash
1759 .checked_add(1)
1760 .ok_or_else(|| anyhow!("synthetic hash id overflow"))?;
1761 }
1762 Ok(())
1763}
1764
1765fn sample_delay_ms(spec: &DelaySpec, rng: &mut StdRng) -> Result<f64> {
1766 match spec {
1767 DelaySpec::None => Ok(0.0),
1768 DelaySpec::ConstantMs(delay_ms) => {
1769 if !delay_ms.is_finite() || *delay_ms < 0.0 {
1770 bail!("delay must be a finite non-negative number, got {delay_ms}");
1771 }
1772 Ok(*delay_ms)
1773 }
1774 DelaySpec::ExponentialMs { mean_ms } => {
1775 if !mean_ms.is_finite() || *mean_ms < 0.0 {
1776 bail!("mean_ms must be a finite non-negative number, got {mean_ms}");
1777 }
1778 Ok(sample_exponential_delay_ms(*mean_ms, rng))
1779 }
1780 }
1781}
1782
1783fn sample_length(spec: &LengthSpec, min_value: usize, rng: &mut StdRng) -> usize {
1784 if spec.stddev == 0.0 {
1785 return spec.mean.max(min_value);
1786 }
1787
1788 let stddev = spec.stddev.abs();
1789 let u1 = (1.0 - rng.random::<f64>()).clamp(f64::MIN_POSITIVE, 1.0);
1790 let u2 = rng.random::<f64>();
1791 let z0 = (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos();
1792 let sample = spec.mean as f64 + z0 * stddev;
1793 sample.round().max(min_value as f64) as usize
1794}
1795
1796fn sample_exponential_delay_ms(mean_ms: f64, rng: &mut StdRng) -> f64 {
1797 if mean_ms == 0.0 {
1798 return 0.0;
1799 }
1800 let u = (1.0 - rng.random::<f64>()).clamp(f64::MIN_POSITIVE, 1.0);
1801 -mean_ms * u.ln()
1802}