1use std::{fmt, str::FromStr};
11
12use serde::{Deserialize, Serialize};
13use typed_builder::TypedBuilder;
14
15macro_rules! define_sdk_enum {
39 (
40 $(#[$meta:meta])*
41 $name:ident {
42 $(
43 $(#[$vmeta:meta])*
44 $variant:ident => $wire:literal
45 ),+ $(,)?
46 }
47 ) => {
48 $(#[$meta])*
49 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
50 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
51 pub enum $name {
52 $(
53 $(#[$vmeta])*
54 $variant,
55 )+
56 }
57
58 impl fmt::Display for $name {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 let s = match self {
61 $( Self::$variant => $wire, )+
62 };
63 f.write_str(s)
64 }
65 }
66
67 impl FromStr for $name {
68 type Err = String;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 match s {
72 $( $wire => Ok(Self::$variant), )+
73 other => Err(format!(concat!("Unrecognized ", stringify!($name), ": {:?}"), other)),
74 }
75 }
76 }
77 };
78}
79
80macro_rules! define_sdk_enum_custom_serde {
86 (
87 $(#[$meta:meta])*
88 $name:ident {
89 $(
90 $(#[$vmeta:meta])*
91 $variant:ident => $wire:literal
92 ),+ $(,)?
93 }
94 ) => {
95 $(#[$meta])*
96 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
97 pub enum $name {
98 $(
99 $(#[$vmeta])*
100 #[serde(rename = $wire)]
101 $variant,
102 )+
103 }
104
105 impl fmt::Display for $name {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 let s = match self {
108 $( Self::$variant => $wire, )+
109 };
110 f.write_str(s)
111 }
112 }
113
114 impl FromStr for $name {
115 type Err = String;
116
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 match s {
119 $( $wire => Ok(Self::$variant), )+
120 other => Err(format!(concat!("Unrecognized ", stringify!($name), ": {:?}"), other)),
121 }
122 }
123 }
124 };
125}
126
127define_sdk_enum! {
128 StepType {
130 TextResponse => "TEXT_RESPONSE",
132 ToolCall => "TOOL_CALL",
134 SystemMessage => "SYSTEM_MESSAGE",
136 Compaction => "COMPACTION",
138 Finish => "FINISH",
140 Thinking => "THINKING",
142 #[default]
144 Unknown => "UNKNOWN",
145 }
146}
147
148define_sdk_enum! {
149 StepSource {
151 System => "SYSTEM",
153 User => "USER",
155 Model => "MODEL",
157 #[default]
159 Unknown => "UNKNOWN",
160 }
161}
162
163define_sdk_enum! {
164 StepStatus {
166 Active => "ACTIVE",
168 Done => "DONE",
170 WaitingForUser => "WAITING_FOR_USER",
172 Error => "ERROR",
174 Canceled => "CANCELED",
176 #[default]
178 Unknown => "UNKNOWN",
179 }
180}
181
182define_sdk_enum_custom_serde! {
183 StepTarget {
189 Model => "TARGET_MODEL",
191 User => "TARGET_USER",
193 Environment => "TARGET_ENVIRONMENT",
195 Unspecified => "TARGET_UNSPECIFIED",
197 #[default]
199 Unknown => "UNKNOWN",
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct ToolCallInfo {
206 pub name: String,
208 #[serde(default)]
210 pub args: serde_json::Value,
211 #[serde(default)]
213 pub id: Option<String>,
214 #[serde(default)]
216 pub canonical_path: Option<String>,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221pub struct ToolResult {
222 pub name: String,
224 #[serde(default)]
226 pub id: Option<String>,
227 #[serde(default)]
229 pub result: serde_json::Value,
230 #[serde(default)]
232 pub error: Option<String>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
237pub struct UsageMetadata {
238 #[serde(default)]
240 pub prompt_token_count: Option<u64>,
241 #[serde(default)]
243 pub cached_content_token_count: Option<u64>,
244 #[serde(default)]
246 pub candidates_token_count: Option<u64>,
247 #[serde(default)]
249 pub thoughts_token_count: Option<u64>,
250 #[serde(default)]
252 pub total_token_count: Option<u64>,
253}
254
255#[non_exhaustive]
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(rename_all = "lowercase")]
259#[derive(Default)]
260pub enum MessageRole {
261 #[default]
263 User,
264 Model,
266 System,
268 #[serde(untagged)]
271 Unknown(String),
272}
273
274impl std::fmt::Display for MessageRole {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 match self {
277 Self::User => f.write_str("user"),
278 Self::Model => f.write_str("model"),
279 Self::System => f.write_str("system"),
280 Self::Unknown(s) => f.write_str(s),
281 }
282 }
283}
284
285#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
290pub struct ConversationMessage {
291 #[serde(default)]
293 pub role: MessageRole,
294 #[serde(default)]
296 pub content: String,
297}
298
299#[non_exhaustive]
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, TypedBuilder)]
321#[builder(field_defaults(default))]
322pub struct Step {
323 #[serde(default)]
325 #[builder(setter(into))]
326 pub id: String,
327 #[serde(default)]
329 pub step_index: u32,
330 #[serde(default)]
338 #[builder(setter(into))]
339 pub trajectory_id: String,
340 #[serde(default)]
347 #[builder(setter(into))]
348 pub cascade_id: String,
349 #[serde(default, rename = "type")]
351 pub step_type: StepType,
352 #[serde(default)]
354 pub source: StepSource,
355 #[serde(default)]
357 pub target: StepTarget,
358 #[serde(default)]
360 pub status: StepStatus,
361 #[serde(default)]
363 #[builder(setter(into))]
364 pub content: String,
365 #[serde(default)]
367 #[builder(setter(into))]
368 pub content_delta: String,
369 #[serde(default)]
371 #[builder(setter(into))]
372 pub thinking: String,
373 #[serde(default)]
375 #[builder(setter(into))]
376 pub thinking_delta: String,
377 #[serde(default)]
379 #[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<ToolCallInfo>>| v.into_iter().map(Into::into).collect()))]
380 pub tool_calls: Vec<ToolCallInfo>,
381 #[serde(default)]
383 #[builder(setter(into))]
384 pub error: String,
385 #[serde(default)]
390 pub http_code: u16,
391 #[serde(default)]
396 #[builder(setter(strip_option))]
397 pub is_complete_response: Option<bool>,
398 #[serde(default)]
403 #[builder(setter(strip_option))]
404 pub structured_output: Option<serde_json::Value>,
405 #[serde(default)]
407 #[builder(setter(strip_option))]
408 pub usage_metadata: Option<UsageMetadata>,
409}
410
411impl Step {
412 #[must_use]
423 pub fn is_subagent_step(&self) -> bool {
424 !self.cascade_id.is_empty()
425 && !self.trajectory_id.is_empty()
426 && self.trajectory_id != self.cascade_id
427 }
428}
429
430macro_rules! impl_from_py_object {
431 ($($t:ty),+) => {
432 $(
433 impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for $t {
434 type Error = pyo3::PyErr;
435
436 fn extract(ob: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
437 crate::runtime::py_scripts::warm_up_lazy_imports(ob.py());
438 pythonize::depythonize(&*ob).map_err(|e| {
439 pyo3::exceptions::PyValueError::new_err(format!(
440 "Failed to deserialize {} from Python dict: {}",
441 stringify!($t),
442 e
443 ))
444 })
445 }
446 }
447 )+
448 };
449}
450
451impl_from_py_object!(
452 StepType,
453 StepSource,
454 StepStatus,
455 StepTarget,
456 ToolCallInfo,
457 ToolResult,
458 UsageMetadata,
459 MessageRole,
460 ConversationMessage,
461 Step
462);
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
473 fn test_step_type_roundtrip() {
474 for (variant, expected_str) in [
475 (StepType::TextResponse, "\"TEXT_RESPONSE\""),
476 (StepType::ToolCall, "\"TOOL_CALL\""),
477 (StepType::SystemMessage, "\"SYSTEM_MESSAGE\""),
478 (StepType::Compaction, "\"COMPACTION\""),
479 (StepType::Finish, "\"FINISH\""),
480 (StepType::Thinking, "\"THINKING\""),
481 (StepType::Unknown, "\"UNKNOWN\""),
482 ] {
483 let json = serde_json::to_string(&variant).unwrap();
484 assert_eq!(
485 json, expected_str,
486 "StepType serialization mismatch for {variant:?}"
487 );
488 let parsed: StepType = serde_json::from_str(&json).unwrap();
489 assert_eq!(parsed, variant);
490 }
491 }
492
493 #[test]
494 fn test_step_type_parse() {
495 assert_eq!(
496 "TEXT_RESPONSE".parse::<StepType>().unwrap(),
497 StepType::TextResponse
498 );
499 assert_eq!("TOOL_CALL".parse::<StepType>().unwrap(), StepType::ToolCall);
500 assert_eq!(
501 "SYSTEM_MESSAGE".parse::<StepType>().unwrap(),
502 StepType::SystemMessage
503 );
504 assert_eq!(
505 "COMPACTION".parse::<StepType>().unwrap(),
506 StepType::Compaction
507 );
508 assert_eq!("FINISH".parse::<StepType>().unwrap(), StepType::Finish);
509 assert_eq!("THINKING".parse::<StepType>().unwrap(), StepType::Thinking);
510 }
511
512 #[test]
513 fn test_step_source_roundtrip() {
514 for (variant, expected_str) in [
515 (StepSource::System, "\"SYSTEM\""),
516 (StepSource::User, "\"USER\""),
517 (StepSource::Model, "\"MODEL\""),
518 (StepSource::Unknown, "\"UNKNOWN\""),
519 ] {
520 let json = serde_json::to_string(&variant).unwrap();
521 assert_eq!(json, expected_str);
522 let parsed: StepSource = serde_json::from_str(&json).unwrap();
523 assert_eq!(parsed, variant);
524 }
525 }
526
527 #[test]
528 fn test_step_source_parse() {
529 assert_eq!("SYSTEM".parse::<StepSource>().unwrap(), StepSource::System);
530 assert_eq!("USER".parse::<StepSource>().unwrap(), StepSource::User);
531 assert_eq!("MODEL".parse::<StepSource>().unwrap(), StepSource::Model);
532 }
533
534 #[test]
535 fn test_step_status_roundtrip() {
536 for (variant, expected_str) in [
537 (StepStatus::Active, "\"ACTIVE\""),
538 (StepStatus::Done, "\"DONE\""),
539 (StepStatus::WaitingForUser, "\"WAITING_FOR_USER\""),
540 (StepStatus::Error, "\"ERROR\""),
541 (StepStatus::Canceled, "\"CANCELED\""),
542 (StepStatus::Unknown, "\"UNKNOWN\""),
543 ] {
544 let json = serde_json::to_string(&variant).unwrap();
545 assert_eq!(json, expected_str);
546 let parsed: StepStatus = serde_json::from_str(&json).unwrap();
547 assert_eq!(parsed, variant);
548 }
549 }
550
551 #[test]
552 fn test_step_status_parse() {
553 assert_eq!("ACTIVE".parse::<StepStatus>().unwrap(), StepStatus::Active);
554 assert_eq!("DONE".parse::<StepStatus>().unwrap(), StepStatus::Done);
555 assert_eq!(
556 "WAITING_FOR_USER".parse::<StepStatus>().unwrap(),
557 StepStatus::WaitingForUser
558 );
559 assert_eq!("ERROR".parse::<StepStatus>().unwrap(), StepStatus::Error);
560 assert_eq!(
561 "CANCELED".parse::<StepStatus>().unwrap(),
562 StepStatus::Canceled
563 );
564 }
565
566 #[test]
567 fn test_step_type_parse_returns_err_for_unrecognized() {
568 assert!("NONEXISTENT".parse::<StepType>().is_err());
569 }
570
571 #[test]
572 fn test_step_source_parse_returns_err_for_unrecognized() {
573 assert!("???".parse::<StepSource>().is_err());
574 }
575
576 #[test]
577 fn test_step_status_parse_returns_err_for_unrecognized() {
578 assert!("nope".parse::<StepStatus>().is_err());
579 }
580
581 #[test]
582 fn test_tool_call_info_roundtrip() {
583 let tc = ToolCallInfo {
584 name: "view_file".to_string(),
585 args: serde_json::json!({"path": "/tmp/foo.rs", "line": 42}),
586 id: Some("call_123".to_string()),
587 canonical_path: Some("/tmp/foo.rs".to_string()),
588 };
589 let json = serde_json::to_string(&tc).unwrap();
590 let parsed: ToolCallInfo = serde_json::from_str(&json).unwrap();
591 assert_eq!(parsed, tc);
592 }
593
594 #[test]
595 fn test_tool_call_info_minimal() {
596 let json = r#"{"name":"custom_tool"}"#;
597 let parsed: ToolCallInfo = serde_json::from_str(json).unwrap();
598 assert_eq!(parsed.name, "custom_tool");
599 assert_eq!(parsed.args, serde_json::Value::Null);
600 assert!(parsed.id.is_none());
601 assert!(parsed.canonical_path.is_none());
602 }
603
604 #[test]
605 fn test_tool_result_roundtrip() {
606 let tr = ToolResult {
607 name: "run_command".to_string(),
608 id: Some("result_456".to_string()),
609 result: serde_json::json!({"output": "hello world"}),
610 error: None,
611 };
612 let json = serde_json::to_string(&tr).unwrap();
613 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
614 assert_eq!(parsed, tr);
615 }
616
617 #[test]
618 fn test_tool_result_with_error() {
619 let tr = ToolResult {
620 name: "create_file".to_string(),
621 id: None,
622 result: serde_json::Value::Null,
623 error: Some("permission denied".to_string()),
624 };
625 let json = serde_json::to_string(&tr).unwrap();
626 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
627 assert_eq!(parsed.error.as_deref(), Some("permission denied"));
628 }
629
630 #[test]
631 fn test_usage_metadata_roundtrip() {
632 let um = UsageMetadata {
633 prompt_token_count: Some(100),
634 cached_content_token_count: Some(20),
635 candidates_token_count: Some(50),
636 thoughts_token_count: Some(30),
637 total_token_count: Some(180),
638 };
639 let json = serde_json::to_string(&um).unwrap();
640 let parsed: UsageMetadata = serde_json::from_str(&json).unwrap();
641 assert_eq!(parsed, um);
642 }
643
644 #[test]
645 fn test_usage_metadata_defaults() {
646 let um: UsageMetadata = serde_json::from_str("{}").unwrap();
647 assert!(um.prompt_token_count.is_none());
648 assert!(um.total_token_count.is_none());
649 }
650
651 #[test]
652 fn test_step_full_roundtrip() {
653 let step = Step {
654 id: "traj:0".to_string(),
655 step_index: 3,
656 trajectory_id: "traj-sub".to_string(),
657 cascade_id: "traj-root".to_string(),
658 step_type: StepType::ToolCall,
659 source: StepSource::Model,
660 target: StepTarget::Environment,
661 status: StepStatus::Done,
662 content: "Running command...".to_string(),
663 content_delta: "Running".to_string(),
664 thinking: "I should run the command".to_string(),
665 thinking_delta: "I should".to_string(),
666 tool_calls: vec![ToolCallInfo {
667 name: "run_command".to_string(),
668 args: serde_json::json!({"command": "ls -la"}),
669 id: Some("call_1".to_string()),
670 canonical_path: None,
671 }],
672 error: String::new(),
673 http_code: 0,
674 is_complete_response: Some(false),
675 structured_output: None,
676 usage_metadata: Some(UsageMetadata {
677 prompt_token_count: Some(500),
678 cached_content_token_count: None,
679 candidates_token_count: Some(100),
680 thoughts_token_count: Some(50),
681 total_token_count: Some(650),
682 }),
683 };
684
685 let json = serde_json::to_string_pretty(&step).unwrap();
686 let parsed: Step = serde_json::from_str(&json).unwrap();
687 assert_eq!(parsed, step);
688 assert_eq!(parsed.tool_calls.len(), 1);
689 assert_eq!(parsed.tool_calls[0].name, "run_command");
690 assert!(parsed.is_subagent_step());
692 }
693
694 #[test]
695 fn test_step_minimal_deserialization() {
696 let json = r#"{"id":"s1"}"#;
698 let step: Step = serde_json::from_str(json).unwrap();
699 assert_eq!(step.id, "s1");
700 assert_eq!(step.step_index, 0);
701 assert_eq!(step.step_type, StepType::Unknown);
702 assert_eq!(step.source, StepSource::Unknown);
703 assert_eq!(step.target, StepTarget::Unknown);
704 assert_eq!(step.status, StepStatus::Unknown);
705 assert!(step.content.is_empty());
706 assert!(step.content_delta.is_empty());
707 assert!(step.thinking.is_empty());
708 assert!(step.thinking_delta.is_empty());
709 assert!(step.tool_calls.is_empty());
710 assert!(step.error.is_empty());
711 assert!(step.is_complete_response.is_none());
712 assert!(step.structured_output.is_none());
713 assert!(step.usage_metadata.is_none());
714 assert!(step.trajectory_id.is_empty());
716 assert!(step.cascade_id.is_empty());
717 assert!(!step.is_subagent_step());
718 }
719
720 #[test]
721 fn is_subagent_step_matches_sdk_discrimination() {
722 let primary = Step {
724 trajectory_id: "traj-root".to_string(),
725 cascade_id: "traj-root".to_string(),
726 ..Step::default()
727 };
728 assert!(!primary.is_subagent_step());
729
730 let subagent = Step {
732 trajectory_id: "traj-child".to_string(),
733 cascade_id: "traj-root".to_string(),
734 ..Step::default()
735 };
736 assert!(subagent.is_subagent_step());
737
738 let no_cascade = Step {
743 trajectory_id: "traj-child".to_string(),
744 cascade_id: String::new(),
745 ..Step::default()
746 };
747 assert!(!no_cascade.is_subagent_step());
748 }
749
750 #[test]
751 fn step_deserializes_trajectory_and_cascade_ids() {
752 let json = r#"{"id":"s","trajectory_id":"t","cascade_id":"c"}"#;
755 let step: Step = serde_json::from_str(json).unwrap();
756 assert_eq!(step.trajectory_id, "t");
757 assert_eq!(step.cascade_id, "c");
758 assert!(step.is_subagent_step());
759 }
760
761 #[test]
766 fn step_with_multiple_tool_calls() {
767 let step = Step {
768 id: "multi-tc".to_string(),
769 step_index: 7,
770 trajectory_id: String::new(),
771 cascade_id: String::new(),
772 step_type: StepType::ToolCall,
773 source: StepSource::Model,
774 target: StepTarget::Environment,
775 status: StepStatus::Done,
776 content: String::new(),
777 content_delta: String::new(),
778 thinking: String::new(),
779 thinking_delta: String::new(),
780 tool_calls: vec![
781 ToolCallInfo {
782 name: "view_file".to_string(),
783 args: serde_json::json!({"path": "/a.rs"}),
784 id: Some("tc1".to_string()),
785 canonical_path: Some("/a.rs".to_string()),
786 },
787 ToolCallInfo {
788 name: "run_command".to_string(),
789 args: serde_json::json!({"command": "cargo test"}),
790 id: Some("tc2".to_string()),
791 canonical_path: None,
792 },
793 ],
794 error: String::new(),
795 http_code: 0,
796 is_complete_response: None,
797 structured_output: None,
798 usage_metadata: None,
799 };
800 let json = serde_json::to_string(&step).unwrap();
801 let parsed: Step = serde_json::from_str(&json).unwrap();
802 assert_eq!(parsed.tool_calls.len(), 2);
803 assert_eq!(parsed.tool_calls[0].name, "view_file");
804 assert_eq!(parsed.tool_calls[1].name, "run_command");
805 assert_eq!(
806 parsed.tool_calls[0].canonical_path.as_deref(),
807 Some("/a.rs")
808 );
809 assert!(parsed.tool_calls[1].canonical_path.is_none());
810 }
811
812 #[test]
817 fn tool_call_info_with_complex_args() {
818 let tc = ToolCallInfo {
819 name: "run_command".to_string(),
820 args: serde_json::json!({
821 "command": "cargo test",
822 "env": {"RUST_LOG": "debug"},
823 "timeout": 300,
824 "nested": [1, 2, {"deep": true}]
825 }),
826 id: None,
827 canonical_path: None,
828 };
829 let json = serde_json::to_string(&tc).unwrap();
830 let parsed: ToolCallInfo = serde_json::from_str(&json).unwrap();
831 assert_eq!(parsed.args["env"]["RUST_LOG"], "debug");
832 assert_eq!(parsed.args["nested"][2]["deep"], true);
833 }
834
835 #[test]
836 fn tool_result_with_complex_result() {
837 let tr = ToolResult {
838 name: "search_dir".to_string(),
839 id: Some("r1".to_string()),
840 result: serde_json::json!({
841 "matches": [
842 {"file": "/src/main.rs", "line": 42},
843 {"file": "/src/lib.rs", "line": 10},
844 ],
845 "total": 2
846 }),
847 error: None,
848 };
849 let json = serde_json::to_string(&tr).unwrap();
850 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
851 assert_eq!(parsed.result["total"], 2);
852 assert_eq!(parsed.result["matches"][0]["line"], 42);
853 }
854
855 #[test]
860 fn usage_metadata_partial_fields() {
861 let json = r#"{"prompt_token_count":100,"total_token_count":200}"#;
862 let um: UsageMetadata = serde_json::from_str(json).unwrap();
863 assert_eq!(um.prompt_token_count, Some(100));
864 assert!(um.cached_content_token_count.is_none());
865 assert!(um.candidates_token_count.is_none());
866 assert!(um.thoughts_token_count.is_none());
867 assert_eq!(um.total_token_count, Some(200));
868 }
869
870 #[test]
875 fn test_step_target_roundtrip() {
876 for (variant, expected_str) in [
877 (StepTarget::User, "\"TARGET_USER\""),
878 (StepTarget::Environment, "\"TARGET_ENVIRONMENT\""),
879 (StepTarget::Unspecified, "\"TARGET_UNSPECIFIED\""),
880 (StepTarget::Unknown, "\"UNKNOWN\""),
881 ] {
882 let json = serde_json::to_string(&variant).unwrap();
883 assert_eq!(
884 json, expected_str,
885 "StepTarget serialization mismatch for {variant:?}"
886 );
887 let parsed: StepTarget = serde_json::from_str(&json).unwrap();
888 assert_eq!(parsed, variant);
889 }
890 }
891
892 #[test]
893 fn test_step_target_parse() {
894 assert_eq!(
895 "TARGET_MODEL".parse::<StepTarget>().unwrap(),
896 StepTarget::Model
897 );
898 assert_eq!(
899 "TARGET_USER".parse::<StepTarget>().unwrap(),
900 StepTarget::User
901 );
902 assert_eq!(
903 "TARGET_ENVIRONMENT".parse::<StepTarget>().unwrap(),
904 StepTarget::Environment
905 );
906 assert_eq!(
907 "TARGET_UNSPECIFIED".parse::<StepTarget>().unwrap(),
908 StepTarget::Unspecified
909 );
910 assert_eq!(
911 "UNKNOWN".parse::<StepTarget>().unwrap(),
912 StepTarget::Unknown
913 );
914 }
915
916 #[test]
917 fn test_step_target_parse_returns_err_for_unrecognized() {
918 assert!("INVALID_TARGET".parse::<StepTarget>().is_err());
919 }
920
921 #[test]
926 fn test_step_type_display() {
927 assert_eq!(StepType::TextResponse.to_string(), "TEXT_RESPONSE");
928 assert_eq!(StepType::ToolCall.to_string(), "TOOL_CALL");
929 assert_eq!(StepType::SystemMessage.to_string(), "SYSTEM_MESSAGE");
930 assert_eq!(StepType::Compaction.to_string(), "COMPACTION");
931 assert_eq!(StepType::Finish.to_string(), "FINISH");
932 assert_eq!(StepType::Thinking.to_string(), "THINKING");
933 assert_eq!(StepType::Unknown.to_string(), "UNKNOWN");
934 }
935
936 #[test]
937 fn test_step_source_display() {
938 assert_eq!(StepSource::System.to_string(), "SYSTEM");
939 assert_eq!(StepSource::User.to_string(), "USER");
940 assert_eq!(StepSource::Model.to_string(), "MODEL");
941 assert_eq!(StepSource::Unknown.to_string(), "UNKNOWN");
942 }
943
944 #[test]
945 fn test_step_status_display() {
946 assert_eq!(StepStatus::Active.to_string(), "ACTIVE");
947 assert_eq!(StepStatus::Done.to_string(), "DONE");
948 assert_eq!(StepStatus::WaitingForUser.to_string(), "WAITING_FOR_USER");
949 assert_eq!(StepStatus::Error.to_string(), "ERROR");
950 assert_eq!(StepStatus::Canceled.to_string(), "CANCELED");
951 assert_eq!(StepStatus::Unknown.to_string(), "UNKNOWN");
952 }
953
954 #[test]
955 fn test_step_target_display() {
956 assert_eq!(StepTarget::User.to_string(), "TARGET_USER");
957 assert_eq!(StepTarget::Environment.to_string(), "TARGET_ENVIRONMENT");
958 assert_eq!(StepTarget::Unspecified.to_string(), "TARGET_UNSPECIFIED");
959 assert_eq!(StepTarget::Unknown.to_string(), "UNKNOWN");
960 }
961
962 #[test]
967 fn test_step_type_display_from_str_roundtrip() {
968 for variant in [
969 StepType::TextResponse,
970 StepType::ToolCall,
971 StepType::SystemMessage,
972 StepType::Compaction,
973 StepType::Finish,
974 StepType::Thinking,
975 StepType::Unknown,
976 ] {
977 let s = variant.to_string();
978 let parsed: StepType = s.parse().unwrap();
979 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
980 }
981 }
982
983 #[test]
984 fn test_step_source_display_from_str_roundtrip() {
985 for variant in [
986 StepSource::System,
987 StepSource::User,
988 StepSource::Model,
989 StepSource::Unknown,
990 ] {
991 let s = variant.to_string();
992 let parsed: StepSource = s.parse().unwrap();
993 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
994 }
995 }
996
997 #[test]
998 fn test_step_status_display_from_str_roundtrip() {
999 for variant in [
1000 StepStatus::Active,
1001 StepStatus::Done,
1002 StepStatus::WaitingForUser,
1003 StepStatus::Error,
1004 StepStatus::Canceled,
1005 StepStatus::Unknown,
1006 ] {
1007 let s = variant.to_string();
1008 let parsed: StepStatus = s.parse().unwrap();
1009 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
1010 }
1011 }
1012
1013 #[test]
1014 fn test_step_target_display_from_str_roundtrip() {
1015 for variant in [
1016 StepTarget::Model,
1017 StepTarget::User,
1018 StepTarget::Environment,
1019 StepTarget::Unspecified,
1020 StepTarget::Unknown,
1021 ] {
1022 let s = variant.to_string();
1023 let parsed: StepTarget = s.parse().unwrap();
1024 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
1025 }
1026 }
1027
1028 #[test]
1033 fn test_from_str_garbage_returns_err() {
1034 assert!("xyzzy".parse::<StepType>().is_err());
1035 assert!("xyzzy".parse::<StepSource>().is_err());
1036 assert!("xyzzy".parse::<StepStatus>().is_err());
1037 assert!("xyzzy".parse::<StepTarget>().is_err());
1038 }
1039
1040 #[test]
1041 fn test_from_str_empty_returns_err() {
1042 assert!("".parse::<StepType>().is_err());
1043 assert!("".parse::<StepSource>().is_err());
1044 assert!("".parse::<StepStatus>().is_err());
1045 assert!("".parse::<StepTarget>().is_err());
1046 }
1047
1048 #[test]
1049 fn test_from_str_case_sensitive() {
1050 assert!("text_response".parse::<StepType>().is_err());
1052 assert!("system".parse::<StepSource>().is_err());
1053 assert!("active".parse::<StepStatus>().is_err());
1054 assert!("target_user".parse::<StepTarget>().is_err());
1055 }
1056
1057 #[test]
1062 fn test_message_role_roundtrip() {
1063 for (variant, expected_str) in [
1064 (MessageRole::User, "\"user\""),
1065 (MessageRole::Model, "\"model\""),
1066 (MessageRole::System, "\"system\""),
1067 (MessageRole::Unknown("custom".to_string()), "\"custom\""),
1068 ] {
1069 let json = serde_json::to_string(&variant).unwrap();
1070 assert_eq!(json, expected_str);
1071 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1072 assert_eq!(parsed, variant);
1073 }
1074 }
1075
1076 #[test]
1077 fn test_conversation_message_roundtrip() {
1078 let msg = ConversationMessage {
1079 role: MessageRole::Model,
1080 content: "Hello!".to_string(),
1081 };
1082 let json = serde_json::to_string(&msg).unwrap();
1083 let parsed: ConversationMessage = serde_json::from_str(&json).unwrap();
1084 assert_eq!(parsed, msg);
1085 }
1086
1087 #[test]
1092 fn test_pyo3_extract_roundtrip() {
1093 use pyo3::{prelude::*, types::PyDictMethods};
1094 pyo3::Python::initialize();
1095 pyo3::Python::attach(|py| {
1096 let dict = pyo3::types::PyDict::new(py);
1097 dict.set_item("id", "step-1").unwrap();
1098 dict.set_item("step_index", 42).unwrap();
1099 dict.set_item("type", "TEXT_RESPONSE").unwrap();
1100 dict.set_item("source", "MODEL").unwrap();
1101 dict.set_item("target", "TARGET_USER").unwrap();
1102 dict.set_item("status", "DONE").unwrap();
1103
1104 let step: Step = dict.extract().expect("failed to extract Step");
1105 assert_eq!(step.id, "step-1");
1106 assert_eq!(step.step_index, 42);
1107 assert_eq!(step.step_type, StepType::TextResponse);
1108 assert_eq!(step.source, StepSource::Model);
1109 assert_eq!(step.target, StepTarget::User);
1110 assert_eq!(step.status, StepStatus::Done);
1111
1112 let s = pyo3::types::PyString::new(py, "SYSTEM_MESSAGE");
1114 let st: StepType = s.extract().unwrap();
1115 assert_eq!(st, StepType::SystemMessage);
1116 });
1117 }
1118
1119 #[test]
1124 fn step_with_deltas_and_thinking() {
1125 let step = Step {
1126 id: "s2".to_string(),
1127 step_index: 1,
1128 trajectory_id: String::new(),
1129 cascade_id: String::new(),
1130 step_type: StepType::TextResponse,
1131 source: StepSource::Model,
1132 target: StepTarget::User,
1133 status: StepStatus::Active,
1134 content: "Hello world".to_string(),
1135 content_delta: "world".to_string(),
1136 thinking: "The user said hi".to_string(),
1137 thinking_delta: "said hi".to_string(),
1138 tool_calls: vec![],
1139 error: String::new(),
1140 http_code: 0,
1141 is_complete_response: Some(true),
1142 structured_output: None,
1143 usage_metadata: None,
1144 };
1145 let json = serde_json::to_string(&step).unwrap();
1146 let parsed: Step = serde_json::from_str(&json).unwrap();
1147 assert_eq!(parsed.content_delta, "world");
1148 assert_eq!(parsed.thinking, "The user said hi");
1149 assert_eq!(parsed.thinking_delta, "said hi");
1150 assert_eq!(parsed.is_complete_response, Some(true));
1151 assert_eq!(parsed.target, StepTarget::User);
1152 }
1153
1154 #[test]
1155 fn step_with_structured_output() {
1156 let payload = serde_json::json!({"answer": 42, "valid": true});
1157 let step = Step {
1158 id: "finish-1".to_string(),
1159 step_index: 5,
1160 trajectory_id: String::new(),
1161 cascade_id: String::new(),
1162 step_type: StepType::Finish,
1163 source: StepSource::Model,
1164 target: StepTarget::User,
1165 status: StepStatus::Done,
1166 content: String::new(),
1167 content_delta: String::new(),
1168 thinking: String::new(),
1169 thinking_delta: String::new(),
1170 tool_calls: vec![],
1171 error: String::new(),
1172 http_code: 0,
1173 is_complete_response: Some(true),
1174 structured_output: Some(payload.clone()),
1175 usage_metadata: None,
1176 };
1177 let json = serde_json::to_string(&step).unwrap();
1178 let parsed: Step = serde_json::from_str(&json).unwrap();
1179 assert_eq!(parsed.structured_output, Some(payload));
1180 assert_eq!(parsed.step_type, StepType::Finish);
1181 }
1182}