Skip to main content

Runner

Struct Runner 

Source
pub struct Runner<S: Clone + 'static> { /* private fields */ }
Expand description

Executes a Workflow step by step, handling retries, waits, and routing.

Implementations§

Source§

impl<S: Clone + 'static> Runner<S>

Source

pub fn new(wf: Workflow<S>) -> Self

Create a runner for the given workflow with default limits (max_steps: 10,000, max_retries: 3).

Examples found in repository?
examples/workflow.rs (line 42)
32fn main() {
33    let mut ctx = Ctx::new();
34    let wf = Workflow::builder("demo")
35        .register(AddOne)
36        .register(StopAtThree)
37        .start_at("add_one")
38        .then("stop")
39        .build()
40        .unwrap();
41
42    let final_state = Runner::new(wf).run(State { n: 0 }, &mut ctx).unwrap();
43    println!("final n={}", final_state.n);
44}
More examples
Hide additional examples
examples/assistant.rs (line 131)
116fn main() {
117    let mut ctx = Ctx::new();
118
119    let wf = Workflow::builder("daily-briefing")
120        .register(FetchWeather)
121        .register(FetchCalendar)
122        .register(FetchEmail)
123        .register(Summarize)
124        .start_at("fetch_weather")
125        .then("fetch_calendar")
126        .then("fetch_email")
127        .then("summarize")
128        .build()
129        .unwrap();
130
131    let mut runner = Runner::new(wf).with_tracing();
132
133    let mut iteration = 0;
134    loop {
135        iteration += 1;
136        println!("=== Briefing #{iteration} ===\n");
137
138        match runner.run(BriefingState::new(), &mut ctx) {
139            Ok(state) => {
140                println!("{}\n", state.summary);
141            }
142            Err(e) => {
143                eprintln!("Briefing failed: {e}\n");
144            }
145        }
146
147        ctx.clear_logs();
148
149        println!("(sleeping 30s before next briefing...)\n");
150        thread::sleep(Duration::from_secs(30));
151    }
152}
examples/edit_loop.rs (lines 93-102)
90fn main() {
91    let mut ctx = Ctx::new();
92
93    let mut runner = Runner::new(
94        Workflow::builder("edit-loop")
95            .register(Writer)
96            .register(Validator)
97            .register(Fixer { retried: false })
98            .start_at("writer")
99            .then("validator")
100            .build()
101            .unwrap(),
102    );
103
104    let mut revision = 0;
105    for round in 1..=3 {
106        println!("=== Round {round} ===");
107
108        let doc = Doc {
109            text: String::new(),
110            revision,
111        };
112
113        match runner.run(doc, &mut ctx) {
114            Ok(doc) => {
115                println!("  Final text: {:?}", doc.text);
116                println!("  Revisions:  {}", doc.revision);
117                revision = doc.revision;
118            }
119            Err(e) => println!("  Error: {e}"),
120        }
121
122        println!("  Log:");
123        for entry in ctx.logs() {
124            println!("    {entry}");
125        }
126        ctx.clear_logs();
127        println!();
128    }
129}
examples/coder.rs (line 181)
161fn main() {
162    let tmp = std::env::temp_dir().join("agent-line-coder");
163    scaffold_project(&tmp);
164
165    let lib_path = tmp.join("src/lib.rs").display().to_string();
166    let manifest = tmp.join("Cargo.toml").display().to_string();
167
168    let mut ctx = Ctx::new();
169    ctx.set("manifest_path", &manifest);
170
171    let wf = Workflow::builder("coding-agent")
172        .register(Planner)
173        .register(Coder)
174        .register(Tester)
175        .start_at("planner")
176        .then("coder")
177        .then("tester")
178        .build()
179        .unwrap();
180
181    let mut runner = Runner::new(wf);
182
183    let result = runner.run(
184        Task {
185            description: "Add a function called `reverse_string` that reverses a string \
186                          and add unit tests"
187                .into(),
188            file_path: lib_path,
189            code: String::new(),
190            test_output: String::new(),
191            attempts: 0,
192            max_attempts: 3,
193        },
194        &mut ctx,
195    );
196
197    println!("=== Result ===");
198    match result {
199        Ok(task) => {
200            println!("  Success after {} fix attempts", task.attempts);
201            println!("  Final code:\n{}", task.code);
202        }
203        Err(e) => println!("  Failed: {e}"),
204    }
205
206    println!("\n=== Log ===");
207    for entry in ctx.logs() {
208        println!("  {entry}");
209    }
210}
examples/newsletter.rs (line 169)
155fn main() {
156    let mut ctx = Ctx::new();
157
158    // could populate ctx.store with some writing rules or could read in a markdown skill and pass
159    // that into the agent.
160    // Phase 1: find topics
161    let topic_wf = Workflow::builder("find-topics")
162        .register(TopicSearcher)
163        .register(TopicPicker)
164        .start_at("topic_searcher")
165        .then("topic_picker")
166        .build()
167        .unwrap();
168
169    let mut topic_runner = Runner::new(topic_wf);
170    let topics = topic_runner
171        .run(
172            TopicState {
173                query: "bluecollar engineering newsletter".into(),
174                topics: vec![],
175                selected: vec![],
176            },
177            &mut ctx,
178        )
179        .unwrap();
180
181    println!("=== Topics ===");
182    for entry in ctx.logs() {
183        println!("  {entry}");
184    }
185    ctx.clear_logs();
186    println!();
187
188    // Phase 2: write one article per topic
189    let article_wf = Workflow::builder("write-article")
190        .register(ArticleWriter)
191        .register(ArticleValidator)
192        .register(ArticleFixer)
193        .start_at("article_writer")
194        .then("article_validator")
195        .build()
196        .unwrap();
197
198    let mut article_runner = Runner::new(article_wf);
199    let mut finished_articles: Vec<String> = Vec::new();
200
201    for (i, topic) in topics.selected.iter().enumerate() {
202        println!("=== Article {} ===", i + 1);
203
204        let result = article_runner
205            .run(
206                ArticleState {
207                    topic: topic.clone(),
208                    draft: String::new(),
209                    revision: 0,
210                },
211                &mut ctx,
212            )
213            .unwrap();
214
215        finished_articles.push(result.draft.clone());
216        println!("  Revisions: {}", result.revision);
217
218        for entry in ctx.logs() {
219            println!("  {entry}");
220        }
221        ctx.clear_logs();
222        println!();
223    }
224
225    // Phase 3: "store" the articles
226    println!("=== Stored ===");
227    for (i, article) in finished_articles.iter().enumerate() {
228        let preview: String = article.chars().take(60).collect();
229        println!("  article_{}.md: {preview}...", i + 1);
230    }
231}
examples/parallel.rs (line 237)
199fn main() {
200    let topics = vec![
201        "Rust in embedded systems".to_string(),
202        "Why plumbers love side projects".to_string(),
203        "Electricians using Raspberry Pi on the job".to_string(),
204    ];
205
206    // Shared guidelines for all writers -- the author's voice and style rules
207    let guidelines = "\
208        Write in first person. \
209        Do not use emdashes. \
210        Add a touch of humor. \
211        Keep it under 300 words."
212        .to_string();
213
214    println!("=== Fan-out: {} threads ===\n", topics.len());
215
216    // Fan-out: spawn one thread per topic
217    let handles: Vec<_> = topics
218        .into_iter()
219        .enumerate()
220        .map(|(i, topic)| {
221            let guidelines = guidelines.clone();
222
223            thread::spawn(move || {
224                // Each thread gets its own Ctx and Runner -- no shared mutable state
225                let mut ctx = Ctx::new();
226
227                let wf = Workflow::builder("write-article")
228                    .register(Researcher)
229                    .register(Writer)
230                    .register(Editor)
231                    .start_at("researcher")
232                    .then("writer")
233                    .then("editor")
234                    .build()
235                    .unwrap();
236
237                let mut runner = Runner::new(wf).with_max_retries(5);
238
239                let result = runner.run(ArticleState::new(topic.clone(), guidelines), &mut ctx);
240
241                // Print the log from this thread's pipeline
242                for entry in ctx.logs() {
243                    println!("  [thread {}] {}", i, entry);
244                }
245
246                result
247            })
248        })
249        .collect();
250
251    // Fan-in: join all threads, collect results
252    let mut finished = Vec::new();
253    for handle in handles {
254        match handle.join().unwrap() {
255            Ok(state) => finished.push(state),
256            Err(e) => eprintln!("thread failed: {e}"),
257        }
258    }
259
260    // Show results
261    println!("\n=== Fan-in: {} articles ===\n", finished.len());
262    for (i, article) in finished.iter().enumerate() {
263        let preview: String = article.draft.chars().take(72).collect();
264        println!(
265            "  {}. {} (rev {})\n     {preview}...\n",
266            i + 1,
267            article.topic,
268            article.revision
269        );
270    }
271}
Source

