use agent_line::{Agent, Ctx, Outcome, Runner, StepResult, Workflow};
use std::thread;
#[derive(Clone, Debug)]
struct ArticleState {
topic: String,
research: String,
#[allow(dead_code)] guidelines: String,
draft: String,
feedback: String,
revision: u32,
}
impl ArticleState {
fn new(topic: String, guidelines: String) -> Self {
Self {
topic,
research: String::new(),
guidelines,
draft: String::new(),
feedback: String::new(),
revision: 0,
}
}
}
struct Researcher;
impl Agent<ArticleState> for Researcher {
fn name(&self) -> &'static str {
"researcher"
}
fn run(&mut self, mut state: ArticleState, ctx: &mut Ctx) -> StepResult<ArticleState> {
ctx.log(format!("researching: {}", state.topic));
state.research = match state.topic.as_str() {
t if t.contains("embedded") => {
"Rust's ownership model prevents memory bugs common in C firmware. \
The embassy framework provides async on bare metal. \
Companies like Espressif ship official Rust support for ESP32."
.into()
}
t if t.contains("plumber") => {
"Side projects help tradespeople automate billing and scheduling. \
Low-code tools like AppSheet let plumbers build apps without coding. \
One plumber built a leak-detection IoT sensor with a Raspberry Pi."
.into()
}
_ => "Raspberry Pi runs Node-RED for home automation wiring diagrams. \
Electricians use it to monitor panel loads in real time. \
The $35 price point makes it practical for small shops."
.into(),
};
Ok((state, Outcome::Continue))
}
}
struct Writer;
impl Agent<ArticleState> for Writer {
fn name(&self) -> &'static str {
"writer"
}
fn run(&mut self, mut state: ArticleState, ctx: &mut Ctx) -> StepResult<ArticleState> {
state.revision += 1;
let is_rewrite = !state.feedback.is_empty();
if is_rewrite {
ctx.log(format!(
"rewriting draft {} for: {} (feedback: {})",
state.revision, state.topic, state.feedback
));
state.draft = format!(
"# {}\n\n\
Ever wonder how {} is changing the trades?\n\n\
{}",
state.topic,
state.topic.to_lowercase(),
state.research,
);
state.feedback.clear();
} else {
ctx.log(format!(
"writing draft {} for: {}",
state.revision, state.topic
));
state.draft = format!(
"# {}\n\n\
{} is a interesting topic that many people are talking about.\n\n\
{}",
state.topic, state.topic, state.research,
);
}
Ok((state, Outcome::Continue))
}
}
struct Editor;
impl Agent<ArticleState> for Editor {
fn name(&self) -> &'static str {
"editor"
}
fn run(&mut self, mut state: ArticleState, ctx: &mut Ctx) -> StepResult<ArticleState> {
ctx.log(format!("reviewing rev {}: {}", state.revision, state.topic));
if state.revision < 2 {
state.feedback =
"opening is bland, needs a hook. 'a interesting' should be 'an interesting'".into();
ctx.log(format!("needs revision: {}", state.feedback));
Ok((state, Outcome::Next("writer")))
} else {
ctx.log(format!("approved: {}", state.topic));
Ok((state, Outcome::Done))
}
}
}
fn main() {
let topics = vec![
"Rust in embedded systems".to_string(),
"Why plumbers love side projects".to_string(),
"Electricians using Raspberry Pi on the job".to_string(),
];
let guidelines = "\
Write in first person. \
Do not use emdashes. \
Add a touch of humor. \
Keep it under 300 words."
.to_string();
println!("=== Fan-out: {} threads ===\n", topics.len());
let handles: Vec<_> = topics
.into_iter()
.enumerate()
.map(|(i, topic)| {
let guidelines = guidelines.clone();
thread::spawn(move || {
let mut ctx = Ctx::new();
let wf = Workflow::builder("write-article")
.register(Researcher)
.register(Writer)
.register(Editor)
.start_at("researcher")
.then("writer")
.then("editor")
.build()
.unwrap();
let mut runner = Runner::new(wf).with_max_retries(5);
let result = runner.run(ArticleState::new(topic.clone(), guidelines), &mut ctx);
for entry in ctx.logs() {
println!(" [thread {}] {}", i, entry);
}
result
})
})
.collect();
let mut finished = Vec::new();
for handle in handles {
match handle.join().unwrap() {
Ok(state) => finished.push(state),
Err(e) => eprintln!("thread failed: {e}"),
}
}
println!("\n=== Fan-in: {} articles ===\n", finished.len());
for (i, article) in finished.iter().enumerate() {
let preview: String = article.draft.chars().take(72).collect();
println!(
" {}. {} (rev {})\n {preview}...\n",
i + 1,
article.topic,
article.revision
);
}
}