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