Skip to main content

zeph_common/
types.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Strongly-typed identifiers and shared tool types across `zeph-*` crates.
5//!
6//! This module defines `ToolName`, `SessionId`, and `ToolDefinition` — types shared
7//! by multiple crates without creating cross-crate dependencies.
8//!
9//! `ToolName` and `SessionId` use `#[serde(transparent)]` for zero-cost serialization
10//! compatibility: the JSON wire format is unchanged relative to plain `String` fields.
11
12use std::borrow::Borrow;
13use std::fmt;
14use std::str::FromStr;
15use std::sync::Arc;
16
17use serde::{Deserialize, Serialize};
18
19/// Strongly-typed tool name label.
20///
21/// `ToolName` identifies a tool by its canonical name (e.g., `"shell"`, `"web_scrape"`).
22/// It is produced by the LLM in JSON tool-use responses and matched against the registered
23/// tool registry at dispatch time.
24///
25/// # Label semantics (not a validated reference)
26///
27/// `ToolName` is an unvalidated label from untrusted input (LLM JSON). It does **not**
28/// guarantee that a tool with this name is registered. Validation happens downstream at
29/// tool dispatch, not at construction.
30///
31/// # Inner type: `Arc<str>`
32///
33/// The inner type is `Arc<str>`, not `String`. Tool names are cloned into multiple contexts
34/// (event channels, tracing spans, tool output structs) during a single tool execution.
35/// `Arc<str>` makes all clones O(1) vs O(n) for `String`. Use `.clone()` to duplicate
36/// a `ToolName` — it is cheap.
37///
38/// # No `Deref<Target=str>`
39///
40/// `ToolName` does **not** implement `Deref<Target=str>`. This prevents the `.to_owned()`
41/// footgun where muscle memory returns `String` instead of `ToolName`. Use `.as_str()` for
42/// explicit string conversion and `.clone()` to duplicate the `ToolName`.
43///
44/// # Examples
45///
46/// ```
47/// use zeph_common::ToolName;
48///
49/// let name = ToolName::new("shell");
50/// assert_eq!(name.as_str(), "shell");
51/// assert_eq!(name, "shell");
52///
53/// // Clone is O(1) — Arc reference count increment only.
54/// let name2 = name.clone();
55/// assert_eq!(name, name2);
56/// ```
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58#[serde(transparent)]
59pub struct ToolName(Arc<str>);
60
61impl ToolName {
62    /// Construct a `ToolName` from any value convertible to `Arc<str>`.
63    ///
64    /// This is the primary constructor. The name is accepted without validation — it is a
65    /// label from the LLM wire or tool registry, not a proof of registration.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use zeph_common::ToolName;
71    ///
72    /// let name = ToolName::new("shell");
73    /// assert_eq!(name.as_str(), "shell");
74    /// ```
75    #[must_use]
76    pub fn new(s: impl Into<Arc<str>>) -> Self {
77        Self(s.into())
78    }
79
80    /// Return the inner string slice.
81    ///
82    /// Prefer this over `Deref` (which is intentionally not implemented) when an `&str`
83    /// reference is needed.
84    ///
85    /// # Examples
86    ///
87    /// ```
88    /// use zeph_common::ToolName;
89    ///
90    /// let name = ToolName::new("web_scrape");
91    /// assert_eq!(name.as_str(), "web_scrape");
92    /// ```
93    #[must_use]
94    pub fn as_str(&self) -> &str {
95        &self.0
96    }
97}
98
99impl Default for ToolName {
100    /// Returns an empty `ToolName`.
101    ///
102    /// This implementation exists solely for `#[serde(default)]` on optional fields.
103    /// Do not construct a `ToolName` with an empty string in application code.
104    fn default() -> Self {
105        Self(Arc::from(""))
106    }
107}
108
109impl fmt::Display for ToolName {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(&self.0)
112    }
113}
114
115impl AsRef<str> for ToolName {
116    fn as_ref(&self) -> &str {
117        &self.0
118    }
119}
120
121impl Borrow<str> for ToolName {
122    fn borrow(&self) -> &str {
123        &self.0
124    }
125}
126
127impl From<&str> for ToolName {
128    fn from(s: &str) -> Self {
129        Self(Arc::from(s))
130    }
131}
132
133impl From<String> for ToolName {
134    fn from(s: String) -> Self {
135        Self(Arc::from(s.as_str()))
136    }
137}
138
139impl FromStr for ToolName {
140    type Err = std::convert::Infallible;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        Ok(Self::from(s))
144    }
145}
146
147impl PartialEq<str> for ToolName {
148    fn eq(&self, other: &str) -> bool {
149        self.0.as_ref() == other
150    }
151}
152
153impl PartialEq<&str> for ToolName {
154    fn eq(&self, other: &&str) -> bool {
155        self.0.as_ref() == *other
156    }
157}
158
159impl PartialEq<String> for ToolName {
160    fn eq(&self, other: &String) -> bool {
161        self.0.as_ref() == other.as_str()
162    }
163}
164
165impl PartialEq<ToolName> for str {
166    fn eq(&self, other: &ToolName) -> bool {
167        self == other.0.as_ref()
168    }
169}
170
171impl PartialEq<ToolName> for String {
172    fn eq(&self, other: &ToolName) -> bool {
173        self.as_str() == other.0.as_ref()
174    }
175}
176
177// ── SessionId ────────────────────────────────────────────────────────────────
178
179/// Identifies a single agent session (one binary invocation or one ACP connection).
180///
181/// Uses `String` internally to support both UUID-based IDs (production) and
182/// arbitrary string IDs (tests, experiments). UUID validation is enforced only at
183/// [`SessionId::generate`] time; [`SessionId::new`] accepts any non-empty string for
184/// flexibility in test fixtures.
185///
186/// # Serialization
187///
188/// `SessionId` uses `#[serde(transparent)]` — it serializes as a plain JSON string
189/// identical to the raw `String` fields it replaces. No wire format change, no DB
190/// schema migration required.
191///
192/// # ACP Note
193///
194/// `acp::SessionId` from the external `agent_client_protocol` crate is distinct.
195/// This type is for **our own** session tracking only.
196///
197/// # Examples
198///
199/// ```
200/// use zeph_common::SessionId;
201///
202/// // Production: generate a fresh UUID session
203/// let id = SessionId::generate();
204/// assert!(!id.as_str().is_empty());
205///
206/// // Tests: use a readable fixture string
207/// let test_id = SessionId::new("test-session");
208/// assert_eq!(test_id.as_str(), "test-session");
209/// ```
210#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
211#[serde(transparent)]
212pub struct SessionId(String);
213
214impl SessionId {
215    /// Create a `SessionId` from any non-empty string.
216    ///
217    /// Accepts UUID strings (production), readable names (tests), or any other
218    /// non-empty value. In debug builds, an empty string triggers a `debug_assert!`
219    /// to catch accidental construction early.
220    ///
221    /// # Panics
222    ///
223    /// Panics in **debug builds only** if `s` is empty.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use zeph_common::SessionId;
229    ///
230    /// let id = SessionId::new("test-session");
231    /// assert_eq!(id.as_str(), "test-session");
232    /// ```
233    pub fn new(s: impl Into<String>) -> Self {
234        let s = s.into();
235        debug_assert!(!s.is_empty(), "SessionId must not be empty");
236        Self(s)
237    }
238
239    /// Generate a new session ID backed by a random UUID v4.
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use zeph_common::SessionId;
245    ///
246    /// let id = SessionId::generate();
247    /// assert!(!id.as_str().is_empty());
248    /// // UUIDs are 36 chars: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
249    /// assert_eq!(id.as_str().len(), 36);
250    /// ```
251    #[must_use]
252    pub fn generate() -> Self {
253        Self(uuid::Uuid::new_v4().to_string())
254    }
255
256    /// Return the inner string slice.
257    ///
258    /// # Examples
259    ///
260    /// ```
261    /// use zeph_common::SessionId;
262    ///
263    /// let id = SessionId::new("s1");
264    /// assert_eq!(id.as_str(), "s1");
265    /// ```
266    #[must_use]
267    pub fn as_str(&self) -> &str {
268        &self.0
269    }
270}
271
272impl Default for SessionId {
273    /// Generate a new UUID-backed session ID.
274    fn default() -> Self {
275        Self::generate()
276    }
277}
278
279impl fmt::Display for SessionId {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.write_str(&self.0)
282    }
283}
284
285impl AsRef<str> for SessionId {
286    fn as_ref(&self) -> &str {
287        &self.0
288    }
289}
290
291impl std::ops::Deref for SessionId {
292    type Target = str;
293
294    fn deref(&self) -> &str {
295        &self.0
296    }
297}
298
299impl From<String> for SessionId {
300    fn from(s: String) -> Self {
301        Self::new(s)
302    }
303}
304
305impl From<&str> for SessionId {
306    fn from(s: &str) -> Self {
307        Self::new(s)
308    }
309}
310
311impl From<uuid::Uuid> for SessionId {
312    fn from(u: uuid::Uuid) -> Self {
313        Self(u.to_string())
314    }
315}
316
317impl FromStr for SessionId {
318    type Err = std::convert::Infallible;
319
320    fn from_str(s: &str) -> Result<Self, Self::Err> {
321        Ok(Self::new(s))
322    }
323}
324
325impl PartialEq<str> for SessionId {
326    fn eq(&self, other: &str) -> bool {
327        self.0 == other
328    }
329}
330
331impl PartialEq<&str> for SessionId {
332    fn eq(&self, other: &&str) -> bool {
333        self.0 == *other
334    }
335}
336
337impl PartialEq<String> for SessionId {
338    fn eq(&self, other: &String) -> bool {
339        self.0 == *other
340    }
341}
342
343impl PartialEq<SessionId> for str {
344    fn eq(&self, other: &SessionId) -> bool {
345        self == other.0
346    }
347}
348
349impl PartialEq<SessionId> for String {
350    fn eq(&self, other: &SessionId) -> bool {
351        *self == other.0
352    }
353}
354
355// ── ToolDefinition ───────────────────────────────────────────────────────────
356
357/// Minimal tool definition passed to LLM providers.
358///
359/// Decoupled from `zeph-tools::ToolDef` to avoid cross-crate dependencies.
360/// Providers translate this into their native tool/function format before sending to the API.
361///
362/// # Examples
363///
364/// ```
365/// use zeph_common::types::ToolDefinition;
366/// use zeph_common::ToolName;
367///
368/// let tool = ToolDefinition {
369///     name: ToolName::new("get_weather"),
370///     description: "Return current weather for a city.".into(),
371///     parameters: serde_json::json!({
372///         "type": "object",
373///         "properties": {
374///             "city": { "type": "string" }
375///         },
376///         "required": ["city"]
377///     }),
378/// };
379/// assert_eq!(tool.name, "get_weather");
380/// ```
381#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
382pub struct ToolDefinition {
383    /// Tool name — must match the name used in the response `ToolUseRequest`.
384    pub name: ToolName,
385    /// Human-readable description guiding the model on when to call this tool.
386    pub description: String,
387    /// JSON Schema object describing parameters.
388    pub parameters: serde_json::Value,
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn tool_name_construction_and_equality() {
397        let name = ToolName::new("shell");
398        assert_eq!(name.as_str(), "shell");
399        assert_eq!(name, "shell");
400        assert_eq!(name, "shell".to_owned());
401        assert_eq!(name, "shell"); // symmetric check via PartialEq<str>
402    }
403
404    #[test]
405    fn tool_name_clone_is_cheap() {
406        let name = ToolName::new("web_scrape");
407        let name2 = name.clone();
408        assert_eq!(name, name2);
409        // Both Arc<str> point to same allocation
410        assert!(Arc::ptr_eq(&name.0, &name2.0));
411    }
412
413    #[test]
414    fn tool_name_from_impls() {
415        let from_str: ToolName = ToolName::from("bash");
416        let from_string: ToolName = ToolName::from("bash".to_owned());
417        let parsed: ToolName = "bash".parse().unwrap();
418        assert_eq!(from_str, from_string);
419        assert_eq!(from_str, parsed);
420    }
421
422    #[test]
423    fn tool_name_as_hashmap_key() {
424        use std::collections::HashMap;
425        let mut map: HashMap<ToolName, u32> = HashMap::new();
426        map.insert(ToolName::new("shell"), 1);
427        // Borrow<str> enables lookup by &str
428        assert_eq!(map.get("shell"), Some(&1));
429    }
430
431    #[test]
432    fn tool_name_display() {
433        let name = ToolName::new("my_tool");
434        assert_eq!(format!("{name}"), "my_tool");
435    }
436
437    #[test]
438    fn tool_name_serde_transparent() {
439        let name = ToolName::new("shell");
440        let json = serde_json::to_string(&name).unwrap();
441        assert_eq!(json, r#""shell""#);
442        let back: ToolName = serde_json::from_str(&json).unwrap();
443        assert_eq!(back, name);
444    }
445
446    #[test]
447    fn session_id_new_roundtrip() {
448        let id = SessionId::new("test-session");
449        assert_eq!(id.as_str(), "test-session");
450        assert_eq!(id.to_string(), "test-session");
451    }
452
453    #[test]
454    fn session_id_generate_is_uuid() {
455        let id = SessionId::generate();
456        assert_eq!(id.as_str().len(), 36);
457        assert!(uuid::Uuid::parse_str(id.as_str()).is_ok());
458    }
459
460    #[test]
461    fn session_id_default_is_generated() {
462        let id = SessionId::default();
463        assert!(!id.as_str().is_empty());
464        assert_eq!(id.as_str().len(), 36);
465    }
466
467    #[test]
468    fn session_id_from_uuid() {
469        let u = uuid::Uuid::new_v4();
470        let id = SessionId::from(u);
471        assert_eq!(id.as_str(), u.to_string());
472    }
473
474    #[test]
475    fn session_id_deref_slicing() {
476        let id = SessionId::new("abcdefgh");
477        // Deref<Target=str> enables string slicing
478        assert_eq!(&id[..4], "abcd");
479    }
480
481    #[test]
482    fn session_id_serde_transparent() {
483        let id = SessionId::new("sess-abc");
484        let json = serde_json::to_string(&id).unwrap();
485        assert_eq!(json, r#""sess-abc""#);
486        let back: SessionId = serde_json::from_str(&json).unwrap();
487        assert_eq!(back, id);
488    }
489
490    #[test]
491    fn session_id_from_str_parses() {
492        let id: SessionId = "my-session".parse().unwrap();
493        assert_eq!(id.as_str(), "my-session");
494    }
495}