Skip to main content

kaizen/core/
session.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Session lifecycle state machine — pure core, no IO.
3//!
4//! # Invariants
5//! - `Done` is terminal: `transition` always returns `None` for Done sessions.
6//! - `gc` fires only when `elapsed >= GC_DEADLINE`.
7
8pub const GC_DEADLINE: u32 = 24;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum Status {
12    Running,
13    Waiting,
14    Idle,
15    Done,
16}
17
18pub struct Session {
19    pub status: Status,
20    pub elapsed: u32,
21}
22
23impl Default for Session {
24    fn default() -> Self {
25        Session {
26            status: Status::Running,
27            elapsed: 0,
28        }
29    }
30}
31
32/// Pure transition: returns next `Session` or `None` if action is not enabled.
33pub fn transition(s: &Session, action: &str) -> Option<Session> {
34    let next = |status| Session {
35        status,
36        elapsed: s.elapsed + 1,
37    };
38    match (action, &s.status) {
39        ("tool_call", Status::Running) => Some(next(Status::Waiting)),
40        ("user_responds", Status::Waiting) => Some(next(Status::Running)),
41        ("agent_idles", Status::Running) => Some(next(Status::Idle)),
42        ("agent_resumes", Status::Idle) => Some(next(Status::Running)),
43        ("session_ends", st) if *st != Status::Done => Some(next(Status::Done)),
44        ("gc", st) if *st != Status::Done && s.elapsed >= GC_DEADLINE => Some(next(Status::Done)),
45        _ => None,
46    }
47}