pub struct ToolSpec {
pub name: String,
pub version: String,
pub side_effect: String,
pub risk_level: String,
pub idempotency_required: bool,
pub approval_required: bool,
pub sandbox_required: bool,
pub sandbox_executor: String,
pub sandbox_policy: State,
pub input_schema: Option<Value>,
pub output_schema: Option<Value>,
pub func: ToolFunc,
}Fields§
§name: String§version: String§side_effect: String§risk_level: String§idempotency_required: bool§approval_required: bool§sandbox_required: bool§sandbox_executor: String§sandbox_policy: State§input_schema: Option<Value>§output_schema: Option<Value>§func: ToolFuncImplementations§
Source§impl ToolSpec
impl ToolSpec
Sourcepub fn new(name: &str, func: ToolFunc) -> Self
pub fn new(name: &str, func: ToolFunc) -> Self
Examples found in repository?
examples/three_minute_demo.rs (lines 23-31)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}More examples
examples/travel_assistant.rs (line 514)
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}Sourcepub fn side_effect(self, side_effect: &str) -> Self
pub fn side_effect(self, side_effect: &str) -> Self
Examples found in repository?
examples/three_minute_demo.rs (line 32)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}More examples
examples/travel_assistant.rs (line 515)
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}Sourcepub fn risk_level(self, risk_level: &str) -> Self
pub fn risk_level(self, risk_level: &str) -> Self
Examples found in repository?
examples/travel_assistant.rs (line 516)
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}Sourcepub fn idempotency_required(self, required: bool) -> Self
pub fn idempotency_required(self, required: bool) -> Self
Examples found in repository?
examples/three_minute_demo.rs (line 33)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}More examples
examples/travel_assistant.rs (line 547)
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}Sourcepub fn approval_required(self, required: bool) -> Self
pub fn approval_required(self, required: bool) -> Self
Examples found in repository?
examples/travel_assistant.rs (line 548)
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}pub fn sandbox_required(self, required: bool) -> Self
pub fn sandbox_executor(self, executor: &str) -> Self
pub fn sandbox_policy(self, policy: State) -> Self
Sourcepub fn input_schema(self, schema: Value) -> Self
pub fn input_schema(self, schema: Value) -> Self
Examples found in repository?
examples/three_minute_demo.rs (lines 34-40)
20fn main() -> agentledger::Result<()> {
21 let mut runtime = Runtime::new();
22 runtime.register_tool(
23 ToolSpec::new(
24 "ticket.create",
25 Box::new(|args| {
26 Ok(Value::Object(state(&[
27 ("external_id", "TICKET-1".into()),
28 ("title", args["title"].clone()),
29 ])))
30 }),
31 )
32 .side_effect("external_write")
33 .idempotency_required(true)
34 .input_schema(Value::Object(state(&[
35 ("type", "object".into()),
36 (
37 "required",
38 Value::Array(vec![Value::String("title".to_string())]),
39 ),
40 ]))),
41 );
42 let (run_id, _) = runtime.create_run(State::new());
43
44 let first_claim = runtime.store.claim_step("worker-before-crash", &run_id, 60.0)?;
45 let first_ctx = AgentContext {
46 run_id: run_id.clone(),
47 session_id: first_claim.session_id.clone(),
48 step_id: first_claim.step_id.clone(),
49 agent_role: "SupportAgent".to_string(),
50 lease_token: first_claim.lease_token.clone(),
51 attempt: first_claim.attempt,
52 state_version: first_claim.state_version,
53 pending_patch: State::new(),
54 };
55 let ticket = runtime.call_tool(
56 &first_ctx,
57 "ticket.create",
58 state(&[
59 ("title", "Investigate failed payment".into()),
60 ("_logical_operation", "open-payment-ticket".into()),
61 ]),
62 )?;
63 runtime.store.mark_retry(
64 &run_id,
65 &first_ctx.step_id,
66 "RetryableAgentError",
67 "after external ticket create, before state commit",
68 );
69
70 let second_claim = runtime.store.claim_step("worker-after-restart", &run_id, 60.0)?;
71 let mut second_ctx = AgentContext {
72 run_id: run_id.clone(),
73 session_id: second_claim.session_id.clone(),
74 step_id: second_claim.step_id.clone(),
75 agent_role: "SupportAgent".to_string(),
76 lease_token: second_claim.lease_token.clone(),
77 attempt: second_claim.attempt,
78 state_version: second_claim.state_version,
79 pending_patch: State::new(),
80 };
81 let replayed_ticket = runtime.call_tool(
82 &second_ctx,
83 "ticket.create",
84 state(&[
85 ("title", "Investigate failed payment".into()),
86 ("_logical_operation", "open-payment-ticket".into()),
87 ]),
88 )?;
89 second_ctx.write_state("ticket", replayed_ticket);
90 second_ctx.write_state("recovered", true.into());
91 runtime.store.commit_state_patch(
92 &run_id,
93 &second_ctx.step_id,
94 &second_ctx.lease_token,
95 second_ctx.state_version,
96 second_ctx.pending_patch,
97 )?;
98
99 let summary = replay(&runtime.store, &run_id)?;
100 let ledger = runtime.store.ledger(&run_id);
101 let final_state = runtime.store.final_state(&run_id)?;
102 println!(
103 "{{\n \"run_id\": \"{}\",\n \"first_attempt_ok\": false,\n \"second_attempt_ok\": true,\n \"external_ticket_count\": 1,\n \"actual_tool_executions\": 1,\n \"tool_ledger_count\": {},\n \"tool_ledger_status\": \"{}\",\n \"first_ticket_external_id\": \"{}\",\n \"final_state_recovered\": {},\n \"replay\": {{\n \"safe\": {},\n \"event_count\": {},\n \"tool_call_count\": {}\n }}\n}}",
104 run_id,
105 ledger.len(),
106 ledger.first().map(|row| row.status.as_str()).unwrap_or(""),
107 get_string(&ticket, "external_id"),
108 final_state.get("recovered") == Some(&Value::Bool(true)),
109 summary.replay_safe,
110 summary.event_count,
111 summary.tool_call_count
112 );
113 Ok(())
114}More examples
examples/travel_assistant.rs (lines 517-523)
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}pub fn output_schema(self, schema: Value) -> Self
Auto Trait Implementations§
impl !RefUnwindSafe for ToolSpec
impl !UnwindSafe for ToolSpec
impl Freeze for ToolSpec
impl Send for ToolSpec
impl Sync for ToolSpec
impl Unpin for ToolSpec
impl UnsafeUnpin for ToolSpec
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more