pub fn with_max_steps(self, max_steps: usize) -> Self

Prevent accidental infinite loops.

Source

pub fn with_max_retries(self, max_retries: usize) -> Self

Set the maximum consecutive retries per agent before failing.

Examples found in repository?
examples/parallel.rs (line 237)
199fn main() {
200    let topics = vec![
201        "Rust in embedded systems".to_string(),
202        "Why plumbers love side projects".to_string(),
203        "Electricians using Raspberry Pi on the job".to_string(),
204    ];
205
206    // Shared guidelines for all writers -- the author's voice and style rules
207    let guidelines = "\
208        Write in first person. \
209        Do not use emdashes. \
210        Add a touch of humor. \
211        Keep it under 300 words."
212        .to_string();
213
214    println!("=== Fan-out: {} threads ===\n", topics.len());
215
216    // Fan-out: spawn one thread per topic
217    let handles: Vec<_> = topics
218        .into_iter()
219        .enumerate()
220        .map(|(i, topic)| {
221            let guidelines = guidelines.clone();
222
223            thread::spawn(move || {
224                // Each thread gets its own Ctx and Runner -- no shared mutable state
225                let mut ctx = Ctx::new();
226
227                let wf = Workflow::builder("write-article")
228                    .register(Researcher)
229                    .register(Writer)
230                    .register(Editor)
231                    .start_at("researcher")
232                    .then("writer")
233                    .then("editor")
234                    .build()
235                    .unwrap();
236
237                let mut runner = Runner::new(wf).with_max_retries(5);
238
239                let result = runner.run(ArticleState::new(topic.clone(), guidelines), &mut ctx);
240
241                // Print the log from this thread's pipeline
242                for entry in ctx.logs() {
243                    println!("  [thread {}] {}", i, entry);
244                }
245
246                result
247            })
248        })
249        .collect();
250
251    // Fan-in: join all threads, collect results
252    let mut finished = Vec::new();
253    for handle in handles {
254        match handle.join().unwrap() {
255            Ok(state) => finished.push(state),
256            Err(e) => eprintln!("thread failed: {e}"),
257        }
258    }
259
260    // Show results
261    println!("\n=== Fan-in: {} articles ===\n", finished.len());
262    for (i, article) in finished.iter().enumerate() {
263        let preview: String = article.draft.chars().take(72).collect();
264        println!(
265            "  {}. {} (rev {})\n     {preview}...\n",
266            i + 1,
267            article.topic,
268            article.revision
269        );
270    }
271}
Source

