forked from tinyhumansai/tinyagents
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresilient_graph.rs
More file actions
135 lines (125 loc) · 5.14 KB
/
Copy pathresilient_graph.rs
File metadata and controls
135 lines (125 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Surviving network blips and restarting a failed run.
//!
//! Two resilience primitives, on one durable graph:
//!
//! 1. **Node-level retry.** [`CompiledGraph::with_node_retry`] re-runs a node
//! from its start when its handler fails with a transient
//! ([retryable][tinyagents::harness::retry::is_retryable]) error — a model or
//! tool/network error. A `fetch` node here fails its first two attempts (a
//! simulated connectivity blip) and succeeds on the third, so the run
//! completes without any operator involvement.
//!
//! 2. **Resumable failure + restart.** A `commit` node fails *harder* than the
//! retry budget allows. On a checkpointed thread the run does not vanish: it
//! persists a resumable failure-boundary checkpoint (the failed node is
//! scheduled to re-run, earlier progress is preserved) and returns the error.
//! The "outage" then clears and [`CompiledGraph::retry`] restarts the run
//! from that checkpoint to completion. A run could equally be *continued on
//! user feedback* by editing state with `update_state` before `retry`.
//!
//! Run with:
//!
//! ```text
//! cargo run --example resilient_graph
//! ```
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tinyagents::harness::ids::ExecutionStatus;
use tinyagents::harness::retry::RetryPolicy;
use tinyagents::{
GraphBuilder, InMemoryCheckpointer, NodeContext, NodeResult, Result, TinyAgentsError,
};
/// A tiny pipeline state: how far we got, plus an audit log.
#[derive(Clone, Debug, Default)]
struct Pipeline {
fetched: bool,
committed: bool,
log: Vec<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
let checkpointer = Arc::new(InMemoryCheckpointer::<Pipeline>::new());
// A flaky `fetch`: fails (retryable model error) its first two attempts,
// then succeeds — a transient connectivity blip the node-retry policy
// absorbs on its own.
let fetch_attempts = Arc::new(AtomicUsize::new(0));
// A `commit` that stays down until the "outage" clears between runs. The
// outer restart, not the inner retry policy, is what recovers it.
let outage = Arc::new(AtomicUsize::new(0));
let fa = fetch_attempts.clone();
let og = outage.clone();
let graph = GraphBuilder::<Pipeline, Pipeline>::overwrite()
.add_node("fetch", move |mut state: Pipeline, _c: NodeContext| {
let fa = fa.clone();
async move {
let n = fa.fetch_add(1, Ordering::SeqCst);
if n < 2 {
return Err(TinyAgentsError::Model(format!(
"connection reset (fetch attempt {})",
n + 1
)));
}
state.fetched = true;
state.log.push(format!("fetched on attempt {}", n + 1));
Ok(NodeResult::Update(state))
}
})
.add_node("commit", move |mut state: Pipeline, _c: NodeContext| {
let og = og.clone();
async move {
// Down while the outage flag is 0; healthy once it is raised.
if og.load(Ordering::SeqCst) == 0 {
return Err(TinyAgentsError::Tool(
"commit endpoint unreachable (outage)".into(),
));
}
state.committed = true;
state.log.push("committed".into());
Ok(NodeResult::Update(state))
}
})
.set_entry("fetch")
.add_edge("fetch", "commit")
.set_finish("commit")
.compile()
.unwrap()
// 1 try + 3 retries per node, absorbing the fetch blips.
.with_node_retry(RetryPolicy::default().with_max_attempts(4))
.with_checkpointer(checkpointer.clone());
// ---- First run: fetch self-heals, but commit is fully down. ------------
let thread = "orders-4711";
let err = graph
.run_with_thread(thread, Pipeline::default())
.await
.expect_err("commit is down, so the first run fails");
println!("first run failed as expected: {err}");
println!(
" fetch took {} attempts",
fetch_attempts.load(Ordering::SeqCst)
);
// The failure left a resumable checkpoint: `fetch`'s progress is committed
// and `commit` is scheduled to re-run.
let snapshot = graph
.get_state(thread, None)
.await?
.expect("a failure-boundary checkpoint exists");
println!(
" durable state: fetched={}, committed={}",
snapshot.values.fetched, snapshot.values.committed
);
println!(" will re-run on resume: {:?}", snapshot.next_nodes);
println!(" checkpoints on thread: {}", checkpointer.count(thread));
// ---- The outage clears; restart the run from the checkpoint. -----------
outage.store(1, Ordering::SeqCst);
let resumed = graph.retry(thread).await?;
assert_eq!(resumed.status.status, ExecutionStatus::Completed);
println!("\nrestarted to completion:");
println!(
" fetched={}, committed={}",
resumed.state.fetched, resumed.state.committed
);
for line in &resumed.state.log {
println!(" - {line}");
}
Ok(())
}