use std::sync::Arc;
use tinyagents::Result;
use tinyagents::graph::{CompiledGraph, END, NodeFuture};
use tinyagents::harness::message::Message;
use tinyagents::harness::providers::openai::OpenAiModel;
use tinyagents::harness::runtime::AgentHarness;
use tinyagents::language::compiler::{
BoxedNode, CapabilityResolver, NodeFactory, bind_capabilities, build_graph, compile,
};
use tinyagents::language::parser::parse_str;
use tinyagents::language::types::{NodeSpec, Routing};
use tinyagents::{Command, NodeContext, NodeResult};
const SYSTEM_PROMPT: &str = r#"
You author agent graphs in the tinyagents expressive language (`.rag`).
Output ONLY `.rag` source code. No prose, no explanation, no markdown fences.
Grammar (one graph):
graph <name> {
start <node>
node <node> {
kind agent // or: tool_executor
model "<model>" // agent nodes only
system "<instruction>"
tools ["<tool>", ...] // optional
routes { // conditional routing (agent nodes)
tool_call -> <node>
final -> END
}
}
node <node> {
kind tool_executor
next <node> // unconditional successor
}
}
Worked example:
graph helpdesk {
start agent
node agent {
kind agent
model "default"
system "Answer support questions, using tools when useful."
tools ["search_kb"]
routes {
tool_call -> tools
final -> END
}
}
node tools {
kind tool_executor
next agent
}
}
Constraints for YOUR output:
- Use model "default" only.
- You may only reference these tools: "search_kb", "create_ticket".
- Keep it to at most three nodes.
"#;
#[derive(Clone, Debug, Default)]
struct BlueprintState {
trail: Vec<String>,
}
struct TrailFactory;
impl NodeFactory<BlueprintState> for TrailFactory {
fn make(&self, spec: &NodeSpec) -> Result<BoxedNode<BlueprintState>> {
let name = spec.name.clone();
let routing = spec.routing.clone();
Ok(Arc::new(
move |mut state: BlueprintState, _ctx: NodeContext| -> NodeFuture<BlueprintState> {
let name = name.clone();
let routing = routing.clone();
Box::pin(async move {
state.trail.push(name);
let result = match &routing {
Routing::Next(_) | Routing::Terminal => NodeResult::Update(state),
Routing::Conditional(_) => {
NodeResult::Command(Command::goto([END]).with_update(state))
}
};
Ok(result)
})
},
))
}
}
fn extract_rag(reply: &str) -> String {
let trimmed = reply.trim();
if let Some(start) = trimmed.find("```") {
let after = &trimmed[start + 3..];
let after = match after.find('\n') {
Some(nl) => &after[nl + 1..],
None => after,
};
if let Some(end) = after.find("```") {
return after[..end].trim().to_string();
}
return after.trim().to_string();
}
trimmed.to_string()
}
async fn run_pipeline(source: &str) -> Result<()> {
println!("\n--- stage 1: parse + compile ---");
let program = match parse_str(source) {
Ok(program) => program,
Err(error) => {
eprintln!("parse failed: {error}");
eprintln!("offending source:\n{source}");
return Ok(());
}
};
let mut blueprints = match compile(&program) {
Ok(blueprints) => blueprints,
Err(error) => {
eprintln!("compile failed: {error}");
eprintln!("offending source:\n{source}");
return Ok(());
}
};
if blueprints.is_empty() {
eprintln!("compile produced no blueprints");
return Ok(());
}
let blueprint = blueprints.remove(0);
println!("\n--- stage 2: blueprint ---");
println!("graph : {}", blueprint.graph_id);
println!("start : {}", blueprint.start);
for node in &blueprint.nodes {
println!(
"node : {} (kind {}, model {:?}, tools {:?}) routing {:?}",
node.name, node.kind, node.model, node.tools, node.routing
);
}
println!("\n--- stage 3: capability binding (policy gate) ---");
let resolver = CapabilityResolver::new()
.allow_model("default")
.allow_tool("search_kb")
.allow_tool("create_ticket");
match bind_capabilities(&blueprint, &resolver) {
Ok(()) => println!("binding OK: every referenced model/tool is allowlisted"),
Err(error) => {
eprintln!("binding rejected by the allowlist: {error}");
eprintln!("(this is the safety gate doing its job)");
return Ok(());
}
}
println!("\n--- stage 4: build graph + run to END ---");
let graph: CompiledGraph<BlueprintState, BlueprintState> =
build_graph(&blueprint, &TrailFactory)?;
let run = graph.run(BlueprintState::default()).await?;
println!("visited: {:?}", run.visited);
println!("trail : {:?}", run.state.trail);
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::dotenv().ok();
let model = OpenAiModel::from_env()?;
println!("=== OpenAI self-authored blueprint ===");
println!("model: {}", model.model());
let mut harness: AgentHarness<()> = AgentHarness::new();
harness
.register_model("openai", Arc::new(model))
.set_default_model("openai");
let task = "Author a `.rag` graph for a customer-support agent that can \
search the knowledge base and create a ticket.";
println!("task : {task}\n");
let run = harness
.invoke_default(
&(),
vec![Message::system(SYSTEM_PROMPT), Message::user(task)],
)
.await?;
let reply = run.text().unwrap_or_default();
println!("--- stage 0: model-authored reply ---");
println!("{reply}");
let source = extract_rag(&reply);
run_pipeline(&source).await?;
Ok(())
}