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}