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
//! The smallest thing this runtime does: one skill, one run, one replay.
//!
//! Every other example layers a real concern on top — crash recovery, cases,
//! plans, sagas, memory. This one is deliberately none of that. It exists so a
//! newcomer has a runnable file, not a doc snippet, that shows the whole shape:
//!
//! * a skill is one `invoke`, handed a `StepCtx` and a labelled input;
//! * the clock is an effect (`cx.now()`), so replay reads the recorded instant
//! rather than reading the wall clock again;
//! * a strict replay re-runs the logic and reproduces the answer byte for byte
//! without performing anything.
//!
//! Run it: `cargo run --example hello_skill`
use std::sync::Arc;
// The getting-started page calls this "this exact skill", so it imports the way
// that page does: one prelude line, which is the whole point of having one —
// `async_trait` included, so writing a skill starts here rather than in
// `Cargo.toml`.
use agentplane::prelude::*;
use serde_json::{Value, json};
#[derive(Debug)]
struct Greet;
#[async_trait]
impl Skill for Greet {
// No `.provides(..)`: a skill that declares nothing answers its own name.
fn descriptor(&self) -> SkillDescriptor {
SkillDescriptor::new("greet")
}
async fn invoke(
&self,
cx: &mut StepCtx<'_>,
input: Tainted<Value>,
) -> Result<Outcome, SkillError> {
// A journaled effect: on replay this returns the recorded instant, not a
// fresh reading of the clock. `input.map` carries the label along.
let at = cx.now().await?;
Ok(Outcome::done(
input.map(|v| json!({ "greeted": v, "at": at.to_string() })),
))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store: Arc<dyn JournalStore> = Arc::new(RedbStore::open_in_memory()?);
let runtime = Runtime::builder(Arc::clone(&store)).skill(Greet).build();
let outcome = runtime
.run("greet", Tainted::trusted(json!({ "name": "world" })))
.await?;
// Re-executes the logic and reads every effect back from the journal.
// Nothing is performed again — no clock is read, no answer is invented.
let replayed = runtime.replay(outcome.run_id, Mode::Strict).await?;
assert_eq!(outcome.output, replayed.output, "replay reproduced the run");
// `.success()` turns "did it work, and what did it say" into one `?`:
// the labelled answer, or a typed error naming the run and its status.
let answer = outcome.success()?;
println!("live: {}", answer.peek());
println!("replay: {}", replayed.success()?.peek());
println!("\nSame answer, and the second time nothing happened.");
Ok(())
}