pub fn on_step(self, cb: impl FnMut(&StepEvent<'_>) + 'static) -> Self

Register a callback that fires after each successful agent step.

Source

pub fn on_error(self, cb: impl FnMut(&ErrorEvent<'_>) + 'static) -> Self

Register a callback that fires when an agent errors or a limit is exceeded.

Source

pub fn with_tracing(self) -> Self

Set both hooks to print step transitions and errors to stderr.

Examples found in repository?
examples/assistant.rs (line 131)
116fn main() {
117    let mut ctx = Ctx::new();
118
119    let wf = Workflow::builder("daily-briefing")
120        .register(FetchWeather)
121        .register(FetchCalendar)
122        .register(FetchEmail)
123        .register(Summarize)
124        .start_at("fetch_weather")
125        .then("fetch_calendar")
126        .then("fetch_email")
127        .then("summarize")
128        .build()
129        .unwrap();
130
131    let mut runner = Runner::new(wf).with_tracing();
132
133    let mut iteration = 0;
134    loop {
135        iteration += 1;
136        println!("=== Briefing #{iteration} ===\n");
137
138        match runner.run(BriefingState::new(), &mut ctx) {
139            Ok(state) => {
140                println!("{}\n", state.summary);
141            }
142            Err(e) => {
143                eprintln!("Briefing failed: {e}\n");
144            }
145        }
146
147        ctx.clear_logs();
148
149        println!("(sleeping 30s before next briefing...)\n");
150        thread::sleep(Duration::from_secs(30));
151    }
152}
Source

pub fn run(&mut self, state: S, ctx: &mut Ctx) -> Result<S, StepError>

Run the workflow to completion, returning the final state or an error. Can be called multiple times on the same runner.

Examples found in repository?
examples/workflow.rs (line 42)
32fn main() {
33    let mut ctx = Ctx::new();
34    let wf = Workflow::builder("demo")
35        .register(AddOne)
36        .register(StopAtThree)
37        .start_at("add_one")
38        .then("stop")
39        .build()
40        .unwrap();
41
42    let final_state = Runner::new(wf).run(State { n: 0 }, &mut ctx).unwrap();
43    println!("final n={}", final_state.n);
44}
More examples
Hide additional examples
examples/assistant.rs (line 138)
116fn main() {
117    let mut ctx = Ctx::new();
118
119    let wf = Workflow::builder("daily-briefing")
120        .register(FetchWeather)
121        .register(FetchCalendar)
122        .register(FetchEmail)
123        .register(Summarize)
124        .start_at("fetch_weather")
125        .then("fetch_calendar")
126        .then("fetch_email")
127        .then("summarize")
128        .build()
129        .unwrap();
130
131    let mut runner = Runner::new(wf).with_tracing();
132
133    let mut iteration = 0;
134    loop {
135        iteration += 1;
136        println!("=== Briefing #{iteration} ===\n");
137
138        match runner.run(BriefingState::new(), &mut ctx) {
139            Ok(state) => {
140                println!("{}\n", state.summary);
141            }
142            Err(e) => {
143                eprintln!("Briefing failed: {e}\n");
144            }
145        }
146
147        ctx.clear_logs();
148
149        println!("(sleeping 30s before next briefing...)\n");
150        thread::sleep(Duration::from_secs(30));
151    }
152}
examples/edit_loop.rs (line 113)
90fn main() {
91    let mut ctx = Ctx::new();
92
93    let mut runner = Runner::new(
94        Workflow::builder("edit-loop")
95            .register(Writer)
96            .register(Validator)
97            .register(Fixer { retried: false })
98            .start_at("writer")
99            .then("validator")
100            .build()
101            .unwrap(),
102    );
103
104    let mut revision = 0;
105    for round in 1..=3 {
106        println!("=== Round {round} ===");
107
108        let doc = Doc {
109            text: String::new(),
110            revision,
111        };
112
113        match runner.run(doc, &mut ctx) {
114            Ok(doc) => {
115                println!("  Final text: {:?}", doc.text);
116                println!("  Revisions:  {}", doc.revision);
117                revision = doc.revision;
118            }
119            Err(e) => println!("  Error: {e}"),
120        }
121
122        println!("  Log:");
123        for entry in ctx.logs() {
124            println!("    {entry}");
125        }
126        ctx.clear_logs();
127        println!();
128    }
129}
examples/coder.rs (lines 183-195)
161fn main() {
162    let tmp = std::env::temp_dir().join("agent-line-coder");
163    scaffold_project(&tmp);
164
165    let lib_path = tmp.join("src/lib.rs").display().to_string();
166    let manifest = tmp.join("Cargo.toml").display().to_string();
167
168    let mut ctx = Ctx::new();
169    ctx.set("manifest_path", &manifest);
170
171    let wf = Workflow::builder("coding-agent")
172        .register(Planner)
173        .register(Coder)
174        .register(Tester)
175        .start_at("planner")
176        .then("coder")
177        .then("tester")
178        .build()
179        .unwrap();
180
181    let mut runner = Runner::new(wf);
182
183    let result = runner.run(
184        Task {
185            description: "Add a function called `reverse_string` that reverses a string \
186                          and add unit tests"
187                .into(),
188            file_path: lib_path,
189            code: String::new(),
190            test_output: String::new(),
191            attempts: 0,
192            max_attempts: 3,
193        },
194        &mut ctx,
195    );
196
197    println!("=== Result ===");
198    match result {
199        Ok(task) => {
200            println!("  Success after {} fix attempts", task.attempts);
201            println!("  Final code:\n{}", task.code);
202        }
203        Err(e) => println!("  Failed: {e}"),
204    }
205
206    println!("\n=== Log ===");
207    for entry in ctx.logs() {
208        println!("  {entry}");
209    }
210}
examples/newsletter.rs (lines 171-178)
155fn main() {
156    let mut ctx = Ctx::new();
157
158    // could populate ctx.store with some writing rules or could read in a markdown skill and pass
159    // that into the agent.
160    // Phase 1: find topics
161    let topic_wf = Workflow::builder("find-topics")
162        .register(TopicSearcher)
163        .register(TopicPicker)
164        .start_at("topic_searcher")
165        .then("topic_picker")
166        .build()
167        .unwrap();
168
169    let mut topic_runner = Runner::new(topic_wf);
170    let topics = topic_runner
171        .run(
172            TopicState {
173                query: "bluecollar engineering newsletter".into(),
174                topics: vec![],
175                selected: vec![],
176            },
177            &mut ctx,
178        )
179        .unwrap();
180
181    println!("=== Topics ===");
182    for entry in ctx.logs() {
183        println!("  {entry}");
184    }
185    ctx.clear_logs();
186    println!();
187
188    // Phase 2: write one article per topic
189    let article_wf = Workflow::builder("write-article")
190        .register(ArticleWriter)
191        .register(ArticleValidator)
192        .register(ArticleFixer)
193        .start_at("article_writer")
194        .then("article_validator")
195        .build()
196        .unwrap();
197
198    let mut article_runner = Runner::new(article_wf);
199    let mut finished_articles: Vec<String> = Vec::new();
200
201    for (i, topic) in topics.selected.iter().enumerate() {
202        println!("=== Article {} ===", i + 1);
203
204        let result = article_runner
205            .run(
206                ArticleState {
207                    topic: topic.clone(),
208                    draft: String::new(),
209                    revision: 0,
210                },
211                &mut ctx,
212            )
213            .unwrap();
214
215        finished_articles.push(result.draft.clone());
216        println!("  Revisions: {}", result.revision);
217
218        for entry in ctx.logs() {
219            println!("  {entry}");
220        }
221        ctx.clear_logs();
222        println!();
223    }
224
225    // Phase 3: "store" the articles
226    println!("=== Stored ===");
227    for (i, article) in finished_articles.iter().enumerate() {
228        let preview: String = article.chars().take(60).collect();
229        println!("  article_{}.md: {preview}...", i + 1);
230    }
231}
examples/parallel.rs (line 239)
199fn main() {
200    let topics = vec![
201        "Rust in embedded systems".to_string(),
202        "Why plumbers love side projects".to_string(),
203        "Electricians using Raspberry Pi on the job".to_string(),
204    ];
205
206    // Shared guidelines for all writers -- the author's voice and style rules
207    let guidelines = "\
208        Write in first person. \
209        Do not use emdashes. \
210        Add a touch of humor. \
211        Keep it under 300 words."
212        .to_string();
213
214    println!("=== Fan-out: {} threads ===\n", topics.len());
215
216    // Fan-out: spawn one thread per topic
217    let handles: Vec<_> = topics
218        .into_iter()
219        .enumerate()
220        .map(|(i, topic)| {
221            let guidelines = guidelines.clone();
222
223            thread::spawn(move || {
224                // Each thread gets its own Ctx and Runner -- no shared mutable state
225                let mut ctx = Ctx::new();
226
227                let wf = Workflow::builder("write-article")
228                    .register(Researcher)
229                    .register(Writer)
230                    .register(Editor)
231                    .start_at("researcher")
232                    .then("writer")
233                    .then("editor")
234                    .build()
235                    .unwrap();
236
237                let mut runner = Runner::new(wf).with_max_retries(5);
238
239                let result = runner.run(ArticleState::new(topic.clone(), guidelines), &mut ctx);
240
241                // Print the log from this thread's pipeline
242                for entry in ctx.logs() {
243                    println!("  [thread {}] {}", i, entry);
244                }
245
246                result
247            })
248        })
249        .collect();
250
251    // Fan-in: join all threads, collect results
252    let mut finished = Vec::new();
253    for handle in handles {
254        match handle.join().unwrap() {
255            Ok(state) => finished.push(state),
256            Err(e) => eprintln!("thread failed: {e}"),
257        }
258    }
259
260    // Show results
261    println!("\n=== Fan-in: {} articles ===\n", finished.len());
262    for (i, article) in finished.iter().enumerate() {
263        let preview: String = article.draft.chars().take(72).collect();
264        println!(
265            "  {}. {} (rev {})\n     {preview}...\n",
266            i + 1,
267            article.topic,
268            article.revision
269        );
270    }
271}

Auto Trait Implementations§

§

impl<S> Freeze for Runner<S>

§

impl<S> !RefUnwindSafe for Runner<S>

§

impl<S> !Send for Runner<S>

§

impl<S> !Sync for Runner<S>

§

impl<S> Unpin for Runner<S>

§

impl<S> UnsafeUnpin for Runner<S>

§

impl<S> !UnwindSafe for Runner<S>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.