| Crates.io | policyai |
| lib.rs | policyai |
| version | 0.4.0 |
| created_at | 2025-01-31 15:08:37.036685+00 |
| updated_at | 2025-10-16 05:23:49.530179+00 |
| description | PolicyAI provides a mechanism for unstructured, composable policies that transform unstructured text into structured outputs. |
| homepage | |
| repository | https://github.com/rescrv/policyai |
| max_upload_size | |
| id | 1537595 |
| size | 447,675 |
Composable, conflict-aware policies for reliable agents
When building agents, you quickly discover that large language models have subtle biases that make structured outputs unreliable:
"priority": "low" appears 10× more often than "priority": "high" in your prompts, the LLM will default to "low" even when "high" is correct.These aren't edge cases. They're fundamental limitations of how LLMs process instructions. Your agent appears to work in testing, then fails unpredictably in production.
PolicyAI provides a layer on top of structured outputs that makes agent behavior composable and conflict-aware:
PolicyAI trades latency and cost for reliability and debuggability. If you're building production agents where correctness matters, that's a trade worth making.
Here's what happens with vanilla structured outputs when you have conflicting policies:
# Your agent instructions
"""
- When Alice sends a message, set priority to HIGH
- When Bob sends a message, set priority to LOW
"""
# Message from: alice@example.com, bob@example.com
# LLM output: {"priority": "LOW"} # Wrong! But which instruction should it follow?
# The model picked one silently. No error. No warning.
With PolicyAI, this scenario produces a conflict error because two policies disagree on the priority field's value, and you've configured it to require agreement.
A PolicyType is like a schema, but with conflict resolution strategies:
use policyai::{PolicyType, Field, OnConflict};
let policy_type = PolicyType {
name: "EmailPolicy".to_string(),
fields: vec![
Field::Bool {
name: "unread".to_string(),
default: Some(true),
on_conflict: OnConflict::Default,
},
Field::StringEnum {
name: "priority".to_string(),
values: vec!["low".to_string(), "medium".to_string(), "high".to_string()],
default: None,
on_conflict: OnConflict::LargestValue, // "high" wins over "low"
},
Field::StringArray {
name: "labels".to_string(),
},
],
};
A semantic injection is a natural language instruction that generates structured actions:
let policy1 = policy_type
.with_semantic_injection(
&client,
"If the email is about football, mark \"unread\" false with low \"priority\""
)
.await?;
let policy2 = policy_type
.with_semantic_injection(
&client,
"If the email is from mom@example.org, set high \"priority\" and add Family \"label\""
)
.await?;
let policy3 = policy_type
.with_semantic_injection(
&client,
"If the email is about shopping, add Shopping \"label\""
)
.await?;
let mut manager = Manager::default();
manager.add(policy1);
manager.add(policy2);
manager.add(policy3);
let report = manager.apply(
&client,
template,
"From: mom@example.org\nSubject: Shopping for football gear",
None
).await?;
// Result: unread=false, priority=high, labels=["Family", "Shopping"]
// - policy1 sets unread=false, priority=low
// - policy2 sets priority=high (wins via LargestValue)
// - policy3 adds Shopping label
// - labels compose (arrays merge)
The policies compose cleanly because:
priority uses OnConflict::LargestValue → "high" overrides "low"labels is an array → values merge automaticallyunread uses OnConflict::Default → takes the default value when conflicts occurPolicyAI provides three strategies for handling conflicts:
All policies must agree on the value, or you get a conflict error. Best for fields where inconsistency indicates a logic error in your policies.
Field::String {
name: "template".to_string(),
default: None,
on_conflict: OnConflict::Agreement,
}
The largest value wins. This makes important values "sticky" and enables monotonic overrides:
true > false10 > 5Field::StringEnum {
name: "priority".to_string(),
values: vec!["low".to_string(), "medium".to_string(), "high".to_string()],
on_conflict: OnConflict::LargestValue, // "high" > "medium" > "low"
}
Why this matters: Once a policy sets priority to "high", no other policy can downgrade it to "low". This prevents surprising interactions between policies.
Use the field type's default behavior (usually last-writer-wins, but arrays append) when conflicts occur. Useful for fields where you want predictable behavior regardless of policy interactions.
Field::Bool {
name: "unread".to_string(),
default: Some(true),
on_conflict: OnConflict::Default,
}
PolicyAI provides a concise syntax for defining policy types:
type policyai::EmailPolicy {
unread: bool = true,
priority: ["low", "medium", "high"] @ highest wins,
category: ["ai", "distributed systems", "other"] @ agreement = "other",
template: string @ agreement,
labels: [string],
}
You can parse this syntax directly:
let policy_type = PolicyType::parse(r#"
type EmailPolicy {
priority: ["low", "high"] @ highest wins,
labels: [string]
}
"#)?;
PolicyAI excels when your agent needs to:
PolicyAI is not the right tool when:
For production agents with large policy sets, you don't want to apply every policy to every input. Use vector retrieval to select relevant policies dynamically:
(Example is illustrative, but likely needs work to work because Claude hallucinated some of this)
use chromadb::{ChromaClient, Collection};
use policyai::{Policy, Manager};
async fn process_with_retrieval(
client: &Anthropic,
chroma: &Collection,
input: &str,
) -> Result<Report, Box<dyn std::error::Error>> {
// 1. Retrieve relevant policies from vector database
let results = chroma.query(
vec![input.to_string()],
5, // top 5 most relevant policies
None,
None,
None,
).await?;
// 2. Deserialize policies from metadata
let policies: Vec<Policy> = results.metadatas
.into_iter()
.flatten()
.filter_map(|meta| {
serde_json::from_value(meta.get("policy")?.clone()).ok()
})
.collect();
// 3. Apply only relevant policies
let mut manager = Manager::default();
for policy in policies {
manager.add(policy);
}
let report = manager.apply(client, template, input, None).await?;
Ok(report)
}
When adding policies to your vector database, store both the semantic injection and the full policy:
// Create policy
let policy = policy_type
.with_semantic_injection(
&client,
"If email from VIP, set high priority"
)
.await?;
// Store in Chroma with embedding of the semantic injection
chroma.add(
vec![Uuid::new_v4().to_string()], // id
vec![policy.prompt.clone()], // text to embed
Some(vec![serde_json::json!({
"policy": policy,
"type": "email_triage",
})]),
None,
).await?;
policy.prompt) captures intentPolicy object for applicationThis pattern combines the best of both worlds:
PolicyAI sacrifices performance for reliability:
| Metric | vs Vanilla Structured Outputs |
|---|---|
| Latency | Higher (additional LLM calls) |
| Token Usage | Higher (policy composition) |
| Cost | Higher (more tokens) |
| Reliability | Much higher (conflict detection) |
| Debuggability | Much higher (isolated policies) |
Why it's worth it: In production agents, silent failures are expensive. PolicyAI makes agent behavior predictable and testable. You can verify each policy independently, then compose them with some confidence.
Add PolicyAI to your Cargo.toml:
[dependencies]
policyai = "0.3"
Basic usage:
use policyai::{PolicyType, Field, OnConflict, Manager};
use claudius::Anthropic;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Anthropic::new(None)?;
// Define your policy type
let policy_type = PolicyType::parse(r#"
type MyPolicy {
priority: ["low", "high"] @ highest wins
}
"#)?;
// Create a policy from natural language
let policy = policy_type
.with_semantic_injection(&client, "Set high priority for urgent messages")
.await?;
// Apply it
let mut manager = Manager::default();
manager.add(policy);
let report = manager.apply(
&client,
template,
"This is urgent!",
None
).await?;
println!("{}", report.value());
Ok(())
}
PolicyAI includes tools for testing and debugging:
policyai-verify-policies: Verify policies are well-formedpolicyai-regression-report: Generate reports on policy behaviorpolicyai-extract-regressions: Extract failing cases for analysispolicyai-regressions-to-examples: Convert regressions to test examplesPolicyAI deliberately orders arguments in tool calls carefully. Agents are surprisingly susceptible to argument order, so the framework maintains consistent ordering to avoid bias.
See the examples/ directory for:
Issues and pull requests welcome at https://github.com/rescrv/policyai
Apache-2.0