use serde::Serialize;
#[derive(Debug, Clone, Copy, Serialize)]
pub struct BuiltinAgent {
pub id: &'static str,
pub display_name: &'static str,
pub description: &'static str,
pub capabilities: &'static [&'static str],
pub system_prompt: &'static str,
}
pub const BUILTIN_AGENTS: &[BuiltinAgent] = &[
BuiltinAgent {
id: "researcher",
display_name: "Researcher",
description: "Finds factual answers and cites sources. Flags uncertainty.",
capabilities: &["research", "search-knowledge"],
system_prompt: "You are a research assistant. Answer factually, cite sources \
when possible, and flag uncertainty explicitly.",
},
BuiltinAgent {
id: "summarizer",
display_name: "Summarizer",
description: "Concise summaries — bullet points by default, prose on request.",
capabilities: &["summarize"],
system_prompt: "You are a summarization agent. Produce a concise, accurate \
summary of the input. Default to bullet points unless the \
user requests prose.",
},
BuiltinAgent {
id: "verifier",
display_name: "Verifier",
description: "Evaluates the truthfulness of a claim with reasoning.",
capabilities: &["verify-claim"],
system_prompt: "You are a verifier. Given a claim, evaluate whether it's \
likely true, likely false, or uncertain. Show your reasoning.",
},
BuiltinAgent {
id: "transcriber",
display_name: "Transcriber",
description: "Cleans up transcripts: punctuation, mis-recognitions, speaker turns.",
capabilities: &["transcribe-audio"],
system_prompt: "You are a transcription assistant. Clean up the input \
transcript: fix obvious mis-recognitions, restore punctuation, \
and preserve speaker turns when present.",
},
BuiltinAgent {
id: "note-taker",
display_name: "Note Taker",
description: "Captures decisions, action items, and open questions as bullets.",
capabilities: &["create-note", "summarize"],
system_prompt: "You are a note-taking assistant. Capture decisions, action \
items, and open questions from the input as terse bullet lists.",
},
];
pub fn agent_metadata(id: &str) -> Option<&'static BuiltinAgent> {
BUILTIN_AGENTS.iter().find(|a| a.id == id)
}
pub fn register_builtins(registry: &super::AgentCapabilityRegistry) {
for agent in BUILTIN_AGENTS {
for cap in agent.capabilities {
registry.register(*cap, agent.id);
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CapabilityPayloadError {
#[error("payload_json is not valid JSON: {0}")]
InvalidJson(String),
#[error("capability '{capability}' requires JSON object with string field '{field}'")]
MissingField {
capability: String,
field: &'static str,
},
}
pub fn format_capability_payload(
capability: &str,
payload_json: &str,
) -> Result<String, CapabilityPayloadError> {
let v: serde_json::Value = serde_json::from_str(payload_json)
.map_err(|e| CapabilityPayloadError::InvalidJson(e.to_string()))?;
let pull = |field: &'static str| -> Result<String, CapabilityPayloadError> {
v.get(field)
.and_then(|t| t.as_str())
.map(|s| s.to_string())
.ok_or_else(|| CapabilityPayloadError::MissingField {
capability: capability.to_string(),
field,
})
};
Ok(match capability {
"summarize" => format!("Summarize the following:\n\n{}", pull("text")?),
"transcribe-audio" => format!("Transcribe the audio file at: {}", pull("audio_path")?),
"research" | "search-knowledge" => format!("Research and answer: {}", pull("question")?),
"verify-claim" => format!("Verify whether this claim is accurate: {}", pull("claim")?),
"create-note" => format!("Capture notes for:\n\n{}", pull("content")?),
_ => payload_json.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_builtin_has_unique_id() {
let mut ids: Vec<_> = BUILTIN_AGENTS.iter().map(|a| a.id).collect();
ids.sort();
let n = ids.len();
ids.dedup();
assert_eq!(ids.len(), n, "duplicate id in BUILTIN_AGENTS");
}
#[test]
fn register_builtins_populates_registry() {
let r = crate::AgentCapabilityRegistry::new();
register_builtins(&r);
let summarizers = r.agents_for("summarize");
assert!(summarizers.contains(&"summarizer".to_string()));
assert!(summarizers.contains(&"note-taker".to_string()));
}
#[test]
fn format_payload_summarize_happy_path() {
let p = r#"{"text":"hello world"}"#;
let out = format_capability_payload("summarize", p).unwrap();
assert!(out.contains("hello world"));
}
#[test]
fn format_payload_summarize_missing_field_errors() {
let p = r#"{"txet":"typo"}"#; let err = format_capability_payload("summarize", p).unwrap_err();
match err {
CapabilityPayloadError::MissingField { field, .. } => assert_eq!(field, "text"),
other => panic!("wrong error: {other:?}"),
}
}
#[test]
fn format_payload_invalid_json_errors() {
let err = format_capability_payload("summarize", "{").unwrap_err();
assert!(matches!(err, CapabilityPayloadError::InvalidJson(_)));
}
#[test]
fn agent_metadata_lookups() {
let r = agent_metadata("researcher").expect("present");
assert_eq!(r.display_name, "Researcher");
assert!(r.capabilities.contains(&"research"));
assert!(agent_metadata("nonexistent").is_none());
}
}