1use serde::Serialize;
26
27#[derive(Debug, Clone, Copy, Serialize)]
29pub struct BuiltinAgent {
30 pub id: &'static str,
32 pub display_name: &'static str,
35 pub description: &'static str,
38 pub capabilities: &'static [&'static str],
41 pub system_prompt: &'static str,
46}
47
48pub const BUILTIN_AGENTS: &[BuiltinAgent] = &[
51 BuiltinAgent {
52 id: "researcher",
53 display_name: "Researcher",
54 description: "Finds factual answers and cites sources. Flags uncertainty.",
55 capabilities: &["research", "search-knowledge"],
56 system_prompt: "You are a research assistant. Answer factually, cite sources \
57 when possible, and flag uncertainty explicitly.",
58 },
59 BuiltinAgent {
60 id: "summarizer",
61 display_name: "Summarizer",
62 description: "Concise summaries — bullet points by default, prose on request.",
63 capabilities: &["summarize"],
64 system_prompt: "You are a summarization agent. Produce a concise, accurate \
65 summary of the input. Default to bullet points unless the \
66 user requests prose.",
67 },
68 BuiltinAgent {
69 id: "verifier",
70 display_name: "Verifier",
71 description: "Evaluates the truthfulness of a claim with reasoning.",
72 capabilities: &["verify-claim"],
73 system_prompt: "You are a verifier. Given a claim, evaluate whether it's \
74 likely true, likely false, or uncertain. Show your reasoning.",
75 },
76 BuiltinAgent {
77 id: "transcriber",
78 display_name: "Transcriber",
79 description: "Cleans up transcripts: punctuation, mis-recognitions, speaker turns.",
80 capabilities: &["transcribe-audio"],
81 system_prompt: "You are a transcription assistant. Clean up the input \
82 transcript: fix obvious mis-recognitions, restore punctuation, \
83 and preserve speaker turns when present.",
84 },
85 BuiltinAgent {
86 id: "note-taker",
87 display_name: "Note Taker",
88 description: "Captures decisions, action items, and open questions as bullets.",
89 capabilities: &["create-note", "summarize"],
90 system_prompt: "You are a note-taking assistant. Capture decisions, action \
91 items, and open questions from the input as terse bullet lists.",
92 },
93];
94
95pub fn agent_metadata(id: &str) -> Option<&'static BuiltinAgent> {
97 BUILTIN_AGENTS.iter().find(|a| a.id == id)
98}
99
100pub fn register_builtins(registry: &super::AgentCapabilityRegistry) {
103 for agent in BUILTIN_AGENTS {
104 for cap in agent.capabilities {
105 registry.register(*cap, agent.id);
106 }
107 }
108}
109
110#[derive(Debug, thiserror::Error)]
114pub enum CapabilityPayloadError {
115 #[error("payload_json is not valid JSON: {0}")]
116 InvalidJson(String),
117
118 #[error("capability '{capability}' requires JSON object with string field '{field}'")]
119 MissingField {
120 capability: String,
121 field: &'static str,
122 },
123}
124
125pub fn format_capability_payload(
136 capability: &str,
137 payload_json: &str,
138) -> Result<String, CapabilityPayloadError> {
139 let v: serde_json::Value = serde_json::from_str(payload_json)
140 .map_err(|e| CapabilityPayloadError::InvalidJson(e.to_string()))?;
141
142 let pull = |field: &'static str| -> Result<String, CapabilityPayloadError> {
143 v.get(field)
144 .and_then(|t| t.as_str())
145 .map(|s| s.to_string())
146 .ok_or_else(|| CapabilityPayloadError::MissingField {
147 capability: capability.to_string(),
148 field,
149 })
150 };
151
152 Ok(match capability {
153 "summarize" => format!("Summarize the following:\n\n{}", pull("text")?),
154 "transcribe-audio" => format!("Transcribe the audio file at: {}", pull("audio_path")?),
155 "research" | "search-knowledge" => format!("Research and answer: {}", pull("question")?),
156 "verify-claim" => format!("Verify whether this claim is accurate: {}", pull("claim")?),
157 "create-note" => format!("Capture notes for:\n\n{}", pull("content")?),
158 _ => payload_json.to_string(),
163 })
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn every_builtin_has_unique_id() {
172 let mut ids: Vec<_> = BUILTIN_AGENTS.iter().map(|a| a.id).collect();
173 ids.sort();
174 let n = ids.len();
175 ids.dedup();
176 assert_eq!(ids.len(), n, "duplicate id in BUILTIN_AGENTS");
177 }
178
179 #[test]
180 fn register_builtins_populates_registry() {
181 let r = crate::AgentCapabilityRegistry::new();
182 register_builtins(&r);
183 let summarizers = r.agents_for("summarize");
185 assert!(summarizers.contains(&"summarizer".to_string()));
186 assert!(summarizers.contains(&"note-taker".to_string()));
187 }
188
189 #[test]
190 fn format_payload_summarize_happy_path() {
191 let p = r#"{"text":"hello world"}"#;
192 let out = format_capability_payload("summarize", p).unwrap();
193 assert!(out.contains("hello world"));
194 }
195
196 #[test]
197 fn format_payload_summarize_missing_field_errors() {
198 let p = r#"{"txet":"typo"}"#; let err = format_capability_payload("summarize", p).unwrap_err();
200 match err {
201 CapabilityPayloadError::MissingField { field, .. } => assert_eq!(field, "text"),
202 other => panic!("wrong error: {other:?}"),
203 }
204 }
205
206 #[test]
207 fn format_payload_invalid_json_errors() {
208 let err = format_capability_payload("summarize", "{").unwrap_err();
209 assert!(matches!(err, CapabilityPayloadError::InvalidJson(_)));
210 }
211
212 #[test]
213 fn agent_metadata_lookups() {
214 let r = agent_metadata("researcher").expect("present");
215 assert_eq!(r.display_name, "Researcher");
216 assert!(r.capabilities.contains(&"research"));
217 assert!(agent_metadata("nonexistent").is_none());
218 }
219}