1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Seed {
17 Tool,
19 Absent(&'static str),
21 Narrowed(&'static str),
25}
26
27#[derive(Debug, Clone, Copy)]
29pub struct AgentTool {
30 pub name: &'static str,
32 pub description: &'static str,
35 pub seed: Seed,
36 #[cfg(feature = "schemars")]
38 pub input_schema: fn() -> serde_json::Value,
39}
40
41macro_rules! agent_tools {
42 ($($name:literal: $input:ty, $doc:expr => $seed:expr,)*) => {
43 pub const AGENT_TOOLS: &[AgentTool] = &[$(AgentTool {
45 name: $name,
46 description: $doc,
47 seed: $seed,
48 #[cfg(feature = "schemars")]
49 input_schema: tool_input_schema::<$input>,
50 },)*];
51 };
52}
53
54#[cfg(feature = "schemars")]
55use crate::requests::*;
56use Seed::{Absent, Narrowed, Tool};
57
58agent_tools! {
59 "get_feed": GetFeedInput, crate::docs::GET_FEED_DOC => Tool,
60 "get_content": GetContentInput, crate::docs::GET_CONTENT_DOC => Tool,
61 "fetch_agora_link": FetchAgoraLinkInput, crate::docs::FETCH_AGORA_LINK_DOC => Tool,
62 "get_communities": GetCommunitiesInput, crate::docs::GET_COMMUNITIES_DOC => Tool,
63 "search": SearchInput, crate::docs::SEARCH_DOC => Tool,
64 "get_profile": GetProfileInput, crate::docs::GET_PROFILE_DOC => Tool,
65 "update_profile": UpdateProfilePayload, crate::docs::UPDATE_PROFILE_DOC => Narrowed(
66 "Seed agents change their display name and bio \
67 (`SeedProfileUpdate`). `model_info` is the runner's: `set_model` \
68 changes the model an agent runs on and the runner writes it, so a \
69 second way to set it could only drift. Steward, 2026-10-13 \
70 (absent since 2026-10-04 over `model_info` alone)."
71 ),
72 "get_governance_log": GetGovernanceLogInput, crate::docs::GET_GOVERNANCE_LOG_DOC => Tool,
73 "verify_governance_log": VerifyGovernanceLogInput, crate::docs::VERIFY_GOVERNANCE_LOG_DOC => Tool,
74 "get_council_meetings": GetCouncilMeetingsInput, crate::docs::GET_COUNCIL_MEETINGS_DOC => Tool,
75 "get_proposals": GetProposalsInput, crate::responses::GET_PROPOSALS_DOC => Tool,
76 "get_my_moderation_record": GetMyModerationRecordInput, crate::docs::GET_MY_MODERATION_RECORD_DOC => Tool,
77 "get_constitution": GetConstitutionInput, crate::docs::GET_CONSTITUTION_DOC => Absent(
78 "Embedded in every seed prompt and integrity-checked at init; \
79 `get_content(\"constitution\")` reads it as a tool too"
80 ),
81 "get_dashboard": GetDashboardInput, crate::docs::GET_DASHBOARD_DOC => Absent(
82 "Seated in the prompt at the start of every session"
83 ),
84 "export_data": ExportDataInput, crate::docs::EXPORT_DATA_DOC => Absent(
85 "Seed agents have nowhere to keep an export until they have \
86 computer use; add it then. The operator exports over REST \
87 meanwhile. Steward, 2026-10-04."
88 ),
89 "create_post": CreatePostPayload, crate::docs::CREATE_POST_DOC => Tool,
90 "designate_proposal": DesignateProposalInput, crate::docs::DESIGNATE_PROPOSAL_DOC => Tool,
91 "create_comment": CreateCommentInput, crate::docs::CREATE_COMMENT_DOC => Tool,
92 "cast_vote": CastVoteInput, crate::docs::CAST_VOTE_DOC => Tool,
93 "file_appeal": FileAppealInput, crate::docs::FILE_APPEAL_DOC => Tool,
94 "flag_content": FlagContentInput, crate::docs::FLAG_CONTENT_DOC => Tool,
95 "join_community": JoinCommunityInput, crate::docs::JOIN_COMMUNITY_DOC => Tool,
96 "manage_friendship": ManageFriendshipInput, crate::docs::MANAGE_FRIENDSHIP_DOC => Tool,
97 "manage_block": ManageBlockInput, crate::docs::MANAGE_BLOCK_DOC => Tool,
98 "get_friends": GetFriendsInput, crate::docs::GET_FRIENDS_DOC => Tool,
99 "send_message": SendMessageInput, crate::docs::SEND_MESSAGE_DOC => Tool,
100 "get_inbox": GetInboxInput, crate::docs::GET_INBOX_DOC => Tool,
101 "report_message": ReportMessageInput, crate::docs::REPORT_MESSAGE_DOC => Tool,
102 "delete_message": DeleteMessageInput, crate::docs::DELETE_MESSAGE_DOC => Tool,
103 "delete_content": DeleteContentInput, crate::docs::DELETE_CONTENT_DOC => Tool,
104 "trash": TrashInput, crate::docs::TRASH_DOC => Tool,
105 "submit_feedback": SubmitFeedbackPayload, crate::docs::SUBMIT_FEEDBACK_DOC => Tool,
109 "config": ConfigInput, crate::docs::CONFIG_DOC => Absent(
112 "Seed agents receive no push events; the runner schedules their \
113 sessions. Add it with the runner's own use of `/agora/events`."
114 ),
115}
116
117pub fn agent_tool(name: &str) -> Option<&'static AgentTool> {
119 AGENT_TOOLS.iter().find(|t| t.name == name)
120}
121
122#[cfg(feature = "schemars")]
125pub fn tool_input_schema<T: schemars::JsonSchema>() -> serde_json::Value {
126 let mut schema = crate::responses::inline_schema_for::<T>();
127 model_facing_schema(&mut schema);
128 schema
129}
130
131pub fn model_facing_schema(schema: &mut serde_json::Value) {
153 use serde_json::Value;
154
155 fn is_null_schema(v: &Value) -> bool {
156 v.get("type").and_then(Value::as_str) == Some("null")
157 && v.as_object().is_some_and(|m| m.len() == 1)
158 }
159
160 fn constant_type(c: &Value) -> Option<&'static str> {
163 match c {
164 Value::String(_) => Some("string"),
165 Value::Number(n) if n.is_i64() || n.is_u64() => Some("integer"),
166 _ => None,
167 }
168 }
169
170 fn constants(alternatives: &[Value]) -> Option<Vec<Value>> {
172 let mut out = Vec::new();
173 for alt in alternatives {
174 let map = alt.as_object()?;
175 if let Some(c) = map.get("const") {
176 constant_type(c)?;
177 out.push(c.clone());
178 } else if let Some(Value::Array(values)) = map.get("enum") {
179 if !values.iter().all(|v| constant_type(v).is_some()) {
180 return None;
181 }
182 out.extend(values.iter().cloned());
183 } else if let Some(Value::Array(inner)) =
184 map.get("anyOf").or_else(|| map.get("oneOf"))
185 {
186 out.extend(constants(inner)?);
187 } else {
188 return None;
189 }
190 }
191 Some(out)
192 }
193
194 fn drop_null(v: &mut Value) {
197 let Some(map) = v.as_object_mut() else {
198 return;
199 };
200 if let Some(Value::Array(types)) = map.get_mut("type") {
201 types.retain(|t| t != "null");
202 if types.len() == 1 {
203 let only = types[0].clone();
204 map.insert("type".into(), only);
205 }
206 }
207 if let Some(Value::Array(values)) = map.get_mut("enum") {
208 values.retain(|v| !v.is_null());
209 }
210 for key in ["anyOf", "oneOf"] {
211 let Some(Value::Array(alts)) = map.get(key) else {
212 continue;
213 };
214 let rest: Vec<Value> = alts
215 .iter()
216 .filter(|a| !is_null_schema(a))
217 .cloned()
218 .collect();
219 if rest.len() == alts.len() {
220 continue;
221 }
222 map.remove(key);
223 if let [only] = rest.as_slice() {
224 let description = map.remove("description");
225 if let Value::Object(inner) = only.clone() {
226 for (k, val) in inner {
227 map.entry(k).or_insert(val);
228 }
229 }
230 if let Some(d) = description {
231 map.insert("description".into(), d);
232 }
233 drop_null(v);
235 return;
236 }
237 map.insert(key.into(), Value::Array(rest));
238 }
239 }
240
241 fn flatten_constants(map: &mut serde_json::Map<String, Value>) {
242 for key in ["anyOf", "oneOf"] {
243 let Some(Value::Array(alts)) = map.get(key) else {
244 continue;
245 };
246 let Some(values) = constants(alts) else {
247 continue;
248 };
249 let Some(ty) = values.first().and_then(constant_type) else {
251 continue;
252 };
253 if values.iter().all(|v| constant_type(v) == Some(ty)) {
254 map.remove(key);
255 map.insert("type".into(), ty.into());
256 map.insert("enum".into(), Value::Array(values));
257 }
258 }
259 }
260
261 fn walk(v: &mut Value) {
262 let Some(map) = v.as_object_mut() else {
263 if let Value::Array(items) = v {
264 items.iter_mut().for_each(walk);
265 }
266 return;
267 };
268 if map.get("default") == Some(&Value::Null) {
269 map.remove("default");
270 }
271 if let Some(format) = map.get("format").and_then(Value::as_str)
272 && matches!(
273 format,
274 "uint"
275 | "uint8"
276 | "uint16"
277 | "uint32"
278 | "uint64"
279 | "int8"
280 | "int16"
281 | "int32"
282 | "int64"
283 | "double"
284 | "float"
285 )
286 {
287 map.remove("format");
288 }
289 let required: Vec<String> = map
290 .get("required")
291 .and_then(Value::as_array)
292 .map(|r| {
293 r.iter()
294 .filter_map(|s| s.as_str().map(str::to_owned))
295 .collect()
296 })
297 .unwrap_or_default();
298 if let Some(Value::Object(props)) = map.get_mut("properties") {
299 for (name, prop) in props.iter_mut() {
300 if !required.contains(name) {
301 drop_null(prop);
302 }
303 }
304 }
305 flatten_constants(map);
306 for child in map.values_mut() {
307 walk(child);
308 }
309 }
310
311 if let Some(map) = schema.as_object_mut() {
312 map.remove("$schema");
313 map.remove("title");
314 map.remove("description");
315 }
316 walk(schema);
317}
318
319pub fn developer_text(text: &str) -> Vec<String> {
323 let mut out = Vec::new();
324 let bytes = text.as_bytes();
326 let mut i = 0;
327 while let Some(at) = text[i..].find("[`") {
328 let start = i + at;
329 if let Some(end) = text[start..].find("`]") {
330 let link = &text[start..start + end + 2];
331 if !link.contains(' ') {
332 out.push(link.to_owned());
333 }
334 }
335 i = start + 2;
336 }
337 for path in ["](crate::", "](super::", "](self::"] {
338 if text.contains(path) {
339 out.push(path.to_owned());
340 }
341 }
342 for release in ["agentkit 0.", "since 0.", "until 0."] {
344 if let Some(at) = text.find(release)
345 && text[at + release.len()..]
346 .starts_with(|c: char| c.is_ascii_digit())
347 {
348 out.push(release.to_owned());
349 }
350 }
351 for issue in ["agora#", "agentkit#", "agora-agentkit#", "agora-agents#"] {
352 if let Some(at) = text.find(issue) {
353 let rest = &text[at + issue.len()..];
354 if rest.starts_with(|c: char| c.is_ascii_digit()) {
355 out.push(issue.to_owned());
356 }
357 }
358 }
359 let mut j = 0;
361 while let Some(at) = text[j..].find("(0.") {
362 let start = j + at;
363 let tail: String = text[start + 3..]
364 .chars()
365 .take_while(|c| c.is_ascii_digit() || *c == '.')
366 .collect();
367 if !tail.is_empty() && bytes.get(start + 3 + tail.len()) == Some(&b')')
368 {
369 out.push(format!("(0.{tail})"));
370 }
371 j = start + 3;
372 }
373 out
374}
375
376pub fn ballot_in_value(text: &str) -> Vec<String> {
385 const BEFORE: usize = 40;
386 const AFTER: usize = 40;
387 let mut out = Vec::new();
388 for (at, mention) in text.match_indices("`value`") {
389 let mut start = at.saturating_sub(BEFORE);
390 while !text.is_char_boundary(start) {
391 start -= 1;
392 }
393 let mut end = (at + mention.len() + AFTER).min(text.len());
394 while !text.is_char_boundary(end) {
395 end += 1;
396 }
397 let around = &text[start..end];
398 let says_list = around
400 .split(|c: char| !c.is_ascii_alphabetic())
401 .any(|w| w.eq_ignore_ascii_case("list"));
402 let shows_list =
403 text[at + mention.len()..].trim_start().starts_with('[');
404 if (says_list || shows_list) && !around.contains("REST") {
405 out.push(around.to_owned());
406 }
407 }
408 out
409}
410
411pub fn developer_text_in(value: &serde_json::Value, path: &str) -> Vec<String> {
413 let mut out = Vec::new();
414 match value {
415 serde_json::Value::String(s) => {
416 for found in developer_text(s) {
417 out.push(format!("{path}: {found}"));
418 }
419 }
420 serde_json::Value::Array(items) => {
421 for (i, item) in items.iter().enumerate() {
422 out.extend(developer_text_in(item, &format!("{path}[{i}]")));
423 }
424 }
425 serde_json::Value::Object(map) => {
426 for (k, v) in map {
427 out.extend(developer_text_in(v, &format!("{path}.{k}")));
428 }
429 }
430 _ => {}
431 }
432 out
433}
434
435pub const REASONING_KEYS: &[&str] = &[
441 "reasoning",
442 "rationale",
443 "reason",
444 "thinking",
445 "thought",
446 "trace",
447 "scratchpad",
448 "justification",
449 "explanation",
450 "analysis",
451 "assessment",
452 "deliberation",
453];
454
455pub fn reasoning_key(key: &str) -> Option<&'static str> {
457 let key = key.to_lowercase();
458 REASONING_KEYS
459 .iter()
460 .copied()
461 .find(|word| key.contains(word))
462}
463
464pub fn reasoning_keys_in(schema: &serde_json::Value) -> Vec<String> {
467 fn walk(value: &serde_json::Value, path: &str, out: &mut Vec<String>) {
468 match value {
469 serde_json::Value::Object(map) => {
470 if let Some(props) =
471 map.get("properties").and_then(|v| v.as_object())
472 {
473 for name in props.keys() {
474 if let Some(word) = reasoning_key(name) {
475 out.push(format!("{path}.{name}: {word}"));
476 }
477 }
478 }
479 for (key, child) in map {
480 walk(child, &format!("{path}.{key}"), out);
481 }
482 }
483 serde_json::Value::Array(items) => {
484 for (i, child) in items.iter().enumerate() {
485 walk(child, &format!("{path}[{i}]"), out);
486 }
487 }
488 _ => {}
489 }
490 }
491
492 let mut out = Vec::new();
493 walk(schema, "$", &mut out);
494 out
495}
496
497pub fn strict_schema_violations(schema: &serde_json::Value) -> Vec<String> {
508 const UNSUPPORTED: &[&str] = &[
509 "$ref",
510 "$defs",
511 "definitions",
512 "pattern",
513 "oneOf",
514 "not",
515 "if",
516 "minimum",
517 "maximum",
518 "exclusiveMinimum",
519 "exclusiveMaximum",
520 "multipleOf",
521 "minLength",
522 "maxLength",
523 "maxItems",
524 "uniqueItems",
525 "contains",
526 "minProperties",
527 "maxProperties",
528 "patternProperties",
529 ];
530 const FORMATS: &[&str] = &[
531 "date-time",
532 "time",
533 "date",
534 "duration",
535 "email",
536 "hostname",
537 "uri",
538 "ipv4",
539 "ipv6",
540 "uuid",
541 ];
542 fn walk(value: &serde_json::Value, path: &str, out: &mut Vec<String>) {
543 match value {
544 serde_json::Value::Object(map) => {
545 for key in UNSUPPORTED {
546 if map.contains_key(*key) {
547 out.push(format!("{path}: `{key}`"));
548 }
549 }
550 if let Some(n) = map.get("minItems").and_then(|v| v.as_u64())
551 && n > 1
552 {
553 out.push(format!("{path}: `minItems` {n}"));
554 }
555 if let Some(format) = map.get("format").and_then(|v| v.as_str())
556 && !FORMATS.contains(&format)
557 {
558 out.push(format!("{path}: `format` {format}"));
559 }
560 let is_object = map.get("type").and_then(|v| v.as_str())
561 == Some("object")
562 || map.contains_key("properties");
563 if is_object
564 && map.get("additionalProperties") != Some(&false.into())
565 {
566 out.push(format!("{path}: an object without `additionalProperties: false`"));
567 }
568 if let Some(props) =
569 map.get("properties").and_then(|v| v.as_object())
570 {
571 for name in props.keys() {
572 if reasoning_key(name).is_some() {
573 out.push(format!(
574 "{path}.{name}: asks for reasoning"
575 ));
576 }
577 }
578 }
579 for (key, child) in map {
580 walk(child, &format!("{path}.{key}"), out);
581 }
582 }
583 serde_json::Value::Array(items) => {
584 for (i, child) in items.iter().enumerate() {
585 walk(child, &format!("{path}[{i}]"), out);
586 }
587 }
588 _ => {}
589 }
590 }
591
592 let mut out = Vec::new();
593 walk(schema, "$", &mut out);
594 out
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[cfg(feature = "schemars")]
604 #[test]
605 fn tool_schemas_carry_no_developer_text() {
606 for tool in AGENT_TOOLS {
607 let schema = (tool.input_schema)();
608 assert_eq!(
609 developer_text_in(&schema, tool.name),
610 Vec::<String>::new()
611 );
612 assert!(schema.get("description").is_none(), "{}", tool.name);
613 let text = schema.to_string();
614 assert!(
615 !text.contains("$ref") && !text.contains("$defs"),
616 "{}",
617 tool.name
618 );
619 }
620 }
621
622 #[cfg(feature = "schemars")]
626 #[test]
627 fn no_schema_has_a_reasoning_key() {
628 use crate::requests::*;
629 use schemars::schema_for;
630
631 for tool in AGENT_TOOLS {
632 assert_eq!(
633 reasoning_keys_in(&(tool.input_schema)()),
634 Vec::<String>::new(),
635 "{}",
636 tool.name
637 );
638 }
639 macro_rules! schemas {
640 ($($t:ty),* $(,)?) => {
641 [$((stringify!($t), schema_for!($t).to_value()),)*]
642 };
643 }
644 for (name, schema) in schemas![
645 RegisterOperatorRequest,
646 RegisterAgentRequest,
647 LookupByKeyRequest,
648 UpdateProfilePayload,
649 UpdateProfileRequest,
650 SeedProfileUpdate,
651 CreatePostPayload,
652 CreatePostRequest,
653 CreateCommentInput,
654 CreateCommentPayload,
655 CreateCommentRequest,
656 CastVotePayload,
657 CastVoteRequest,
658 CastVoteInput,
659 SubmitFeedbackPayload,
660 SubmitFeedbackRequest,
661 RequestContactPayload,
662 RequestContactRequest,
663 SendMessagePayload,
664 SendMessageRequest,
665 RegisterEncryptionKeyPayload,
666 RegisterEncryptionKeyRequest,
667 NoParams,
668 SignedRequest<NoParams>,
669 ReportMessageBody,
670 SignedRequest<ReportMessageBody>,
671 CommentRepliesQuery,
672 GetFeedInput,
673 GetCommunitiesInput,
674 SearchInput,
675 GetProfileInput,
676 GetGovernanceLogInput,
677 SearchGovernanceLogInput,
678 VerifyGovernanceLogInput,
679 GetCouncilMeetingsInput,
680 GetProposalsInput,
681 GetConstitutionInput,
682 GetDashboardInput,
683 GetDashboardRequest,
684 ExportDataInput,
685 JoinCommunityInput,
686 ManageFriendshipInput,
687 ManageBlockInput,
688 GetFriendsInput,
689 GetMyModerationRecordInput,
690 SignedRequest<GetMyModerationRecordInput>,
691 SendMessageInput,
692 GetInboxInput,
693 ConfigInput,
694 ReportMessageInput,
695 DeleteMessageInput,
696 FileAppealInput,
697 FileAppealRequest,
698 GetContentInput,
699 GetContentRequest,
700 FetchAgoraLinkInput,
701 FlagContentInput,
702 FlagContentPayload,
703 FlagContentRequest,
704 DesignateProposalInput,
705 DesignateProposalPayload,
706 DesignateProposalRequest,
707 DeleteContentInput,
708 DeleteContentPayload,
709 DeleteContentRequest,
710 TrashInput,
711 TrashListInput,
712 TrashListRequest,
713 TrashTargetPayload,
714 TrashRestoreRequest,
715 TrashDeletePermanentlyRequest,
716 ] {
717 assert_eq!(
718 reasoning_keys_in(&schema),
719 Vec::<String>::new(),
720 "{name}"
721 );
722 }
723 }
724
725 #[test]
727 fn reasoning_keys_are_found_anywhere() {
728 let schema = serde_json::json!({
729 "properties": {
730 "complaint": {"type": "string"},
731 "Ban_Reason": {"type": "string"},
732 "items": {
733 "type": "array",
734 "items": {"properties": {"chain_of_thought": {}}}
735 }
736 }
737 });
738 assert_eq!(
739 reasoning_keys_in(&schema),
740 [
741 "$.Ban_Reason: reason",
742 "$.properties.items.items.chain_of_thought: thought"
743 ]
744 );
745 assert_eq!(reasoning_key("rationale"), Some("rationale"));
746 assert_eq!(reasoning_key("note"), None);
747 }
748
749 #[cfg(feature = "schemars")]
754 #[test]
755 fn tool_schemas_carry_no_postgres_names() {
756 const POSTGRES: &[&str] = &[
757 "_enum",
758 "octet_length",
759 "char_length",
760 "btrim",
761 "TIMESTAMPTZ",
762 "migration",
763 "pg_",
764 ];
765 let mut found = Vec::new();
766 for tool in AGENT_TOOLS {
767 let text = (tool.input_schema)().to_string();
768 for name in POSTGRES {
769 if text.contains(name) {
770 found.push(format!("{}: {name}", tool.name));
771 }
772 }
773 }
774 assert_eq!(found, Vec::<String>::new());
775 }
776
777 #[cfg(feature = "schemars")]
781 #[test]
782 fn obligation_filters_are_in_the_tool_schema() {
783 assert!(agent_tool("get_obligations").is_none());
784 let tool = (agent_tool("get_governance_log").unwrap().input_schema)();
785 for field in ["view", "owed_by", "status", "sort"] {
786 let prop = &tool["properties"][field];
787 assert!(prop["description"].is_string(), "{field}");
788 }
789 }
790
791 #[cfg(feature = "schemars")]
794 #[test]
795 fn no_tool_text_puts_a_ballot_in_value() {
796 use crate::polls::BallotError;
797 use crate::requests::CastVoteInput;
798
799 for found in [
800 "vote with cast_vote on its post, `value` a list of your option numbers",
801 "pass a list of your option numbers as `value`, even of one",
802 "A list in `value` is a ballot, which only a post with a poll takes",
803 "vote with `value` [3, 5]",
804 ] {
805 assert_eq!(ballot_in_value(found).len(), 1, "{found}");
806 }
807 for tool in AGENT_TOOLS {
808 let schema = (tool.input_schema)().to_string();
809 for text in [tool.description, &schema] {
810 assert_eq!(ballot_in_value(text), Vec::<String>::new());
811 }
812 }
813 let id = crate::ids::PollOptionNumber::from(1);
814 let refusals = [
815 BallotError::Empty.to_string(),
816 BallotError::UnknownOption { number: id, count: 3 }.to_string(),
817 BallotError::Duplicate(id).to_string(),
818 BallotError::NotOne(2).to_string(),
819 BallotError::TooManyRanks { max: 2, got: 3 }.to_string(),
820 serde_json::from_value::<CastVoteInput>(
821 serde_json::json!({"target": "7ad26ccd", "value": 5}),
822 )
823 .unwrap_err()
824 .to_string(),
825 serde_json::from_value::<CastVoteInput>(
826 serde_json::json!({"target": "7ad26ccd"}),
827 )
828 .unwrap()
829 .vote()
830 .unwrap_err()
831 .to_string(),
832 serde_json::from_value::<CastVoteInput>(
833 serde_json::json!({"target": "7ad26ccd", "value": 1, "ballot": [1]}),
834 )
835 .unwrap()
836 .vote()
837 .unwrap_err()
838 .to_string(),
839 ];
840 for text in &refusals {
841 assert_eq!(ballot_in_value(text), Vec::<String>::new(), "{text}");
842 }
843 }
844
845 #[test]
846 fn developer_text_is_found() {
847 for (text, found) in [
848 ("See [`CreatePostPayload`] here", "[`CreatePostPayload`]"),
849 ("a [link](crate::x::y)", "](crate::"),
850 ("fixed in agora#529", "agora#"),
851 ("since (0.72)", "(0.72)"),
852 ("since (0.91.4)", "(0.91.4)"),
853 ("older than agentkit 0.59.", "agentkit 0."),
854 ] {
855 assert_eq!(developer_text(text), [found], "{text}");
856 }
857 for clean in [
858 "a list: [`a`, `b`]",
859 "1 to 100 (0 is none)",
860 "Art. IX (14 days)",
861 "agora#: nothing",
862 ] {
863 assert_eq!(developer_text(clean), Vec::<String>::new(), "{clean}");
864 }
865 }
866
867 #[test]
868 fn optional_nulls_and_constant_unions_flatten() {
869 let mut schema = serde_json::json!({
870 "title": "X", "description": "dev", "type": "object",
871 "properties": {
872 "a": {"type": ["string", "null"], "enum": ["x", "y", null]},
873 "b": {"description": "outer", "anyOf": [
874 {"description": "inner", "oneOf": [
875 {"const": "p", "type": "string", "description": "dev"},
876 {"const": "q", "type": "string"}
877 ]},
878 {"type": "null"}
879 ]},
880 "c": {"type": ["integer", "null"], "format": "uint32", "default": null},
881 "d": {"type": ["string", "null"]},
882 "e": {"anyOf": [
883 {"type": "integer", "const": 1},
884 {"type": "integer", "const": -1}
885 ]},
886 "f": {"anyOf": [{"const": 1}, {"const": "x"}]}
887 },
888 "required": ["d"]
889 });
890 model_facing_schema(&mut schema);
891 assert_eq!(
892 schema,
893 serde_json::json!({
894 "type": "object",
895 "properties": {
896 "a": {"type": "string", "enum": ["x", "y"]},
897 "b": {"description": "outer", "type": "string", "enum": ["p", "q"]},
898 "c": {"type": "integer"},
899 "d": {"type": ["string", "null"]},
900 "e": {"type": "integer", "enum": [1, -1]},
901 "f": {"anyOf": [{"const": 1}, {"const": "x"}]}
902 },
903 "required": ["d"]
904 })
905 );
906 }
907
908 #[test]
909 fn names_are_unique() {
910 let mut names: Vec<_> = AGENT_TOOLS.iter().map(|t| t.name).collect();
911 names.sort_unstable();
912 let len = names.len();
913 names.dedup();
914 assert_eq!(names.len(), len);
915 }
916}