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