Herald Output Formatting
The Herald system provides pluggable output formatters for Paladin and Battalion execution
results. A Herald transforms a PaladinResult or BattalionResult into a human-readable or
machine-readable string — JSON, Markdown, or ASCII table.
The Herald trait is defined in crates/paladin-core/src/platform/container/herald.rs.
Adapters are in src/infrastructure/adapters/herald/.
Table of Contents
- Overview
- Available Heralds
- Herald Trait
- Attaching to a Paladin
- Attaching to a Battalion Service
- Custom Herald Implementation
- Streaming Output
- Error Handling
Overview
| Herald | Import | Best For |
|---|---|---|
JsonHerald | paladin::infrastructure::adapters::herald::JsonHerald | APIs, logging, programmatic consumption |
MarkdownHerald | paladin::infrastructure::adapters::herald::MarkdownHerald | Terminal display, reports, documentation |
TableHerald | paladin::infrastructure::adapters::herald::TableHerald | Tabular terminal output, log files |
Available Heralds
JsonHerald
Serialises PaladinResult and BattalionResult to JSON.
use paladin::infrastructure::adapters::herald::JsonHerald;
use paladin::infrastructure::adapters::herald::json_herald::JsonHeraldConfig;
// Default: pretty = true, include_metadata = true
let herald = JsonHerald::new();
// Compact JSON without metadata
let herald = JsonHerald::with_config(JsonHeraldConfig {
pretty: false,
include_metadata: false,
});
let json_str = herald.format_paladin_result(&result)?;
// {"output": "...", "token_count": 150, "execution_time_ms": 1230, ...}
MarkdownHerald
Formats results with Markdown headings, status badges, and code blocks. Supports ANSI colour codes for terminal output.
use paladin::infrastructure::adapters::herald::MarkdownHerald;
use paladin::infrastructure::adapters::herald::markdown_herald::MarkdownHeraldConfig;
// Default: auto-detects terminal colour support
let herald = MarkdownHerald::new();
// Custom: force no colours, H1 headings
let herald = MarkdownHerald::with_config(MarkdownHeraldConfig {
include_colors: false,
heading_level: 1,
});
TableHerald
Renders results as ASCII tables using comfy-table.
use paladin::infrastructure::adapters::herald::TableHerald;
use paladin::infrastructure::adapters::herald::table_herald::TableHeraldConfig;
// Default configuration
let herald = TableHerald::default();
// Custom: 80-char column width, rounded borders
let herald = TableHerald::new(TableHeraldConfig {
max_column_width: 80,
border_style: "rounded".to_string(),
});
Herald Trait
pub trait Herald: Send + Sync {
/// Format a completed Paladin result
fn format_paladin_result(&self, result: &PaladinResult) -> Result<String, HeraldError>;
/// Format a completed Battalion result
fn format_battalion_result(&self, result: &BattalionResult) -> Result<String, HeraldError>;
/// Format a streaming chunk — returns Some(String) when output is ready to emit,
/// or None if the Herald is buffering
fn format_stream_chunk(&self, chunk: &StreamChunk) -> Result<Option<String>, HeraldError>;
}
Attaching to a Paladin
use paladin::application::services::paladin::paladin_builder::PaladinBuilder;
use paladin::infrastructure::adapters::herald::JsonHerald;
use paladin_core::platform::container::herald::Herald;
use std::sync::Arc;
let herald: Arc<dyn Herald> = Arc::new(JsonHerald::new());
let paladin = PaladinBuilder::new(llm_port)
.system_prompt("You are an API assistant.")
.with_herald(herald)
.build()
.await?;
let result = paladin.execute("List all Rust 2024 edition features").await?;
// result.output is already formatted as JSON
println!("{}", result.output);
Attaching to a Battalion Service
Formation, Phalanx, and other services accept a Herald via .with_herald():
use paladin_battalion::phalanx_service::PhalanxExecutionService;
use paladin::infrastructure::adapters::herald::MarkdownHerald;
use std::sync::Arc;
let service = PhalanxExecutionService::new(paladin_port)
.with_herald(Arc::new(MarkdownHerald::new()));
let result = service.execute(&phalanx, "Analyse this dataset").await?;
println!("{}", result.output);
Custom Herald Implementation
Implement the Herald trait to create a bespoke formatter:
use paladin_core::platform::container::herald::{
Herald, HeraldError, PaladinResult, BattalionResult, StreamChunk,
};
pub struct CsvHerald;
impl Herald for CsvHerald {
fn format_paladin_result(&self, result: &PaladinResult) -> Result<String, HeraldError> {
Ok(format!(
"{},{},{},{:?}\n",
result.output.replace(',', ";"),
result.token_count,
result.execution_time_ms,
result.stop_reason,
))
}
fn format_battalion_result(&self, result: &BattalionResult) -> Result<String, HeraldError> {
Ok(result.output.clone())
}
fn format_stream_chunk(&self, chunk: &StreamChunk) -> Result<Option<String>, HeraldError> {
Ok(Some(chunk.text.clone()))
}
}
Streaming Output
Use format_stream_chunk() during execute_stream():
use paladin_ports::output::paladin_port::PaladinStreamChunk;
use paladin_core::platform::container::herald::Herald;
let mut stream = paladin.execute_stream("Generate a long report").await?;
let herald = MarkdownHerald::new();
while let Some(chunk_result) = stream.recv().await {
match chunk_result {
Ok(chunk) => {
// chunk.text is raw text; wrap in a StreamChunk for the Herald
if let Some(formatted) = herald.format_stream_chunk(&chunk.into())? {
print!("{}", formatted);
}
if chunk.is_final { break; }
}
Err(e) => eprintln!("Stream error: {}", e),
}
}
Error Handling
HeraldError variants from paladin_core::platform::container::herald_error:
| Variant | Cause | Recovery |
|---|---|---|
SerializationError(String) | JSON serialisation failure | Check result data for non-serialisable fields |
FormatError(String) | Internal formatter error | Report as bug; fallback to to_string() |
InvalidInput(String) | Unexpected input shape | Validate result before formatting |