Skip to main content

ag_ui/
ids.rs

1//! String-backed identifier newtypes.
2//!
3//! # Why these wrap `String` and not `Uuid`
4//!
5//! AG-UI identifiers are *opaque strings*. Nothing in the protocol requires a
6//! UUID, and real producers routinely send values that are not one: LangGraph
7//! emits thread ids like `"thread-abc"` and run ids that are plain integers,
8//! and several adapters reuse provider-side ids verbatim.
9//!
10//! An earlier community Rust SDK typed these fields as `Uuid`, which made every
11//! LangGraph payload fail to deserialize (ag-ui-protocol/ag-ui#2195, #2196).
12//! These newtypes therefore accept any string, including the empty one, and
13//! round-trip it byte-for-byte. Callers that *want* UUIDs can generate one and
14//! pass its string form.
15
16use std::borrow::Cow;
17use std::convert::Infallible;
18use std::fmt;
19use std::str::FromStr;
20
21use serde::{Deserialize, Serialize};
22
23macro_rules! string_id {
24    ($(#[$meta:meta])* $name:ident) => {
25        $(#[$meta])*
26        ///
27        /// An opaque string. Any value round-trips losslessly, including
28        /// non-UUID and empty strings — see the [module docs](self).
29        #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
30        #[serde(transparent)]
31        #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
32        #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
33        pub struct $name(String);
34
35        impl $name {
36            /// Wraps any string-like value without validating it.
37            #[inline]
38            pub fn new(value: impl Into<String>) -> Self {
39                Self(value.into())
40            }
41
42            /// Borrows the identifier as a string slice.
43            #[inline]
44            pub fn as_str(&self) -> &str {
45                &self.0
46            }
47
48            /// Consumes the identifier and returns the wrapped [`String`].
49            #[inline]
50            pub fn into_inner(self) -> String {
51                self.0
52            }
53
54            /// Returns `true` when the identifier is the empty string.
55            #[inline]
56            pub fn is_empty(&self) -> bool {
57                self.0.is_empty()
58            }
59        }
60
61        impl fmt::Display for $name {
62            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63                f.write_str(&self.0)
64            }
65        }
66
67        impl From<String> for $name {
68            #[inline]
69            fn from(value: String) -> Self {
70                Self(value)
71            }
72        }
73
74        impl From<&str> for $name {
75            #[inline]
76            fn from(value: &str) -> Self {
77                Self(value.to_owned())
78            }
79        }
80
81        impl From<Cow<'_, str>> for $name {
82            #[inline]
83            fn from(value: Cow<'_, str>) -> Self {
84                Self(value.into_owned())
85            }
86        }
87
88        impl From<$name> for String {
89            #[inline]
90            fn from(value: $name) -> Self {
91                value.0
92            }
93        }
94
95        impl AsRef<str> for $name {
96            #[inline]
97            fn as_ref(&self) -> &str {
98                &self.0
99            }
100        }
101
102        impl std::ops::Deref for $name {
103            type Target = str;
104
105            #[inline]
106            fn deref(&self) -> &str {
107                &self.0
108            }
109        }
110
111        impl FromStr for $name {
112            type Err = Infallible;
113
114            #[inline]
115            fn from_str(s: &str) -> Result<Self, Self::Err> {
116                Ok(Self(s.to_owned()))
117            }
118        }
119
120        impl PartialEq<str> for $name {
121            #[inline]
122            fn eq(&self, other: &str) -> bool {
123                self.0 == other
124            }
125        }
126
127        impl PartialEq<&str> for $name {
128            #[inline]
129            fn eq(&self, other: &&str) -> bool {
130                self.0 == *other
131            }
132        }
133
134        impl PartialEq<$name> for str {
135            #[inline]
136            fn eq(&self, other: &$name) -> bool {
137                self == other.0
138            }
139        }
140    };
141}
142
143string_id! {
144    /// Identifies a conversation thread across runs.
145    ThreadId
146}
147
148string_id! {
149    /// Identifies a single agent run within a thread.
150    RunId
151}
152
153string_id! {
154    /// Identifies a message within a thread.
155    MessageId
156}
157
158string_id! {
159    /// Identifies one tool invocation.
160    ToolCallId
161}
162
163string_id! {
164    /// Identifies an agent, for multi-agent routing and discovery UIs.
165    AgentId
166}
167
168string_id! {
169    /// Names a step inside a run, as carried by `STEP_STARTED` / `STEP_FINISHED`.
170    StepName
171}
172
173string_id! {
174    /// Identifies **one invocation** of a subagent.
175    ///
176    /// Running the same subagent twice yields two different values. It is not a
177    /// name and not a stable id for a reusable definition — that is
178    /// `SUBAGENT_STARTED.name` — so key transient UI state by it and persist
179    /// nothing by it. See [`crate::event::subagent`].
180    SubagentRunId
181}