Skip to main content

lash_sansio/
session.rs

1use crate::{AttachmentRef, MediaType, ToolCallRecord};
2
3/// An image emitted by a trajectory step.
4///
5/// Printed images deliberately live outside the provider-attachment source
6/// seams, while sharing their validated [`MediaType`] vocabulary.
7#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub struct ExecImage {
9    pub mime: MediaType,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub reference: Option<AttachmentRef>,
12    #[serde(default, skip_serializing_if = "Vec::is_empty")]
13    pub data: Vec<u8>,
14    pub label: String,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub width: Option<u32>,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub height: Option<u32>,
19}
20
21#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
22pub struct TextProjectionMetadata {
23    pub truncated: bool,
24    pub original_chars: usize,
25    pub projected_chars: usize,
26    pub original_lines: usize,
27    pub projected_lines: usize,
28    pub limit: usize,
29    pub limit_mode: String,
30    pub max_lines: usize,
31}
32
33#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
34pub struct ExecResponse {
35    pub observations: Vec<String>,
36    pub observation_truncation: Vec<TextProjectionMetadata>,
37    pub tool_calls: Vec<ToolCallRecord>,
38    pub images: Vec<ExecImage>,
39    pub printed_images: Vec<AttachmentRef>,
40    pub error: Option<String>,
41    pub duration_ms: u64,
42    /// When the surrounding session uses protocol-specific finish behavior,
43    /// this carries the protocol's terminal value. The dispatch loop uses it
44    /// as the terminal result of the session. `None` for chat-style sessions
45    /// and for typed sessions whose step continued without finishing.
46    pub terminal_finish: Option<serde_json::Value>,
47}
48
49/// Exact prompt-usage snapshot from the most recent completed LLM call.
50#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
51pub struct PromptUsage {
52    pub prompt_context_tokens: usize,
53    pub input_tokens: usize,
54    pub cache_read_input_tokens: usize,
55    pub cache_write_input_tokens: usize,
56    pub context_budget_tokens: usize,
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn exec_image_media_type_round_trips_as_the_existing_string_wire_shape() {
65        let image = ExecImage {
66            mime: MediaType::parse("image/webp").unwrap(),
67            reference: None,
68            data: vec![1, 2, 3],
69            label: "plot".to_string(),
70            width: Some(320),
71            height: Some(180),
72        };
73
74        let value = serde_json::to_value(&image).unwrap();
75        assert_eq!(value["mime"], "image/webp");
76        assert_eq!(serde_json::from_value::<ExecImage>(value).unwrap(), image);
77    }
78
79    #[test]
80    fn exec_image_rejects_an_invalid_media_type_on_deserialization() {
81        let error = serde_json::from_value::<ExecImage>(serde_json::json!({
82            "mime": "not-a-mime",
83            "data": [],
84            "label": "plot"
85        }))
86        .unwrap_err();
87
88        assert!(error.to_string().contains("invalid media type"));
89    }
90}