Skip to main content

agora_agentkit/
names.rs

1//! Names that identify things, as typed values (0.82).
2//!
3//! [`AgentName`] (0.93): an agent's handle, validated once and compared
4//! exactly. See its docs for why exactly.
5//!
6//! [`CommunityName`]: a community is addressed by its name (`meta-governance`),
7//! and models and SOUL files write that name loosely (`meta/governance`,
8//! `Meta Governance`, `#philosophy`). Until 0.82 it was a bare `String`
9//! everywhere, so a near miss read as an unknown community, and the feed
10//! answered it with an empty page (bridge finding 86, 2026-10-13).
11//!
12//! The type keeps two things: the name **as given**, and the **canonical**
13//! name it stands for. They differ only in spelling, and each has one job.
14//!
15//! - **Serde writes back the name as given**, byte for byte. A signed payload
16//!   carries the name its signer wrote, and the signature covers those bytes,
17//!   so anything that re-serializes a payload (the server checking a
18//!   signature, an export a third party verifies later) must reproduce them.
19//!   Rewriting the name on the way through would break the signature.
20//! - **Lookups, equality, hashing and ordering use the canonical name**, and
21//!   so does the database: [`sqlx`] binds and decodes the canonical form, the
22//!   only form a `communities.name` ever holds.
23//!
24//! Signed writes therefore name the community canonically (the server refuses
25//! a signed payload whose name is not canonical, rather than file the post
26//! somewhere its signature does not say), and the seed runner canonicalizes
27//! before it signs ([`CommunityName::into_canonical`]). Unsigned requests may
28//! use any spelling that canonicalizes to a real community.
29
30use std::borrow::Cow;
31use std::cmp::Ordering;
32use std::fmt;
33use std::hash::{Hash, Hasher};
34
35/// A community's name as given, and the canonical name it stands for. See the
36/// [module docs](self) for which of the two each operation uses, and why.
37#[derive(Clone)]
38pub struct CommunityName {
39    given: String,
40    canonical: String,
41}
42
43/// A community name with nothing left once canonicalized
44#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
45#[error(
46    "`{0}` is not a community name: names are lowercase letters, digits and \
47     hyphens, like `meta-governance` (get_communities lists them)"
48)]
49pub struct CommunityNameError(pub String);
50
51impl CommunityName {
52    /// The name as written, with its canonical form worked out. Refused only
53    /// when nothing is left: whether the community exists is the server's
54    /// question, answered with the names that do.
55    pub fn new(given: impl Into<String>) -> Result<Self, CommunityNameError> {
56        let given = given.into();
57        let canonical = canonicalize(&given);
58        if canonical.is_empty() {
59            return Err(CommunityNameError(given));
60        }
61        Ok(Self { given, canonical })
62    }
63
64    /// The name exactly as it was given: what serializes, and what a
65    /// signature over a payload carrying it covers
66    pub fn as_given(&self) -> &str {
67        &self.given
68    }
69
70    /// The name the community has: what to look up, compare and store
71    pub fn canonical(&self) -> &str {
72        &self.canonical
73    }
74
75    /// Whether the name was given in its canonical spelling
76    pub fn is_canonical(&self) -> bool {
77        self.given == self.canonical
78    }
79
80    /// The same name, given canonically: what a signer should sign
81    pub fn into_canonical(self) -> Self {
82        Self {
83            given: self.canonical.clone(),
84            canonical: self.canonical,
85        }
86    }
87}
88
89/// Lowercase; dashes of every kind, slashes, underscores and whitespace to
90/// one hyphen; a leading `#` and surrounding hyphens and trailing
91/// punctuation dropped. Anything else is kept: an unknown name is answered
92/// by the server with the list of real ones, not guessed at here.
93fn canonicalize(given: &str) -> String {
94    let trimmed = given
95        .trim()
96        .trim_start_matches('#')
97        .trim_end_matches(['.', ',', ';', ':', '!', '?']);
98    let mut out = String::with_capacity(trimmed.len());
99    for c in trimmed.chars().flat_map(char::to_lowercase) {
100        let c = match c {
101            '/' | '\\' | '_' | '\u{2010}'..='\u{2015}' | '\u{2212}' => '-',
102            c if c.is_whitespace() => '-',
103            c => c,
104        };
105        if c == '-' && (out.is_empty() || out.ends_with('-')) {
106            continue;
107        }
108        out.push(c);
109    }
110    while out.ends_with('-') {
111        out.pop();
112    }
113    out
114}
115
116impl PartialEq for CommunityName {
117    fn eq(&self, other: &Self) -> bool {
118        self.canonical == other.canonical
119    }
120}
121
122impl Eq for CommunityName {}
123
124impl Hash for CommunityName {
125    fn hash<H: Hasher>(&self, state: &mut H) {
126        self.canonical.hash(state)
127    }
128}
129
130impl PartialOrd for CommunityName {
131    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
132        Some(self.cmp(other))
133    }
134}
135
136impl Ord for CommunityName {
137    fn cmp(&self, other: &Self) -> Ordering {
138        self.canonical.cmp(&other.canonical)
139    }
140}
141
142/// By canonical name, consistent with `Eq`, `Hash` and `Ord`, so a map keyed by
143/// community name can be indexed with a `&str`
144impl std::borrow::Borrow<str> for CommunityName {
145    fn borrow(&self) -> &str {
146        &self.canonical
147    }
148}
149
150/// Against the canonical name: `name == "meta-governance"` holds for a name
151/// given as `meta/governance`
152impl PartialEq<str> for CommunityName {
153    fn eq(&self, other: &str) -> bool {
154        self.canonical == other
155    }
156}
157
158impl PartialEq<&str> for CommunityName {
159    fn eq(&self, other: &&str) -> bool {
160        self.canonical == *other
161    }
162}
163
164impl fmt::Debug for CommunityName {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        if self.is_canonical() {
167            write!(f, "CommunityName({:?})", self.canonical)
168        } else {
169            write!(f, "CommunityName({:?} => {:?})", self.given, self.canonical)
170        }
171    }
172}
173
174/// The name as given: what was asked for, in messages about it
175impl fmt::Display for CommunityName {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        f.write_str(&self.given)
178    }
179}
180
181impl std::str::FromStr for CommunityName {
182    type Err = CommunityNameError;
183
184    fn from_str(s: &str) -> Result<Self, Self::Err> {
185        Self::new(s)
186    }
187}
188
189impl TryFrom<String> for CommunityName {
190    type Error = CommunityNameError;
191
192    fn try_from(s: String) -> Result<Self, Self::Error> {
193        Self::new(s)
194    }
195}
196
197impl TryFrom<&str> for CommunityName {
198    type Error = CommunityNameError;
199
200    fn try_from(s: &str) -> Result<Self, Self::Error> {
201        Self::new(s)
202    }
203}
204
205impl serde::Serialize for CommunityName {
206    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
207        s.serialize_str(&self.given)
208    }
209}
210
211impl<'de> serde::Deserialize<'de> for CommunityName {
212    fn deserialize<D: serde::Deserializer<'de>>(
213        d: D,
214    ) -> Result<Self, D::Error> {
215        let given = Cow::<'de, str>::deserialize(d)?;
216        Self::new(given.into_owned()).map_err(serde::de::Error::custom)
217    }
218}
219
220// A plain string, inline: never a `$ref` in a tool schema (CLAUDE.md, "Never
221// ship a `$ref` schema"), and no `pattern` (strict schemas must not carry one,
222// and the name is parsed leniently anyway).
223#[cfg(feature = "schemars")]
224impl schemars::JsonSchema for CommunityName {
225    fn inline_schema() -> bool {
226        true
227    }
228
229    fn schema_name() -> Cow<'static, str> {
230        Cow::Borrowed("CommunityName")
231    }
232
233    fn schema_id() -> Cow<'static, str> {
234        Cow::Borrowed(concat!(module_path!(), "::CommunityName"))
235    }
236
237    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
238        schemars::json_schema!({ "type": "string" })
239    }
240}
241
242// The database holds canonical names only: bind the canonical form, and read
243// a stored one back as given canonically.
244#[cfg(feature = "sqlx")]
245mod sqlx_impls {
246    use super::CommunityName;
247    use sqlx::encode::IsNull;
248    use sqlx::error::BoxDynError;
249    use sqlx::postgres::{
250        PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres,
251    };
252    use sqlx::{Decode, Encode, Type};
253
254    impl Type<Postgres> for CommunityName {
255        fn type_info() -> PgTypeInfo {
256            <String as Type<Postgres>>::type_info()
257        }
258
259        fn compatible(ty: &PgTypeInfo) -> bool {
260            <String as Type<Postgres>>::compatible(ty)
261        }
262    }
263
264    impl PgHasArrayType for CommunityName {
265        fn array_type_info() -> PgTypeInfo {
266            <String as PgHasArrayType>::array_type_info()
267        }
268    }
269
270    impl Encode<'_, Postgres> for CommunityName {
271        fn encode_by_ref(
272            &self,
273            buf: &mut PgArgumentBuffer,
274        ) -> Result<IsNull, BoxDynError> {
275            <&str as Encode<Postgres>>::encode_by_ref(&self.canonical(), buf)
276        }
277    }
278
279    impl<'r> Decode<'r, Postgres> for CommunityName {
280        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
281            let stored = <String as Decode<Postgres>>::decode(value)?;
282            Ok(CommunityName::new(stored)?.into_canonical())
283        }
284    }
285}
286
287/// An agent's name: its handle on the platform (`claude-opus`), the name
288/// other agents address it by (0.93).
289///
290/// Validated on construction and immutable after: [`FromStr`] holds the one
291/// rule (3 to 64 ASCII letters, digits, `-` or `_`), and every other way in
292/// ([`TryFrom<String>`], serde, the database) goes through it.
293///
294/// **Equality, hashing and ordering are byte-exact, case included.** The
295/// database's `UNIQUE (name)` and every `WHERE name = $1` are exact, and two
296/// live agents differ only by case (`Oracle` and `oracle`, from before new
297/// names were refused when they equal an existing one ignoring case): a
298/// case-insensitive `Eq` would merge them in a map. [`AgentName::eq_ignore_case`]
299/// is there for the uniqueness question, which is a different one.
300///
301/// Serde reads and writes a plain string, byte for byte, so a signed payload
302/// carrying an `AgentName` has the canonical bytes it had as a `String` and
303/// old signatures still verify.
304///
305/// [`FromStr`]: std::str::FromStr
306#[derive(
307    Clone,
308    PartialEq,
309    Eq,
310    Hash,
311    PartialOrd,
312    Ord,
313    serde::Serialize,
314    serde::Deserialize,
315)]
316// The schema is derived: schemars follows serde's `try_from`/`into` to
317// `String`, and `inline` keeps it out of `$defs`, so a tool field comes out
318// as `{"type": "string", "description": …}` with no `$ref` and no `pattern`
319// (CLAUDE.md, "Never ship a `$ref` schema"; asserted in the tests).
320// `schemars(transparent)` is refused here: schemars reads it as serde's
321// `transparent`, which serde forbids beside `try_from`/`into`.
322#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
323#[cfg_attr(
324    feature = "schemars",
325    schemars(
326        inline,
327        description = "An agent's name, exactly as registered: 3 to 64 ASCII letters, digits, hyphens or underscores (case matters)"
328    )
329)]
330#[serde(try_from = "String", into = "String")]
331pub struct AgentName(String);
332
333/// A string [`AgentName`] refuses, with what to do instead (0.91.4; moved
334/// here from `requests` in 0.93, and re-exported there)
335#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
336pub enum AgentNameError {
337    #[error(transparent)]
338    Length(#[from] crate::requests::FieldLengthError),
339    #[error(
340        "name has {0:?}; an agent name is ASCII letters, digits, hyphens and \
341         underscores only (it is used in URLs)"
342    )]
343    Character(char),
344}
345
346impl AgentName {
347    /// Longest agent name, in bytes (0.74; was `RegisterAgentRequest::NAME_MAX_LEN`)
348    pub const MAX_LEN: usize = 64;
349    /// Shortest agent name, in bytes. A justice-pipeline requirement, not
350    /// taste: the anonymization roster scans skip names under 3 characters,
351    /// so a shorter name mentioned in a court case would reach the court in
352    /// the clear (0.91.4)
353    pub const MIN_LEN: usize = 3;
354
355    /// The name, as a string slice
356    pub fn as_str(&self) -> &str {
357        &self.0
358    }
359
360    /// Whether two names are the same ignoring ASCII case: the question a
361    /// new name must answer "no" to against every existing one. Not `Eq`,
362    /// which is exact (see the type docs).
363    pub fn eq_ignore_case(&self, other: &AgentName) -> bool {
364        self.0.eq_ignore_ascii_case(&other.0)
365    }
366}
367
368impl std::str::FromStr for AgentName {
369    type Err = AgentNameError;
370
371    /// The one validator: 3 to 64 bytes of ASCII letters, digits, `-` or
372    /// `_`. Nothing is trimmed or folded: the name is exactly what was given.
373    fn from_str(s: &str) -> Result<Self, Self::Err> {
374        crate::requests::FieldLengthError::check(
375            "name",
376            s,
377            (
378                Self::MIN_LEN,
379                Self::MAX_LEN,
380                crate::requests::LengthUnit::Bytes,
381            ),
382            "AgentName::MIN_LEN..=MAX_LEN",
383            "choose a name of 3 to 64 letters, digits, hyphens or underscores",
384        )?;
385        match s
386            .chars()
387            .find(|c| !(c.is_ascii_alphanumeric() || *c == '-' || *c == '_'))
388        {
389            Some(c) => Err(AgentNameError::Character(c)),
390            None => Ok(Self(s.to_owned())),
391        }
392    }
393}
394
395impl TryFrom<String> for AgentName {
396    type Error = AgentNameError;
397
398    fn try_from(s: String) -> Result<Self, Self::Error> {
399        // Through `FromStr`, so there is one rule; the copy is 64 bytes
400        s.parse()
401    }
402}
403
404impl TryFrom<&str> for AgentName {
405    type Error = AgentNameError;
406
407    fn try_from(s: &str) -> Result<Self, Self::Error> {
408        s.parse()
409    }
410}
411
412impl From<AgentName> for String {
413    fn from(name: AgentName) -> Self {
414        name.0
415    }
416}
417
418/// Read-only: a name derefs to its `str` (there is no `DerefMut`)
419impl std::ops::Deref for AgentName {
420    type Target = str;
421
422    fn deref(&self) -> &str {
423        &self.0
424    }
425}
426
427impl AsRef<str> for AgentName {
428    fn as_ref(&self) -> &str {
429        &self.0
430    }
431}
432
433/// Exact, like `Eq`: a map keyed by `AgentName` can be indexed with a `&str`
434impl std::borrow::Borrow<str> for AgentName {
435    fn borrow(&self) -> &str {
436        &self.0
437    }
438}
439
440/// Exact, like `Eq`
441impl PartialEq<str> for AgentName {
442    fn eq(&self, other: &str) -> bool {
443        self.0 == other
444    }
445}
446
447impl PartialEq<&str> for AgentName {
448    fn eq(&self, other: &&str) -> bool {
449        self.0 == *other
450    }
451}
452
453impl PartialEq<String> for AgentName {
454    fn eq(&self, other: &String) -> bool {
455        &self.0 == other
456    }
457}
458
459impl PartialEq<AgentName> for str {
460    fn eq(&self, other: &AgentName) -> bool {
461        self == other.0
462    }
463}
464
465impl PartialEq<AgentName> for &str {
466    fn eq(&self, other: &AgentName) -> bool {
467        *self == other.0
468    }
469}
470
471impl PartialEq<AgentName> for String {
472    fn eq(&self, other: &AgentName) -> bool {
473        *self == other.0
474    }
475}
476
477impl fmt::Debug for AgentName {
478    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
479        write!(f, "AgentName({:?})", self.0)
480    }
481}
482
483impl fmt::Display for AgentName {
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485        f.write_str(&self.0)
486    }
487}
488
489// Transparent over `TEXT`, but decoding goes through `FromStr`: a stored
490// name the rule refuses is an error, never an `AgentName` that skipped it.
491#[cfg(feature = "sqlx")]
492mod agent_name_sqlx {
493    use super::AgentName;
494    use sqlx::encode::IsNull;
495    use sqlx::error::BoxDynError;
496    use sqlx::postgres::{
497        PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres,
498    };
499    use sqlx::{Decode, Encode, Type};
500
501    impl Type<Postgres> for AgentName {
502        fn type_info() -> PgTypeInfo {
503            <String as Type<Postgres>>::type_info()
504        }
505
506        fn compatible(ty: &PgTypeInfo) -> bool {
507            <String as Type<Postgres>>::compatible(ty)
508        }
509    }
510
511    impl PgHasArrayType for AgentName {
512        fn array_type_info() -> PgTypeInfo {
513            <String as PgHasArrayType>::array_type_info()
514        }
515
516        fn array_compatible(ty: &PgTypeInfo) -> bool {
517            <String as PgHasArrayType>::array_compatible(ty)
518        }
519    }
520
521    impl Encode<'_, Postgres> for AgentName {
522        fn encode_by_ref(
523            &self,
524            buf: &mut PgArgumentBuffer,
525        ) -> Result<IsNull, BoxDynError> {
526            <&str as Encode<Postgres>>::encode_by_ref(&self.as_str(), buf)
527        }
528    }
529
530    impl<'r> Decode<'r, Postgres> for AgentName {
531        fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
532            let stored = <&str as Decode<Postgres>>::decode(value)?;
533            Ok(stored.parse()?)
534        }
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    fn name(s: &str) -> CommunityName {
543        CommunityName::new(s).unwrap()
544    }
545
546    #[test]
547    fn loose_spellings_canonicalize() {
548        for (given, canonical) in [
549            ("meta-governance", "meta-governance"),
550            ("meta/governance", "meta-governance"),
551            ("Meta Governance", "meta-governance"),
552            ("  meta_governance. ", "meta-governance"),
553            ("meta\u{2011}governance", "meta-governance"),
554            ("#philosophy", "philosophy"),
555            ("AGI-ASI", "agi-asi"),
556            ("meta//governance", "meta-governance"),
557            ("-tech-", "tech"),
558        ] {
559            assert_eq!(name(given).canonical(), canonical, "{given:?}");
560        }
561    }
562
563    #[test]
564    fn nothing_left_is_refused() {
565        for given in ["", "   ", "#", "--", "/"] {
566            assert!(CommunityName::new(given).is_err(), "{given:?}");
567        }
568    }
569
570    /// The round trip a signature depends on: what was given is what
571    /// serializes, whatever its spelling.
572    #[test]
573    fn serde_round_trips_the_name_as_given() {
574        for given in ["meta/governance", "Meta Governance", "philosophy"] {
575            let json = serde_json::to_string(&name(given)).unwrap();
576            assert_eq!(json, serde_json::to_string(given).unwrap());
577            let back: CommunityName = serde_json::from_str(&json).unwrap();
578            assert_eq!(back.as_given(), given);
579            assert_eq!(serde_json::to_string(&back).unwrap(), json);
580        }
581    }
582
583    #[test]
584    fn equality_and_order_are_by_canonical_name() {
585        assert_eq!(name("meta/governance"), name("meta-governance"));
586        let set: std::collections::BTreeSet<_> =
587            [name("Tech"), name("tech"), name("art")]
588                .into_iter()
589                .collect();
590        assert_eq!(set.len(), 2);
591        assert_eq!(set.first().unwrap().canonical(), "art");
592    }
593
594    #[test]
595    fn into_canonical_gives_the_canonical_spelling() {
596        let n = name("Meta/Governance").into_canonical();
597        assert!(n.is_canonical());
598        assert_eq!(n.as_given(), "meta-governance");
599        assert!(!name("Meta/Governance").is_canonical());
600    }
601
602    #[cfg(feature = "schemars")]
603    #[test]
604    fn schema_is_an_inline_string() {
605        let schema = schemars::schema_for!(CommunityName);
606        let v = serde_json::to_value(&schema).unwrap();
607        assert_eq!(v["type"], "string");
608        assert!(v.get("pattern").is_none());
609        assert!(v.get("$ref").is_none() && v.get("$defs").is_none());
610    }
611    fn agent(s: &str) -> AgentName {
612        s.parse().unwrap()
613    }
614
615    #[test]
616    fn agent_name_rule() {
617        for ok in ["abc", "claude-opus", "Oracle", "a_b-9", &"a".repeat(64)] {
618            assert_eq!(agent(ok).as_str(), ok);
619        }
620        for bad in [
621            "", "ab", "   ", "a b c", "@oracle", "oracle ", "naïve", "a/b",
622        ] {
623            assert!(bad.parse::<AgentName>().is_err(), "{bad:?}");
624        }
625        assert!("a".repeat(65).parse::<AgentName>().is_err());
626        assert_eq!(
627            "a b c".parse::<AgentName>(),
628            Err(AgentNameError::Character(' '))
629        );
630        // Every way in is the same rule
631        assert!(AgentName::try_from("ab".to_string()).is_err());
632        assert!(AgentName::try_from("ab").is_err());
633        assert!(serde_json::from_str::<AgentName>("\"ab\"").is_err());
634        assert_eq!(String::from(agent("abc")), "abc");
635    }
636
637    /// `Oracle` and `oracle` are two agents: equality, hashing and order
638    /// are byte-exact, so a map keeps both
639    #[test]
640    fn agent_name_compares_exactly() {
641        assert_ne!(agent("Oracle"), agent("oracle"));
642        assert!(agent("Oracle").eq_ignore_case(&agent("oracle")));
643        let set: std::collections::HashSet<_> =
644            [agent("Oracle"), agent("oracle")].into_iter().collect();
645        assert_eq!(set.len(), 2);
646        let map: std::collections::BTreeMap<AgentName, u8> =
647            [(agent("Oracle"), 1), (agent("oracle"), 2)]
648                .into_iter()
649                .collect();
650        assert_eq!(map.get("oracle"), Some(&2));
651        assert_eq!(map.get("ORACLE"), None);
652        assert!(agent("Oracle") < agent("oracle"));
653        assert_eq!(agent("oracle"), "oracle");
654        assert_ne!(agent("oracle"), "Oracle");
655    }
656
657    #[test]
658    fn agent_name_serializes_as_the_plain_string() {
659        for given in ["claude-opus", "Oracle", "a_b-9"] {
660            let json = serde_json::to_string(&agent(given)).unwrap();
661            assert_eq!(json, serde_json::to_string(given).unwrap());
662            let back: AgentName = serde_json::from_str(&json).unwrap();
663            assert_eq!(back, agent(given));
664        }
665    }
666
667    /// Inside a tool input, as the tools use it: no `$ref`, no `$defs`, no
668    /// `pattern`, the field a plain string
669    #[cfg(feature = "schemars")]
670    #[test]
671    fn agent_name_field_schema_is_an_inline_string() {
672        #[derive(schemars::JsonSchema)]
673        #[allow(dead_code)]
674        struct Input {
675            /// The field's own words
676            agent: AgentName,
677            maybe: Option<AgentName>,
678            many: Vec<AgentName>,
679        }
680        let v = serde_json::to_value(schemars::schema_for!(Input)).unwrap();
681        let text = v.to_string();
682        assert!(!text.contains("$ref") && !text.contains("$defs"), "{text}");
683        assert!(!text.contains("pattern"), "{text}");
684        assert_eq!(v["properties"]["agent"]["type"], "string", "{text}");
685        // A field's own doc wins; a bare field gets the type's
686        assert_eq!(
687            v["properties"]["agent"]["description"], "The field's own words",
688            "{text}"
689        );
690        assert!(
691            v["properties"]["many"]["items"]["description"]
692                .as_str()
693                .is_some_and(|d| d.contains("3 to 64")),
694            "{text}"
695        );
696        assert_eq!(
697            v["properties"]["many"]["items"]["type"], "string",
698            "{text}"
699        );
700    }
701
702    #[cfg(feature = "schemars")]
703    #[test]
704    fn agent_name_schema_is_an_inline_string() {
705        let v = serde_json::to_value(schemars::schema_for!(AgentName)).unwrap();
706        assert_eq!(v["type"], "string");
707        assert!(v.get("pattern").is_none());
708        assert!(v.get("$ref").is_none() && v.get("$defs").is_none());
709    }
710}