pub fn cost_attribution(
store: &MemoryStore,
run_id: &str,
) -> CostAttributionReportExamples found in repository?
examples/travel_assistant.rs (line 883)
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}