Skip to main content

lash_sansio/
session.rs

1use crate::{AttachmentRef, ToolCallRecord};
2
3#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
4pub struct ExecImage {
5    pub mime: String,
6    #[serde(default, skip_serializing_if = "Option::is_none")]
7    pub reference: Option<AttachmentRef>,
8    #[serde(default, skip_serializing_if = "Vec::is_empty")]
9    pub data: Vec<u8>,
10    pub label: String,
11    #[serde(default, skip_serializing_if = "Option::is_none")]
12    pub width: Option<u32>,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub height: Option<u32>,
15}
16
17#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
18pub struct TextProjectionMetadata {
19    pub truncated: bool,
20    pub original_chars: usize,
21    pub projected_chars: usize,
22    pub original_lines: usize,
23    pub projected_lines: usize,
24    pub limit: usize,
25    pub limit_mode: String,
26    pub max_lines: usize,
27}
28
29#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
30pub struct ExecResponse {
31    pub observations: Vec<String>,
32    pub observation_truncation: Vec<TextProjectionMetadata>,
33    pub tool_calls: Vec<ToolCallRecord>,
34    pub images: Vec<ExecImage>,
35    pub printed_images: Vec<AttachmentRef>,
36    pub error: Option<String>,
37    pub duration_ms: u64,
38    /// When the surrounding session uses protocol-specific finish behavior,
39    /// this carries the value the lashlang program ended with via
40    /// `submit <expr>`. The dispatch loop uses it as the terminal
41    /// result of the session. `None` for chat-style sessions and for
42    /// typed sessions whose step continued without finishing.
43    pub terminal_finish: Option<serde_json::Value>,
44}
45
46/// Exact prompt-usage snapshot from the most recent completed LLM call.
47#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
48pub struct PromptUsage {
49    pub prompt_context_tokens: usize,
50    pub input_tokens: usize,
51    pub cached_input_tokens: usize,
52    #[serde(default)]
53    pub context_budget_tokens: usize,
54}
55
56/// Pure multi-turn session state for hosts that want lash behavior without the runtime.
57#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
58pub struct SansIoSessionState {
59    pub session_id: String,
60    #[serde(default)]
61    pub messages: Vec<crate::Message>,
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub tool_calls: Vec<ToolCallRecord>,
64    #[serde(default)]
65    pub protocol_iteration: usize,
66    #[serde(default)]
67    pub token_usage: crate::TokenUsage,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub last_prompt_usage: Option<PromptUsage>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub protocol_state: Option<serde_json::Value>,
72}
73
74#[derive(Clone, Debug, Default)]
75pub struct CompletedTurn {
76    pub messages: Vec<crate::Message>,
77    pub tool_calls: Vec<ToolCallRecord>,
78    pub protocol_iteration: usize,
79    pub token_usage: crate::TokenUsage,
80    pub last_prompt_usage: Option<PromptUsage>,
81    pub protocol_state: Option<serde_json::Value>,
82}
83
84pub fn apply_completed_turn(
85    mut state: SansIoSessionState,
86    turn: CompletedTurn,
87) -> SansIoSessionState {
88    state.messages = turn.messages;
89    state.tool_calls = turn.tool_calls;
90    state.protocol_iteration = turn.protocol_iteration;
91    state.token_usage = turn.token_usage;
92    state.last_prompt_usage = turn.last_prompt_usage;
93    state.protocol_state = turn.protocol_state;
94    state
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn completed_turn_replaces_projected_session_state() {
103        let state = SansIoSessionState {
104            session_id: "session".to_string(),
105            protocol_iteration: 1,
106            ..SansIoSessionState::default()
107        };
108        let reduced = apply_completed_turn(
109            state,
110            CompletedTurn {
111                protocol_iteration: 4,
112                token_usage: crate::TokenUsage {
113                    input_tokens: 10,
114                    output_tokens: 3,
115                    cached_input_tokens: 1,
116                    reasoning_tokens: 2,
117                },
118                last_prompt_usage: Some(PromptUsage {
119                    prompt_context_tokens: 7,
120                    input_tokens: 6,
121                    cached_input_tokens: 1,
122                    context_budget_tokens: 100,
123                }),
124                ..CompletedTurn::default()
125            },
126        );
127
128        assert_eq!(reduced.protocol_iteration, 4);
129        assert_eq!(reduced.token_usage.input_tokens, 10);
130        assert_eq!(
131            reduced
132                .last_prompt_usage
133                .expect("prompt usage present")
134                .prompt_context_tokens,
135            7
136        );
137    }
138}