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 pub terminal_finish: Option<serde_json::Value>,
31}
32
33#[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#[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}