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, 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}