1use chrono::{DateTime, Utc};
31use serde::{Deserialize, Serialize};
32use serde_json::Value;
33
34use crate::{ThreadEvent, ThreadItemDetails, ToolCallStatus};
35
36pub const ATIF_SCHEMA_VERSION: &str = "ATIF-v1.4";
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Trajectory {
46 pub schema_version: String,
48 pub session_id: String,
50 pub agent: AtifAgent,
52 pub steps: Vec<Step>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub notes: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub final_metrics: Option<FinalMetrics>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub extra: Option<Value>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AtifAgent {
68 pub name: String,
70 pub version: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub model_name: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub extra: Option<Value>,
78}
79
80impl AtifAgent {
81 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
83 Self {
84 name: name.into(),
85 version: version.into(),
86 model_name: None,
87 extra: None,
88 }
89 }
90
91 pub fn vtcode() -> Self {
93 Self::new("vtcode", env!("CARGO_PKG_VERSION"))
94 }
95
96 pub fn with_model(mut self, model: impl Into<String>) -> Self {
98 self.model_name = Some(model.into());
99 self
100 }
101}
102
103#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
105#[serde(rename_all = "lowercase")]
106pub enum StepSource {
107 System,
109 User,
111 Agent,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Step {
118 pub step_id: u64,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub timestamp: Option<String>,
123 pub source: StepSource,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub model_name: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub message: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub reasoning_content: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub tool_calls: Option<Vec<AtifToolCall>>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub observation: Option<Observation>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub metrics: Option<StepMetrics>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub extra: Option<Value>,
146}
147
148impl Step {
149 pub fn user(step_id: u64, message: impl Into<String>) -> Self {
151 Self {
152 step_id,
153 timestamp: Some(Utc::now().to_rfc3339()),
154 source: StepSource::User,
155 model_name: None,
156 message: Some(message.into()),
157 reasoning_content: None,
158 tool_calls: None,
159 observation: None,
160 metrics: None,
161 extra: None,
162 }
163 }
164
165 pub fn agent(step_id: u64, message: impl Into<String>) -> Self {
167 Self {
168 step_id,
169 timestamp: Some(Utc::now().to_rfc3339()),
170 source: StepSource::Agent,
171 model_name: None,
172 message: Some(message.into()),
173 reasoning_content: None,
174 tool_calls: None,
175 observation: None,
176 metrics: None,
177 extra: None,
178 }
179 }
180
181 pub fn system(step_id: u64, message: impl Into<String>) -> Self {
183 Self {
184 step_id,
185 timestamp: Some(Utc::now().to_rfc3339()),
186 source: StepSource::System,
187 model_name: None,
188 message: Some(message.into()),
189 reasoning_content: None,
190 tool_calls: None,
191 observation: None,
192 metrics: None,
193 extra: None,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct AtifToolCall {
201 pub tool_call_id: String,
203 pub function_name: String,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub arguments: Option<Value>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct Observation {
213 pub results: Vec<ObservationResult>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ObservationResult {
220 pub source_call_id: String,
222 pub content: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct StepMetrics {
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub prompt_tokens: Option<u64>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub completion_tokens: Option<u64>,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub cached_tokens: Option<u64>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub cost_usd: Option<f64>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub logprobs: Option<Vec<f64>>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub completion_token_ids: Option<Vec<u64>>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub prompt_token_ids: Option<Vec<u64>>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub extra: Option<Value>,
253}
254
255impl StepMetrics {
256 pub fn from_usage(usage: &crate::Usage) -> Self {
258 Self {
259 prompt_tokens: Some(usage.input_tokens),
260 completion_tokens: Some(usage.output_tokens),
261 cached_tokens: if usage.cached_input_tokens > 0 {
262 Some(usage.cached_input_tokens)
263 } else {
264 None
265 },
266 cost_usd: None,
267 logprobs: None,
268 completion_token_ids: None,
269 prompt_token_ids: None,
270 extra: if usage.cache_creation_tokens > 0 {
271 Some(serde_json::json!({
272 "cache_creation_tokens": usage.cache_creation_tokens
273 }))
274 } else {
275 None
276 },
277 }
278 }
279}
280
281#[derive(Debug, Clone, Default, Serialize, Deserialize)]
283pub struct FinalMetrics {
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub total_prompt_tokens: Option<u64>,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub total_completion_tokens: Option<u64>,
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub total_cached_tokens: Option<u64>,
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub total_cost_usd: Option<f64>,
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub total_steps: Option<u64>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub extra: Option<Value>,
302}
303
304pub struct AtifTrajectoryBuilder {
316 agent: AtifAgent,
317 session_id: Option<String>,
318 steps: Vec<Step>,
319 next_step_id: u64,
320 total_input_tokens: u64,
322 total_output_tokens: u64,
323 total_cached_tokens: u64,
324 num_turns: usize,
325 pending_tool_calls: Vec<PendingToolCall>,
327}
328
329struct PendingToolCall {
330 call_id: String,
331 tool_call_id: Option<String>,
332 tool_name: String,
333 arguments: Option<Value>,
334 timestamp: String,
335}
336
337impl AtifTrajectoryBuilder {
338 pub fn new(agent: AtifAgent) -> Self {
340 Self {
341 agent,
342 session_id: None,
343 steps: Vec::new(),
344 next_step_id: 1,
345 total_input_tokens: 0,
346 total_output_tokens: 0,
347 total_cached_tokens: 0,
348 num_turns: 0,
349 pending_tool_calls: Vec::new(),
350 }
351 }
352
353 pub fn set_session_id(&mut self, id: impl Into<String>) {
356 self.session_id = Some(id.into());
357 }
358
359 pub fn process_event(&mut self, event: &ThreadEvent) {
361 self.process_event_at(event, Utc::now());
362 }
363
364 pub fn process_event_at(&mut self, event: &ThreadEvent, ts: DateTime<Utc>) {
366 let ts_str = ts.to_rfc3339();
367 match event {
368 ThreadEvent::ThreadStarted(e) => {
369 if self.session_id.is_none() {
370 self.session_id = Some(e.thread_id.clone());
371 }
372 }
373 ThreadEvent::ThreadCompleted(e) => {
374 if self.session_id.is_none() {
375 self.session_id = Some(e.session_id.clone());
376 }
377 self.total_input_tokens =
379 self.total_input_tokens.saturating_add(e.usage.input_tokens);
380 self.total_output_tokens =
381 self.total_output_tokens.saturating_add(e.usage.output_tokens);
382 self.total_cached_tokens =
383 self.total_cached_tokens.saturating_add(e.usage.cached_input_tokens);
384 self.num_turns = e.num_turns;
385 }
386 ThreadEvent::TurnCompleted(e) => {
387 self.total_input_tokens =
388 self.total_input_tokens.saturating_add(e.usage.input_tokens);
389 self.total_output_tokens =
390 self.total_output_tokens.saturating_add(e.usage.output_tokens);
391 self.total_cached_tokens =
392 self.total_cached_tokens.saturating_add(e.usage.cached_input_tokens);
393 self.num_turns += 1;
394
395 let mut step = Step::system(self.next_step_id, "turn_completed");
396 step.timestamp = Some(ts_str);
397 step.metrics = Some(StepMetrics::from_usage(&e.usage));
398 self.push_step(step);
399 }
400 ThreadEvent::TurnFailed(e) => {
401 if let Some(usage) = &e.usage {
402 self.total_input_tokens =
403 self.total_input_tokens.saturating_add(usage.input_tokens);
404 self.total_output_tokens =
405 self.total_output_tokens.saturating_add(usage.output_tokens);
406 }
407 let mut step = Step::system(self.next_step_id, &e.message);
408 step.timestamp = Some(ts_str);
409 step.metrics = e.usage.as_ref().map(StepMetrics::from_usage);
410 self.push_step(step);
411 }
412 ThreadEvent::ItemCompleted(e) => {
413 self.process_item_completed(&e.item.id, &e.item.details, &ts_str);
414 }
415 ThreadEvent::ThreadCompactBoundary(e) => {
416 let msg = format!(
417 "context_compaction: {} messages -> {} messages ({})",
418 e.original_message_count,
419 e.compacted_message_count,
420 e.trigger.as_str()
421 );
422 let mut step = Step::system(self.next_step_id, msg);
423 step.timestamp = Some(ts_str);
424 self.push_step(step);
425 }
426 ThreadEvent::Error(e) => {
427 let mut step = Step::system(self.next_step_id, &e.message);
428 step.timestamp = Some(ts_str);
429 self.push_step(step);
430 }
431 ThreadEvent::TurnStarted(_)
433 | ThreadEvent::ItemStarted(_)
434 | ThreadEvent::ItemUpdated(_)
435 | ThreadEvent::PlanDelta(_)
436 | ThreadEvent::Unknown => {}
437 }
438 }
439
440 fn process_item_completed(&mut self, item_id: &str, details: &ThreadItemDetails, ts: &str) {
441 match details {
442 ThreadItemDetails::AgentMessage(msg) => {
443 let mut step = Step::agent(self.next_step_id, &msg.text);
444 step.timestamp = Some(ts.to_string());
445 self.push_step(step);
446 }
447 ThreadItemDetails::Plan(plan) => {
448 let mut step = Step::agent(self.next_step_id, &plan.text);
449 step.timestamp = Some(ts.to_string());
450 step.extra = Some(serde_json::json!({ "vtcode_item_type": "plan" }));
451 self.push_step(step);
452 }
453 ThreadItemDetails::Reasoning(r) => {
454 let mut step = Step::agent(self.next_step_id, "");
455 step.timestamp = Some(ts.to_string());
456 step.reasoning_content = Some(r.text.clone());
457 step.message = None;
458 self.push_step(step);
459 }
460 ThreadItemDetails::ToolInvocation(inv) => {
461 self.pending_tool_calls.push(PendingToolCall {
463 call_id: item_id.to_string(),
464 tool_call_id: inv.tool_call_id.clone(),
465 tool_name: inv.tool_name.clone(),
466 arguments: inv.arguments.clone(),
467 timestamp: ts.to_string(),
468 });
469 }
470 ThreadItemDetails::ToolOutput(output) => {
471 let pending_idx =
473 self.pending_tool_calls.iter().position(|p| p.call_id == output.call_id);
474
475 let (tool_name, arguments, tool_call_id, inv_ts) = if let Some(idx) = pending_idx {
476 let p = self.pending_tool_calls.remove(idx);
477 (p.tool_name, p.arguments, p.tool_call_id, p.timestamp)
478 } else {
479 ("unknown".to_string(), None, output.tool_call_id.clone(), ts.to_string())
480 };
481
482 let call_id = tool_call_id.clone().unwrap_or_else(|| output.call_id.clone());
483
484 let mut step = Step::agent(self.next_step_id, "");
485 step.timestamp = Some(inv_ts);
486 step.message = None;
487 step.tool_calls = Some(vec![AtifToolCall {
488 tool_call_id: call_id.clone(),
489 function_name: tool_name,
490 arguments,
491 }]);
492
493 let status_suffix = match output.status {
494 ToolCallStatus::Failed => " [FAILED]",
495 ToolCallStatus::InProgress => " [IN_PROGRESS]",
496 ToolCallStatus::Completed => "",
497 };
498 let content = format!("{}{}", output.output, status_suffix);
499 step.observation = Some(Observation {
500 results: vec![ObservationResult { source_call_id: call_id, content }],
501 });
502 self.push_step(step);
503 }
504 ThreadItemDetails::CommandExecution(cmd) => {
505 let call_id = item_id.to_string();
506 let mut step = Step::agent(self.next_step_id, "");
507 step.timestamp = Some(ts.to_string());
508 step.message = None;
509 step.tool_calls = Some(vec![AtifToolCall {
510 tool_call_id: call_id.clone(),
511 function_name: "command_execution".to_string(),
512 arguments: Some(serde_json::json!({
513 "command": cmd.command,
514 "arguments": cmd.arguments,
515 })),
516 }]);
517 step.observation = Some(Observation {
518 results: vec![ObservationResult {
519 source_call_id: call_id,
520 content: cmd.aggregated_output.clone(),
521 }],
522 });
523 if let Some(exit_code) = cmd.exit_code {
524 step.extra = Some(serde_json::json!({ "exit_code": exit_code }));
525 }
526 self.push_step(step);
527 }
528 ThreadItemDetails::McpToolCall(mcp) => {
529 let call_id = item_id.to_string();
530 let mut step = Step::agent(self.next_step_id, "");
531 step.timestamp = Some(ts.to_string());
532 step.message = None;
533 step.tool_calls = Some(vec![AtifToolCall {
534 tool_call_id: call_id.clone(),
535 function_name: mcp.tool_name.clone(),
536 arguments: mcp.arguments.clone(),
537 }]);
538 if let Some(result) = &mcp.result {
539 step.observation = Some(Observation {
540 results: vec![ObservationResult {
541 source_call_id: call_id,
542 content: result.clone(),
543 }],
544 });
545 }
546 self.push_step(step);
547 }
548 ThreadItemDetails::FileChange(fc) => {
549 let changes: Vec<String> =
550 fc.changes.iter().map(|c| format!("{}: {:?}", c.path, c.kind)).collect();
551 let msg = format!("file_changes: {}", changes.join(", "));
552 let mut step = Step::system(self.next_step_id, msg);
553 step.timestamp = Some(ts.to_string());
554 self.push_step(step);
555 }
556 ThreadItemDetails::WebSearch(ws) => {
557 let mut step = Step::system(self.next_step_id, format!("web_search: {}", ws.query));
558 step.timestamp = Some(ts.to_string());
559 if let Some(results) = &ws.results {
560 step.observation = Some(Observation {
561 results: results
562 .iter()
563 .enumerate()
564 .map(|(i, r)| ObservationResult {
565 source_call_id: format!("search_{i}"),
566 content: r.clone(),
567 })
568 .collect(),
569 });
570 }
571 self.push_step(step);
572 }
573 ThreadItemDetails::Harness(h) => {
574 let msg = format!("harness: {:?}", h.event);
575 let mut step = Step::system(self.next_step_id, msg);
576 step.timestamp = Some(ts.to_string());
577 if let Some(m) = &h.message {
578 step.extra = Some(serde_json::json!({ "harness_message": m }));
579 }
580 self.push_step(step);
581 }
582 ThreadItemDetails::Error(e) => {
583 let mut step = Step::system(self.next_step_id, &e.message);
584 step.timestamp = Some(ts.to_string());
585 self.push_step(step);
586 }
587 }
588 }
589
590 fn push_step(&mut self, step: Step) {
591 self.next_step_id = step.step_id + 1;
592 self.steps.push(step);
593 }
594
595 pub fn finish(self, override_metrics: Option<FinalMetrics>) -> Trajectory {
600 let final_metrics = override_metrics.unwrap_or_else(|| FinalMetrics {
601 total_prompt_tokens: Some(self.total_input_tokens),
602 total_completion_tokens: Some(self.total_output_tokens),
603 total_cached_tokens: if self.total_cached_tokens > 0 {
604 Some(self.total_cached_tokens)
605 } else {
606 None
607 },
608 total_cost_usd: None,
609 total_steps: Some(self.steps.len() as u64),
610 extra: Some(serde_json::json!({ "num_turns": self.num_turns })),
611 });
612
613 Trajectory {
614 schema_version: ATIF_SCHEMA_VERSION.to_string(),
615 session_id: self.session_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
616 agent: self.agent,
617 steps: self.steps,
618 notes: None,
619 final_metrics: Some(final_metrics),
620 extra: None,
621 }
622 }
623
624 pub fn step_count(&self) -> usize {
626 self.steps.len()
627 }
628}
629
630impl crate::EventEmitter for AtifTrajectoryBuilder {
631 fn emit(&mut self, event: &ThreadEvent) {
632 self.process_event(event);
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639 use crate::{
640 AgentMessageItem, ItemCompletedEvent, ThreadItem, ThreadStartedEvent, ToolInvocationItem,
641 ToolOutputItem, TurnCompletedEvent, TurnStartedEvent, Usage,
642 };
643
644 fn fixed_ts() -> DateTime<Utc> {
645 "2025-01-15T10:30:00Z".parse().unwrap()
646 }
647
648 #[test]
649 fn trajectory_round_trip() {
650 let trajectory = Trajectory {
651 schema_version: ATIF_SCHEMA_VERSION.to_string(),
652 session_id: "test-session".to_string(),
653 agent: AtifAgent::vtcode(),
654 steps: vec![Step::user(1, "hello")],
655 notes: None,
656 final_metrics: None,
657 extra: None,
658 };
659
660 let json = serde_json::to_string_pretty(&trajectory).unwrap();
661 let restored: Trajectory = serde_json::from_str(&json).unwrap();
662 assert_eq!(restored.schema_version, ATIF_SCHEMA_VERSION);
663 assert_eq!(restored.session_id, "test-session");
664 assert_eq!(restored.steps.len(), 1);
665 }
666
667 #[test]
668 fn builder_thread_started_sets_session_id() {
669 let mut builder = AtifTrajectoryBuilder::new(AtifAgent::vtcode());
670 let event =
671 ThreadEvent::ThreadStarted(ThreadStartedEvent { thread_id: "thread-abc".to_string() });
672 builder.process_event_at(&event, fixed_ts());
673 let trajectory = builder.finish(None);
674 assert_eq!(trajectory.session_id, "thread-abc");
675 }
676
677 #[test]
678 fn builder_agent_message_step() {
679 let mut builder = AtifTrajectoryBuilder::new(AtifAgent::vtcode());
680 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
681 item: ThreadItem {
682 id: "msg-1".to_string(),
683 details: ThreadItemDetails::AgentMessage(AgentMessageItem {
684 text: "Hello, world!".to_string(),
685 }),
686 },
687 });
688 builder.process_event_at(&event, fixed_ts());
689 let trajectory = builder.finish(None);
690
691 assert_eq!(trajectory.steps.len(), 1);
692 let step = &trajectory.steps[0];
693 assert_eq!(step.step_id, 1);
694 assert_eq!(step.source, StepSource::Agent);
695 assert_eq!(step.message.as_deref(), Some("Hello, world!"));
696 }
697
698 #[test]
699 fn builder_tool_invocation_with_output() {
700 let mut builder = AtifTrajectoryBuilder::new(AtifAgent::vtcode());
701 let ts = fixed_ts();
702
703 let inv_event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
705 item: ThreadItem {
706 id: "tool_1".to_string(),
707 details: ThreadItemDetails::ToolInvocation(ToolInvocationItem {
708 tool_name: "read_file".to_string(),
709 arguments: Some(serde_json::json!({"path": "README.md"})),
710 tool_call_id: Some("tc_0".to_string()),
711 status: ToolCallStatus::Completed,
712 }),
713 },
714 });
715 builder.process_event_at(&inv_event, ts);
716
717 let out_event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
719 item: ThreadItem {
720 id: "tool_1:output".to_string(),
721 details: ThreadItemDetails::ToolOutput(ToolOutputItem {
722 call_id: "tool_1".to_string(),
723 tool_call_id: Some("tc_0".to_string()),
724 spool_path: None,
725 output: "file contents here".to_string(),
726 exit_code: Some(0),
727 status: ToolCallStatus::Completed,
728 }),
729 },
730 });
731 builder.process_event_at(&out_event, ts);
732
733 let trajectory = builder.finish(None);
734 assert_eq!(trajectory.steps.len(), 1);
736 let step = &trajectory.steps[0];
737 assert_eq!(step.source, StepSource::Agent);
738
739 let calls = step.tool_calls.as_ref().unwrap();
740 assert_eq!(calls.len(), 1);
741 assert_eq!(calls[0].function_name, "read_file");
742 assert_eq!(calls[0].tool_call_id, "tc_0");
743
744 let obs = step.observation.as_ref().unwrap();
745 assert_eq!(obs.results.len(), 1);
746 assert_eq!(obs.results[0].content, "file contents here");
747 }
748
749 #[test]
750 fn builder_turn_completed_accumulates_metrics() {
751 let mut builder = AtifTrajectoryBuilder::new(AtifAgent::vtcode());
752 let event = ThreadEvent::TurnCompleted(TurnCompletedEvent {
753 usage: Usage {
754 input_tokens: 500,
755 cached_input_tokens: 100,
756 cache_creation_tokens: 0,
757 output_tokens: 200,
758 },
759 });
760 builder.process_event_at(&event, fixed_ts());
761
762 let trajectory = builder.finish(None);
763 let fm = trajectory.final_metrics.as_ref().unwrap();
764 assert_eq!(fm.total_prompt_tokens, Some(500));
765 assert_eq!(fm.total_completion_tokens, Some(200));
766 assert_eq!(fm.total_cached_tokens, Some(100));
767 }
768
769 #[test]
770 fn step_metrics_from_usage() {
771 let usage = Usage {
772 input_tokens: 1000,
773 cached_input_tokens: 200,
774 cache_creation_tokens: 50,
775 output_tokens: 300,
776 };
777 let metrics = StepMetrics::from_usage(&usage);
778 assert_eq!(metrics.prompt_tokens, Some(1000));
779 assert_eq!(metrics.completion_tokens, Some(300));
780 assert_eq!(metrics.cached_tokens, Some(200));
781 assert!(metrics.extra.is_some());
782 }
783
784 #[test]
785 fn builder_implements_event_emitter() {
786 let mut builder = AtifTrajectoryBuilder::new(AtifAgent::vtcode());
787 let event = ThreadEvent::ThreadStarted(ThreadStartedEvent { thread_id: "t-1".to_string() });
788 crate::EventEmitter::emit(&mut builder, &event);
790 assert_eq!(builder.step_count(), 0); }
792
793 #[test]
794 fn skips_lifecycle_events() {
795 let mut builder = AtifTrajectoryBuilder::new(AtifAgent::vtcode());
796 builder.process_event(&ThreadEvent::TurnStarted(TurnStartedEvent::default()));
797 assert_eq!(builder.step_count(), 0);
798 }
799}