1use std::path::PathBuf;
7
8use serde::{Deserialize, Serialize};
9use tokio::sync::mpsc;
10
11use crate::message::AgentEvent;
12use crate::message::Message;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum SessionOp {
18 Submit { message: String },
20 PreviewRequest { message: String },
22 SetSkillContext {
28 name: Option<String>,
30 content: Option<String>,
32 },
33 Interrupt,
35 Shutdown,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(tag = "type", rename_all = "snake_case")]
42#[non_exhaustive]
43pub enum SessionEvent {
44 AgentEvent {
46 event: AgentEvent,
48 },
49 ApprovalRequired {
51 tool_name: String,
52 arguments: String,
53 call_id: String,
54 },
55 TurnStarted { turn_id: String },
57 TurnCompleted {
59 turn_id: String,
60 status: TurnCompletionStatus,
61 },
62 Error { message: String },
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(tag = "status", rename_all = "snake_case")]
69pub enum TurnCompletionStatus {
70 Success {
72 #[serde(default)]
74 final_text: String,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 new_messages: Vec<crate::message::Message>,
79 },
80 Cancelled,
82 Error {
84 message: String,
86 },
87}
88
89pub struct SessionHandle {
93 pub sq_tx: mpsc::Sender<SessionOp>,
95 pub eq_rx: mpsc::UnboundedReceiver<SessionEvent>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SessionConfig {
104 #[serde(default)]
106 pub runtime_policy: RuntimePolicy,
107 pub workspace_root: PathBuf,
109 #[serde(default)]
111 pub initial_history: Vec<Message>,
112 #[serde(default = "default_model_context_limit")]
114 pub model_context_limit: u32,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(default)]
120pub struct RuntimePolicy {
121 pub approval_mode: ApprovalMode,
124}
125
126impl RuntimePolicy {
127 #[must_use]
129 pub fn interactive() -> Self {
130 Self {
131 approval_mode: ApprovalMode::Interactive,
132 }
133 }
134
135 #[must_use]
137 pub fn headless_deny() -> Self {
138 Self {
139 approval_mode: ApprovalMode::HeadlessDeny,
140 }
141 }
142}
143
144impl Default for RuntimePolicy {
145 fn default() -> Self {
146 Self::interactive()
147 }
148}
149
150#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum ApprovalMode {
154 #[default]
156 Interactive,
157 HeadlessDeny,
159}
160
161fn default_model_context_limit() -> u32 {
162 128_000
163}
164
165#[cfg(test)]
166#[allow(warnings)]
167#[allow(warnings)]
168#[allow(warnings)]
169#[allow(warnings)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn session_op_serde_roundtrip() {
175 let ops = vec![
176 SessionOp::Submit {
177 message: "hello".into(),
178 },
179 SessionOp::PreviewRequest {
180 message: "diagnostic".into(),
181 },
182 SessionOp::Interrupt,
183 SessionOp::Shutdown,
184 ];
185 for op in &ops {
186 let json = serde_json::to_string(op).unwrap();
187 let back: SessionOp = serde_json::from_str(&json).unwrap();
188 assert_eq!(
189 serde_json::to_value(op).unwrap(),
190 serde_json::to_value(&back).unwrap()
191 );
192 }
193 }
194
195 #[test]
196 fn session_event_serde_roundtrip() {
197 let events = vec![
198 SessionEvent::AgentEvent {
199 event: AgentEvent::TextDelta {
200 delta: "hello".into(),
201 },
202 },
203 SessionEvent::ApprovalRequired {
204 tool_name: "write".into(),
205 arguments: "{}".into(),
206 call_id: "call_1".into(),
207 },
208 SessionEvent::TurnStarted {
209 turn_id: "1".into(),
210 },
211 SessionEvent::TurnCompleted {
212 turn_id: "1".into(),
213 status: TurnCompletionStatus::Success {
214 final_text: String::new(),
215 new_messages: vec![],
216 },
217 },
218 SessionEvent::TurnCompleted {
219 turn_id: "2".into(),
220 status: TurnCompletionStatus::Cancelled,
221 },
222 SessionEvent::TurnCompleted {
223 turn_id: "3".into(),
224 status: TurnCompletionStatus::Error {
225 message: "boom".into(),
226 },
227 },
228 SessionEvent::Error {
229 message: "fail".into(),
230 },
231 ];
232 for event in &events {
233 let json = serde_json::to_string(event).unwrap();
234 let back: SessionEvent = serde_json::from_str(&json).unwrap();
235 assert_eq!(
236 serde_json::to_value(event).unwrap(),
237 serde_json::to_value(&back).unwrap()
238 );
239 }
240 }
241
242 #[test]
243 fn session_config_serde_roundtrip() {
244 let config = SessionConfig {
245 runtime_policy: RuntimePolicy::headless_deny(),
246 workspace_root: PathBuf::from("/tmp/test"),
247 initial_history: vec![],
248 model_context_limit: 128_000,
249 };
250 let json = serde_json::to_string(&config).unwrap();
251 let back: SessionConfig = serde_json::from_str(&json).unwrap();
252 assert_eq!(config.runtime_policy, back.runtime_policy);
253 assert_eq!(config.workspace_root, back.workspace_root);
254 assert_eq!(config.initial_history, back.initial_history);
255 assert_eq!(config.model_context_limit, back.model_context_limit);
256 }
257
258 #[test]
259 fn session_config_defaults_to_interactive_runtime_policy() {
260 let json = r#"{
261 "workspace_root": "/tmp/test",
262 "initial_history": [],
263 "model_context_limit": 128000
264 }"#;
265 let back: SessionConfig = serde_json::from_str(json).unwrap();
266 assert_eq!(back.runtime_policy, RuntimePolicy::interactive());
267 }
268}