1use std::time::Instant;
16
17use machi_agent::Agent;
18use machi_llm::LlmSampler;
19use machi_state::{ChatPersistence, ChatStateHandle};
20use machi_types::MachiError;
21
22use crate::metrics::{MetricsSink, NoopMetrics, record_turn};
23use crate::state::{ConversationState, VecConversationState};
24use crate::turn::{TurnInput, TurnOptions, TurnOutcome, TurnRuntime};
25
26#[derive(Debug, Clone, Copy)]
28pub struct Session {
29 runtime: TurnRuntime,
30 turn_count: u64,
31}
32
33impl Default for Session {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl Session {
40 #[must_use]
42 pub const fn new() -> Self {
43 Self {
44 runtime: TurnRuntime::new(),
45 turn_count: 0,
46 }
47 }
48
49 #[must_use]
51 pub const fn turn_count(self) -> u64 {
52 self.turn_count
53 }
54
55 pub async fn run_turn(
61 &mut self,
62 agent: &Agent,
63 sampler: &dyn LlmSampler,
64 state: &mut dyn ConversationState,
65 input: TurnInput,
66 options: TurnOptions,
67 ) -> Result<TurnOutcome, MachiError> {
68 self.run_turn_with_metrics(agent, sampler, state, input, options, &NoopMetrics)
69 .await
70 }
71
72 pub async fn run_turn_with_metrics(
78 &mut self,
79 agent: &Agent,
80 sampler: &dyn LlmSampler,
81 state: &mut dyn ConversationState,
82 input: TurnInput,
83 options: TurnOptions,
84 metrics: &dyn MetricsSink,
85 ) -> Result<TurnOutcome, MachiError> {
86 let started = Instant::now();
87 let outcome = self
88 .runtime
89 .run(agent, sampler, state, input, options)
90 .await;
91 let ms = started.elapsed().as_secs_f64() * 1000.0;
92 match &outcome {
93 Ok(o) => {
94 self.turn_count = self.turn_count.saturating_add(1);
95 let status = if o.cancelled { "cancelled" } else { "ok" };
96 let steps = u64::try_from(o.steps).unwrap_or(u64::MAX);
97 record_turn(metrics, status, steps, ms);
98 }
99 Err(_) => {
100 record_turn(metrics, "error", 0, ms);
101 }
102 }
103 outcome
104 }
105
106 pub async fn run_turn_on_handle(
115 &mut self,
116 agent: &Agent,
117 sampler: &dyn LlmSampler,
118 handle: &ChatStateHandle,
119 input: TurnInput,
120 options: TurnOptions,
121 ) -> Result<TurnOutcome, MachiError> {
122 self.run_turn_on_handle_with_metrics(agent, sampler, handle, input, options, &NoopMetrics)
123 .await
124 }
125
126 pub async fn run_turn_on_handle_with_metrics(
132 &mut self,
133 agent: &Agent,
134 sampler: &dyn LlmSampler,
135 handle: &ChatStateHandle,
136 input: TurnInput,
137 options: TurnOptions,
138 metrics: &dyn MetricsSink,
139 ) -> Result<TurnOutcome, MachiError> {
140 self.run_turn_on_handle_checkpointed(agent, sampler, handle, input, options, metrics, None)
141 .await
142 }
143
144 #[allow(
153 clippy::too_many_arguments,
154 reason = "session turn needs agent, sampler, handle, I/O, metrics, optional store"
155 )]
156 pub async fn run_turn_on_handle_checkpointed(
157 &mut self,
158 agent: &Agent,
159 sampler: &dyn LlmSampler,
160 handle: &ChatStateHandle,
161 input: TurnInput,
162 options: TurnOptions,
163 metrics: &dyn MetricsSink,
164 store: Option<&dyn ChatPersistence>,
165 ) -> Result<TurnOutcome, MachiError> {
166 let snap = handle.snapshot().await;
167 let mut local = VecConversationState::from_messages(snap.messages);
168 let outcome = self
169 .run_turn_with_metrics(agent, sampler, &mut local, input, options, metrics)
170 .await;
171 handle.replace(local.messages().to_vec()).await;
173 if let Ok(o) = &outcome {
174 handle.record_main_usage(o.usage).await;
175 if o.cancelled {
176 handle.mark_incomplete().await;
177 }
178 } else {
179 handle.mark_incomplete().await;
180 }
181 if let Some(store) = store {
182 handle.save_to(store).await?;
183 }
184 outcome
185 }
186
187 #[allow(
195 clippy::too_many_arguments,
196 reason = "open+turn packs agent, sampler, store, input, options, metrics"
197 )]
198 pub async fn open_checkpointed_turn(
199 &mut self,
200 agent: &Agent,
201 sampler: &dyn LlmSampler,
202 store: &dyn ChatPersistence,
203 input: TurnInput,
204 options: TurnOptions,
205 metrics: &dyn MetricsSink,
206 ) -> Result<(ChatStateHandle, TurnOutcome), MachiError> {
207 let handle = ChatStateHandle::open_or_new(store).await?;
208 let outcome = self
209 .run_turn_on_handle_checkpointed(
210 agent,
211 sampler,
212 &handle,
213 input,
214 options,
215 metrics,
216 Some(store),
217 )
218 .await?;
219 Ok((handle, outcome))
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use std::sync::Arc;
226
227 use machi_agent::AgentBuilder;
228 use machi_llm::MockSampler;
229 use machi_state::ChatStateHandle;
230
231 use super::*;
232 use crate::state::VecConversationState;
233
234 #[tokio::test]
235 async fn multi_turn() {
236 let sampler = Arc::new(MockSampler::new());
237 sampler.push_text("one");
238 sampler.push_text("two");
239 let agent = AgentBuilder::named("a")
240 .model("mock")
241 .build()
242 .expect("agent");
243 let mut state = VecConversationState::new();
244 let mut session = Session::new();
245 let o1 = session
246 .run_turn(
247 &agent,
248 sampler.as_ref(),
249 &mut state,
250 TurnInput::Text("hi".into()),
251 TurnOptions::default(),
252 )
253 .await
254 .expect("t1");
255 let o2 = session
256 .run_turn(
257 &agent,
258 sampler.as_ref(),
259 &mut state,
260 TurnInput::Text("again".into()),
261 TurnOptions::default(),
262 )
263 .await
264 .expect("t2");
265 assert_eq!(o1.output_text, "one");
266 assert_eq!(o2.output_text, "two");
267 assert_eq!(session.turn_count(), 2);
268 assert!(state.messages().len() >= 4);
269 }
270
271 #[tokio::test]
272 async fn multi_turn_on_handle() {
273 let sampler = Arc::new(MockSampler::new());
274 sampler.push_text("alpha");
275 sampler.push_text("beta");
276 let agent = AgentBuilder::named("a")
277 .model("mock")
278 .build()
279 .expect("agent");
280 let handle = ChatStateHandle::spawn(vec![]);
281 let mut session = Session::new();
282 let o1 = session
283 .run_turn_on_handle(
284 &agent,
285 sampler.as_ref(),
286 &handle,
287 TurnInput::Text("hi".into()),
288 TurnOptions::default(),
289 )
290 .await
291 .expect("t1");
292 let o2 = session
293 .run_turn_on_handle(
294 &agent,
295 sampler.as_ref(),
296 &handle,
297 TurnInput::Text("again".into()),
298 TurnOptions::default(),
299 )
300 .await
301 .expect("t2");
302 assert_eq!(o1.output_text, "alpha");
303 assert_eq!(o2.output_text, "beta");
304 let snap = handle.snapshot().await;
305 assert!(snap.messages.len() >= 4);
306 assert!(snap.usage.main.total_tokens > 0);
307 handle.shutdown().await;
308 }
309
310 #[tokio::test]
311 async fn handle_checkpoint_to_memory() {
312 use machi_state::MemoryPersistence;
313
314 let sampler = Arc::new(MockSampler::new());
315 sampler.push_text("ckpt");
316 let agent = AgentBuilder::named("a")
317 .model("mock")
318 .build()
319 .expect("agent");
320 let handle = ChatStateHandle::spawn(vec![]);
321 let store = MemoryPersistence::default();
322 let mut session = Session::new();
323 session
324 .run_turn_on_handle_checkpointed(
325 &agent,
326 sampler.as_ref(),
327 &handle,
328 TurnInput::Text("hi".into()),
329 TurnOptions::default(),
330 &NoopMetrics,
331 Some(&store),
332 )
333 .await
334 .expect("turn");
335 let loaded = store.load().await.expect("load").expect("some");
336 assert!(!loaded.messages.is_empty());
337 handle.shutdown().await;
338 }
339
340 #[tokio::test]
341 async fn open_checkpointed_turn_round_trip() {
342 use machi_state::MemoryPersistence;
343
344 let sampler = Arc::new(MockSampler::new());
345 sampler.push_text("one");
346 sampler.push_text("two");
347 let agent = AgentBuilder::named("a")
348 .model("mock")
349 .build()
350 .expect("agent");
351 let store = MemoryPersistence::default();
352 let mut session = Session::new();
353 let (h1, o1) = session
354 .open_checkpointed_turn(
355 &agent,
356 sampler.as_ref(),
357 &store,
358 TurnInput::Text("a".into()),
359 TurnOptions::default(),
360 &NoopMetrics,
361 )
362 .await
363 .expect("t1");
364 assert_eq!(o1.output_text, "one");
365 h1.shutdown().await;
366
367 let (h2, o2) = session
368 .open_checkpointed_turn(
369 &agent,
370 sampler.as_ref(),
371 &store,
372 TurnInput::Text("b".into()),
373 TurnOptions::default(),
374 &NoopMetrics,
375 )
376 .await
377 .expect("t2");
378 assert_eq!(o2.output_text, "two");
379 assert!(h2.messages().await.len() >= 4);
381 h2.shutdown().await;
382 }
383}