Output Formatting Guide
This guide covers the Herald system for formatting and controlling Paladin output in various formats and styles.
Table of Contents
- Overview
- Herald Architecture
- Built-in Formatters
- Custom Formatters
- Streaming Output
- Multi-Format Output
- Post-Processing
- Best Practices
- Advanced Patterns
- Troubleshooting
Overview
The Herald system controls how Paladin output is formatted and presented to users.
Key Capabilities:
- Format Transformation: Convert LLM output to JSON, Markdown, HTML, etc.
- Streaming: Real-time output delivery for better UX
- Validation: Ensure output meets schema requirements
- Post-Processing: Clean, enhance, or transform responses
- Multi-Channel: Different formats for different output destinations
Key Concepts:
- Herald: Output formatting system
- Formatter: Converts raw LLM output to specific format
- OutputFormat: Target format specification (JSON, Markdown, Plain, etc.)
- StreamHandler: Processes output chunks in real-time
Herald Architecture
Core Components
#![allow(unused)] fn main() { // Output format types pub enum OutputFormat { Plain, // Raw LLM output Markdown, // Markdown-formatted Json, // Structured JSON Html, // HTML rendering Custom(String), // Custom format name } // Herald interface #[async_trait] pub trait Herald: Send + Sync { /// Format complete output async fn format(&self, content: &str) -> Result<String, HeraldError>; /// Format streaming chunk async fn format_chunk(&self, chunk: &str) -> Result<String, HeraldError>; /// Validate output against format requirements fn validate(&self, content: &str) -> Result<(), HeraldError>; /// Get format metadata fn metadata(&self) -> FormatMetadata; } // Format metadata pub struct FormatMetadata { pub format_name: String, pub mime_type: String, pub file_extension: String, pub supports_streaming: bool, } }
Integration with Paladin
#![allow(unused)] fn main() { let paladin = PaladinBuilder::new(llm_adapter) .name("Assistant") .system_prompt("You are a helpful assistant.") .output_format(OutputFormat::Markdown) .with_herald(Arc::new(MarkdownHerald::default())) .build()?; let response = paladin.execute("Explain async/await").await?; // response.content is formatted as Markdown }
Built-in Formatters
Plain Text Herald
No formatting, returns raw LLM output.
#![allow(unused)] fn main() { use paladin::herald::*; let herald = Arc::new(PlainHerald::default()); let paladin = PaladinBuilder::new(llm_adapter) .with_herald(herald) .build()?; let response = paladin.execute("Hello").await?; println!("{}", response.content); // Raw output }
Markdown Herald
Formats output as Markdown with proper structure.
#![allow(unused)] fn main() { use paladin::herald::*; let herald = Arc::new(MarkdownHerald::new() .with_code_highlighting(true) .with_header_ids(true) .with_table_of_contents(true) ); let paladin = PaladinBuilder::new(llm_adapter) .system_prompt("Format all responses as Markdown with proper headers and code blocks.") .with_herald(herald) .build()?; let response = paladin.execute("Explain Rust ownership").await?; println!("{}", response.content); }
Output example:
# Rust Ownership
Ownership is a core concept in Rust that ensures memory safety.
## Key Rules
1. Each value has a single owner
2. When the owner goes out of scope, the value is dropped
3. Values can be borrowed immutably or mutably
## Example
```rust
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved
// println!("{}", s1); // Error: s1 is no longer valid
}
Benefits
- Memory safety without garbage collection
- No data races at compile time
- Zero-cost abstractions
### JSON Herald
Formats output as structured JSON.
```rust
use paladin::herald::*;
use serde_json::json;
let herald = Arc::new(JsonHerald::new()
.with_schema(json!({
"type": "object",
"properties": {
"summary": {"type": "string"},
"key_points": {
"type": "array",
"items": {"type": "string"}
},
"confidence": {"type": "number"}
},
"required": ["summary", "key_points"]
}))
.validate_output(true)
);
let paladin = PaladinBuilder::new(llm_adapter)
.system_prompt("Always respond in JSON format matching this schema: \
{summary: string, key_points: string[], confidence: number}")
.with_herald(herald)
.build()?;
let response = paladin.execute("Analyze sentiment of: 'This product is amazing!'").await?;
// Parse structured output
let json: serde_json::Value = serde_json::from_str(&response.content)?;
println!("Summary: {}", json["summary"]);
println!("Key points: {:?}", json["key_points"]);
Output example:
{
"summary": "Highly positive sentiment expressing enthusiasm",
"key_points": [
"Strong positive emotion indicated by 'amazing'",
"Exclamation mark reinforces enthusiasm",
"No negative indicators present"
],
"confidence": 0.95
}
HTML Herald
Formats output as styled HTML.
#![allow(unused)] fn main() { use paladin::herald::*; let herald = Arc::new(HtmlHerald::new() .with_css_framework(CssFramework::Tailwind) .with_syntax_highlighting(true) .with_responsive_design(true) ); let paladin = PaladinBuilder::new(llm_adapter) .with_herald(herald) .build()?; let response = paladin.execute("Create a todo list").await?; // Serve as web page let html = format!(r#" <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Paladin Response</title> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2/dist/tailwind.min.css" rel="stylesheet"> </head> <body class="bg-gray-100 p-8"> {} </body> </html> "#, response.content); }
Code Herald
Specialized for code generation with syntax validation.
#![allow(unused)] fn main() { use paladin::herald::*; let herald = Arc::new(CodeHerald::new() .language("rust") .with_syntax_check(true) .with_formatting(true) .with_linting(true) ); let paladin = PaladinBuilder::new(llm_adapter) .system_prompt("You are a Rust code generator. Return ONLY valid Rust code.") .with_herald(herald) .build()?; let response = paladin.execute("Write a function to reverse a string").await?; // Output is validated, formatted Rust code println!("{}", response.content); }
Output:
#![allow(unused)] fn main() { pub fn reverse_string(s: &str) -> String { s.chars().rev().collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_reverse_string() { assert_eq!(reverse_string("hello"), "olleh"); assert_eq!(reverse_string(""), ""); } } }
Custom Formatters
Create custom heralds for specialized output formats.
Simple Custom Herald
#![allow(unused)] fn main() { use paladin::herald::*; use async_trait::async_trait; pub struct UppercaseHerald; #[async_trait] impl Herald for UppercaseHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { Ok(content.to_uppercase()) } async fn format_chunk(&self, chunk: &str) -> Result<String, HeraldError> { Ok(chunk.to_uppercase()) } fn validate(&self, _content: &str) -> Result<(), HeraldError> { Ok(()) // No validation needed } fn metadata(&self) -> FormatMetadata { FormatMetadata { format_name: "uppercase".to_string(), mime_type: "text/plain".to_string(), file_extension: "txt".to_string(), supports_streaming: true, } } } // Usage let herald = Arc::new(UppercaseHerald); let paladin = PaladinBuilder::new(llm_adapter) .with_herald(herald) .build()?; }
XML Herald
#![allow(unused)] fn main() { use paladin::herald::*; use quick_xml::Writer; use std::io::Cursor; pub struct XmlHerald { root_element: String, } impl XmlHerald { pub fn new(root_element: &str) -> Self { Self { root_element: root_element.to_string(), } } } #[async_trait] impl Herald for XmlHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { let mut writer = Writer::new(Cursor::new(Vec::new())); // Write XML declaration writer.write_event(quick_xml::events::Event::Decl( quick_xml::events::BytesDecl::new("1.0", Some("UTF-8"), None) ))?; // Parse content as structured data let data: serde_json::Value = serde_json::from_str(content) .map_err(|e| HeraldError::FormatError(e.to_string()))?; // Convert to XML self.json_to_xml(&mut writer, &self.root_element, &data)?; let xml_bytes = writer.into_inner().into_inner(); Ok(String::from_utf8(xml_bytes)?) } fn validate(&self, content: &str) -> Result<(), HeraldError> { // Validate JSON structure serde_json::from_str::<serde_json::Value>(content) .map(|_| ()) .map_err(|e| HeraldError::ValidationError(e.to_string())) } fn metadata(&self) -> FormatMetadata { FormatMetadata { format_name: "xml".to_string(), mime_type: "application/xml".to_string(), file_extension: "xml".to_string(), supports_streaming: false, } } } // Usage let herald = Arc::new(XmlHerald::new("response")); let paladin = PaladinBuilder::new(llm_adapter) .system_prompt("Return JSON that will be converted to XML") .with_herald(herald) .build()?; }
CSV Herald
#![allow(unused)] fn main() { use paladin::herald::*; use csv::Writer; pub struct CsvHerald { headers: Vec<String>, delimiter: u8, } impl CsvHerald { pub fn new(headers: Vec<String>) -> Self { Self { headers, delimiter: b',', } } pub fn with_delimiter(mut self, delimiter: u8) -> Self { self.delimiter = delimiter; self } } #[async_trait] impl Herald for CsvHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { // Parse JSON array let rows: Vec<serde_json::Value> = serde_json::from_str(content) .map_err(|e| HeraldError::FormatError(e.to_string()))?; let mut wtr = Writer::from_writer(vec![]); // Write headers wtr.write_record(&self.headers)?; // Write data rows for row in rows { let record: Vec<String> = self.headers.iter() .map(|h| { row.get(h) .map(|v| v.to_string()) .unwrap_or_default() }) .collect(); wtr.write_record(&record)?; } wtr.flush()?; let csv_bytes = wtr.into_inner()?; Ok(String::from_utf8(csv_bytes)?) } fn validate(&self, content: &str) -> Result<(), HeraldError> { // Validate JSON array structure let _: Vec<serde_json::Value> = serde_json::from_str(content) .map_err(|e| HeraldError::ValidationError(e.to_string()))?; Ok(()) } fn metadata(&self) -> FormatMetadata { FormatMetadata { format_name: "csv".to_string(), mime_type: "text/csv".to_string(), file_extension: "csv".to_string(), supports_streaming: false, } } } // Usage let herald = Arc::new(CsvHerald::new(vec![ "name".to_string(), "age".to_string(), "city".to_string(), ])); let paladin = PaladinBuilder::new(llm_adapter) .system_prompt("Return data as JSON array of objects with name, age, city fields") .with_herald(herald) .build()?; let response = paladin.execute("Generate 5 sample user records").await?; // Output is formatted CSV }
Streaming Output
Process and format output in real-time for better user experience.
Basic Streaming
#![allow(unused)] fn main() { use paladin::herald::*; use futures::StreamExt; let herald = Arc::new(MarkdownHerald::default()); let paladin = PaladinBuilder::new(llm_adapter) .with_herald(herald.clone()) .build()?; // Execute with streaming let mut stream = paladin.execute_stream("Write a story").await?; while let Some(chunk) = stream.next().await { let chunk = chunk?; // Format chunk let formatted = herald.format_chunk(&chunk.content).await?; // Print in real-time print!("{}", formatted); std::io::stdout().flush()?; } println!(); }
Streaming with Accumulation
#![allow(unused)] fn main() { pub struct StreamAccumulator { herald: Arc<dyn Herald>, buffer: String, } impl StreamAccumulator { pub fn new(herald: Arc<dyn Herald>) -> Self { Self { herald, buffer: String::new(), } } pub async fn process_chunk(&mut self, chunk: &str) -> Result<String, HeraldError> { self.buffer.push_str(chunk); // Format accumulated content self.herald.format(&self.buffer).await } pub fn buffer(&self) -> &str { &self.buffer } } // Usage let mut accumulator = StreamAccumulator::new(herald); let mut stream = paladin.execute_stream("Explain quantum computing").await?; while let Some(chunk) = stream.next().await { let chunk = chunk?; let formatted_so_far = accumulator.process_chunk(&chunk.content).await?; // Update UI with fully formatted content update_ui(&formatted_so_far); } }
Progress Indicators
#![allow(unused)] fn main() { pub struct ProgressHerald { inner: Arc<dyn Herald>, show_progress: bool, } #[async_trait] impl Herald for ProgressHerald { async fn format_chunk(&self, chunk: &str) -> Result<String, HeraldError> { let formatted = self.inner.format_chunk(chunk).await?; if self.show_progress { // Add visual progress indicator Ok(format!("{} .", formatted)) } else { Ok(formatted) } } async fn format(&self, content: &str) -> Result<String, HeraldError> { self.inner.format(content).await } fn validate(&self, content: &str) -> Result<(), HeraldError> { self.inner.validate(content) } fn metadata(&self) -> FormatMetadata { self.inner.metadata() } } }
Multi-Format Output
Generate output in multiple formats simultaneously.
Multi-Format Herald
#![allow(unused)] fn main() { pub struct MultiFormatHerald { heralds: HashMap<String, Arc<dyn Herald>>, } impl MultiFormatHerald { pub fn new() -> Self { Self { heralds: HashMap::new(), } } pub fn add_format(mut self, name: &str, herald: Arc<dyn Herald>) -> Self { self.heralds.insert(name.to_string(), herald); self } pub async fn format_all(&self, content: &str) -> Result<HashMap<String, String>, HeraldError> { let mut results = HashMap::new(); for (name, herald) in &self.heralds { let formatted = herald.format(content).await?; results.insert(name.clone(), formatted); } Ok(results) } } // Usage let multi_herald = MultiFormatHerald::new() .add_format("json", Arc::new(JsonHerald::default())) .add_format("markdown", Arc::new(MarkdownHerald::default())) .add_format("html", Arc::new(HtmlHerald::default())); let paladin = PaladinBuilder::new(llm_adapter).build()?; let response = paladin.execute("Summarize Rust features").await?; // Generate all formats let all_formats = multi_herald.format_all(&response.content).await?; // Save or serve each format std::fs::write("output.json", &all_formats["json"])?; std::fs::write("output.md", &all_formats["markdown"])?; std::fs::write("output.html", &all_formats["html"])?; }
Adaptive Format Selection
#![allow(unused)] fn main() { pub struct AdaptiveHerald { formats: HashMap<String, Arc<dyn Herald>>, default: Arc<dyn Herald>, } impl AdaptiveHerald { pub async fn format_for_context( &self, content: &str, context: &OutputContext, ) -> Result<String, HeraldError> { let herald = self.select_herald(context); herald.format(content).await } fn select_herald(&self, context: &OutputContext) -> &Arc<dyn Herald> { match context.channel { OutputChannel::Web => self.formats.get("html").unwrap_or(&self.default), OutputChannel::Api => self.formats.get("json").unwrap_or(&self.default), OutputChannel::Terminal => self.formats.get("markdown").unwrap_or(&self.default), OutputChannel::File(ref ext) => { self.formats.get(ext.as_str()).unwrap_or(&self.default) } } } } pub struct OutputContext { pub channel: OutputChannel, pub user_preferences: HashMap<String, String>, } pub enum OutputChannel { Web, Api, Terminal, File(String), } // Usage let adaptive = AdaptiveHerald::new() .with_format("html", Arc::new(HtmlHerald::default())) .with_format("json", Arc::new(JsonHerald::default())) .with_format("markdown", Arc::new(MarkdownHerald::default())) .with_default(Arc::new(PlainHerald::default())); // Format based on context let web_output = adaptive.format_for_context( &content, &OutputContext { channel: OutputChannel::Web, user_preferences: HashMap::new(), } ).await?; let api_output = adaptive.format_for_context( &content, &OutputContext { channel: OutputChannel::Api, user_preferences: HashMap::new(), } ).await?; }
Post-Processing
Transform or enhance output after formatting.
Sanitization Herald
#![allow(unused)] fn main() { pub struct SanitizingHerald { inner: Arc<dyn Herald>, remove_patterns: Vec<regex::Regex>, } impl SanitizingHerald { pub fn new(inner: Arc<dyn Herald>) -> Self { Self { inner, remove_patterns: vec![ // Remove potential PII regex::Regex::new(r"\b\d{3}-\d{2}-\d{4}\b").unwrap(), // SSN regex::Regex::new(r"\b[\w\.-]+@[\w\.-]+\.\w+\b").unwrap(), // Email regex::Regex::new(r"\b\d{3}-\d{3}-\d{4}\b").unwrap(), // Phone ], } } } #[async_trait] impl Herald for SanitizingHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { let formatted = self.inner.format(content).await?; // Remove sensitive patterns let mut sanitized = formatted; for pattern in &self.remove_patterns { sanitized = pattern.replace_all(&sanitized, "[REDACTED]").to_string(); } Ok(sanitized) } // Implement other methods... } }
Enhancement Herald
#![allow(unused)] fn main() { pub struct EnhancingHerald { inner: Arc<dyn Herald>, } #[async_trait] impl Herald for EnhancingHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { let formatted = self.inner.format(content).await?; // Add enhancements let enhanced = self.add_table_of_contents(&formatted); let enhanced = self.add_footnotes(&enhanced); let enhanced = self.add_timestamps(&enhanced); Ok(enhanced) } fn add_table_of_contents(&self, content: &str) -> String { // Extract headers and generate TOC let headers = self.extract_headers(content); if headers.is_empty() { return content.to_string(); } let toc = headers.iter() .map(|(level, text, id)| { let indent = " ".repeat(*level - 1); format!("{}* [{}](#{})", indent, text, id) }) .collect::<Vec<_>>() .join("\n"); format!("## Table of Contents\n\n{}\n\n{}", toc, content) } fn add_footnotes(&self, content: &str) -> String { // Process [^1] style footnote references // Implementation... content.to_string() } fn add_timestamps(&self, content: &str) -> String { format!("Generated at: {}\n\n{}", chrono::Utc::now().to_rfc3339(), content) } } }
Caching Herald
#![allow(unused)] fn main() { use std::collections::HashMap; use std::sync::RwLock; pub struct CachingHerald { inner: Arc<dyn Herald>, cache: RwLock<HashMap<String, String>>, max_cache_size: usize, } #[async_trait] impl Herald for CachingHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { // Check cache { let cache = self.cache.read().unwrap(); if let Some(cached) = cache.get(content) { return Ok(cached.clone()); } } // Format let formatted = self.inner.format(content).await?; // Store in cache { let mut cache = self.cache.write().unwrap(); // Evict oldest if at capacity if cache.len() >= self.max_cache_size { if let Some(key) = cache.keys().next().cloned() { cache.remove(&key); } } cache.insert(content.to_string(), formatted.clone()); } Ok(formatted) } // Implement other methods... } }
Best Practices
1. Match Format to Use Case
#![allow(unused)] fn main() { // ✅ API endpoints - use JSON let api_herald = Arc::new(JsonHerald::new() .with_schema(api_schema) .validate_output(true) ); // ✅ Documentation - use Markdown let docs_herald = Arc::new(MarkdownHerald::new() .with_table_of_contents(true) .with_code_highlighting(true) ); // ✅ Web display - use HTML let web_herald = Arc::new(HtmlHerald::new() .with_css_framework(CssFramework::Bootstrap) .with_responsive_design(true) ); // ✅ Data export - use CSV let export_herald = Arc::new(CsvHerald::new(headers)); }
2. Validate Structured Output
#![allow(unused)] fn main() { let herald = Arc::new(JsonHerald::new() .with_schema(schema) .validate_output(true) // Validate against schema ); // Paladin will retry if output doesn't match schema let paladin = PaladinBuilder::new(llm_adapter) .system_prompt("CRITICAL: Output MUST be valid JSON matching the schema") .with_herald(herald) .max_retries(3) // Retry on validation failures .build()?; }
3. Use Streaming for Long Responses
#![allow(unused)] fn main() { // ❌ Bad: Wait for complete response let response = paladin.execute(long_prompt).await?; println!("{}", response.content); // User waits 30 seconds // ✅ Good: Stream for immediate feedback let mut stream = paladin.execute_stream(long_prompt).await?; while let Some(chunk) = stream.next().await { let chunk = chunk?; print!("{}", chunk.content); // Immediate output std::io::stdout().flush()?; } }
4. Layer Heralds for Composability
#![allow(unused)] fn main() { // Layer: Base -> Enhancement -> Sanitization -> Caching let herald = Arc::new( CachingHerald::new( Arc::new(SanitizingHerald::new( Arc::new(EnhancingHerald::new( Arc::new(MarkdownHerald::default()) )) )), 100, // cache size ) ); }
5. Provide Format Guidance in System Prompt
#![allow(unused)] fn main() { // ✅ Explicit format instructions let paladin = PaladinBuilder::new(llm_adapter) .system_prompt( "You MUST respond in valid JSON format:\n\ {\n\ \"answer\": \"your response\",\n\ \"confidence\": 0.0 to 1.0,\n\ \"sources\": [\"source1\", \"source2\"]\n\ }\n\ Do NOT include any text outside this JSON structure." ) .with_herald(Arc::new(JsonHerald::default())) .build()?; }
Advanced Patterns
Template-Based Herald
#![allow(unused)] fn main() { use handlebars::Handlebars; pub struct TemplateHerald { handlebars: Handlebars<'static>, template_name: String, } impl TemplateHerald { pub fn new(template: &str, template_name: &str) -> Result<Self, HeraldError> { let mut handlebars = Handlebars::new(); handlebars.register_template_string(template_name, template)?; Ok(Self { handlebars, template_name: template_name.to_string(), }) } } #[async_trait] impl Herald for TemplateHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { // Parse content as JSON let data: serde_json::Value = serde_json::from_str(content)?; // Render template let rendered = self.handlebars.render(&self.template_name, &data)?; Ok(rendered) } // Implement other methods... } // Usage let template = r#" {{title}} **Summary:** {{summary}} # Details {{#each items}} - {{this}} {{/each}} *Generated: {{timestamp}}* "#; let herald = Arc::new(TemplateHerald::new(template, "report")?); let paladin = PaladinBuilder::new(llm_adapter) .system_prompt("Return JSON: {title, summary, items: [], timestamp}") .with_herald(herald) .build()?; }
Diff Herald
#![allow(unused)] fn main() { pub struct DiffHerald { previous_content: RwLock<Option<String>>, } #[async_trait] impl Herald for DiffHerald { async fn format(&self, content: &str) -> Result<String, HeraldError> { let previous = self.previous_content.read().unwrap().clone(); let formatted = if let Some(prev) = previous { // Generate diff self.generate_diff(&prev, content) } else { // First time, show all content.to_string() }; // Update previous content *self.previous_content.write().unwrap() = Some(content.to_string()); Ok(formatted) } fn generate_diff(&self, old: &str, new: &str) -> String { // Use diff algorithm // Implementation... format!("--- Old\n+++ New\n{}", new) } } }
Troubleshooting
Invalid JSON Output
Problem: JSON Herald fails to parse LLM output.
Solutions:
- Strengthen system prompt with explicit JSON instructions
- Add JSON schema to prompt
- Enable output validation with retries
- Use JSON mode in LLM if supported
#![allow(unused)] fn main() { let paladin = PaladinBuilder::new(llm_adapter) .system_prompt( "CRITICAL INSTRUCTION: You MUST respond with ONLY valid JSON. \ No additional text before or after. No markdown code blocks. \ Just pure JSON.\n\n\ Schema: {\"result\": string, \"confidence\": number}" ) .output_format(OutputFormat::Json) // Some LLMs support JSON mode .max_retries(3) .build()?; }
Streaming Format Inconsistency
Problem: Streamed chunks don't format correctly.
Solutions:
- Use accumulation pattern
- Implement chunk boundary detection
- Buffer until complete format units
#![allow(unused)] fn main() { pub struct BufferedStreamHerald { buffer: RwLock<String>, delimiter: String, } impl BufferedStreamHerald { async fn format_chunk(&self, chunk: &str) -> Result<String, HeraldError> { let mut buffer = self.buffer.write().unwrap(); buffer.push_str(chunk); // Check for complete units (e.g., sentences, paragraphs) if buffer.ends_with(&self.delimiter) { let complete = buffer.clone(); buffer.clear(); Ok(complete) } else { Ok(String::new()) // Not ready yet } } } }
Performance Issues with Complex Formatting
Problem: Formatting is slow for large outputs.
Solutions:
- Implement caching
- Use lazy formatting (format on demand)
- Optimize regex patterns
- Consider parallel processing
#![allow(unused)] fn main() { // Lazy formatting pub struct LazyHerald { inner: Arc<dyn Herald>, cached_result: RwLock<Option<String>>, } impl LazyHerald { pub async fn get_formatted(&self, content: &str) -> Result<String, HeraldError> { // Check cache if let Some(cached) = self.cached_result.read().unwrap().as_ref() { return Ok(cached.clone()); } // Format and cache let formatted = self.inner.format(content).await?; *self.cached_result.write().unwrap() = Some(formatted.clone()); Ok(formatted) } } }
Testing
Unit Testing Heralds
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_json_herald_formats_correctly() { let herald = JsonHerald::default(); let input = r#"{"name": "Alice", "age": 30}"#; let formatted = herald.format(input).await.unwrap(); // Verify valid JSON let parsed: serde_json::Value = serde_json::from_str(&formatted).unwrap(); assert_eq!(parsed["name"], "Alice"); assert_eq!(parsed["age"], 30); } #[tokio::test] async fn test_json_herald_validates_schema() { let schema = json!({ "type": "object", "properties": { "name": {"type": "string"} }, "required": ["name"] }); let herald = JsonHerald::new().with_schema(schema); // Valid assert!(herald.validate(r#"{"name": "Bob"}"#).is_ok()); // Invalid - missing required field assert!(herald.validate(r#"{"age": 25}"#).is_err()); } } }
Examples
See working examples:
examples/herald_markdown_output.rs- Markdown formattingexamples/herald_json_output.rs- Structured JSON outputexamples/herald_streaming.rs- Real-time streamingexamples/herald_custom_formatter.rs- Custom herald implementation
Next Steps
- Tool Integration - Format tool results
- Battalion Patterns - Format multi-agent outputs
- API Reference - Herald API documentation