Example AU Template
Version: 1.0 Status: AU Development Template Last Updated: 2025-11-17
This document provides a complete template for creating new Agentic Units. Use this as a starting point for your own AUs.
Table of Contents
1. Project Setup
Create New AU Project
# Create new Rust project
cargo new agx-example --name agx-example
cd agx-example
# Initialize git
git init
git remote add origin https://github.com/agenix-sh/agx-example.git
# Create directory structure
mkdir -p src tests/fixtures benches docs
Directory Structure
agx-example/
�� Cargo.toml
�� LICENSE-MIT
�� LICENSE-APACHE
�� README.md
�� CLAUDE.md
�� src/
�� main.rs
�� types.rs
�� describe.rs
�� process.rs
�� tests/
�� fixtures/
�� sample_input.txt
�� expected_output.json
�� integration_test.rs
�� contract_test.rs
�� benches/
�� benchmark.rs
�� docs/
�� USAGE.md
2. Cargo.toml
[package]
name = "agx-example"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
license = "MIT OR Apache-2.0"
description = "AGEniX Agentic Unit for [your use case]"
repository = "https://github.com/agenix-sh/agx-example"
readme = "README.md"
[dependencies]
anyhow = "1.0"
clap = { version = "4.0", features = ["derive", "env"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# Add domain-specific dependencies here
# image = "0.24" # For image processing
# pdf = "0.8" # For PDF parsing
# regex = "1.10" # For text processing
[dev-dependencies]
criterion = "0.5"
jsonschema = "0.17"
tempfile = "3.8"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Strip symbols
[[bench]]
name = "benchmark"
harness = false
3. Source Code
3.1 main.rs
use std::io::{self, Read};
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
mod types;
mod describe;
mod process;
use crate::types::ExampleResult;
/// agx-example: Example Agentic Unit
#[derive(Parser, Debug)]
#[command(name = "agx-example")]
#[command(about = "AGEniX Example AU", long_about = None)]
struct Cli {
/// Print AU model description as JSON (for --describe contract)
#[arg(long = "describe")]
describe: bool,
/// Optional configuration parameter
#[arg(long = "config", env = "EXAMPLE_CONFIG")]
config: Option<String>,
}
fn main() -> Result<()> {
// Handle --describe flag
if std::env::args().any(|arg| arg == "--describe") {
describe::print_model_card()?;
return Ok(());
}
let cli = Cli::parse();
// Read binary input from stdin
let mut buf = Vec::new();
io::stdin()
.read_to_end(&mut buf)
.context("Failed to read input from stdin")?;
// Process input
let result = process::run(&buf, cli.config.as_deref())?;
// Write structured JSON to stdout
let json = serde_json::to_string_pretty(&result)
.context("Failed to serialize result to JSON")?;
println!("{}", json);
Ok(())
}
3.2 types.rs
use serde::Serialize;
/// AU output structure.
/// This is the stable contract for AGEniX pipelines.
#[derive(Debug, Serialize)]
pub struct ExampleResult {
/// Processed output
pub output: String,
/// Optional metadata
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
/// Processing info
pub info: ProcessingInfo,
}
#[derive(Debug, Serialize)]
pub struct Metadata {
/// Custom metadata fields
pub custom_field: String,
}
#[derive(Debug, Serialize)]
pub struct ProcessingInfo {
/// Input size in bytes
pub input_size: usize,
/// Processing time in milliseconds
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
}
3.3 describe.rs
use anyhow::Result;
use serde::Serialize;
/// AU model card structure compatible with describe.schema.json
#[derive(Debug, Serialize)]
struct ModelCard {
name: String,
version: String,
description: String,
capabilities: Vec<String>,
inputs: Vec<IoFormat>,
outputs: Vec<IoFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
config: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
struct IoFormat {
media_type: String,
description: String,
}
pub fn print_model_card() -> Result<()> {
let card = ModelCard {
name: "agx-example".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
description: "Example Agentic Unit demonstrating AU contract compliance."
.to_string(),
capabilities: vec!["example".to_string(), "template".to_string()],
inputs: vec![IoFormat {
media_type: "text/plain".to_string(),
description: "Plain text input via stdin".to_string(),
}],
outputs: vec![IoFormat {
media_type: "application/json".to_string(),
description: "Processed result as structured JSON".to_string(),
}],
config: Some(serde_json::json!({
"config": {
"type": "string",
"description": "Optional configuration parameter",
"default": null
}
})),
};
let json = serde_json::to_string_pretty(&card)?;
println!("{json}");
Ok(())
}
3.4 process.rs
use anyhow::{Context, Result};
use crate::types::{ExampleResult, ProcessingInfo, Metadata};
/// Main processing logic
pub fn run(input: &[u8], config: Option<&str>) -> Result<()> {
// Validate input
validate_input(input)?;
// Process
let output = process_core(input, config)?;
// Build result
let result = ExampleResult {
output,
metadata: config.map(|c| Metadata {
custom_field: c.to_string(),
}),
info: ProcessingInfo {
input_size: input.len(),
duration_ms: None,
},
};
Ok(result)
}
fn validate_input(input: &[u8]) -> Result<()> {
const MAX_INPUT_SIZE: usize = 10 * 1024 * 1024; // 10MB
if input.is_empty() {
anyhow::bail!("Input is empty");
}
if input.len() > MAX_INPUT_SIZE {
anyhow::bail!(
"Input too large: {}MB (max 10MB)",
input.len() / 1024 / 1024
);
}
Ok(())
}
fn process_core(input: &[u8], config: Option<&str>) -> Result<String> {
// Convert input to string
let text = std::str::from_utf8(input)
.context("Input is not valid UTF-8")?;
// Example processing: uppercase transformation
let mut output = text.to_uppercase();
// Apply config if provided
if let Some(cfg) = config {
output = format!("[{}] {}", cfg, output);
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_input_empty() {
assert!(validate_input(b"").is_err());
}
#[test]
fn test_validate_input_too_large() {
let huge = vec![0u8; 20 * 1024 * 1024];
assert!(validate_input(&huge).is_err());
}
#[test]
fn test_process_core_basic() {
let result = process_core(b"hello world", None).unwrap();
assert_eq!(result, "HELLO WORLD");
}
#[test]
fn test_process_core_with_config() {
let result = process_core(b"hello", Some("TEST")).unwrap();
assert_eq!(result, "[TEST] HELLO");
}
}
4. Tests
4.1 Integration Test (tests/integration_test.rs)
use std::process::{Command, Stdio};
use std::io::Write;
fn run_au(input: &[u8]) -> std::process::Output {
let mut child = Command::new("cargo")
.args(&["run", "--quiet"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(input).unwrap();
child.wait_with_output().unwrap()
}
#[test]
fn test_basic_input() {
let output = run_au(b"hello world");
assert!(output.status.success());
let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(result["output"], "HELLO WORLD");
assert_eq!(result["info"]["input_size"], 11);
}
#[test]
fn test_empty_input_fails() {
let output = run_au(b"");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("empty"));
}
#[test]
fn test_with_config() {
let mut child = Command::new("cargo")
.args(&["run", "--quiet", "--", "--config", "TEST"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(b"hello").unwrap();
let output = child.wait_with_output().unwrap();
let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(result["output"].as_str().unwrap().contains("[TEST]"));
}
4.2 Contract Test (tests/contract_test.rs)
use std::process::Command;
#[test]
fn test_describe_valid_json() {
let output = Command::new("cargo")
.args(&["run", "--quiet", "--", "--describe"])
.output()
.unwrap();
assert!(output.status.success());
let card: serde_json::Value = serde_json::from_slice(&output.stdout)
.expect("--describe must output valid JSON");
// Verify required fields
assert_eq!(card["name"], "agx-example");
assert!(card.get("version").is_some());
assert!(card.get("description").is_some());
assert!(card.get("capabilities").is_some());
}
#[test]
fn test_stdout_is_json() {
let output = Command::new("cargo")
.args(&["run", "--quiet"])
.stdin(std::process::Stdio::piped())
.spawn()
.unwrap()
.stdin
.unwrap()
.write_all(b"test")
.unwrap();
let output = child.wait_with_output().unwrap();
// Must be valid JSON
let _: serde_json::Value = serde_json::from_slice(&output.stdout)
.expect("stdout must be valid JSON");
}
4.3 Benchmark (benches/benchmark.rs)
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn bench_process(c: &mut Criterion) {
let input = b"hello world test input";
c.bench_function("process_basic", |b| {
b.iter(|| {
agx_example::process::run(black_box(input), None)
});
});
}
criterion_group!(benches, bench_process);
criterion_main!(benches);
5. Documentation
5.1 README.md
# agx-example
AGEniX Agentic Unit for [brief description].
## Installation
\`\`\`bash
cargo install agx-example
\`\`\`
## Usage
### Basic Usage
\`\`\`bash
echo "hello world" | agx-example
\`\`\`
### With Configuration
\`\`\`bash
agx-example --config "custom" < input.txt
\`\`\`
### Get Model Card
\`\`\`bash
agx-example --describe
\`\`\`
## Contract
This AU follows the AGEniX AU specification:
- Reads binary input from stdin
- Outputs structured JSON to stdout
- Errors go to stderr only
- Implements `--describe` flag
See [AU Specification](https://github.com/agenix-sh/agenix/docs/au-specs/agentic-unit-spec.md).
## License
Dual-licensed under MIT OR Apache-2.0.
5.2 CLAUDE.md
# CLAUDE.md
Development guidelines for Claude Code working with agx-example.
## Project Overview
`agx-example` is an AGEniX Agentic Unit that [description].
## Key Architecture
- **types.rs**: Stable AU contract types
- **process.rs**: Core processing logic
- **describe.rs**: Model card generation
## Development Commands
\`\`\`bash
cargo build
cargo test
cargo run -- --describe
echo "test" | cargo run
\`\`\`
## AU Contract
Must follow AU specification:
- Binary stdin, JSON stdout
- `--describe` flag
- Exit codes: 0 (success), 1 (error), 2 (invalid input)
6. CI/CD
.github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Format check
run: cargo fmt -- --check
- name: Clippy
run: cargo clippy -- -D warnings
- name: Test
run: cargo test
- name: Contract validation
run: |
cargo run -- --describe > describe.json
jq empty describe.json
- name: Build release
run: cargo build --release
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Coverage
run: cargo tarpaulin --fail-under 80
Quick Start Checklist
- Create project:
cargo new agx-yourname - Copy template files to your project
- Update Cargo.toml with your AU details
- Implement core logic in
process.rs - Define output types in
types.rs - Create model card in
describe.rs - Add test fixtures to
tests/fixtures/ - Write integration tests
- Write contract tests
- Add LICENSE files
- Create README.md
- Set up CI/CD
- Test AU:
echo "test" | cargo run - Validate contract:
cargo run -- --describe - Commit to git
Related Documentation
- AU Specification - Complete AU contract
- Testing AUs - Comprehensive testing guide
- Security Guidelines - Security requirements
Maintained by: AGX Core Team Review cycle: Per release Questions? Open an issue in agenix/agenix