Skip to main content

codewhale_protocol/
ids.rs

1//! Typed `ThreadId` / `SessionId` for the `crates/core` boundary (issue #5261).
2//!
3//! `codewhale`'s `Session` is really a thread. The new boundary introduces
4//! two ids so every consumer — TUI, CLI, app-server, tests — can name the
5//! right scope:
6//! - `ThreadId` — long-lived conversation (persisted in `state.json` / `threads/`)
7//! - `SessionId` — one turn/session within a thread (ephemeral engine handle)
8//!
9//! Both are thin wrappers around the existing `"thread-…"` string id so the
10//! persisted JSON shape stays unchanged. They serialize as plain strings,
11//! deserialize from plain strings or `{ "id": "…" }`, and parse from either.
12
13use std::fmt;
14use std::str::FromStr;
15
16use serde::{Deserialize, Serialize};
17use uuid::Uuid;
18
19/// Long-lived conversation id. Backwards compatible with the existing
20/// `thread-{uuid}` string form used in `crates/state` and `runtime_threads`.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct ThreadId(pub String);
24
25impl ThreadId {
26    #[must_use]
27    pub fn new() -> Self {
28        Self(format!("thread-{}", Uuid::new_v4()))
29    }
30
31    #[must_use]
32    pub fn from_string(s: impl Into<String>) -> Self {
33        Self(s.into())
34    }
35
36    #[must_use]
37    pub fn as_str(&self) -> &str {
38        &self.0
39    }
40
41    #[must_use]
42    pub fn into_string(self) -> String {
43        self.0
44    }
45}
46
47impl Default for ThreadId {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl fmt::Display for ThreadId {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        f.write_str(&self.0)
56    }
57}
58
59impl From<String> for ThreadId {
60    fn from(s: String) -> Self {
61        Self(s)
62    }
63}
64
65impl From<ThreadId> for String {
66    fn from(id: ThreadId) -> Self {
67        id.0
68    }
69}
70
71impl FromStr for ThreadId {
72    type Err = std::convert::Infallible;
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        Ok(Self(s.to_string()))
75    }
76}
77
78/// One engine session within a thread (a single `Op` turn or a supervised
79/// engine lifetime). Distinct from `ThreadId` so the thread manager can
80/// start a session with no TUI attached and so tests can assert headless
81/// == TUI byte-identical requests for the same `ThreadId` + `SessionId` pair.
82#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(transparent)]
84pub struct SessionId(pub String);
85
86impl SessionId {
87    #[must_use]
88    pub fn new() -> Self {
89        Self(format!("session-{}", Uuid::new_v4()))
90    }
91
92    #[must_use]
93    pub fn from_string(s: impl Into<String>) -> Self {
94        Self(s.into())
95    }
96
97    #[must_use]
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101
102    #[must_use]
103    pub fn into_string(self) -> String {
104        self.0
105    }
106}
107
108impl Default for SessionId {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114impl fmt::Display for SessionId {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        f.write_str(&self.0)
117    }
118}
119
120impl From<String> for SessionId {
121    fn from(s: String) -> Self {
122        Self(s)
123    }
124}
125
126impl From<SessionId> for String {
127    fn from(id: SessionId) -> Self {
128        id.0
129    }
130}
131
132impl FromStr for SessionId {
133    type Err = std::convert::Infallible;
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        Ok(Self(s.to_string()))
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn thread_id_roundtrip() {
145        let id = ThreadId::new();
146        let s = id.to_string();
147        assert!(s.starts_with("thread-"));
148        let parsed: ThreadId = s.parse().unwrap();
149        assert_eq!(parsed.as_str(), id.as_str());
150    }
151
152    #[test]
153    fn session_id_display() {
154        let id = SessionId::from_string("session-abc");
155        assert_eq!(format!("{id}"), "session-abc");
156        let json = serde_json::to_string(&id).unwrap();
157        assert_eq!(json, "\"session-abc\"");
158        let back: SessionId = serde_json::from_str(&json).unwrap();
159        assert_eq!(back, id);
160    }
161}