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 #[default]
142 Unknown => "UNKNOWN",
143 }
144}
145
146define_sdk_enum! {
147 StepSource {
149 System => "SYSTEM",
151 User => "USER",
153 Model => "MODEL",
155 #[default]
157 Unknown => "UNKNOWN",
158 }
159}
160
161define_sdk_enum! {
162 StepStatus {
164 Active => "ACTIVE",
166 Done => "DONE",
168 WaitingForUser => "WAITING_FOR_USER",
170 Error => "ERROR",
172 Canceled => "CANCELED",
174 #[default]
176 Unknown => "UNKNOWN",
177 }
178}
179
180define_sdk_enum_custom_serde! {
181 StepTarget {
187 Model => "TARGET_MODEL",
189 User => "TARGET_USER",
191 Environment => "TARGET_ENVIRONMENT",
193 Unspecified => "TARGET_UNSPECIFIED",
195 #[default]
197 Unknown => "UNKNOWN",
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct ToolCallInfo {
204 pub name: String,
206 #[serde(default)]
208 pub args: serde_json::Value,
209 #[serde(default)]
211 pub id: Option<String>,
212 #[serde(default)]
214 pub canonical_path: Option<String>,
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct ToolResult {
220 pub name: String,
222 #[serde(default)]
224 pub id: Option<String>,
225 #[serde(default)]
227 pub result: serde_json::Value,
228 #[serde(default)]
230 pub error: Option<String>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
235pub struct UsageMetadata {
236 #[serde(default)]
238 pub prompt_token_count: Option<u64>,
239 #[serde(default)]
241 pub cached_content_token_count: Option<u64>,
242 #[serde(default)]
244 pub candidates_token_count: Option<u64>,
245 #[serde(default)]
247 pub thoughts_token_count: Option<u64>,
248 #[serde(default)]
250 pub total_token_count: Option<u64>,
251}
252
253#[non_exhaustive]
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "lowercase")]
257#[derive(Default)]
258pub enum MessageRole {
259 #[default]
261 User,
262 Model,
264 System,
266 #[serde(untagged)]
269 Unknown(String),
270}
271
272impl std::fmt::Display for MessageRole {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 match self {
275 Self::User => f.write_str("user"),
276 Self::Model => f.write_str("model"),
277 Self::System => f.write_str("system"),
278 Self::Unknown(s) => f.write_str(s),
279 }
280 }
281}
282
283#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
288pub struct ConversationMessage {
289 #[serde(default)]
291 pub role: MessageRole,
292 #[serde(default)]
294 pub content: String,
295}
296
297#[non_exhaustive]
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, TypedBuilder)]
319#[builder(field_defaults(default))]
320pub struct Step {
321 #[serde(default)]
323 #[builder(setter(into))]
324 pub id: String,
325 #[serde(default)]
327 pub step_index: u32,
328 #[serde(default, rename = "type")]
330 pub step_type: StepType,
331 #[serde(default)]
333 pub source: StepSource,
334 #[serde(default)]
336 pub target: StepTarget,
337 #[serde(default)]
339 pub status: StepStatus,
340 #[serde(default)]
342 #[builder(setter(into))]
343 pub content: String,
344 #[serde(default)]
346 #[builder(setter(into))]
347 pub content_delta: String,
348 #[serde(default)]
350 #[builder(setter(into))]
351 pub thinking: String,
352 #[serde(default)]
354 #[builder(setter(into))]
355 pub thinking_delta: String,
356 #[serde(default)]
358 #[builder(setter(transform = |v: impl IntoIterator<Item = impl Into<ToolCallInfo>>| v.into_iter().map(Into::into).collect()))]
359 pub tool_calls: Vec<ToolCallInfo>,
360 #[serde(default)]
362 #[builder(setter(into))]
363 pub error: String,
364 #[serde(default)]
369 pub http_code: u16,
370 #[serde(default)]
375 #[builder(setter(strip_option))]
376 pub is_complete_response: Option<bool>,
377 #[serde(default)]
382 #[builder(setter(strip_option))]
383 pub structured_output: Option<serde_json::Value>,
384 #[serde(default)]
386 #[builder(setter(strip_option))]
387 pub usage_metadata: Option<UsageMetadata>,
388}
389
390macro_rules! impl_from_py_object {
391 ($($t:ty),+) => {
392 $(
393 impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for $t {
394 type Error = pyo3::PyErr;
395
396 fn extract(ob: pyo3::Borrowed<'a, 'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
397 crate::runtime::py_scripts::warm_up_lazy_imports(ob.py());
398 pythonize::depythonize(&*ob).map_err(|e| {
399 pyo3::exceptions::PyValueError::new_err(format!(
400 "Failed to deserialize {} from Python dict: {}",
401 stringify!($t),
402 e
403 ))
404 })
405 }
406 }
407 )+
408 };
409}
410
411impl_from_py_object!(
412 StepType,
413 StepSource,
414 StepStatus,
415 StepTarget,
416 ToolCallInfo,
417 ToolResult,
418 UsageMetadata,
419 MessageRole,
420 ConversationMessage,
421 Step
422);
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
433 fn test_step_type_roundtrip() {
434 for (variant, expected_str) in [
435 (StepType::TextResponse, "\"TEXT_RESPONSE\""),
436 (StepType::ToolCall, "\"TOOL_CALL\""),
437 (StepType::SystemMessage, "\"SYSTEM_MESSAGE\""),
438 (StepType::Compaction, "\"COMPACTION\""),
439 (StepType::Finish, "\"FINISH\""),
440 (StepType::Unknown, "\"UNKNOWN\""),
441 ] {
442 let json = serde_json::to_string(&variant).unwrap();
443 assert_eq!(
444 json, expected_str,
445 "StepType serialization mismatch for {variant:?}"
446 );
447 let parsed: StepType = serde_json::from_str(&json).unwrap();
448 assert_eq!(parsed, variant);
449 }
450 }
451
452 #[test]
453 fn test_step_type_parse() {
454 assert_eq!(
455 "TEXT_RESPONSE".parse::<StepType>().unwrap(),
456 StepType::TextResponse
457 );
458 assert_eq!("TOOL_CALL".parse::<StepType>().unwrap(), StepType::ToolCall);
459 assert_eq!(
460 "SYSTEM_MESSAGE".parse::<StepType>().unwrap(),
461 StepType::SystemMessage
462 );
463 assert_eq!(
464 "COMPACTION".parse::<StepType>().unwrap(),
465 StepType::Compaction
466 );
467 assert_eq!("FINISH".parse::<StepType>().unwrap(), StepType::Finish);
468 }
469
470 #[test]
471 fn test_step_source_roundtrip() {
472 for (variant, expected_str) in [
473 (StepSource::System, "\"SYSTEM\""),
474 (StepSource::User, "\"USER\""),
475 (StepSource::Model, "\"MODEL\""),
476 (StepSource::Unknown, "\"UNKNOWN\""),
477 ] {
478 let json = serde_json::to_string(&variant).unwrap();
479 assert_eq!(json, expected_str);
480 let parsed: StepSource = serde_json::from_str(&json).unwrap();
481 assert_eq!(parsed, variant);
482 }
483 }
484
485 #[test]
486 fn test_step_source_parse() {
487 assert_eq!("SYSTEM".parse::<StepSource>().unwrap(), StepSource::System);
488 assert_eq!("USER".parse::<StepSource>().unwrap(), StepSource::User);
489 assert_eq!("MODEL".parse::<StepSource>().unwrap(), StepSource::Model);
490 }
491
492 #[test]
493 fn test_step_status_roundtrip() {
494 for (variant, expected_str) in [
495 (StepStatus::Active, "\"ACTIVE\""),
496 (StepStatus::Done, "\"DONE\""),
497 (StepStatus::WaitingForUser, "\"WAITING_FOR_USER\""),
498 (StepStatus::Error, "\"ERROR\""),
499 (StepStatus::Canceled, "\"CANCELED\""),
500 (StepStatus::Unknown, "\"UNKNOWN\""),
501 ] {
502 let json = serde_json::to_string(&variant).unwrap();
503 assert_eq!(json, expected_str);
504 let parsed: StepStatus = serde_json::from_str(&json).unwrap();
505 assert_eq!(parsed, variant);
506 }
507 }
508
509 #[test]
510 fn test_step_status_parse() {
511 assert_eq!("ACTIVE".parse::<StepStatus>().unwrap(), StepStatus::Active);
512 assert_eq!("DONE".parse::<StepStatus>().unwrap(), StepStatus::Done);
513 assert_eq!(
514 "WAITING_FOR_USER".parse::<StepStatus>().unwrap(),
515 StepStatus::WaitingForUser
516 );
517 assert_eq!("ERROR".parse::<StepStatus>().unwrap(), StepStatus::Error);
518 assert_eq!(
519 "CANCELED".parse::<StepStatus>().unwrap(),
520 StepStatus::Canceled
521 );
522 }
523
524 #[test]
525 fn test_step_type_parse_returns_err_for_unrecognized() {
526 assert!("NONEXISTENT".parse::<StepType>().is_err());
527 }
528
529 #[test]
530 fn test_step_source_parse_returns_err_for_unrecognized() {
531 assert!("???".parse::<StepSource>().is_err());
532 }
533
534 #[test]
535 fn test_step_status_parse_returns_err_for_unrecognized() {
536 assert!("nope".parse::<StepStatus>().is_err());
537 }
538
539 #[test]
540 fn test_tool_call_info_roundtrip() {
541 let tc = ToolCallInfo {
542 name: "view_file".to_string(),
543 args: serde_json::json!({"path": "/tmp/foo.rs", "line": 42}),
544 id: Some("call_123".to_string()),
545 canonical_path: Some("/tmp/foo.rs".to_string()),
546 };
547 let json = serde_json::to_string(&tc).unwrap();
548 let parsed: ToolCallInfo = serde_json::from_str(&json).unwrap();
549 assert_eq!(parsed, tc);
550 }
551
552 #[test]
553 fn test_tool_call_info_minimal() {
554 let json = r#"{"name":"custom_tool"}"#;
555 let parsed: ToolCallInfo = serde_json::from_str(json).unwrap();
556 assert_eq!(parsed.name, "custom_tool");
557 assert_eq!(parsed.args, serde_json::Value::Null);
558 assert!(parsed.id.is_none());
559 assert!(parsed.canonical_path.is_none());
560 }
561
562 #[test]
563 fn test_tool_result_roundtrip() {
564 let tr = ToolResult {
565 name: "run_command".to_string(),
566 id: Some("result_456".to_string()),
567 result: serde_json::json!({"output": "hello world"}),
568 error: None,
569 };
570 let json = serde_json::to_string(&tr).unwrap();
571 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
572 assert_eq!(parsed, tr);
573 }
574
575 #[test]
576 fn test_tool_result_with_error() {
577 let tr = ToolResult {
578 name: "create_file".to_string(),
579 id: None,
580 result: serde_json::Value::Null,
581 error: Some("permission denied".to_string()),
582 };
583 let json = serde_json::to_string(&tr).unwrap();
584 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
585 assert_eq!(parsed.error.as_deref(), Some("permission denied"));
586 }
587
588 #[test]
589 fn test_usage_metadata_roundtrip() {
590 let um = UsageMetadata {
591 prompt_token_count: Some(100),
592 cached_content_token_count: Some(20),
593 candidates_token_count: Some(50),
594 thoughts_token_count: Some(30),
595 total_token_count: Some(180),
596 };
597 let json = serde_json::to_string(&um).unwrap();
598 let parsed: UsageMetadata = serde_json::from_str(&json).unwrap();
599 assert_eq!(parsed, um);
600 }
601
602 #[test]
603 fn test_usage_metadata_defaults() {
604 let um: UsageMetadata = serde_json::from_str("{}").unwrap();
605 assert!(um.prompt_token_count.is_none());
606 assert!(um.total_token_count.is_none());
607 }
608
609 #[test]
610 fn test_step_full_roundtrip() {
611 let step = Step {
612 id: "traj:0".to_string(),
613 step_index: 3,
614 step_type: StepType::ToolCall,
615 source: StepSource::Model,
616 target: StepTarget::Environment,
617 status: StepStatus::Done,
618 content: "Running command...".to_string(),
619 content_delta: "Running".to_string(),
620 thinking: "I should run the command".to_string(),
621 thinking_delta: "I should".to_string(),
622 tool_calls: vec![ToolCallInfo {
623 name: "run_command".to_string(),
624 args: serde_json::json!({"command": "ls -la"}),
625 id: Some("call_1".to_string()),
626 canonical_path: None,
627 }],
628 error: String::new(),
629 http_code: 0,
630 is_complete_response: Some(false),
631 structured_output: None,
632 usage_metadata: Some(UsageMetadata {
633 prompt_token_count: Some(500),
634 cached_content_token_count: None,
635 candidates_token_count: Some(100),
636 thoughts_token_count: Some(50),
637 total_token_count: Some(650),
638 }),
639 };
640
641 let json = serde_json::to_string_pretty(&step).unwrap();
642 let parsed: Step = serde_json::from_str(&json).unwrap();
643 assert_eq!(parsed, step);
644 assert_eq!(parsed.tool_calls.len(), 1);
645 assert_eq!(parsed.tool_calls[0].name, "run_command");
646 }
647
648 #[test]
649 fn test_step_minimal_deserialization() {
650 let json = r#"{"id":"s1"}"#;
652 let step: Step = serde_json::from_str(json).unwrap();
653 assert_eq!(step.id, "s1");
654 assert_eq!(step.step_index, 0);
655 assert_eq!(step.step_type, StepType::Unknown);
656 assert_eq!(step.source, StepSource::Unknown);
657 assert_eq!(step.target, StepTarget::Unknown);
658 assert_eq!(step.status, StepStatus::Unknown);
659 assert!(step.content.is_empty());
660 assert!(step.content_delta.is_empty());
661 assert!(step.thinking.is_empty());
662 assert!(step.thinking_delta.is_empty());
663 assert!(step.tool_calls.is_empty());
664 assert!(step.error.is_empty());
665 assert!(step.is_complete_response.is_none());
666 assert!(step.structured_output.is_none());
667 assert!(step.usage_metadata.is_none());
668 }
669
670 #[test]
675 fn step_with_multiple_tool_calls() {
676 let step = Step {
677 id: "multi-tc".to_string(),
678 step_index: 7,
679 step_type: StepType::ToolCall,
680 source: StepSource::Model,
681 target: StepTarget::Environment,
682 status: StepStatus::Done,
683 content: String::new(),
684 content_delta: String::new(),
685 thinking: String::new(),
686 thinking_delta: String::new(),
687 tool_calls: vec![
688 ToolCallInfo {
689 name: "view_file".to_string(),
690 args: serde_json::json!({"path": "/a.rs"}),
691 id: Some("tc1".to_string()),
692 canonical_path: Some("/a.rs".to_string()),
693 },
694 ToolCallInfo {
695 name: "run_command".to_string(),
696 args: serde_json::json!({"command": "cargo test"}),
697 id: Some("tc2".to_string()),
698 canonical_path: None,
699 },
700 ],
701 error: String::new(),
702 http_code: 0,
703 is_complete_response: None,
704 structured_output: None,
705 usage_metadata: None,
706 };
707 let json = serde_json::to_string(&step).unwrap();
708 let parsed: Step = serde_json::from_str(&json).unwrap();
709 assert_eq!(parsed.tool_calls.len(), 2);
710 assert_eq!(parsed.tool_calls[0].name, "view_file");
711 assert_eq!(parsed.tool_calls[1].name, "run_command");
712 assert_eq!(
713 parsed.tool_calls[0].canonical_path.as_deref(),
714 Some("/a.rs")
715 );
716 assert!(parsed.tool_calls[1].canonical_path.is_none());
717 }
718
719 #[test]
724 fn tool_call_info_with_complex_args() {
725 let tc = ToolCallInfo {
726 name: "run_command".to_string(),
727 args: serde_json::json!({
728 "command": "cargo test",
729 "env": {"RUST_LOG": "debug"},
730 "timeout": 300,
731 "nested": [1, 2, {"deep": true}]
732 }),
733 id: None,
734 canonical_path: None,
735 };
736 let json = serde_json::to_string(&tc).unwrap();
737 let parsed: ToolCallInfo = serde_json::from_str(&json).unwrap();
738 assert_eq!(parsed.args["env"]["RUST_LOG"], "debug");
739 assert_eq!(parsed.args["nested"][2]["deep"], true);
740 }
741
742 #[test]
743 fn tool_result_with_complex_result() {
744 let tr = ToolResult {
745 name: "search_dir".to_string(),
746 id: Some("r1".to_string()),
747 result: serde_json::json!({
748 "matches": [
749 {"file": "/src/main.rs", "line": 42},
750 {"file": "/src/lib.rs", "line": 10},
751 ],
752 "total": 2
753 }),
754 error: None,
755 };
756 let json = serde_json::to_string(&tr).unwrap();
757 let parsed: ToolResult = serde_json::from_str(&json).unwrap();
758 assert_eq!(parsed.result["total"], 2);
759 assert_eq!(parsed.result["matches"][0]["line"], 42);
760 }
761
762 #[test]
767 fn usage_metadata_partial_fields() {
768 let json = r#"{"prompt_token_count":100,"total_token_count":200}"#;
769 let um: UsageMetadata = serde_json::from_str(json).unwrap();
770 assert_eq!(um.prompt_token_count, Some(100));
771 assert!(um.cached_content_token_count.is_none());
772 assert!(um.candidates_token_count.is_none());
773 assert!(um.thoughts_token_count.is_none());
774 assert_eq!(um.total_token_count, Some(200));
775 }
776
777 #[test]
782 fn test_step_target_roundtrip() {
783 for (variant, expected_str) in [
784 (StepTarget::User, "\"TARGET_USER\""),
785 (StepTarget::Environment, "\"TARGET_ENVIRONMENT\""),
786 (StepTarget::Unspecified, "\"TARGET_UNSPECIFIED\""),
787 (StepTarget::Unknown, "\"UNKNOWN\""),
788 ] {
789 let json = serde_json::to_string(&variant).unwrap();
790 assert_eq!(
791 json, expected_str,
792 "StepTarget serialization mismatch for {variant:?}"
793 );
794 let parsed: StepTarget = serde_json::from_str(&json).unwrap();
795 assert_eq!(parsed, variant);
796 }
797 }
798
799 #[test]
800 fn test_step_target_parse() {
801 assert_eq!(
802 "TARGET_MODEL".parse::<StepTarget>().unwrap(),
803 StepTarget::Model
804 );
805 assert_eq!(
806 "TARGET_USER".parse::<StepTarget>().unwrap(),
807 StepTarget::User
808 );
809 assert_eq!(
810 "TARGET_ENVIRONMENT".parse::<StepTarget>().unwrap(),
811 StepTarget::Environment
812 );
813 assert_eq!(
814 "TARGET_UNSPECIFIED".parse::<StepTarget>().unwrap(),
815 StepTarget::Unspecified
816 );
817 assert_eq!(
818 "UNKNOWN".parse::<StepTarget>().unwrap(),
819 StepTarget::Unknown
820 );
821 }
822
823 #[test]
824 fn test_step_target_parse_returns_err_for_unrecognized() {
825 assert!("INVALID_TARGET".parse::<StepTarget>().is_err());
826 }
827
828 #[test]
833 fn test_step_type_display() {
834 assert_eq!(StepType::TextResponse.to_string(), "TEXT_RESPONSE");
835 assert_eq!(StepType::ToolCall.to_string(), "TOOL_CALL");
836 assert_eq!(StepType::SystemMessage.to_string(), "SYSTEM_MESSAGE");
837 assert_eq!(StepType::Compaction.to_string(), "COMPACTION");
838 assert_eq!(StepType::Finish.to_string(), "FINISH");
839 assert_eq!(StepType::Unknown.to_string(), "UNKNOWN");
840 }
841
842 #[test]
843 fn test_step_source_display() {
844 assert_eq!(StepSource::System.to_string(), "SYSTEM");
845 assert_eq!(StepSource::User.to_string(), "USER");
846 assert_eq!(StepSource::Model.to_string(), "MODEL");
847 assert_eq!(StepSource::Unknown.to_string(), "UNKNOWN");
848 }
849
850 #[test]
851 fn test_step_status_display() {
852 assert_eq!(StepStatus::Active.to_string(), "ACTIVE");
853 assert_eq!(StepStatus::Done.to_string(), "DONE");
854 assert_eq!(StepStatus::WaitingForUser.to_string(), "WAITING_FOR_USER");
855 assert_eq!(StepStatus::Error.to_string(), "ERROR");
856 assert_eq!(StepStatus::Canceled.to_string(), "CANCELED");
857 assert_eq!(StepStatus::Unknown.to_string(), "UNKNOWN");
858 }
859
860 #[test]
861 fn test_step_target_display() {
862 assert_eq!(StepTarget::User.to_string(), "TARGET_USER");
863 assert_eq!(StepTarget::Environment.to_string(), "TARGET_ENVIRONMENT");
864 assert_eq!(StepTarget::Unspecified.to_string(), "TARGET_UNSPECIFIED");
865 assert_eq!(StepTarget::Unknown.to_string(), "UNKNOWN");
866 }
867
868 #[test]
873 fn test_step_type_display_from_str_roundtrip() {
874 for variant in [
875 StepType::TextResponse,
876 StepType::ToolCall,
877 StepType::SystemMessage,
878 StepType::Compaction,
879 StepType::Finish,
880 StepType::Unknown,
881 ] {
882 let s = variant.to_string();
883 let parsed: StepType = s.parse().unwrap();
884 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
885 }
886 }
887
888 #[test]
889 fn test_step_source_display_from_str_roundtrip() {
890 for variant in [
891 StepSource::System,
892 StepSource::User,
893 StepSource::Model,
894 StepSource::Unknown,
895 ] {
896 let s = variant.to_string();
897 let parsed: StepSource = s.parse().unwrap();
898 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
899 }
900 }
901
902 #[test]
903 fn test_step_status_display_from_str_roundtrip() {
904 for variant in [
905 StepStatus::Active,
906 StepStatus::Done,
907 StepStatus::WaitingForUser,
908 StepStatus::Error,
909 StepStatus::Canceled,
910 StepStatus::Unknown,
911 ] {
912 let s = variant.to_string();
913 let parsed: StepStatus = s.parse().unwrap();
914 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
915 }
916 }
917
918 #[test]
919 fn test_step_target_display_from_str_roundtrip() {
920 for variant in [
921 StepTarget::Model,
922 StepTarget::User,
923 StepTarget::Environment,
924 StepTarget::Unspecified,
925 StepTarget::Unknown,
926 ] {
927 let s = variant.to_string();
928 let parsed: StepTarget = s.parse().unwrap();
929 assert_eq!(parsed, variant, "roundtrip failed for {variant:?}");
930 }
931 }
932
933 #[test]
938 fn test_from_str_garbage_returns_err() {
939 assert!("xyzzy".parse::<StepType>().is_err());
940 assert!("xyzzy".parse::<StepSource>().is_err());
941 assert!("xyzzy".parse::<StepStatus>().is_err());
942 assert!("xyzzy".parse::<StepTarget>().is_err());
943 }
944
945 #[test]
946 fn test_from_str_empty_returns_err() {
947 assert!("".parse::<StepType>().is_err());
948 assert!("".parse::<StepSource>().is_err());
949 assert!("".parse::<StepStatus>().is_err());
950 assert!("".parse::<StepTarget>().is_err());
951 }
952
953 #[test]
954 fn test_from_str_case_sensitive() {
955 assert!("text_response".parse::<StepType>().is_err());
957 assert!("system".parse::<StepSource>().is_err());
958 assert!("active".parse::<StepStatus>().is_err());
959 assert!("target_user".parse::<StepTarget>().is_err());
960 }
961
962 #[test]
967 fn test_message_role_roundtrip() {
968 for (variant, expected_str) in [
969 (MessageRole::User, "\"user\""),
970 (MessageRole::Model, "\"model\""),
971 (MessageRole::System, "\"system\""),
972 (MessageRole::Unknown("custom".to_string()), "\"custom\""),
973 ] {
974 let json = serde_json::to_string(&variant).unwrap();
975 assert_eq!(json, expected_str);
976 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
977 assert_eq!(parsed, variant);
978 }
979 }
980
981 #[test]
982 fn test_conversation_message_roundtrip() {
983 let msg = ConversationMessage {
984 role: MessageRole::Model,
985 content: "Hello!".to_string(),
986 };
987 let json = serde_json::to_string(&msg).unwrap();
988 let parsed: ConversationMessage = serde_json::from_str(&json).unwrap();
989 assert_eq!(parsed, msg);
990 }
991
992 #[test]
997 fn test_pyo3_extract_roundtrip() {
998 use pyo3::{prelude::*, types::PyDictMethods};
999 pyo3::Python::initialize();
1000 pyo3::Python::attach(|py| {
1001 let dict = pyo3::types::PyDict::new(py);
1002 dict.set_item("id", "step-1").unwrap();
1003 dict.set_item("step_index", 42).unwrap();
1004 dict.set_item("type", "TEXT_RESPONSE").unwrap();
1005 dict.set_item("source", "MODEL").unwrap();
1006 dict.set_item("target", "TARGET_USER").unwrap();
1007 dict.set_item("status", "DONE").unwrap();
1008
1009 let step: Step = dict.extract().expect("failed to extract Step");
1010 assert_eq!(step.id, "step-1");
1011 assert_eq!(step.step_index, 42);
1012 assert_eq!(step.step_type, StepType::TextResponse);
1013 assert_eq!(step.source, StepSource::Model);
1014 assert_eq!(step.target, StepTarget::User);
1015 assert_eq!(step.status, StepStatus::Done);
1016
1017 let s = pyo3::types::PyString::new(py, "SYSTEM_MESSAGE");
1019 let st: StepType = s.extract().unwrap();
1020 assert_eq!(st, StepType::SystemMessage);
1021 });
1022 }
1023
1024 #[test]
1029 fn step_with_deltas_and_thinking() {
1030 let step = Step {
1031 id: "s2".to_string(),
1032 step_index: 1,
1033 step_type: StepType::TextResponse,
1034 source: StepSource::Model,
1035 target: StepTarget::User,
1036 status: StepStatus::Active,
1037 content: "Hello world".to_string(),
1038 content_delta: "world".to_string(),
1039 thinking: "The user said hi".to_string(),
1040 thinking_delta: "said hi".to_string(),
1041 tool_calls: vec![],
1042 error: String::new(),
1043 http_code: 0,
1044 is_complete_response: Some(true),
1045 structured_output: None,
1046 usage_metadata: None,
1047 };
1048 let json = serde_json::to_string(&step).unwrap();
1049 let parsed: Step = serde_json::from_str(&json).unwrap();
1050 assert_eq!(parsed.content_delta, "world");
1051 assert_eq!(parsed.thinking, "The user said hi");
1052 assert_eq!(parsed.thinking_delta, "said hi");
1053 assert_eq!(parsed.is_complete_response, Some(true));
1054 assert_eq!(parsed.target, StepTarget::User);
1055 }
1056
1057 #[test]
1058 fn step_with_structured_output() {
1059 let payload = serde_json::json!({"answer": 42, "valid": true});
1060 let step = Step {
1061 id: "finish-1".to_string(),
1062 step_index: 5,
1063 step_type: StepType::Finish,
1064 source: StepSource::Model,
1065 target: StepTarget::User,
1066 status: StepStatus::Done,
1067 content: String::new(),
1068 content_delta: String::new(),
1069 thinking: String::new(),
1070 thinking_delta: String::new(),
1071 tool_calls: vec![],
1072 error: String::new(),
1073 http_code: 0,
1074 is_complete_response: Some(true),
1075 structured_output: Some(payload.clone()),
1076 usage_metadata: None,
1077 };
1078 let json = serde_json::to_string(&step).unwrap();
1079 let parsed: Step = serde_json::from_str(&json).unwrap();
1080 assert_eq!(parsed.structured_output, Some(payload));
1081 assert_eq!(parsed.step_type, StepType::Finish);
1082 }
1083}