Validators and a retry-until-valid guard for LLM output — the core pattern behind Python's Guardrails AI. No Rust equivalent as of 2026.
cargo add guardflowGuard runs cheap sync checks (length, regex, PII shape, JSON schema). AsyncGuard adds checks that need a network call — an LLM-as-judge, a moderation API — and only makes that call once the free checks already pass:
use guardflow::validators::NoPii;
use guardflow::{AsyncGuard, CustomAsyncValidator, Guard, guard_with_retry_async};
let guard = AsyncGuard::from_guard(Guard::new().with(NoPii::default()))
.with_async(CustomAsyncValidator::new("refund_policy", |text| async move {
if text.to_lowercase().contains("guaranteed refund") {
Err("promises a refund outside policy".into())
} else {
Ok(())
}
}));
let reply = guard_with_retry_async(&guard, 3, |last_failure| {
let prompt = match last_failure {
Some(f) => format!("Fix this: {:?}. Try again.", f.failures),
None => "Reply to the customer.".to_string(),
};
call_llm(prompt) // returns a Future<Output = String>
}).await;Run it: cargo run --example support_bot — a fake LLM leaks an email (rejected by the free no_pii check), then over-promises a refund (rejected by the async policy check, which only ran because the free check passed first), then succeeds on the third try.
NotEmpty, MaxLength, MinLength, MatchesRegex, OneOf, ValidJson, NoPii (heuristic regex, not ML), Profanity (bring your own word list), JsonSchema (feature json-schema). Implement Validator (sync) or AsyncValidator for your own checks.
Auto-fix instead of reject: some validators can repair input instead of just failing it — Truncate shortens instead of rejecting on length. Guard::fix(text) applies every fixable validator in order and returns the corrected text; Guard::check never rewrites anything, so Truncate counts as a failure there.
Load a rule set from YAML or JSON instead of Rust code (feature spec):
# rules.yaml
rules:
- rule: not_empty
- rule: max_length
max: 500
- rule: no_piilet guard = guardflow::spec::guard_from_file("rules.yaml")?;With the graphflow feature, GuardedTask<T> wraps any graph_flow::Task, retrying until its response passes:
use guardflow::graphflow::GuardedTask;
let guarded = GuardedTask::new(my_task, guard).with_max_attempts(3);Works with graphflow-stream — wrap a task before passing it to spawn_task/spawn_graph.
cargo run --example support_bot # sync + async guard, retry-until-valid, full storycargo bench (benches/overhead.rs):
| Scenario | Time |
|---|---|
Guard::check, 1 validator |
~27 ns |
Guard::check, 5 validators (incl. NoPii) |
~1.2 µs |
MIT