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 pub terminal_finish: Option<serde_json::Value>,
43}
44
45#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
47pub struct PromptUsage {
48 pub prompt_context_tokens: usize,
49 pub input_tokens: usize,
50 pub cached_input_tokens: usize,
51 #[serde(default)]
52 pub context_budget_tokens: usize,
53}
54
55#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
57pub struct SansIoSessionState {
58 pub session_id: String,
59 #[serde(default)]
60 pub messages: Vec<crate::Message>,
61 #[serde(default)]
62 pub protocol_iteration: usize,
63 #[serde(default)]
64 pub token_usage: crate::TokenUsage,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub last_prompt_usage: Option<PromptUsage>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub protocol_state: Option<serde_json::Value>,
69}
70
71#[derive(Clone, Debug, Default)]
72pub struct CompletedTurn {
73 pub messages: Vec<crate::Message>,
74 pub tool_calls: Vec<ToolCallRecord>,
75 pub protocol_iteration: usize,
76 pub token_usage: crate::TokenUsage,
77 pub last_prompt_usage: Option<PromptUsage>,
78 pub protocol_state: Option<serde_json::Value>,
79}
80
81pub fn apply_completed_turn(
82 mut state: SansIoSessionState,
83 turn: CompletedTurn,
84) -> SansIoSessionState {
85 state.messages = turn.messages;
86 state.protocol_iteration = turn.protocol_iteration;
87 state.token_usage = turn.token_usage;
88 state.last_prompt_usage = turn.last_prompt_usage;
89 state.protocol_state = turn.protocol_state;
90 state
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn completed_turn_replaces_projected_session_state() {
99 let state = SansIoSessionState {
100 session_id: "session".to_string(),
101 protocol_iteration: 1,
102 ..SansIoSessionState::default()
103 };
104 let reduced = apply_completed_turn(
105 state,
106 CompletedTurn {
107 protocol_iteration: 4,
108 token_usage: crate::TokenUsage {
109 input_tokens: 10,
110 output_tokens: 3,
111 cached_input_tokens: 1,
112 reasoning_tokens: 2,
113 },
114 last_prompt_usage: Some(PromptUsage {
115 prompt_context_tokens: 7,
116 input_tokens: 6,
117 cached_input_tokens: 1,
118 context_budget_tokens: 100,
119 }),
120 ..CompletedTurn::default()
121 },
122 );
123
124 assert_eq!(reduced.protocol_iteration, 4);
125 assert_eq!(reduced.token_usage.input_tokens, 10);
126 assert_eq!(
127 reduced
128 .last_prompt_usage
129 .expect("prompt usage present")
130 .prompt_context_tokens,
131 7
132 );
133 }
134}