Skip to main content

travel_assistant/
travel_assistant.rs

1use agentledger::{
2    debug_html, export_evidence, replay, AgentContext, BudgetLimits, MemoryStore, Runtime,
3    RuntimeError, State, ToolSpec, Value,
4};
5use std::collections::HashMap;
6use std::fs;
7use std::io::{self, BufRead, Write};
8use std::path::PathBuf;
9use std::sync::{LazyLock, Mutex};
10
11// ════════════════════════════════════════════════════════════
12// ANSI Colors
13// ════════════════════════════════════════════════════════════
14const C_R: &str = "\x1b[91m";
15const C_G: &str = "\x1b[92m";
16const C_Y: &str = "\x1b[93m";
17const C_B: &str = "\x1b[94m";
18const C_M: &str = "\x1b[95m";
19const C_C: &str = "\x1b[96m";
20const C_BOLD: &str = "\x1b[1m";
21const C_DIM: &str = "\x1b[2m";
22const C_RST: &str = "\x1b[0m";
23
24// ════════════════════════════════════════════════════════════
25// Mock data (LazyLock for static initialization)
26// ════════════════════════════════════════════════════════════
27static MOCK_FLIGHTS: LazyLock<Mutex<Vec<HashMap<String, Value>>>> = LazyLock::new(|| {
28    Mutex::new(vec![HashMap::from([
29        ("id".to_string(), Value::String("FL-002".into())),
30        ("from_city".to_string(), Value::String("Beijing".into())),
31        ("from_code".to_string(), Value::String("PEK".into())),
32        ("to_city".to_string(), Value::String("Tokyo".into())),
33        ("to_code".to_string(), Value::String("NRT".into())),
34        ("date".to_string(), Value::String("2025-06-15".into())),
35        ("airline".to_string(), Value::String("JAL".into())),
36        ("price_usd".to_string(), Value::Number(580.0)),
37    ])])
38});
39
40static MOCK_HOTELS: LazyLock<Mutex<Vec<HashMap<String, Value>>>> = LazyLock::new(|| {
41    Mutex::new(vec![HashMap::from([
42        ("id".to_string(), Value::String("HT-002".into())),
43        ("city".to_string(), Value::String("Tokyo".into())),
44        ("name".to_string(), Value::String("APA Hotel Shinjuku".into())),
45        ("nightly_usd".to_string(), Value::Number(85.0)),
46        ("stars".to_string(), Value::Number(3.0)),
47    ])])
48});
49
50static BOOKING_DB: LazyLock<Mutex<HashMap<String, HashMap<String, Value>>>> =
51    LazyLock::new(|| Mutex::new(HashMap::new()));
52
53// ════════════════════════════════════════════════════════════
54// Value helpers (since `state()` is private)
55// ════════════════════════════════════════════════════════════
56
57fn make_state(items: &[(&str, Value)]) -> State {
58    let mut out = State::new();
59    for (key, value) in items {
60        out.insert(key.to_string(), value.clone());
61    }
62    out
63}
64
65fn get_string(obj: &Value, key: &str) -> String {
66    match obj {
67        Value::Object(map) => match map.get(key) {
68            Some(Value::String(s)) => s.clone(),
69            _ => String::new(),
70        },
71        _ => String::new(),
72    }
73}
74
75fn get_number(obj: &Value, key: &str) -> f64 {
76    match obj {
77        Value::Object(map) => match map.get(key) {
78            Some(Value::Number(n)) => *n,
79            _ => 0.0,
80        },
81        _ => 0.0,
82    }
83}
84
85fn string_arg(args: &State, key: &str) -> String {
86    match args.get(key) {
87        Some(Value::String(s)) => s.clone(),
88        _ => String::new(),
89    }
90}
91
92fn string_field(map: &HashMap<String, Value>, key: &str) -> String {
93    match map.get(key) {
94        Some(Value::String(s)) => s.clone(),
95        _ => String::new(),
96    }
97}
98
99// ════════════════════════════════════════════════════════════
100// Tool implementations
101// ════════════════════════════════════════════════════════════
102
103fn search_flights(args: State) -> agentledger::Result<Value> {
104    let origin = string_arg(&args, "from").to_lowercase();
105    let dest = string_arg(&args, "to").to_lowercase();
106    let flights = MOCK_FLIGHTS.lock().unwrap();
107    let results: Vec<Value> = flights
108        .iter()
109        .filter(|f| {
110            let fc = string_field(f, "from_city").to_lowercase();
111            let fcode = string_field(f, "from_code").to_lowercase();
112            let tc = string_field(f, "to_city").to_lowercase();
113            let tcode = string_field(f, "to_code").to_lowercase();
114            (fc.contains(&origin) || fcode.contains(&origin))
115                && (tc.contains(&dest) || tcode.contains(&dest))
116        })
117        .map(|f| Value::Object(f.clone()))
118        .collect();
119    let count = results.len();
120    Ok(Value::Object(make_state(&[
121        ("results", Value::Array(results)),
122        ("count", Value::Number(count as f64)),
123    ])))
124}
125
126fn search_hotels(args: State) -> agentledger::Result<Value> {
127    let city = string_arg(&args, "city").to_lowercase();
128    let hotels = MOCK_HOTELS.lock().unwrap();
129    let results: Vec<Value> = hotels
130        .iter()
131        .filter(|h| string_field(h, "city").to_lowercase() == city)
132        .map(|h| Value::Object(h.clone()))
133        .collect();
134    let count = results.len();
135    Ok(Value::Object(make_state(&[
136        ("results", Value::Array(results)),
137        ("count", Value::Number(count as f64)),
138    ])))
139}
140
141fn check_weather(args: State) -> agentledger::Result<Value> {
142    let city = string_arg(&args, "city");
143    match city.as_str() {
144        "Tokyo" => Ok(Value::Object(make_state(&[
145            ("city", Value::String("Tokyo".into())),
146            ("temp_c", Value::Number(24.0)),
147            ("condition", Value::String("Partly Cloudy".into())),
148            ("humidity", Value::Number(65.0)),
149        ]))),
150        _ => Ok(Value::Object(make_state(&[
151            ("city", Value::String(city)),
152            ("temp_c", Value::Number(20.0)),
153            ("condition", Value::String("Unknown".into())),
154        ]))),
155    }
156}
157
158fn book_flight(args: State) -> agentledger::Result<Value> {
159    let flight_id = string_arg(&args, "flight_id");
160    let passenger = string_arg(&args, "passenger");
161    let prefix: String = passenger.chars().take(3).collect();
162    let ref_key = format!("BK-F-{}-{}", flight_id, prefix.to_uppercase());
163
164    let mut db = BOOKING_DB.lock().unwrap();
165    if let Some(existing) = db.get(&ref_key) {
166        return Ok(Value::Object(existing.clone()));
167    }
168
169    let flights = MOCK_FLIGHTS.lock().unwrap();
170    let f = flights.iter().find(|f| string_field(f, "id") == flight_id);
171    match f {
172        Some(flight) => {
173            let airline = string_field(flight, "airline");
174            let price = get_number(&Value::Object(flight.clone()), "price_usd");
175            let booking = make_state(&[
176                ("booking_ref", Value::String(ref_key.clone())),
177                ("type", Value::String("flight".into())),
178                ("airline", Value::String(airline)),
179                ("price_usd", Value::Number(price)),
180                ("status", Value::String("confirmed".into())),
181                ("external_id", Value::String(ref_key.clone())),
182            ]);
183            db.insert(ref_key, booking.clone());
184            Ok(Value::Object(booking))
185        }
186        None => Err(RuntimeError(format!("flight not found: {}", flight_id))),
187    }
188}
189
190fn book_hotel(args: State) -> agentledger::Result<Value> {
191    let hotel_id = string_arg(&args, "hotel_id");
192    let guest = string_arg(&args, "guest");
193    let prefix: String = guest.chars().take(3).collect();
194    let ref_key = format!("BK-H-{}-{}", hotel_id, prefix.to_uppercase());
195
196    let mut db = BOOKING_DB.lock().unwrap();
197    if let Some(existing) = db.get(&ref_key) {
198        return Ok(Value::Object(existing.clone()));
199    }
200
201    let hotels = MOCK_HOTELS.lock().unwrap();
202    let h = hotels.iter().find(|h| string_field(h, "id") == hotel_id);
203    match h {
204        Some(hotel) => {
205            let name = string_field(hotel, "name");
206            let nightly = get_number(&Value::Object(hotel.clone()), "nightly_usd");
207            let booking = make_state(&[
208                ("booking_ref", Value::String(ref_key.clone())),
209                ("type", Value::String("hotel".into())),
210                ("name", Value::String(name)),
211                ("price_total_usd", Value::Number(nightly * 5.0)),
212                ("status", Value::String("confirmed".into())),
213                ("external_id", Value::String(ref_key.clone())),
214            ]);
215            db.insert(ref_key, booking.clone());
216            Ok(Value::Object(booking))
217        }
218        None => Err(RuntimeError(format!("hotel not found: {}", hotel_id))),
219    }
220}
221
222// ════════════════════════════════════════════════════════════
223// Agent logic (called per attempt)
224// ════════════════════════════════════════════════════════════
225
226fn travel_planner(
227    runtime: &mut Runtime,
228    ctx: &mut AgentContext,
229    attempt: u64,
230) -> agentledger::Result<()> {
231    // Phase 1: Research
232    let flights = runtime.call_tool(ctx, "travel.search_flights", make_state(&[
233        ("from", "Beijing".into()),
234        ("to", "Tokyo".into()),
235    ]))?;
236    let hotels = runtime.call_tool(ctx, "travel.search_hotels", make_state(&[
237        ("city", "Tokyo".into()),
238    ]))?;
239    let weather = runtime.call_tool(ctx, "travel.check_weather", make_state(&[
240        ("city", "Tokyo".into()),
241    ]))?;
242
243    let fc = get_number(&flights, "count") as i64;
244    let hc = get_number(&hotels, "count") as i64;
245    let wt = get_number(&weather, "temp_c");
246    ctx.write_state("research", Value::Object(make_state(&[
247        ("flights", Value::Number(fc as f64)),
248        ("hotels", Value::Number(hc as f64)),
249        ("weather", Value::Number(wt)),
250    ])));
251
252    // Phase 2: Book flight (approval required)
253    let flight = runtime.call_tool(ctx, "travel.book_flight", make_state(&[
254        ("flight_id", "FL-002".into()),
255        ("passenger", "Demo User".into()),
256        ("_logical_operation", "book-demo-flight".into()),
257    ]))?;
258
259    // Phase 3: Simulated crash on attempt 2
260    if attempt == 2 {
261        return Err(RuntimeError("retryable".to_string()));
262    }
263
264    // Phase 4: Book hotel (approval required)
265    let hotel = runtime.call_tool(ctx, "travel.book_hotel", make_state(&[
266        ("hotel_id", "HT-002".into()),
267        ("check_in", "2025-06-15".into()),
268        ("check_out", "2025-06-20".into()),
269        ("guest", "Demo User".into()),
270        ("_logical_operation", "book-demo-hotel".into()),
271    ]))?;
272
273    let flight_ref = get_string(&flight, "booking_ref");
274    let hotel_ref = get_string(&hotel, "booking_ref");
275    ctx.write_state("bookings", Value::Object(make_state(&[
276        ("flight", Value::String(flight_ref)),
277        ("hotel", Value::String(hotel_ref)),
278    ])));
279    ctx.write_state("trip_status", Value::String("confirmed".into()));
280    Ok(())
281}
282
283// ════════════════════════════════════════════════════════════
284// Step claim helper
285// ════════════════════════════════════════════════════════════
286
287fn claim_context(
288    runtime: &mut Runtime,
289    run_id: &str,
290    worker: &str,
291    role: &str,
292) -> AgentContext {
293    let claim = runtime.store.claim_step(worker, run_id, 60.0)
294        .expect("claim step failed");
295    let mut payload = State::new();
296    payload.insert("agent_role".to_string(), Value::String(role.to_string()));
297    payload.insert("attempt".to_string(), Value::Number(claim.attempt as f64));
298    runtime.store.append_event(
299        run_id,
300        Some(&claim.session_id),
301        Some(&claim.step_id),
302        "agent_started",
303        payload,
304        Some(role),
305        Some(claim.state_version),
306        None,
307    );
308    AgentContext {
309        run_id: claim.run_id.clone(),
310        session_id: claim.session_id,
311        step_id: claim.step_id.clone(),
312        agent_role: role.to_string(),
313        lease_token: claim.lease_token.clone(),
314        attempt: claim.attempt,
315        state_version: claim.state_version,
316        pending_patch: State::new(),
317    }
318}
319
320// ════════════════════════════════════════════════════════════
321// Display helpers
322// ════════════════════════════════════════════════════════════
323
324fn wait(msg: &str) {
325    print!("\n{}  ⏎ {}...{}", C_DIM, msg, C_RST);
326    io::stdout().flush().ok();
327    let mut line = String::new();
328    io::stdin().lock().read_line(&mut line).ok();
329}
330
331fn show_rows(label: &str, headers: &[&str], rows: &[Vec<String>], color: &str) {
332    if rows.is_empty() {
333        println!("\n  {}{}:{} {}(empty){}", color, label, C_RST, C_DIM, C_RST);
334        return;
335    }
336    println!("\n  {}{} ({} rows):{}", color, label, rows.len(), C_RST);
337    for row in rows {
338        let items: Vec<String> = headers
339            .iter()
340            .zip(row.iter())
341            .map(|(h, v)| format!("{}={}{}{}", h, C_BOLD, v, C_RST))
342            .collect();
343        println!("    {}{}{}", C_DIM, items.join(" | "), C_RST);
344    }
345}
346
347fn show_db(store: &MemoryStore, run_id: &str) {
348    // Runs
349    let mut run_rows = Vec::new();
350    if let Ok(run) = store.run(run_id) {
351        let short_id = if run.run_id.len() > 24 {
352            format!("{}...", &run.run_id[..24])
353        } else {
354            run.run_id.clone()
355        };
356        run_rows.push(vec![short_id, run.status, run.state_version.to_string()]);
357    }
358    show_rows("Runs", &["run_id", "status", "state_version"], &run_rows, C_B);
359
360    // Steps
361    let step_rows: Vec<Vec<String>> = store
362        .steps(run_id)
363        .into_iter()
364        .map(|s| {
365            let short_id = if s.step_id.len() > 24 {
366                format!("{}...", &s.step_id[..24])
367            } else {
368                s.step_id.clone()
369            };
370            vec![short_id, s.status, s.attempt.to_string()]
371        })
372        .collect();
373    show_rows("Steps", &["step_id", "status", "attempt"], &step_rows, C_B);
374
375    // Tool Ledger
376    let ledger_rows: Vec<Vec<String>> = store
377        .ledger(run_id)
378        .into_iter()
379        .map(|tl| {
380            let key = &tl.idempotency_key;
381            let short_key = match key.rfind(':') {
382                Some(idx) => {
383                    let prev = key[..idx].rfind(':').unwrap_or(0);
384                    if prev > 0 {
385                        format!("{}:{}", &key[prev + 1..idx], &key[idx + 1..])
386                    } else {
387                        format!("{}", &key[idx + 1..])
388                    }
389                }
390                None => key[..key.len().min(25)].to_string(),
391            };
392            vec![tl.tool_name, tl.status, short_key]
393        })
394        .collect();
395    show_rows(
396        "Tool Ledger",
397        &["tool", "status", "idemp_key"],
398        &ledger_rows,
399        C_Y,
400    );
401
402    // Approval requests
403    let approval_rows: Vec<Vec<String>> = store
404        .approval_requests(run_id)
405        .into_iter()
406        .map(|a| {
407            let approved_by = a.approved_by.unwrap_or_else(|| "-".to_string());
408            vec![a.tool_name, a.status, approved_by]
409        })
410        .collect();
411    show_rows(
412        "Approval Requests",
413        &["tool", "status", "approved_by"],
414        &approval_rows,
415        C_R,
416    );
417
418    println!();
419}
420
421// ════════════════════════════════════════════════════════════
422// Main
423// ════════════════════════════════════════════════════════════
424
425fn main() -> Result<(), Box<dyn std::error::Error>> {
426    let args: Vec<String> = std::env::args().collect();
427    let root = if args.len() > 1 {
428        PathBuf::from(&args[1])
429    } else {
430        std::env::temp_dir().join(format!("agentledger-rust-{}", std::process::id()))
431    };
432    let _ = fs::create_dir_all(&root);
433
434    // Intro
435    println!(
436        "\n{}{}  ╔════════════════════════════════════════════════════╗{}",
437        C_BOLD, C_C, C_RST
438    );
439    println!(
440        "{}{}  ║   AgentLedger Travel Assistant (Rust) — Interactive Demo  ║{}",
441        C_BOLD, C_C, C_RST
442    );
443    println!(
444        "{}{}  ║   See real database state at every step                 ║{}",
445        C_BOLD, C_C, C_RST
446    );
447    println!(
448        "{}{}  ╚════════════════════════════════════════════════════╝{}",
449        C_BOLD, C_C, C_RST
450    );
451
452    println!(
453        "\n  {}AgentLedger — Durable Execution Runtime for AI Agents{}",
454        C_DIM, C_RST
455    );
456    println!("  ┌────────────────────────────────────────────────────┐");
457    println!(
458        "  │  {}✓{} Durable execution — crash recovery              │",
459        C_G, C_RST
460    );
461    println!(
462        "  │  {}✓{} Tool Ledger — idempotent replay                │",
463        C_G, C_RST
464    );
465    println!(
466        "  │  {}✓{} Approval gates — human-in-the-loop              │",
467        C_G, C_RST
468    );
469    println!(
470        "  │  {}✓{} Policy engine — risk-based access               │",
471        C_G, C_RST
472    );
473    println!(
474        "  │  {}✓{} Budget control — tool call limits               │",
475        C_G, C_RST
476    );
477    println!(
478        "  │  {}✓{} Evidence export — full audit trail              │",
479        C_G, C_RST
480    );
481    println!("  └────────────────────────────────────────────────────┘");
482
483    wait("Press Enter to start / 按 Enter 开始");
484
485    // ════════════════════════════════════════════════════════
486    // Step 1: Setup
487    // ════════════════════════════════════════════════════════
488    println!(
489        "\n{}{}{}",
490        C_BOLD,
491        C_B,
492        "═".repeat(60)
493    );
494    println!(
495        "{}{}  Step 1: Initialize — Register tools, configure policy{}",
496        C_BOLD, C_B, C_RST
497    );
498    println!(
499        "{}{}{}",
500        C_BOLD,
501        C_B,
502        "═".repeat(60)
503    );
504
505    let mut runtime = Runtime::new();
506    runtime.set_budget(BudgetLimits {
507        max_tool_calls: Some(25.0),
508        max_model_tokens: None,
509        max_total_usd: None,
510    });
511
512    // Register tools
513    runtime.register_tool(
514        ToolSpec::new("travel.search_flights", Box::new(search_flights))
515            .side_effect("none")
516            .risk_level("low")
517            .input_schema(Value::Object(make_state(&[
518                ("type", "object".into()),
519                (
520                    "required",
521                    Value::Array(vec!["from".into(), "to".into()]),
522                ),
523            ]))),
524    );
525    runtime.register_tool(
526        ToolSpec::new("travel.search_hotels", Box::new(search_hotels))
527            .side_effect("none")
528            .risk_level("low")
529            .input_schema(Value::Object(make_state(&[
530                ("type", "object".into()),
531                ("required", Value::Array(vec!["city".into()])),
532            ]))),
533    );
534    runtime.register_tool(
535        ToolSpec::new("travel.check_weather", Box::new(check_weather))
536            .side_effect("none")
537            .risk_level("low")
538            .input_schema(Value::Object(make_state(&[
539                ("type", "object".into()),
540                ("required", Value::Array(vec!["city".into()])),
541            ]))),
542    );
543    runtime.register_tool(
544        ToolSpec::new("travel.book_flight", Box::new(book_flight))
545            .side_effect("external_write")
546            .risk_level("high")
547            .idempotency_required(true)
548            .approval_required(true)
549            .input_schema(Value::Object(make_state(&[
550                ("type", "object".into()),
551                (
552                    "required",
553                    Value::Array(vec!["flight_id".into(), "passenger".into()]),
554                ),
555            ]))),
556    );
557    runtime.register_tool(
558        ToolSpec::new("travel.book_hotel", Box::new(book_hotel))
559            .side_effect("external_write")
560            .risk_level("high")
561            .idempotency_required(true)
562            .approval_required(true)
563            .input_schema(Value::Object(make_state(&[
564                ("type", "object".into()),
565                (
566                    "required",
567                    Value::Array(vec!["hotel_id".into(), "guest".into()]),
568                ),
569            ]))),
570    );
571
572    println!("\n  {}Registered 5 tools:{}", C_C, C_RST);
573    for (name, risk, approval) in [
574        ("travel.search_flights", "low", false),
575        ("travel.search_hotels", "low", false),
576        ("travel.check_weather", "low", false),
577        ("travel.book_flight", "high", true),
578        ("travel.book_hotel", "high", true),
579    ] {
580        let rc = if risk == "low" { C_G } else { C_R };
581        let ac = if approval {
582            format!("{}needs approval{}", C_R, C_RST)
583        } else {
584            format!("{}no approval{}", C_G, C_RST)
585        };
586        println!(
587            "    {}•{} {}  [{}{}{}] [{}]",
588            C_DIM, C_RST, name, rc, risk, C_RST, ac
589        );
590    }
591    println!(
592        "  {}Policy: Rust uses risk-based policy (low=allow, high=deny+approval){}",
593        C_DIM, C_RST
594    );
595    println!("  {}Budget: max 25 tool calls{}", C_DIM, C_RST);
596
597    let (run_id, _step_id) = runtime.create_run(make_state(&[
598        ("trip", "Tokyo".into()),
599        ("budget_usd", Value::Number(3000.0)),
600    ]));
601    println!(
602        "\n  {}Run created: {}{}{}",
603        C_B, C_BOLD, run_id, C_RST
604    );
605    show_db(&runtime.store, &run_id);
606    wait("Press Enter to continue");
607
608    // ════════════════════════════════════════════════════════
609    // Step 2: Attempt 1 — Approval interception
610    // ════════════════════════════════════════════════════════
611    println!(
612        "\n{}{}{}",
613        C_BOLD,
614        C_R,
615        "═".repeat(60)
616    );
617    println!(
618        "{}{}  Step 2: Attempt 1 — Agent runs → Approval triggered{}",
619        C_BOLD, C_R, C_RST
620    );
621    println!(
622        "{}{}{}",
623        C_BOLD,
624        C_R,
625        "═".repeat(60)
626    );
627    println!(
628        "\n  {}Agent executing: search flights → search hotels → check weather → book flight...{}",
629        C_DIM, C_RST
630    );
631
632    {
633        let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
634        let result = travel_planner(&mut runtime, &mut ctx, 1);
635        match result {
636            Err(err) if err.0.starts_with("approval required:") => {
637                let approval_id = err.0.trim_start_matches("approval required:");
638                runtime
639                    .store
640                    .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
641            }
642            Err(err) => return Err(format!("unexpected error at step 2: {}", err.0).into()),
643            Ok(()) => {}
644        }
645    }
646
647    println!(
648        "\n  {}book_flight triggered approval! Runtime paused, waiting for human.{}",
649        C_R, C_RST
650    );
651    show_db(&runtime.store, &run_id);
652    println!(
653        "  {}Note: Tool Ledger has RESERVED entry, approval status is PENDING{}",
654        C_R, C_RST
655    );
656    wait("Press Enter to approve / 按 Enter 审批");
657
658    for req in runtime.store.approval_requests(&run_id) {
659        if req.status == "PENDING" {
660            runtime
661                .store
662                .approve_request(&req.approval_id, "traveler", "Within budget, approved")?;
663            println!(
664                "\n  {}✅ Approved: {} — by traveler{}",
665                C_G, req.tool_name, C_RST
666            );
667        }
668    }
669    show_db(&runtime.store, &run_id);
670    wait("Press Enter to continue");
671
672    // ════════════════════════════════════════════════════════
673    // Step 3: Attempt 2 — Execute + Crash
674    // ════════════════════════════════════════════════════════
675    println!(
676        "\n{}{}{}",
677        C_BOLD,
678        C_Y,
679        "═".repeat(60)
680    );
681    println!(
682        "{}{}  Step 3: Attempt 2 — Approved → Execute booking → Simulated crash{}",
683        C_BOLD, C_Y, C_RST
684    );
685    println!(
686        "{}{}{}",
687        C_BOLD,
688        C_Y,
689        "═".repeat(60)
690    );
691    println!(
692        "\n  {}Re-running agent (approval passed, book_flight will execute)...{}",
693        C_DIM, C_RST
694    );
695
696    {
697        let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
698        let result = travel_planner(&mut runtime, &mut ctx, 2);
699        match result {
700            Err(err) if err.0 == "retryable" => {
701                runtime.store.mark_retry(
702                    &run_id,
703                    &ctx.step_id,
704                    "RetryableAgentError",
705                    "after flight booking",
706                );
707            }
708            Err(err) if err.0.starts_with("approval required:") => {
709                let approval_id = err.0.trim_start_matches("approval required:");
710                runtime
711                    .store
712                    .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
713            }
714            Err(err) => return Err(format!("unexpected error at step 3: {}", err.0).into()),
715            Ok(()) => {}
716        }
717    }
718
719    println!(
720        "\n  {}Agent booked flight, then crashed before committing state!{}",
721        C_Y, C_RST
722    );
723    println!(
724        "  {}Flight is booked in external system, but agent state was NOT persisted.{}",
725        C_Y, C_RST
726    );
727    show_db(&runtime.store, &run_id);
728    println!(
729        "  {}Key: Tool Ledger book_flight status = {}SUCCEEDED{} (external side effect executed){}",
730        C_Y, C_G, C_Y, C_RST
731    );
732    println!(
733        "  {}      Step status = retry_scheduled (state not committed, waiting for retry){}",
734        C_Y, C_RST
735    );
736    wait("Press Enter to continue");
737
738    // ════════════════════════════════════════════════════════
739    // Step 4: Attempt 3 — Recovery + Hotel approval
740    // ════════════════════════════════════════════════════════
741    println!(
742        "\n{}{}{}",
743        C_BOLD,
744        C_G,
745        "═".repeat(60)
746    );
747    println!(
748        "{}{}  Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay{}",
749        C_BOLD, C_G, C_RST
750    );
751    println!(
752        "{}{}{}",
753        C_BOLD,
754        C_G,
755        "═".repeat(60)
756    );
757    println!(
758        "\n  {}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...{}",
759        C_DIM, C_RST
760    );
761    println!(
762        "  {}{}→ Returns cached result, no duplicate API call, no double charge!{}",
763        C_DIM, C_G, C_RST
764    );
765
766    {
767        let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
768        let result = travel_planner(&mut runtime, &mut ctx, 3);
769        match result {
770            Err(err) if err.0.starts_with("approval required:") => {
771                let approval_id = err.0.trim_start_matches("approval required:");
772                runtime
773                    .store
774                    .mark_waiting_human(&run_id, &ctx.step_id, &err.0, approval_id);
775            }
776            Err(err) => return Err(format!("unexpected error at step 4: {}", err.0).into()),
777            Ok(()) => {}
778        }
779    }
780
781    println!(
782        "\n  {}✅ Flight idempotent replay successful! (no duplicate _book_flight call){}",
783        C_G, C_RST
784    );
785    println!("  {}Hotel booking → triggers approval again{}", C_R, C_RST);
786
787    for req in runtime.store.approval_requests(&run_id) {
788        if req.status == "PENDING" {
789            runtime
790                .store
791                .approve_request(&req.approval_id, "traveler", "Hotel within budget, approved")?;
792            println!(
793                "\n  {}✅ Approved: {} — by traveler{}",
794                C_G, req.tool_name, C_RST
795            );
796        }
797    }
798    show_db(&runtime.store, &run_id);
799    wait("Press Enter to continue");
800
801    // ════════════════════════════════════════════════════════
802    // Step 5: Attempt 4 — Complete
803    // ════════════════════════════════════════════════════════
804    println!(
805        "\n{}{}{}",
806        C_BOLD,
807        C_G,
808        "═".repeat(60)
809    );
810    println!(
811        "{}{}  Step 5: Attempt 4 — Hotel approved → Full execution → State committed{}",
812        C_BOLD, C_G, C_RST
813    );
814    println!(
815        "{}{}{}",
816        C_BOLD,
817        C_G,
818        "═".repeat(60)
819    );
820
821    {
822        let mut ctx = claim_context(&mut runtime, &run_id, "worker-rust", "TravelPlanner");
823        travel_planner(&mut runtime, &mut ctx, 4)?;
824        runtime.store.commit_state_patch(
825            &run_id,
826            &ctx.step_id,
827            &ctx.lease_token,
828            ctx.state_version,
829            ctx.pending_patch,
830        )?;
831    }
832
833    {
834        let db = BOOKING_DB.lock().unwrap();
835        if db.len() != 2 {
836            return Err(format!("Expected 2 bookings, got {}", db.len()).into());
837        }
838    }
839
840    println!(
841        "\n  {}✅ Travel planning complete! State persisted to database.{}",
842        C_G, C_RST
843    );
844    show_db(&runtime.store, &run_id);
845    println!(
846        "  {}Step status = completed, State has bookings + trip_status{}",
847        C_G, C_RST
848    );
849    let db = BOOKING_DB.lock().unwrap();
850    let keys: Vec<String> = db.keys().cloned().collect();
851    println!(
852        "  {}External bookings: {:?} ({} total, no duplicates){}",
853        C_G,
854        keys,
855        keys.len(),
856        C_RST
857    );
858    drop(db);
859    wait("Press Enter to continue");
860
861    // ════════════════════════════════════════════════════════
862    // Step 6: Evidence + Cost + Replay
863    // ════════════════════════════════════════════════════════
864    println!(
865        "\n{}{}{}",
866        C_BOLD,
867        C_M,
868        "═".repeat(60)
869    );
870    println!(
871        "{}{}  Step 6: Evidence export + Cost attribution + Replay verification{}",
872        C_BOLD, C_M, C_RST
873    );
874    println!(
875        "{}{}{}",
876        C_BOLD,
877        C_M,
878        "═".repeat(60)
879    );
880
881    let bundle = export_evidence(&runtime.store, &run_id)?;
882    let replay_result = replay(&runtime.store, &run_id)?;
883    let cost = agentledger::cost_attribution(&runtime.store, &run_id);
884
885    println!(
886        "\n  {}Cost attribution: {} tool calls{}",
887        C_M, cost.total.tool_calls, C_RST
888    );
889    println!(
890        "  {}Replay: {} events, safe={}{}{}",
891        C_M, replay_result.event_count, C_G, replay_result.replay_safe, C_RST
892    );
893    println!(
894        "  {}Evidence bundle: {} events total{}",
895        C_M,
896        bundle.events.len(),
897        C_RST
898    );
899
900    // Write evidence HTML
901    let html_path = root.join("evidence.html");
902    let html = debug_html(&bundle);
903    fs::write(&html_path, html)?;
904    let html_abs = html_path.canonicalize().unwrap_or_else(|_| html_path.clone());
905
906    // Final summary
907    println!(
908        "\n{}{}{}",
909        C_BOLD,
910        C_G,
911        "═".repeat(60)
912    );
913    println!(
914        "{}{}  Summary: What AgentLedger (Rust) did in this demo{}",
915        C_BOLD, C_G, C_RST
916    );
917    println!(
918        "{}{}{}",
919        C_BOLD,
920        C_G,
921        "═".repeat(60)
922    );
923    println!(
924        "
925  ┌──────────────────────────────────────────────────────────┐
926  │                                                          │
927  │  {g}✓ Durable execution{rst}  Crash → auto retry, state preserved         │
928  │                   Step: retry_scheduled → completed           │
929  │                                                          │
930  │  {g}✓ Tool Ledger{rst}      Idempotent replay, flight booked {bold}1x{rst} only       │
931  │                   SUCCEEDED → cached result on retry        │
932  │                                                          │
933  │  {g}✓ Approval gates{rst}    Flight + hotel each trigger approval          │
934  │                   approval_requests records in store        │
935  │                                                          │
936  │  {g}✓ Policy engine{rst}     Risk-based policy (high → deny+approval)     │
937  │                   low-risk tools auto-allowed              │
938  │                                                          │
939  │  {g}✓ Budget control{rst}    Tracked {tc} tool calls                       │
940  │                   BudgetController.before_tool_call()       │
941  │                                                          │
942  │  {g}✓ Evidence export{rst}   {ec} events recorded                         │
943  │                   events stored in memory store             │
944  │                                                          │
945  │  {g}✓ Cost attribution{rst}  Auto-recorded per run                        │
946  │                   CostAttribution by agent                │
947  │                                                          │
948  │  {g}✓ Replay engine{rst}     Event hash verification passed                │
949  │                   Verify history without re-running       │
950  │                                                          │
951  └──────────────────────────────────────────────────────────┘
952",
953        g = C_G,
954        rst = C_RST,
955        bold = C_BOLD,
956        tc = cost.total.tool_calls,
957        ec = bundle.events.len()
958    );
959
960    println!("  {}Storage: in-memory (MemoryStore){}", C_DIM, C_RST);
961    println!("  {}Evidence HTML: {:?}{}", C_DIM, html_abs, C_RST);
962    println!();
963
964    Ok(())
965}