Skip to main content

agy_bridge/
types.rs

1//! Core types for the agent SDK bridge.
2//!
3//! This module defines the data structures that model an agent's execution
4//! trajectory: individual [`Step`](crate::types::Step)s,
5//! [`ToolCallInfo`](crate::types::ToolCallInfo) requests,
6//! [`ToolResult`](crate::types::ToolResult) responses, and
7//! [`UsageMetadata`](crate::types::UsageMetadata) for token accounting. All types derive
8//! `Serialize`/`Deserialize` for JSON interchange with the Python SDK.
9
10use std::{fmt, str::FromStr};
11
12use serde::{Deserialize, Serialize};
13use typed_builder::TypedBuilder;
14
15// =============================================================================
16// Step / ToolCall / ToolResult types (§1.6)
17// =============================================================================
18
19/// Define an SDK enum with `SCREAMING_SNAKE_CASE` serde rename and auto-generated
20/// `Display` and `FromStr` impls.
21///
22/// Each variant maps to a wire-format string. Unrecognized strings parse as `Err`
23/// via `FromStr` — they never panic.
24///
25/// # Syntax
26///
27/// ```text
28/// define_sdk_enum! {
29///     /// Doc comment for the enum.
30///     EnumName {
31///         Variant1 => "WIRE_STRING_1",
32///         Variant2 => "WIRE_STRING_2",
33///         #[default]
34///         Unknown => "UNKNOWN",
35///     }
36/// }
37/// ```
38macro_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
80/// Like [`define_sdk_enum!`] but for enums where the serde wire format uses
81/// per-variant `#[serde(rename = "...")]` instead of `rename_all`.
82///
83/// This is needed for [`StepTarget`] whose SDK strings have a `TARGET_` prefix
84/// that doesn't match the `SCREAMING_SNAKE_CASE` of the enum name.
85macro_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    /// The high-level type of a step in the agent trajectory.
129    StepType {
130        /// A textual response from the model.
131        TextResponse => "TEXT_RESPONSE",
132        /// A tool invocation requested by the model.
133        ToolCall => "TOOL_CALL",
134        /// A system-generated message (e.g. context injection).
135        SystemMessage => "SYSTEM_MESSAGE",
136        /// A context-window compaction event.
137        Compaction => "COMPACTION",
138        /// The agent has signaled task completion.
139        Finish => "FINISH",
140        /// Unrecognized step type (forward-compatibility fallback).
141        #[default]
142        Unknown => "UNKNOWN",
143    }
144}
145
146define_sdk_enum! {
147    /// The source that generated a step.
148    StepSource {
149        /// Generated by the system runtime.
150        System => "SYSTEM",
151        /// Provided by the user.
152        User => "USER",
153        /// Generated by the model.
154        Model => "MODEL",
155        /// Unrecognized source (forward-compatibility fallback).
156        #[default]
157        Unknown => "UNKNOWN",
158    }
159}
160
161define_sdk_enum! {
162    /// The execution status of a step.
163    StepStatus {
164        /// Step is currently executing.
165        Active => "ACTIVE",
166        /// Step completed successfully.
167        Done => "DONE",
168        /// Step is blocked waiting for user input.
169        WaitingForUser => "WAITING_FOR_USER",
170        /// Step failed with an error.
171        Error => "ERROR",
172        /// Step was canceled before completion.
173        Canceled => "CANCELED",
174        /// Unrecognized status (forward-compatibility fallback).
175        #[default]
176        Unknown => "UNKNOWN",
177    }
178}
179
180define_sdk_enum_custom_serde! {
181    /// Target of a step interaction, mirroring the Python SDK's `StepTarget`.
182    ///
183    /// The Python SDK uses `TARGET_` prefixed strings (e.g. `TARGET_USER`).
184    /// Uses per-variant `#[serde(rename)]` because the SDK's wire format has a
185    /// `TARGET_` prefix that doesn't follow `SCREAMING_SNAKE_CASE` of the enum name.
186    StepTarget {
187        /// Step is directed at the model.
188        Model => "TARGET_MODEL",
189        /// Step is directed at the user.
190        User => "TARGET_USER",
191        /// Step is directed at the environment (tool execution).
192        Environment => "TARGET_ENVIRONMENT",
193        /// Target is unspecified.
194        Unspecified => "TARGET_UNSPECIFIED",
195        /// Unknown target (fallback).
196        #[default]
197        Unknown => "UNKNOWN",
198    }
199}
200
201/// A tool call from the model, mirroring the Python SDK's `ToolCall`.
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct ToolCallInfo {
204    /// Tool name — either a `BuiltinTools` string or a custom tool name.
205    pub name: String,
206    /// Arguments as a JSON value (typically an object/dict).
207    #[serde(default)]
208    pub args: serde_json::Value,
209    /// Optional unique identifier for the call.
210    #[serde(default)]
211    pub id: Option<String>,
212    /// Optional normalized filesystem path for file-related tools.
213    #[serde(default)]
214    pub canonical_path: Option<String>,
215}
216
217/// Result of a single tool execution, mirroring the Python SDK's `ToolResult`.
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct ToolResult {
220    /// The name of the tool that was executed.
221    pub name: String,
222    /// Optional identifier correlating this result with a `ToolCallInfo.id`.
223    #[serde(default)]
224    pub id: Option<String>,
225    /// The tool's return value (any JSON-serializable value).
226    #[serde(default)]
227    pub result: serde_json::Value,
228    /// An error message if execution failed, or `None` on success.
229    #[serde(default)]
230    pub error: Option<String>,
231}
232
233/// Token usage metadata from the model API, mirroring the SDK's `UsageMetadata`.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
235pub struct UsageMetadata {
236    /// Number of tokens in the prompt.
237    #[serde(default)]
238    pub prompt_token_count: Option<u64>,
239    /// Number of tokens from cached content (subset of prompt tokens).
240    #[serde(default)]
241    pub cached_content_token_count: Option<u64>,
242    /// Number of tokens in the generated candidates (excluding thinking).
243    #[serde(default)]
244    pub candidates_token_count: Option<u64>,
245    /// Number of tokens used for thinking/reasoning.
246    #[serde(default)]
247    pub thoughts_token_count: Option<u64>,
248    /// Sum of prompt + candidates + thinking tokens.
249    #[serde(default)]
250    pub total_token_count: Option<u64>,
251}
252
253/// The role of a message author in the conversation.
254#[non_exhaustive]
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "lowercase")]
257#[derive(Default)]
258pub enum MessageRole {
259    /// A user-authored message.
260    #[default]
261    User,
262    /// A model-generated message.
263    Model,
264    /// A system-level message.
265    System,
266    /// An unrecognized role — preserves the original string for forward
267    /// compatibility with new SDK roles.
268    #[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/// A single message in the conversation history, mirroring the Python SDK's
284/// `ConversationMessage`.
285///
286/// Each message has a [`MessageRole`] and textual `content`.
287#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
288pub struct ConversationMessage {
289    /// The role of the message author.
290    #[serde(default)]
291    pub role: MessageRole,
292    /// The textual content of the message.
293    #[serde(default)]
294    pub content: String,
295}
296
297/// A single step in the agent trajectory, mirroring the SDK's `Step`.
298///
299/// # Construction
300///
301/// `Step` is `#[non_exhaustive]`, so outside this crate it can only be built
302/// with the [`TypedBuilder`]. Every field defaults (matching [`Default`]), so
303/// callers set only the fields they care about:
304///
305/// ```
306/// use agy_bridge::Step;
307///
308/// let step = Step::builder()
309///     .id("traj:0")
310///     .content("Running command...")
311///     .build();
312/// assert_eq!(step.id, "traj:0");
313/// // Unset fields fall back to their defaults.
314/// assert_eq!(step.step_index, 0);
315/// assert!(step.tool_calls.is_empty());
316/// ```
317#[non_exhaustive]
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, TypedBuilder)]
319#[builder(field_defaults(default))]
320pub struct Step {
321    /// Unique string identifier for the step.
322    #[serde(default)]
323    #[builder(setter(into))]
324    pub id: String,
325    /// Integer index of the step in the trajectory.
326    #[serde(default)]
327    pub step_index: u32,
328    /// The high-level type of the step.
329    #[serde(default, rename = "type")]
330    pub step_type: StepType,
331    /// The source that generated the step.
332    #[serde(default)]
333    pub source: StepSource,
334    /// The target of the step interaction.
335    #[serde(default)]
336    pub target: StepTarget,
337    /// The status of the step.
338    #[serde(default)]
339    pub status: StepStatus,
340    /// The text content/output of the step.
341    #[serde(default)]
342    #[builder(setter(into))]
343    pub content: String,
344    /// Incremental text content added since the last update for this step.
345    #[serde(default)]
346    #[builder(setter(into))]
347    pub content_delta: String,
348    /// Full model reasoning/thinking text for planner responses.
349    #[serde(default)]
350    #[builder(setter(into))]
351    pub thinking: String,
352    /// Incremental thinking text added since the last update for this step.
353    #[serde(default)]
354    #[builder(setter(into))]
355    pub thinking_delta: String,
356    /// List of tool calls associated with the step.
357    #[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    /// Short error message if the step failed.
361    #[serde(default)]
362    #[builder(setter(into))]
363    pub error: String,
364    /// HTTP status code from the harness error, if any (e.g. 400, 429, 503).
365    ///
366    /// The SDK populates this from the harness's `error.http_code` field.
367    /// Used by error detection in `forward_step_to_writer` for logging.
368    #[serde(default)]
369    pub http_code: u16,
370    /// Whether this step is a completed model response directed at the user.
371    ///
372    /// Multiple steps per turn may have this flag set; consumers wanting only
373    /// the last response should iterate fully.
374    #[serde(default)]
375    #[builder(setter(strip_option))]
376    pub is_complete_response: Option<bool>,
377    /// Structured output payload extracted from the FINISH step.
378    ///
379    /// This is `serde_json::Value` because it contains user-defined schema data
380    /// whose shape is not known at compile time.
381    #[serde(default)]
382    #[builder(setter(strip_option))]
383    pub structured_output: Option<serde_json::Value>,
384    /// Token usage for this step's model invocation.
385    #[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    // =========================================================================
429    // Step / ToolCall / ToolResult tests
430    // =========================================================================
431
432    #[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        // Should deserialize with all defaults.
651        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    // =========================================================================
671    // Step with multiple tool calls
672    // =========================================================================
673
674    #[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    // =========================================================================
720    // ToolCallInfo / ToolResult edge cases
721    // =========================================================================
722
723    #[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    // =========================================================================
763    // UsageMetadata partial fields
764    // =========================================================================
765
766    #[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    // =========================================================================
778    // StepTarget tests
779    // =========================================================================
780
781    #[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    // =========================================================================
829    // Display trait tests
830    // =========================================================================
831
832    #[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    // =========================================================================
869    // Display → FromStr roundtrip tests
870    // =========================================================================
871
872    #[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    // =========================================================================
934    // FromStr with garbage input tests
935    // =========================================================================
936
937    #[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        // SDK strings are case-sensitive — lowercase should return Err.
956        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    // =========================================================================
963    // MessageRole / ConversationMessage Tests
964    // =========================================================================
965
966    #[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    // =========================================================================
993    // PyO3 Extract Tests
994    // =========================================================================
995
996    #[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            // Now test an enum
1018            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    // =========================================================================
1025    // Step new fields tests
1026    // =========================================================================
1027
1028    #[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}