Skip to main content

lash_sansio/
session.rs

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