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}