Skip to main content

wt/agent/
types.rs

1//! Normalized code-agent results and the JSON shapes emitted by agents'
2//! output modes (issue #11).
3
4use serde::{Deserialize, Serialize};
5
6use crate::agent::spec::AgentKind;
7
8/// A code-agent CLI detected on `PATH`, with its resolved version.
9#[derive(Debug, Clone, Serialize)]
10pub struct DetectedAgent {
11    /// Which agent was detected.
12    pub kind: AgentKind,
13    /// The binary name found on `PATH`.
14    pub binary: String,
15    /// The parsed version information.
16    pub version: AgentVersion,
17}
18
19/// A parsed `--version` result: a best-effort version number plus the raw line.
20#[derive(Debug, Clone, Serialize)]
21pub struct AgentVersion {
22    /// Best-effort `MAJOR.MINOR[.PATCH]` extracted from the output, or `None`
23    /// if no version-shaped token was found.
24    pub version: Option<String>,
25    /// The raw first line of `--version` output, trimmed.
26    pub raw: String,
27}
28
29/// A normalized end-to-end run result, independent of which agent produced it.
30#[derive(Debug, Clone, Serialize)]
31pub struct AgentRun {
32    /// Which agent produced this result.
33    pub kind: AgentKind,
34    /// Whether the agent reported an error.
35    pub is_error: bool,
36    /// The agent's final textual result.
37    pub result: String,
38    /// The raw JSON value the agent emitted, preserved for agent-specific
39    /// fields not in the normalized shape.
40    pub raw: serde_json::Value,
41}
42
43/// The JSON shape of `claude -p --output-format json`. Only the normalized
44/// fields are named; everything else rides along in [`AgentRun::raw`].
45#[derive(Debug, Clone, Deserialize)]
46pub struct ClaudeResult {
47    /// The agent's final result text.
48    #[serde(default)]
49    pub result: String,
50    /// Whether the agent flagged an error.
51    #[serde(default)]
52    pub is_error: bool,
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn deserializes_claude_result() {
61        let json = r#"{"type":"result","is_error":false,"result":"hello","total_cost_usd":0.01}"#;
62        let parsed: ClaudeResult = serde_json::from_str(json).unwrap();
63        assert!(!parsed.is_error);
64        assert_eq!(parsed.result, "hello");
65    }
66
67    #[test]
68    fn claude_result_defaults_missing_fields() {
69        let parsed: ClaudeResult = serde_json::from_str("{}").unwrap();
70        assert!(!parsed.is_error);
71        assert_eq!(parsed.result, "");
72    }
73
74    #[test]
75    fn agent_run_serializes_to_json() {
76        let run = AgentRun {
77            kind: AgentKind::Claude,
78            is_error: false,
79            result: "hi".into(),
80            raw: serde_json::json!({"result": "hi"}),
81        };
82        let serialized = serde_json::to_string(&run).unwrap();
83        assert!(serialized.contains("\"kind\":\"claude\""));
84        assert!(serialized.contains("\"result\":\"hi\""));
85        assert!(serialized.contains("\"is_error\":false"));
86    }
87
88    #[test]
89    fn detected_agent_serializes() {
90        let detected = DetectedAgent {
91            kind: AgentKind::Claude,
92            binary: "claude".into(),
93            version: AgentVersion {
94                version: Some("1.2.3".into()),
95                raw: "1.2.3 (Claude Code)".into(),
96            },
97        };
98        let serialized = serde_json::to_string(&detected).unwrap();
99        assert!(serialized.contains("\"binary\":\"claude\""));
100        assert!(serialized.contains("\"version\":\"1.2.3\""));
101    }
102}