1use chrono::{DateTime, Utc};
31use serde::{Deserialize, Serialize};
32use serde_json::Value;
33
34use crate::{ThreadEvent, ThreadItemDetails, ToolCallStatus};
35
36const ATIF_SCHEMA_VERSION: &str = "ATIF-v1.4";
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Trajectory {
46 schema_version: String,
48 session_id: String,
50 agent: AtifAgent,
52 steps: Vec<Step>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 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 extra: Option<Value>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AtifAgent {
68 name: String,
70 version: String,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 model_name: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 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 step_id: u64,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 timestamp: Option<String>,
123 source: StepSource,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 model_name: Option<String>,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 message: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 reasoning_content: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 tool_calls: Option<Vec<AtifToolCall>>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 observation: Option<Observation>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 metrics: Option<StepMetrics>,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 extra: Option<Value>,
146}
147
148impl Step {
149 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 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 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 tool_call_id: String,
203 function_name: String,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 arguments: Option<Value>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct Observation {
213 results: Vec<ObservationResult>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct ObservationResult {
220 source_call_id: String,
222 content: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct StepMetrics {
229 #[serde(skip_serializing_if = "Option::is_none")]
231 prompt_tokens: Option<u64>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 completion_tokens: Option<u64>,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 cached_tokens: Option<u64>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 cost_usd: Option<f64>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 logprobs: Option<Vec<f64>>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 completion_token_ids: Option<Vec<u64>>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 prompt_token_ids: Option<Vec<u64>>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 extra: Option<Value>,
253}
254
255impl StepMetrics {
256 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 total_cost_usd: Option<f64>,
296 #[serde(skip_serializing_if = "Option::is_none")]
298 total_steps: Option<u64>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 extra: Option<Value>,
302}
303
304#[must_use = "finish the trajectory before discarding the builder"]
316pub struct AtifTrajectoryBuilder {
317 completed_at: Option<String>,
318 agent: AtifAgent,
319 session_id: Option<String>,
320 steps: Vec<Step>,
321 next_step_id: u64,
322 total_input_tokens: u64,
324 total_output_tokens: u64,
325 total_cached_tokens: u64,
326 num_turns: usize,
327 saw_per_turn_usage: bool,
330 pending_tool_calls: Vec<PendingToolCall>,
332}
333
334struct PendingToolCall {
335 call_id: String,
336 tool_call_id: Option<String>,
337 tool_name: String,
338 arguments: Option<Value>,
339 timestamp: String,
340}
341
342impl AtifTrajectoryBuilder {
343 pub fn new(agent: AtifAgent) -> Self {
345 Self {
346 agent,
347 completed_at: None,
348 session_id: None,
349 steps: Vec::new(),
350 next_step_id: 1,
351 total_input_tokens: 0,
352 total_output_tokens: 0,
353 total_cached_tokens: 0,
354 num_turns: 0,
355 saw_per_turn_usage: false,
356 pending_tool_calls: Vec::new(),
357 }
358 }
359
360 pub fn set_session_id(&mut self, id: impl Into<String>) {
363 self.session_id = Some(id.into());
364 }
365
366 pub fn process_event(&mut self, event: &ThreadEvent) {
368 self.process_event_at(event, Utc::now());
369 }
370
371 pub fn process_event_at(&mut self, event: &ThreadEvent, ts: DateTime<Utc>) {
373 let first_step = self.steps.len();
374 let ts_str = ts.to_rfc3339();
375 match event {
376 ThreadEvent::ThreadStarted(e) => {
377 if self.session_id.is_none() {
378 self.session_id = Some(e.thread_id.clone());
379 }
380 }
381 ThreadEvent::ThreadCompleted(e) => {
382 self.completed_at = e.completed_at.clone();
383 if self.session_id.is_none() {
384 self.session_id = Some(e.session_id.clone());
385 }
386 self.num_turns = e.num_turns;
387 if !self.saw_per_turn_usage {
393 self.total_input_tokens = self.total_input_tokens.saturating_add(e.usage.input_tokens);
394 self.total_output_tokens = self.total_output_tokens.saturating_add(e.usage.output_tokens);
395 self.total_cached_tokens = self.total_cached_tokens.saturating_add(e.usage.cached_input_tokens);
396 }
397 }
398 ThreadEvent::TurnCompleted(e) => {
399 self.saw_per_turn_usage = true;
400 self.total_input_tokens = self.total_input_tokens.saturating_add(e.usage.input_tokens);
401 self.total_output_tokens = self.total_output_tokens.saturating_add(e.usage.output_tokens);
402 self.total_cached_tokens = self.total_cached_tokens.saturating_add(e.usage.cached_input_tokens);
403 self.num_turns += 1;
404
405 let mut step = Step::system(self.next_step_id, "turn_completed");
406 step.timestamp = Some(ts_str);
407 step.metrics = Some(StepMetrics::from_usage(&e.usage));
408 if !e.in_progress_exec_sessions.is_empty() {
409 step.extra = Some(serde_json::json!({
410 "in_progress_exec_sessions": e.in_progress_exec_sessions,
411 }));
412 }
413 self.push_step(step);
414 }
415 ThreadEvent::TurnFailed(e) => {
416 if let Some(usage) = &e.usage {
417 self.saw_per_turn_usage = true;
418 self.total_input_tokens = self.total_input_tokens.saturating_add(usage.input_tokens);
419 self.total_output_tokens = self.total_output_tokens.saturating_add(usage.output_tokens);
420 self.total_cached_tokens = self.total_cached_tokens.saturating_add(usage.cached_input_tokens);
421 }
422 let mut step = Step::system(self.next_step_id, &e.message);
423 step.timestamp = Some(ts_str);
424 step.metrics = e.usage.as_ref().map(StepMetrics::from_usage);
425 self.push_step(step);
426 }
427 ThreadEvent::TurnBlocked(e) => {
428 if let Some(usage) = &e.usage {
429 self.saw_per_turn_usage = true;
430 self.total_input_tokens = self.total_input_tokens.saturating_add(usage.input_tokens);
431 self.total_output_tokens = self.total_output_tokens.saturating_add(usage.output_tokens);
432 self.total_cached_tokens = self.total_cached_tokens.saturating_add(usage.cached_input_tokens);
433 }
434 let mut step = Step::system(self.next_step_id, &e.message);
435 step.timestamp = Some(ts_str);
436 step.metrics = e.usage.as_ref().map(StepMetrics::from_usage);
437 step.extra = Some(serde_json::json!({
438 "last_tool": e.last_tool,
439 "blocked_streak": e.blocked_streak,
440 "blocked_total": e.blocked_total,
441 "consecutive_cap": e.consecutive_cap,
442 "total_cap": e.total_cap,
443 "recovery_active": e.recovery_active,
444 }));
445 self.push_step(step);
446 }
447 ThreadEvent::ItemCompleted(e) => {
448 self.process_item_completed(&e.item.id, &e.item.details, &ts_str);
449 }
450 ThreadEvent::AppSandboxDenied(e) => {
451 let mut step = Step::system(
452 self.next_step_id,
453 format!("app_sandbox_denied: {} refused {}", e.tool, e.uris.join(", ")),
454 );
455 step.timestamp = Some(ts_str);
456 step.extra = Some(serde_json::json!({
457 "tool": e.tool,
458 "tier": e.tier,
459 "uris": e.uris,
460 }));
461 self.push_step(step);
462 }
463 ThreadEvent::ThreadCompactBoundary(e) => {
464 let msg = format!(
465 "context_compaction: {} messages -> {} messages ({})",
466 e.original_message_count,
467 e.compacted_message_count,
468 e.trigger.as_str()
469 );
470 let mut step = Step::system(self.next_step_id, msg);
471 step.timestamp = Some(ts_str);
472 self.push_step(step);
473 }
474 ThreadEvent::ContextReset(e) => {
475 let msg = format!(
476 "context_reset: {}% context used; plan preserved: {}; tool budget reset: {}",
477 e.previous_context_usage_percent, e.plan_preserved, e.tool_budget_reset
478 );
479 let mut step = Step::system(self.next_step_id, msg);
480 step.timestamp = Some(ts_str);
481 step.extra = Some(serde_json::json!({
482 "thread_id": e.thread_id,
483 "turn_id": e.turn_id,
484 "trigger": e.trigger,
485 "plan_preserved": e.plan_preserved,
486 "previous_context_usage_percent": e.previous_context_usage_percent,
487 "tool_budget_reset": e.tool_budget_reset,
488 }));
489 self.push_step(step);
490 }
491 ThreadEvent::Error(e) => {
492 let mut step = Step::system(self.next_step_id, &e.message);
493 step.timestamp = Some(ts_str);
494 self.push_step(step);
495 }
496 ThreadEvent::TurnStarted(e) => {
498 if let Some(context) = &e.context {
499 let mut step = Step::agent(self.next_step_id, context.goal.as_deref().unwrap_or("Turn started"));
500 step.source = StepSource::User;
501 step.timestamp = Some(context.timestamp.clone());
502 step.extra = Some(serde_json::json!({"execution_context": context}));
503 self.push_step(step);
504 }
505 }
506 ThreadEvent::MatrixUpdated(_)
507 | ThreadEvent::TrackerUpdated(_)
508 | ThreadEvent::ItemStarted(_)
509 | ThreadEvent::ItemUpdated(_)
510 | ThreadEvent::PlanDelta(_)
511 | ThreadEvent::PlanApprovalRequested(_)
512 | ThreadEvent::PlanApprovalResolved(_)
513 | ThreadEvent::PermissionRequested(_)
514 | ThreadEvent::PermissionResolved(_)
515 | ThreadEvent::Interjected(_)
516 | ThreadEvent::Unknown => {}
517 }
518 for step in self.steps.iter_mut().skip(first_step) {
519 let context = match event {
520 ThreadEvent::ItemCompleted(e) => e.item.context.as_deref(),
521 _ => None,
522 };
523 if let Some(context) = context {
524 step.timestamp = Some(context.timestamp.clone());
525 let extra = step.extra.get_or_insert_with(|| serde_json::json!({}));
526 if let Some(extra) = extra.as_object_mut() {
527 let _ = extra.insert("item_context".into(), serde_json::json!(context));
528 }
529 }
530 match event {
531 ThreadEvent::TurnCompleted(e) => {
532 if let Some(ts) = &e.completed_at {
533 step.timestamp = Some(ts.to_string());
534 }
535 }
536 ThreadEvent::TurnFailed(e) => {
537 if let Some(ts) = &e.completed_at {
538 step.timestamp = Some(ts.to_string());
539 }
540 }
541 ThreadEvent::TurnBlocked(e) => {
542 if let Some(ts) = &e.completed_at {
543 step.timestamp = Some(ts.clone());
544 }
545 }
546 _ => {}
547 }
548 }
549 }
550
551 fn process_item_completed(&mut self, item_id: &str, details: &ThreadItemDetails, ts: &str) {
552 match details {
553 ThreadItemDetails::Decision(d) => {
554 let mut step = Step::agent(self.next_step_id, &d.summary);
555 step.timestamp = Some(ts.to_owned());
556 step.extra = Some(
557 serde_json::json!({"vtcode_item_type": "decision", "public_rationale": d.rationale, "alternatives": d.alternatives, "evidence_ids": d.evidence_ids}),
558 );
559 self.push_step(step);
560 }
561 ThreadItemDetails::AgentMessage(msg) => {
562 let mut step = Step::agent(self.next_step_id, &msg.text);
563 step.timestamp = Some(ts.to_string());
564 self.push_step(step);
565 }
566 ThreadItemDetails::Plan(plan) => {
567 let mut step = Step::agent(self.next_step_id, &plan.text);
568 step.timestamp = Some(ts.to_string());
569 step.extra = Some(serde_json::json!({ "vtcode_item_type": "plan" }));
570 self.push_step(step);
571 }
572 ThreadItemDetails::Reasoning(r) => {
573 let mut step = Step::agent(self.next_step_id, "");
574 step.timestamp = Some(ts.to_string());
575 if let Some(diagnostic) = &r.diagnostic {
576 step.message = Some(diagnostic.summary.clone());
577 step.extra = Some(serde_json::json!({ "diagnostic": diagnostic }));
578 } else {
579 step.reasoning_content = Some(r.text.clone());
580 step.message = None;
581 }
582 self.push_step(step);
583 }
584 ThreadItemDetails::ToolInvocation(inv) => {
585 self.pending_tool_calls.push(PendingToolCall {
587 call_id: item_id.to_string(),
588 tool_call_id: inv.tool_call_id.clone(),
589 tool_name: inv.tool_name.clone(),
590 arguments: inv.arguments.clone(),
591 timestamp: ts.to_string(),
592 });
593 }
594 ThreadItemDetails::ToolOutput(output) => {
595 let pending_idx = self.pending_tool_calls.iter().position(|p| p.call_id == output.call_id);
597
598 let (tool_name, arguments, tool_call_id, inv_ts) = if let Some(idx) = pending_idx {
599 let p = self.pending_tool_calls.remove(idx);
600 (p.tool_name, p.arguments, p.tool_call_id, p.timestamp)
601 } else {
602 ("unknown".to_string(), None, output.tool_call_id.clone(), ts.to_string())
603 };
604
605 let call_id = tool_call_id.clone().unwrap_or_else(|| output.call_id.clone());
606
607 let mut step = Step::agent(self.next_step_id, "");
608 step.timestamp = Some(inv_ts);
609 step.message = None;
610 step.tool_calls = Some(vec![AtifToolCall {
611 tool_call_id: call_id.clone(),
612 function_name: tool_name,
613 arguments,
614 }]);
615
616 let status_suffix = match output.status {
617 ToolCallStatus::Failed => " [FAILED]",
618 ToolCallStatus::InProgress => " [IN_PROGRESS]",
619 ToolCallStatus::Completed => "",
620 };
621 let content = format!("{}{}", output.output, status_suffix);
622 step.observation = Some(Observation {
623 results: vec![ObservationResult { source_call_id: call_id, content }],
624 });
625 self.push_step(step);
626 }
627 ThreadItemDetails::CommandExecution(cmd) => {
628 let call_id = item_id.to_string();
629 let mut step = Step::agent(self.next_step_id, "");
630 step.timestamp = Some(ts.to_string());
631 step.message = None;
632 step.tool_calls = Some(vec![AtifToolCall {
633 tool_call_id: call_id.clone(),
634 function_name: "command_execution".to_string(),
635 arguments: Some(serde_json::json!({
636 "command": cmd.command,
637 "arguments": cmd.arguments,
638 })),
639 }]);
640 step.observation = Some(Observation {
641 results: vec![ObservationResult {
642 source_call_id: call_id,
643 content: cmd.aggregated_output.clone(),
644 }],
645 });
646 if let Some(exit_code) = cmd.exit_code {
647 step.extra = Some(serde_json::json!({ "exit_code": exit_code }));
648 }
649 self.push_step(step);
650 }
651 ThreadItemDetails::McpToolCall(mcp) => {
652 let call_id = item_id.to_string();
653 let mut step = Step::agent(self.next_step_id, "");
654 step.timestamp = Some(ts.to_string());
655 step.message = None;
656 step.tool_calls = Some(vec![AtifToolCall {
657 tool_call_id: call_id.clone(),
658 function_name: mcp.tool_name.clone(),
659 arguments: mcp.arguments.clone(),
660 }]);
661 if let Some(result) = &mcp.result {
662 step.observation = Some(Observation {
663 results: vec![ObservationResult { source_call_id: call_id, content: result.clone() }],
664 });
665 }
666 self.push_step(step);
667 }
668 ThreadItemDetails::FileChange(fc) => {
669 let changes: Vec<String> = fc.changes.iter().map(|c| format!("{}: {:?}", c.path, c.kind)).collect();
670 let msg = format!("file_changes: {}", changes.join(", "));
671 let mut step = Step::system(self.next_step_id, msg);
672 step.timestamp = Some(ts.to_string());
673 self.push_step(step);
674 }
675 ThreadItemDetails::WebSearch(ws) => {
676 let mut step = Step::system(self.next_step_id, format!("web_search: {}", ws.query));
677 step.timestamp = Some(ts.to_string());
678 if let Some(results) = &ws.results {
679 step.observation = Some(Observation {
680 results: results
681 .iter()
682 .enumerate()
683 .map(|(i, r)| ObservationResult {
684 source_call_id: format!("search_{i}"),
685 content: r.clone(),
686 })
687 .collect(),
688 });
689 }
690 self.push_step(step);
691 }
692 ThreadItemDetails::Harness(h) => {
693 let msg = format!("harness: {:?}", h.event);
694 let mut step = Step::system(self.next_step_id, msg);
695 step.timestamp = Some(ts.to_string());
696 let mut extra = serde_json::Map::new();
697 if let Some(m) = &h.message {
698 let _ = extra.insert("harness_message".to_string(), Value::String(m.clone()));
699 }
700 if matches!(
701 h.event,
702 crate::HarnessEventKind::BackgroundSubprocessCompleted
703 | crate::HarnessEventKind::SessionMessageSent
704 | crate::HarnessEventKind::SessionMessageReceived
705 | crate::HarnessEventKind::SessionMessageExpired
706 ) {
707 for (key, value) in [
708 ("task_id", h.task_id.as_ref()),
709 ("session_id", h.session_id.as_ref()),
710 ("correlation_id", h.correlation_id.as_ref()),
711 ("exec_session_id", h.exec_session_id.as_ref()),
712 ("status", h.status.as_ref()),
713 ("transcript_path", h.transcript_path.as_ref()),
714 ("archive_path", h.archive_path.as_ref()),
715 ("error_category", h.error_category.as_ref()),
716 ] {
717 if let Some(value) = value {
718 let _ = extra.insert(key.to_string(), Value::String(value.clone()));
719 }
720 }
721 if let Some(exit_code) = h.exit_code {
722 let _ = extra.insert("exit_code".to_string(), Value::from(exit_code));
723 }
724 }
725 if !extra.is_empty() {
726 step.extra = Some(Value::Object(extra));
727 }
728 self.push_step(step);
729 }
730 ThreadItemDetails::Error(e) => {
731 let mut step = Step::system(self.next_step_id, &e.message);
732 step.timestamp = Some(ts.to_string());
733 self.push_step(step);
734 }
735 }
736 }
737
738 fn push_step(&mut self, step: Step) {
739 self.next_step_id = step.step_id + 1;
740 self.steps.push(step);
741 }
742
743 #[must_use]
748 pub fn finish(self, override_metrics: Option<FinalMetrics>) -> Trajectory {
749 let final_metrics = override_metrics.unwrap_or_else(|| FinalMetrics {
750 total_prompt_tokens: Some(self.total_input_tokens),
751 total_completion_tokens: Some(self.total_output_tokens),
752 total_cached_tokens: if self.total_cached_tokens > 0 {
753 Some(self.total_cached_tokens)
754 } else {
755 None
756 },
757 total_cost_usd: None,
758 total_steps: Some(self.steps.len() as u64),
759 extra: Some(serde_json::json!({ "num_turns": self.num_turns })),
760 });
761
762 Trajectory {
763 schema_version: ATIF_SCHEMA_VERSION.to_string(),
764 session_id: self.session_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
765 agent: self.agent,
766 steps: self.steps,
767 notes: None,
768 final_metrics: Some(final_metrics),
769 extra: self.completed_at.map(|timestamp| serde_json::json!({"completed_at":timestamp})),
770 }
771 }
772
773 pub fn step_count(&self) -> usize {
775 self.steps.len()
776 }
777}
778
779impl crate::EventEmitter for AtifTrajectoryBuilder {
780 fn emit(&mut self, event: &ThreadEvent) {
781 self.process_event(event);
782 }
783}
784
785#[cfg(test)]
786mod tests;