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

  1. Overview
  2. Available Heralds
  3. Herald Trait
  4. Attaching to a Paladin
  5. Attaching to a Battalion Service
  6. Custom Herald Implementation
  7. Streaming Output
  8. Error Handling

Overview

HeraldImportBest For
JsonHeraldpaladin::infrastructure::adapters::herald::JsonHeraldAPIs, logging, programmatic consumption
MarkdownHeraldpaladin::infrastructure::adapters::herald::MarkdownHeraldTerminal display, reports, documentation
TableHeraldpaladin::infrastructure::adapters::herald::TableHeraldTabular 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:

VariantCauseRecovery
SerializationError(String)JSON serialisation failureCheck result data for non-serialisable fields
FormatError(String)Internal formatter errorReport as bug; fallback to to_string()
InvalidInput(String)Unexpected input shapeValidate result before formatting