Skip to main content

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
  2. Cargo.toml
  3. Source Code
  4. Tests
  5. Documentation
  6. CI/CD

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


Maintained by: AGX Core Team Review cycle: Per release Questions? Open an issue in agenix/agenix