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
11const 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
24static 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
53fn 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
99fn 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
222fn travel_planner(
227 runtime: &mut Runtime,
228 ctx: &mut AgentContext,
229 attempt: u64,
230) -> agentledger::Result<()> {
231 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 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 if attempt == 2 {
261 return Err(RuntimeError("retryable".to_string()));
262 }
263
264 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
283fn 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
320fn 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 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 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 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 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
421fn 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 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 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 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 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 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 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 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 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 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 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}