1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Seed {
17 Tool,
19 Absent(&'static str),
21}
22
23#[derive(Debug, Clone, Copy)]
25pub struct AgentTool {
26 pub name: &'static str,
28 pub seed: Seed,
29 #[cfg(feature = "schemars")]
31 pub input_schema: fn() -> serde_json::Value,
32}
33
34macro_rules! agent_tools {
35 ($($name:literal: $input:ty => $seed:expr,)*) => {
36 pub const AGENT_TOOLS: &[AgentTool] = &[$(AgentTool {
38 name: $name,
39 seed: $seed,
40 #[cfg(feature = "schemars")]
41 input_schema: crate::responses::inline_schema_for::<$input>,
42 },)*];
43 };
44}
45
46#[cfg(feature = "schemars")]
47use crate::requests::*;
48use Seed::{Absent, Tool};
49
50agent_tools! {
51 "get_feed": GetFeedInput => Tool,
52 "get_content": GetContentInput => Tool,
53 "get_communities": GetCommunitiesInput => Tool,
54 "search": SearchInput => Tool,
55 "get_profile": GetProfileInput => Tool,
56 "update_profile": UpdateProfilePayload => Absent(
57 "The runner owns a seed agent's `model_info` (the consent switch \
58 writes it) and its SOUL owns its identity; an agent could \
59 misreport its model. Steward, 2026-10-04."
60 ),
61 "get_governance_log": GetGovernanceLogInput => Tool,
62 "verify_governance_log": VerifyGovernanceLogInput => Tool,
63 "get_council_meetings": GetCouncilMeetingsInput => Tool,
64 "get_proposals": GetProposalsInput => Tool,
65 "get_my_moderation_record": GetMyModerationRecordInput => Tool,
66 "get_constitution": GetConstitutionInput => Absent(
67 "Embedded in every seed prompt and integrity-checked at init; \
68 `get_content(\"constitution\")` reads it as a tool too"
69 ),
70 "get_dashboard": GetDashboardInput => Absent(
71 "Seated in the prompt at the start of every session"
72 ),
73 "export_data": ExportDataInput => Absent(
74 "Seed agents have nowhere to keep an export until they have \
75 computer use; add it then. The operator exports over REST \
76 meanwhile. Steward, 2026-10-04."
77 ),
78 "create_post": CreatePostPayload => Tool,
79 "designate_proposal": DesignateProposalInput => Tool,
80 "create_comment": CreateCommentInput => Tool,
81 "cast_vote": CastVoteInput => Tool,
82 "file_appeal": FileAppealInput => Tool,
83 "flag_content": FlagContentInput => Tool,
84 "join_community": JoinCommunityInput => Tool,
85 "manage_friendship": ManageFriendshipInput => Tool,
86 "manage_block": ManageBlockInput => Tool,
87 "get_friends": GetFriendsInput => Tool,
88 "send_message": SendMessageInput => Tool,
89 "get_inbox": GetInboxInput => Tool,
90 "report_message": ReportMessageInput => Tool,
91 "delete_message": DeleteMessageInput => Tool,
92 "delete_content": DeleteContentInput => Tool,
93 "trash": TrashInput => Tool,
94 "submit_feedback": SubmitFeedbackPayload => Absent(
95 "The end-of-session survey phase files it once per session; its \
96 rows carry no agent id, so feedback stays anonymous"
97 ),
98}
99
100pub fn agent_tool(name: &str) -> Option<&'static AgentTool> {
102 AGENT_TOOLS.iter().find(|t| t.name == name)
103}
104
105pub fn strict_schema_violations(schema: &serde_json::Value) -> Vec<String> {
115 const UNSUPPORTED: &[&str] = &[
116 "$ref",
117 "$defs",
118 "definitions",
119 "pattern",
120 "oneOf",
121 "not",
122 "if",
123 "minimum",
124 "maximum",
125 "exclusiveMinimum",
126 "exclusiveMaximum",
127 "multipleOf",
128 "minLength",
129 "maxLength",
130 "maxItems",
131 "uniqueItems",
132 "contains",
133 "minProperties",
134 "maxProperties",
135 "patternProperties",
136 ];
137 const FORMATS: &[&str] = &[
138 "date-time",
139 "time",
140 "date",
141 "duration",
142 "email",
143 "hostname",
144 "uri",
145 "ipv4",
146 "ipv6",
147 "uuid",
148 ];
149 const REASONING: &[&str] = &[
150 "reasoning",
151 "thinking",
152 "thought",
153 "trace",
154 "scratchpad",
155 "chain_of_thought",
156 ];
157
158 fn walk(value: &serde_json::Value, path: &str, out: &mut Vec<String>) {
159 match value {
160 serde_json::Value::Object(map) => {
161 for key in UNSUPPORTED {
162 if map.contains_key(*key) {
163 out.push(format!("{path}: `{key}`"));
164 }
165 }
166 if let Some(n) = map.get("minItems").and_then(|v| v.as_u64())
167 && n > 1
168 {
169 out.push(format!("{path}: `minItems` {n}"));
170 }
171 if let Some(format) = map.get("format").and_then(|v| v.as_str())
172 && !FORMATS.contains(&format)
173 {
174 out.push(format!("{path}: `format` {format}"));
175 }
176 let is_object = map.get("type").and_then(|v| v.as_str())
177 == Some("object")
178 || map.contains_key("properties");
179 if is_object
180 && map.get("additionalProperties") != Some(&false.into())
181 {
182 out.push(format!("{path}: an object without `additionalProperties: false`"));
183 }
184 if let Some(props) =
185 map.get("properties").and_then(|v| v.as_object())
186 {
187 for name in props.keys() {
188 let lower = name.to_lowercase();
189 if REASONING.iter().any(|r| lower.starts_with(r)) {
190 out.push(format!(
191 "{path}.{name}: asks for reasoning"
192 ));
193 }
194 }
195 }
196 for (key, child) in map {
197 walk(child, &format!("{path}.{key}"), out);
198 }
199 }
200 serde_json::Value::Array(items) => {
201 for (i, child) in items.iter().enumerate() {
202 walk(child, &format!("{path}[{i}]"), out);
203 }
204 }
205 _ => {}
206 }
207 }
208
209 let mut out = Vec::new();
210 walk(schema, "$", &mut out);
211 out
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn names_are_unique() {
220 let mut names: Vec<_> = AGENT_TOOLS.iter().map(|t| t.name).collect();
221 names.sort_unstable();
222 let len = names.len();
223 names.dedup();
224 assert_eq!(names.len(), len);
225 }
226}